二叉搜索樹
這道題目使用二叉搜索樹實現,並且都要用到插入結點和查找結點的基操。更多基礎內容可以查看博客——樹表查找。
結構體定義
typedef struct TNode
{
int data;
struct TNode* left, * right;
} TNode, * BinTree;
插入操作
二叉搜索樹的插入本質上是查找操作,時間復雜度在 O(㏒2n) ~ O(n) 之間,這要根據樹的形態而定。
void Insert(BinTree& BST, int num)
{
if (BST == NULL) //找到插入位置,插入結點
{
BST = new TNode;
BST->data = num;
BST->left = NULL;
BST->right = NULL;
}
else
{
if (num < BST->data)
{
Insert(BST->left, num);
}
else if (num > BST->data) //注意不要漏條件
{
Insert(BST->right, num);
}
}
}
查找操作
bool Find(BinTree BST, int num)
{
bool flag = true;
while (BST != NULL && BST->data != num) //查找直到成功或失敗
{
if (num < BST->data)
{
BST = BST->left;
}
else
{
BST = BST->right;
}
}
if (BST == NULL)
{
flag = false;
}
return flag;
}
是否完全二叉搜索樹
測試樣例 1
輸入樣例
9
38 45 42 24 58 30 67 12 51
輸出樣例
38 45 24 58 42 30 12 67 51
YES
測試樣例 2
輸入樣例
8
38 24 12 45 58 67 42 51
輸出樣例
38 45 24 58 42 12 67 51
NO
題目分析
這道題目可以被分為 2 部分,分別是建立二叉搜索樹和判斷是否是完全二叉樹。首先是建立二叉搜索樹,這個操作並不難,只需要使用上文給出的建樹函數,循環調用插入函數就行。值得注意的是這道題左子樹是較大的關鍵字,右子樹是較小的關鍵字。
接下來就是判斷是否是完全二叉樹,首先我們先回憶一下什么是完全二叉樹。我使用通俗的話來說,所謂完全二叉樹就是生成結點的順序是嚴格按照從上到下,從左往右的順序來構建的二叉樹。例如對於題設測試樣例 1 所建立的二叉搜索樹,我把它展開為拓展二叉樹的形式:
若按照“從上到下,從左到右”的順序去讀這個二叉樹,會發現空結點只會集中出現在末尾部分。下面再看測試樣例 2:
從定義上講,這不是個完全二叉樹,若展開成拓展二叉樹的形式,按照“從上到下,從左到右”的順序去讀這個二叉樹,會發現有個空結點穿插在了結點之間。
也就是說要判斷一個二叉樹是否是完全二叉樹,可以先展開為拓展二叉樹,然后按照“從上到下,從左到右”的順序遍歷這個二叉樹,若在所有實際存在的結點遍歷完畢之前遇到了空結點,就說明這不是完全二叉樹。如何實現“從上到下,從左到右”的順序遍歷?這就是所謂的層序遍歷法,需要通過一個隊列結構來輔助實現。對於二叉樹的相關概念和操作,可以前往博客——二叉樹結構詳解進行回顧。
總體的思路已經很明確了,接下來就是如何體現中間遇到了空結點?在層序遍歷中我們可以直接忽略空結點,不讓空結點入隊列,但是這里必須用拓展二叉樹的思想讓空結點入隊列,這樣我們才能確定是否有空結點的出現。但是如果是這樣的話,可以在空結點入隊列時判斷不是完全二叉樹嗎?也不行,因為這么操作在最后會有一系列空結點入隊列。
再觀察一下完全二叉樹的特點,我們就會明白了,若二叉樹是完全二叉樹,那么遇到空結點之前入隊列的結點數就會和二叉搜索樹中的結點數相等。此時我們可以另外定義一個變量來統計結點數,當遇到空結點入隊列時就停止統計,在層序遍歷結束后返回這個變量。若返回的結點數和實際結點數相同,說明是完全二叉樹,否則就不是,這樣就能同時實現層序遍歷和完全二叉樹的判定了。
主函數 main()
int main()
{
BinTree T = NULL;
int fre; //查找次數
int num;
int count; //遇到 NULL 前遍歷的結點數
cin >> fre;
for (int i = 0; i < fre; i++) //建樹
{
cin >> num;
Insert(T, num);
}
//PreOrderTraverse(T); //前序遍歷檢查建樹是否正確
count = levelOrder(T);
if (count == fre) //若返回的結點數和實際結點數相同,說明是完全二叉樹
{
cout << "\nYES";
}
else //否則不是
{
cout << "\nNO";
}
return 0;
}
層序遍歷函數 levelOrder(BinTree t)
偽代碼
由於需要把所有結點都過一遍,因此時間復雜度 O(n)。
代碼實現
int levelOrder(BinTree t) //層序遍歷並判斷完全二叉樹
{
BinTree ptr;
queue<BinTree> que_level; //層序結點隊列
int flag = 0; //是否有 NULL 入隊列的 flag
int count = 0; //統計遇到 NULL 之前的結點數
if (t == NULL) //空樹處理
{
cout << "NULL";
}
que_level.push(t); //根結點入隊列
while (!que_level.empty()) //直至空隊列,結束循環
{
if (que_level.front() == NULL) //隊列讀取到空結點
{
flag = 1; //修改 flag 表示接下來不再統計結點數
}
else //隊列頭結點非空
{
if (count == 0)
{
cout << que_level.front()->data;
}
else
{
cout << " " << que_level.front()->data;
}
if (flag == 0)
{
count++; //統計結點數
}
que_level.push(que_level.front()->left); //左結點入隊列
que_level.push(que_level.front()->right); //右結點入隊列
}
que_level.pop(); //隊列頭出隊列
}
return count;
}
調試遇到的問題
這道題雖然是一次過了,但是調試時遇到的問題很多。
Q1:層序遍歷操作得出的結點序列,與測試樣例差別很大,順序混亂。
A1:按照層次分開,發現每一層的結點都是逆序的,重新讀題發現題目要求左子樹是較大的關鍵字,右子樹是較小的關鍵字。因此通過修改結點插入函數的判斷條件,就能得到正確的序列。
Q2:判定完全二叉樹時,發現無論什么情況判斷為是。
A2:因為沒有按照拓展二叉樹去寫,空結點並不會入隊列,而我的判斷語句是在出隊列時發揮作用的,這就導致了我無法進行任何判斷。修改方式為,遍歷到了空結點也入隊列。
Q3:修改好 Q3 后,發現無論什么情況判斷為否。
A3:我的判斷語句是根據是否是空結點來判斷的,但是用拓展二叉樹的思想讓空結點入隊列,操作在最后會有一系列空結點入隊列,這就導致了無論如何都有空結點的出現。這就說明我的判斷條件寫錯了,或者判斷機制得重新設計。最后的解法是另外定義一個變量來統計結點數,當遇到空結點入隊列時就停止統計,在層序遍歷結束后返回這個變量。若返回的結點數和實際結點數相同,說明是完全二叉樹,否則就不是。
知識總結
- 二叉搜索樹的基操,這道題的前提條件就是建出二叉搜索樹,沒有這一步后面的所有都免談。這就需要熟悉二叉搜索樹的建立方式,二叉搜索樹的建立基礎是插入數據,而插入數據的本質是查找,雖然是基礎操作,但是也可以加深對二叉搜索樹的理解。
- 層序遍歷法,這個操作是屬於二叉樹遍歷法之一。層序遍歷就好像從根結點開始,一層一層向下擴散搜索,這就跟我們隊列實現迷宮算法非常類似,因為迷宮算法的不同路徑也是無關聯的,但是我們是用廣度優先搜索的思想可以找到最短路徑。層序遍歷需要結合隊列結構協同操作,在這里有熟悉了這個遍歷手法。
- 完全二叉樹的性質,完全二叉樹的概念不好理解,但是用“從上到下,從左到右”這個順序就會變得形象。在這里對完全二叉樹的判斷提出要求,這就需要理解其特點和性質,同時這也是堆結構的基礎,在這里加深理解是很必要的。
- 輔助變量的使用,在這里我使用了 count 變量順手判斷了是否是完全二叉樹。這個變量的設計,不僅是從需求和問題出發,更是結合了細化的知識點,可見細致的分析對問題的解決而言極為重要。