這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏。html
繼上一篇總結了二叉樹的基礎操做後,這一篇文章彙總下常見的二叉樹相關面試題,主要分爲判斷類、構建類、存儲類、查找類、距離類、混合類這六類大問題。本文全部代碼在 這裏 。node
判斷類問題主要分下下判斷二叉樹是不是二叉搜索樹、二叉徹底樹,以及兩棵二叉樹是否同構這三個問題。git
題: 給定一棵二叉樹,判斷該二叉樹是不是二叉搜索樹。github
二叉搜索樹是一種二叉樹,可是它有附加的一些約束條件,這些約束條件必須對每一個結點都成立:面試
一種錯誤解法算法
初看這個問題,容易這麼實現:假定當前結點值爲 k,對於二叉樹中每一個結點,判斷其左孩子的值是否小於 k,其右孩子的值是否大於 k。若是全部結點都知足該條件,則該二叉樹是一棵二叉搜索樹。實現代碼以下:編程
int isBSTError(BTNode *root)
{
if (!root) return 1;
if (root->left && root->left->value >= root->value)
return 0;
if (root->right && root->right->value < root->value)
return 0;
if (!isBSTError(root->left) || !isBSTError(root->right))
return 0;
return 1;
}
複製代碼
很不幸,這種作法是錯誤的,以下面這棵二叉樹知足上面的條件,可是它並非二叉搜索樹。數組
10
/ \
5 15 -------- binary tree(1) 符合上述條件的二叉樹,可是並非二叉搜索樹。
/ \
6 20
複製代碼
解1:蠻力法bash
上面的錯誤解法是由於判斷不完整致使,能夠這樣來判斷:數據結構
bstMax
和 bstMin
函數功能分別是返回二叉樹中的最大值和最小值結點,這裏假定二叉樹爲二叉搜索樹,實際返回的不必定是最大值和最小值結點)int isBSTUnefficient(BTNode *root)
{
if (!root) return 1;
if (root->left && bstMax(root->left)->value >= root->value)
return 0;
if (root->right && bstMin(root->right)->value < root->value)
return 0;
if (!isBSTUnefficient(root->left) || !isBSTUnefficient(root->right))
return 0;
return 1;
}
複製代碼
解2:一次遍歷法
之前面提到的 binary tree(1)
爲例,當咱們遍歷到結點 15
時,咱們知道右子樹結點值確定都 >=10
。當咱們遍歷到結點 15
的左孩子結點 6
時,咱們知道結點 15
的左子樹結點值都必須在 10
到 15
之間。顯然,結點 6
不符合條件,所以它不是一棵二叉搜索樹。
int isBSTEfficient(BTNode* root, BTNode *left, BTNode *right)
{
if (!root) return 1;
if (left && root->value <= left->value)
return 0;
if (right && root->value > right->value)
return 0;
return isBSTEfficient(root->left, left, root) && isBSTEfficient(root->right, root, right);
}
複製代碼
解3:中序遍歷解法
還能夠模擬樹的中序遍從來判斷BST,能夠直接將中序遍歷的結果存到一個輔助數組,而後判斷數組是否有序便可判斷是不是BST。固然,咱們能夠不用輔助數組,在遍歷時經過保留前一個指針 prev
,據此來實現判斷BST的解法,初始時 prev = NULL
。
int isBSTInOrder(BTNode *root, BTNode *prev)
{
if (!root) return 1;
if (!isBSTInOrder(root->left, prev))
return 0;
if (prev && root->value < prev->value)
return 0;
return isBSTInOrder(root->right, root);
}
複製代碼
題: 給定一棵二叉樹,判斷該二叉樹是不是徹底二叉樹(徹底二叉樹定義:若設二叉樹的深度爲 h
,除第 h
層外,其它各層 (1~h-1)
的結點數都達到最大個數,第 h
層全部的結點都連續集中在最左邊,這就是徹底二叉樹,以下圖所示)。
解1:常規解法-中序遍歷
先定義一個 滿結點 的概念:即一個結點存在左右孩子結點,則該結點爲滿結點。在代碼中定義變量 flag
來標識是否發現非滿結點,爲1表示該二叉樹存在非滿結點。徹底二叉樹若是存在非滿結點,則根據層序遍歷隊列中剩下結點必須是葉子結點,且若是一個結點的左孩子爲空,則右孩子結點也必須爲空。
int isCompleteBTLevelOrder(BTNode *root)
{
if (!root) return 1;
BTNodeQueue *queue = queueNew(btSize(root));
enqueue(queue, root);
int flag = 0;
while (QUEUE_SIZE(queue) > 0) {
BTNode *node = dequeue(queue);
if (node->left) {
if (flag) return 0;
enqueue(queue, node->left);
} else {
flag = 1;
}
if (node->right) {
if (flag) return 0;
enqueue(queue, node->right);
} else {
flag = 1;
}
}
return 1;
}
複製代碼
解2:更簡單的方法-判斷結點序號法
更簡單的方法是判斷結點序號法,由於徹底二叉樹的結點序號都是有規律的,如結點 i
的左右子結點序號爲 2i+1
和 2i+2
,如根結點序號是 0
,它的左右子結點序號是 1
和 2
(若是都存在的話)。咱們能夠計算二叉樹的結點數目,而後依次判斷全部結點的序號,若是不是徹底二叉樹,那確定會存在結點它的序號大於等於結點數目的。如前面提到的 binary tree(1)
就不是徹底二叉樹。
10(0)
/ \
5(1) 15(2) - 結點數目爲5,若是是徹底二叉樹結點最大的序號應該是4,而它的是6,因此不是。
/ \
6(5) 20(6)
複製代碼
實現代碼以下:
int isCompleteBTIndexMethod(BTNode *root, int index, int nodeCount)
{
if (!root) return 1;
if (index >= nodeCount)
return 0;
return (isCompleteBTIndexMethod(root->left, 2*index+1, nodeCount) &&
isCompleteBTIndexMethod(root->right, 2*index+2, nodeCount));
}
複製代碼
題: 判斷一棵二叉樹是不是平衡二叉樹。所謂平衡二叉樹,指的是其任意結點的左右子樹高度之差不大於1。
__2__
/ \
1 4 ---- 平衡二叉樹示例
\ / \
3 5 6
複製代碼
解1:自頂向下方法
判斷一棵二叉樹是不是平衡的,對每一個結點計算左右子樹高度差是否大於1便可,時間複雜度爲O(N^2)
。
int isBalanceBTTop2Down(BTNode *root)
{
if (!root) return 1;
int leftHeight = btHeight(root->left);
int rightHeight = btHeight(root->right);
int hDiff = abs(leftHeight - rightHeight);
if (hDiff > 1) return 0;
return isBalanceBTTop2Down(root->left) && isBalanceBTTop2Down(root->right);
}
複製代碼
解2:自底向上方法
由於解1會重複的遍歷不少結點,爲此咱們能夠採用相似後序遍歷的方式,自底向上來判斷左右子樹的高度差,這樣時間複雜度爲 O(N)
。
int isBalanceBTDown2Top(BTNode *root, int *height)
{
if (!root) {
*height = 0;
return 1;
}
int leftHeight, rightHeight;
if (isBalanceBTDown2Top(root->left, &leftHeight) &&
isBalanceBTDown2Top(root->right, &rightHeight)) {
int diff = abs(leftHeight - rightHeight);
return diff > 1 ? 0 : 1;
}
return 0;
}
複製代碼
題: 給定兩棵二叉樹,根結點分別爲 t1
和 t2
,斷定這兩棵二叉樹是否同構。所謂二叉樹同構就是指它們的結構相同,以下二叉樹 (1) 和 (2) 是同構的,而它們和 (3) 是不一樣結構的:
5 9 6
/ \ / \ / \
1 2 7 12 5 9
/ \ / \ \
4 3 5 8 10
二叉樹(1) 二叉樹(2) 二叉樹(3)
複製代碼
解: 二叉樹結構是否相同,仍是遞歸實現,先判斷根結點是否同構,而後再判斷左右子樹。
int isOmorphism(BTNode *t1, BTNode *t2)
{
if (!t1 || !t2)
return (!t1) && (!t2);
return isOmorphism(t1->left, t2->left) && isOmorphism(t1->right, t2->right);
}
複製代碼
構建類問題主要是使用二叉樹的兩種遍歷順序來肯定二叉樹的另一種遍歷順序問題。在上一篇文章中咱們分析過二叉樹的先序、中序、後序遍歷的遞歸和非遞歸實現。那麼,是否能夠根據先序、中序或者先序、後序或者中序、後序惟一肯定一棵二叉樹呢?
答案是 在沒有重複值的二叉樹中, 根據先序遍歷和後序遍歷沒法惟一肯定一棵二叉樹,而根據先序、中序或者中序、後序遍歷是能夠惟一肯定一棵二叉樹的。
1)先序和後序遍歷沒法惟一肯定一棵二叉樹
一個簡單的例子以下,這兩棵二叉樹的先序遍歷和後序遍歷相同,由此能夠證實先序遍歷和後序遍歷沒法惟一肯定一棵二叉樹。
1 1
/ /
2 2
\ /
3 3
先序遍歷: 1 2 3
後序遍歷: 3 2 1
複製代碼
2)先序和中序遍歷能夠惟一肯定二叉樹
簡單證實:由於先序遍歷的第一個元素是根結點,該元素將二叉樹中序遍歷序列分紅兩部分,左邊(假設有L個元素)表示左子樹,若左邊無元素,則說明左子樹爲空;右邊(假設有R個元素)是右子樹,若爲空,則右子樹爲空。根據前序遍歷中"根-左子樹-右子樹"的順序,則由從先序序列的第二元素開始的L個結點序列和中序序列根左邊的L個結點序列構造左子樹,由先序序列最後R個元素序列與中序序列根右邊的R個元素序列構造右子樹。
3)中序和後序遍歷能夠惟一肯定二叉樹
簡單證實: 假定二叉樹結點數爲 n
,假定中序遍歷爲 S1, S2, ..., Sn,然後序遍歷爲 P1, P2, ..., Pn,由於後序遍歷最後一個結點 Pn 是根結點,則能夠根據 Pn 將中序遍歷分爲兩部分,則其中左邊L個結點是左子樹結點,右邊R個結點是右子樹結點,則後序遍歷中的 1~L 個結點是左子樹的後序遍歷,由此 PL 是左子樹的根,與前面同理能夠將中序遍歷分紅兩部分,直到最終肯定該二叉樹。
題: 給定一棵二叉樹的先序和中序遍歷序列,請構建該二叉樹(注:二叉樹沒有重複的值)。
先序遍歷: 7 10 4 3 1 2 8 11
中序遍歷: 4 10 3 1 7 11 8 2
二叉樹以下:
7
/ \
10 2
/ \ /
4 3 8
\ /
1 11
複製代碼
解: 根據前面的分析來解這個問題。
{4,10,3,1}
這四個結點屬於左子樹,而根結點7右邊的 {11,8,2}
屬於右子樹。O(N)
的時間,整個算法須要 O(N^2)
的時間。若是要提升效率,也能夠哈希表來存儲與查找根結點在中序遍歷中的位置,每次查找只須要 O(1)
的時間,這樣構建整棵樹只須要 O(N)
的時間。buildBTFromPreInOrder(preorder, inorder, n, 0, n);
,其中 preorder
和 inorder
分別爲先序中序遍歷數組,n
爲數組大小。/**
* 輔助函數,查找根結點在中序遍歷中的位置。
*/
int findBTRootIndex(int inorder[], int count, int rootVal)
{
int i;
for (i = 0; i < count; i++) {
if (inorder[i] == rootVal)
return i;
}
return -1;
}
/**
/**
* 根據先序和中序遍歷構建二叉樹
*/
BTNode *buildBTFromPreInOrder(int preorder[], int inorder[], int n, int offset, int count)
{
if (n == 0) return NULL;
int rootVal = preorder[0];
int rootIndex = findBTRootIndex(inorder, count, rootVal);
int leftCount = rootIndex - offset; // 左子樹結點數目
int rightCount = n - leftCount - 1; // 右子樹結點數目
BTNode *root = btNewNode(rootVal);
root->left = buildBTFromPreInOrder(preorder+1, inorder, leftCount, offset, count);
root->right = buildBTFromPreInOrder(preorder+leftCount+1, inorder, rightCount, offset+leftCount+1, count);
return root;
}
複製代碼
題: 給定一棵二叉樹的中序和後序遍歷序列,請構建該二叉樹(注:二叉樹沒有重複的值)。
中序遍歷: 4 10 3 1 7 11 8 2
後序遍歷: 4 1 3 10 11 8 2 7
二叉樹以下:
7
/ \
10 2
/ \ /
4 3 8
\ /
1 11
複製代碼
解: 跟前面一題相似,只是這裏根結點是從後序遍歷數組的最後一個元素取。
/**
* 根據中序和後序遍歷構建二叉樹
*/
BTNode *buildBTFromInPostOrder(int postorder[], int inorder[], int n, int offset, int count)
{
if (n == 0) return NULL;
int rootVal = postorder[n-1];
int rootIndex = findBTRootIndex(inorder, count, rootVal);
int leftCount = rootIndex - offset; // 左子樹結點數目
int rightCount = n - leftCount - 1; // 右子樹結點數目
BTNode *root = btNewNode(rootVal);
root->left = buildBTFromInPostOrder(postorder, inorder, leftCount, offset, count);
root->right = buildBTFromInPostOrder(postorder+leftCount, inorder, rightCount, offset+leftCount+1, count);
return root;
}
複製代碼
題: 設計一個算法,將一棵二叉搜索樹(BST)保存到文件中,須要可以從文件中恢復原來的二叉搜索樹,注意算法的時空複雜度。
30
/ \
20 40
/ / \
10 35 50
複製代碼
思路
二叉樹遍歷算法有先序遍歷、中序遍歷、後序遍歷算法等。可是它們中間哪種可以用於保存BST到文件中並從文件中恢復原來的BST,這是個要考慮的問題。
假定用中序遍歷,由於這棵BST的中序遍歷爲 10 20 30 35 40 50
,可能的結構是下面這樣,所以 中序遍歷不符合要求 。
50
/
40
/
35
/
30
/
20
/
10
複製代碼
既然中序遍歷不行,後序遍歷如何?後序遍歷該BST能夠獲得:10 20 35 50 40 30
。讀取這些結點並構造出原來的BST是個難題,由於在構造二叉樹時是先構造父結點再插入孩子結點,然後序遍歷序列是先讀取到孩子結點而後纔是父結點,因此 後續遍歷也不符合條件 。
綜合看來,只有先序遍歷知足條件 。該BST的先序遍歷是 30 20 10 40 35 50
。咱們觀察到重要的一點就是:一個結點的父親結點老是在該結點以前輸出 。有了這個觀察,咱們從文件中讀取BST結點序列後,老是能夠在構造孩子結點以前構造它們的父結點。將BST寫入到文件的代碼跟先序遍歷同樣。
那麼讀取恢復怎麼作呢?使用二叉搜索樹 bstInsert()
方法執行 N 次插入操做便可,若是二叉搜索樹平衡的話每次插入須要時間 O(lgN)
,共須要 O(NlgN)
的時間,而最壞狀況下爲 O(N^2)
。
/**
* 存儲二叉樹到文件中-使用先序遍歷
*/
void bstSave(BTNode *root, FILE *fp)
{
if (!root) return;
char temp[30];
sprintf(temp, "%d\n", root->value);
fputs(temp, fp);
bstSave(root->left, fp);
bstSave(root->right, fp);
}
/**
* 從文件中恢復二叉樹
*/
BTNode *bstRestore(FILE *fp)
{
BTNode *root = NULL;
char *s;
char buf[30];
while ((s = fgets(buf, 30, fp))) {
int nodeValue = atoi(s);
root = bstInsert(root, nodeValue);
}
return root;
}
複製代碼
題: 設計一個算法可以實現二叉樹(注意,不是二叉搜索樹BST)存儲和恢復。
解: 3.1節提到過使用先序遍歷能夠保存和恢復二叉搜索樹,而這個題目是針對二叉樹,並非BST,因此不能用前面的方式。不過,咱們能夠採用先序遍歷的思想,只是在這裏須要改動。爲了可以在重構二叉樹時結點可以插入到正確的位置,在使用先序遍歷保存二叉樹到文件中的時候須要把 NULL
結點也保存起來(可使用特殊符號如 #
來標識 NULL
結點)。
注意: 本題採用 #
保存 NULL
結點的方法存在缺陷,如本方法中二叉樹結點值就不能是 #
。若是要能保存各類字符,則須要採用其餘方法來實現了。
30
/ \
10 20
/ / \
50 45 35
複製代碼
如上面這棵二叉樹,保存到文件中則爲 30 10 50 # # # 20 45 # # 35 # #
。因而,保存和恢復實現的代碼以下:
/**
* 存儲二叉樹到文件中
*/
void btSave(BTNode *root, FILE *fp)
{
if (!root) {
fputs("#\n", fp);
} else {
char temp[30];
sprintf(temp, "%d\n", root->value);
fputs(temp, fp);
btSave(root->left, fp);
btSave(root->right, fp);
}
}
/**
* 從文件恢復二叉樹
*/
BTNode *btRestore(BTNode *root, FILE *fp)
{
char buf[30];
char *s = fgets(buf, 30, fp);
if (!s || strcmp(s, "#\n") == 0)
return NULL;
int nodeValue = atoi(s);
root = btNewNode(nodeValue);
root->left = btRestore(root->left, fp);
root->right = btRestore(root->right, fp);
return root;
}
複製代碼
查找類問題主要包括:查找二叉樹/二叉搜索樹的最低公共祖先結點,或者是二叉樹中的最大的子樹且該子樹爲二叉搜索樹等。
題: 給定一棵二叉搜索樹(BST),找出樹中兩個結點的最低公共祖先結點(LCA)。以下面這棵二叉樹結點 2 和 結點 8 的 LCA 是 6,而結點 4 和 結點 2 的 LCA 是 2。
______6______
/ \
__2__ __8__
/ \ / \
0 4 7 9
/ \
3 5
複製代碼
解: 咱們從頂往下遍歷二叉搜索樹時,對每一個遍歷到的結點,待求LCA的兩個結點可能有以下四種分佈狀況:
BTNode *bstLCA(BTNode *root, BTNode *p, BTNode *q)
{
if (!root || !p || !q) return NULL;
int maxValue = p->value >= q->value ? p->value : q->value;
int minValue = p->value < q->value ? p->value : q->value;
if (maxValue < root->value) {
return bstLCA(root->left, p, q);
} else if (minValue > root->value) {
return bstLCA(root->right, p, q);
} else {
return root;
}
}
複製代碼
題: 給定二叉樹中的兩個結點,輸出這兩個結點的最低公共祖先結點(LCA)。注意,該二叉樹不必定是二叉搜索樹。
_______3______
/ \
___5__ ___1__
/ \ / \
6 2 0 8
/ \
7 4
複製代碼
解1:自頂向下方法
由於不必定是BST,因此不能根據值大小來判斷,不過整體思路是同樣的:咱們能夠從根結點出發,判斷當前結點的左右子樹是否包含這兩個結點。
由於對每一個結點都要重複判斷結點 p
和 q
的位置,總的時間複雜度爲 O(N^2)
,爲此,咱們能夠考慮找一個效率更高的方法。
/**
* 二叉樹最低公共祖先結點-自頂向下解法 O(N^2)
*/
BTNode *btLCATop2Down(BTNode *root, BTNode *p, BTNode *q)
{
if (!root || !p || !q) return NULL;
if (btExist(root->left, p) && btExist(root->left, q)) {
return btLCATop2Down(root->left, p, q);
} else if (btExist(root->right, p) && btExist(root->right, q)) {
return btLCATop2Down(root->right, p, q);
} else {
return root;
}
}
/**
* 二叉樹結點存在性判斷
*/
int btExist(BTNode *root, BTNode *node)
{
if (!root) return 0;
if (root == node) return 1;
return btExist(root->left, node) || btExist(root->right, node);
}
複製代碼
解2:自底向上方法
由於自頂向下方法有不少重複的判斷,因而有了這個自底向上的方法。自底向上遍歷結點,一旦遇到結點等於p
或者 q
,則將其向上傳遞給它的父結點。父結點會判斷它的左右子樹是否都包含其中一個結點,若是是,則父結點必定是這兩個結點 p
和 q
的 LCA。若是不是,咱們向上傳遞其中的包含結點 p
或者 q
的子結點,或者 NULL
(若是左右子樹都沒有結點p或q)。該方法時間複雜度爲O(N)。
/**
* 二叉樹最低公共祖先結點-自底向上解法 O(N)
*/
BTNode *btLCADown2Top(BTNode *root, BTNode *p, BTNode *q)
{
if (!root) return NULL;
if (root == p || root == q) return root;
BTNode *left = btLCADown2Top(root->left, p, q);
BTNode *right = btLCADown2Top(root->right, p, q);
if (left && right)
return root; // 若是p和q位於不一樣的子樹
return left ? left: right; //p和q在相同的子樹,或者p和q不在子樹中
}
複製代碼
題: 找出二叉樹中最大的子樹,該子樹爲二叉搜索樹。所謂最大的子樹就是指結點數目最多的子樹。
___10___
/ \
_5_ 15
/ \ \
1 8 7
___10____
/ \
_5_ 15 -------- subtree (1)
/ \
1 8
_5_
/ \ -------- subtree (2)
1 8
複製代碼
根據維基百科對 子樹 的定義,一棵二叉樹T的子樹由T的某個結點和該結點全部的後代構成。也就是說,該題目中,subtree(2)
纔是正確的答案,由於 subtree(1)
不包含結點7,不知足子樹的定義。
解1:自頂向下解法
最天然的解法是以根結點開始遍歷二叉樹全部的結點,斷定以當前結點爲根的子樹是不是BST,若是是,則該結點爲根的BST就是最大的BST。若是不是,遞歸調用左右子樹,返回其中包含較多結點的子樹。
/**
* 查找二叉樹最大的二叉搜索子樹-自頂向下方法
*/
BTNode *largestSubBSTTop2Down(BTNode *root, int *bstSize)
{
if (!root) {
*bstSize = 0;
return NULL;
}
if (isBSTEfficient(root, NULL, NULL)) { //以root爲根結點的樹爲BST,則設置結果爲root並返回。
*bstSize = btSize(root);
return root;
}
int lmax, rmax;
BTNode *leftBST = largestSubBSTTop2Down(root->left, &lmax); //找出左子樹中爲BST的最大的子樹
BTNode *rightBST = largestSubBSTTop2Down(root->right, &rmax); //找出右子樹中爲BST的最大的子樹
*bstSize = lmax > rmax ? lmax : rmax; //設定結點最大數目
BTNode *result = lmax > rmax ? leftBST : rightBST;
return result;
}
複製代碼
解2:自底向上解法
自頂向下的解法時間複雜度爲 O(N^2)
,每一個結點都要判斷是否知足BST的條件,能夠用從底向上方法優化。咱們在判斷上面結點爲根的子樹是不是BST以前已經知道底部結點爲根的子樹是不是BST,所以只要以底部結點爲根的子樹不是BST,則以它上面結點爲根的子樹必定不是BST。咱們能夠記錄子樹包含的結點數目,而後跟父結點所在的二叉樹比較,來求得最大BST子樹。
/**
* 查找二叉樹最大的二叉搜索子樹-自底向上方法
*/
BTNode *largestSubBSTDown2Top(BTNode *root, int *bstSize)
{
BTNode *largestBST = NULL;
int min, max, maxNodes=0;
findLargestSubBST(root, &min, &max, &maxNodes, &largestBST);
*bstSize = maxNodes;
return largestBST;
}
/**
* 查找最大二叉搜索子樹自底向上方法主體函數
* 若是是BST,則返回BST的結點數目,不然返回-1
*/
int findLargestSubBST(BTNode *root, int *min, int *max, int *maxNodes, BTNode **largestSubBST)
{
if (!root) return 0;
int isBST = 1;
int leftNodes = findLargestSubBST(root->left, min, max, maxNodes, largestSubBST);
int currMin = (leftNodes == 0) ? root->value : *min;
if (leftNodes == -1 || (leftNodes != 0 && root->value <= *max))
isBST = 0;
int rightNodes = findLargestSubBST(root->right, min, max, maxNodes, largestSubBST);
int currMax = (rightNodes == 0) ? root->value : *max;
if (rightNodes == -1 || (rightNodes != 0 && root->value > *min))
isBST = 0;
if (!isBST)
return -1;
*min = currMin;
*max = currMax;
int totalNodes = leftNodes + rightNodes + 1;
if (totalNodes > *maxNodes) {
*maxNodes = totalNodes;
*largestSubBST = root;
}
return totalNodes;
}
複製代碼
題: 已知二叉樹中兩個結點,求這兩個結點之間的最短距離(注:最短距離是指從一個結點到另外一個結點須要通過的邊的條數)。
___1___
/ \
2 3
/ \ / \
4 5 6 7
\
8
Distance(4, 5) = 2
Distance(4, 6) = 4
Distance(3, 4) = 3
Distance(2, 4) = 1
Distance(8, 5) = 5
複製代碼
解: 兩個結點的距離比較好辦,先求出兩個結點的最低公共祖先結點(LCA),而後計算 LCA 到兩個結點的距離之和便可,時間複雜度 O(N)
。
/**
* 計算二叉樹兩個結點最短距離
*/
int distanceOf2BTNodes(BTNode *root, BTNode *p, BTNode *q)
{
if (!root) return 0;
BTNode *lca = btLCADown2Top(root, p, q);
int d1 = btDistanceFromRoot(lca, p, 0);
int d2 = btDistanceFromRoot(lca, q, 0);
return d1+d2;
}
/**
* 計算二叉樹結點node和root的距離
*/
int btDistanceFromRoot(BTNode *root, BTNode *node, int level)
{
if (!root) return -1;
if (root == node) return level;
int left = btDistanceFromRoot(root->left, node, level+1);
if (left == -1)
return btDistanceFromRoot(root->right, node, level+1);
return left;
}
複製代碼
題: 求一棵二叉搜索樹中的兩個結點的最短距離。
解: 與前面不一樣的是,這是一棵BST,那麼咱們可使用二叉搜索樹的特色來簡化距離計算流程,固然直接用 5.1 的方法是徹底OK的,由於它是通用的計算方法。
/**
* 計算BST兩個結點最短距離。
*/
int distanceOf2BSTNodes(BTNode *root, BTNode *p, BTNode *q)
{
if (!root) return 0;
if (root->value > p->value && root->value > q->value) {
return distanceOf2BSTNodes(root->left, p, q);
} else if(root->value <= p->value && root->value <= q->value){
return distanceOf2BSTNodes(root->right, p, q);
} else {
return bstDistanceFromRoot(root, p) + bstDistanceFromRoot(root, q);
}
}
/**
* 計算BST結點node和root的距離
*/
int bstDistanceFromRoot(BTNode *root, BTNode *node)
{
if (root->value == node->value)
return 0;
else if (root->value > node->value)
return 1 + bstDistanceFromRoot(root->left, node);
else
return 1 + bstDistanceFromRoot(root->right, node);
}
複製代碼
題: 寫一個程序求一棵二叉樹中相距最遠的兩個結點之間的距離。
解: 《編程之美》上有這道題,這題跟前面不一樣,要求相距最遠的兩個結點的距離,並且並無指定兩個結點位置。計算一個二叉樹的最大距離有兩個狀況:
___10___
/ \
_5_ 15 ------ 第1種狀況
/ \ \
1 8 7
10
/
5
/ \ ------ 第2種狀況
1 8
/ \
2 3
複製代碼
咱們定義函數 maxDistanceOfBT(BTNode *root)
用於計算二叉樹相距最遠的兩個結點的距離,能夠遞歸的先計算左右子樹的最遠結點距離,而後比較左子樹最遠距離、右子樹最遠距離以及左右子樹最大深度之和,從而求出整個二叉樹的相距最遠的兩個結點的距離。
int btMaxDistance(BTNode *root, int *maxDepth)
{
if (!root) {
*maxDepth = 0;
return 0;
}
int leftMaxDepth, rightMaxDepth;
int leftMaxDistance = btMaxDistance(root->left, &leftMaxDepth);
int rightMaxDistance = btMaxDistance(root->right, &rightMaxDepth);
*maxDepth = max(leftMaxDepth+1, rightMaxDepth+1);
int maxDistance = max3(leftMaxDistance, rightMaxDistance, leftMaxDepth+rightMaxDepth); // max求兩個數最大值,max3求三個數最大值,詳見代碼
return maxDistance;
}
複製代碼
題: 給定一棵二叉樹,求該二叉樹的最大寬度。二叉樹的寬度指的是每一層的結點數目。以下面這棵二叉樹,從上往下1-4層的寬度分別是 1,2,3,2
,因而它的最大寬度爲3。
1
/ \
2 3
/ \ \
4 5 8
/ \
6 7
複製代碼
解1:層序遍歷法
最容易想到的方法就是使用層序遍歷,而後計算每一層的結點數,而後得出最大結點數。該方法時間複雜度爲 O(N^2)
。固然若是優化爲使用隊列來實現層序遍歷,能夠獲得 O(N)
的時間複雜度。
/**
* 二叉樹最大寬度
*/
int btMaxWidth(BTNode *root)
{
int h = btHeight(root);
int level, width;
int maxWidth = 0;
for (level = 1; level <= h; level++) {
width = btLevelWidth(root, level);
if (width > maxWidth)
maxWidth = width;
}
return maxWidth;
}
/**
* 二叉樹第level層的寬度
*/
int btLevelWidth(BTNode *root, int level)
{
if (!root) return 0;
if (level == 1) return 1;
return btLevelWidth(root->left, level-1) + btLevelWidth(root->right, level-1);
}
複製代碼
解2:先序遍歷法
咱們能夠先建立一個大小爲二叉樹高度 h 的輔助數組來存儲每一層的寬度,初始化爲0。經過先序遍歷的方式來遍歷二叉樹,並設置好每層的寬度。最後,從這個輔助數組中求最大值便是二叉樹最大寬度。
/**
* 二叉樹最大寬度-先序遍歷法
*/
int btMaxWidthPreOrder(BTNode *root)
{
int h = btHeight(root);
int *count = (int *)calloc(sizeof(int), h);
btLevelWidthCount(root, 0, count);
int i, maxWidth = 0;
for (i = 0; i < h; i++) {
if (count[i] > maxWidth)
maxWidth = count[i];
}
return maxWidth;
}
/**
* 計算二叉樹從 level 開始的每層寬度,並存儲到數組 count 中。
*/
void btLevelWidthCount(BTNode *root, int level, int count[])
{
if (!root) return;
count[level]++;
btLevelWidthCount(root->left, level+1, count);
btLevelWidthCount(root->right, level+1, count);
}
複製代碼
此類問題主要考察二叉樹和鏈表/數組等結合,形式偏新穎。
題: 給定一個有序數組,數組元素升序排列,試根據該數組元素構建一棵平衡二叉搜索樹(Balanced Binary Search Tree)。所謂平衡的定義,就是指二叉樹的子樹高度之差不能超過1。
__3__
/ \
1 5 ---- 平衡二叉搜索樹示例
\ / \
2 4 6
複製代碼
解: 若是要從一個有序數組中選擇一個元素做爲根結點,應該選擇哪一個元素呢?咱們應該選擇有序數組的中間元素做爲根結點。選擇了中間元素做爲根結點並建立後,剩下的元素分爲兩部分,能夠看做是兩個數組。這樣剩下的元素在根結點左邊的做爲左子樹,右邊的做爲右子樹。
BTNode *sortedArray2BST(int a[], int start, int end)
{
if (start > end) return NULL;
int mid = start + (end-start)/2;
BTNode *root = btNewNode(a[mid]);
root->left = sortedArray2BST(a, start, mid-1);
root->right = sortedArray2BST(a, mid+1, end);
return root;
}
複製代碼
題: 給定一個有序的單向鏈表,構建一棵平衡二叉搜索樹。
解: 最天然的想法是先將鏈表中的結點的值保存在數組中,而後採用 6.1 中方法實現,時間複雜度爲 O(N)
。咱們還能夠採用自底向上的方法,在這裏咱們再也不須要每次查找中間元素。
下面代碼依舊須要鏈表長度做爲參數,計算鏈表長度時間複雜度爲O(N),算法時間複雜度也爲O(N),因此總的時間複雜度爲O(N)。代碼中須要注意的是每次調用 sortedList2BST
函數時,list
位置都會變化,調用完函數後 list
老是指向 mid+1
的位置 (若是知足返回條件,則 list
位置不變)。
BTNode *sortedList2BST(ListNode **pList, int start, int end)
{
if (start > end) return NULL;
int mid = start + (end-start)/2;
BTNode *left = sortedList2BST(pList, start, mid-1);
BTNode *parent = btNewNode((*pList)->value);
parent->left = left;
*pList = (*pList)->next;
parent->right = sortedList2BST(pList, mid+1, end);
return parent;
}
複製代碼
例如鏈表只有2個節點 3->5->NULL
,則初始 start=0, end=1, mid=0
,繼而遞歸調用 sortedList2BST(pList, start,mid-1)
,此時直接返回 NULL
。即左孩子爲NULL
, 根結點爲 3
,然後鏈表指向 5
,再調用 sortedList2BST(pList, mid+1, end)
,而此次調用返回結點 5
,將其賦給根結點 3
的右孩子。此次調用的 mid=1
,調用完成後 list
已經指向鏈表末尾。
題: 給定一棵二叉搜索樹(BST),將其轉換爲雙向的有序循環鏈表。
解: 如圖所示,須要將 BST 的左右孩子指針替換成鏈表的 prev
和 next
指針,分別指向雙向鏈表的前一個和後一個結點。相信大多數人第一反應就是中序遍歷這棵二叉樹,同時改變樹中結點的 left
和 right
指針。這裏須要額外考慮的是如何將最後一個結點的right
指針指向第一個結點,以下圖所展現的那樣。
以中序遍歷遍歷一棵二叉樹的時候,每遍歷到一個結點,咱們就能夠修改該結點的left指針指向前一個遍歷到的結點,由於在後續操做中咱們不會再用到 left
指針;與此同時,咱們還須要修改前一個遍歷結點的 right
指針,讓前一個遍歷結點的 right
指針指向當前結點。好比咱們遍歷到結點2,則咱們修改結點2的 left
指針指向結點1,同時須要修改結點1的 right
指針指向結點2。須要注意一點,這裏的前一個遍歷結點不是當前結點的父結點,而是當前結點的前一個比它小的結點。
看似問題已經解決,慢着,咱們其實落下了重要的兩步。1)咱們沒有對頭結點head賦值。 2)最後一個結點的right指針沒有指向第一個結點。
解決這兩個問題的方案很是簡單:在每次遞歸調用的時候,更新當前遍歷結點的 right
指針讓其指向頭結點 head
,同時更新頭結點 head
的 left
指針讓其指向當前遍歷結點。當遞歸調用結束的時候,鏈表的頭尾結點會指向正確的位置。不要忘記只有一個結點的特殊狀況,它的 left
和 right
指針都是指向本身。
void bt2DoublyList(BTNode *node, BTNode **pPrev, BTNode **pHead)
{
if (!node) return;
bt2DoublyList(node->left, pPrev, pHead);
// 當前結點的left指向前一個結點pPrev
node->left = *pPrev;
if (*pPrev)
(*pPrev)->right = node; // 前一個結點的right指向當前結點
else
*pHead = node; // 若是前面沒有結點,則設置head爲當前結點(當前結點爲最小的結點)。
// 遞歸結束後,head的left指針指向最後一個結點,最後一個結點的右指針指向head結點。
// 注意保存當前結點的right指針,由於在後面代碼中會修改該指針。
BTNode *right = node->right;
(*pHead)->left = node;
node->right = (*pHead);
*pPrev = node;//更新前一個結點
bt2DoublyList(right, pPrev, pHead);
}
複製代碼
這個解法很是的精巧,由於該算法是對中序遍歷的一個改進,所以它的時間複雜度爲O(N),N爲結點數目。固然,相比中序遍歷,咱們在每次遞歸調用過程當中增長了額外的賦值操做。