二叉樹各種遍歷真的很難?大sai帶你拿捏!
前言
大家好,我是bigsai,好久不見,甚是想念!
今天帶大家征服二叉樹的前中后序遍歷,包含遞歸和非遞歸方式,學(xué)到就是賺到!
很多時(shí)候我們需要使用非遞歸的方式實(shí)現(xiàn)二叉樹的遍歷,非遞歸枚舉相比遞歸方式的難度要高出一些,效率一般會(huì)高一些,并且前中后序枚舉的難度呈一個(gè)遞增的形式,非遞歸方式的枚舉有人停在非遞歸后序,有人停在非遞歸中序,有人停在非遞歸前序(這就有點(diǎn)拉胯了啊兄弟)。
我們回顧遞歸,它底層其實(shí)是維護(hù)一個(gè)棧,將數(shù)據(jù)存到棧中,每次拋出棧頂?shù)臄?shù)據(jù)進(jìn)行處理(也就是遞歸、dfs的方向化、極端化枚舉特征非常明顯),我們駕馭遞歸的時(shí)候更重要的是掌握上下層之間的邏輯關(guān)系。
而非遞歸方式我們除了需要掌握上下層的邏輯關(guān)系之外,要手動(dòng)的處理各種條件變更的細(xì)節(jié), 遞歸是一個(gè)一來一回的過程,如果我們的邏輯只是在單趟的來或者回中還好,有時(shí)候甚至要自己維護(hù)來和回的狀態(tài),所以邏輯上難度還是比較大的。
二叉樹的前序遍歷
二叉樹的前序遍歷是最簡單的,其枚舉方式就是一個(gè)最簡單的dfs,學(xué)會(huì)了二叉樹的前序遍歷,那么后面入門dfs就容易很多。
二叉樹的前序遍歷的枚舉規(guī)則為:根結(jié)點(diǎn) ---> 左子樹 ---> 右子樹,也就是給定一棵樹,輸出操作當(dāng)前節(jié)點(diǎn),然后枚舉左子樹(左子樹依然按照根左右的順序進(jìn)行),最后枚舉右子樹(右子樹也按照根左右的順序進(jìn)行),這樣得到的一個(gè)枚舉序列就是二叉樹的前序遍歷序列(也叫先序)。
前序遍歷在二叉樹樹的順序可以看下圖(紅色箭頭指向的表示需要訪問的,可以看出從父節(jié)點(diǎn)枚舉下來第一次就要被訪問)。
在具體實(shí)現(xiàn)的方式上,有遞歸方式和非遞歸方式實(shí)現(xiàn)。
遞歸
遞歸方式實(shí)現(xiàn)的二叉樹前序遍歷很簡單,遞歸我們只需要考慮初始情況、結(jié)束邊界、中間正常點(diǎn)邏輯。
初始情況:從root根節(jié)點(diǎn)開始枚舉,函數(shù)執(zhí)行傳入root根節(jié)點(diǎn)作為參數(shù)。
結(jié)束邊界:節(jié)點(diǎn)的左(或右)子節(jié)點(diǎn)為null那么就停止對應(yīng)節(jié)點(diǎn)的遞歸執(zhí)行。
正常點(diǎn)邏輯:先處理當(dāng)前點(diǎn)(存儲或輸出),遞歸調(diào)用枚舉左子樹(如果不為null),遞歸調(diào)用枚舉右子樹(如果不為null)。
剛好力扣144二叉樹的前序遍歷可以嘗試ac:
- class Solution {
- List<Integer>value=new ArrayList();
- public List<Integer> preorderTraversal(TreeNode root) {
- qianxu(root);
- return value;
- }
- private void qianxu(TreeNode node) {
- if(node==null)
- return;
- value.add(node.val);
- qianxu(node.left);
- qianxu(node.right);
- }
- }
非遞歸
非遞歸的前序還是非常簡單的,前序遍歷的規(guī)則是:根節(jié)點(diǎn),左節(jié)點(diǎn),右節(jié)點(diǎn)。但是根左方向一直下去,手動(dòng)枚舉又沒有遞歸回的過程,一直下去我們怎么找到回來時(shí)候的右?guī)c(diǎn)呢?
用棧將路過的節(jié)點(diǎn)先存儲,第一次枚舉節(jié)點(diǎn)輸出儲存然后放入棧中,第二次就是被拋出時(shí)候枚舉其右側(cè)節(jié)點(diǎn)。
它的規(guī)則大致為:
一直訪問當(dāng)前節(jié)點(diǎn)并用棧存儲,節(jié)點(diǎn)指向左節(jié)點(diǎn),直到左孩子為null。
拋出棧頂不訪問。如果有右節(jié)點(diǎn),訪問其右節(jié)點(diǎn)重復(fù)步驟1,如有沒右節(jié)點(diǎn),繼續(xù)重復(fù)步驟2拋出。
這樣的一個(gè)邏輯,就會(huì)從根出發(fā)一直先往左訪問,訪問結(jié)束根、左之后再訪問右節(jié)點(diǎn)(子樹),得到一個(gè)完成的前序遍歷的序列。
具體實(shí)現(xiàn)的代碼為:
- class Solution {
- public List<Integer> preorderTraversal(TreeNode root) {
- List<Integer>value=new ArrayList();
- Stack<TreeNode> q1 = new Stack();
- while(!q1.isEmpty()||root!=null)
- {
- while (root!=null) {
- value.add(root.val);
- q1.push(root);
- root=root.left;
- }
- root=q1.pop();//拋出
- root=root.right;//準(zhǔn)備訪問其右節(jié)點(diǎn)
- }
- return value;
- }
- }
二叉樹的中序遍歷
二叉樹的中序遍歷出現(xiàn)的頻率還是蠻高的,如果是二叉排序樹相關(guān)問題還是蠻多的,你要知道二叉排序樹的中序遍歷是一個(gè)有序的序列,如果求二叉排序樹的topk問題,非遞歸中序那效率是非常高的。
中序遍歷在二叉樹樹的順序可以看下圖(紅色箭頭指向的表示需要訪問的,可以看出如果子樹為null,那肯定要訪問,否則就是從左子樹回來的時(shí)候才訪問這個(gè)節(jié)點(diǎn))。
遞歸方式
遞歸方式實(shí)現(xiàn)很簡單,其邏輯和前序遞歸相似的,力扣94剛好有二叉樹中序遍歷,這里我直接放代碼:
- class Solution {
- public List<Integer> inorderTraversal(TreeNode root) {
- List<Integer>value=new ArrayList<Integer>();
- zhongxu(root,value);
- return value;
- }
- private void zhongxu(TreeNode root, List<Integer> value) {
- if(root==null)
- return;
- zhongxu(root.left, value);
- value.add(root.val);
- zhongxu(root.right, value);
- }
- }
非遞歸方式
非遞歸的中序和前序是非常相似的,前序遍歷的規(guī)則是:根節(jié)點(diǎn),左節(jié)點(diǎn),右節(jié)點(diǎn)。中序遍歷的順序是左節(jié)點(diǎn),根節(jié)點(diǎn),右節(jié)點(diǎn) ,在前序中先根后左其實(shí)是有點(diǎn)覆蓋的關(guān)系(這個(gè)左就是下一個(gè)跟),在其非遞歸枚舉實(shí)現(xiàn)上我們訪問右節(jié)點(diǎn)時(shí)候是先拋出父節(jié)點(diǎn)不訪問,直接訪問父節(jié)點(diǎn)的右節(jié)點(diǎn),如果拋出父節(jié)點(diǎn)訪問這個(gè)父節(jié)點(diǎn),其實(shí)它就是一個(gè)中間順序的節(jié)點(diǎn)。
它的規(guī)則大致為:
枚舉當(dāng)前節(jié)點(diǎn)(不存儲輸出)并用棧存儲,節(jié)點(diǎn)指向左節(jié)點(diǎn),直到左孩子為null。
拋出棧頂訪問。如果有右節(jié)點(diǎn),訪問其右節(jié)點(diǎn)重復(fù)步驟1,如有沒右節(jié)點(diǎn),繼續(xù)重復(fù)步驟2拋出。
這樣的一個(gè)邏輯,就會(huì)形成一個(gè)中序序列,因?yàn)槿~子節(jié)點(diǎn)的左右都為null,這樣的規(guī)則依然滿足中序。
實(shí)現(xiàn)代碼為:
- class Solution {
- public List<Integer> inorderTraversal(TreeNode root) {
- List<Integer>value=new ArrayList<Integer>();
- Stack<TreeNode> q1 = new Stack();
- while(!q1.isEmpty()||root!=null)
- {
- while (root!=null) {
- q1.push(root);
- root=root.left;
- }
- root=q1.pop();//拋出
- value.add(root.val);
- root=root.right;//準(zhǔn)備訪問其右節(jié)點(diǎn)
- }
- return value;
- }
- }
二叉樹的后序遍歷
二叉樹的后序遍歷非遞歸方式實(shí)現(xiàn)起來難度最大的,能夠手寫非遞歸后序,一定能亮瞎面試官的眼!
后序遍歷在二叉樹樹的順序可以看下圖(紅色箭頭指向的表示需要訪問的,可以看出如果子樹為null,那肯定要訪問,否則就是從右子樹回來的時(shí)候才訪問這個(gè)節(jié)點(diǎn))。
遞歸
二叉樹遞歸方式后序遍歷很簡單,跟前序中序的邏輯一樣,在力扣145有后序的code測試大家可以自己嘗試一下。
這里直接放我寫的后序遞歸方式:
- class Solution {
- List<Integer>value=new ArrayList<>();
- public List<Integer> postorderTraversal(TreeNode root) {
- houxu(root);
- return value;
- }
- private void houxu(TreeNode root) {
- if(root==null)
- return;
- houxu(root.left);
- houxu(root.right);//右子樹回來
- value.add(root.val);
- }
- }
非遞歸
非遞歸的后序就稍微難一點(diǎn)了,大家可以回顧一下二叉樹的前序和中序遍歷,其實(shí)都是只用一個(gè)棧直接拋出就可以找到右節(jié)點(diǎn),拋出后棧就空了,但是這個(gè)后序遍歷的順序是 左子樹 ---> 右子樹 --->根節(jié)點(diǎn),也就是處理完右節(jié)點(diǎn),還要再回到根節(jié)點(diǎn)訪問。所以從邏輯結(jié)構(gòu)上和前序、中序的非遞歸實(shí)現(xiàn)方式有一些略有不同。
前序,中序遍歷的順序提取為:
前序: 中入棧—>左入棧—>左孩子入出—>左出棧—>中出棧—>右入棧—>右孩子入出—>右出棧
前序遍歷同一大流程按照入棧順序即形成一個(gè)前序序列
中序: 中入棧—>左入棧—>左孩子入出—>左出棧—>中出棧—>右入棧 —>右孩子入出—>右出棧
中序遍歷同一大流程按照出棧順序即形成一個(gè)中序序列
但是后序的話應(yīng)該怎么考慮呢?
其實(shí)就是在從左孩子到中準(zhǔn)備出棧的時(shí)候,先不出棧記成第一次,再將它放入棧中,如果從右孩子返回那么這個(gè)節(jié)點(diǎn)就是第三次遍歷(第一次訪問中,然后枚舉左返回第二次,枚舉右返回第三次),這時(shí)候?qū)⑵鋻伋鼍托纬梢粋€(gè)后序。
如果不理解這里畫了一個(gè)簡單的圖幫助理解:
思路理解了,怎么實(shí)現(xiàn)呢?最簡單的就是使用一個(gè)hashmap存儲節(jié)點(diǎn)訪問次數(shù)。
附一下個(gè)人實(shí)現(xiàn)的代碼:
- class Solution {
- public List<Integer> postorderTraversal(TreeNode root) {
- List<Integer> value=new ArrayList();
- Stack<TreeNode> q1 = new Stack();
- Map<TreeNode,Integer >map=new HashMap<>();
- while(!q1.isEmpty()||root!=null)
- {
- if (root!=null) {
- q1.push(root);
- map.put(root, 1); //t.value標(biāo)記這個(gè)值節(jié)點(diǎn)出現(xiàn)的次數(shù)
- root=root.left;
- }
- else {
- root=q1.peek();
- if(map.get(root)==2) {//第二次訪問,拋出
- q1.pop();
- value.add(root.val);
- root=null;//需要往上走
- }
- else {
- map.put(root, 2);
- root=root.right;
- }
- }
- }
- return value;
- }
- }
但是這個(gè)情況如果面試官問你如果有hash沖突怎么辦?雖然這種概率非常小幾乎不會(huì)但是面試官不會(huì)放過你,但是還是要用正統(tǒng)方法來實(shí)現(xiàn)。
那么正統(tǒng)方法怎么解決呢?
也很容易,用一個(gè)pre節(jié)點(diǎn)一直保存上一次拋出訪問的點(diǎn),如果當(dāng)前被拋出的右孩子是pre或者當(dāng)前節(jié)點(diǎn)右為null,那么就將這個(gè)點(diǎn)拋出,否則就將它"回爐重造"一次!
實(shí)現(xiàn)代碼為:
- class Solution {
- public List<Integer> postorderTraversal(TreeNode root) {
- TreeNode temp=root;//枚舉的臨時(shí)節(jié)點(diǎn)
- List<Integer>value=new ArrayList<>();
- TreeNode pre=null;//前置節(jié)點(diǎn)
- Stack<TreeNode>stack=new Stack<>();
- while (!stack.isEmpty()||temp!=null){
- while(temp!=null){
- stack.push(temp);
- temp=temp.left;
- }
- temp=stack.pop();
- if(temp.right==pre||temp.right==null)//需要彈出
- {
- value.add(temp.val);
- pre=temp;
- temp=null;//需要重新從棧中拋出
- }else{
- stack.push(temp);
- temp=temp.right;
- }
- }
- return value;
- }
- }
是不是覺得非常巧妙?那我再說一種騷操作的代碼,你看 左右中,它反過來就是中右左,這不就是一個(gè)反的前序遍歷嘛!所以進(jìn)行一次反的前序遍歷,然后將結(jié)果翻轉(zhuǎn)一下也能得到這個(gè)值啦,當(dāng)然,你使用雙棧同時(shí)翻轉(zhuǎn)也是一樣的道理!
實(shí)現(xiàn)代碼為:
- class Solution {
- public List<Integer> postorderTraversal(TreeNode root) {
- List<Integer>value=new ArrayList();
- Stack<TreeNode> q1 = new Stack();
- while(!q1.isEmpty()||root!=null)
- {
- while (root!=null) {
- value.add(root.val);
- q1.push(root);
- root=root.right;
- }
- root=q1.pop();//拋出
- root=root.left;//準(zhǔn)備訪問其右節(jié)點(diǎn)
- }
- Collections.reverse(value);//將結(jié)果翻轉(zhuǎn)
- return value;
- }
- }
給兩個(gè)序列如何構(gòu)造一棵二叉樹
經(jīng)常會(huì)遇到給兩個(gè)序列確定一個(gè)二叉樹,當(dāng)然這個(gè)序列其中之一必須包含中序遍歷序列。前序、中序確定二叉樹和后序、中序確定一個(gè)二叉樹的原理一致。
前序中序確定一棵二叉樹
根據(jù)一棵樹的前序遍歷與中序遍歷構(gòu)造二叉樹。當(dāng)然也是力扣105的原題。
注意:你可以假設(shè)樹中沒有重復(fù)的元素。
分析:
給定一個(gè)前序序列和一個(gè)中序序列,且里面沒有重復(fù)的元素,如何構(gòu)造一棵二叉樹呢?我們可以先單獨(dú)觀察兩個(gè)序列的特征:
前序遍歷:遍歷規(guī)則為(根,[左側(cè)區(qū)域],[右側(cè)區(qū)域])。
中序遍歷:遍歷規(guī)則為([左側(cè)區(qū)域],根,[右側(cè)區(qū)域])。
其中前序遍歷的左側(cè)區(qū)域和中序遍歷的左側(cè)區(qū)域包含元素的范圍相同,根也是相同的。所以可以進(jìn)行這樣的操作:
根據(jù)前序遍歷的第一個(gè)找到根節(jié)點(diǎn),可以確定根。
通過中序遍歷找到根節(jié)點(diǎn)的值,這樣可以知道左側(cè)區(qū)域和右側(cè)區(qū)域節(jié)點(diǎn)個(gè)數(shù)多少。
根節(jié)點(diǎn)左側(cè)區(qū)域由前中序列確定的左側(cè)區(qū)域確定,根節(jié)點(diǎn)的右側(cè)節(jié)點(diǎn)由前中序序列的右側(cè)區(qū)域確定。
一些操作可以借助這張圖進(jìn)行理解:
具體的實(shí)現(xiàn)上,可以使用一個(gè)HashMap存儲中序存儲的序列,避免重復(fù)計(jì)算。實(shí)現(xiàn)的代碼為:
- /**
- * Definition for a binary tree node.
- * public class TreeNode {
- * int val;
- * TreeNode left;
- * TreeNode right;
- * TreeNode(int x) { val = x; }
- * }
- */
- class Solution {
- public TreeNode buildTree(int[] preorder, int[] inorder) {
- if(preorder.length==0)
- return null;
- TreeNode root=new TreeNode(preorder[0]);
- Map<Integer, Integer>map=new HashMap<Integer, Integer>();
- for(int i=0;i<inorder.length;i++)
- {
- map.put(inorder[i], i);
- }
- return buildTree(preorder,0,preorder.length-1, map,0,inorder.length-1);
- }
- private TreeNode buildTree(int[] preorder, int preStart, int preEnd, Map<Integer, Integer> map, int inStart, int inEnd) {
- // TODO Auto-generated method stub
- if(preEnd<preStart||inEnd<inStart)
- return null;
- TreeNode node=new TreeNode(preorder[preStart]);
- int i=map.get(preorder[preStart]);//節(jié)點(diǎn)的值
- int leftlen=i-inStart;//左面的長度
- node.left=buildTree(preorder, preStart+1, preStart+1+leftlen, map, inStart, i-1);
- node.right=buildTree(preorder, preStart+leftlen+1,preEnd, map, i+1, inEnd);
- return node;
- }
- }
中序后序確定一個(gè)二叉樹
根據(jù)一棵樹的中序遍歷與后序遍歷構(gòu)造二叉樹,力扣106題
注意:你可以假設(shè)樹中沒有重復(fù)的元素。
例如,給出
中序遍歷 inorder = [9,3,15,20,7]
后序遍歷 postorder = [9,15,7,20,3]
返回如下的二叉樹:
- 3
- / \
- 9 20
- / \
- 15 7
分析:
有了上面的分析,那么通過一個(gè)后序遍歷和中序遍歷去構(gòu)造一棵二叉樹,其實(shí)原理和前面的也是一樣的。
后序遍歷:遍歷規(guī)則為([左側(cè)區(qū)域],[右側(cè)區(qū)域],根)。
中序遍歷:遍歷規(guī)則為([左側(cè)區(qū)域],根,[右側(cè)區(qū)域])。
具體實(shí)現(xiàn)的代碼為:
- /**
- * Definition for a binary tree node.
- * public class TreeNode {
- * int val;
- * TreeNode left;
- * TreeNode right;
- * TreeNode(int x) { val = x; }
- * }
- */
- class Solution {
- public TreeNode buildTree(int[] inorder,int[] postorder) {
- if(postorder.length==0)
- return null;
- Map<Integer, Integer>map=new HashMap<Integer, Integer>();
- for(int i=0;i<inorder.length;i++)
- {
- map.put(inorder[i], i);
- }
- return buildTree(postorder,0,postorder.length-1, map,0,inorder.length-1);
- }
- private TreeNode buildTree(int[] postorder, int postStart, int postEnd, Map<Integer, Integer> map, int inStart, int inEnd) {
- // TODO Auto-generated method stub
- if(postEnd<postStart||inEnd<inStart)
- return null;
- TreeNode node=new TreeNode(postorder[postEnd]);
- int i=map.get(postorder[postEnd]);
- int leftlen=i-inStart;
- node.left=buildTree(postorder, postStart,postStart+leftlen-1, map, inStart, i-1);
- node.right=buildTree(postorder, postStart+leftlen,postEnd-1, map, i+1, inEnd);
- return node;
- }
- }
結(jié)語
好了,今天到這里就先介紹完了,但二叉樹的內(nèi)容遠(yuǎn)不止于此,還有非常牛批的Morris遍歷,以及一些二叉樹騷操作技巧??紗栴}后面還會(huì)慢慢總結(jié)分享給大家。
原創(chuàng)不易,如果本文對你有幫助,還請動(dòng)動(dòng)小手,幫忙點(diǎn)個(gè)贊和在看,分享給好友或者朋友圈,謝謝啦!