BST刪除操作

BST的刪除操作向來被認爲難度很大,因爲它不同於插入,定位到了那個該插入的位置選擇左邊/右邊進行插入即可,而刪除操作則需要分成以下三種情況進行討論,刪除難度從上到下依次遞增:

  • case1 葉子結點
  • case2 只有左子樹/只有右子樹的結點
  • case3 左子樹和右子樹都存在的結點

其實case1,case2的難度係數都不大,重難點就是在於處理case3這種情況的結點

  • case1 葉子結點直接將結點刪除(樹少了ta照樣長得很茁壯,沒有人會記得一片葉子的存在)
  • case2 要刪除的結點用一個指針進行保存,而自己跳到自己的左/右子樹上,即parent犧牲自己抱住了lchild/rchild(孩子在路中央呆若木雞,眼看着大卡車迎面而來,父親眼疾手快將孩子推向一邊,而讓冰冷的鋼鐵撞擊在自己炙熱的身軀上,代替了孩子的犧牲)
  • case3 見下文具體分析
注:以下講解的代碼思路參考於《大話數據結構》

本體代碼樹結構如下定義:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

那麼下面我們就開始進行整個代碼思路的講解
首先我們給出解BST問題的一個代碼框架,我們定義這裏刪除BST中某個結點的函數爲deleteNode(TreeNode* root, int key),其中TreeNode* root爲已經構造好了的一棵符合規範的BST,而int key則是我們即將要刪除的目標

TreeNode* deleteNode(TreeNode* root, int key) {
	if ( root == nullptr )
		return root;

	if ( key == root->val ) {
		// 定位到目標進行刪除操作...
	}
	// 若key小於當前定位到的root->val,則說明root->val應該繼續往左走纔有機會定位到key
	else if ( key < root->val )
		root->left = deleteNode(root->left, key);
	// 若key大於當前定位到的root->val,則說明root->val應該往右走纔有機會定位到當前key
    else
        root->right = deleteNode(root->right, key);
}

以上的模版在接BST題時非常有用!完美利用了BST的性質,一定要牢記於心。
剩下的便是今天我們要攻克的難題:刪除結點。

case1 葉子結點

直接進行刪除(直接使用delete刪除其實是不嚴謹,這裏我們忽略刪除的細節,具體講解整體思路部分)

if ( key == root->val ) {
	// 定位到目標進行刪除操作...
	// case1 : 葉子結點的刪除操作
	if ( root->left == nullptr && root->right == nullptr )
		delete root;
}

case2 只有左子樹/只有右子樹的結點

保存這個被刪除的結點,把孩子推向一邊,有左子樹就把孩子推到左邊,有右子樹就把孩子推到右邊

TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
	// 定位到目標進行刪除操作...
	// case1 : 葉子結點的刪除操作
	if ( root->left == nullptr && root->right == nullptr )
		delete root;
	// case2 : 只有左子樹/只有右子樹的結點
	// 重接左子樹
    if ( root->right == nullptr ) {
        p = root;
        root = root->left;
        delete p;
    }
    // 重接右子樹
    else if ( root->left == nullptr ) {
         p = root;
         root = root->right;
         delete p;
    }
}

case3 左子樹和右子樹都存在的結點

在開始講解最關鍵的這一部分之前,首先介紹一個BST中直接前驅/直接後繼的概念:
在這裏插入圖片描述
如果不能理解這個概念,那麼我給你一個建議:找一棵普通的樹,完完整整的結合中序遍歷的代碼過一遍遍歷的順序,好好的去理解中序遍歷回溯的時機。
在理解了上述概念後,直接後繼留給你們去推導。
其次需要注意的是,這裏我們所說的直接前驅/後繼是數組進行中序遍歷時遍歷到的當前結點的上一個元素!
好,接着往下講,我們又知道,BST的中序遍歷結果是一個升序數組,但是結合BST的這點性質,可以得出一個結論:BST中某個結點的前驅/後繼結點 就是 BST中序遍歷得到的已排序數組中前一個/後一個元素。
還是看上面的例子,5的直接前驅結點是4,也是中序遍歷得到的排序數組中5的前一個元素,這不是巧合!
奇怪的思路增加了:
狸貓換太子
這是什麼意思呢?不難發現:我們的直接前驅/後繼結點所屬的情況一定屬於case1和case2,即一定爲葉子結點or只有左/右子樹的結點。假設我們將要刪除的結點僞裝成其直接前驅/後繼結點,再將直接前驅/後繼結點刪除,神不知鬼不覺的“算法版狸貓換太子”豈不美哉?好,現在我們已經清晰的轉化了問題,我們來開始寫代碼:

TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
	// 定位到目標進行刪除操作...
	// case1 : 葉子結點的刪除操作
	if ( root->left == nullptr && root->right == nullptr )
		delete root;
	// case2 : 只有左子樹/只有右子樹的結點
	// 重接左子樹
    if ( root->right == nullptr ) {
        p = root;
        root = root->left;
        delete p;
    }
    // 重接右子樹
    else if ( root->left == nullptr ) {
         p = root;
         root = root->right;
         delete p;
    }
    else {
    	// case3 : 左子樹右子樹都存在的結點
		// 我們首先按僞裝成前驅結點來寫代碼,找到當前結點root的左結點的最右邊的結點	
		q = root->left;
		while ( q->right ) {
			q = q->right;
		}
		// 狸貓換太子
		root->val = q->val;
		// ...
    }
}

