今天來詳細介紹下二叉搜索樹的一些操作以及代碼實現;
參考書籍:《算法導論第三版》(二叉搜索樹的相關章節),《數據結構與算法》
一,二叉搜索樹的定義
一顆二叉搜索樹是以一顆二叉樹來組織的,這樣的一棵樹可以使用一個鏈表數據結構來表示:
struct TreeNode
{
TreeNode* le; //需要注意,C語言中需要這樣寫:struct TreeNode* le,下同,寫慣了C++的人這裏是一個小坑
TreeNode* rh;
int value;
//有些時候會加上父親節點 TreeNode* pa;
}
現在我們有了樹的數據結構,可以給出二叉樹的定義了: 設 X是二叉搜索樹中的一個節點。如果Y是 X左子樹中的一個節點,那麼 必定有: Y.value<= X.value。 如果Y是X右子樹中的一個節點,那麼,必定有Y.value>=X.value ;
這是算法導論上的定義,翻譯一下就是對於任意一個二叉搜索樹中的節點,左子樹中的任意一個節點的值都會小於它的值,右子樹中的任意一個節點的值都會大於它的
值
二叉搜索樹本身是自排序的,看定義就明白了,一直往右是最大值,一直往左是最小值,因此二叉搜索樹可以作爲一個優先級隊列,這個以後介紹。
二叉搜索樹的基本操作所花費的時間和這個樹的高度是成正比的,對於一個有N個節點的二叉搜索樹的各種操作的平均時間是O(logn); 當極度不平衡時,即任意節點沒
有左子樹或者右子樹時,退化爲鏈表;
二,二叉搜索樹的插入操作
插入操作挺好理解的,就是從根節點開始往下走,如果插入的值大於等於該節點的值就往右子樹走,否則就往左子樹走,這裏我就直接貼代碼了,代碼中有詳細的註釋
#include<iostream>
using namespace std;
struct TreeNode
{
TreeNode* le;
TreeNode* rh;
int value;
};
void addNodeToSearchBT(TreeNode** ptrPtrRoot, int k)
{
TreeNode* pa = nullptr; //用於保存插入位置的父節點,因爲下面循環中出來的節點是nullptr
auto root = *ptrPtrRoot;
while (root != nullptr)
{
//循環遍歷比較,判斷該往左走還是往右走
pa = root;
if (k >= root->value)
root = root->rh;
else if (k < root->value)
root = root->le;
}
TreeNode* newNode = new TreeNode;
newNode->le = nullptr;
newNode->rh = nullptr;
newNode->value = k;
//這兒就是簡單的判斷大小然後插入就可以了
if (pa == nullptr)
{
*ptrPtrRoot = newNode; //二叉樹爲空
return;
}
if (k >= pa->value)
pa->rh = newNode;
else if (k < pa->value)
pa->le = newNode;
}
三,二叉搜索樹的刪除操作
這裏感覺第一次看還是比較複雜的,會詳細的介紹,一般來說都是使用被刪除節點的右子樹中的最小值作爲替換節點,所以下面就不討論使用左子樹的最大值作爲替換
節點的情況了;
首先第一步當然是找到這個要被刪除的節點,假設這個節點是D, 需要注意的是,我們這裏並沒有使用雙向鏈,所需需要保存D節點的父親節點,因爲,拼接子樹的時候
需要用到父親節點,這裏假設爲Dpa;,被用來替換的節點假設爲X;
1.如果D是葉子節點,直接刪除就OK,見圖一
2.如果D有子樹,並且只有右子樹,那也就是可以直接將D的右子樹接到Dpa上就可以,見圖二
3.如果D有子樹,並且只有左子樹,那麼直接將D的左子樹接到Dpa上就可以,見圖三
4.如果D有兩個子樹,那麼就找到右子樹中的最小值,也就是從D開始一直往左走,直到走到葉子左子樹爲空的節點,那麼我們就找到可以被用來替換的節點了,見圖四
圖一 圖二
圖三 圖四
TreeNode* findNodeInSearchBT(TreeNode*root, int k, TreeNode**pa = nullptr) { //這裏是不需要進行null判斷的,因爲後面隱式的判斷了是否爲null while (root != nullptr) { if (k > root->value) { *pa = root; root = root->rh; } else if (k < root->value) { *pa = root; root = root->le; } else break; //找到了相應的節點 } //找到了相應的節點就返回該節點,若不存在該節點的父節點則返回nullptr return root; } bool deleteNodeInSearchBT(TreeNode*root, int k) { TreeNode*PatoDelete = nullptr; //用於保存D的父節點 TreeNode* toDelete = findNodeInSearchBT(root, k, &PatoDelete); //找到D節點 //需要先判斷一下是否找到了這個節點,沒找到就直接返回false if (nullptr == toDelete) return false; //D節點沒有左子樹,或者左右子樹都沒有 第一第二中情況 if (toDelete->le == nullptr) { if (k < PatoDelete->value) PatoDelete->le = toDelete->rh; //需要把這個父節點的左節點替換爲用來替換的節點 else PatoDelete->rh = toDelete->rh; //這裏就是右子樹需要被替換了 delete toDelete; return true; } //D節點沒有右子樹第三種情況 if (toDelete->rh == nullptr) { if (k < PatoDelete->value) PatoDelete->le = toDelete->le; //需要把這個父節點的左節點替換爲用來替換的節點 else PatoDelete->rh = toDelete->le; //這裏就是右子樹需要被替換了 delete toDelete; return true; } //D節點左右子樹都有,第四種情況 auto minInRh = toDelete; TreeNode* minInRhpa = nullptr; //這裏用來保存用來替換的節點的父節點 while (minInRh->le != nullptr) { minInRhpa = minInRh; minInRh = minInRh->le; } //找到X節點,保存了其父節點 //其實這裏就是對X做了一次刪除操作,只不過這個X一定是第一種或者第二種情況 minInRhpa->le = minInRh->rh; //這裏右子樹是否非空是沒有關係的;直接對X使用第一或者第二中方法刪除,然後用X替換D //需要把用來替換的節點的左右子樹換成被刪除子樹的左右子樹,不然就把樹給弄斷了 minInRh->le = toDelete->le; minInRh->rh = toDelete->rh; //一切準備就緒,可以替換了 if (k < PatoDelete->value) PatoDelete->le = minInRh; //需要把這個父節點的左節點替換爲用來替換的節點 else PatoDelete->rh = minInRh; //這裏就是右子樹需要被替換了 //注意不要忘記釋放空間了 delete toDelete; //走到了這裏說明一切都很順利,可以返回true了 return true; }
三,遍歷操作
中序遍歷:左子節點--父節點--右子節點
void midTraverse(TreeNode*root) { if (root == nullptr) return; midTraverse(root->le); cout << root->value; midTraverse(root->rh); }
前序遍歷:父節點--左子節點--右子節點
void midTraverse(TreeNode*root)
{
if (root == nullptr) return;
cout << root->value;
midTraverse(root->le);
midTraverse(root->rh);
}
後序遍歷:左子節點--右子節點--父節點
void midTraverse(TreeNode*root)
{
if (root == nullptr) return;
midTraverse(root->le);
midTraverse(root->rh);
cout << root->value;
}