前言:二叉樹的遍歷形式有很多,比如前序、中序、後序、層序遍歷,在最近的一次面試中,面試官要求手寫層序遍歷程式碼(非遞迴的形式),由此可見遍歷的重要性.本篇部落格我們就來看一下二叉樹的幾種遍歷方式.本篇部落格語言均採用java實現:
目錄:
一:二叉樹簡介
二:前序遍歷
三:中序遍歷
四:後序遍歷
五:層序遍歷
六:總結
一:二叉樹簡介
1.1:二叉樹結構
維基百科中對二叉樹是這樣定義的:(英文名:Binary tree)是每個節點最多隻有兩個分支(即不存在分支度大於2的節點)的樹結構。通常分支被稱作“左子樹”或“右子樹”,二元樹的分支具有左右次序,不能隨意顛倒.如下圖,是一顆標準的二叉樹(其中標明瞭節點的屬性,比如左一、右二等,下面用到的示例均採用該樹)
1.2:二叉樹的遍歷
二叉樹的遍歷是指按照一定的規則訪問二叉樹的每一個結點並且檢視它的值。有很多常見的順序來訪問所有的結點,而且每一種都有有用的性質。一般分為前序遍歷(根 左 右),中序遍歷(左 根 右),後序遍歷(左 右 根)
二:前序遍歷(根、左、右)
前序遍歷是指按照根左右的順序依次遍歷,使用非遞迴遍歷,一般會用到棧,利用先進後出的特性來達到訪問二叉樹節點目的。來看一下
2.1 前序遍歷非遞迴實現思路:
①:首先將根節點放入到stack中儲存
②:遍歷棧,如果stack不為空,直接彈出根節點
③:如果右節點不為空,將右節點放入到棧中
④:如果左節點不為空,將左節點放入到棧中
解釋:因為棧都是後進先出的,所以在遍歷子樹的時候應該先將右節點放入棧中,再把左節點放入棧中.
2.2 程式碼實現:
/** * 前序遍歷 (根 左 右) * * * @param head */ public List<Integer> preOrderIteration(TreeNode head) { if (head == null) { return new ArrayList<>(); } // 結果集 List<Integer> resultList = new ArrayList<>(); // 棧 Stack<TreeNode> stack = new Stack<>(); // 首先放入頭節點 stack.push(head); while (!stack.isEmpty()) { TreeNode node = stack.pop(); resultList.add(node.val); // 放入右節點 if (node.right != null) { stack.push(node.right); } // 放入左節點 if (node.left != null) { stack.push(node.left); } } return resultList; }
按照上圖給的二叉樹節點,走完測試用例結果:
2.3: 棧中的資料變化(從下往上看)
三:中序遍歷(左、根、右)
中序遍歷的非遞迴實現思路:
①一直遞迴的遍歷左子節點,然後彈出左子節點,直到不為null
② 然後彈出最近的一個左節點,如果它的right節點不為null,將當前的節點置為right
③ 然後依次繼續①的步驟 遍歷到左子樹為null停止
程式碼實現:
/** * 中序遍歷 (左 根 右) * * @param head */ public static List<Integer> inOrderIteration(TreeNode head) { if (head == null) { return new ArrayList<>(); } // 結果集 List<Integer> resultList = new ArrayList<>(); TreeNode cur = head; Stack<TreeNode> stack = new Stack<>(); while (!stack.isEmpty() || cur != null) { // 一直先把左節點依次放入棧中 while (cur != null) { stack.push(cur); cur = cur.left; } TreeNode node = stack.pop(); resultList.add(node.val); if (node.right != null) { cur = node.right; } } return resultList; }
按照上圖給的二叉樹節點,走完測試用例結果:
棧中的資料節點變化(從下往上看):
四:後序遍歷(左 右 根)
後序遍歷需要用到兩個棧,可以想一想為什麼需要兩個棧? stack1負責將節點的值依次儲存,stack2負責儲存stack1彈出的節點值 ,因為後序遍歷的根節點是最後遍歷的順序,因此需要一箇中轉的棧首先將根放入,然後再放入右節點,再放入左節點.最後再逆序(pop)出去就是後序遍歷的順序了。
①首先將根節點放入到stack1中,再彈出根節點,放入到stack2,保證stack2儲存的第一個節點是根節點
②再將根節點的左右節點依次放入到stack1中,注意這裡是先加入左節點,再加入右節點
③最終再將stack2的節點依次pop出去加入到結果集裡面
/** * 後序遍歷 (左 右 根) * * * @param head */ public static List<Integer> postOrderIteration(TreeNode head) { if (head == null) { return new ArrayList(); } List<Integer> resultList= new ArrayList<>(); Stack<TreeNode> stack1 = new Stack<>(); Stack<TreeNode> stack2 = new Stack<>(); stack1.push(head); while (!stack1.isEmpty()) { TreeNode node = stack1.pop(); stack2.push(node); if (node.left != null) { stack1.push(node.left); } if (node.right != null) { stack1.push(node.right); } } while (!stack2.isEmpty()) { resultList.add(stack2.pop().val); } return resultList; }
棧中節點的變化圖:
五:層序遍歷
實現思路:
層序遍歷需要用到Queue(先進先出),而不是棧,因為它是順序新增的。
①宣告一個佇列,首先新增進去根節點
②彈出根節點,再依次加入左節點,再加入右節點
③依次迴圈,再彈出上一次的左節點,再加入左節點的左節點、左節點的右節點
程式碼實現:
/** * 層序遍歷 * @param root * @return */ public List<List<Integer>> levelOrder(TreeNode root) { // 返回的結果集 List<List<Integer>> res = new ArrayList<>(); // 佇列 Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { List<Integer> temp = new ArrayList<>(); int size = queue.size(); for (int i = 0; i < size; i++) { TreeNode poll = queue.poll(); temp.add(poll.val); if (poll.left != null) { queue.add(poll.left); } if (poll.right != null) { queue.add(poll.right); } } res.add(temp); } return res; }
佇列中的資料變化圖(從下往上看):
六:總結
二叉樹作為一種經典的資料結構, 在實現遍歷的時候,採用遞迴的方式非常簡單。但是在面試中,一旦讓面試者寫非遞迴的實現方式,很多人就望而卻步了。對於藉助於棧還是佇列按照何種規律來巧妙實現所有的遍歷,其實也是值得深入學習的。本篇部落格就分析了二叉樹的四種遍歷方式,全部採用非遞迴的方法,希望大家對於二叉樹的最基本操作遍歷有一個深刻的理解。