二叉樹作爲面試中高頻出現的數據結構,本文將介紹二叉樹基礎。
對於二叉樹,最常見的面試問題就是:請描述一棵二叉樹,二叉樹的XX遍歷的遞歸算法與非遞歸算法。這些都是基礎,也就是說是必須要掌握的內容。
本篇博文就致力於梳理清楚,這些基礎問題。希望有所收穫。
很多人會問,二叉樹在我們平時開發中用的很少啊,爲什麼面試官那麼愛問,以及算法題中出現的那麼平凡,很多人甚至沒有辦法立刻想到二叉樹的應用有哪些?那麼就從這個問題開始本文吧。
1. 實際開發中哪裏用到二叉樹了呢?
- 用的最多的應該是平衡二叉樹,有種特殊的平衡二叉樹紅黑樹,查找、插入、刪除的時間複雜度最壞爲
O(logn)
。 堆排序
,堆排序是利用堆
這種數據結構而設計的一種排序算法。- 堆是具有以下性質的
完全二叉樹
:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
- 堆是具有以下性質的
- Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虛擬內存的管理,都是通過紅黑樹去實現的。
- 還有哈夫曼樹編碼方面的應用,他是一種壓縮算法。
- B-Tree,B±Tree在文件系統中的應用。
2.二叉樹
花花醬說過,樹是用遞歸定義的數據結構,凡是樹的題目都可以用遞歸做。當然遞歸本質上還是利用了程序執行時候的入棧與出棧操作。我們也可以在代碼中模擬這種操作,因此樹的題目同樣可以使用非遞歸的方式進行解答
用語言描述一棵二叉樹?
二叉樹是遞歸定義的數據結構,其結點最多隻能有2個子節點,他們被稱爲左右子節點。
寫到這裏,我發現了一篇寫的非常好的遍歷二叉樹文章(見參考文獻二),既然前人已經寫了那麼好的文章了,那我就直接參考。下面記錄二叉樹的三種遍歷方式。
定義二叉樹
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
前序遍歷(遞歸)
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
中序遍歷(遞歸)
public void inOrder(TreeNode root) {
if (root==null) {
return;
}
inOrder(root.left);
System.out.println(root.val);
inOrder(root.right);
}
後序遍歷(遞歸)
public void postOrder(TreeNode root) {
if (root==null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.println(root.val);
}
前序遍歷(非遞歸)
public void preOrder(TreeNode root) {
if(root==null) {
return;
}
Stack<TreeNode> stack = new Stack();
while(root!=null) {
System.out.println(root.val);
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
TreeNode node = top.right;
stack.pop();
while(node!=null) {
System.out.println(node.val);
stack.push(node);
node = node.left;
}
}
}
中序遍歷(非遞歸)
public void inOrder(TreeNode root) {
if(root==null) {
return;
}
Stack<TreeNode> stack = new Stack();
while(root!=null) {
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
TreeNode node = top.right;
System.out.println(top.val);
stack.pop();
while(node!=null) {
stack.push(node);
node = node.left;
}
}
}
後序遍歷(非遞歸)
後續遍歷相對複雜,原因就在於出棧的情況不一樣了。
在先序和中序遍歷過程中,只要左子樹處理完畢實際上棧頂元素就可以出棧了,但是後續遍歷情況不同,什麼是後續遍歷?只有左子樹和右子樹都遍歷完畢纔可以處理當前節點,這是後續遍歷,那麼我們該如何知道當前節點的左子樹和右子樹都處理完了呢?
顯然我們需要某種方法記錄下遍歷的過程,實際上我們只需要記錄下遍歷的前一個節點就足夠了。
如果我們知道了遍歷過程中的前一個節點,那麼我們就可以做如下判斷了:
- 如果前一個節點是當前節點的右子樹,那麼說明右子樹遍歷完畢可以pop了
- 如果前一個節點是當前節點的左子樹而且當前節點右子樹爲空,那麼說明可以pop了
- 如果當前節點的左子樹和右子樹都爲空,也就是葉子節點那麼說明可以pop了
public void postOrder(TreeNode root) {
if(root!=null) {
return;
}
Stack<TreeNode> stack = new Stack();
TreeNode before = null;
while(root!=null) {
stack.push(root);
root = root.left;
}
while(!stack.isEmpty()) {
TreeNode top = stack.peek();
if(top.left==null&&top.right==null ||
before==top.right ||
before==top.left&&top.right==null) {
System.out.println(top.val);
before = top;
stack.pop();
} else {
TreeNode node = top.right;
while(node!=null) {
stack.push(node);
node = node.left;
}
}
}
}
下一篇文章,主要討論的是堆排序。