數據結構之二叉搜索樹詳解(附C++代碼實現查找、插入、刪除操作)

  最近在分析分析紅黑樹時,感覺上來就挑最難的樹結構之一進行分析,難度太大,所以特意寫這篇二叉搜索樹分析的博客作爲鋪墊。那麼爲啥挑二叉搜索樹進行分析捏?其實紅黑樹也是一種更爲複雜的二叉搜索樹,建議閱讀一下我的另外一篇博客 數據結構之二叉樹、AVL樹、紅黑樹、Trie樹、B樹、B+樹、B*樹淺析 。爲了幫助大家理解紅黑樹,先寫這篇博客分析二叉搜索樹。下面將主要分析二叉搜索樹的查找、插入、刪除三種操作,並且附上C++代碼實現。

一、二叉搜索樹簡述

  二叉搜索樹大致定義爲二叉樹的左子樹任意節點的值小於根節點的值,右子樹任意節點的值大於根節點的值,並且左子樹、右子樹同樣也符合二叉搜索樹的定義(遞歸定義)。中序遍歷順序爲左根右,所以二叉搜索樹的典型特徵是中序遍歷序列有序。
在這裏插入圖片描述

二、二叉搜索樹相關操作

  爲了能讓大家更好的理解二叉搜索樹,將提供C++的編碼實現。下面是TreeNode結構體實現:

/**
 * 這裏額外添加了parent指針,主要是爲了訪問父節點方便
 */
struct TreeNode {
    int value;
    // 三個指針分別指向父節點、左子樹根節點、右子樹根節點
    TreeNode *parent;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int value) : value(value), parent(NULL), left(NULL), right(NULL) {}
    TreeNode(int value, TreeNode *parent, TreeNode *left, TreeNode *right) {
        this->value = value;
        this->parent = parent;
        this->left = left;
        this->right =right;
    }
};

\color{red}溫馨提示:查找插入刪除節點三個操作的複雜度依次增加,如果覺得有點壓力,請按照順序依次閱讀,穩紮穩打。

1、二叉搜索樹中節點的查找

  二叉搜索樹設計成左 < 根 < 右(中序遍歷有序),一個很重要的動機就是快速查找。有過一點算法基礎的應該能想到有一種搜索策略非常相似,沒錯就是二分搜索,每次將target與搜索區間(遞增有序)的中間值mid比較,如果target < mid則縮小搜索區間爲[left, mid - 1],如果target > mid則縮小搜索區間爲[mid + 1, right],否則target == mid。辣麼再來看看二叉搜索樹中查找的僞代碼。

root 指向二叉搜索樹的根,target爲需要搜索的值
while (root != NULL) {
	if root->value == target
		成功搜索到了 return
	else if root->value > target
		// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
		// 既然root->value > target,那麼只可能出現在左子樹,轉移root到左子樹
		root = root->left
	else
		// 否則root->value < target
		// 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
		// 既然root->value < target,那麼只可能出現在右子樹,轉移root到右子樹
		root = root->right
}
// root == NULL,二叉搜索樹中沒有target

C++代碼實現:

// 在二叉搜索樹中查找target,存在返回對應的指針,否則返NULL
TreeNode *searchNode(TreeNode *root, int target){
    while (root != NULL) {
        if (root->value == target) {
            break;
        }
        else if (root->value > target) {
            // 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
            // 既然root->value > target,那麼只可能出現在左子樹,轉移root到左子樹
            root = root->left;
        }
        else {
            // 否則root->value < target
            // 注意二叉搜索樹的特徵:root的右子樹比root->value都大,root的左子樹比root->value都小
            // 既然root->value < target,那麼只可能出現在右子樹,轉移root到右子樹
            root = root->right;
        }
    }
    return root;
}

2、二叉搜索樹中節點的插入

  二叉搜索樹中查找充分利用左 < 根 < 右特性,辣麼插入也能用上這個特性麼?答案是顯然的。
  首先思考一下,我們插入節點後是不是還需要維持二叉樹仍然滿足二叉搜索樹特性,這是必須的,要不能你的二叉搜索樹就變成一次性的了。辣麼我們就先要找到它真實應該插入的位置,保證中序遍歷爲遞增有序。下面是刪除的僞代碼。

