C語言詳解實(shí)現(xiàn)鏈?zhǔn)蕉鏄涞谋闅v與相關(guān)接口
前言
二叉樹的順序結(jié)構(gòu)就是用數(shù)組來存儲(chǔ),而「數(shù)組」一般只適合表示「滿二叉樹」或「完全二叉樹」,因?yàn)椴皇峭耆鏄鋾?huì)有「空間的浪費(fèi)」。
普通二叉樹的增刪查改沒有意義,主要學(xué)習(xí)它的結(jié)構(gòu),要加上搜索樹的規(guī)則,才有價(jià)值。
一、二叉樹的鏈?zhǔn)浇Y(jié)構(gòu)
在學(xué)習(xí)二叉樹的基本操作前,需先要?jiǎng)?chuàng)建一棵二叉樹,此處我們手動(dòng)快速創(chuàng)建一棵簡單的二叉樹,快速進(jìn)入二叉樹操作學(xué)習(xí),等二叉樹結(jié)構(gòu)了解的差不多時(shí),我們反過頭再來研究二叉樹真正的創(chuàng)建方式。

#include<stdio.h> // perror, printf
#include<stdlib.h> // malloc
typedef char BTDataType;
// 定義二叉樹的節(jié)點(diǎn)
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 動(dòng)態(tài)申請一個(gè)新節(jié)點(diǎn)
BTNode* BuyNode(BTDataType x)
{
//
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->data = x;
newnode->left = newnode->right = NULL;
return newnode;
}
// 二叉樹的鏈?zhǔn)浇Y(jié)構(gòu)
BTNode* CreatBinaryTree()
{
// 創(chuàng)建多個(gè)節(jié)點(diǎn)
BTNode* node_A = BuyNode('A');
BTNode* node_B = BuyNode('B');
BTNode* node_C = BuyNode('C');
BTNode* node_D = BuyNode('D');
BTNode* node_E = BuyNode('E');
BTNode* node_F = BuyNode('F');
// 用鏈來指示節(jié)點(diǎn)間的邏輯關(guān)系
node_A->left = node_B;
node_A->right = node_C;
node_B->left = node_D;
node_C->left = node_E;
node_C->right = node_F;
return node_A;
}注意:上述代碼并不是創(chuàng)建二叉樹的方式,真正創(chuàng)建二叉樹方式后續(xù)講解。
二、二叉樹的遍歷方式
1.1 遍歷方式的規(guī)則
學(xué)習(xí)二叉樹結(jié)構(gòu),最簡單的方式就是遍歷。所謂二叉樹遍歷(Traversal)是按照某種特定的規(guī)則,依次對二叉樹中的節(jié)點(diǎn)進(jìn)行相應(yīng)的操作,并且每個(gè)節(jié)點(diǎn)只操作一次。訪問結(jié)點(diǎn)所做的操作依賴于具體的應(yīng)用問題。 遍歷是二叉樹上最重要的運(yùn)算之一,也是二叉樹上進(jìn)行其它運(yùn)算的基礎(chǔ)。

按照規(guī)則,二叉樹的遍歷有:前序/中序/后序的遞歸結(jié)構(gòu)遍歷 和 層序遍歷:
- 前序遍歷(Preorder Traversal) :訪問根結(jié)點(diǎn)的操作發(fā)生在遍歷其左右子樹之前。
根 --> 左子樹 --> 右子樹
(比如上圖中,訪問的路徑為:A B D NULL NULL NULL C E NULL NULL F NULL NULL)
- 中序遍歷(Inorder Traversal) :訪問根結(jié)點(diǎn)的操作發(fā)生在遍歷其左右子樹之中(間)。
左子樹 --> 根 --> 右子樹
(比如上圖中,訪問的路徑為:NULL D NULL B NULL A NULL E NULL C NULL F NULL)
計(jì)算中序遍歷訪問路徑可以用簡單直觀的投影法:

