|
| 1 | +<!-- START doctoc generated TOC please keep comment here to allow auto update --> |
| 2 | +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> |
| 3 | +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* |
| 4 | + |
| 5 | +- [栈](#%E6%A0%88) |
| 6 | + - [原理](#%E5%8E%9F%E7%90%86) |
| 7 | + - [实现](#%E5%AE%9E%E7%8E%B0) |
| 8 | + - [应用](#%E5%BA%94%E7%94%A8) |
| 9 | +- [队列](#%E9%98%9F%E5%88%97) |
| 10 | + - [原理](#%E5%8E%9F%E7%90%86-1) |
| 11 | + - [实现](#%E5%AE%9E%E7%8E%B0-1) |
| 12 | + - [单链队列](#%E5%8D%95%E9%93%BE%E9%98%9F%E5%88%97) |
| 13 | + - [循环队列](#%E5%BE%AA%E7%8E%AF%E9%98%9F%E5%88%97) |
| 14 | +- [链表](#%E9%93%BE%E8%A1%A8) |
| 15 | + - [原理](#%E5%8E%9F%E7%90%86-2) |
| 16 | + - [实现](#%E5%AE%9E%E7%8E%B0-2) |
| 17 | +- [树](#%E6%A0%91) |
| 18 | + - [二叉树](#%E4%BA%8C%E5%8F%89%E6%A0%91) |
| 19 | + - [二分搜索树](#%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2%E6%A0%91) |
| 20 | + - [实现](#%E5%AE%9E%E7%8E%B0-3) |
| 21 | + |
| 22 | +<!-- END doctoc generated TOC please keep comment here to allow auto update --> |
| 23 | + |
1 | 24 | # 栈
|
2 | 25 |
|
3 | 26 | ## 原理
|
@@ -313,9 +336,11 @@ class BST {
|
313 | 336 |
|
314 | 337 | 对于树的遍历来说,有三种遍历方法,分别是先序遍历、中序遍历、后序遍历。三种遍历的区别在于何时访问节点。在遍历树的过程中,每个节点都会遍历三次,分别是遍历到自己,遍历左子树和遍历右子树。如果需要实现先序遍历,那么只需要第一次遍历到节点时进行操作即可。
|
315 | 338 |
|
| 339 | +以下都是递归实现,如果你想学习非递归实现,可以 [点击这里阅读](../Algorithm/algorithm-ch.md#%E9%9D%9E%E9%80%92%E5%BD%92%E5%AE%9E%E7%8E%B0) |
| 340 | + |
316 | 341 | ```js
|
317 | 342 | // 先序遍历可用于打印树的结构
|
318 |
| -// 先序遍历表示先访问根节点,然后访问左节点,最后访问右节点。 |
| 343 | +// 先序遍历先访问根节点,然后访问左节点,最后访问右节点。 |
319 | 344 | preTraversal() {
|
320 | 345 | this._pre(this.root)
|
321 | 346 | }
|
@@ -361,8 +386,13 @@ _back(node) {
|
361 | 386 | breadthTraversal() {
|
362 | 387 | if (!this.root) return null
|
363 | 388 | let q = new Queue()
|
| 389 | + // 将根节点入队 |
364 | 390 | q.enQueue(this.root)
|
| 391 | + // 循环判断队列是否为空,为空 |
| 392 | + // 代表树遍历完毕 |
365 | 393 | while (!q.isEmpty()) {
|
| 394 | + // 将队首出队,判断是否有左右子树 |
| 395 | + // 有的话,就先左后右入队 |
366 | 396 | let n = q.deQueue()
|
367 | 397 | console.log(n.value)
|
368 | 398 | if (n.left) q.enQueue(n.left)
|
@@ -390,3 +420,140 @@ _getMax(node) {
|
390 | 420 | }
|
391 | 421 | ```
|
392 | 422 |
|
| 423 | +**向上取整和向下取整**,这两个操作是相反的,所以代码也是类似的,这里只介绍如何向下取整。既然是向下取整,那么根据二分搜索树的特性,值一定在根节点的左侧。只需要一直遍历左子树直到当前节点的值不再大于等于需要的值,然后判断节点是否还拥有右子树。如果有的话,继续上面的递归判断。 |
| 424 | + |
| 425 | +```js |
| 426 | +floor(v) { |
| 427 | + let node = this._floor(this.root, v) |
| 428 | + return node ? node.value : null |
| 429 | +} |
| 430 | +_floor(node, v) { |
| 431 | + if (!node) return null |
| 432 | + if (node.value === v) return v |
| 433 | + // 如果当前节点值还比需要的值大,就继续递归 |
| 434 | + if (node.value > v) { |
| 435 | + return this._floor(node.left, v) |
| 436 | + } |
| 437 | + // 判断当前节点是否拥有右子树 |
| 438 | + let right = this._floor(node.right, v) |
| 439 | + if (right) return right |
| 440 | + return node |
| 441 | +} |
| 442 | +``` |
| 443 | + |
| 444 | +**排名**,这是用于获取给定值的排名或者排名第几的节点的值,这两个操作也是相反的,所以这个只介绍如何获取排名第几的节点的值。对于这个操作而言,我们需要略微的改造点代码,让每个节点拥有一个 `size` 属性。该属性表示该节点下有多少子节点(包含自身)。 |
| 445 | + |
| 446 | +```js |
| 447 | +class Node { |
| 448 | + constructor(value) { |
| 449 | + this.value = value |
| 450 | + this.left = null |
| 451 | + this.right = null |
| 452 | + // 修改代码 |
| 453 | + this.size = 1 |
| 454 | + } |
| 455 | +} |
| 456 | +// 新增代码 |
| 457 | +_getSize(node) { |
| 458 | + return node ? node.size : 0 |
| 459 | +} |
| 460 | +_addChild(node, v) { |
| 461 | + if (!node) { |
| 462 | + return new Node(v) |
| 463 | + } |
| 464 | + if (node.value > v) { |
| 465 | + // 修改代码 |
| 466 | + node.size++ |
| 467 | + node.left = this._addChild(node.left, v) |
| 468 | + } else if (node.value < v) { |
| 469 | + // 修改代码 |
| 470 | + node.size++ |
| 471 | + node.right = this._addChild(node.right, v) |
| 472 | + } |
| 473 | + return node |
| 474 | +} |
| 475 | +select(k) { |
| 476 | + let node = this._select(this.root, k) |
| 477 | + return node ? node.value : null |
| 478 | +} |
| 479 | +_select(node, k) { |
| 480 | + if (!node) return null |
| 481 | + // 先获取左子树下有几个节点 |
| 482 | + let size = node.left ? node.left.size : 0 |
| 483 | + // 判断 size 是否大于 k |
| 484 | + // 如果大于 k,代表所需要的节点在左节点 |
| 485 | + if (size > k) return this._select(node.left, k) |
| 486 | + // 如果小于 k,代表所需要的节点在右节点 |
| 487 | + // 注意这里需要重新计算 k,减去根节点除了右子树的节点数量 |
| 488 | + if (size < k) return this._select(node.right, k - size - 1) |
| 489 | + return node |
| 490 | +} |
| 491 | +``` |
| 492 | + |
| 493 | +接下来讲解的是二分搜索树中最难实现的部分:删除节点。因为对于删除节点来说,会存在以下几种情况 |
| 494 | + |
| 495 | +- 需要删除的节点没有子树 |
| 496 | +- 需要删除的节点只有一条子树 |
| 497 | +- 需要删除的节点有左右两条树 |
| 498 | + |
| 499 | +对于前两种情况很好解决,但是第三种情况就有难度了,所以先来实现相对简单的操作:删除最小节点,对于删除最小节点来说,是不存在第三种情况的,删除最大节点操作是和删除最小节点相反的,所以这里也就不再赘述。 |
| 500 | + |
| 501 | +```js |
| 502 | +delectMin() { |
| 503 | + this.root = this._delectMin(this.root) |
| 504 | + console.log(this.root) |
| 505 | +} |
| 506 | +_delectMin(node) { |
| 507 | + // 一直递归左子树 |
| 508 | + // 如果左子树为空,就判断节点是否拥有右子树 |
| 509 | + // 有右子树的话就把需要删除的节点替换为右子树 |
| 510 | + if ((node != null) & !node.left) return node.right |
| 511 | + node.left = this._delectMin(node.left) |
| 512 | + // 最后需要重新维护下节点的 `size` |
| 513 | + node.size = this._getSize(node.left) + this._getSize(node.right) + 1 |
| 514 | + return node |
| 515 | +} |
| 516 | +``` |
| 517 | + |
| 518 | +最后讲解的就是如何删除任意节点了。对于这个操作,T.Hibbard 在 1962 年提出了解决这个难题的办法,也就是如何解决第三种情况。 |
| 519 | + |
| 520 | +当遇到这种情况时,需要取出当前节点的后继节点(也就是当前节点右子树的最小节点)来替换需要删除的节点。然后将需要删除节点的左子树赋值给后继结点,右子树删除后继结点后赋值给他。 |
| 521 | + |
| 522 | +你如果对于这个解决办法有疑问的话,可以这样考虑。因为二分搜索树的特性,父节点一定比所有左子节点大,比所有右子节点小。那么当需要删除父节点时,势必需要拿出一个比父节点大的节点来替换父节点。这个节点肯定不存在于左子树,必然存在于右子树。然后又需要保持父节点都是比右子节点小的,那么就可以取出右子树中最小的那个节点来替换父节点。 |
| 523 | + |
| 524 | +```js |
| 525 | +delect(v) { |
| 526 | + this.root = this._delect(this.root, v) |
| 527 | +} |
| 528 | +_delect(node, v) { |
| 529 | + if (!node) return null |
| 530 | + // 寻找的节点比当前节点小,去左子树找 |
| 531 | + if (node.value < v) { |
| 532 | + node.right = this._delect(node.right, v) |
| 533 | + } else if (node.value > v) { |
| 534 | + // 寻找的节点比当前节点大,去右子树找 |
| 535 | + node.left = this._delect(node.left, v) |
| 536 | + } else { |
| 537 | + // 进入这个条件说明已经找到节点 |
| 538 | + // 先判断节点是否拥有拥有左右子树中的一个 |
| 539 | + // 是的话,将子树返回出去,这里和 `_delectMin` 的操作一样 |
| 540 | + if (!node.left) return node.right |
| 541 | + if (!node.right) return node.left |
| 542 | + // 进入这里,代表节点拥有左右子树 |
| 543 | + // 先取出当前节点的后继结点,也就是取当前节点右子树的最小值 |
| 544 | + let min = this._getMin(node.right) |
| 545 | + // 取出最小值后,删除最小值 |
| 546 | + // 然后把删除节点后的子树赋值给最小值节点 |
| 547 | + min.right = this._delectMin(node.right) |
| 548 | + // 左子树不动 |
| 549 | + min.left = node.left |
| 550 | + node = min |
| 551 | + } |
| 552 | + // 维护 size |
| 553 | + node.size = this._getSize(node.left) + this._getSize(node.right) + 1 |
| 554 | + return node |
| 555 | +} |
| 556 | +``` |
| 557 | + |
| 558 | + |
| 559 | + |
0 commit comments