root 指向二叉搜索樹的根,value爲需要插入的值
if root == NULL
	// 二叉搜索樹爲空,插入的節點即是根節點
	root = new TreeNode(value)
else
	// 我們需要找到插入的位置
	TreeNode *ptr = root;
	while (true) {
		if ptr->value == value
			// 樹中已經存在這個value,不進行插入(這裏簡化邏輯)
			break
		else if ptr->value > value
			// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
			// 既然ptr->value > value,value只能插入ptr左子樹
			if ptr->left == NULL
				// 如果ptr左子樹爲空,則插入的節點正好做左子樹的根
				ptr->left = new TreeNode(value)
				break
			else
				// 否則轉移到左子樹,繼續查找
				ptr = ptr->left
		else
			// 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
			// 既然ptr->value < value,value只能插入ptr右子樹
			if ptr->right == NULL
				// 如果ptr右子樹爲空,則插入的節點正好做右子樹的根
				ptr->right = new TreeNode(value)
				break
			else
				// 否則轉移到右子樹,繼續查找
				ptr = ptr->right
	}
return root

C++代碼實現:

// 在二叉搜索樹中插入value,如果二叉樹中已經存在則不進行插入(簡化處理邏輯)
TreeNode *searchNode(TreeNode *root, int value) {
    if (root == NULL) {
        // 二叉搜索樹爲空,插入的節點即是根節點
        root = new TreeNode(value);
    }
    else {
        // 我們需要找到插入的位置
        TreeNode *ptr = root;
        while (true) {
            if (ptr->value == value) {
                // 樹中已經存在這個value,不進行插入(這裏簡化邏輯)
                break;
            }
            else if (ptr->value > value) {
                // 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
                // 既然ptr->value > value,value只能插入ptr左子樹
                if (ptr->left == NULL) {
                    // 如果ptr左子樹爲空,則插入的節點正好做左子樹的根
                    ptr->left = new TreeNode(value);
                    break;
                }
                else {
                    // 否則轉移到左子樹,繼續查找
                    ptr = ptr->left;
                }
            }
            else {
                // 注意二叉搜索樹的特徵:ptr的右子樹比ptr->value都大,ptr的左子樹比ptr->value都小
                // 既然ptr->value < value,value只能插入ptr右子樹
                if (ptr->right == NULL) {
                    // 如果ptr右子樹爲空,則插入的節點正好做右子樹的根
                    ptr->right = new TreeNode(value);
                    break;
                }
                else {
                    // 否則轉移到右子樹,繼續查找
                    ptr = ptr->right;
                }
            }
        }
    }
    return root;
}

3、二叉搜索樹中節點的刪除

  在二叉搜索樹刪除節點,首先我們應該在樹中查找到這個節點的位置吧,然後將其移除,並且移除後我們需要進行調整,使其任然滿足二叉搜索樹。這個刪除操作可以分成好幾種情況,需要分別討論。

①、刪除葉節點

  刪除葉節點,只要將其移除即可,不需要進行任何調整操作。
在這裏插入圖片描述

②、刪除非葉節點

  刪除非葉節點可以細分爲兩種,第一種是刪除有右子樹的節點,刪除節點後需要將右子樹中序遍歷第一個節點填充到刪除節點A位置(爲什麼選右子樹中序遍歷第一個節點?因爲整棵樹的中序遍歷序列中,節點A的下一個節點就是其右子樹中序遍歷的第一個節點)。
  右子樹中序遍歷第一個節點爲某個節點的左子節點,直接將最左的左子節點填補到已刪除節點的位置。
在這裏插入圖片描述

a、刪除節點有右子樹

  右子樹中序遍歷序列中的第一個節點爲某個沒有左子節點的節點B。將節點B填入已刪除節點的位置,並且將節點B的右子樹置於節點B的位置。
右子樹

b、刪除節點沒有右子樹

  第二種是刪除沒有右子樹的節點A,此時尋找整顆二叉樹中序遍歷中節點A的下一個節點,稍微複雜一點,需要利用parent指針。找到遠祖父節點B,並且使得節點A在遠祖父節點B的左子樹中!遠祖父節點B.value替換到節點A.value後,它自己也需要它的下一個節點來填充。
