前言:本篇文章只是記錄王爭的數據結構與算法之美的學習筆記,寫下來能強迫自己系統的再過一遍,加深理解。這門課
以實際開發中遇到的問題為例
,引入解決問題涉及到的的數據結構和算法,但不會講的太細,最好結合一本實體書進行學習。
二叉查找樹是一種
特殊的二叉樹
,支持動態數據集合
的快速插入、刪除、查找操作。
1. 二叉查找(搜索)樹
特點:
- 任意節點的
左子樹
中節點的值,都要小于
這個節點的值 - 任意節點的
右子樹
中節點的值,都要大于
這個節點的值
如下圖所示:
image.jpeg
2. 二叉查找樹的操作
2.1 查找操作
具體操作:
- 先取根節點
- 如果它等于要查找的數據,就返回
- 如果它比要查找的數據大,就從左子樹中繼續查找
- 如果它比要查找的數據小,就從右子樹中繼續查找
如下圖所示:
image.jpeg
代碼也很好理解,如下圖所示:
public class BinarySearchTree {
private Node tree;
public Node find(int data) {
Node p = tree;
while (p != null) {
if (data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
public static class Node {
private int data;
private Node left;
private Node right;
public Node(int data) {
this.data = data;
}
}
}
2.2 插入操作
新插入的數據是在葉子節點
上,所以要從根節點開始,依次比較要插入的數據和節點的大小關系。
- 如果要插入的數據
比節點的數據大
,并且節點的右子樹為空,就插入到右子節點的位置;如果不為空,則遍歷右子樹,查找插入的位置 - 如果插入的數據
比節點數據小
,并且節點的左子樹為空,就將數據插入到左子節點的位置;如果不為空,則遍歷左子樹,查找插入位置
image.jpeg
代碼如下:
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;
} else { // data < p.data
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
2.3 刪除操作
需要分三種情況去處理:
- 如果要刪除的節點沒有子節點,只需要將其父節點中
指向要刪除節點的指針置為 null
- 如果要刪除的節點只有一個子節點,只需要將父節點中指向要刪除節點的指針,
指向要刪除節點的唯一的子節點
- 如果要刪除的節點有兩個子節點,需要找到這個節點的
右子樹中的最小節點
,把它替換到要刪除的節點上,然后再刪除掉這個最小節點,因為最小節點肯定沒有左子節點
如下圖:
image.jpeg
代碼如下:
public void delete(int data) {
Node p = tree; // p指向要刪除的節點,初始化指向根節點
Node pp = null; // pp記錄的是p的父節點
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return; // 沒有找到
// 要刪除的節點有兩個子節點
if (p.left != null && p.right != null) { // 查找右子樹中最小節點
Node minP = p.right;
Node minPP = p; // minPP表示minP的父節點
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 將minP的數據替換到p中
p = minP; // 下面就變成了刪除minP了
pp = minPP;
}
// 刪除節點是葉子節點或者僅有一個子節點
Node child; // p的子節點
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child;
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
2.4 其他操作
比如:
- 快速查找最大節點和最小節點
- 查找前驅節點和后繼節點
中序遍歷
二叉查找樹,可以輸出有序的數據序列,時間復雜度是 O(n),所以又稱為二叉排序樹
。
3. 支持重復數據的二叉查找樹
實際應用中,二叉查找樹中存儲的,是一個包含很多字段的對象。可以利用對象的某個字段值(key)來構建二叉查找樹,對象中其他字段叫作衛星數據
。
對于包含相同鍵值
的數據的二叉查找樹,有兩種解決方法:
- 可以通過鏈表或者支持動態擴容的數組等結構,將值相同的數據都
存儲在同一個節點
上 - 每個節點仍然只存儲一個節點,在插入時,將要插入的數據,放到相同數據節點的右子樹,把這個新插入的數據當作大于這個節點的值來處理。
image.jpeg
在查找數據的時候,遇到值相同的節點之后,繼續在其右子樹中查找,直到遇到葉子節點,才停止。這樣就可以把鍵值等于要查找值的所有節點都找出來。
image.png
對于刪除操作,我們也需要先查找到每個要刪除的節點,然后再按前面的刪除操作的方法,依次刪除。
image.jpeg
image.png
4. 時間復雜度分析
對于不同類型
的二叉查找樹,執行效率是不同的,比如下圖:
image.jpeg
比如第一種,已經退化成鏈表了,查找的時間復雜度就是 O(n)了。
最理想的情況就是二叉查找樹是一棵完全二叉樹
或者滿二叉樹
,插入、刪除、查找的時間復雜度都是跟樹的高度成正比,也就是 O(height)
。
樹的高度 height = 最大層數 - 1
,完全二叉樹的層數小于等于 log2n + 1
,也就是完全二叉樹的高度小于等于 log2n。平衡二叉查找樹的高度接近logn
,所以插入、刪除、查找操作的時間復雜度也比較穩定,是O(logn)
。
5. 有了散列表之后為啥還需要二叉查找樹?
- 散列表中的數據是
無序
的,輸出時需要先排序,對于二叉查找樹,只需要中序遍歷
即可 - 散列表
擴容耗時
,遇到散列沖突時,性能不穩定,平衡二叉查找樹的性能非常穩定
,時間復雜度穩定在 O(logn)。 - 散列表涉及因素較多
- 散列表裝載因子不能太大,會浪費內存
6. 練習
- 二叉查找樹的插入、刪除、查找操作(包含相同數據)
- 獲取二叉樹的高度
- 快速查找最大節點和最小節點
- 查找前驅節點和后繼節點