生活中的树结构

截屏2020-04-03 上午10.54.19.png

生物树形态

截屏2020-04-03 上午10.55.19.png

社会文化中的树结构

截屏2020-04-03 上午10.56.12.png

抽象出来的树结构

截屏2020-04-03 上午10.57.03.png

分析:

  • 倒立的树。
  • 根节点。
  • 子节点又可以分成两(逻辑上的两个,是非题的价值参考了)个子节点。

数组链表的优势劣势

截屏2020-04-03 上午11.10.28.png
数组的优点:用下标读取和赋值很快。
数组缺点:查找,很慢,盲目遍历。除非先排序,再二分查找!删除,插入操作效率低。还有扩容。
链表的优点:随意插入删除,不需要位移所以效率高。查找也是盲目遍历。不考虑扩容问题。
链表的缺点:查找很慢,也没法排序,二分法。

哈希表与树结构的对比

截屏2020-04-03 上午11.15.47.png

哈希表的优点,查找,插入,删除都很快!
哈希表的缺点,消耗内存。无序的排列。无法快速找出最大值,最小值。

树结构,仿生的结构,层级关系,一对多的关系,等等。

树结构理论

概念 术语

tree , root , subTree
**
截屏2020-04-03 上午11.22.30.png

degree

截屏2020-04-03 上午11.46.36.png截屏2020-04-03 上午11.46.36.png截屏2020-04-03 上午11.24.16.png

普通表示方式

截屏2020-04-03 上午11.37.23.png

这里很不明白了,感觉很容易构建的,,,

  1. {
  2. parent:XXX,
  3. children:[], //这里老师说很难表示,因为一个,两个,三个,,,left,middle,right。没法表示
  4. level:XXX,
  5. }

我想了一下,自认为明白了,如果用数组去存储children,那还会高效?其实就是普通的数组加了个外包装而已了!树结构,全都是下标类似的查找,而下标不是数字,而是属性名。

儿子兄弟表示法

截屏2020-04-03 上午11.46.36.png

这种数据结构可以链式调用了:

  1. Node {
  2. data : XX,
  3. parent:XXX,
  4. level:XXX,
  5. sibling: otherNode
  6. }

推出二叉树

截屏2020-04-03 上午11.58.20.png

每个节点最多有两个子节点!
我对于这种变换的哲学逻辑其实挺感兴趣的,这是一种逻辑上的集权了。
简单来说就是长兄如父!如果回归到之前的管理层行政职能的树形结果来说,就是总经理跟下一层级的一个职务结合了。比如假如省长又兼职了一个市的市长如此的感官逻辑了。

二叉树概念

截屏2020-04-03 下午2.58.56.png

任何树都可以转化为二叉树的形式。所以二叉树很重要!

二叉树特性

截屏2020-04-03 下午3.17.27.png

完美二叉树

满的:
截屏2020-04-03 下午3.26.58.png

完全二叉树

最后一层:从左到右开始缺失。
截屏2020-04-03 下午3.42.25.png

二叉树结构

数组表示不行

截屏2020-04-03 下午4.16.50.png

  1. this.leftIndex= this.index * 2
  2. this.rightIndex = this.index*2+1

很简单,但是非完美二叉树就没办法了。

链表表示

截屏2020-04-03 下午4.29.17.png

最终版的探索!就是如此。很像是递归的感觉。

二叉搜索树

截屏2020-04-03 下午5.00.55.png

我只说一个规律:正三角形的三个点,左小,顶点大,右最大!如有缺失顶点,可虚拟补上一个。
最小值:最左下角的。最大值:最右下角的。
**

搜索性能强

截屏2020-04-03 下午5.02.29.png

这让我想起了二分法了,一样的味道!
老师说了,二叉搜索树的低层就是二分法实现的。

二叉搜索树封装

一定要有根节点!

截屏2020-04-03 下午5.15.44.png

  1. class Node {
  2. constructor(key, data) {
  3. if (typeof key !== "number") {
  4. throw new Error("Node 构造函数必须要有一个Number类型的key参数!");
  5. }
  6. this.key = key;
  7. data && (this.data = data);
  8. this.left = null;
  9. this.right = null;
  10. }
  11. }
  12. class BinarySearchTree {
  13. constructor(root) {
  14. this.root = root || null
  15. }
  16. }

截屏2020-04-03 下午5.24.29.png

插入

截屏2020-04-03 下午5.55.43.png

说实在的,我原本认为的插入操作是,尽可能地按照从小到到的顺序进行插入的完美结果。什么意思?

711585914044_.pic.jpg