在這裏插入圖片描述
  \color{red}備註:其實根本不需要進行刪除操作,只要尋找到這個節點中序遍歷序列的下一個節點,然後直接替換即可。
C++代碼實現:

// 在二叉搜索樹中插入value,如果二叉樹中已經存在則不進行插入(簡化處理邏輯)
TreeNode *deleteNode(TreeNode *root, TreeNode *targetPtr) {
    if (targetPtr == NULL || root == NULL) {
        return root;
    }
    if (root == targetPtr && root->right == NULL) {
        // 一、刪除的是根節點,並且根節點沒有右子樹
        // 處理:切斷targetPtr與左子樹的關聯,返回左子樹,釋放targetPtr
        root = root->left;
        if (root != NULL) {
            root->parent = NULL;
        }
        targetPtr->left = NULL;
        delete targetPtr;
    }
    else if (targetPtr->left == targetPtr->right) {
        // 二、刪除的葉節點
        // targetPtr->left == targetPtr->right,只能是同時爲NULL
        // 操作:切斷parent與targetPtr的關聯,返回root,釋放targetPtr
        if (targetPtr->parent->left == targetPtr) {
            targetPtr->parent->left = NULL;
        } else {
            targetPtr->parent->right = NULL;
        }
        // 切斷targetPtr 與targetPtr->parent的關聯
        targetPtr->parent = NULL;
        delete targetPtr;
    } else if (targetPtr->right == NULL) {
        // 三、刪除節點右子樹爲空      需要找到遠祖父節點
        TreeNode *pParent = targetPtr;
        // 祖父節點B,並且使得節點targetPtr在遠祖父節點B的左子樹
        while (pParent->parent != NULL && pParent->parent->right == pParent) {
            pParent = pParent->parent;
        }
        if (pParent->parent == NULL) {
            // 如果targetPtr不存在一個遠祖父節點B,使得leftPtr在遠祖父B的左子樹
            // 操作:只能刪除這個節點,並且把左子樹放到當前節點的位置
            targetPtr->parent->right = targetPtr->left;
            targetPtr->left->parent = targetPtr->parent;
            // 切斷targetPtr與parent、left的關係
            targetPtr->parent = NULL;
            targetPtr->left = NULL;
            delete targetPtr;
        }
        else {
            // 否則pParent->parent->value替換targetPtr->value,再刪除pParent->parent(遞歸)
            targetPtr->value = pParent->parent->value;
            root = deleteNode(root, pParent->parent);
        }
    }
    else {
        // 四、刪除節點存在右子樹,直接在右子樹尋找中序遍歷的第一個節點
        TreeNode * leftPtr = targetPtr->right;
        // 一直往left尋找
        while (leftPtr->left != NULL) {
            leftPtr = leftPtr->left;
        }
        // 將leftPtr->value替換到targetPtr->value
        targetPtr->value = leftPtr->value;
        // targetPtr->right就是leftPtr
        if (targetPtr->right == leftPtr) {
            // 將targetPtr->right指向leftPtr右子樹
            targetPtr->right = leftPtr->right;
            // 如果targetPtr->right != NULL,還需要設置parent
            if (leftPtr->right != NULL) {
                leftPtr->right->parent = targetPtr;
            }
        }
        else {
            // 否則leftPtr->parent與leftPtr切斷關係
            leftPtr->parent->left = NULL;
        }
        // 將leftPtr於其parent切斷關係並釋放
        leftPtr->parent = NULL;
        delete leftPtr;
    }
    return root;
}

TreeNode *deleteNodeByValue(TreeNode *root, int value) {
    // 首先查找value所在的位置
    TreeNode *targetPtr = searchNode(root, value);
    if (targetPtr == NULL) {
        // value都沒找到還刪除啥...
        return root;
    }
    return deleteNode(root, targetPtr);
}

三、思考與總結

  可以看出二叉搜索樹的查找、插入還是比較簡單的,刪除稍微複雜一點。不過二叉搜索樹可能存在退化成鏈表的缺陷,比如給你一個本來遞增有序的序列讓你插入到一顆空二叉搜索樹中,這時就退化成鏈表了。
在這裏插入圖片描述
  因此我們需要將二叉搜索樹增加平衡的特性,即AVL樹紅黑樹。後面有時間會更新一篇紅黑樹的博客,敬請期待~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章