目錄
- 樹
- 二叉樹
- 二叉樹的遍歷
- 總結
- 參考資料
序
樹是學習資料結構的時候非常重要的一個資料結構,尤其是二叉樹更為重要。像Java
的HashMap
就使用了紅黑樹,而Mysql
的索引就使用到了B+樹。恰好最近刷leetcode
碰到了不少的有關
二叉樹的題目,今天想著寫個總結。
1. 樹
1.1 樹的概念
樹(Tree)是n(n>=0)個優先資料元素的結合。當n=0時,這棵樹稱之為空樹,在一棵非空樹T中:
- 有一個特殊的元素被稱之為根節點,根節點沒有前驅節點
- 若n>=1,除根節點外,其餘元素分為m(m>0)個互不相交的集合T1、T2、.....、Tn,
其中每一個集合又是一棵樹,並且稱之為根的子樹。
1.2 樹的性質
- 樹的根節點沒有前驅節點,除根節點之外所有的節點有且只有一個前驅節點
- 樹的所有節點可以有零個或者多個後繼節點
1.3 其他術語
- 節點的度:節點所擁有的子樹的個數稱為該節點的度。
- 樹的度:樹中個節點的度的最大值稱為該樹的度。圖1-2中樹的度為3.
- 葉節點:度為0的節點稱之為葉節點
- 分支節點:度不為0的節點稱之為分支節點或者非終端節點。一棵樹的節點除了葉節點之外,其餘的全是分支節點。
- 孩子、左孩子、右孩子、雙親、兄弟:樹的一個節點的子樹的根節點稱之為這個節點的孩子。
在二叉樹中,左子樹的根稱之為左孩子,右子樹的跟稱之為右孩子。反過來這個節點稱之為他孩子節點的父節點。
具有同一個父節點的孩子互稱為兄弟 - 路徑、路徑長度:如果一棵樹中的一串節點n1、n2、......、nk有如下關係:節點ni是節點n(i+1)的父節點,
就把n1、n2、......、nk稱之為一條從n1到nk的路徑,這條路徑的長度是k-1 - 節點的層數:規定樹的根節點的層數是1,其餘節點的層數等於他的父節點的層數+1
2.二叉樹
2.1 二叉樹的定義
二叉樹指書中節點的度不大於2的有序樹,是一種最簡單且最重要的樹。
遞迴定義:二叉樹是一棵空樹或者是一顆由一個根節點和兩顆互不相交二叉樹組成的非空樹。
特點:
- 每個節點最多由兩棵子樹,所以二叉樹中不存在度大於2的節點
- 左子樹和有子樹是有順序的,次序不能任意顛倒
- 即使樹中某節點只有一顆子樹,也要區分他是左子樹還是右子樹
有關二叉樹的一些概念:
- 二叉樹的深度:樹種節點的最大層數稱之為樹的深度。
- 滿二叉樹:如果一個二叉樹每一層的節點數都到達了最大,這顆二叉樹就稱作滿二叉樹。
對於滿二叉樹,所有的分支節點都存在左子樹和右子樹,所有的葉結點都在同一層(最下面一層)。圖(2-2)就是一顆滿二叉樹- 完全二叉樹:一顆深度為k的有那個節點的二叉樹,對其節點按照從上至下,從左至右的順序進行編號,如果編號i(1<=i<=n) 的節點與滿二叉樹種編號為i的節點在二叉樹種的位置相同,則這棵二叉樹稱之為完全二叉樹。完全二叉樹的特點是:葉子節點只能出現在最下層 和次最下層,且下層的葉子節點集中在左側。一棵慢二叉樹必然是一顆完全二叉樹,而完全二叉樹未必是滿二叉樹。
圖(2-2) 滿二叉樹
圖(2-3) 完全二叉樹
2.2 二叉樹的性質
- 二叉樹的第i層上至多有2(i-1)(i≥1)個節點
- 深度為h的二叉樹中至多含有2的h次方-1個節點
- 若任意一顆二叉樹中右n0個葉子節點,右n2個度為2的節點,則必有n0=n2+1
- 在完全二叉樹中,具有n個節點的完全二叉樹的深度為[log2n]+1,其中[log2n]是向下取整。
- 若對含 n 個結點的完全二叉樹從上到下且從左至右進行 1 至 n 的編號,則對完全二叉樹中任意一個編號為 i 的結點有如下特性:
- 若 i=1,則該結點是二叉樹的根,無雙親, 否則,編號為 [i/2] 的結點為其雙親結點;
- 若 2i>n,則該結點無左孩子, 否則,編號為 2i 的結點為其左孩子結點;
- 若 2i+1>n,則該結點無右孩子結點, 否則,編號為2i+1 的結點為其右孩子結點。
3 二叉樹的遍歷
二叉樹的遍歷是指從二叉樹的根結點出發,按照某種次序一次訪問二叉樹中所有的節點,使得每個節點被訪問且僅訪問一次。
二叉樹的訪問次序可以分為四種:
- 前序遍歷。訪問順序:父節點——>左子樹——>右子樹
- 中序遍歷。訪問數序:左子樹——>父節點——>右子樹
- 後序遍歷。訪問順序:左子樹——>右子樹——>父節點
- 層序遍歷。訪問順序:僅僅需按層次遍歷就可以。
節點定義:這裡先將後續程式碼例項的節點進行定義,節點的結構如下:
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int x){
this.val=x;
}
}
前序遍歷
根據前面說的,訪問順序:父節點——>左子樹——>右子樹。那麼對於圖(2-2)所示的滿二叉樹
則有以下訪問順序:
- 使用遞迴進行遍歷
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);
}
}
}
}
4. 總結
本文簡單介紹了樹的概念與性質,重點介紹了二叉樹的幾種遍歷方式,儘管遞迴遍歷很簡答,但是手寫還是會寫錯,而且一般會要求通過迭代來完成。
剛才又看到了新的套路,jvm
中的迭代的本質就是壓棧和彈棧,然後可以把遞迴轉化為迭代。後面還有很多種樹的遍歷方式。多多學習吧,希望自己能進個大廠嘍。
5. 參考資料
- 資料結構與演算法Java版——羅文劼, 王苗, 張小莉編著
- 深入學習二叉樹(一) 二叉樹基礎