- 后序遍歷(Postorder Traversal):訪問根結(jié)點(diǎn)的操作發(fā)生在遍歷其左右子樹之后。
左子樹 --> 右子樹 --> 根
(比如上圖中,訪問的路徑為:NULL NULL D NULL B NULL NULL E NULL NULL F C A)
- 層序遍歷(LevelOrder traversal):一層一層的走
(比如上圖中,訪問的路徑為:A B C D NULL E F NULL NULL NULL NULL NULL NULL)
由于被訪問的結(jié)點(diǎn)必是「某子樹的根」,所以N(Node)、L(Left subtree)和R(Right subtree)又可解釋為根、根的左子樹和根的右子樹。NLR、LNR和LRN分別又稱為先根遍歷、中根遍歷和后根遍歷。
深度優(yōu)先遍歷:前序、中序、后序
廣度優(yōu)先遍歷:層序
【理解前/中/后序遍歷的思路】
前中后序遍歷中,每一顆子樹都會(huì)被分為(根、左子樹、右子樹)三部分來看待,分而治之。
舉個(gè)栗子:
校長想要統(tǒng)計(jì)全校學(xué)生的人數(shù),他并不會(huì)自己挨個(gè)挨個(gè)去數(shù),而是把每個(gè)年級(jí)的負(fù)責(zé)人叫過來,各年級(jí)負(fù)責(zé)人又把各班的班主任叫過來,各班主任又把各班班長叫過來,班長統(tǒng)計(jì)人數(shù)后,大家把結(jié)果再層層上報(bào),最終傳回到校長這里,就知道學(xué)???cè)藬?shù)了。
1.2 前序遍歷
代碼是非常簡單的:
// 二叉樹前序遍歷
void PreOrder(BTNode* root)
{
if (root) // 先判斷樹是否為空
{
// 根 --> 左子樹 --> 右子樹
printf("%c ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
}
int main()
{
// 創(chuàng)建一顆鏈?zhǔn)蕉鏄?
BTNode* root = CreatBinaryTree();
// 前序遍歷
PreOrder(root); // A B D C E F
return 0;
}
前序遍歷函數(shù)遞歸調(diào)用圖解:每個(gè)函數(shù)調(diào)用,都會(huì)建立一個(gè)自己的棧幀。

前序遍歷遞歸圖解:

1.3 中序遍歷
// 二叉樹中序遍歷
void InOrder(BTNode* root)
{
if (root) // 先判斷樹是否為空
{
// 左子樹 --> 根 --> 右子樹
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
}
1.4 后序遍歷
// 二叉樹后序遍歷
void PostOrder(BTNode* root)
{
if (root) // 先判斷樹是否為空
{
// 左子樹 --> 右子樹 --> 根
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
}
1.5 層序遍歷
層序遍歷:除了先序遍歷、中序遍歷、后序遍歷外,還可以對二叉樹進(jìn)行層序遍歷。設(shè)二叉樹的根節(jié)點(diǎn)所在層數(shù)為1,層序遍歷就是從所在二叉樹的根節(jié)點(diǎn)出發(fā),首先訪問第一層的樹根節(jié)點(diǎn),然后從左到右訪問第2層上的節(jié)點(diǎn),接著是第三層的節(jié)點(diǎn),以此類推,自上而下,自左至右逐層訪問樹的結(jié)點(diǎn)的過程就是層序遍歷。

核心思路:
用一個(gè)隊(duì)列來進(jìn)行層序遍歷:
- 先入第一層的節(jié)點(diǎn)(根節(jié)點(diǎn))
- 上一層出來后,再入下一層(即它的左右孩子節(jié)點(diǎn))
比如:
先入根節(jié)點(diǎn) A
根節(jié)點(diǎn) A 出來后,再入它的孩子節(jié)點(diǎn) B 和 C
節(jié)點(diǎn) B 出來后,再入它的孩子節(jié)點(diǎn) D 和 E,節(jié)點(diǎn) C 出來后,再入它的孩子節(jié)點(diǎn) F ……

// 二叉樹的層序遍歷
void LevelOrder(BTNode* root)
{
LinkQueue q; // 鏈?zhǔn)疥?duì)列
QueueInit(&q); // 初始化隊(duì)列
// 樹的根節(jié)點(diǎn)root不為空,把根節(jié)點(diǎn)入隊(duì)
if (root)
{
QueuePush(&q, root);
}
// 當(dāng)隊(duì)列不為空時(shí),不斷的出隊(duì),以及入隊(duì)根節(jié)點(diǎn)root的左右子樹
while (!QueueEmpty(&q))
{
// 當(dāng)前樹的根節(jié)點(diǎn)出隊(duì)
BTNode* front = QueueFront(&q); // 獲取隊(duì)頭元素
printf("%c ", front->data); // 打印節(jié)點(diǎn)值
QueuePop(&q); // 出隊(duì)
// 如果當(dāng)前樹根的左右孩子不為空,則分別入隊(duì)
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q); // 銷毀隊(duì)列
}
三、二叉樹的相關(guān)接口實(shí)現(xiàn)
3.1 二叉樹節(jié)點(diǎn)個(gè)數(shù)
// 二叉樹節(jié)點(diǎn)個(gè)數(shù)
/* 方法一:
1、遞歸遍歷 -- 用全局變量/靜態(tài)局部變量來記錄節(jié)點(diǎn)個(gè)數(shù)
2、遞歸遍歷 -- 函數(shù)外定義一個(gè)局部變量記錄節(jié)點(diǎn)個(gè)數(shù),傳址給函數(shù)
*/
// 方法二:分而治之的思路
int BinaryTreeSize(BTNode* root)
{
if (root == NULL) // 1. 先判斷當(dāng)前訪問的節(jié)點(diǎn)是否為空
{
return 0;
}
// 2. 當(dāng)前節(jié)點(diǎn)不為空,節(jié)點(diǎn)個(gè)數(shù)累+1,則繼續(xù)訪問其左右子樹
return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}
3.2 二叉樹葉子節(jié)點(diǎn)個(gè)數(shù)
// 二叉樹葉子節(jié)點(diǎn)個(gè)數(shù)
/* 方法一:
1、遞歸遍歷 -- 用全局變量/靜態(tài)局部變量來記錄節(jié)點(diǎn)個(gè)數(shù)
2、遞歸遍歷 -- 函數(shù)外定義一個(gè)局部變量記錄節(jié)點(diǎn)個(gè)數(shù),傳址給函數(shù)
*/
// 方法二:分而治之的思路
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL) // 1. 先判斷當(dāng)前訪問的節(jié)點(diǎn)是否為空
{
return 0;
}
// 2. 當(dāng)前節(jié)點(diǎn)不為空,它的左右孩子都為空,說明該節(jié)點(diǎn)是葉子節(jié)點(diǎn)
if (root->left == NULL && root->right == NULL)
{
return 1;
}
// 3. 當(dāng)前節(jié)點(diǎn)不為空,左右孩子不都為空,則繼續(xù)往下遍歷
return BinaryTreeLeafSize(root->left)
+ BinaryTreeLeafSize(root->right);
}3.3 二叉樹第 k 層節(jié)點(diǎn)個(gè)數(shù)
核心思路:
- 求當(dāng)前樹第 k 層的節(jié)點(diǎn)個(gè)數(shù) = 求左子樹第 k-1 層的節(jié)點(diǎn)個(gè)數(shù) + 求右子樹第 k-1 層的節(jié)點(diǎn)個(gè)數(shù)
- 比如:
- 求當(dāng)前樹(A)第 2 層的節(jié)點(diǎn)個(gè)數(shù)
- = 求左子樹(B)第 1 層的節(jié)點(diǎn)個(gè)數(shù) + 求右子樹(C)第 1 層的節(jié)點(diǎn)個(gè)數(shù)
- = 1 + 1
- = 2
如何知道這個(gè)節(jié)點(diǎn)是不是第 k 層的?我自己復(fù)習(xí)時(shí)是用的這個(gè)思路來寫,感覺容易理解些:
求二叉樹第 k 層的節(jié)點(diǎn)個(gè)數(shù),我們從根節(jié)點(diǎn)開始往下遍歷(我在代碼中是根左右的順序),每遍歷一次 k 減 1一次,當(dāng) k == 1 時(shí),說明我們遍歷到了第 k 層,我們此時(shí)訪問該層的節(jié)點(diǎn),如果它不為空,則二叉樹第 k 層的節(jié)點(diǎn)個(gè)數(shù)就要+1。

// 二叉樹第k層節(jié)點(diǎn)個(gè)數(shù)
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL) // 1. 先判斷當(dāng)前訪問的節(jié)點(diǎn)是否為空
{
return 0;
}
if (k == 1) // 2. 當(dāng)前節(jié)點(diǎn)不為空,而k已經(jīng)減到1了,說明遍歷到了第k層,說明該節(jié)點(diǎn)是第k層的
{
return 1;
}
// 3. 還沒有遍歷到第k層,我們就繼續(xù)往下遍歷
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
3.4 二叉樹的深度(高度)
核心思想:
當(dāng)前樹的深度 = Max(左子樹的深度,右子樹的深度) + 1

- root 是空節(jié)點(diǎn):height ( root ) = 0
- root 是非空節(jié)點(diǎn):height ( root ) = max ( height ( root->left ), height ( root->right ) ) + 1
// 二叉樹的深度(高度)
int BinaryTreeDepth(BTNode* root)
{
// 1. 先判斷當(dāng)前樹的根節(jié)點(diǎn)是否為空
if (root == NULL)
{
return 0;
}
// 2. 當(dāng)前樹的根節(jié)點(diǎn)不為空,分別計(jì)算其左右子樹的深度
int leftDepth = BinaryTreeDepth(root->left);
int rightDepth = BinaryTreeDepth(root->right);
// 3. 比較當(dāng)前樹左右子樹的深度,最大的那個(gè)+1 就是當(dāng)前樹的深度
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
有一道OJ題考到了該算法,鏈接如下:二叉樹的最大深度
3.5 二叉樹查找值為 x 的節(jié)點(diǎn)
核心思路:
先判斷是不是當(dāng)前節(jié)點(diǎn),是就返回,不是就先去該節(jié)點(diǎn)的左子樹找,找到了就返回,左子樹沒找到,再去該節(jié)點(diǎn)的右子樹找。
// 二叉樹查找值為x的節(jié)點(diǎn),若有則返回該節(jié)點(diǎn)的地址,若沒有則返回NULL
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL) // 1. 先判斷當(dāng)前訪問的節(jié)點(diǎn)是否為空
{
return NULL;
}
if (root->data == x) // 2. 判斷要找的x值節(jié)點(diǎn)是不是當(dāng)前節(jié)點(diǎn)
{
return root;
}
// 3. 不是當(dāng)前節(jié)點(diǎn),則繼續(xù)去該節(jié)點(diǎn)的左子樹中找
BTNode* ret = BinaryTreeFind(root->left, x);
if (ret != NULL)
{
return ret; // 找到了返回地址
}
// 3. 還沒找到,再繼續(xù)去該節(jié)點(diǎn)的右子樹中找
ret = BinaryTreeFind(root->right, x);
if (ret != NULL)
{
return ret; // 找到了返回地址
}
// 4. 當(dāng)前節(jié)點(diǎn)及其左右子樹中都沒找到,返回NULL
return NULL;
}3.6 總結(jié) & 注意
二叉樹相關(guān)的算法,如果用的是遞歸遍歷,且代碼中需要一個(gè)變量在整個(gè)遞歸過程中去記錄什么信息,一定要注意,不要把這個(gè)變量直接定義成了局部變量。(因?yàn)槊看芜f歸調(diào)用,都會(huì)建立一個(gè)棧幀,各棧幀中的局部變量是彼此獨(dú)立的)
所以需要下面這樣做:
1、遞歸遍歷 – 用全局變量/靜態(tài)局部變量來記錄節(jié)點(diǎn)個(gè)數(shù)
2、遞歸遍歷 – 函數(shù)外定義一個(gè)局部變量記錄節(jié)點(diǎn)個(gè)數(shù),傳址給函數(shù)
四、二叉樹的創(chuàng)建和銷毀
4.1 通過前序遍歷的字符串來構(gòu)建二叉樹
// 通過前序遍歷的字符串?dāng)?shù)組arr "ABD##E#H##CF##G##" 構(gòu)建二叉樹 BTNode* BinaryTreeCreate(BTDataType* arr, int size, int* pi);
4.2 二叉樹銷毀
// 二叉樹銷毀
// 一級(jí)指針(頭節(jié)點(diǎn)指針),形參是實(shí)參的一份拷貝,函數(shù)內(nèi)改變形參的值,無法改變外部實(shí)參的值
// 所以我們需要在函數(shù)外置頭節(jié)點(diǎn)指針為NULL
void BinaryTreeDestroy(BTNode* root)
{
// 不建議使用前中序遍歷銷毀,如果節(jié)點(diǎn)先被銷毀,就變成隨機(jī)值了,不知道它的左右子樹位置了
// 所以我們采用后序遍歷銷毀
if (root)
{
BinaryTreeDestroy(root->left);
BinaryTreeDestroy(root->right);
free(root);
}
}
int main()
{
// 創(chuàng)建一顆鏈?zhǔn)蕉鏄?
BTNode* root = CreatBinaryTree();
// 銷毀二叉樹
BinaryTreeDestroy(root);
// 頭節(jié)點(diǎn)指針置NULL
root = NULL;
return 0;
}4.3 判斷二叉樹是否是完全二叉樹
核心思路:
層序遍歷時(shí),把空節(jié)點(diǎn)也入隊(duì)列
- 完全二叉樹,「非空節(jié)點(diǎn)」是連續(xù)的,則「空節(jié)點(diǎn)」是連續(xù)的。
- 非完全二叉樹,「非空節(jié)點(diǎn)」不是連續(xù)的,則「空節(jié)點(diǎn)」不是連續(xù)的。
所以在出隊(duì)時(shí),判斷一下,出到第一個(gè)「空節(jié)點(diǎn)」時(shí),跳出循環(huán);
在下面重新寫一個(gè)循環(huán)繼續(xù)出隊(duì),并檢查出隊(duì)元素:
- 如果「第一個(gè)空節(jié)點(diǎn)」后面的全是「空節(jié)點(diǎn)」,說明是完全二叉樹
- 如果「第一個(gè)空節(jié)點(diǎn)」后面的有「非空節(jié)點(diǎn)」,說明是非完全二叉樹

