树结构
- 树(Tree): n(n>=0)个节点构成的有限集合。
- 当 n = 0时,称为空树
- 对于任一课非空树(n>0),它具备以下性质:
- 树中有一个成为“根(root)”的特殊节点,用 r标识
- 其余节点分为m(m>0)个互不相交的有限集T1,T2,T3....Tm, 其中每个集合本身又是一颗树,称为原来树的“子树(SubTree)”
树的术语
- 节点的度: 节点的子树个数(比如二叉树一个节点只有两个分支,度为2. 最下面的节点,没有分支了,度为0)。
- 树的度: 树的所有节点中最大的度数 (如果某个节点有4个分支,其他节点都没这么多,那么树的度为4)。
- 叶节点: 度为0的节点
- 父节点:有子树的节点
- 子节点:若A节点是B节点的父节点,则称B节点是A节点的子节点
- 兄弟节点: 具有同一父节点的节点彼此称为兄弟节点
- 路径和路径长度: 从节点n1到nk的路径称为一个节点的序列n1,n2,n3....nk, ni是ni+1的父节点。 路径说包含
边
的个数为路径的长度。(从跟节点沿着分支一直走到底,没有其他分支了。这个就是路径。路径中每个节点连接的分支就是边 ) - 节点的层次: 规定根节点在1层, 其他任一节点的层数是其父节点的层数加1
- 树的深度: 所有节点的最大层级是这个树的深度。
二叉树
二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树 [2] 。
特殊类型
- 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树 。
- 完全二叉树:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树
任何一棵树都可以转换为二叉树
二叉树的存储
- 数组(2n-1找到左边的节点, 2n+2找右边的节点)
-
链表(使用链表更合适)
二叉搜索树
二叉搜索树(Binary Search Tree),(又:二叉查找树,二叉排序树)
- 二叉搜索树是一颗二叉树,可以为空
- 如果不为空,满足以下性质:
- 非空左子树的所有键值小于根节点的键值
- 非空右子树的所有键值大于根节点的键值
- 左、右子树本身也是二叉搜索树
特点:
- 二叉搜索树的特点是 较小的值 总是保存在左节点上, 较大的值总是保存在右节点上
树的遍历
树的遍历是树的一种重要的运算。所谓遍历是指对树中所有结点的系统的访问,即依次对树中每个结点访问一次且仅访问一次。树的3种最重要的遍历方式分别称为前序遍历、中序遍历和后序遍历。 (先访问父节点(从根节点开始)就叫前序, 中间访问父节点叫做中序, 最后访问父节点叫做后序)
js 实现代码
// 创建BinarySearchTree
function BinarySerachTree() {
// 创建节点构造函数
function Node(key) {
this.key = key
this.left = null
this.right = null
}
// 保存根的属性
this.root = null
// 二叉搜索树相关的操作方法
// 向树中插入数据
BinarySerachTree.prototype.insert = function (key) {
// 1.根据key创建对应的node
var newNode = new Node(key)
// 2.判断根节点是否有值,没有直接赋值
if (this.root === null) {
this.root = newNode
} else {
this.insertNode(this.root, newNode)
}
}
BinarySerachTree.prototype.insertNode = function (node, newNode) {
// 1.准备向左子树插入数据 插入的值比当前节点的值小,往左节点插入
if (newNode.key < node.key) {
if (node.left === null) { // 1.1.node的左子树上没有内容
node.left = newNode
} else {
// 1.2.node的左子树上已经有了内容 递归调用,直到插入到正确的位置
this.insertNode(node.left, newNode)
}
} else { // 2.准备向右子树插入数据
if (node.right === null) { // 2.1.node的右子树上没有内容
node.right = newNode
} else { // 2.2.node的右子树上有内容
this.insertNode(node.right, newNode)
}
}
}
// 获取最大值和最小值
// 最小值,最左边的节点值
BinarySerachTree.prototype.min = function () {
var node = this.root
while (node.left !== null) {
node = node.left
}
return node.key
}
// 最大值 最右边的节点值
BinarySerachTree.prototype.max = function () {
var node = this.root
while (node.right !== null) {
node = node.right
}
return node.key
}
// 搜搜特定的值
/*
BinarySerachTree.prototype.search = function (key) {
return this.searchNode(this.root, key)
}
BinarySerachTree.prototype.searchNode = function (node, key) {
// 1.如果传入的node为null那么, 那么就退出递归
if (node === null) {
return false
}
// 2.判断node节点的值和传入的key大小
if (node.key > key) { // 2.1.传入的key较小, 向左边继续查找
return this.searchNode(node.left, key)
} else if (node.key < key) { // 2.2.传入的key较大, 向右边继续查找
return this.searchNode(node.right, key)
} else { // 2.3.相同, 说明找到了key
return true
}
}
*/
// 查找值 存不存在
BinarySerachTree.prototype.search = function (key) {
var node = this.root
while (node !== null) {
if (node.key > key) {
node = node.left
} else if (node.key < key) {
node = node.right
} else {
// 找到 返回 true
return true
}
}
// 其他情况 返回 false
return false
}
BinarySerachTree.prototype.removeNode = function (node, key) {
// 1.如果传入的node为null, 直接退出递归.
if (node === null) return null
// 2.判断key和对应node.key的大小
if (node.key > key) {
node.left = this.removeNode(node.left, key)
}
}
// 删除结点
BinarySerachTree.prototype.remove = function (key) {
// 1.定义临时保存的变量
var current = this.root
// 当前节点的父节点 初始根节点无父节点
var parent = null
// 标识 判断是否是 左节点
var isLeftChild = true
// 2.开始查找节点
while (current.key !== key) {
// 再查找子节点前,先把当前节点保存到parent中。 就是之后节点的父节点
parent = current
// 如果 需要删除的值小于 当前节点值
if (key < current.key) {
// 设置 标识 isLeftChild 为true
// 并 把当前节点设置为 左节点
isLeftChild = true
current = current.left
} else {
isLeftChild = false
current = current.right
}
// 如果发现current已经指向null, 那么说明没有找到要删除的数据
if (current === null) return false
}
// 到这里没有 return 说名找到了节点
// 3.删除的结点是叶结点 该 节点 没有 子节点 最简单
if (current.left === null && current.right === null) {
// 如果当前节点就是根节点,并且没有子节点 。直接把根节点 置为 null 即可
if (current == this.root) {
this.root == null
} else if (isLeftChild) {
// 如果 这是 左节点 ,那把当前节点的父节点 的 left置为null
parent.left = null
} else {
parent.right = null
}
}
// 4.删除有一个子节点的节点 (上面已经排除了 左右节点同时 不存在的情况)
// 只有 判断 一个节点不存在的情况,另一个节点肯定存在
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
}
}
// 5.删除有两个节点的节点
// 存在两个节点的情况 比较复杂。 要么采用前驱找到左边的最大节点(都小于右边的节点值),要么才有后继找到右边的最小节点(都大于左边的节点值)
// 替换掉需要删除的节点
else {
// 1.获取后继节点 (这里用后继方式)
// getSuccessor获取的后继节点 以及处理好了右子节点
var successor = this.getSuccessor(current)
// 2.判断是否是根节点
// 如果需要删除的是根节点,那么直接把右边找到的最小节点赋值给root即可
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
// 如果需要删除的节点是左节点
// 把删除节点的父节点 的left 改为 后继节点
parent.left = successor
} else {
parent.right = successor
}
// 上面只是 把后继节点接上 了父节点
// 还需要把删除节点的左子节点 拼接到 后继节点上的左节点上
// 3.将删除节点的左子树赋值给successor
successor.left = current.left
}
return true
}
// 前驱,是小于x.key的最大关键字的结点。(获取需要删除节点,左边节点里面最大的节点值| 左边节点,一直往右)
// 后继,是大于x.key的最小关键字的结点 (获取需要删除节点,右边节点里面的最小的节点值 | 右边节点一直往左)
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
// 1.使用变量保存临时的节点
// 保存 后继节点的父节点
var successorParent = delNode
// 保存找到的后继 默认为删除节点
var successor = delNode
// 要从右子树开始找
var current = delNode.right
// 2.寻找节点
// current 为 null 的情况下 就找到了
// 一直往昨早
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
// 1. 如果后继节点 不是 删除节点的右节点(即 删除的节点不是 刚好是只有一个右节点)
// 需要把删除节点的 由节点 赋值到 后继节点的右节点
// 2. 后继节点 虽然没有左节点了。但是存在 右节点的情况。
// 需要把 后继节点的右节点 赋值给 后继节点父节点的左节点
// 然后再把 删除节点的右节点赋值给后继节点的 右节点
// 返回 getSuccessor 之后 会把后继节点移动到 删除节点的位置
if (successor != delNode.right) {
successorParent.left = successorParent.right
successor.right = delNode.right
}
}
// 遍历方法
// 先序遍历 (先执行handler函数)
BinarySerachTree.prototype.preOrderTraversal = function (handler) {
this.preOrderTranversalNode(this.root, handler)
}
BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
if (node !== null) {
// 先执行 handler函数
handler(node.key)
// 一直递归调用查找左节点,直到没有左节点后,返回上一层,再执行右节点
// 右节点里面又递归,找左节点。。。。
this.preOrderTranversalNode(node.left, handler)
this.preOrderTranversalNode(node.right, handler)
}
}
// 中序遍历(再找到最左边的节点后再执行handler函数)
BinarySerachTree.prototype.inOrderTraversal = function (handler) {
this.inOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.inOrderTraversalNode = function (node, handler) {
if (node !== null) {
this.inOrderTraversalNode(node.left, handler)
handler(node.key)
this.inOrderTraversalNode(node.right, handler)
}
}
// 后续遍历
BinarySerachTree.prototype.postOrderTraversal = function (handler) {
this.postOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
if (node !== null) {
this.postOrderTraversalNode(node.left, handler)
this.postOrderTraversalNode(node.right, handler)
handler(node.key)
}
}
}
// 测试代码
var bst = new BinarySerachTree()
// 插入数据
bst.insert(11)
bst.insert(7)
bst.insert(15)
bst.insert(5)
bst.insert(3)
bst.insert(9)
bst.insert(8)
bst.insert(10)
bst.insert(13)
bst.insert(12)
bst.insert(14)
bst.insert(20)
bst.insert(18)
bst.insert(25)
bst.insert(6)
// // 测试前序遍历结果
// var resultString = ""
// bst.preOrderTraversal(function (key) {
// resultString += key + " "
// })
// alert(resultString) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
//
// // 测试中序遍历结果
// resultString = ""
// bst.inOrderTraversal(function (key) {
// resultString += key + " "
// })
// alert(resultString) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
//
// // 测试后续遍历结果
// resultString = ""
// bst.postOrderTraversal(function (key) {
// resultString += key + " "
// })
// alert(resultString) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
//
// // 获取最值
// alert(bst.min()) // 3
// alert(bst.max()) // 25
//
// // 查找特定的值
// alert(bst.search(10)) // true
// alert(bst.search(21)) // false
// 查找数据
alert(bst.remove(10))
alert(bst.remove(20))
alert(bst.remove(21))
缺点:
不平衡二叉树: 如果按顺序插入节点,比如 9,8,7,6,5,4,3,1. 这样二叉树就类似于链表了。如果输入序列已经排序,则时间复杂度为O(N)
平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内