數據結構——樹與二叉樹的遍歷

目錄

  1. 二叉樹
  2. 二叉樹的遍歷
  3. 總結
  4. 參考資料

樹是學習數據結構的時候很是重要的一個數據結構,尤爲是二叉樹更爲重要。像JavaHashMap
就使用了紅黑樹,而Mysql的索引就使用到了B+樹。剛好最近刷leetcode碰到了很多的有關
二叉樹的題目,今天想着寫個總結。java

1. 樹

1.1 樹的概念

樹(Tree)是n(n>=0)個優先數據元素的結合。當n=0時,這棵樹稱之爲空樹,在一棵非空樹T中:node

  1. 有一個特殊的元素被稱之爲根節點,根節點沒有前驅節點
  2. 若n>=1,除根節點外,其他元素分爲m(m>0)個互不相交的集合T一、T二、.....、Tn,
    其中每個集合又是一棵樹,而且稱之爲根的子樹。

圖(1-1)  一顆普通的樹

1.2 樹的性質

  1. 樹的根節點沒有前驅節點,除根節點以外全部的節點有且只有一個前驅節點
  2. 樹的全部節點能夠有零個或者多個後繼節點

1.3 其餘術語

  • 節點的度:節點所擁有的子樹的個數稱爲該節點的度。

圖(1-2)  節點的度
  • 樹的度:樹中個節點的度的最大值稱爲該樹的度。圖1-2中樹的度爲3.
  • 葉節點:度爲0的節點稱之爲葉節點
  • 分支節點:度不爲0的節點稱之爲分支節點或者非終端節點。一棵樹的節點除了葉節點以外,其他的全是分支節點。
  • 孩子、左孩子、右孩子、雙親、兄弟:樹的一個節點的子樹的根節點稱之爲這個節點的孩子。
    在二叉樹中,左子樹的根稱之爲左孩子,右子樹的跟稱之爲右孩子。反過來這個節點稱之爲他孩子節點的父節點。
    具備同一個父節點的孩子互稱爲兄弟
  • 路徑、路徑長度:若是一棵樹中的一串節點n一、n二、......、nk有以下關係:節點ni是節點n(i+1)的父節點,
    就把n一、n二、......、nk稱之爲一條從n1到nk的路徑,這條路徑的長度是k-1
  • 節點的層數:規定樹的根節點的層數是1,其他節點的層數等於他的父節點的層數+1

圖(1-3)  節點的層數
- 有序樹和無序樹,若是一顆樹中各個子樹從左到右是有次序的,則稱這棵樹是有序的,反之稱爲無序樹。 - 森林:有限棵不相交的樹的集合稱之爲森林。

2.二叉樹

2.1 二叉樹的定義

二叉樹指書中節點的度不大於2的有序樹,是一種最簡單且最重要的樹。算法

遞歸定義:二叉樹是一棵空樹或者是一顆由一個根節點和兩顆互不相交二叉樹組成的非空樹。sql


圖(2-1)  二叉樹

特色:數據結構

  • 每一個節點最多由兩棵子樹,因此二叉樹中不存在度大於2的節點
  • 左子樹和有子樹是有順序的,次序不能任意顛倒
  • 即便樹中某節點只有一顆子樹,也要區分他是左子樹仍是右子樹

有關二叉樹的一些概念jvm

  • 二叉樹的深度:樹種節點的最大層數稱之爲樹的深度。
  • 滿二叉樹:若是一個二叉樹每一層的節點數都到達了最大,這顆二叉樹就稱做滿二叉樹。
    對於滿二叉樹,全部的分支節點都存在左子樹和右子樹,全部的葉結點都在同一層(最下面一層)。圖(2-2)就是一顆滿二叉樹

圖(2-2)  滿二叉樹
- 徹底二叉樹:一顆深度爲k的有那個節點的二叉樹,對其節點按照從上至下,從左至右的順序進行編號,若是編號i(1<=i<=n) 的節點與滿二叉樹種編號爲i的節點在二叉樹種的位置相同,則這棵二叉樹稱之爲徹底二叉樹。徹底二叉樹的特色是:葉子節點只能出如今最下層 和次最下層,且下層的葉子節點集中在左側。一棵慢二叉樹必然是一顆徹底二叉樹,而徹底二叉樹未必是滿二叉樹。

圖(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 的結點有以下特性:
  1. 若 i=1,則該結點是二叉樹的根,無雙親, 不然,編號爲 [i/2] 的結點爲其雙親結點;
  2. 若 2i>n,則該結點無左孩子, 不然,編號爲 2i 的結點爲其左孩子結點;
  3. 若 2i+1>n,則該結點無右孩子結點, 不然,編號爲2i+1 的結點爲其右孩子結點。

3 二叉樹的遍歷

二叉樹的遍歷是指從二叉樹的根結點出發,按照某種次序一次訪問二叉樹中全部的節點,使得每一個節點被訪問且僅訪問一次。post

二叉樹的訪問次序能夠分爲四種:學習

  • 前序遍歷。訪問順序:父節點——>左子樹——>右子樹
  • 中序遍歷。訪問數序:左子樹——>父節點——>右子樹
  • 後序遍歷。訪問順序:左子樹——>右子樹——>父節點
  • 層序遍歷。訪問順序:僅僅需按層次遍歷就能夠。

節點定義:這裏先將後續代碼實例的節點進行定義,節點的結構以下:this

public class TreeNode {
    public int val;

    public TreeNode left;

    public TreeNode right;

    public TreeNode(int x){
        this.val=x;
    }
}

前序遍歷

根據前面說的,訪問順序:父節點——>左子樹——>右子樹。那麼對於圖(2-2)所示的滿二叉樹
則有如下訪問順序:3d


圖(3-1)  前序遍歷
訪問結果爲:十、五、三、七、1五、十二、17
  1. 使用遞歸進行遍歷
public void preorderTraversal(TreeNode root){
    if(root==null) return;
    //先訪問根節點
    System.out.println(root.val);
    //訪問左子樹
    preorderTraversal(root.left);
    //訪問右子樹
    preorderTraversal(root.left);
}
  1. 使用迭代的方法遍歷
    使用迭代的方法須要藉助棧來實現。

考慮,本着一個節點訪問一次的原則,則訪問一個節點的時候 除了將自身的值輸出,還須要將兩個子節點加入到棧中。
訪問兩個子樹的時候需先訪問左子樹,在訪問有子樹,所以應該先將右孩子入棧(若是有),再將左孩子入棧(若是有)。
而後再將節點出棧重複這個動做。直到棧爲空。

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)所示的滿二叉樹
則有如下訪問順序:


圖(3-2)  中序遍歷
訪問結果爲:三、五、七、十、十二、1五、17
  1. 使用遞歸遍歷
public void middleTraversal(TreeNode root){
    if(root==null) return;
    middleTraversal(root.left);
    System.out.println(root.val);
    System.out.println(root.right);
}
  1. 使用迭代的方式遍歷
    使用迭代的方法須要藉助棧來實現。

首先訪問的順序是左子樹——>父節點——>右子樹,咱們有的線索就是一個根節點,須要先找到左子樹的最左邊的節點輸出(圖(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)所示的滿二叉樹
則有如下訪問順序:


圖(3-3)  中序遍歷
訪問結果爲:三、七、五、十二、1七、1五、10

1)使用遞歸遍歷

public void postorderTraversal(TreeNode root){
    if (root==null) return;
    postorderTraversal(root.left);
    postorderTraversal(root.right);
    System.out.println(root.val);
}
  1. 使用迭代遍歷
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. 參考資料

相關文章
相關標籤/搜索