hello-algo/docs/chapter_tree/binary_search_tree.md
2023-10-08 01:22:57 +08:00

337 lines
10 KiB
Markdown
Executable file
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.

# 二叉搜索树
如下图所示,「二叉搜索树 binary search tree」满足以下条件。
1. 对于根节点,左子树中所有节点的值 $<$ 根节点的值 $<$ 右子树中所有节点的值。
2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 `1.`
![二叉搜索树](binary_search_tree.assets/binary_search_tree.png)
## 二叉搜索树的操作
我们将二叉搜索树封装为一个类 `ArrayBinaryTree` ,并声明一个成员变量 `root` ,指向树的根节点。
### 查找节点
给定目标节点值 `num` ,可以根据二叉搜索树的性质来查找。如下图所示,我们声明一个节点 `cur` ,从二叉树的根节点 `root` 出发,循环比较节点值 `cur.val``num` 之间的大小关系。
-`cur.val < num` ,说明目标节点在 `cur` 的右子树中,因此执行 `cur = cur.right`
-`cur.val > num` ,说明目标节点在 `cur` 的左子树中,因此执行 `cur = cur.left`
-`cur.val = num` ,说明找到目标节点,跳出循环并返回该节点。
=== "<1>"
![二叉搜索树查找节点示例](binary_search_tree.assets/bst_search_step1.png)
=== "<2>"
![bst_search_step2](binary_search_tree.assets/bst_search_step2.png)
=== "<3>"
![bst_search_step3](binary_search_tree.assets/bst_search_step3.png)
=== "<4>"
![bst_search_step4](binary_search_tree.assets/bst_search_step4.png)
二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。
=== "Python"
```python title="binary_search_tree.py"
[class]{BinarySearchTree}-[func]{search}
```
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{search}
```
=== "Java"
```java title="binary_search_tree.java"
[class]{BinarySearchTree}-[func]{search}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{search}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{search}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{search}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{search}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{search}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{search}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{search}
```
=== "C"
```c title="binary_search_tree.c"
[class]{binarySearchTree}-[func]{search}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{search}
```
### 插入节点
给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根节点 < 右子树的性质插入操作流程如下图所示
1. **查找插入位置**与查找操作相似从根节点出发根据当前节点值和 `num` 的大小关系循环向下搜索直到越过叶节点遍历至 $\text{None}$ 时跳出循环
2. **在该位置插入节点**初始化节点 `num` 将该节点置于 $\text{None}$ 的位置
![在二叉搜索树中插入节点](binary_search_tree.assets/bst_insert.png)
在代码实现中需要注意以下两点
- 二叉搜索树不允许存在重复节点否则将违反其定义因此若待插入节点在树中已存在则不执行插入直接返回
- 为了实现插入节点我们需要借助节点 `pre` 保存上一轮循环的节点这样在遍历至 $\text{None}$ 我们可以获取到其父节点从而完成节点插入操作
=== "Python"
```python title="binary_search_tree.py"
[class]{BinarySearchTree}-[func]{insert}
```
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Java"
```java title="binary_search_tree.java"
[class]{BinarySearchTree}-[func]{insert}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{insert}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{insert}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{insert}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{insert}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{insert}
```
=== "C"
```c title="binary_search_tree.c"
[class]{binarySearchTree}-[func]{insert}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{insert}
```
与查找节点相同插入节点使用 $O(\log n)$ 时间
### 删除节点
先在二叉树中查找到目标节点再将其从二叉树中删除
与插入节点类似我们需要保证在删除操作完成后二叉搜索树的左子树 < 根节点 < 右子树的性质仍然满足
因此我们需要根据目标节点的子节点数量共分为 01 2 这三种情况执行对应的删除节点操作
如下图所示当待删除节点的度为 $0$ 表示该节点是叶节点可以直接删除
![在二叉搜索树中删除节点(度为 0 ](binary_search_tree.assets/bst_remove_case1.png)
如下图所示当待删除节点的度为 $1$ 将待删除节点替换为其子节点即可
![在二叉搜索树中删除节点(度为 1 ](binary_search_tree.assets/bst_remove_case2.png)
当待删除节点的度为 $2$ 我们无法直接删除它而需要使用一个节点替换该节点由于要保持二叉搜索树 $<$ $<$ 的性质**因此这个节点可以是右子树的最小节点或左子树的最大节点**。
假设我们选择右子树的最小节点即中序遍历的下一个节点则删除操作流程如下图所示
1. 找到待删除节点在中序遍历序列中的下一个节点记为 `tmp`
2. `tmp` 的值覆盖待删除节点的值并在树中递归删除节点 `tmp`
=== "<1>"
![在二叉搜索树中删除节点(度为 2 ](binary_search_tree.assets/bst_remove_case3_step1.png)
=== "<2>"
![bst_remove_case3_step2](binary_search_tree.assets/bst_remove_case3_step2.png)
=== "<3>"
![bst_remove_case3_step3](binary_search_tree.assets/bst_remove_case3_step3.png)
=== "<4>"
![bst_remove_case3_step4](binary_search_tree.assets/bst_remove_case3_step4.png)
删除节点操作同样使用 $O(\log n)$ 时间,其中查找待删除节点需要 $O(\log n)$ 时间,获取中序遍历后继节点需要 $O(\log n)$ 时间。
=== "Python"
```python title="binary_search_tree.py"
[class]{BinarySearchTree}-[func]{remove}
```
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Java"
```java title="binary_search_tree.java"
[class]{BinarySearchTree}-[func]{remove}
```
=== "C#"
```csharp title="binary_search_tree.cs"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Go"
```go title="binary_search_tree.go"
[class]{binarySearchTree}-[func]{remove}
```
=== "Swift"
```swift title="binary_search_tree.swift"
[class]{BinarySearchTree}-[func]{remove}
```
=== "JS"
```javascript title="binary_search_tree.js"
[class]{BinarySearchTree}-[func]{remove}
```
=== "TS"
```typescript title="binary_search_tree.ts"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Dart"
```dart title="binary_search_tree.dart"
[class]{BinarySearchTree}-[func]{remove}
```
=== "Rust"
```rust title="binary_search_tree.rs"
[class]{BinarySearchTree}-[func]{remove}
```
=== "C"
```c title="binary_search_tree.c"
[class]{binarySearchTree}-[func]{removeNode}
```
=== "Zig"
```zig title="binary_search_tree.zig"
[class]{BinarySearchTree}-[func]{remove}
```
### 中序遍历有序
如下图所示,二叉树的中序遍历遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历顺序,而二叉搜索树满足“左子节点 $<$ 根节点 $<$ 右子节点”的大小关系。
这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:**二叉搜索树的中序遍历序列是升序的**。
利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,无须进行额外的排序操作,非常高效。
![二叉搜索树的中序遍历序列](binary_search_tree.assets/bst_inorder_traversal.png)
## 二叉搜索树的效率
给定一组数据,我们考虑使用数组或二叉搜索树存储。观察下表,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。
<p align="center"><id> &nbsp; 数组与搜索树的效率对比 </p>
| | 无序数组 | 二叉搜索树 |
| -------- | -------- | ----------- |
| 查找元素 | $O(n)$ | $O(\log n)$ |
| 插入元素 | $O(1)$ | $O(\log n)$ |
| 删除元素 | $O(n)$ | $O(\log n)$ |
在理想情况下,二叉搜索树是“平衡”的,这样就可以在 $\log n$ 轮循环内查找任意节点。
然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为下图所示的链表,这时各种操作的时间复杂度也会退化为 $O(n)$ 。
![二叉搜索树的退化](binary_search_tree.assets/bst_degradation.png)
## 二叉搜索树常见应用
- 用作系统中的多级索引,实现高效的查找、插入、删除操作。
- 作为某些搜索算法的底层数据结构。
- 用于存储数据流,以保持其有序状态。