一.树的术语
- 树(tree):n(n>=0)个节点构成的有限集合,当n=0时,称为空树
- 对于任一棵非空树(n>0),它具备以下性质:
a.树中有一个称为“根(root)”的特殊节点,用r表示
b.其余节点可分为m(m>0)个互不相交的有限集T1,T2,…Tm,其中每个集合本身又是一棵树,称为原来树的“子树(SubTree)”
二.二叉树
- 如果树中的每个节点最多只能有两个子节点,这样的树就称为“二叉树”。实际上,所有的树都可以用二叉树模拟出来
- 二叉树的特性:
a.一个二叉树第i层的最大节点数为:2^(i-1),i>=1
b.深度为k的二叉树有最大节点总数为:2^k-1,k>=1
c.对任意非空二叉树T,若n0表示叶子节点个数,n2表示度为2的非叶节点个数,那么两者满足关系n0=n2+1 - 完美二叉树:也称为满二叉树,在二叉树中,除了最下一层的叶节点外,每层节点都有两个子节点,就构成了满二叉树
- 完全二叉树:除二叉树最后一层外,其他个层的节点数都达到最大个数;且最后一层从左向右的叶节点连续存在,只缺右侧若干节点;完美二叉树是特殊的完全二叉树
- 二叉树的存储:二叉树常见的存储方式是采用链表存储,每个节点封装成一个node,node中包含存储的数据,左节点的引用,右节点的引用
三.二叉搜索树
- 二叉搜索树(BST,Binary Search Tree),也称为二叉排序树或二叉查找树
- 二叉搜索树是一棵二叉树,可以为空
- 如果不为空,需要满足以下性质:
a.非空左子树的所有键值小于其根节点的键值
b.非空右子树的所有键值大于其根节点的键值
c.左、右子树本身也是二叉搜索树 - 二叉搜索树的特点:
a.二叉搜索树的特点就是相对较小的值总是保存在左节点上,相对较大的值总是保存在右节点上
b.通过这一特点,查找效率将会非常高
c.获取最大值和最小值会非常方便,即最左侧的叶子节点值最小,最右侧的叶子节点值最大
四.二叉搜索树的基本操作
- 封装
// 封装二叉搜索树
function BinarySearchTree(){
function Node(key){
this.key = key
this.left = null
this.right = null
}
// 属性
this.root = null
// 方法
}
- 插入数据
// 插入数据
BinarySearchTree.prototype.insert = function(key){
// 1.根据key创建节点
var newNode = new Node(key)
// 2.判断root是否存在
if(this.root == null){
this.root = newNode
}else{
this.insertNode(this.root,newNode)
}
}
//递归操作
BinarySearchTree.prototype.insertNode = function(node,newNode){
if(newNode.key < node.key){ // 向左查找
if(node.left == null){
node.left = newNode
}else{
this.insertNode(node.left,newNode)
}
}else{// 向右查找
if(node.right == null){
node.right = newNode
}else{
this.insertNode(node.right,newNode)
}
}
}
3.遍历二叉搜索树
遍历一棵树是指访问树的每一个节点,以下所提出的遍历方法,针对所有的二叉树都适用,不仅仅是二叉搜索树
二叉树的常见遍历有三种方法:
- 先序遍历
a.先访问根节点
b.先序遍历其左子树
c.先序遍历其右子树
代码示例:
BinarySearchTree.prototype.preOrderTraversal = function(handler){
this.preOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){
if(node != null){
// 1.处理经过的节点
handler(node.key)
// 2.处理经过节点的左子节点
this.preOrderTraversalNode(node.left,handler)
// 3.处理经过节点的右子节点
this.preOrderTraversalNode(node.right,handler)
}
}
// 测试遍历
var resultString = ''
bst.preOrderTraversal(function(key){
resultString += key + ' '
})
alert(resultString)
- 中序遍历
a.中序遍历其左子树
b.访问根节点
c.中序遍历其右子树
代码示例:
BinarySearchTree.prototype.midOrderTraversal = function(handler){
this.midOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.midOrderTraversalNode = function(node,handler){
if(node != null){
// 1.处理左子树中的节点
this.midOrderTraversalNode(node.left,handler)
// 2.处理节点
handler(node.key)
// 3.处理右子树中的节点
this.midOrderTraversalNode(node.right,handler)
}
}
- 后序遍历
a.后序遍历其左子树
b.后序遍历其右子树
c.访问根节点
代码示例:
BinarySearchTree.prototype.postOrderTraversal = function(handler){
this.postOrderTraversalNode(this.root,handler)
}
BinarySearchTree.prototype.postOrderTraversalNode = function(node,handler){
if(node != null){
// 1.处理左子树中的节点
this.postOrderTraversalNode(node.left,handler)
// 2.处理右子树中的节点
this.postOrderTraversalNode(node.right,handler)
// 3.处理节点
handler(node.key)
}
}
四.二叉树的搜索
- 获取最大值&最小值
// 查找最大值
BinarySearchTree.prototype.max = function(){
// 1.获取根节点
var node = this.root
// 2.持续向右查找,直到找到最右侧节点
while(node.right != null){
node = node.right
}
return node.key
}
// 查找最小值
BinarySearchTree.prototype.min = function(){
// 1.获取根节点
var node = this.root
// 2.持续向左查找,直到找到最左侧节点
while(node.left != null){
node = node.left
}
return node.key
}
- 搜索特定的值
提供两种方法实现这一功能:递归和循环
a.递归法:
BinarySearchTree.prototype.search = function(key){
return this.searchNode(this.root,key)
}
BinarySearchTree.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
}
}
b.循环法:
BinarySearchTree.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{
return true
}
}
return false
}
五.二叉搜索树的删除
1.分析删除流程:
a.先找到要删除的节点,如果没有找到,则不需要删除
b.找到要删除的节点,需要考虑三种情况
情况一:删除叶子节点
情况二:删除只有一个子节点的节点
情况三:删除有两个子节点的节点
注意:执行删除操作时,一定要考虑根节点的情况
2.代码示例
BinarySearchTree.prototype.remove = function(key){
// 1.寻找要删除的节点
// 1.1.定义变量,保存一些信息
var current = this.root
var parent = null
var isLeftChild = true
// 1.2.开始寻找删除的节点
while(current.key != key){
parent = current
if(current.key > key){
isLeftChild = true
current = current.left
}else{
isLeftChild = false
current = current.right
}
// 某种情况:已经找到最后一个节点,依然没有找到key
if(current == null) return false
}
// 2.找到了current.key == key
// 2.1.删除的节点是叶子节点(没有子节点)
if(current.left == null && current.right == null){
if(current == this.root){ // 即整棵树只有根节点
this.root = null
}else if(isLeftChild){
parent.left = null
}else{
parent.right = null
}
}
// 2.2.删除的节点有一个子节点
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
}
}
}
3.删除有两个子节点的节点是比较复杂的,逻辑上会选择其左子树的最大值或右子树的最小值来填补删除的空节点。
在二叉搜索树中,这两个特殊的节点有特殊的名字:
比current小一点点的节点,称为current节点的前驱。比如下图中的节点5就是节点7的前驱;
比current大一点点的节点,称为current节点的后继。比如下图中的节点8就是节点7的后继;
- 查找需要被删除的节点current的后继时,需要在current的右子树中查找最小值,即在current的右子树中一直向左遍历查找;
- 查找前驱时,则需要在current的左子树中查找最大值,即在current的左子树中一直向右遍历查找
代码示例:
else{
//1.获取后继节点
let successor = this.getSuccessor(current)
//2.判断是否根节点
if (current == this.root) {
this.root = successor
}else if (isLeftChild){
parent.left = successor
}else{
parent.right = successor
}
//3.将后继的左子节点改为被删除节点的左子节点
successor.left = current.left
}
}
//封装查找后继的方法
BinarySearchTree.prototype.getSuccessor = function(delNode){
//1.定义变量,保存找到的后继
let successor = delNode
let current = delNode.right
let successorParent = delNode
//2.循环查找current的右子树节点
while(current != null){
successorParent = successor
successor = current
current = current.left
}
//3.判断寻找到的后继节点是否直接就是删除节点的right节点
if(successor != delNode.right){
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
六.非平衡树
- 比较好的二叉搜索树应该是左右分布均匀的
- 但是当插入连续数据后,分布的不均匀,称这种树为非平衡树
- 对于一棵平衡二叉树来说,查找/插入等操作的效率是O(logN)
- 对于一棵非平衡二叉树,相当于编写了一个链表,查找效率变为O(N)
1.树的平衡性
- 为了能以较快的速度O(logN)操作一棵树,需要保证树总是平衡的
- 平衡树也是二叉搜索树
- 目前常见的平衡树为–AVL树和红黑树