二分查找很好的解決了查找問題,將時間複雜度從 O(n)降到了O(logn)。 可是二分查找的前提條件是數據必須是有序的,而且具備線性的下標。 對於線性表,能夠很好的應用二分查找,可是在插入和刪除操做時則可能會形成整個線性表的動盪,時間複雜度達到了O(n) 鏈表更是無法應用二分查找。php
因而有了下面將要介紹的算法,其在查找、插入、刪除都可以達到O(logn)的時間複雜度 —— 二叉查找樹node
見名知意,其數據結構基礎爲二叉樹,初次接觸到二叉樹時並無感受到其有什麼突出之處。但看到經過二叉樹構建出的二叉查找樹方案時,確被深深的震撼了。git
二叉查找樹(英語:Binary Search Tree),也稱二叉搜索樹、有序二叉樹(英語:ordered binary tree),排序二叉樹(英語:sorted binary tree),是指一棵空樹或者具備下列性質的二叉樹:程序員
若任意結點的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值; 若任意結點的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值; 任意結點的左、右子樹也分別爲二叉查找樹; 沒有鍵值相等的結點。github
根據上面的規則咱們先來定義一顆二叉樹 算法
這裏能夠很容易看出其規律,不須要過多的解釋。bash
如今再插入一個元素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;
}
複製代碼
更詳細的實習細節和調用示例請參考單元測試。
因爲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這幾個數據,那麼咱們會獲得這樣一顆歪脖子樹
固然二叉查找樹依舊是各類樹的根基,還請認真理解。