樹的前中後層序遍歷(遞歸與非遞歸方式)

Description

LeetCode的第590題與429、589題型類似,都爲樹(不一定是二叉樹)的各種形式的遍歷,因此放在一起總結。

對於上圖,要求求出前序遍歷、後序遍歷和層級遍歷的結果。

Example

前序遍歷結果:[1,3,5,6,2,4]
後序遍歷結果:[5,6,3,2,4,1]
層級遍歷結果:
[
     [1],
     [3,2,4],
     [5,6]
]

Analysis

對於樹我們一般有兩種策略:

  • 廣度優先搜索(BFS):從上到下一層一層的遍歷樹 ,也就是題目要求的層級遍歷。
  • 深度優先搜索(DFS):從一個根節點開始,一直到達某個葉子節點,然後回到根節點到達另一個分支的葉子節點。根據根節點、左節點和右節點之間的相對順序,DFS策略可以進一步區分爲前序、中序和後序。

根據深度優先搜索與廣度優先搜索可以整理出下圖的四種情況:

Solution

對於第一題求前序遍歷,我們可以使用遞歸或者循環來完成,實際上這三道題都是如此。我們先看看遞歸版本:

public class Solution {
	List<Integer> list;
	
    public List<Integer> preorder(Node root) {
    	list = new ArrayList<>();
        if(root == null) return list;
        preorderCore(root);
        return list;
    }

    
    private void preorderCore(Node root) {
    	if(root == null)
    		return;
    	list.add(root.val);
    	for(Node node : root.children)
    		preorderCore(node);
    }
}

前序遍歷就是先將根節點放入結果列表中,然後再將左右子節點放入。遞歸的解法較爲簡單,下面看看循環的解法:

public class Solution2 {
    public List<Integer> preorder(Node root) {
    	LinkedList<Integer> res = new LinkedList<>();
    	if(root == null)
    		return res;
    	Stack<Node> stack = new Stack<>();
    	stack.push(root);
    	while(!stack.isEmpty()) {
    		Node node = stack.pop();
    		res.add(node.val);
    		Collections.reverse(node.children);
    		for(Node children : node.children) {
    			stack.push(children);
    		}
    	}
    	return res;
    }
}

在遞歸中我們使用棧來保存接下來要訪問的節點。首先我們將根節點壓入棧,棧中元素爲[1],然後我們將它彈出至結果列表並把它的子節點翻轉並放入棧,此時棧中元素爲[4, 2, 3];由於棧頂元素爲3,因此將3彈出至結果列表並把它的子節點翻轉並放入棧,此時棧中元素爲[4, 2, 6, 5];棧頂元素爲5,因此將5彈出至結果列表,5沒有子節點,再把6彈出至結果列表。如此反覆,我們便可以通過這種方式得到前序遍歷的結果列表[1, 3, 5, 6, 2, 4]

求後序遍歷與這題異曲同工,同樣先看看遞歸版本:

public class Solution {
	List<Integer> list;
	
    public List<Integer> postorder(Node root) {
    	list = new ArrayList<>();
        if(root == null) return list;
        postorderCore(root);
        return list;
    }
    
    private void postorderCore(Node root) {
    	if(root == null)
    		return;
    	for(Node node : root.children)
    		postorderCore(node);
    	list.add(root.val);
    }
}

我們僅僅將list.add(root.val); 這行代碼放到了遍歷子節點的for語句之後,意味着先將所有子節點加入結果列表,最後再將根節點加入結果列表。下面是使用循環的解法:

public class Solution2 {
    public List<Integer> postorder(Node root) {
    	LinkedList<Integer> res = new LinkedList<>();
    	if(root == null) 
    		return res;
    	
    	Stack<Node> stack = new Stack<>();
    	stack.push(root);
    	while(!stack.isEmpty()) {
    		Node node = stack.pop();
    		res.addFirst(node.val);
    		for(Node children : node.children) {
    			stack.push(children);
    		}
    	}
    	return res;
    }
}

與前序遍歷不同的是我們不需要翻轉子節點列表,但是每次將結果添加到結果列表頭而不是尾。

第三題是層序遍歷(廣度優先搜索),不像上面兩題用遞歸實現更加簡單,我們通過循環來實現會更加簡潔明瞭,思路是使用一個隊列而非棧來保存每一層節點:

public class Solution {
    public List<List<Integer>> levelOrder(Node root) {
    	List<List<Integer>> res = new ArrayList<>();
    	if(root == null) return res;
    	Queue<Node> queue = new LinkedList<>();
    	queue.add(root);
    	while(!queue.isEmpty()) {
    		int size = queue.size();
    		List<Integer> list = new ArrayList<>();
    		while(size-- != 0) {
    			Node node = queue.poll();
    			for(Node children : node.children) queue.add(children);
    			list.add(node.val);
    		}
    		res.add(list);
    	}
    	return res;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章