我想到的是左边的想法,所以我想复杂了,,,然后当时怎么实现都觉得不合理了。
然后直接就看老师的逻辑。才明白是右边的逻辑,这就简单了,要么while循环,要么递归了。

  1. class BinarySearchTree {
  2. constructor(root) {
  3. this.root = root || null;
  4. }
  5. _insertNode(current, node) {
  6. if (current.key > node.key) {
  7. const {left} = current;
  8. if (left === null) return current.left = node;
  9. this._insertNode(left, node);
  10. } else {
  11. const {right} = current;
  12. if (right === null) return current.right = node;
  13. this._insertNode(right, node);
  14. }
  15. }
  16. insert(key, data) {
  17. const node = new Node(key, data);
  18. if (this.root) {
  19. this._insertNode(this.root, node);
  20. } else {
  21. this.root = node;
  22. }
  23. }
  24. }
  25. const binarySearchTree = new BinarySearchTree()
  26. const arr = []
  27. for(let i =0;i<10;i++){
  28. arr.push(Math.floor(Math.random()*100))
  29. }
  30. arr.map(
  31. (num,ind)=>binarySearchTree.insert(num,{id:ind})
  32. )
  33. console.log(binarySearchTree);

看老师的例子,加深一下,插入的规则!说明我对二叉树的理解还是有些不正确的!

截屏2020-04-03 下午7.58.11.png

遍历

截屏2020-04-03 下午8.11.30.png

先序遍历

截屏2020-04-03 下午8.34.41.png

说真的,我真的自己想了好久,就是没想过递归的方向的。所以,除了用状态管理不断地暂存数据,然后再判断中循环,我是真的没想到其他的思路了。
拿来主义了,我好菜啊!新的套路了!递归!

给自己洗脑强化一番!递归,为什么更适合树?源于我上面的那个感觉,职能重合,长兄如父的感觉了。本就是一个个正三角形的子树组成的大树。嵌套的数据结构的时候,就用递归
**

  1. //先序遍历
  2. preOrderTraversalNode(callback) {
  3. //先遍历好左节点,再遍历右节点:
  4. this._preOrderTraversalNode(this.root,callback)
  5. }
  6. _preOrderTraversalNode(node,callback) {
  7. //先遍历好左节点,再遍历右节点:
  8. if (node !== null){
  9. callback(node.key,node.data)
  10. this._preOrderTraversalNode(node.left,callback)
  11. //其实我不能明白的是如何能先左同时又照顾层级,后右的?原来我又要复习一下栈结构了:
  12. //拿上面图的树做例子推导一下:
  13. 11->7->
  14. 5->(3->null->null)->3->
  15. 5->(6->null->null)->
  16. 5->7
  17. ->9->(8->null->null)->9->(10->null->null)
  18. ->9->7->11
  19. 11->15->13->(12->null->null)->13->(14->null->null)->13->15
  20. ->20->(18->null->null)->20->(25->null->null)
  21. this._preOrderTraversalNode(node.right,callback)
  22. }
  23. }

递归没怎么如此抽象地用过,,,以前就是对很实在的嵌套的数据对象进行过处理。要加深这个套路了!

中序遍历

截屏2020-04-03 下午11.51.14.png

同样的套路:递归,会深层嵌套,不断入栈新函数

  1. midOrderTraversalNode(callback) {
  2. this._midOrderTraversalNode(this.root, callback);
  3. }
  4. _midOrderTraversalNode(node, callback) {
  5. if (node) {
  6. this._midOrderTraversalNode(node.left, callback);
  7. callback(node.key, node.data);
  8. this._midOrderTraversalNode(node.right, callback);
  9. }
  10. }

后序遍历

截屏2020-04-04 上午12.51.24.png

同样的套路:递归!

  1. postOrderTraversalNode(callback) {
  2. this._postOrderTraversalNode(this.root,callback)
  3. }
  4. _postOrderTraversalNode(node, callback) {
  5. if(node){
  6. this._postOrderTraversalNode(node.left,callback)
  7. this._postOrderTraversalNode(node.right,callback)
  8. callback(node.key,node.data)
  9. }
  10. }

总结

看路径,怎么画三角形的!
正序:顶点开始逆时针。
中序:左下点开始顺时针。
后续:左下开始逆时针。
关于递归,有了更深层的认识了,确实是很巧妙的,层层嵌套的!
想过试试尾递归,不过这种二叉树的形式,差了个反向的指针了!除非搞成双链表那种。如果想用尾递归,各个节点还需要加上个parent属性,其实加上这个属性,让我想起了关系型数据表里的一个专门的业务表了。

最大值最小值

截屏2020-04-04 上午1.22.49.png

