9.树结构
9.1树的特点
树是n(n>=0)个节点构成的有限集合。它具有以下特点:
-
每个节点有零个或多个子节点
-
没有父节点的节点称为根节点
-
每一个非根节点有且只有一个父节点
-
除了根节点外,每个子节点可以分为多个不相交的子树
具有以下性质:
-
树中结点数等于所有结点的度数之和加1
-
高度为h的m次树最多有m^h-1/m-1个结点
-
度为m的树中第i层上最多有m^(i-1)个结点(i>=1)
9.2树的术语
-
节点的度:节点的子树个数
-
树的度:树所有节点中最大的度数
-
叶节点:度为0的节点
-
父节点:有子树的节点为父节点
-
子节点:有父节点的节点为子节点,如A节点是B节点的父节点,则称B节点为A节点的子节点
-
兄弟节点:具有同一父节点的各节点彼此间是兄弟节点
-
路径和路径长度:从节点n1到nk的路径为一个节点序列n1,n2,...nk,ni是ni+1的父节点,路径所包含边的个数为路径长度,如下图,A节点到D节点的路径为A-B-D,长度为2(A-B,B-D)
-
节点的层次:规定根节点在1层,其它任一节点的层数是其父节点的层数加1
-
树的深度:树中所有节点中的最大层次是这颗树的深度
9.3二叉树
如果树中每个节点最多只能有两个子节点,这样的树称为二叉树
二叉树的五种形态
9.3.1二叉树的特性
-
一个二叉树第i层的最大节点数为:2^(i-1),i>=1
-
深度为k的二叉树有最大节点总数为:2^k-1,k>=1
-
对任何非空二叉权利,若n0表示叶节点个数,n2是度为2的非叶节点数,那么n0=n2+1
-
满二叉树:节点数达到最大值,除叶节点外,每层节点都有2个子节点且每条路径长度一致(长度一致的话就意味着叶子结点都在同一层)
-
完全二叉树:除了树最后一层外,其他各层节点数都达到最大,且最后一层从左向中的叶节点连续存在,只缺右侧若干节点,下图为非完全二叉树
-
如何将上面非完全二叉树变为完全二叉树?
因为叶节点是从左向右连续存在的,D结点缺少了右结点所以导致叶节点不连续,所以我们可以将D的右结点补上或将E节点的J,K两个节点去掉
9.3.2遍历二叉树
9.3.2.1先序遍历
-
访问根节点
-
按照从左到右的顺序先根遍历根结点的每一颗子树
使用先序遍历上图【二叉树遍历】得到结果:A-BD-EF-CG
//先序遍历 BST.prototype.preOrderTraverse = function (node) { console.log(node.key) if (node.left != null) { this.preOrderTraverse(node.left) } if (node.right != null) { this.preOrderTraverse(node.right) } }
9.3.2.2中序遍历
-
中序遍历其左子树
-
访问根节点
-
中序遍历其右子树
使用中序遍历上图【二叉树遍历】得到结果:DF-BE-A-CG
//中序遍历 BST.prototype.inOrderTraverse = function (node) { if (node.left != null) { this.inOrderTraverse(node.left) } this.Traverse.push(node.key) if (node.right != null) { this.inOrderTraverse(node.right) } }
9.3.2.3后序遍历
- 按照从左到右的顺序后根遍历根结点的每一棵子树
-
再访问根结点
使用后序遍历上图【二叉树遍历】得到结果:FD-EB-GC-A
//后序遍历 BST.prototype.postOrderTraverse = function (node) { if (node.left != null) { this.postOrderTraverse(node.left) } if (node.right != null) { console.log(node.key) this.postOrderTraverse(node.right) } this.Traverse.push(node.key) }
9.3.3二叉搜索树(BST,二叉排序树或二叉查找树)
9.3.3.1二叉搜索树的特点
不为空的二叉搜索树满足以下性质:
-
非空左子树的所有键值小于其根节点的键值
-
非空右子树的所有键值大于其根节点的键值
-
左、右子树本身也是二叉搜索树
9.3.3.2二叉搜索树的常见操作
-
insert(key):向树中插入一个新的键
-
search(key):在树中查找一个键,如果节点存在,则返回true,如果不存在则返回false
-
inOrderTraverse:通过中序遍历方式遍历所有节点
-
preOrderTraverse:通过先序遍历方式遍历所有节点
-
postOrderTraverse:通过后序遍历方式遍历所有节点
-
min:返回树中最小的值/键
-
max:返回树中最大的值/键
-
remove(key):从树中移除某个键
9.3.3.3二叉搜索树的封装
-
二叉搜索树整体实现预览
function BST () { function Node (key) { this.key = key this.left = null this.right = null } this.Traverse = [] this.root = null//根节点 //插入数据 BST.prototype.insert = function (key) {} BST.prototype.insertNode = function (node, newNode) {} //先序遍历 BST.prototype.preTraversal = function (node) {} //中序遍历 BST.prototype.inOrderTraverse = function (node) {} //后序遍历 BST.prototype.postOrderTraverse = function (node) {} //最小值 BST.prototype.min = function (node) {} //最大值 BST.prototype.max = function (node) {} //查找是否有某节点 BST.prototype.search = function (key) {} //删除某节点 BST.prototype.remove = function (key) {} //找后继的方法 BST.prototype.getSuccessor = function (delNode) {} }
-
insert(key)
思路:
-
先根据传入的key创建对应的Node
-
如果是第一次插入则直接修改根节点
-
如果非第一次插入则需要进行相关比较再决定插入位置
BST.prototype.insert = function (key) { //根据KEY创建节点 var newNode = new Node(key)//创建新元素 if (this.root == null) { this.root = newNode } else { this.insertNode(this.root, newNode) } }
-
-
min()
二叉搜索树最左侧的节点为最小值
BST.prototype.min = function (node) { while (node.left != null) { node = node.left } return node.key }
-
max()
二叉搜索树最右侧的节点为最大值
BST.prototype.max = function (node) { while (node.right != null) { node = node.right } return node.key }
-
search(key)
BST.prototype.search = function (key) { var node = this.root while (node != null) { if (node.key == key) return true else if (node.key > key) node = node.left else node = node.right } return false }
-
remove(key)
-
删除节点需要考虑的情况:
-
该节点是叶节点
-
检测当前的left与right是否都为null
-
都为null之后还要检测当前left是否为根,是的话清空二叉树(因为是在没有子节点的情况下,所以当此节点为根的时候代表整个二叉树就只有此节点)
-
不为根的话就把父节点的left或right字段设为null
-
该节点有一个子节点
将该结点的子节点直接替换自身就行
-
该节点有两个子节点,有以下情况
情况一:删除9节点
解决方法1:将8替换9,7的right指向8并将10放在8的right(8变成10的父结点,10比8大因此在右边)
解决方法2:将10替换9,7的right指向10并将8放在10的left(10变成8的父结点,8比10小因此在左边)
情况二:删除7节点
解决方法1:直接将5替代7,将9放在5的right上
解决方法2:将8替代7,8的left指向5,right指向9
为什么要选择8?因为在二叉搜索树中要保证每个结点它的左边要比它小,右边比它大,所以在7节点的右侧寻找替代7的节点时只有8合适,将8替代7才能保证,左边比它小(5,3),右边比它大(9,10)
情况三:删除节点15
解决方法1:若想在15的左边找替代15的节点,则要选择14,因为只有14才能保证删除15后仍是一棵二叉搜索树,14替代15的话能保证左子树都比14小,右子树都比14大,如果在15左边找的是12的话,那么13与14都要往右子树移动(13,14>12),这样代码实现起来基本上是对右子树进行一个重构了,代码量大也难以实现
解决方法2:若想在15的右边找替代15的节点,则要选择18与上一解决方法理由相似,只有18才能保证左子树比18小,右子树都比18大。将18替换15的位置,20的left指向19即可
总结:若想在要删除的节点左子树中找替代的节点则要选择左子树集中值最大的一个节点,若想在要删除的节点右子树中找替代的节点则要选择右子树集中值最小的一个节点
BST.prototype.remove = function (key) { var current = this.root var parent = this.root var isLeftChild = true //开始寻找要删除的节点 while (current.key != key) { parent = current if (key < current.key) { isLeftChild = true current = current.left } else { isLeftChild = false current = current.right } } //已经找到了最后,仍然未找到 if (current == null) return false //当删除的是叶子节点时 if (current.left == null && current.right == null) { if (current == this.root) { this.root = null//当前节点既是叶子节点也是头节点 } else { if (isLeftChild) { parent.left = null } else parent.right = null } } //删除的节点有一个子节点 else if (current.right == null) { if (current == this.root) { this.root = current.left } else if (isLeftChild) { parent.left = current.left } else { parent.right = current.left } } else if (current.left == null) { if (current == this.root) { this.root = current.right } else if (isLeftChild) { parent.left = current.right } else { parent.right = current.right } } //删除的节点有两个子节点 else { //获取后续节点 var successor = this.getSuccessor(current) //判断是否是根节点 if (current == this.root) { this.root = successor } else if (isLeftChild) { parent.left = successor } else { parent.right = successor } //将删除节点的左子树=current.left successor.left = current.left } } //找后继的方法 BST.prototype.getSuccessor = function (delNode) { //定义变量,保存找到的后继 var successor = delNode var current = delNode.right var successorParent = delNode //循环查找,找到要删除节点右子树集中最左边的节点,为右子树集中最小值 while (current != null) { successorParent = successor successor = current current = current.left } //判断寻找的后继节点是否直接就是delNode的right节点 if (successor != delNode.right) { successorParent.left = null successor.right = delNode.right } return successor }
-