不知道你是否敏感,我們在狸貓換太子之後出現了問題。正如我上面所說,直接前驅結點也是分兩種情況的:i.一種是當前的結點的左結點的最右邊的結點 ii.那麼如果出現當前結點的左結點沒有右子樹,則當前結點的左結點就是當前結點的前驅結點(這段有點套娃,不理解的就去看上面我給出的圖)。所以我是如何得知這裏的前驅結點屬於哪一種情況的呢?
這裏又不得不提一個鏈表問題中常用的思想:用一個指針pre記錄之前遍歷過的位置。
我們不難發現,假設我們用一個指針p記錄指針q上一次遍歷到的結點,那麼當我對應情況 i 時,我的p結點只要繼承q結點的“遺產”就好了(下面會具體講遺產是什麼),而對應到情況 ii 的時候,q剛到達當前結點的左結點,p根本就沒有挪動的必要。不難看出,我們可以用一個指針p在不同情況會處於不同的位置上來區分兩種情況。

TreeNode* p;
TreeNode* q;
if ( key == root->val ) {
	// 定位到目標進行刪除操作...
	// case1 : 葉子結點的刪除操作
	if ( root->left == nullptr && root->right == nullptr )
		delete root;
	// case2 : 只有左子樹/只有右子樹的結點
	// 重接左子樹
    if ( root->right == nullptr ) {
        p = root;
        root = root->left;
        delete p;
    }
    // 重接右子樹
    else if ( root->left == nullptr ) {
         p = root;
         root = root->right;
         delete p;
    }
    else {
    	// case3 : 左子樹右子樹都存在的結點
		// 我們首先按僞裝成前驅結點來寫代碼,找到當前結點root的左結點的最右邊的結點
		// p指向當前結點,q指向當前結點的左結點
		p = root;	
		q = root->left;
		// 假設q有右子樹就不斷往右推進,p隨之跟進,而假設q沒有右子樹,p和q都滯留在原地
		while ( q->right ) {
			p = q;
			q = q->right;
		}
		// 狸貓換太子,不難理解,我們即將要刪除的並不是root,而是q
		root->val = q->val;
		// 情況i:直接前驅爲當前的結點的左結點的最右邊的結點
		if ( p != root )
			// 直接前驅結點是一定不會存在右子樹這一說了,已經推到了最右邊,所以左子樹是他留給p的遺產(當然如果q是個葉子結點的就是一個窮光蛋,什麼都不剩),接到p的右子樹上
			p->right = q->left;
		// 情況ii:當前結點的左結點就是當前結點的前驅結點,將遺產接到p的左子樹上
		else 
			p->left = q->left;

		delete q;
    }
}

別忘記之前我們所講的模版,這一部分只是屬於if ( key == root->val )這個代碼段的,下面附上完整代碼,我在基準情形那一塊做了點優化,即當樹中僅有一個結點且這個結點的val值就是序要刪除的值的時候直接返回nullptr:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* deleteNode(TreeNode* root, int key) {
        TreeNode* p;
        TreeNode* q;

        if ( root == nullptr || root->val == key && root->left == nullptr && root->right == nullptr )
            return nullptr;

        else {
            if ( key == root->val ) {
                if ( root->left == nullptr && root->right == nullptr )
                    delete root;

                if ( root->right == nullptr ) {
                    p = root;
                    root = root->left;
                    delete p;
                }
                
                else if ( root->left == nullptr ) {
                    p = root;
                    root = root->right;
                    delete p;
                }
                
                else {
                    p = root;
                    q = root->left;

                    while ( q->right ) {
                        p = q;
                        q = q->right;
                    }
                    
                    root->val = q->val;
                    
                    if ( p != root )
                        p->right = q->left;
                    else
                        p->left = q->left;

                    delete q;
                }
            }

            else if ( key < root->val )
                root->left = deleteNode(root->left, key);
            else
                root->right = deleteNode(root->right, key);

            return root;
        }
    }
};

LeetCode 450.Delete Node in a BST就是對BST刪除操作的考察,這個操作我真的講的很細緻了。如何檢驗自己是否聽明白了呢?沒錯,直接後繼就是留給你檢驗自己有沒有聽懂的機會,首先你要推導出直接後繼對應的是當前結點的什麼結點,其次需要分析像分析直接前驅那樣,分析直接後繼中的情況,答案我會放在Github上,獨立思考後可以進行參考o!

回顧總結

1.BST模版
2.樹的直接前驅/後繼結點
3.BST中序遍歷結果對應升序數組
4.記錄前驅結點的思想
5.利用指針位置的不同顯性化出分類討論出的結果
6.關注我

之後的博客風格可能都會像這樣了,可能不會專門的去做LeetCode刷題欄目了,會專門拆開某個算法知識點,可能有針對性的對應一些題目留給大家去思考。另外之前自己基於nodejs和github搞了個博客,由於github對國內是真的不友好,可能之後會基於gitee再創建個博客吧。各位同學如果覺得我的博客內容寫得不錯求你們給個關注8!我會努力寫出更優質的內容的!

github地址:https://github.com/18260036169/LeetCode-Tree

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