二叉樹(shù)遞歸迭代及morris層序前中后序遍歷詳解
分析二叉樹(shù)的前序,中序,后序的遍歷步驟
1.層序遍歷
方法一:廣度優(yōu)先搜索
? (以下解釋來(lái)自leetcode官方題解)
我們可以用廣度優(yōu)先搜索解決這個(gè)問(wèn)題。
我們可以想到最樸素的方法是用一個(gè)二元組 (node, level) 來(lái)表示狀態(tài),它表示某個(gè)節(jié)點(diǎn)和它所在的層數(shù),每個(gè)新進(jìn)隊(duì)列的節(jié)點(diǎn)的 level 值都是父親節(jié)點(diǎn)的 level 值加一。最后根據(jù)每個(gè)點(diǎn)的 level 對(duì)點(diǎn)進(jìn)行分類(lèi),分類(lèi)的時(shí)候我們可以利用哈希表,維護(hù)一個(gè)以 level 為鍵,對(duì)應(yīng)節(jié)點(diǎn)值組成的數(shù)組為值,廣度優(yōu)先搜索結(jié)束以后按鍵 level 從小到大取出所有值,組成答案返回即可。
考慮如何優(yōu)化空間開(kāi)銷(xiāo):如何不用哈希映射,并且只用一個(gè)變量 node 表示狀態(tài),實(shí)現(xiàn)這個(gè)功能呢?
我們可以用一種巧妙的方法修改廣度優(yōu)先搜索:
- ?首先根元素入隊(duì)
- ?當(dāng)隊(duì)列不為空的時(shí)候,
求當(dāng)前隊(duì)列的長(zhǎng)度 S_i;?
依次從隊(duì)列中取 S_i?個(gè)元素進(jìn)行拓展,然后進(jìn)入下一次迭代.
它和普通廣度優(yōu)先搜索的區(qū)別在于,普通廣度優(yōu)先搜索每次只取一個(gè)元素拓展,而這里每次取 S_i個(gè)元素。在上述過(guò)程中的第 i?次迭代就得到了二叉樹(shù)的第?i 層的?S_i?個(gè)元素。
為什么這么做是對(duì)的呢?我們觀察這個(gè)算法,可以歸納出這樣的循環(huán)不變式:第 i 次迭代前,隊(duì)列中的所有元素就是第 i 層的所有元素,并且按照從左向右的順序排列。證明它的三條性質(zhì)(你也可以把它理解成數(shù)學(xué)歸納法):
初始化:i=1 的時(shí)候,隊(duì)列里面只有 root,是唯一的層數(shù)為 1 的元素,因?yàn)橹挥幸粋€(gè)元素,所以也顯然滿(mǎn)足「從左向右排列」;
保持:如果 i=k 時(shí)性質(zhì)成立,即第 k 輪中出隊(duì) S_k的元素是第 k 層的所有元素,并且順序從左到右。因?yàn)閷?duì)樹(shù)進(jìn)行廣度優(yōu)先搜索的時(shí)候由低 k 層的點(diǎn)拓展出的點(diǎn)一定也只能是 k+1 層的點(diǎn),并且 k+1 層的點(diǎn)只能由第 k 層的點(diǎn)拓展到,所以由這?S_k?個(gè)點(diǎn)能拓展到下一層所有的?S_k+1 ?個(gè)點(diǎn)。又因?yàn)殛?duì)列的先進(jìn)先出(FIFO)特性,既然第 k 層的點(diǎn)的出隊(duì)順序是從左向右,那么第 k+1 層也一定是從左向右。至此,我們已經(jīng)可以通過(guò)數(shù)學(xué)歸納法證明循環(huán)不變式的正確性。
終止:因?yàn)樵撗h(huán)不變式是正確的,所以按照這個(gè)方法迭代之后每次迭代得到的也就是當(dāng)前層的層次遍歷結(jié)果。至此,我們證明了算法是正確的。
廣度優(yōu)先搜索的簡(jiǎn)化步驟為
初始化隊(duì)列 q,并將根節(jié)點(diǎn) root 加入到隊(duì)列中;
當(dāng)隊(duì)列不為空時(shí):
隊(duì)列中彈出節(jié)點(diǎn) node,加入到結(jié)果中;
如果左子樹(shù)非空,左子樹(shù)加入隊(duì)列;
如果右子樹(shù)非空,右子樹(shù)加入隊(duì)列;
由于題目要求每一層保存在一個(gè)子數(shù)組中,所以我們額外加入了 level 保存每層的遍歷結(jié)果,并使用 for 循環(huán)來(lái)實(shí)現(xiàn)。
看個(gè)圖例用以說(shuō)明:
廣度優(yōu)先需要用隊(duì)列作為輔助結(jié)構(gòu),我們先將根節(jié)點(diǎn)放到隊(duì)列中,然后不斷遍歷隊(duì)列。
首先拿出根節(jié)點(diǎn),如果左子樹(shù)/右子樹(shù)不為空,就將他們放入隊(duì)列中。第一遍處理完后,根節(jié)點(diǎn)已經(jīng)從隊(duì)列中拿走了,而根節(jié)點(diǎn)的兩個(gè)孩子已放入隊(duì)列中了,現(xiàn)在隊(duì)列中就有兩個(gè)節(jié)點(diǎn) 2 和 5。
第二次處理,會(huì)將 2 和 5 這兩個(gè)節(jié)點(diǎn)從隊(duì)列中拿走,然后再將 2 和 5 的子節(jié)點(diǎn)放入隊(duì)列中,現(xiàn)在隊(duì)列中就有三個(gè)節(jié)點(diǎn) 3,4,6。
我們把每層遍歷到的節(jié)點(diǎn)都放入到一個(gè)結(jié)果集中,最后返回這個(gè)結(jié)果集就可以了。
public List<List<Integer>> levelOrder(TreeNode root) { //返回的結(jié)果 List<List<Integer>> res = new ArrayList<List<Integer>>(); if(null == root){ return res; } Queue<TreeNode> queue = new LinkedList<TreeNode>(); //根節(jié)點(diǎn)入隊(duì) queue.add(root); while(!queue.isEmpty()){ //一層的結(jié)果 List<Integer> level = new ArrayList<Integer>(); int levelCount = queue.size(); //添加節(jié)點(diǎn)到一層的List中去 for(int i = 0; i < levelCount ; i ++){ //節(jié)點(diǎn)出隊(duì) TreeNode node = queue.remove(); //節(jié)點(diǎn)的左子樹(shù)入隊(duì) if(node.left != null){ queue.add(node.left); } //節(jié)點(diǎn)的右子樹(shù)入隊(duì) if(node.right != null){ queue.add(node.right); } level.add(node.val); } res.add(level); } return res; }
方法二:遞歸
用廣度優(yōu)先處理是很直觀的,可以想象成是一把刀橫著切割了每一層,但是深度優(yōu)先遍歷就不那么直觀了。
我們開(kāi)下腦洞,把這個(gè)二叉樹(shù)的樣子調(diào)整一下,擺成一個(gè)田字形的樣子。田字形的每一層就對(duì)應(yīng)一個(gè) list。
按照深度優(yōu)先的處理順序,會(huì)先訪問(wèn)節(jié)點(diǎn) 1,再訪問(wèn)節(jié)點(diǎn) 2,接著是節(jié)點(diǎn) 3。
之后是第二列的 4 和 5,最后是第三列的 6。
每次遞歸的時(shí)候都需要帶一個(gè) index(表示當(dāng)前的層數(shù)),也就對(duì)應(yīng)那個(gè)田字格子中的第幾行,如果當(dāng)前行對(duì)應(yīng)的 list 不存在,就加入一個(gè)空 list 進(jìn)去。
public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new ArrayList<List<Integer>>(); if(root==null) { return res; } LinkedList<TreeNode> queue = new LinkedList<TreeNode>(); //將根節(jié)點(diǎn)放入隊(duì)列中,然后不斷遍歷隊(duì)列 queue.add(root); while(queue.size()>0) { //獲取當(dāng)前隊(duì)列的長(zhǎng)度,這個(gè)長(zhǎng)度相當(dāng)于 當(dāng)前這一層的節(jié)點(diǎn)個(gè)數(shù) int size = queue.size(); ArrayList<Integer> tmp = new ArrayList<Integer>(); //將隊(duì)列中的元素都拿出來(lái)(也就是獲取這一層的節(jié)點(diǎn)),放到臨時(shí)list中 //如果節(jié)點(diǎn)的左/右子樹(shù)不為空,也放入隊(duì)列中 for(int i=0;i<size;++i) { TreeNode t = queue.remove(); tmp.add(t.val); if(t.left!=null) { queue.add(t.left); } if(t.right!=null) { queue.add(t.right); } } //將臨時(shí)list加入最終返回結(jié)果中 res.add(tmp); } return res; }
2.前序遍歷
2.1先輸出當(dāng)前節(jié)點(diǎn)(初始的時(shí)候是root節(jié)點(diǎn))
2.2如果左子節(jié)點(diǎn)不為空,則遞歸繼續(xù)前序遍歷
2.3如果右子節(jié)點(diǎn)不為空,則遞歸繼續(xù)前序遍歷
3.中序遍歷
3.1如果當(dāng)前節(jié)點(diǎn)的左子節(jié)點(diǎn)不為空,則遞歸中序遍歷
3.2輸出當(dāng)前節(jié)點(diǎn)
3.3如果當(dāng)前節(jié)點(diǎn)的右子節(jié)點(diǎn)不為空,則遞歸中序遍歷
4.后序遍歷
4.1如果當(dāng)前節(jié)點(diǎn)的左子節(jié)點(diǎn)不為空,則遞歸后序遍歷
4.2如果當(dāng)前節(jié)點(diǎn)的右子節(jié)點(diǎn)不為空,則遞歸后序遍歷
4.3輸出當(dāng)前節(jié)點(diǎn)
如果你按照 根節(jié)點(diǎn) -> 左孩子 -> 右孩子 的方式遍歷,即「先序遍歷」,每次先遍歷根節(jié)點(diǎn),遍歷結(jié)果為 1 2 4 5 3 6 7;
同理,如果你按照 左孩子 -> 根節(jié)點(diǎn) -> 右孩子 的方式遍歷,即「中序序遍歷」,遍歷結(jié)果為 4 2 5 1 6 3 7;
如果你按照 左孩子 -> 右孩子 -> 根節(jié)點(diǎn) 的方式遍歷,即「后序序遍歷」,遍歷結(jié)果為 4 5 2 6 7 3 1;
最后,層序遍歷就是按照每一層從左向右的方式進(jìn)行遍歷,遍歷結(jié)果為 1 2 3 4 5 6 7。
遞歸解法
由于層次遍歷的遞歸解法不是主流,因此只介紹前三種的遞歸解法
前序遍歷--遞歸
public List<Integer> preorderTraversal(TreeNode root) { //遞歸 List<Integer> list = new ArrayList<>(); preOrder(root,list); return list; } public void preOrder(TreeNode root,List<Integer> list){ if(root == null){ return; } list.add(root.val); preOrder(root.left,list); preOrder(root.right,list); }
中序遍歷--遞歸
public List<Integer> inorderTraversal(TreeNode root) { //遞歸 List<Integer> list = new LinkedList<>(); inOrder(root,list); return list; } public void inOrder(TreeNode root,List<Integer> list){ if(root == null){ return; } inOrder(root.left,list); list.add(root.val); inOrder(root.right,list); }
后序遍歷--遞歸
public List<Integer> postorderTraversal(TreeNode root) { //遞歸 List<Integer> list = new LinkedList<>(); postOrder(root,list); return list; } public void postOrder(TreeNode root,List<Integer> list){ if(root == null){ return; } postOrder(root.left,list); postOrder(root.right,list); list.add(root.val); }
三種遞歸遍歷的總結(jié):遞歸終止的條件為碰到空節(jié)點(diǎn)。
迭代解法
前序遍歷--迭代
核心思想:
1.每拿到一個(gè)節(jié)點(diǎn)就把它保存在棧中
2.繼續(xù)對(duì)這個(gè)節(jié)點(diǎn)的左子樹(shù)重復(fù)過(guò)程1,直到左子樹(shù)為空
3.因?yàn)楸4嬖跅V械墓?jié)點(diǎn)都遍歷了左子樹(shù)但是沒(méi)有遍歷右子樹(shù),所以對(duì)棧中節(jié)點(diǎn)出棧并對(duì)它的右子樹(shù)重復(fù)過(guò)程1
4.直到遍歷完所有節(jié)點(diǎn)
public List<Integer> preorderTraversal(TreeNode root) { //迭代 List<Integer> list = new ArrayList<>(); if(root == null){ return list; } Deque<TreeNode> stack = new LinkedList<TreeNode>(); //臨時(shí)節(jié)點(diǎn),幫助遍歷二叉樹(shù) TreeNode node = root; //棧的作用是用來(lái)短暫的保存遍歷節(jié)點(diǎn)的值,以助于最后值的返回 while(!stack.isEmpty() || node != null){ while(node != null){ list.add(node.val); stack.push(node); node = node.left; } node = stack.pop(); node = node.right; } return list; }
中序遍歷--迭代
public List<Integer> inorderTraversal(TreeNode root) { //迭代 List<Integer> list = new ArrayList<>(); if(root == null){ return list; } Deque<TreeNode> stack = new LinkedList<>(); while(!stack.isEmpty() || root != null){ while(root != null){ stack.push(root); root = root.left; } root = stack.pop(); list.add(root.val);//出棧的時(shí)候才將父節(jié)點(diǎn) 的值加入到結(jié)果中 root = root.right; } return list; }
和前序遍歷的代碼完全相同,只是在出棧的時(shí)候才將父節(jié)點(diǎn)?的值加入到結(jié)果中。
后序遍歷--迭代
public List<Integer> postorderTraversal(TreeNode root) { LinkedList<Integer> list = new LinkedList<>(); Stack<TreeNode> stack = new Stack<>(); while (root != null || !stack.isEmpty()) { if (root != null) { stack.push(root); list.addFirst(root.val);//addFirst():向鏈表首部添加元素 root = root.right; } else { root = stack.pop(); root = root.left; } } return list; }
三種迭代解法的總結(jié):
前序遍歷和后序遍歷之間的關(guān)系:
前序遍歷順序?yàn)椋焊?-> 左 -> 右
后序遍歷順序?yàn)椋鹤?-> 右 -> 根
如果1: 將前序遍歷中節(jié)點(diǎn)插入結(jié)果鏈表尾部的邏輯,修改為將節(jié)點(diǎn)插入結(jié)果鏈表的頭部
那么結(jié)果鏈表就變?yōu)榱耍河?-> 左 -> 根
如果2: 將遍歷的順序由從左到右修改為從右到左,配合如果1
那么結(jié)果鏈表就變?yōu)榱耍鹤?-> 右 -> 根
這剛好是后序遍歷的順序
基于這兩個(gè)思路,想一下如何處理:
修改前序遍歷代碼中,節(jié)點(diǎn)寫(xiě)入結(jié)果鏈表的代碼,將插入隊(duì)尾修改為插入隊(duì)首
修改前序遍歷代碼中,每次先查看左節(jié)點(diǎn)再查看右節(jié)點(diǎn)的邏輯,變?yōu)橄炔榭从夜?jié)點(diǎn)再查看左節(jié)點(diǎn)
前序遍歷和中序遍歷之間的關(guān)系:
和前序遍歷的代碼完全相同,只是在出棧的時(shí)候才將父節(jié)點(diǎn)的值加入到結(jié)果中。
Morris遍歷
遍歷特點(diǎn):Morris 遍歷利用了樹(shù)中大量空閑指針的特性
當(dāng)前節(jié)點(diǎn)cur,一開(kāi)始cur來(lái)到整樹(shù)頭
1)cur無(wú)左樹(shù),cur? = cur.right(cur右移)
2)cur有左樹(shù),找到左樹(shù)最右節(jié)點(diǎn),mostright;此時(shí)我們又可以分為兩種情況,一種是葉子節(jié)點(diǎn)添加 right 指針的情況,一種是去除葉子節(jié)點(diǎn) right 指針的情況
?A.mostright 的右指針指向null的mostright.right = cur,? cur = cur.left(cur左移)
?B.mostright 的右指針指向cur的mostright.right = null(為了防止重復(fù)執(zhí)行,則需要去掉 right 指針),? ????cur = cur.right(cur右移)
當(dāng)cur == null時(shí),整個(gè)過(guò)程結(jié)束。
遍歷特點(diǎn):有左樹(shù)節(jié)點(diǎn)必遍歷到兩次,沒(méi)有左樹(shù)的節(jié)點(diǎn)必遍歷到一次
public static void morris(Node head){ if(head == null){ return; } Node cur = head; Node mostRight = null; while(cur != null){ //cur有沒(méi)有左樹(shù) mostRight = cur.left; if(mostRight != null){//有左樹(shù)的情況 //找到cur左樹(shù)上,真實(shí)的最右節(jié)點(diǎn) //前者說(shuō)明是第一次來(lái)到當(dāng)前的cur,后者說(shuō)明是第二次來(lái)到當(dāng)前的cur while(mostRight.right != null && mostRight.right != cur){ mostRight = mostRight.right; } //從while中出來(lái),mostRight一定是cur左樹(shù)上的最右節(jié)點(diǎn) if(mostRight.right == null){ mostRight.right = cur; cur = cur.left; continue;//結(jié)束的是外層的循環(huán)?。。。。。。。。。。。?! }else{//走到這里意味著:mostRight.right == cur mostRight.right = null; } } //cur沒(méi)有左樹(shù) cur = cur.right; } }
空間復(fù)雜度:利用空閑的指針,使用了兩個(gè)變量完成了遍歷,空間復(fù)雜度是常數(shù)級(jí)別的
時(shí)間復(fù)雜度:?
morris--前序遍歷
第一次來(lái)到一個(gè)節(jié)點(diǎn),就打印;第二次來(lái)到這個(gè)節(jié)點(diǎn),不打印
public static void morrisPre(Node head){ if(head == null){ return; } Node cur = head; Node mostRight = null; while(cur != null){ mostRight = cur.left; if(mostRight != null){ while(mostRight.right != null && mostRight.right != cur){ mostRight = mostRight.right; } if(mostRight.right == null){ mostRight.right = cur; System.out.print(cur.value + " "); cur = cur.left; continue; }else{ mostRight.right = null; } }else{ System.out.print(cur.value + " "); } cur = cur.right; } System.out.println(); }
morris--中序遍歷
對(duì)于能回到自己兩次的節(jié)點(diǎn),第二次時(shí)打印,對(duì)于只能來(lái)到自己一次的節(jié)點(diǎn),直接打印
只要一個(gè)節(jié)點(diǎn)要往右移動(dòng),就打印
public static void morrisIn(Node head){ if(head == null){ return; } Node cur = head; Node mostRight = null; while(cur != null){ mostRight = cur.left; if(mostRight != null){ while(mostRight.right != null && mostRight.right != cur){ mostRight = mostRight.right; } if(mostRight.right == null){ mostRight.right = cur; cur = cur.left; continue; }else{ mostRight.right = null; } } System.out.print(cur.value + " "); cur = cur.right; } System.out.println(); }
morris--后序遍歷:
public static void morrisPos(Node head) { if(head == null) { return; } Node cur = head; Node mostRight = null; while(cur != null) { mostRight = cur.left; if(mostRight != null) { while(mostRight.right != null && mostRight.right != cur) { mostRight = mostRight.right; } if(mostRight.right == null) { mostRight.right = cur; cur = cur.left; continue; }else { mostRight.right = null; printEdge(cur.left);//逆序打印左樹(shù)的右邊界 } } cur = cur.right; } printEdge(head);//最后打印整棵樹(shù)的右邊界 System.out.println(); } public static void printEdge(Node head) { Node tail = reverseEdge(head); Node cur = tail; while(cur != null) { System.out.print(cur.value + " "); cur = cur.right; } reverseEdge(tail); } private static Node reverseEdge(Node from) { Node pre = null; Node next = null; while(from != null) { next = from.right; from.right = pre; pre = from; from = next; } return pre; }
Morris后序遍歷比較復(fù)雜,可以看看相關(guān)的視頻講解--左神算法系列。
以上就是二叉樹(shù)遞歸迭代及morris層序前中后序遍歷詳解的詳細(xì)內(nèi)容,更多關(guān)于二叉樹(shù)遞歸迭代遍歷詳解的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA連接mysql報(bào)錯(cuò)的問(wèn)題及解決方法
這篇文章主要介紹了IDEA連接mysql報(bào)錯(cuò)的問(wèn)題及解決方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08Java class文件格式之方法_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java class文件格式之方法的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06SpringCloud之監(jiān)控?cái)?shù)據(jù)聚合Turbine的實(shí)現(xiàn)
這篇文章主要介紹了SpringCloud之監(jiān)控?cái)?shù)據(jù)聚合Turbine的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08詳解WebSocket+spring示例demo(已使用sockJs庫(kù))
本篇文章主要介紹了WebSocket spring示例demo(已使用sockJs庫(kù)),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01如何解決線程太多導(dǎo)致java socket連接池出現(xiàn)的問(wèn)題
這篇文章主要介紹了如何解決線程太多導(dǎo)致socket連接池出現(xiàn)的問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12MybatisPlus查詢(xún)條件為空字符串或null問(wèn)題及解決
這篇文章主要介紹了MybatisPlus查詢(xún)條件為空字符串或null問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06