二分查詢很好的解決了查詢問題,將時間複雜度從 O(n)降到了O(logn)。
但是二分查詢的前提條件是資料必須是有序的,並且具有線性的下標。
對於線性表,可以很好的應用二分查詢,但是在插入和刪除操作時則可能會造成整個線性表的動盪,時間複雜度達到了O(n)
連結串列更是沒法應用二分查詢。
於是有了下面將要介紹的演算法,其在查詢、插入、刪除都能夠達到O(logn)的時間複雜度 —— 二叉查詢樹
見名知意,其資料結構基礎為二叉樹,初次接觸到二叉樹時並沒有感覺到其有什麼突出之處。但看到通過二叉樹構建出的二叉查詢樹方案時,確被深深的震撼了。
定義
二叉查詢樹(英語:Binary Search Tree),也稱二叉搜尋樹、有序二叉樹(英語:ordered binary tree),排序二叉樹(英語:sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
若任意結點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
若任意結點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值;
任意結點的左、右子樹也分別為二叉查詢樹;
沒有鍵值相等的結點。
根據上面的規則我們先來定義一顆二叉樹
這裡可以很容易看出其規律,不需要過多的解釋。
插入
現在再插入一個元素13。 13>12所以往右邊走來到14,13 < 14則左走,發現14沒有左孩子,所以將13插入之,得到下面這張圖
查詢
按照上面插入的思路,可以很容易實現搜尋操作。並且發現其查詢的時間複雜度就為這顆樹的深度。
根據完全二叉樹的性質,具有n個結點的完全二叉樹的深度為
[logn] + 1
忽略掉+1
得到二叉查詢樹的查詢時間複雜度為 O(logn)
,但是實際上並非如此,後面我們分析。
遍歷
二叉樹的遍歷有前序、中序、後序遍歷三種方式,這裡著重介紹後序遍歷。
對二差查詢樹進行中序遍歷時,可以得到一個asc
的排序結果。如上面的樹中序遍歷的結果是 3, 8, 9, 12, 13, 14。中序遍歷從一顆子樹最左的節點開始輸出,既該樹的最小值
。實現中序遍歷只需要將資料收集點置於左遞迴點與右遞迴點之間,這樣說還是有些含糊了,看程式碼吧
/**
* 中序遍歷
* @param $root
* @return array
*/
public function inorder($root)
{
$data = [];
if ($root->left) {
$data = array_merge($data, $this->inorder($root->left)); //左孩子遞迴點
}
$data[] = $root->data; // 這裡是中序遍歷的資料收集點
if ($root->right) {
$data = array_merge($data, $this->inorder($root->right)); // 右孩子遞迴點
}
return $data;
}
前驅與後繼, 以9節點為例, 12屬於9的後繼,8屬於9的前驅。
刪除
我們給這顆樹多加幾個結點
刪除樹中的結點分為很多種情況,如被刪除的結點不存在子結點,只存在左子樹/右子樹,左右子樹都存在,這裡已覆蓋率最廣的左右子樹都存在為例。
分析一個需求時要並不是需求存在多少中情況我們就寫多少種情況。而應該分析情況之間的關係,是否存在重複,或者屬於關係等,程式設計師應該做的就是提取需求的本質,力求於最簡潔的實現
現在我們打算刪除25這個結點,你會怎麼做?
如果只是簡單把18來頂替原來25的位置,則需要對18這顆子樹的孩子們進行重新調整。18只有三個孩子還好,但是當孩子成千上萬時,顯然會造成大面積的調整。
所以我希望能夠找到一個更好的節點來代替25,按照演算法導論中的描述,我們應該尋找該結點的前驅或者後繼來代替,比如圖中的24和27分別是25的前驅和後繼。
為什麼要使用前驅或者字尾來代替?這點我十分不確定,我給自己的理由是
- 該結點是一個特殊值,屬於某顆子樹的最大值或者最小值,具有確定性,可以被比較好的定義且查詢出來。
- 由於該結點屬於被刪除節點的前驅或者後繼,則刪除該結點對資料結構造成的影響最小。我並不確定是對什麼的資料結構造成的影響最小
上面描述的情況的圖解如下 ↓
刪除還存在一些其他的情況,比如下面這種情況↓
對於這種情況直接將30提升到25即可,接下來看一下看php的程式碼實現:
public function delete($root, $data)
{
if (!$root) {
return null;
}
if ($root->data === $data) {
if ($root->left) {
// 左轉
$node = $root->left;
$parent = $root;
$toward = 'left';
while ($node->right) {
$parent = $node;
$toward = 'right';
$node = $node->right;
}
$root->data = $node->data;
$parent->{$toward} = $this->delete($node, $node->data);
} else {
return $root->right;
}
} elseif ($root->data > $data) {
// 如果root的左孩子沒有被刪除,那就原樣返回回來, 如果被刪除了,那就找個孩子代替
$root->left = $this->delete($root->left, $data);
} else {
$root->right = $this->delete($root->right, $data);
}
return $root;
}
由於php有記憶體回收機制,因此我們沒有辦法像c一樣直接去修改記憶體,所以這裡藉助遞迴的特性來解決這個問題 $root->left = $this->delete($root->left, $data);
做類似這樣一個處理,這可能會有些理解上的困難。但總歸還是能夠明白的~
除了遞迴解決外,也可以用下面這種辦法。
即定義一個parent和toward來做一個導向,這在上面的程式碼中也有體現。該方法更加適用於迭代處理
$parent = $root;
$toward = 'left';
while ($node->right) {
$parent = $node;
$toward = 'right';
$node = $node->right;
}
更詳細的實習細節和呼叫示例請參考單元測試。
https://github.com/weiwenhao/algorithm/blo...
演算法實現
https://github.com/weiwenhao/algorithm/blo...
補充
由於php沒有像js一樣的字面量物件或者c一樣的struct。因此直接使用物件來表示樹中的結點
class BiTNode
{
public $data;
public $left;
public $right;
public function __construct($data, $left = null, $right = null)
{
$this->data = $data;
$this->left = $left;
$this->right = $right;
}
}
在查詢的時候指出了,二叉查詢樹的查詢的時間複雜度並不是嚴格意義上的O(logn) 是因為有這樣的情況發生, 假設需要插入 12, 10, 9, 5, 4, 1這幾個資料,那麼我們會得到這樣一顆歪脖子樹
此時的時間複雜度儼然已經變成了O(n),不過對於這樣的問題自然已經有解決方案。下一節將會在AVL樹和紅黑樹這兩種解決方案中選一種來BB~
當然二叉查詢樹依舊是各種樹的根基,還請認真理解。
本作品採用《CC 協議》,轉載必須註明作者和本文連結