這個系列是我多年前找工作時對資料結構和演算法總結,其中有基礎部分,也有各大公司的經典的面試題,最早釋出在CSDN。現整理為一個系列給需要的朋友參考,如有錯誤,歡迎指正。本系列完整程式碼地址在 這裡。
0 概述
在說二叉樹前,先來看看什麼是樹。樹中基本單位是結點,結點之間的連結,稱為分支。一棵樹最上面的結點稱之為根節點,而下面的結點為子結點。一個結點可以有0個或多個子結點,沒有子結點的結點我們稱之為葉結點。
二叉樹是指子結點數目不超過2個的樹,它是一種很經典的資料結構。而二叉搜尋樹(BST)是有序的二叉樹,BST需要滿足如下條件:
- 若任意結點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
- 若任意結點的右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值;(有些書裡面定義為BST不能有相同值結點,本文將相同值結點插入到右子樹)
- 任意結點的左、右子樹也分別為二叉查詢樹;
本文接下來會從定義,二叉搜尋樹的增刪查以及二叉樹的遞迴和非遞迴遍歷進行整理。 下一篇文章會對二叉樹相關的經典面試題進行全面解析,本文程式碼在 這裡。
1 定義
我們先定義一個二叉樹的結點,如下:
typedef struct BTNode {
int value;
struct BTNode *left;
struct BTNode *right;
} BTNode;
複製程式碼
其中 value
儲存值,left
和 right
指標分別指向左右子結點。二叉搜尋樹跟二叉樹可以使用同一個結構,只是在插入或者查詢時會有不同。
2 基本操作
接下來看看二叉樹和二叉查詢樹的一些基本操作,包括BST插入結點,BST查詢結點,BST最大值和最小值,二叉樹結點數目和高度等。二叉查詢樹(BST)特有的操作都在函式前加了 bst
字首區分,其他函式則是二叉樹通用的。
1) 建立結點
分配記憶體,初始化值即可。
/**
* 建立BTNode
*/
BTNode *newNode(int value)
{
BTNode *node = (BTNode *)malloc(sizeof(BTNode));
node->value = value;
node->left = node->right = NULL;
return node;
}
複製程式碼
2) BST 插入結點
插入結點可以用遞迴或者非遞迴實現,如果待插入值比根節點值大,則插入到右子樹中,否則插入到左子樹中。如下圖所示(圖來自參考資料1,2,3):
/**
* BST中插入值,遞迴方法
*/
/**
* BST中插入結點,遞迴方法
*/
BTNode *bstInsert(BTNode *root, int value)
{
if (!root)
return newNode(value);
if (root->value > value) {
root->left = bstInsert(root->left, value);
} else {
root->right = bstInsert(root->right, value);
}
return root;
}
/**
* BST中插入結點,非遞迴方法
*/
BTNode *bstInsertIter(BTNode *root, int value)
{
BTNode *node = newNode(value);
if (!root)
return node;
BTNode *current = root, *parent = NULL;
while (current) {
parent = current;
if (current->value > value)
current = current->left;
else
current = current->right;
}
if (parent->value >= value)
parent->left = node;
else
parent->right = node;
return root;
}
複製程式碼
3) BST 刪除結點
刪除結點稍微複雜一點,要考慮3種情況:
- 刪除的是葉子結點,好辦,移除該結點並將該葉子結點的父結點的
left
或者right
指標置空即可。
- 刪除的結點有兩個子結點,則需要找到該結點左子樹的最大結點(使用後面的
bstSearchIter
函式),並將其值替換到待刪除結點中,然後遞迴呼叫刪除函式刪除該結點左子樹最大結點即可。
- 刪除的結點只有一個子結點,則移除該結點並將其子結點的值填充到該刪除結點即可(需要判斷是左孩子還是右孩子結點)。
/**
* BST中刪除結點
*/
BTNode *bstDelete(BTNode *root, int value)
{
BTNode *parent = NULL, *current = root;
BTNode *node = bstSearchIter(root, &parent, value);
if (!node) {
printf("Value not found\n");
return root;
}
if (!node->left && !node->right) {
// 情況1:待刪除結點是葉子結點
if (node != root) {
if (parent->left == node) {
parent->left = NULL;
} else {
parent->right = NULL;
}
} else {
root = NULL;
}
free(node);
} else if (node->left && node->right) {
// 情況2:待刪除結點有兩個子結點
BTNode *predecessor = bstMax(node->left);
bstDelete(root, predecessor->value);
node->value = predecessor->value;
} else {
// 情況3:待刪除結點只有一個子結點
BTNode *child = (node->left) ? node->left : node->right;
if (node != root) {
if (node == parent->left)
parent->left = child;
else
parent->right = child;
} else {
root = child;
}
free(node);
}
return root;
}
複製程式碼
4) BST 查詢結點
注意在非遞迴查詢中會將父結點也記錄下來。
/**
* BST查詢結點-遞迴
*/
BTNode *bstSearch(BTNode *root, int value)
{
if (!root) return NULL;
if (root->value == value) {
return root;
} else if (root->value > value) {
return bstSearch(root->left, value);
} else {
return bstSearch(root->left, value);
}
}
/**
* BST查詢結點-非遞迴
*/
BTNode *bstSearchIter(BTNode *root, BTNode **parent, int value)
{
if (!root) return NULL;
BTNode *current = root;
while (current && current->value != value) {
*parent = current;
if (current->value > value)
current = current->left;
else
current = current->right;
}
return current;
}
複製程式碼
5)BST 最小值結點和最大值結點
最小值結點從左子樹遞迴查詢,最大值結點從右子樹遞迴找。
/**
* BST最小值結點
*/
BTNode *bstMin(BTNode *root)
{
if (!root->left)
return root;
return bstMin(root->left);
}
/**
* BST最大值結點
*/
BTNode *bstMax(BTNode *root)
{
if (!root->right)
return root;
return bstMax(root->right);
}
複製程式碼
6)二叉樹結點數目和高度
/**
* 二叉樹結點數目
*/
int btSize(BTNode *root)
{
if (!root) return 0;
return btSize(root->left) + btSize(root->right) + 1;
}
/**
* 二叉樹高度
*/
int btHeight(BTNode *root)
{
if (!root) return 0;
int leftHeight = btHeight(root->left);
int rightHeight = btHeight(root->right);
int maxHeight = leftHeight > rightHeight ? leftHeight+1 : rightHeight+1;
return maxHeight;
}
複製程式碼
3 二叉樹遍歷
遞迴遍歷-先序、中序、後序、層序
二叉樹遍歷的遞迴實現比較簡單,直接給出程式碼。這裡值得一提的是層序遍歷,先是計算了二叉樹的高度,然後呼叫的輔助函式依次遍歷每一層的結點,這種方式比較容易理解,雖然在時間複雜度上會高一些。
/**
* 二叉樹先序遍歷
*/
void preOrder(BTNode *root)
{
if (!root) return;
printf("%d ", root->value);
preOrder(root->left);
preOrder(root->right);
}
/**
* 二叉樹中序遍歷
*/
void inOrder(BTNode *root)
{
if (!root) return;
inOrder(root->left);
printf("%d ", root->value);
inOrder(root->right);
}
/**
* 二叉樹後序遍歷
*/
void postOrder(BTNode *root)
{
if (!root) return;
postOrder(root->left);
postOrder(root->right);
printf("%d ", root->value);
}
/**
* 二叉樹層序遍歷
*/
void levelOrder(BTNode *root)
{
int btHeight = height(root);
int level;
for (level = 1; level <= btHeight; level++) {
levelOrderInLevel(root, level);
}
}
/**
* 二叉樹層序遍歷輔助函式-列印第level層的結點
*/
void levelOrderInLevel(BTNode *root, int level)
{
if (!root) return;
if (level == 1) {
printf("%d ", root->value);
return;
}
levelOrderInLevel(root->left, level-1);
levelOrderInLevel(root->right, level-1);
}
複製程式碼
非遞迴遍歷-先序、中序、後序、層序
- 非遞迴遍歷裡面先序遍歷最簡單,使用一個棧來儲存結點,先訪問根結點,然後將右孩子和左孩子依次壓棧,然後迴圈這個過程。中序遍歷稍微複雜一點,需要先遍歷完左子樹,然後才是根結點,最後才是右子樹。
- 後序遍歷使用一個棧的方法
postOrderIter()
會有點繞,也易錯。所以在面試時推薦用兩個棧的版本postOrderIterWith2Stack()
,容易理解,也比較好寫。 - 層序遍歷用了佇列來輔助儲存結點,還算簡單。
- 這裡我另外實現了一個佇列
BTNodeQueue
和棧BTNodeStack
,用於二叉樹非遞迴遍歷。
/*********************/
/** 二叉樹遍歷-非遞迴 **/
/*********************/
/**
* 先序遍歷-非遞迴
*/
void preOrderIter(BTNode *root)
{
if (!root) return;
int size = btSize(root);
BTNodeStack *stack = stackNew(size);
push(stack, root);
while (!IS_EMPTY(stack)) {
BTNode *node = pop(stack);
printf("%d ", node->value);
if (node->right)
push(stack, node->right);
if (node->left)
push(stack, node->left);
}
free(stack);
}
/**
* 中序遍歷-非遞迴
*/
void inOrderIter(BTNode *root)
{
if (!root) return;
BTNodeStack *stack = stackNew(btSize(root));
BTNode *current = root;
while (current || !IS_EMPTY(stack)) {
if (current) {
push(stack, current);
current = current->left;
} else {
BTNode *node = pop(stack);
printf("%d ", node->value);
current = node->right;
}
}
free(stack);
}
/**
* 後續遍歷-使用一個棧非遞迴
*/
void postOrderIter(BTNode *root)
{
BTNodeStack *stack = stackNew(btSize(root));
BTNode *current = root;
do {
// 移動至最左邊結點
while (current) {
// 將該結點右孩子和自己入棧
if (current->right)
push(stack, current->right);
push(stack, current);
// 往左子樹遍歷
current = current->left;
}
current = pop(stack);
if (current->right && peek(stack) == current->right) {
pop(stack);
push(stack, current);
current = current->right;
} else {
printf("%d ", current->value);
current = NULL;
}
} while (!IS_EMPTY(stack));
}
/**
* 後續遍歷-使用兩個棧,更好理解一點。
*/
void postOrderIterWith2Stack(BTNode *root)
{
if (!root) return;
BTNodeStack *stack = stackNew(btSize(root));
BTNodeStack *output = stackNew(btSize(root));
push(stack, root);
BTNode *node;
while (!IS_EMPTY(stack)) {
node = pop(stack);
push(output, node);
if (node->left)
push(stack, node->left);
if (node->right)
push(stack, node->right);
}
while (!IS_EMPTY(output)) {
node = pop(output);
printf("%d ", node->value);
}
}
/**
* 層序遍歷-非遞迴
*/
void levelOrderIter(BTNode *root)
{
if (!root) return;
BTNodeQueue *queue = queueNew(btSize(root));
enqueue(queue, root);
while (1) {
int nodeCount = QUEUE_SIZE(queue);
if (nodeCount == 0)
break;
btHeight
while (nodeCount > 0) {
BTNode *node = dequeue(queue);
printf("%d ", node->value);
if (node->left)
enqueue(queue, node->left);
if (node->right)
enqueue(queue, node->right);
nodeCount--;
}
printf("\n");
}
}
複製程式碼