// 判斷二叉樹是否是完全二叉樹(利用層序遍歷的思想來判斷)
bool BinaryTreeComplete(BTNode* root)
{
LinkQueue q; // 鏈?zhǔn)疥?duì)列
QueueInit(&q); // 初始化隊(duì)列
// 樹的根節(jié)點(diǎn)root不為空,把根節(jié)點(diǎn)入隊(duì)
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
// 當(dāng)前樹的根節(jié)點(diǎn)出隊(duì)
BTNode* front = QueueFront(&q); // 獲取隊(duì)頭元素
QueuePop(&q); // 出隊(duì)
// @@@ 出隊(duì)的節(jié)點(diǎn)中,出到第一個(gè)空節(jié)點(diǎn)時(shí),跳出循環(huán) @@@
if (front == NULL)
{
break;
}
// 不管當(dāng)前樹根的左右孩子是否為空,都分別入隊(duì)
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// @@@ 出隊(duì)的節(jié)點(diǎn)中,出到第一個(gè)空節(jié)點(diǎn)時(shí),跳出上面循環(huán) @@@
// 在這里繼續(xù)出隊(duì):
// 1、如果隊(duì)列中全是空節(jié)點(diǎn),則是完全二叉樹
// 2、如果隊(duì)列中有非空節(jié)點(diǎn),則是非完全二叉樹
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q); // 獲取隊(duì)頭元素
QueuePop(&q); // 出隊(duì)
// @@@ 出隊(duì)的節(jié)點(diǎn)中,如果出現(xiàn)非空節(jié)點(diǎn),說明是非完全二叉樹 @@@
if (front)
{
QueueDestroy(&q); // 銷毀隊(duì)列
return false;
}
}
QueueDestroy(&q); // 銷毀隊(duì)列
// @@@ 出隊(duì)的節(jié)點(diǎn)中,如果沒有出現(xiàn)非空節(jié)點(diǎn),說明是完全二叉樹 @@@
return true;
}到此這篇關(guān)于C語言詳解實(shí)現(xiàn)鏈?zhǔn)蕉鏄涞谋闅v與相關(guān)接口的文章就介紹到這了,更多相關(guān)C語言鏈?zhǔn)蕉鏄浔闅v內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode數(shù)組練習(xí)題
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode的幾道數(shù)組練習(xí)題,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
C++?select模型簡單聊天室的實(shí)現(xiàn)示例
本文主要介紹了C++?select模型簡單聊天室的實(shí)現(xiàn)示例,使用CMake項(xiàng)目進(jìn)行開發(fā),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
C++設(shè)計(jì)模式編程中使用Bridge橋接模式的完全攻略
這篇文章主要介紹了C++設(shè)計(jì)模式編程中使用Bridge橋接模式的完全攻略,Bridge將抽象部分與它的實(shí)現(xiàn)部分分離,使它們都可以獨(dú)立地變化需要的朋友可以參考下2016-03-03
Qt利用QJson實(shí)現(xiàn)解析數(shù)組的示例詳解
這篇文章主要為大家詳細(xì)介紹了Qt如何利用QJson實(shí)現(xiàn)解析數(shù)組功能,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Qt有一定幫助,需要的小伙伴可以了解一下2022-10-10
C++實(shí)例分析講解臨時(shí)對象與右值引用的用法
對性能來說,許多的問題都需要和出現(xiàn)頻率及本身執(zhí)行一次的開銷掛鉤,有些問題雖然看似比較開銷較大,但是很少會(huì)執(zhí)行到,那也不會(huì)對程序有大的影響;同樣一個(gè)很小開銷的函數(shù)執(zhí)行很頻繁,同樣會(huì)對程序的執(zhí)行效率有很大影響。本章中作者主要根據(jù)臨時(shí)對象來闡述這樣一個(gè)觀點(diǎn)2022-08-08