没啥可说的,最简单的视角是一个正三角是一个内部作用域,嵌套的三角形,越顶点的越极端。

  1. get max() {
  2. let current = this.root;
  3. if (current === null) return null;
  4. while (current.right) {
  5. current = current.right;
  6. }
  7. return current.key;
  8. }
  9. get min() {
  10. let current = this.root;
  11. if (current === null) return null;
  12. while (current.left) {
  13. current = current.left;
  14. }
  15. return current.key;
  16. }

搜索特定的值

截屏2020-04-04 上午1.25.45.png

  1. search(key) {
  2. return this._search(this.root, key);
  3. }
  4. _search(node, key) {
  5. if (node !== null) {
  6. if (node.key === key) return node;
  7. return node.key > key ?
  8. this._search(node.left, key) :
  9. this._search(node.right, key);
  10. }
  11. return null;
  12. }

不用递归的话:

  1. search(key){
  2. let current = this.root;
  3. if (current === null) return null;
  4. let res = null;
  5. while (current) {
  6. if (current.key === key) return res = current;
  7. current = current[current.key > key ? "left" : "right"];
  8. }
  9. return res;
  10. }

删除:复杂!!

分析

删除节点,我原本觉得很复杂,其实我也是没有思路了。就算当初自己想到了删除叶子节点之类的简单,到删除子节点的时候,依旧是一头雾水的。子节点的左右子节点们如何安排??

其实当时是卡在这里了。如今再想一想,其实差的是一个标准,一个需求,一个安排而已。我甚至可以规定,左为尊,有左的优先左的节点重新添加到二叉树,然后右的再添加。也可以右为尊,甚至可以一层一层地左为尊或者右为尊的。如此就简单多了!
当然,以上只是我的预想意淫,后期打脸了,我也不会删了的。我不认为自己的想法不是一种需求啊!

截屏2020-04-04 下午12.45.34.png

没有子节点

截屏2020-04-04 下午12.42.31.png

无论是什么情况,只要是根节点,那就不一样的!
这里也要分是不是根节点!

一个子节点

截屏2020-04-04 下午4.29.10.png

无论是什么情况,只要是根节点,那就不一样的!
这里也要分是不是根节点!

寻找删除节点

  1. _findNode1(key) {
  2. let parent = this.root, current = null;
  3. if (parent === null) return null;
  4. while (current === null) {
  5. if (parent.key === key) return {node: parent};
  6. const testKey = parent.key > key ? "left" : "right";
  7. const child = parent[testKey];
  8. if (child) {
  9. if (child.key === key) {
  10. return {parent, child: testKey};
  11. } else {
  12. parent = child;
  13. }
  14. } else {
  15. return null;
  16. }
  17. }
  18. }
  19. //不递归与递归
  20. findNode(key) {
  21. return this._findNode(this.root, key);
  22. }
  23. _findNode(node, key) {
  24. if (node === null) return null;
  25. if (node.key === key) {
  26. return {node};
  27. }
  28. const test = node.key > key;
  29. const testKey = test ? "left" : "right";
  30. const child = node[testKey];
  31. if (child) {
  32. if (child.key === key)
  33. return {parent: node, child: testKey};
  34. return this._findNode(child,key);
  35. } else {
  36. return null;
  37. }
  38. }
  39. //注意我的返回结果的形式: null , {node},{parent,child:'left'},涉及到接口了。

删除简单节点——叶节点或者节点本身只有一个子节点

  1. _removeRoot(node) {
  2. if (node.right === null && node.left === null) {
  3. this.root = null; //木有子节点
  4. return true;
  5. }
  6. if (node.right && node.left) { //两个子节点
  7. } else { //一个子节点
  8. this.root = node.right || node.left;
  9. return true;
  10. }
  11. }
  12. remove(key) {
  13. const res = this._findNode1(key);
  14. if (res === null) return false;
  15. const {node, parent, child} = res;
  16. if (node) {
  17. //删除的是根节点!
  18. return this._removeRoot(node);
  19. } else {
  20. //node 与 parent不共存的:
  21. //1.没有子节点:
  22. const node = parent[child];
  23. if (node.right === null && node.left === null) {
  24. parent[child] = null;
  25. return true;
  26. }
  27. if (node.right && node.left) { //1个子节点
  28. } else { //两个子节点
  29. parent[child] = node.right || node.left;
  30. return true;
  31. }
  32. }
  33. }

这里对一个子节点的时候,为啥要直接赋值父节点的同样的left / right属性呢?我用上面的图中的树进行了演绎,就加深了感觉了,树的每一个节点都是二分法的裁判,每一个节点其实都是一个过滤器,过滤掉比他大的,比他小的。
我突然觉得这种过滤器的设计真的会很有用的,虽然目前我暂时木得想法。

删除节点有两个子节点

演绎,规律

