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!我會努力寫出更優質的內容的!