blog/source/_posts/binary-search-tree.md
2021-05-13 10:19:30 +08:00

172 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 二叉查找树
date: 2021-04-13 11:16:29
tags: [架构知识, 算法设计, 树形结构, 数据结构, 二叉树, 查找树, Java]
categories:
- [架构知识, 算法设计]
- [架构知识, 数据结构]
keywords: 算法设计,树形结构,数据结构,二叉树,查找树,BST,Java,子树,节点搜索
mathjax: true
---
二叉查找树Binary Search TreeBST是一种特殊的二叉树BST通过定义节点的左孩子和右孩子的约定关系提升了二叉树节点的搜索效率。
<!-- more -->
相比普通二叉树BST有以下额外的特点
- 对于任意一个节点n其左子树下的每个后代节点的值都小于节点n的值。
- 对于任意一个节点n其右子树下的每个后代节点的值都大于节点n的值。
以下是一个BST的示例。
{% oss_image binary-search-tree/bst.svg '二叉查找树' 300 %}
可能看着这个二叉树比较丑陋但是这的确是一个现实中常常会碰到的BST。有二叉树排序经验的读者可能会觉得这个二叉树十分熟悉的确BST也常常用来做排序这和你所看到和用到的二叉树排序几乎是一样的。
## BST构建
在使用BST之前需要先对之前定义的二叉树节点类稍微扩展一下。
``` java
public class BSTNode extends Node implements Comparable<BSTNode> {
public BSTNode(Integer value) {
super(value);
}
public bool isLeftChild() {
return this.getParent()
.map(parent -> this.compareTo(parent) < 0)
.orElse(false);
}
public bool isRightChild() {
return this.getParent()
.map(parent -> this.compareTo(parent) > 0)
.orElse(false);
}
@Override
public int compareTo(BSTNode other) {
return this.value - other.getValue();
}
}
```
## 节点搜索
在理论上使用BST查找节点可以使时间减半。要搜索一个节点BST需要利用以下步骤。
1. 如果给定要搜索的值c为空那么这个值必定不在BST中。
1. 取得当前节点n如果是搜索起始此时的节点n是BST的根节点。
1. 比较给定值c和节点n如果值相同则节点n为所需要的节点。
1. 如果节点n的值大于给定值c那么所需要的节点应该存在于节点n的左子树中。此时可以向节点n的左子树前进一层即将节点n的左孩子作为下一次比较的节点n。
1. 如果节点n的值小于给定值c那么所需要的节点应该存在于节点n的右子树中。此时可以向节点n的右子树前进一层即将节点n的右孩子作为下一次比较的节点n。
BST的这种搜索策略查找一个节点不需要遍历整个树只需要遍历树中的一个路径即可这个遍历的复杂度为$O(log_2N)$。以下给出一个BST的搜索方法实现示例。
```java
public Node search(Integer value) {
LinkedList<BSTNode> queue = new LinkedList<>();
queue.push(this.root);
while (!queue.isEmpty()) {
Node pointer = queue.pollFirst();
if (pointer.getValue() == value) {
return pointer;
}
if (pointer.getValue() > value) {
pointer.getLeftChild().ifPresent(queue::push)
}
if (pointer.getValue() < value) {
pointer.getRightChild().ifPresent(queue::push);
}
}
}
```
但是对于BST来说其搜索效率完全取决于其树的拓扑结构。如果树中只存在一条路径那么搜索的复杂度就会变成$O(n)$。所以这也是后面引入自平衡二叉查找树的原因。
## 插入节点
由于BST是一个十分有序的结构任何违反BST规则的树结构都会使BST的搜索失效。所以向BST中插入一个节点的时候最重要的是定位新节点的父节点而且新插入的节点始终是作为叶子节点的不存在需要重新排列的问题。在BST中定位新节点n的父节点的步骤如下。
1. 如果是搜索起始当前节点c为BST的根节点。
1. 如果节点c为空则节点c的父节点p即为节点n的父节点。如果节点n的值小于父节点p的值那么节点n将作为节点p的左孩子否则节点n作为节点p的右孩子。
1. 如果节点c与节点n的值相等那么说明当前正在插入一个重复节点此时可以选择丢弃节点n或者抛出错误。
1. 如果节点n小于节点c的值那么节点n的父节点在节点c的左子树中此时需要取得节点c的左孩子作为新的节点c继续进行比较。
1. 如果节点n大于节点c的值那么节点n的父节点在节点c的右子树中此时需要取得节点c的右孩子作为新的节点c继续进行比较。
以下给出一个向BST中插入新节点的示例方法。
```java
public void insert(Node newNode) throws RepeatValueException {
LinkedList<BSTNode> queue = new LinkedList<>();
queue.push(this.root);
while (!queue.isEmpty()) {
BSTNode pointer = queue.pollFirst();
if (pointer.compareTo(newNode) == 0) {
throw new RepeatValueException();
}
if (pointer.compareTo(newNode) > 0) {
pointer.getLeftChild()
.ifPresentOrElse(queue::push,
() -> pointer.attachLeftChild(newNode));
}
if (pointer.compareTo(newNode) < 0) {
pointer.getRightChild()
.ifPresentOrElse(queue::push,
() -> pointer.attachRightChild(newNode));
}
}
}
```
## 删除节点
从BST中删除一个节点的难度比插入节点更大。如果被删除的是一个非叶子节点那就意味着树的断裂此时就必须选择一个合适的节点来填补。所以在定位被删除的节点之后最重要的事情是选择一个合适的节点来代替被删除节点的位置。一般在删除BST的非叶子节点时会采用以下策略来完成整个操作。
1. 如果被删除的节点没有右孩子,那么就直接选择它的左孩子代替原来的节点。
1. 如果被删除的节点没有左孩子,那么就直接选择它的右孩子代替原来的节点。
1. 如果被删除的节点既有左孩子又有右孩子,那么就需要使用被删除节点的右子树中最小值的节点来代替原来的节点。
在这个策略中第三条策略是最复杂的但是并不难理解。被删除节点的值必定要小于右子树中所有的值且大于左子树中所有的值所以选择右子树中最小的值来代替被删除节点是完全能够保证BST继续遵守约定规则的。
以下是操作BST删除节点的例程这个例程在删除节点的时候首先需要找到被删除节点的父节点然后定位用来替代被删除节点的节点在最复杂的情况下需要做两次节点删除操作。
```java
public BSTNode minimum(BSTNode root) {
return root.getLeftChild()
.map(this::minimum)
.orElse(root);
}
public void remove(Node pendingDelete) {
BSTNode parent = pendingDelete.getParent();
if (!pendingDelete.getRightChild().isPresent()) {
pendingDelete.getLeftChild().ifPresent(
pendingDelete.isLeftChild() ? parent::attachLeftChild : parent::attachRightChild);
} else if (!pendingDelete.getLeftChild().isPresent()) {
pendingDelete.getRightChild().ifPresent(
pendingDelete.isLeftChild() ? parent::attachLeftChild : parent::attachRightChild);
} else {
BSTNode minimumNode = this.minimum(pendingDelete);
this.remove(minimumNode);
pendingDelete.getLeftChild().ifPresent(minimumNode::attachLeftChild);
pendingDelete.getRightChild().ifPresent(minimumNode::attachRightChild);
if (pendingDelete.isLeftChild()) {
parent.attachLeftChild(minimumNode);
} else {
parent.attachRightChild(minimumNode);
}
}
}
```
## BST的缺点
BST的缺点在于如果没有能够很好的规划树的拓扑结构那么BST将会失去它所有的搜索优势节点检索的复杂度也会从$O(logN)$变为$O(n)$。这种情况在大量节点缺失单侧子树时最常发生所以在使用BST时需要仔细规划树的拓扑结构或者采用自平衡二叉查找树。
## 系列文章
1. {% post_link binary-tree-basic %}
1. {% post_link binary-search-tree %}
1. {% post_link avl-tree %}