若是讓你數一下一棵普通二叉樹有多少個節點,這很簡單,只要在二叉樹的遍歷框架上加一點代碼就好了。java
可是,若是給你一棵徹底二叉樹,讓你計算它的節點個數,你會不會?算法的時間複雜度是多少?算法
這個算法的時間複雜度應該是 O(logN*logN),若是你心中的算法沒有達到這麼高效,那麼本文就是給你寫的。數組
首先要明確一下兩個關於二叉樹的名詞「徹底二叉樹」和「滿二叉樹」。markdown
咱們說的徹底二叉樹以下圖,每一層都是緊湊靠左排列的:框架
咱們說的滿二叉樹以下圖,是一種特殊的徹底二叉樹,每層都是是滿的,像一個穩定的三角形:spa
說句題外話,關於這兩個定義,中文語境和英文語境彷佛有點區別,咱們說的徹底二叉樹對應英文 Complete Binary Tree,沒有問題。可是咱們說的滿二叉樹對應英文 Perfect Binary Tree,而英文中的 Full Binary Tree 是指一棵二叉樹的全部節點要麼沒有孩子節點,要麼有兩個孩子節點。以下:3d
以上定義出自 wikipedia,這裏就是順便一提,其實名詞叫什麼都無所謂,重要的是算法操做。code
記住「滿二叉樹」和「徹底二叉樹」的區別,等會會用到。orm
如今迴歸正題,如何求一棵徹底二叉樹的節點個數呢?遞歸
// 輸入一棵徹底二叉樹,返回節點總數
int countNodes(TreeNode root);
複製代碼
若是是一個普通二叉樹,顯然只要向下面這樣遍歷一邊便可,時間複雜度 O(N):
public int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
複製代碼
那若是是一棵滿二叉樹,節點總數就和樹的高度呈指數關係,時間複雜度 O(logN):
public int countNodes(TreeNode root) {
int h = 0;
// 計算樹的高度
while (root != null) {
root = root.left;
h++;
}
// 節點總數就是 2^h - 1
return (int)Math.pow(2, h) - 1;
}
複製代碼
徹底二叉樹比普通二叉樹特殊,但又沒有滿二叉樹那麼特殊,計算它的節點總數,能夠說是普通二叉樹和徹底二叉樹的結合版,先看代碼:
public int countNodes(TreeNode root) {
TreeNode l = root, r = root;
// 記錄左、右子樹的高度
int hl = 0, hr = 0;
while (l != null) {
l = l.left;
hl++;
}
while (r != null) {
r = r.right;
hr++;
}
// 若是左右子樹的高度相同,則是一棵滿二叉樹
if (hl == hr) {
return (int)Math.pow(2, hl) - 1;
}
// 若是左右高度不一樣,則按照普通二叉樹的邏輯計算
return 1 + countNodes(root.left) + countNodes(root.right);
}
複製代碼
結合剛纔針對滿二叉樹和普通二叉樹的算法,上面這段代碼應該不難理解,就是一個結合版,可是其中下降時間複雜度的技巧是很是微妙的。
開頭說了,這個算法的時間複雜度是 O(logN*logN),這是怎麼算出來的呢?
直覺感受好像最壞狀況下是 O(N*logN) 吧,由於以前的 while 須要 logN 的時間,最後要 O(N) 的時間向左右子樹遞歸:
return 1 + countNodes(root.left) + countNodes(root.right);
複製代碼
關鍵點在於,這兩個遞歸只有一個會真的遞歸下去,另外一個必定會觸發hl == hr
而當即返回,不會遞歸下去。
爲何呢?緣由以下:
一棵徹底二叉樹的兩棵子樹,至少有一棵是滿二叉樹:
看圖就明顯了吧,因爲徹底二叉樹的性質,其子樹必定有一棵是滿的,因此必定會觸發hl == hr
,只消耗 O(logN) 的複雜度而不會繼續遞歸。
綜上,算法的遞歸深度就是樹的高度 O(logN),每次遞歸所花費的時間就是 while 循環,須要 O(logN),因此整體的時間複雜度是 O(logN*logN)。
因此說,「徹底二叉樹」這個概念仍是有它存在的緣由的,不只適用於數組實現二叉堆,並且連計算節點總數這種看起來簡單的操做都有高效的算法實現。