Amazing tree —— 二叉查找樹

二分查找很好的解決了查找問題,將時間複雜度從 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的前驅和後繼。

爲何要使用前驅或者後綴來代替?這點我十分不肯定,我給本身的理由是

  1. 該結點是一個特殊值,屬於某顆子樹的最大值或者最小值,具備肯定性,能夠被比較好的定義且查找出來。
  2. 因爲該結點屬於被刪除節點的前驅或者後繼,則刪除該結點對數據結構形成的影響最小。我並不肯定是對什麼的數據結構形成的影響最小

上面描述的狀況的圖解以下 ↓

刪除還存在一些其餘的狀況,好比下面這種狀況↓

對於這種狀況直接將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;
}

複製代碼

更詳細的實習細節和調用示例請參考單元測試。

github.com/weiwenhao/a…

算法實現

github.com/weiwenhao/a…

補充

因爲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~

固然二叉查找樹依舊是各類樹的根基,還請認真理解。

相關文章
相關標籤/搜索