1. 程式人生 > >二叉檢索樹(BST)

二叉檢索樹(BST)

使用無序表和有序表組織的資料,不是查詢時間複雜度偏高,就是插入時間複雜度偏高,而接下來將要介紹的二叉檢索樹(BST)則能很好的解決以上問題。二叉檢索樹又稱二叉查詢樹、二叉排序樹。

BST性質

BST是滿足下面所給出條件的二叉樹:

對於二叉檢索樹的任意一個結點,設其值為K,則該結點左子樹中任意一個結點的值都小於K;該結點右子樹中任意一個結點的值都大於或等於K。

對於一組數,將這組數的兩個排列按規則插入到BST中,如果採用中序遍歷將各個結點打印出來,則會得到由小到大排列的相同序列。如下圖

BST實現

template <typename Key, typename E>
class BST : public Dictionary<Key, E>
{
private:
    BSTNode<Key, E>* root;
    int nodecount;
    
    void clearhelp(BSTNode<Key, E>*);
    BSTNode<Key, E>* inserthelp(BSTNode<Key, E>*, const Key&, const E&);
    BSTNode<Key, E>* deletemin(BSTNode<Key, E>*);
    BSTNode<Key, E>* removehelp(BSTNode<Key, E>*, const Key&);
    E findhelp(BSTNode<Key, E>*, const Key&) const;
    void printhelp(BSTNode<Key, E>*, int) const;

public:
    BST() { root = NULL; nodecount = 0; }
    ~BST() { clearhelp(root); }
    void clear() { clearhelp(root); root = NULL; nodecount = 0; }
    
    void insert(const Key& k, const E& e)
    {
        root = inserthelp(root, k, e);
        nodecount++;
    }

    E remove(const Key& k)
    {
        E temp = findhelp(root, k);
        if(temp != NULL)
        {
            root = removehelp(root, k); // 這裡有點迷啊,已經找了一次,難道還要找一次???
            nodecount--;
        }
        return temp;
    }

    E removeAny()
    {
        if(root != NULL)
        {
            E temp = root->element();
            root = removehelp(root, root->key());
            nodecount--;
            return temp;
        }
        else return NULL;
    }

    E find(const Key& k) const { return findhelp(root, k); }

    int size() { return nodecount; }
    
    void print() const
    {
        if(root == NULL) cout << "The BST is empty.\n";
        else printhelp(root, 0);
    }
};

注:本例中使用的類BSTNode和DictionaryADT的定義可以參見博主這兩篇博文二叉樹 線性表(五) 字典

輔助函式

1.查詢和插入

template <typename Key, typename E>
E BST<Key, E>::findhelp(BSTNode<Key, E>* root, const Key& k) const
{
    if(root == NULL) return NULL;
    if(k < root->key())
        return findhelp(root-left(), k);
    else if(k > root->key())
        return findhelp(root->right(), k);
    else return root->element();
}

template <typename Key, typename E>
BSTNode<Key, E>* BST<Key, E>::inserthelp(BSTNode<Key, E>* root, const Key& k, const E& it)
{
    if(root == NULL)
        return new BSTNode<Key, E>(k, it, NULL, NULL);
    if(k < root->key())
        root->setLeft(inserthelp(root->left(), k, it));
    else root->setRight(inserthelp(root->right(), k , it));
    return root;
}

2.刪除 

template <typename Key, typename E>
BSTNode<Key, E>* BST<Key, E>:: deletemin(BSTNode<Key, E>* rt)
{
    if(rt->left() == NULL)
        return rt->right();
    else
    {
        rt->setLeft(deletemin(rt->left()));
        return it;
    }
}

template <typename Key, typename E>
BSTNode<Key, E>* BST<Key, E>:: getmin(BSTNode<Key, E>* rt)
{
    if(rt->left() == NULL)
        return rt;
    else return getmin(rt->left());
}

// remove
template <typename Key, typename E>
BSTNode<Key, E>* BST<Key, E>:: removehelp(BSTNode<Key, E>* rt, const Key& k)
{
    if(rt == NULL) return NULL;
    else if(k < rt->key())
        rt->setLeft(removehelp(rt->left(), k));
    else if(k > rt->key())
        rt->setRight(removehelp(rt->right(), k));
    else
    {
        BSTNode<Key, E>* temp = rt;
        if(rt->left() == NULL)
        {
            rt = rt->right();
            delete temp;
        }
        else if(rt->right() == NULL)
        {
            rt = rt->left();
            delete temp;
        }
        else
        {
            BSTNode<Key, E>* temp = getmin(rt->right());
            rt->setElement(temp->element());
            rt->setKey(temp->key());
            rt->setRight(deletemin(rt->right()));
            delete temp;
        }
    }
    return rt;
}

刪除操作需要分類討論,被刪除的結點記為 rt

  1. 如果 rt 的左子樹為空,則只需縮短右側的樹鏈
  2. 如果 rt 的右子樹為空,則只需縮短左側的樹鏈
  3. 如果 rt 左右子樹均存在,這時我們需要考慮用一個原樹中的一個元素替換 rt,以保證BST的性質不變

針對情況3,我們的解決方案是:使用 rt 右子樹中的最小結點來替換 rt,這樣能保證左子樹的所有值都小於根結點,右子樹的所有值都大於等於根結點。而我們可以通過getmin()很方便找到右子樹中的最小結點。

 

下面再來討論一下 rt->setLeft(deletemin(rt->left()));

看上去每次退出時都要將路徑上的所有鏈重新賦值增加了時間複雜度,但實際上,不僅時間複雜度沒有增加,這種做法給程式設計提供了極大的便利性:這裡我們首先要建立一個觀念,樹的操作總是將cur指標指向根結點的,而子樹的根結點雖然還有父結點,但是根結點並不知道自己還有父結點,因為由於樹的遞迴定義,樹的操作我們總是採用遞迴來實現,遞迴的過程就是通過樹鏈上的“尋路”將我們的樹的規模不斷的縮小。

如果我們不在遞迴"出口處"將 rt 子樹的左右結點進行修改,而是在刪除結點的遞迴層進行修改的話,我們並不能知道被刪除結點 rt 的父結點,而如果我們像單鏈表一樣對於 rt 的定義進行修改的話,不但程式意圖難以理解,而且還需要增加一些特殊情況的處理程式碼,百害而無一利。

注:removehelp對於遞迴返回值和遞迴出口處的操作的設計十分重要,值得去總結學習

3.清除和列印

// postorder
template <typename Key, typename E>
void BST<Key, E>:: clearhelp(BSTNode<Key, E>* root)
{
    if(root == NULL) return;
    clearhelp(root->left());
    clearhelp(root->right());
    delete root;
}

// inorder
template <typename Key, typename E>
void BST<Key, E>:: printhelp(BSTNode<Key, E>* root, int level) const
{
    if(root == NULL) return;
    printhelp(root->left(), level+1);
    for(int i = 0; i < level; i++)
        cout << " ";
    cout << root->key() << "\n";
    printhelp(root->right(), level+1);
}