題目描述
給你一個二叉樹,請你返回其按 層序遍歷 得到的節點值。 (即逐層地,從左到右訪問所有節點)。
示例:
二叉樹:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其層次遍歷結果:
[
[3],
[9,20],
[15,7]
]
分析
先考慮只需要返回一個層序遍歷結果而不用考慮到底是第幾層。如上面只返回[3,9,20,15,7]
我們可以藉助於一個隊列Q:
首先把根元素放到Q中,while Q不爲空時,一個個取出隊首,將其不爲null的子節點加入隊列,直到隊列爲空。
用上面的例子,先把第一層3加入隊列,然後3出隊,加入子節點9,20,這是第二次;然後分別取出9,20,加入子節點15,7,這是第三次,取出15,7,他們沒有子節點,沒有元素入隊,隊爲空,結束。
這個實現是比較簡單的:
//層序 : 利用隊列
public void layerTraversal(TreeNode root) {
Queue<TreeNode> q= new LinkedBlockingQueue<>();
TreeNode tmp ;
q.add(root);
while (q.size()>0){
tmp = q.poll();
System.out.print(tmp.val+" ");
if (tmp.left!=null)q.add(tmp.left);
if (tmp.right!=null)q.add(tmp.right);
}
}
實際上也可以利用數組,只不過稍微複雜點,原理一樣。
//層序 : 利用數組
public void layerTraversal_iter_arr(TreeNode root){
TreeNode []treeNodes= new TreeNode[100];//缺點:難以預測大小,要麼浪費空間,要麼不足報錯
int in=0,out=0;
treeNodes[in++]=root;
while (in>out){
if (treeNodes[out]!=null){
System.out.print(treeNodes[out].val+" ");
treeNodes[in++]=treeNodes[out].left;
treeNodes[in++]=treeNodes[out].right;
}
out++;
}
}
現在加需求了:需要考慮層次,即需要把不同層的分開。
我們如何在原來的基礎上進行擴展了。
考慮前面使用隊列的實現,一個隊列存放了所有的元素,不能區分層次。如果使用兩個隊列存放,那麼不同層不就可以交替使用,從而分開了嗎?
比如有兩個隊列Q1、Q2,按上面的例子:
- 把root 3放到Q1
- Q1中3出隊變空,子節點9,20放到Q2
- Q2出隊變空,9,20的子節點15,7放到Q1
- Q1元素全部出隊,沒有子節點,兩個隊列都空了,程序結束。
那麼現在就好實現了:
輔助雙隊列
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();//結構
LinkedList<TreeNode> q1 = new LinkedList<>();//Q1
LinkedList<TreeNode> q2 = new LinkedList<>();//Q2
LinkedList<TreeNode> cur,empty;//引用,分別指向當前使用的隊列和空隊列
TreeNode tmp;
if (root!=null)q1.add(root); //先把root加入Q1
while (!q1.isEmpty() || !q2.isEmpty()){//至少一個隊列不爲空
List<Integer> level = new LinkedList<>();
if (q1.isEmpty()){ //使cur指向正在使用的隊列,empty指向即將用於存儲的空隊列
cur = q2;
empty = q1;
}else{
cur = q1;
empty= q2;
}
while (!cur.isEmpty()){//當前隊列不爲空,取出所有元素,把元素的非null子節點放到empty隊列
tmp = cur.pop();
level.add(tmp.val);
if (tmp.left!=null)empty.add(tmp.left);
if (tmp.right!=null)empty.add(tmp.right);
}
res.add(level);
}
return res;
}
性能:
執行用時 :1 ms, 在所有 Java 提交中擊敗了91.30%的用戶
內存消耗 :39.8 MB, 在所有 Java 提交中擊敗了5.71%的用戶
空間開銷有點大,可以考慮使用數組代替隊列。
看了下其他答案,上面的解法還可以優化。
我使用的是兩個隊列供不同層次交替使用從而分層,但是還有一種方式————添加分界節點。
輔助單隊列+分界節點
在上面的實現基礎上,層之間添加一個分界節點。
使用上面的例子說明下:
使用X表示分界節點
- 3入隊,X入隊
- 3,X出隊,子節點9,20入隊,X入隊
- 9,20,X出隊,子節點15,7入隊,X入隊
- 15,7,X出隊,沒有元素入隊,X也不用入隊,代表程序結束 (即:X只有在隊列不爲空的情況下入隊)
實現:
public List<List<Integer>> levelOrder(TreeNode root) {
LinkedList<TreeNode> q = new LinkedList<>();//輔助隊列
TreeNode dummy = new TreeNode(Integer.MAX_VALUE);//分界節點
List<List<Integer>> res = new LinkedList<>();//result
List<Integer> level =new LinkedList<>();//層元素數組
TreeNode tmp;//臨時節點
if (root==null)return res;
q.add(root);
q.add(dummy);//第一層和第二層之間的分界節點
while (!q.isEmpty()){
tmp = q.pop();
if (tmp==dummy){//遇到分界節點,表示已經遍歷完一層了
res.add(level);
level = new LinkedList<>();//下一層的存放數組
if(!q.isEmpty()) q.add(dummy);//dummy只有在隊列不爲空的情況下入隊,保證程序正常退出
}else{//非分界節點
level.add(tmp.val);
if (tmp.left!=null)q.add(tmp.left);
if (tmp.right!=null)q.add(tmp.right);
}
}
return res;
}
性能其實和上面差不多,空間其實也沒省多少:元素都會放到隊列中,雙隊列多一個隊列的空間,單隊列多一個分界節點。
兩種思維而已。
遞歸實現
前面兩種層序遍歷都是迭代實現,其實也可以遞歸實現
遞歸實現的話由於不是一層一層的遍歷的,那麼怎麼控制層數???
我們可以添加一個參數level,代表當前的層數,這樣就可以把元素添加到對應的層中的list中了,不管你是怎麼遞歸,只要把元素遍歷完就好了。
實現:
private List<List<Integer>> res = new LinkedList<>();//result
public List<List<Integer>> levelOrder_recur(TreeNode root){
if (root==null)return res;
helper(root,0);
return res;
}
private void helper(TreeNode node,int level){
if (res.size()==level)res.add(new LinkedList<>());//添加某層的List
res.get(level).add(node.val);//獲取level層,並添加元素
if (node.left!=null)helper(node.left,level+1);//遞歸下去
if (node.right!=null)helper(node.right,level+1);
}
遞歸實現代碼量最少,也很容易理解。
總結
總共寫了三種方法,層序和遞歸,如果要歸一個類的話,就是BFS和DFS
- BFS,雙隊列
- BFS,單隊列+分界節點
- DFS,使用level參數記錄層次
三種方法時間複雜度、空間複雜度都是O(N)