二叉排序树的创建、搜索与遍历

二叉排序树的创建、搜索与遍历

二叉排序树(Binary Sort Tree)又叫二叉查找树。平衡二叉树(AVL树)、B树、B+树、红黑树都能看到二叉排序树的思想,所以说二叉排序树是学习后面树的基础也不为过。

二叉排序树主要具有以下的三个特征:

  • 若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
  • 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  • 左、右子树也分别为二叉排序树;

这个定义读下来直观的感受就是递归。整棵树具有前两个特点,任何子树也具有前两个特点。

直接上代码:

数据结构

树的结点定义为TreeNode类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TreeNode {
public Integer data; // 结点数据
public TreeNode parent; // 父节点
public TreeNode left; // 左孩子结点
public TreeNode right; // 右孩子结点

public TreeNode(Integer data){
this.data = data;
}

@Override
public String toString() {
return "TreeNode [data=" + data + "]";
}
}

整棵树定义为BSTree类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BSTree {
// 根节点
public TreeNode root;
// 结点数
public Integer size = 0;

public TreeNode getRoot() {
return root;
}

public Integer getSize() {
return size;
}
}

创建二叉排序树

采用的策略:

  1. 判断根节点是否为null,为null则创建根节点。
  2. 如果存在父节点,则判断该结点和父节点比较值大小
    1. 如果比父节点小,判断父节点左子树是否为空,为空则插入到左子树
    2. 如果比父节点大,判断父节点右子树是否为空,为空则插入到右子树
  3. 相等值不插入

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public Boolean add(Integer data) {
// 无结点时
if (null == root) {
root = new TreeNode(data);
System.out.println("根结点创建成功,值为:" + data);
return true;
}
// 已存在结点
TreeNode node = new TreeNode(data);
// 记录根结点
TreeNode currentNode = root;
// 父节点,用于比较数值大小
TreeNode parentNode;
while (true) {
// 根节点为父节点
parentNode = currentNode;
// 要插入的值比父节点小时,切换到左子树
if (node.data < parentNode.data) {
currentNode = currentNode.left;
// 如果父节点左子树为空
if (null == currentNode) {
parentNode.left = node;
node.parent = parentNode;
System.out.println(data + " 成功插入到 " + parentNode.data + " 的左子树");
size++;
return true;
}
} else if (node.data > parentNode.data) { //
currentNode = currentNode.right;
// 如果父节点右子树为空
if (null == currentNode) {
parentNode.right = node;
node.parent = parentNode;
System.out.println(data + " 成功插入到 " + parentNode.data + " 的右子树");
size++;
return true;
}
} else { // 插入相等值
System.out.println(data + "已存在!不能重复插入");
return false;
}
}
}

查找结点

采用策略:

  1. 如果根节点为null,直接返回。
  2. 如果根节点不为空,判断当前结点(currentNode)是否为null,不为null则比较大小
    1. 如果要查找的值比当前结点值小,则去左子树查找
    2. 如果要查找的值比当前结点值大,则去右子树查找
  3. 找到则返回,找不到返回null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public TreeNode searchBST(Integer data) {
// 根节点为空,直接返回null 与下面代码表达意思不同
if (null == root) {
return null;
}

TreeNode currentNode = root;
// 因为currentNode查找中会不断变化,比如左子树为null的时候退出循环,返回null跟根节点为null混淆
while (currentNode != null) {
// 值比当前结点值小,去左子树查找
if (data < currentNode.data) {
currentNode = currentNode.left;
} else if (data > currentNode.data) { // 值比当前结点值大,去右子树查找
currentNode = currentNode.right;
} else {
// 如果找到了就直接返回
return currentNode;
}
}
return null;
}

前序遍历

前序遍历相对来说比较简单。

递归前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void preOrderRecursivePrint(TreeNode node) {
// 遇到结点就打印
if (null != node) {
System.out.print(node.data + " ");
}
// 左子树不为空就先进入左子树
if (node.left != null) {
preOrderRecursivePrint(node.left);
}
// 右子树不为空就进入右子树
if (node.right != null) {
preOrderRecursivePrint(node.right);
}
}

迭代前序遍历

迭代替代递归的思想在于用自定义栈替代了系统的方法栈,其实思想都差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void preOrderIterationPrint(TreeNode node) {
if (null != node) {
// 用栈保存代替递归
Stack<TreeNode> stack = new Stack<>();
// 先将结点推入栈
stack.push(node);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
System.out.print(pop.data + " ");
// 因为栈出栈顺序和入栈相反,所以先放右子树结点再放左子树
if (pop.right != null) {
stack.push(pop.right);
}
if (pop.left != null) {
stack.push(pop.left);
}
}
}
}

中序遍历

递归中序遍历

