樹型結構
前言
樹型結構在生活中是非常常見的一種結構,應用範圍很廣,就用一個簡單的例子來說。計算機中的文件目錄就是一個樹型結構,一般創建一個文件,如果文件中沒有文件那麼就相當於一個空樹,如果裏面有文件,就相當於這個文件的子樹,以此類推,就形成了樹型結構的文件目錄。
在學習中我們主要學習二叉樹的一些特性,把樹細化學習。
二叉樹的概念
二叉樹是結點的有限集合,該集合或者爲空,或者由一個
根節點加上兩顆對稱的左子樹和右子樹的二叉樹組成。
二叉樹的每個結點上最多有兩個子樹
二叉樹有五種形態,及只有根結點,空樹,只有左子樹,只有右子樹,左右子樹都有。
二叉樹的分類
二叉樹有完全二叉樹和滿二叉樹兩種
- 滿二叉樹是所有分支節點都存在左右子樹,並且葉子結點在同一層上。
完全二叉樹是具有N個結點的二叉樹的結構與滿二叉樹前N個結點的結構相同
怎麼理解呢?
左圖爲滿二叉樹,有圖爲完全二叉樹。二叉樹性質
二叉樹最大結點數:若規定只有一個根結點的時候,深度爲1,那麼深度爲K的樹,最大結點數爲 (2^K) -1 (K>=0)
二叉樹的第i層上的結點數:若規定只有一個根結點的時候,層數爲1,那麼一顆非空樹的第i層最多有 2^(i-1) (i>0)
葉子結點和非葉子結點的關係:對於任何一顆二叉樹,如果葉子結點個數爲N0,度爲2的非葉子結點個數爲N1,則有 N1 = N0 + 1;
關於完全二叉樹的深度:具有N結點的二叉樹的的深度K爲log2(n+1)上取整數
順序存出中父結點和子結點的關係:子結點要找到對應的父結點,(i - 1)/2;父找到左孩子i*2 + 1;找到右孩子i*2 + 1;
二叉樹的創建
創建一顆二叉樹,以順序表,通過先序遍歷的方式還原一顆二叉樹(順序表中必須有標識NULL的特殊字符存在)
具體思路是採用遞歸的方式,用一個index來記錄樹創建過程中創建到數組中的哪個位置,遞歸的用先序遍歷的方式去創建。
具體代碼:// 創建一個結點 TreeNode* CreateTreeNode(TreeType value) { TreeNode* new_node = (TreeNode*)malloc(sizeof(TreeNode)); // 用assert來判斷new_node空間是否開闢成功。 // 在這裏我們直接讓程序掛掉。(不同場景處理方式不同) assert(new_node); new_node->data = value; new_node->lchild = NULL; new_node->rchild = NULL; return new_node; } // 二叉樹遞歸體,遞歸的建立一顆二叉樹 TreeNode* _CreateTree(TreeType arr[], size_t size, int* index, TreeType nulltype) { if (index == NULL || size < 1) { return NULL; } if (arr[*index] == nulltype) { return NULL; } // 先創建根結點 TreeNode* root = Create Tree Node(TreeType value); ++(*index); // 遞歸的創建左子樹 TreeNode* root->lchild = _CreateTree(arr, size, index, nulltype); ++(*index); // 遞歸的創建右子樹 TreeNode* root->rchild = _CreatTree(arr,size, index, nulltype); return root; } // 函數主體 TreeNode* CreateTree(TreeType arr[], size_t size, TreeType nulltype) { if (size < 1) { return NULL; } // 用來記錄創建到數組中的哪個元素上 int index = 0; return _CreateTree(arr, size, &index, nulltype); }
二叉樹的遍歷
創建好二叉樹後,我們訪問二叉樹中的某個結點,就必須經過遍歷。
我們實現一下,二叉樹的遞歸版的前、中、後序遍歷和非遞歸版的前、中、後遍歷。
遞歸版
先序遍歷(前序遍歷)
void PreOrder(TreeNode* root) { if (root == NULL) { return; } // 先打印根結點 printf("%c ", root->data); // 遞歸的打印左右子樹 PreOrder(root->lchild); PreOrder(root->rchild); }
中序遍歷
void InOrder(TreeNode* root) { if (root == NULL) { return; } // 先遞歸的找到最左邊的孩子 InOrder(root->lchild); // 打印最左邊的孩子,遞歸棧出棧打印根結點 printf("%c ", root->data); // 遞歸進右子樹 InOrder(root->rchild); }
後續遍歷
void PostOrder(TreeNode* root) { if (root == NULL) { return; } // 先遞歸,找最左的孩子,打印,然後遞歸棧出棧,再進行最左子樹的右子樹進行遞歸 PreOrder(root->lchild); PreOrder(root->rchild); printf("%c ", root->data); }
非遞歸版
注意:這裏我們在寫非遞歸版,直接用順序棧的函數接口,不做棧的實現。
如果不知道棧的實現,戳這裏棧的實現前序遍歷
前序遍歷非遞歸採用手動棧來進行操作,棧的特性是先入後出,取棧頂元素,所以我們抓住這兩個特性。 前序遍歷,是根左右順序遍歷。
1)先讓根結點入棧,取棧頂元素打印,成功進(2),失敗退出循環。
2)進行出棧
3)再進行入右孩子,入左孩子。
就這樣循環,直到棧爲空,也就是取棧頂失敗。遍歷完成。
代碼如下:void PreOrederByLoop(TreeNode* root) { if (root == NULL) { return; } // 創建一個棧。 SeqStack stack; // 初始化一個棧(用C語言實現的棧) InitStack(&stack); // 先入根結點 PushStack(&stack, root); TreeNode* top = NULL; while (FindTopStack(&stack, &top) { printf("%c ", top->data); PopStack(&stack); // 先入右孩子,後入左孩子 if (top->rchild != NULL) { PushStack(&stack, top->rchild); } if (top-lchild != NULL) { PushStack(&stack, top->lchild); } } }
中序遍歷
中序遍歷,是採用手動棧。將函數放入一個while(1)的循環中。
1)先循環的去從根結點到最左孩子的入棧。
2)進行取棧頂元素,並判斷是否取棧頂元素成功,
3)如果失敗break,退出while(1)訓話。如果成功,打印棧頂元素,並出棧
4)進行右孩子的判斷是否爲空,如果不爲空,進行讓循環指針指向取棧頂元素的右孩子。
代碼:
void InOrderByLoop(TreeNode* root)
{
if (root == NULL)
{
// 非法輸入
return;
}
SeqStack Stack;
InitStack(&stack);
TreeNode* cur = root;
while (1)
{
// 從根結點到最左邊的孩子,並逐一入棧
while (cur != NULL)
{
PushStack(&stack, cur);
cur = cur->lchild;
}
// 取棧頂元素
TreeNode* top = NULL;
FindTopStack(&stack, &top);
if (top == NULL)
{
// 取失敗,退出循環。因爲因爲已經遍歷完了。
break;
}
// 打印並出棧。
printf("%c ", top->data);
PopStack(&stack);
// 讓cur嘗試的去找最左孩子的右孩子。
cur = top->rchild;
}
}
- 後序遍歷
後序遍歷,藉助手動創建的棧,採用while(1)循環作爲大的循環條件。
1)循環的從根結點到最左孩子,並且逐一入棧。
2)取棧頂元素。如果取失敗就break;
3)判斷是否存在最左孩子是否存在右孩子。還要判斷右孩子是否等於上一個取棧頂的元素,這樣做是防止重複遍歷。
4)如果不存在右孩子或者右孩子不等於上次取棧頂元素,那麼可以打印並且出棧。
5)否則就讓cur 指向棧頂元素的右孩子
代碼:
void PostOrder(TreeNode* root)
{
if (root == NULL)
{
// 非法輸入
return;
}
SeqStack satck;
InitStack(&stack);
// 用來記錄上一個top的元素
TreeNode* pre = NULL;
TreeNode* cur = root;
while (1)
{
// 循環從根到最左孩子的入棧
while (cur != NULL)
{
PushStack(&stack, cur);
cur = cur->lchild;
}
// 取棧頂元素
TreeNode* top = NULL;
FindTopStack(&stack, &top);
if (top == NULL)
{
// 取失敗退出,說明遍歷結束
break;
}
// 滿足個兩個條件中的一個就可以打印並且出棧。
if (top->rchild == NULL || top->rchild == pre)
{
printf("%c ", top->data);
PopStack(&stack);
pre = top;
}
else
{
// 存在右孩子,繼續入右孩子。
cur = top->rchild;
}
}
}
- 層序遍歷
對於層序遍歷,也是一個很重要的點,需要掌握。層序遍歷就是,從上到下,從左到右依次的進行遍歷。那麼前面我們說的,前中後序遍歷,都是藉助棧的先進後出,可以訪問棧頂元素的特點來進行的遍歷。然而對於層序遍歷用隊列的先進先出,訪問隊首的方式遍歷。對隊列不熟悉的可以戳這裏鏈式隊列的實現,循環順序隊列的實現
1)先讓根結點入隊。
2)以取隊守元素爲條件進行循環
3)打印並出隊
4)判斷是否左孩子爲空,不爲空就讓左孩子入隊,
5)再判斷右孩子是否爲空,不爲空就讓右孩子入隊
代碼:
void LevelOrder(TreeNode* root)
{
if (root == NULL)
{
// 非法判斷
return;
}
SeqQueue queue;
InitQueue(&queue);
PushQueue(&queue, root);
// 取棧頂元素,並且取隊守元素是否爲空。
TreeNode* head = NULL;
while (FindHead(&stack, &head))
{
// 打印並且出隊
printf("%c ", head->data);
PopQueue(&queue);
if (head->lchild != NULL)
{
PushQueue(&queue, head->lchild);
}
if (head->rchild != NULL)
{
PushQueue(&queue, head->rchild);
}
}
}
判斷是否是完全二叉樹
判斷是否是完全二叉樹,更多的是在層序遍歷的基礎上變化而來,那麼我們也需要藉助隊這個數據結構來進行。判斷完全二叉樹的依據就是在滿二叉樹的下一層,從左到右,要麼只有左孩子,要麼左右都有,不能只有右沒有左。
1)我們需要創建隊列,進行初始化,將root結點入隊
2)取隊首元素,判斷是否爲NULL,如果爲NULL,直接break。
3)出隊。入左孩子和右孩子。
4)循環判斷隊列是否大於0
如果是,就讓原來的隊列出隊,並且取隊首元素,判斷是否爲空,是就返回0。不是就繼續
如果不是就返回1表示是完全二叉樹
代碼實現:int IsCompleteTree(TreeNode* root) { if (root == NULL) { // 非法判斷 return 0; } SeqQueue queue; InitQueue(&queue); // 用來取隊首元素 TreeNode* head = NULL; PushQueue(&queue, root); while (SizeQueue(&queue) > 0) { FindHeadQueue(&queue, head); if (head == NULL) { break; } PopQueue(&queue); PushQueue(&queue, head->lchild); PushQueue(&queue, head->rchild); } while (SizeQueue(&queue) > 0) { // 當上面循環跳出,進入本循環,隊首元素爲NULL先出隊後取 PopQueue(&queue); FindHeadQueue(&queue, &head); if (head != NULL) { reuturn 0; } } return 1; }
以上爲二叉樹基礎知識整理。