二叉查找树也叫二叉搜索树,是二叉树中最常用的一种类型。它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。而所有这些都依赖于二叉查找树的特殊结构。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
1、相关概念
1.1 树(Tree)
树的定义:树为N个结点构成的有限集合。树中有一个称为“根(Root)”的特殊结点,其余结点可分为若干个互不相交的树,称为原来结点的“子树”。
这里的每个元素都叫做“节点”,用来连接相邻节点之间的关系称为“父子关系”。如下图,A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点 E。我们把没有子节点的节点叫做叶子节点或者叶节点,比如图中的 G、H、I、J、K、L 都是叶子节点。
还有几个比较相似的概念:高度(Height)、深度(Depth)、层(Level)。它们的定义是这样的:
- 节点的高度 = 节点到叶子节点的最长路径(边数)。
- 节点的深度 = 根节点到这个节点所经历的边的个数。
- 节点的层数 = 节点的深度 + 1。
- 树的高度 = 根节点的高度。
小技巧:高度从下至上从0开始计算, 深度从上至下从0开始计算,层数从上至下从1开始计算
1.2 二叉树(Binary Tree)
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。下面这几个都是二叉树。以此类推,你可以想象一下四叉树、八叉树长什么样子。
- 这个图里面,有两个比较特殊的二叉树,分别是编号 2 和编号 3 这两个。
满二叉树(是完全二叉树的一种特殊情况)
编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
完全二叉树
编号 3 的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
1.3 完全二叉树定义的由来
满二叉树很好理解,也很好识别,但是完全二叉树,很多人可能就分不清楚了。下面是几个完全二叉树和非完全二叉树的例子,大家可以对比看看。
完全二叉树定义,目的就是为了方便进行数组形式的存储。
如何表示(或者存储)一棵二叉树?
想要存储一棵二叉树,有两种方法:一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
1)链式存储法
从图中可以很清楚地看到,每个节点有三个属性,其中一个属性存储数据,另外两个属性分别存储指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
2)顺序存储法
如图,我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。
如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。
上例是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置(可以从0开始编号)。如果是非完全二叉树,其实会浪费比较多的数组存储空间。看下例:
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这就是为什么完全二叉树要求最后一层的子节点都靠左的原因。
二叉树的每个节点最多有两个子节点,分别是左子节点和右子节点。二叉树中,有两种比较特殊的树,分别是满二叉树和完全二叉树。满二叉树又是完全二叉树的一种特殊情况。
- 堆其实就是一种完全二叉树,最常用的存储方式就是数组。
- 二叉树既可以用链式存储,也可以用数组顺序存储。数组顺序存储的方式比较适合完全二叉树,其他类型的二叉树用数组存储会比较浪费存储空间。
2、二叉树的遍历
二叉树通过何种方式将所有节点都遍历打印出来呢?经典的方法有三种,前序遍历、中序遍历和后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。
-
前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
-
中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
-
后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。
从上图可以看出,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)。
二叉树的前、中、后序遍历实际上就是一个递归的过程。而写递归代码的关键,就是看能不能写出递推公式,这里的关键就是,如果要解决问题 A,就假设子问题 B、C 已经解决,然后再来看如何利用 B、C 来解决 A。所以,我们可以把前、中、后序遍历的递推公式都写出来。
//前序遍历的递推公式
preOrder(r)=print r->preOrder(r->left)->preOrder(r->right)
//中序遍历的递推公式
inOrder(r)=print inOrder(r->left)->r->inOrder(r->right)
//后序遍历的递推公式
postOrder(r)=print postOrder(r->left)->postOrder(r->right)
先序遍历代码:
/**
* 先序遍历(先根遍历):先访问根节点,再访问左右子节点
*/
public void preOrder() {
System.out.print("先序遍历(先根遍历): ");
preOrder(root);
System.out.println();
}
private void preOrder(TreeNode node) {
if (node != null) {
System.out.print(node.data + " ");
preOrder(node.left);
preOrder(node.right);
}
}
中序遍历代码:
/**
* 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
*/
public void inOrder() {
System.out.print("中序遍历: ");
inOrder(root);
System.out.println();
}
private void inOrder(TreeNode node) {
if (node != null) {
inOrder(node.left);
System.out.print(node.data + " ");
inOrder(node.right);
}
}
后序遍历代码:
/**
* 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
*/
public void postOrder() {
System.out.print("后序遍历: ");
postOrder(root);
System.out.println();
}
private void postOrder(TreeNode node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.print(node.data + " ");
}
}
3、二叉查找树(Binary Search Tree)
二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。它是怎么做到这些的呢?这些都依赖于二叉查找树的特殊结构。二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
3.1 二叉查找树的查找操作
根据二叉查找树的性质,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。所以在二叉查找树中查找一个节点的操作就相对比较简单,我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归(或迭代)查找;如果要查找的数据比根节点的值大,那就在右子树中递归(或迭代)查找。如下图所示:
基于递归实现
public TreeNode find(int value) {
return findInternal(value, root);
}
private TreeNode findInternal(int value, TreeNode node) {
//递归终止条件
if (node == null) return null;
if (node.data == value) return node;
if (node.data > value) {
return findInternal(value, node.left);
} else {
return findInternal(value, node.right);
}
}
基于非递归的实现
public TreeNode find1(int value) {
TreeNode node = root;
//终止条件
while (node != null && node.data != value) {
if (node.data > value) {
node = node.left;
} else {
node = node.right;
}
}
return node;
}
3.2 二叉查找树的插入操作
二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以只需要从根节点开始,依次比较要插入的数据和节点的大小关系。
- 如果树中找到节点的值等于待插入的节点,则直接返回。
- 如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置。
- 如果要插入的数据比节点的数据大,并且节点的右子树不为空,就再递归遍历右子树,直到找到合适的插入位置。
- 如果要插入的数据比节点的数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置。
- 如果要插入的数据比节点的数值小,并且节点的左子树不为空,就再递归遍历左子树,直到找到合适的插入位置。
public void insert(int value) {
//树还没有初始化
if (root == null) {
root = new TreeNode(value);
return;
}
TreeNode p = root;
while (p != null) {
//存在,不需要插入
if (p.data == value) return;
//往 p 的右子树查找
if (value > p.data) {
//p 的 右子树为空
if (p.right == null) {
p.right = new TreeNode(value);
return;
}
p = p.right;
}
//往p的左子树查找
if (value < p.data) {
//p的左子树为空
if (p.left == null) {
p.left = new TreeNode(value);
return;
}
//否则继续在p的左子树中查找
p = p.left;
}
}
}
3.3 二叉查找树的删除操作
二叉查找树的查找、插入操作都比较简单,相对来说删除操作就比较复杂了。针对要删除节点的子节点个数的不同,我们需要分三种情况来处理。
3.3.1 要删除的节点没有子节点
如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
3.3.2 要删除的节点只有一个子节点
如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
3.3.3 要删除的节点有两个子节点
如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。
public void delete(int value) {
//p 表示指向要删除的节点,初始化指向跟结点
TreeNode p = root;
//pp 表示指向p的父节点
TreeNode pp = null;
while (p != null && p.data != value) {
pp = p;
if (value > p.data) p = p.right;
else p = p.left;
}
//没有找到节点p
if (p == null) {
return;
}
//要删除的节点有两个子节点
if (p.left != null && p.right != null) {
TreeNode minP = p.right;
TreeNode minPP = p;
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
//值替换
p.data = minP.data;
//下面变为删除minP了
p = minP;
pp = minPP;
}
//删除结点是叶子节点或者仅有一个子节点
TreeNode child;
if (p.left != null) {
child = p.left;
} else if (p.right != null) {
child = p.right;
} else child = null;
//删除跟结点
if (pp == null) root = child;
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
其实还可以用一种比较简单的思路,删除节点并不是真正的删除,节点不是真的删除,而是打一个标记。但是这样有一个缺陷,如果删除的节点过多,会占用过多的内存空间,而且查找,和插入等的需要遍历的元素也会相应的增多。
- public boolean deleted;
当然删除的元素只是被打一个标记,再插入时如果元素相同,可以重复使用该位置,比如插入元素 3 ,此时打一个标记,deleted = true,过段时间后再插入元素 3 ,则可简单的把标记deleted = false即可。
3.4 二叉查找树的其他操作
除了插入、删除、查找操作之外,二叉查找树中还可以支持快速地查找最大节点和最小节点、前驱节点和后继节点。二叉查找树除了支持上面几个操作之外,还有一个重要的特性,就是中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是 O(n),非常高效。因此,二叉查找树也叫作二叉排序树。
- 根据二叉查找树的性质,对于任何一个节点,其左子树的节点都比其小,右子树的节点都比其大,所以需要寻找最小的节点,只需要不断的往左子树查找,直到某一个节点的左子树为空,或者是到达叶子节点。
public TreeNode findMin() {
if (root == null) {
return null;
}
TreeNode node = root;
while (node.left != null) {
node = node.left;
}
return node;
}
- 快速查找最大节点与最小节点是类似的,快速查找最大节点就是不断的往右进行查找,直到每一个节点的左子树为空,或者是到达叶子节点
public TreeNode findMax() {
if (root == null) {
return null;
}
TreeNode node = root;
while (node.right != null) {
node = node.right;
}
return node;
}
4、支持重复数据的二叉查找树
前面讲二叉查找树的时候,我们默认树中节点存储的都是数字。很多时候,在实际的软件开发中,我们在二叉查找树中存储的,是一个包含很多字段的对象。我们利用对象的某个字段作为键值(key)来构建二叉查找树。我们把对象中的其他字段叫作卫星数据。
前面我们讲的二叉查找树的操作,针对的都是不存在键值相同的情况。那如果存储的两个对象键值相同,这种情况该怎么处理呢?我这里有两种解决方法。
第一种方法比较容易。二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。有点类似与散列表的拉链冲突解决办法。
第二种方法比较不好理解,不过更加优雅。每个节点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树,也就是说,把这个新插入的数据当作大于这个节点的值来处理。
当要查找数据的时候,遇到值相同的节点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值的所有节点都找出来。
对于删除操作,我们也需要先查找到每个要删除的节点,然后再按前面讲的删除操作的方法,依次删除。
5、二叉查找树的时间复杂度分析
现在,我们来分析一下,二叉查找树的插入、删除、查找操作的时间复杂度。实际上,二叉查找树的形态各式各样。比如这个图中,对于同一组数据,我们构造了三种二叉查找树。它们的查找、插入、删除操作的执行效率都是不一样的。图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了 O(n)。
我刚刚其实分析了一种最糟糕的情况,我们现在来分析一个最理想的情况,二叉查找树是一棵完全二叉树(或满二叉树)。这个时候,插入、删除、查找的时间复杂度是多少呢?
从我前面的例子、图,以及还有代码来看,不管操作是插入、删除还是查找,时间复杂度其实都跟树的高度成正比,也就是 O(height)
既然这样,现在问题就转变成另外一个了,也就是,如何求一棵包含 n 个节点的完全二叉树的高度?树的高度就等于最大层数减一,为了方便计算,我们转换成层来表示。从图中可以看出,包含 n 个节点的完全二叉树中,第一层包含 1 个节点,第二层包含 2 个节点,第三层包含 4 个节点,依次类推,下面一层节点个数是上一层的 2 倍,第 K 层包含的节点个数就是 2^(K-1)。
不过,对于完全二叉树来说,最后一层的节点个数有点儿不遵守上面的规律了。它包含的节点个数在 1 个到 2^(L-1) 个之间(我们假设最大层数是 L)。如果我们把每一层的节点个数加起来就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:
n >= 1+2+4+8+...+2^(L-2)+1
n <= 1+2+4+8+...+2^(L-2)+2^(L-1)
借助等比数列的求和公式,我们可以计算出,L 的范围是[log2(n+1), log2n +1]。完全二叉树的层数小于等于 log2n +1,也就是说,完全二叉树的高度小于等于 log2n
显然,极度不平衡的二叉查找树,它的查找性能肯定不能满足我们的需求。我们需要构建一种不管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树(平衡二叉查找树)。
二叉查找树,它支持快速地查找、插入、删除操作。
二叉查找树中,每个节点的值都大于左子树节点的值,小于右子树节点的值。不过,这只是针对没有重复数据的情况。对于存在重复数据的二叉查找树,我介绍了两种构建方法,一种是让每个节点存储多个值相同的数据;另一种是,每个节点中存储一个数据。针对这种情况,我们只需要稍加改造原来的插入、删除、查找操作即可。
在二叉查找树中,查找、插入、删除等很多操作的时间复杂度都跟树的高度成正比。两个极端情况的时间复杂度分别是 O(n) 和 O(logn),分别对应二叉树退化成链表的情况和完全二叉树。
6、二叉查找树和散列表
散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找操作时间复杂度才是 O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?我认为有下面几个原因:
- 散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
- 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O(logn)。
- 笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
- 散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。
- 最后,为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。
综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突。我们在实际的开发过程中,需要结合具体的需求来选择使用哪一个。
7、完整代码实现
7.1 节点定义
public class TreeNode {
public TreeNode left;
public TreeNode right;
public int data;
public TreeNode() {
}
public TreeNode(int data) {
this.data = data;
}
@Override
public String toString() {
return "TreeNode{" +
"left=" + left +
", right=" + right +
", data=" + data +
'}';
}
}
7.2 二叉树操作
package com.kz.example.algorithm.find.treeNode;
import java.util.Stack;
/**
* 二叉查找树
*/
public class BinarySearchTree {
private TreeNode root;
/**
* 查找节点
*
* @param value 节点的值
* @return
*/
public TreeNode find(int value) {
return findInternal(value, root);
}
/**
* 查找节点
*
* @param value 节点的值
* @param node 节点
* @return
*/
private TreeNode findInternal(int value, TreeNode node) {
//递归终止条件
if (node == null) return null;
if (node.data == value) return node;
if (node.data > value) {
return findInternal(value, node.left);
} else {
return findInternal(value, node.right);
}
}
/**
* 查找节点
*
* @param value 节点的值
* @return
*/
public TreeNode find1(int value) {
TreeNode node = root;
//终止条件
while (node != null && node.data != value) {
if (node.data > value) {
node = node.left;
} else {
node = node.right;
}
}
return node;
}
/**
* 插入节点
*
* @param value 节点的值
*/
public void insert(int value) {
//树还没有初始化
if (root == null) {
root = new TreeNode(value);
return;
}
TreeNode p = root;
while (p != null) {
//存在,不需要插入
if (p.data == value) return;
//往 p 的右子树查找
if (value > p.data) {
//p 的 右子树为空
if (p.right == null) {
p.right = new TreeNode(value);
return;
}
p = p.right;
}
//往p的左子树查找
if (value < p.data) {
//p的左子树为空
if (p.left == null) {
p.left = new TreeNode(value);
return;
}
//否则继续在p的左子树中查找
p = p.left;
}
}
}
/**
* 删除节点
*
* @param value 节点的值
*/
public void delete(int value) {
//p 表示指向要删除的节点,初始化指向跟结点
TreeNode p = root;
//pp 表示指向p的父节点
TreeNode pp = null;
while (p != null && p.data != value) {
pp = p;
if (value > p.data) p = p.right;
else p = p.left;
}
//没有找到节点p
if (p == null) {
return;
}
//要删除的节点有两个子节点
if (p.left != null && p.right != null) {
TreeNode minP = p.right;
TreeNode minPP = p;
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
//值替换
p.data = minP.data;
//下面变为删除minP了
p = minP;
pp = minPP;
}
//删除结点是叶子节点或者仅有一个子节点
TreeNode child;
if (p.left != null) {
child = p.left;
} else if (p.right != null) {
child = p.right;
} else child = null;
//删除跟结点
if (pp == null) root = child;
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
/**
* 查找最小值
*
* @return
*/
public TreeNode findMin() {
if (root == null) {
return null;
}
TreeNode node = root;
while (node.left != null) {
node = node.left;
}
return node;
}
/**
* 查找最大值
*
* @return
*/
public TreeNode findMax() {
if (root == null) {
return null;
}
//一直往右找左节点
TreeNode node = root;
while (node.right != null) {
node = node.right;
}
return node;
}
private void checkEmpty() {
if (isEmpty()) {
throw new RuntimeException("没有这样的节点");
}
}
/**
* 返回树中的节点数
*
* @return
*/
public int size() {
return size(root);
}
/**
* 节点个数
*
* @param node
* @return
*/
private int size(TreeNode node) {
if (node == null) {
return 0;
}
//当前节点(1) + 左子树节点数 + 右子树节点数
return 1 + size(node.left) + size(node.right);
}
/**
* 是否为空
*
* @return
*/
public boolean isEmpty() {
return root == null;
}
/**
* 设置为空
*/
public void makeEmpty() {
root = null;
}
/**
* 先序遍历(先根遍历):先访问根节点,再访问左右子节点
*/
public void preOrder() {
System.out.print("先序遍历(先根遍历): ");
preOrder(root);
System.out.println();
}
private void preOrder(TreeNode node) {
if (node != null) {
System.out.print(node.data + " ");
preOrder(node.left);
preOrder(node.right);
}
}
/**
* 中序遍历=:先访问左子节点,再访问根节点,最后访问右子节点
*/
public void inOrder() {
System.out.print("中序遍历: ");
inOrder(root);
System.out.println();
}
private void inOrder(TreeNode node) {
if (node != null) {
inOrder(node.left);
System.out.print(node.data + " ");
inOrder(node.right);
}
}
/**
* 后序遍历:先访问左子节点,再访问右子节点,最后访问根节点
*/
public void postOrder() {
System.out.print("后序遍历: ");
postOrder(root);
System.out.println();
}
private void postOrder(TreeNode node) {
if (node != null) {
postOrder(node.left);
postOrder(node.right);
System.out.print(node.data + " ");
}
}
/**
* 深度优先遍历,相当于先序遍历
*/
public void depthOrder() {
depthOrder(root);
System.out.println();
}
/**
* 和图的深度优先搜索类似
*
* @param node
*/
private void depthOrder(TreeNode node) {
if (node != null) {
Stack<TreeNode> stack = new Stack<>();
stack.push(node);
while (!stack.isEmpty()) {
TreeNode p = stack.pop();
System.out.print(p.data + " ");
//先push right,再push left 达到pop时先访问left的效果
if (p.right != null) {
stack.push(p.right);
}
if (p.left != null) {
stack.push(p.left);
}
}
}
}
/**
* 删除二叉树中最小节点
*/
public void removeMin() {
root = removeMin(root);
}
/**
* 删除node节点最小子树节点
*
* @param node
* @return 新的子树根节点,而不是返回删除的节点
*/
private TreeNode removeMin(TreeNode node) {
//找到了最小左子树节点,返回其右子树
//目的是让其父节点的left指向其右子树
if (node.left == null) {
return node.right;
}
//更新引用
node.left = removeMin(node.left);
return node;
}
public void printTree() {
if (isEmpty()) {
System.out.println("Empty tree");
} else {
print(root);
}
}
private void print(TreeNode node) {
System.out.println(node);
}
public static void main(String[] args) {
BinarySearchTree tree = new BinarySearchTree();
// 插入的值重复时会忽略
tree.insert(1);
tree.insert(1);
tree.insert(3);
tree.insert(2);
tree.insert(1);
tree.insert(4);
tree.insert(5);
int size = tree.size();
System.out.println("节点个数 = " + size);
tree.preOrder();
tree.inOrder();
tree.postOrder();
TreeNode treeNode = tree.find(1);
System.out.println(treeNode.toString());
TreeNode min = tree.findMin();
System.out.println("min = " + min.toString());
TreeNode max = tree.findMax();
System.out.println("max = " + max.toString());
tree.delete(5);
max = tree.findMax();
System.out.println("max = " + max.toString());
}
}
7.3 支持重复数据逇二叉树
package com.kz.example.algorithm.find.treeNode;
import java.util.ArrayList;
import java.util.List;
public class BinarySearchTreeRepeatData {
private TreeNode root;
/**
* 查找符合条件的节点
*
* @param value 节点的值
* @return
*/
public List<TreeNode> find(int value) {
List<TreeNode> listTreeNode = new ArrayList<>();
TreeNode p = root;
while (p != null) {
//如果找到相等的,则加入到返回结果,同时继续去右子树查找
if (p.data == value) {
listTreeNode.add(p);
p = p.right;
} else if (p.data < value) {
p = p.right;
} else {
p = p.left;
}
}
return listTreeNode;
}
/**
* 插入数据
*
* @param value 节点的值
*/
public void insert(int value) {
//树没有初始化
if (root == null) {
root = new TreeNode(value);
return;
}
TreeNode p = root;
while (p != null) {
if (p.data <= value) {
if (p.right == null) {
TreeNode newNode = new TreeNode(value);
p.right = newNode;
return;
}
p = p.right;
} else {
if (p.left == null) {
TreeNode newNode = new TreeNode(value);
p.left = newNode;
return;
}
p = p.left;
}
}
}
/**
* 删除所有符合的节点
*
* @param data 节点的值
*/
public void deleteNode(int data) {
TreeNode p = this.root;
TreeNode pParent = null; // p 的父节点
while (p != null) {
if (p.data == data) {
TreeNode pRe = delete(pParent, p);
p = pRe;
} else if (p.data < data) {
pParent = p;
p = p.right;
} else {
pParent = p;
p = p.left;
}
}
}
/**
* 删除指定节点
*
* @param pParent
* @param p
*/
public TreeNode delete(TreeNode pParent, TreeNode p) {
int flag = 0;
TreeNode pBak = p;
// 要删除的节点有左右子节点
if (p.left != null && p.right != null) {
TreeNode minP = p.right;
TreeNode minPP = p; // minP 的父节点
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 将 minP 的数据替换到 p 中
/* 技巧:对右子树中最小的节点进行删除,
这种情况跟要删除的节点只有一颗子树或者没有子树情况一样,
所以这边将 minPP 赋值给 pParent,minP 赋值给 p,那么重复使用一段代码 */
pParent = minPP;
p = minP;
flag = 1;
}
TreeNode child = null;
// 要删除的节点只有左节点的情况
if (p.left != null) {
child = p.left;
} else if (p.right != null) { // 要删除的节点只有右子节点的情况
child = p.right;
} else { // 要删除的节点左右子节点都无的情况
child = null;
}
// 删除的是根节点的情况
if (pParent == null) {
this.root = child;
}
// 将 p 父节点的左/右子树重新指向
if (pParent.left == p) {
pParent.left = child;
} else if (pParent.right == p) {
pParent.right = child;
}
return flag == 1 ? pBak : child;
}
/**
* 快速查找最小节点
*
* @return
*/
public TreeNode findMin() {
if (root == null) {
return null;
}
//一直往左找左节点
TreeNode node = root;
while (node.left != null) {
node = node.left;
}
return node;
}
/**
* 快速查找最大节点
*
* @return
*/
public TreeNode findMax() {
if (root == null) {
return null;
}
//一直往右找左节点
TreeNode node = root;
while (node.right != null) {
node = node.right;
}
return node;
}
public static void main(String[] args) {
BinarySearchTreeRepeatData repeatData = new BinarySearchTreeRepeatData();
repeatData.insert(1);
repeatData.insert(1);
repeatData.insert(3);
repeatData.insert(2);
repeatData.insert(1);
repeatData.insert(4);
repeatData.insert(5);
List<TreeNode> listTreeNode = repeatData.find(1);
for(TreeNode node: listTreeNode) {
System.out.println(node.toString());
}
TreeNode min = repeatData.findMin();
System.out.println("min = "+min.toString());
TreeNode max = repeatData.findMax();
System.out.println("max = "+max.toString());
repeatData.deleteNode(5);
max = repeatData.findMax();
System.out.println("max = "+max.toString());
}
}
评论区