截屏2020-04-04 下午5.40.17.png

其实,看这个ppt,演绎了一下,才真的是对二叉搜索树进行了构建概念的深入了。前面都是普通认知层面。
上面我恰好对节点的所谓的过滤器机制进行了构建了。我发现这次演绎,其实就是说的这个过滤器机制:

  1. 情况1
  2. 9节点 左边的都比9小,并且比7大。右边的都比9大!
  3. 情况二:
  4. 7节点:左边的都比7小,最大为5。右边都比7大,最小为9
  5. 情况三:
  6. 15节点:左边的都比15小,比11大.右边的都比15大,最小为19

删除的那个节点,是要当过滤器的存在,替补的节点,必须要做到:

  1. 比删除位置的左节点链接的所有节点都大
  2. 比删除位置的右节点链的所有节点都小。

比如左子节点作为根节点的子树的最大值,或右子节点作为根节点的子树的最小值。

删除节点是其父节点的left/right属性,分情况演绎:
left——满足条件1,2:
这个节点根据情况的不同,可以是删除节点的左子节点或左子节点构成树的最大值(相对应的断裂的子节点也需要拼接的),或者是删除节点的右子节点组成的树的最小值!
right——满足条件1,2:
这个节点根据情况的不同,可以是删除节点的左子节点组成的树的最大值,或者是删除节点的右子节点组成的树的最小值!

如果,要从左节点链中拿一个节点,那就是其中的最大值的节点
如果,要从右节点链中拿一个节点,那就是其中的最小值

其实根本结论就是:左子节点作为根节点的子树的最大值,或右子节点作为根节点的子树的最小值。
**

规律

截屏2020-04-04 下午7.56.19.png

截屏2020-04-04 下午11.38.05.png
**

删除封装完整版

**

  1. //找到替换的元素:前驱/后继
  2. findPrecursorOrSuccessor(node) {
  3. const {left, right} = node;
  4. let precursor = left, successor = right, parent;
  5. if (precursor) {
  6. while (precursor.right) {
  7. parent = precursor;
  8. precursor = precursor.right;
  9. }
  10. //预处理,替换的节点跟它的父节点的交接工作:
  11. if (parent) {
  12. parent.right = precursor.left; //有木有值都对!
  13. }
  14. return {precursor, parentNeed: Boolean(parent)};
  15. //这是区分是否是直接为删除节点的直接left节点的
  16. } else {
  17. while (successor.left) {
  18. parent = successor;
  19. successor = successor.left;
  20. }
  21. //预处理,替换的节点跟它的父节点的交接工作:
  22. if (parent) {
  23. parent.left = successor.right; //有木有值都对!
  24. }
  25. return {successor, parentNeed: Boolean(parent)};
  26. //这是区分是否是直接为删除节点的直接right节点的
  27. }
  28. }
  29. //回归删除操作的那部分:完整版
  30. _removeRoot(node) {
  31. if (node.right === null && node.left === null) {
  32. this.root = null; //木有子节点
  33. return node;
  34. }
  35. if (node.right && node.left) { //两个子节点
  36. const {precursor, successor,parentNeed} =
  37. this.findPrecursorOrSuccessor(node);
  38. const newNode = precursor || successor;
  39. if (parentNeed){
  40. newNode.right = node.right
  41. newNode.left = node.left
  42. }else{
  43. precursor && (precursor.right = node.right);
  44. successor && (successor.left = node.left);
  45. }
  46. this.root = newNode;
  47. return node;
  48. } else { //一个子节点
  49. this.root = node.right || node.left;
  50. return node;
  51. }
  52. }
  53. remove(key) {
  54. const res = this._findNode1(key);
  55. if (res === null) return false;
  56. const {node, parent, child} = res;
  57. if (node) {
  58. //删除的是根节点!
  59. return this._removeRoot(node);
  60. } else {
  61. //node 与 parent不共存的:
  62. //1.没有子节点:
  63. const node = parent[child];
  64. if (node.right === null && node.left === null) {
  65. parent[child] = null;
  66. return node;
  67. }
  68. if (node.right && node.left) { //2个子节点
  69. const {precursor, successor, parentNeed} =
  70. this.findPrecursorOrSuccessor(node);
  71. const newNode = precursor || successor;
  72. if (parentNeed) {
  73. newNode.left = node.left;
  74. newNode.right = node.right;
  75. } else {
  76. precursor ? (precursor.right = node.right) :
  77. (successor.left = node.left);
  78. }
  79. parent[child] = newNode;
  80. // console.log(parent, "parent", newNode, "new");
  81. return node;
  82. } else { //1个子节点
  83. parent[child] = node.right || node.left;
  84. return node;
  85. }
  86. }
  87. }