二叉搜索树(Binary Search Tree)作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
二叉搜索树
思考:在 n 个动态的整数中搜索某个整数(查看其是否存在)?
- 使用动态数组,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n);
- 使用有序的动态数组,使用二分搜索,最坏时间负载度:O(log(n)),但是添加和删除的平均时间复杂度是:O(n);
- 使用二叉搜索树,添加、删除和搜索的最坏时间复杂度都是:O(log(n));
二叉搜索树特性
二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为 BST。又被称为:二叉查找树、二叉排序树。二叉搜索树可以大大提高搜索数据的效率。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树。
使用二叉搜索树需要注意:
- 存储的元素必须具备比较性,比如
int
、double
等。 - 如果是自定义类型,需要指定比较方式。
- 存储的元素不可以是
null
。
接口设计
|
|
添加节点
- 找到要添加到的位置,即找到该位置的父节点
parent
; - 创建新节点
node
; - 加入到树中:
parent.left = node
或者parent.right = node
。
注意:遇到值相等的元素可以覆盖旧值。
如添加 12
、1
,先找到父节点 11
、2
:
创建新节点 node
,加入到树中:
代码实现:
元素的比较方案设计
方案一
强制元素实现 Comparable
接口:
添加 extends Comparable
后,要加入到二叉搜索树的元素都必须实现 Comparable
接口。
定义 Person 实现 Comparable
接口:
方案二
- 允许外界传入一个
Comparator
自定义比较方案; - 如果没有传入
Comparator
,强制认定元素实现了Comparable
接口。12345678910111213141516171819public class BinarySearchTree<E> {private Comparator<E> comparator;public BinarySearchTree() {this(null);}public BinarySearchTree(Comparator<E> comparator) {this.comparator = comparator;}private int compare(E e1, E e2) {if (comparator != null) {return comparator.compare(e1, e2);}return ((Comparable<E>)e1).compareTo(e2);}}
打印器
使用 BinaryTrees 打印二叉搜索树。
1、实现 BinaryTreeInfo
接口
2、调用打印API BinaryTrees.println(bst)
:
Comparable
|
|
打印结果:
Comparator
|
|
修改 BinarySearchTree
的 string
方法,查看更详细的打印信息:
打印结果:
国外教材的说法
Full Binary Tree:完满二叉树(真二叉树);
Perfect Binary Tree:完美二叉树(满二叉树);
Complete Binary Tree:完全二叉树;
遍历
遍历是数据结构中的常见操作,线性数据结构(如:数组)的遍历比较简单,有两种:正序遍历和逆序遍历。二叉树的遍历方式有四种:
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后序遍历(Postorder Traversal)
- 层序遍历(Level Order Traversal)
前序遍历
访问顺序:
- 根节点;
- 前序遍历左子树;
- 前序遍历右子树。
递归
7、4、2、1、3、5、9、8、11、10、12
代码实现:
非递归
中序遍历
访问顺序(升序):
- 中序遍历左子树;
- 根节点;
- 中序遍历右子树。
访问顺序(降序):
- 中序遍历右子树;
- 根节点;
- 中序遍历左子树。
递归
1、2、3、4、5、7、8、9、10、11、12(升序)
代码实现:
非递归
后序遍历
访问顺序:
- 后序遍历左子树;
- 后序遍历右子树;
- 根节点。
递归
1、3、2、5、4、8、10、12、11、9、7
代码实现:
非递归
层序遍历
访问顺序:
- 从上到下、从左到右依次访问每一个几点。
7、4、9、2、5、8、11、1、3、10、12
代码实现:
遍历接口
前、中、后、层
遍历接口提供一个回调方法,将遍历到的元素传到方法调用处处理:
前序遍历、中序遍历、后序遍历和中序遍历:
测试
打印结果:
遍历接口增强
实现当遍历到指定节点时,停止遍历。
层序遍历
程序遍历的修改比较简单,只需要:
- 给
Visitor.visit()
方法增加返回值; - 在处理元素的位置添加判断即可。
|
|
前、中、后
前序遍历、中序遍历和后序遍历实现方式相同:
- Visitor 类中记录 isStop 变量(是否停止遍历的标签);
- 在遍历过程中,添加是否停止遍历的判断。12345678/** 回调方法(相当于 OC 的 block)* 返回值:true(停止遍历)/false(继续遍历)*/public static abstract class Visitor<E> {public boolean isStop;public abstract boolean visit(E element);}
前序遍历、中序遍历和后序遍历:
测试:设置遍历到 8 时停止遍历。
打印结果:
遍历的应用
前序遍历:树状结构展示(注意左右子树的顺序);
中序遍历:二叉搜索树的中序遍历按升序或降序处理节点;
后序遍历:适用于一些先子后父的操作;
层序遍历:计算二叉树的高度、判断一棵树是否为完全二叉树;
练习Ⅰ
树状打印二叉树
|
|
测试:
打印结果:
上面👆的打印是通过前序遍历实现的,同样的也可以使用中序遍历、后序遍历实现。
计算二叉树的高度
递归
|
|
迭代
|
|
测试
|
|
打印结果:
完全二叉树的判断
使用层序遍历:
- 如果树为空,返回 false;
- 如果 node.left != null,将 node.left 入队;
- 如果 node.left == null && node.right != null,返回 false;
- 如果 node.right != null,将 node.right 入队;
- 如果 node.right == null,则后边再遍历到的节点必须都是叶子节点,否则返回 false;
- 遍历结束,返回 true;
|
|
测试:
打印结果:
翻转二叉树
226. 翻转二叉树
核心代码:
分别用前序遍历、中序遍历、后序遍历和层序遍历实现:
重构二叉树
以下两种方式,可以保证重构处唯一的一个二叉树。重构的原理是通过两种不同的遍历方式找到根节点,再用同样的方式找到每棵子树的根节点,以此类推:
- 前序遍历 + 中序遍历:
- 后序遍历 + 中序遍历:
如:前序遍历:4、2、1、3、6、5
,中序遍历:1、2、3、4、5、6
。
- 先根据前序遍历可以找到这棵二叉树的根节点
4
; - 再结合中序遍历可以找到左子树的根节点
2
,右子树的根节点6
; - 最后根据中序遍历可以知道
2
的左子树是1
,右子树是3
;6
的左子树是5
。
“前序遍历 + 后序遍历”这种情况比较特殊,存在两种情况:
- 如果是一棵真二叉树(Proper Binary Tree),结果是唯一的;
重构方式同上👆。 - 否则不唯一;
因为只有一棵子树,所以从遍历结果不能确定子树究竟是左子树还是右子树。
前驱节点
前驱节点:中序遍历时的前一个节点。如果是二叉搜索树,前驱节点就是前一个比它小的节点。
查找方式:
node.left != null
;predecessor = node.left.right.right.right...
终止条件:right == null
。
如:6、8、13
node.left == null && node.parent != null
;predecessor = node.parent.parent.parent...
终止条件:node
在parent
的右子树中(node == node.parent.right
)。
如:7、11、9、1
node.left == null && node.parent == null
,没有前驱节点(没有左子树的根节点)。
代码实现:
后继节点
后继节点:终须遍历时的后一个几点。如果是二叉搜索树,后继节点就是后一个比它大的节点。
node.right != null
;successor = node.right.left.left.left...
终止条件:left == null
。
如:1、8、4
node.right == null && node.parent != null
;successor = node.parent.parent.parent...
终止条件:node
在parent
的左子树中(node == node.parent.left
)。
如:7、6、3、11
node.right == null && node.parent == null
,没有后继节点(没有右子树的根节点)。
代码实现:
删除
删除-叶子节点
|
|
删除-度为1的节点
用子节点代替原节点的位置,child 是 node.left 或者 node.right:
删除-度为2的节点
- 先用前驱或后继节点的值覆盖原节点的值;
- 再删除相应的前驱或后继节点;
- 如果一个节点的度为2,那它的前驱、后继节点的度只能是1和0;
代码实现
|
|
练习 Ⅱ
二叉树的前序遍历
递归:
迭代:
二叉树的中序遍历
递归:
迭代:
二叉树的后序遍历
45. 二叉树的后序遍历
递归:
迭代:
二叉树的层序遍历
102. 二叉树的层序遍历
迭代:
二叉树的最大深度
104. 二叉树的最大深度
递归:
迭代:
二叉树的层次遍历 II
107. 二叉树的层次遍历 II
二叉树最大宽度
662. 二叉树最大宽度
N叉树的前序遍历
589. N叉树的前序遍历
N叉树的后序遍历
590. N叉树的后序遍历
N叉树的最大深度
559. N 叉树的最大深度
二叉树展开为链表
114. 二叉树展开为链表
从中序与后序遍历序列构造二叉树
106. 从中序与后序遍历序列构造二叉树
从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树
根据前序和后序遍历构造二叉树
889. 根据前序和后序遍历构造二叉树
对称二叉树
101. 对称二叉树
练习 Ⅲ
删除二叉搜索树中的节点
450. 删除二叉搜索树中的节点
二叉搜索树中的搜索
700. 二叉搜索树中的搜索
二叉搜索树中的插入操作
701. 二叉搜索树中的插入操作
验证二叉搜索树
98. 验证二叉搜索树
二叉搜索树的最小绝对差
530. 二叉搜索树的最小绝对差
二叉搜索树节点最小距离
783. 二叉搜索树节点最小距离
将有序数组转换为二叉搜索树
108. 将有序数组转换为二叉搜索树
二叉搜索树的范围和
938. 二叉搜索树的范围和
二叉搜索树的最近公共祖先
235. 二叉搜索树的最近公共祖先
二叉搜索树中第K小的元素
230. 二叉搜索树中第K小的元素
二叉搜索树迭代器
173. 二叉搜索树迭代器
恢复二叉搜索树
99. 恢复二叉搜索树
ps:
二叉树绘图网站
520it
Binary Tree Visualiser
Data Structure Visualizations
B-Tree