智能指針和二叉樹(3):圖解查找和刪除

前兩篇文章中咱們詳細介紹了使用智能指針構建二叉樹並進行了層序遍歷。html

如今咱們已經掌握了足夠的前置知識,能夠深刻了解二叉搜索樹的查找和刪除了。node

本文索引

二叉搜索樹的查找

查找將分爲兩部分,最值查找和特定值查找。算法

本章中使用的二叉搜索樹的結構和上一篇文章中的相同。函數

下面咱們先來看看最值查找。性能

查找最小值和最大值

這是最簡單的一種查找。測試

根據二叉搜索樹的性質,左子樹的值都比根節點小,右子樹的值都比根節點大,且這一性質對根節點下任意的左子樹或右子樹都適用。this

根據以上的性質,對於一棵二叉搜索樹來講,最小的值的節點必定在左子樹上,且是最左邊的一個節點;同理最大值必定是右子樹上最右邊的那個節點,如圖所示:3d

查找的算法也極爲簡單,只要不停遞歸搜索左子樹/右子樹,而後將左邊或右邊的葉子節點返回,這就是最小值/最大值:指針

NodeType BinaryTreeNode::max()
{
    // 沒有右子樹時根節點就是最大的
    if (!right) {
        return shared_from_this();
    }

    auto child = right;
    while (child) {
        if (child->right) {
            child = child->right;
        } else {
            return child;
        }
    }

    return nullptr;
}

NodeType BinaryTreeNode::min()
{
    // 沒有左子樹時根節點就是最小的
    if (!left) {
        return shared_from_this();
    }

    auto child = left;
    while (child) {
        if (child->left) {
            child = child->left;
        } else {
            return child;
        }
    }

    return nullptr;
}

這裏咱們用循環替代了遞歸,使用遞歸的實現將會更簡潔,讀者能夠本身留做聯繫。code

查找特定值

查找特定值的狀況較最值要複雜一些,由於須要判斷以下幾種狀況,假設咱們查找的值是value

  1. value和當前節點的值相等,查找完成返回當前節點
  2. value小於當前節點的值,繼續所搜左子樹,左子樹的值都比當前節點小
  3. value大於當前節點的值,繼續所搜右子樹,右子樹的值都比當前節點大
  4. 當前節點沒有左/右子樹而須要繼續搜索子樹時,查找失敗value在樹中不存在,返回nullptr

此次咱們決定採用遞歸實現,基於上述描述使用遞歸實現更簡單,若是有興趣的話也能夠用循環實現,雖然二者在性能上的表現並不會相差太多(由於遞歸查找的次數只有log2(N)+1次,次數較少沒法充分體現循環帶來的性能優點):

NodeType BinaryTreeNode::search(int value)
{
    if (value == value_) {
        return shared_from_this();
    }

    // 繼續向下搜索
    if (value < value_ && left) {
        return left->search(value);
    } else if (value > value_ && right) {
        return right->search(value);
    }

    // 未找到value
    return nullptr;
}

刪除節點

查找算法雖然分了兩部分,但和刪除節點相比仍是比較簡單的。

一般咱們刪除一棵樹的某個節點時,將其子節點轉移給本身的parent便可,然而二叉搜索樹須要本身的每一部分都遵照二叉樹搜索樹的性質,所以對於大部分狀況來講直接將子節點交給parent將會致使二叉搜索樹被破壞,因此咱們須要對以下幾個狀況分類討論:

  1. 狀況a:待刪除節點沒有任何子節點,此節點是葉子節點,這時能夠直接刪除它
  2. 狀況b:待刪除節點只有左/右子樹,這時直接刪除節點,將子節點交給parent便可,不會影響二叉搜索樹的性質
  3. 狀況c:待刪除節點同時擁有左右子樹,這時爲了刪除節點後還是一棵二叉搜索樹,有兩個待選方案:
  • 選擇待刪除節點的左子樹的最大值,和待刪除節點交換值,而後將這個左子樹的最大節點刪除,由於左子樹的值都須要比根節點小,所以刪除根節點時從左子樹中找到最大值交換到根節點的位置,便可保證知足二叉搜索樹的性質;接着對左子樹最大節點作相同的分類討論,最後通過交換後節點會知足前兩種中的一種狀況,這是刪除這個節點,整個刪除過程便可完成
  • 原理同上一種,只不過咱們選擇了右子樹中的最小值的節點

