樹是學習數據結構的時候很是重要的一個數據結構,尤爲是二叉樹更爲重要。像Java
的HashMap
就使用了紅黑樹,而Mysql
的索引就使用到了B+樹。剛好最近刷leetcode
碰到了很多的有關
二叉樹的題目,今天想着寫個總結。java
樹(Tree)是n(n>=0)個優先數據元素的結合。當n=0時,這棵樹稱之爲空樹,在一棵非空樹T中:node
二叉樹指書中節點的度不大於2的有序樹,是一種最簡單且最重要的樹。算法
遞歸定義:二叉樹是一棵空樹或者是一顆由一個根節點和兩顆互不相交二叉樹組成的非空樹。sql
特色:數據結構
- 每一個節點最多由兩棵子樹,因此二叉樹中不存在度大於2的節點
- 左子樹和有子樹是有順序的,次序不能任意顛倒
- 即便樹中某節點只有一顆子樹,也要區分他是左子樹仍是右子樹
有關二叉樹的一些概念:jvm
- 二叉樹的深度:樹種節點的最大層數稱之爲樹的深度。
- 滿二叉樹:若是一個二叉樹每一層的節點數都到達了最大,這顆二叉樹就稱做滿二叉樹。
對於滿二叉樹,全部的分支節點都存在左子樹和右子樹,全部的葉結點都在同一層(最下面一層)。圖(2-2)就是一顆滿二叉樹- 徹底二叉樹:一顆深度爲k的有那個節點的二叉樹,對其節點按照從上至下,從左至右的順序進行編號,若是編號i(1<=i<=n) 的節點與滿二叉樹種編號爲i的節點在二叉樹種的位置相同,則這棵二叉樹稱之爲徹底二叉樹。徹底二叉樹的特色是:葉子節點只能出如今最下層 和次最下層,且下層的葉子節點集中在左側。一棵慢二叉樹必然是一顆徹底二叉樹,而徹底二叉樹未必是滿二叉樹。 ![]()
圖(2-2) 滿二叉樹![]()
圖(2-3) 徹底二叉樹
- 若 i=1,則該結點是二叉樹的根,無雙親, 不然,編號爲 [i/2] 的結點爲其雙親結點;
- 若 2i>n,則該結點無左孩子, 不然,編號爲 2i 的結點爲其左孩子結點;
- 若 2i+1>n,則該結點無右孩子結點, 不然,編號爲2i+1 的結點爲其右孩子結點。
二叉樹的遍歷是指從二叉樹的根結點出發,按照某種次序一次訪問二叉樹中全部的節點,使得每一個節點被訪問且僅訪問一次。post
二叉樹的訪問次序能夠分爲四種:學習
節點定義:這裏先將後續代碼實例的節點進行定義,節點的結構以下:this
public class TreeNode { public int val; public TreeNode left; public TreeNode right; public TreeNode(int x){ this.val=x; } }
根據前面說的,訪問順序:父節點——>左子樹——>右子樹。那麼對於圖(2-2)所示的滿二叉樹
則有如下訪問順序:3d
public void preorderTraversal(TreeNode root){ if(root==null) return; //先訪問根節點 System.out.println(root.val); //訪問左子樹 preorderTraversal(root.left); //訪問右子樹 preorderTraversal(root.left); }
考慮,本着一個節點訪問一次的原則,則訪問一個節點的時候 除了將自身的值輸出,還須要將兩個子節點加入到棧中。
訪問兩個子樹的時候需先訪問左子樹,在訪問有子樹,所以應該先將右孩子入棧(若是有),再將左孩子入棧(若是有)。
而後再將節點出棧重複這個動做。直到棧爲空。
public void preorderTraversal(TreeNode root){ if(root==null) return; Stack<TreeNode> stack=new Stack<TreeNode>(); stack.push(root); while (!stack.isEmpty()){ TreeNode node=stack.pop(); System.out.println(node.val); if(node.right!=null){ stack.push(node.right); } if(node.left!=null){ stack.push(node.left); } } }
根據前面說的,訪問順序:左子樹——>父節點——>右子樹。那麼對於圖(2-2)所示的滿二叉樹
則有如下訪問順序:
public void middleTraversal(TreeNode root){ if(root==null) return; middleTraversal(root.left); System.out.println(root.val); System.out.println(root.right); }
首先訪問的順序是左子樹——>父節點——>右子樹,咱們有的線索就是一個根節點,須要先找到左子樹的最左邊的節點輸出(圖(3-2)中的3),
而後輸出父節點,再輸出右節點,若是沿着最左邊的路徑所有入棧,那麼從棧中彈出的第一個元素就是咱們的第一個元素,如今棧頂的元素就是輸出的元素的父節點。
輸出,父節點已經有了就能夠獲取到右子樹,右子樹的根節點入棧,重複這樣的動做,代碼以下:
public void middleTraversal(TreeNode root){ if(root==null) return; Stack<TreeNode> stack=new Stack<>(); TreeNode node=root; while (node!=null||!stack.isEmpty()){ while (node!=null){ stack.push(node); node=node.left; } node=stack.pop(); System.out.println(node.val); node=node.right; } }
根據前面說的,訪問順序:左子樹——>右子樹——>父節點。那麼對於圖(2-2)所示的滿二叉樹
則有如下訪問順序:
1)使用遞歸遍歷
public void postorderTraversal(TreeNode root){ if (root==null) return; postorderTraversal(root.left); postorderTraversal(root.right); System.out.println(root.val); }
public List<Integer> postorderTraversalUseStack(TreeNode root){ if (root==null) return new LinkedList<Integer>(); Stack<TreeNode> stack=new Stack<>(); //由於這種辦法訪問的結果是反序的 //所以這裏使用了一個鏈表,目的是在頭部插入數據 //這樣獲得的結果就是目標結果 //也可使用ArrayList,再循環反序。 LinkedList<Integer> res=new LinkedList<>(); stack.push(root); TreeNode node; while (!stack.isEmpty()){ node=stack.pop(); res.addFirst(node.val); //由於要獲得的結果是左-右,壓棧的時候若是是正序是應該先右再左 //可是訪問的結果是反的,因此是先左後右 if(node.left!=null){ stack.push(node.left); } if(node.right!=null){ stack.push(node.right); } } return res; }
層序遍歷會比簡單,只要使用一個隊列,計算每層的長度,便利的時候按照左孩子,右孩子的順序入隊便可
public void levelOrder(TreeNode root){ if(root==null) return; Queue<TreeNode> queue=new LinkedList<TreeNode>(); queue.add(root); TreeNode node; while (queue.size()!=0){ int len=queue.size(); for (int i=0;i<len;i++){ node=queue.poll(); System.out.println(node.val); if(node.left!=null){ queue.add(node.left); } if (node.right!=null){ queue.add(node.right); } } } }
本文簡單介紹了樹的概念與性質,重點介紹了二叉樹的幾種遍歷方式,儘管遞歸遍歷很簡答,可是手寫仍是會寫錯,並且通常會要求經過迭代來完成。
剛纔又看到了新的套路,jvm
中的迭代的本質就是壓棧和彈棧,而後能夠把遞歸轉化爲迭代。後面還有不少種樹的遍歷方式。多多學習吧,但願本身能進個大廠嘍。