1
2
3
4
5
6
7
8
9
10
11
public void midOrderRecursivePrint(TreeNode node) {
if (null != node) {
if (node.left != null) {
midOrderRecursivePrint(node.left);
}
System.out.print(node.data + " ");
if (node.right != null) {
midOrderRecursivePrint(node.right);
}
}
}

迭代中序遍历

迭代的中序遍历相比之下复杂一些,在左孩子结点、根节点、右孩子节点顺序之下,根节点也可看作其父结点的左孩子结点或右孩子结点。所以不需要针对根节点进行特殊操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void midOrderIterationPrint(TreeNode node) {

if (null != node) {
Stack<TreeNode> stack = new Stack<>();
TreeNode currentNode = node;

while (currentNode != null || !stack.isEmpty()) {
// 先将左子树全部入栈
while (currentNode != null) {
stack.push(currentNode);
currentNode = currentNode.left;
}
// 一个一个出栈,出栈的同时进行中序遍历输出
if (!stack.isEmpty()) {
currentNode = stack.pop();
// 输出左结点
System.out.print(currentNode.data + " ");
// 切换右子树(当前根节点也是左子树的一部分,如果有右子树则打印后进入)
currentNode = currentNode.right;
}
}
}
}

后序遍历

递归后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void postOrderRecursivePrint(TreeNode node) {
if (node != null) {
// 先递归左子树
if (node.left != null) {
postOrderRecursivePrint(node.left);
}
// 再递归右子树
if (node.right != null) {
postOrderRecursivePrint(node.right);
}
// 返回的时候由于先递归左边再递归右边,返回的时候先打印左边叶子结点,再打印右边叶子结点,最后打印根节点
System.out.print(node.data + " ");
}
}

迭代后序遍历

迭代后序遍历比较复杂。由于先将左子树全部入栈,再一个个退栈后,不会立即输出退栈的结点值,还需要查找其是否有右孩子结点,所以需要重新压入栈。所以如果当其再次退栈后又会回到原来的右子树形成死循环,所以需要一个临时结点记录右孩子结点(rightNode)的值防止出现重复遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void postOrderIterationPrint(TreeNode node) {
if (node != null) {
Stack<TreeNode> stack = new Stack<>();
TreeNode currentNode = node;
// rightNode的作用是在返回的时候判断是否重复遍历
TreeNode rightNode = null;
while (currentNode != null || !stack.isEmpty()) {
// 先将本结点和左子树全部推入栈
while (currentNode != null) {
stack.push(currentNode);
currentNode = currentNode.left;
}
// 一个一个弹出栈
currentNode = stack.pop();
// 这里判断右子树是否为null
while (currentNode != null && (currentNode.right == null || currentNode.right == rightNode)) {
System.out.print(currentNode.data + " ");
// 将rightNode赋值为该右结点防止重复遍历
rightNode = currentNode;
if (stack.isEmpty()) {
System.out.println();
return;
}
currentNode = stack.pop();
}
// 如果弹出之后发现它还有右子树,则重新压入栈
stack.push(currentNode);
// 进入它的右子树
currentNode = currentNode.right;
}
}
}

运行结果

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
BSTree tree = new BSTree();
int[] ints = ArrGenerator.generateArr(10, 100); // 自动生成一个包含10个100以内的数字的数组
System.out.print("====要插入的值为:");
ArrGenerator.printArr(ints);
Arrays.stream(ints).forEach(tree::add);
System.out.print("====查找的值为:" + ints[3]);
System.out.println(tree.searchBST(ints[3]).toString());
System.out.print("====递归前序遍历:");
tree.preOrderRecursivePrint(tree.getRoot());
System.out.println();
System.out.print("====非递归前序遍历:");
tree.preOrderIterationPrint(tree.getRoot());
System.out.println();
System.out.print("====递归中序遍历:");
tree.midOrderRecursivePrint(tree.getRoot());
System.out.println();
System.out.print("====非递归中序遍历:");
tree.midOrderIterationPrint(tree.getRoot());
System.out.println();
System.out.print("====递归后序遍历:");
tree.postOrderRecursivePrint(tree.getRoot());
System.out.println();
System.out.print("====非递归后序遍历:");
tree.postOrderIterationPrint(tree.getRoot());
}
image-20210402215203977

根据结果我们可以检查是否正确,预期整棵树应该为:

image-20210402215550750

查看打印出来的插入结果完全符合。

小结

二叉排序树其实并不是很难,因为还没有涉及到平衡(旋转左右子树),而红黑树可以在10亿的数据里查找30次即可找到!背后的基础是基于平衡二叉树、而平衡二叉树是基于二叉排序树。所以想要手撕红黑树(梦想),只得基础先打牢。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!