只有描述會比較抽象,所以每種狀況咱們來看圖:

狀況a:

紅色虛線的部分即爲待刪除節點,這是直接刪除便可。

狀況b:

如圖所示,當只存在一邊的子樹時,直接刪除節點,將子節點交給parent便可。

狀況c較爲複雜,咱們舉例選擇右子樹最小值的狀況,另外一種狀況是類似的:

圖中黃色虛線部分就是「待刪除節點」,加引號是由於咱們並不真正刪除它,而是先要把它的值和右子樹的最小值也就是紅色虛線部分交換:

交換後咱們刪除右子樹的最小值節點,這是它知足狀況a,所以直接被刪除,刪除後的樹還是一棵二叉搜索樹:

這裏解釋下爲何須要交換,首先交換是把狀況c儘可能往狀況a或b轉化簡化了問題,同時保證了二叉搜索樹的性質;其次若是不進行交換,則須要大量移動節點,性能較差且實現極爲複雜,所以咱們纔會選擇交換節點值的作法。

咱們的代碼也會根據上述狀況進行分類討論,此次咱們使用遞歸實現來簡化代碼,一樣讀者若是有興趣能夠研究下循環版本:

// 公開的接口,方便用戶調用,具體實如今私有方法remove_node中
void BinaryTreeNode::remove(int value)
{
    auto node = search(value);
    if (!node) {
        return;
    }

    node->remove_node();
}

// 刪除節點的具體實現
void BinaryTreeNode::remove_node()
{
    // parent是weak_ptr,須要檢查是否可訪問
    auto p{parent.lock()};
    if (!p) {
        return;
    }

    // 狀況a,這時判斷節點在parent的左側仍是右側
    // 隨後對正確的parent子節點賦值nullptr,當前節點會在函數返回後自動被釋放
    if (!left && !right) {
        if (value_ > p->value_) {
            p->right = nullptr;
        } else {
            p->left = nullptr;
        }

        return;
    }

    // 狀況c,選擇和右子樹最小值交換
    if (left && right) {
        auto target = right->min();
        target->remove_node();
        // 這裏和圖解有一點小小的不一樣
        // 刪除target前改變了value_,會致使target被刪除時沒法正確確認本身是在parent的左側仍是右側
        // 因此只能在target刪除結束後再將值賦值給當前節點
        value_ = target->value_;
        return;
    }

    // 下面是狀況b的兩種可能的形式
    // 只存在左子樹
    if (left) {
        if (value_ > p->value_) {
            p->right = left;
        } else {
            p->left = left;
        }
        left->parent = p;
        return;
    }

    // 只存在右子樹
    if (right) {
        if (value_ > p->value_) {
            p->right = right;
        } else {
            p->left = right;
        }
        right->parent = p;
        return;
    }
}

進行分類討論後代碼實現起來也就沒有那麼複雜了。

測試

如今該測試上面的代碼了:

int main()
{
    auto root = std::make_shared<BinaryTreeNode>(3);
    root->insert(1);
    root->insert(0);
    root->insert(2);
    root->insert(5);
    root->insert(4);
    root->insert(6);
    root->insert(7);
    root->layer_print();
    std::cout << "max: " << root->max()->value_ << std::endl;
    std::cout << "min: " << root->min()->value_ << std::endl;
    root->remove(1);
    // 刪除後是否仍是二叉搜索樹使用中序遍歷便可得知
    std::cout << "after remove 1\n";
    root->ldr();
    root->insert(1);
    root->remove(5);
    std::cout << "after remove 5\n";
    root->ldr();
}

結果:

如圖,二叉搜索樹的中序遍歷結果是一個有序的序列,兩次元素的刪除後中序遍歷的結果都爲有序序列,算法是正確的。

相關文章
相關標籤/搜索