資料結構和演算法面試題系列—二叉樹基礎
這個系列是我多年前找工作時對資料結構和演算法總結,其中有基礎部分,也有各大公司的經典的面試題,最早釋出在CSDN。現整理為一個系列給需要的朋友參考,如有錯誤,歡迎指正。本系列完整程式碼地址在 ofollow,noindex">這裡 。
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 size(BTNode *root) { if (!root) return 0; return size(root->left) + size(root->right) + 1; } /** * 二叉樹高度 */ int height(BTNode *root) { if (!root) return 0; int leftHeight = height(root->left); int rightHeight = height(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 btSize = size(root); BTNodeStack *stack = stackNew(btSize); 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(size(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(size(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(size(root)); BTNodeStack *output = stackNew(size(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(size(root)); enqueue(queue, root); while (1) { int nodeCount = QUEUE_SIZE(queue); if (nodeCount == 0) break; 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"); } } 複製程式碼