程序員,你內心就沒點樹嗎?

看官,不要生氣,我沒有罵你也沒有鄙視你的意思,今天就是想單純的給大夥分享一下樹的相關知識,可是我仍是想說做爲一名程序員,本身內心有沒有點樹?你會沒點數嗎?言歸正傳,樹是咱們經常使用的數據結構之一,樹的種類不少有二叉樹、二叉查找樹、平衡二叉樹、紅黑樹、B樹、B+樹等等,咱們今天就來聊聊二叉樹相關的樹。java

什麼是樹?

首先咱們要知道什麼是樹?咱們日常中的樹是往上長有分支的而卻不會造成閉環,數據結構中的樹跟咱們咱們平時看到的樹相似,確切的說是跟樹根長得相似,我畫了一幅圖,讓你們更好的理解樹。程序員

圖1、圖2都是樹,圖3不是樹。每一個紅色的圓圈咱們稱之爲元素也叫節點,用線將兩個節點鏈接起來,這兩個節點就造成了父子關係,同一個父節點的子節點成爲兄弟節點,這跟咱們家族關係同樣,同一個父親的叫作兄弟姐妹,在家族裏面最大的稱爲老子,樹裏面也是同樣的,只是不叫老子,叫作跟節點,沒有子節點的叫作葉子節點。咱們拿圖1來作示例,A爲根節點,B、C爲兄弟節點,E、F爲葉子節點。

一顆樹還會涉及到三個概念高度深度,咱們先來看看這三個名詞的定義:面試

高度:節點到葉子節點的最長路徑,從0開始計數算法

深度:跟節點到這個節點所經歷的邊數,從0開始計數數組

:節點距離根節點的距離,從1開始計數微信

知道了三個名詞的概念以後,咱們用一張圖來更加形象的表示這三個概念。數據結構

以上就是樹的基本概念,樹的種類不少,咱們主要來學學二叉樹。post

二叉樹

二叉樹就像它的名字同樣,每一個元素最多有兩個節點,分別稱爲左節點和右節點。固然並非每一個元素都須要有兩個節點,有的可能只有左節點,有的可能只有右節點。就像國家開放二胎同樣,也不是每一個人都須要生兩個孩子。下面咱們來看看一顆典型的二叉樹。學習

基於樹的存儲模式的不一樣,爲了更好的利用存儲空間,二叉樹又分爲徹底二叉樹和非徹底二叉樹,咱們先來看看什麼是徹底二叉樹、非徹底二叉樹?測試

徹底二叉樹的定義:葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,而且除了最後一層,其餘層的節點個數都要達到最大

也許單看定義會看不明白,咱們來看幾張圖,你就可以明白什麼是徹底二叉樹、非徹底二叉樹。

1、徹底二叉樹

徹底二叉樹

2、非徹底二叉樹

上面咱們說了基於樹的存儲模式不一樣,而分爲徹底二叉樹和非徹底二叉樹,那咱們接下來來看看樹的存儲模式。

二叉樹的存儲模式

二叉樹的存儲模式有兩種,一種是基於指針或者引用的二叉鏈式存儲法,一種是基於數組的順序存儲法

二叉鏈式存儲法

鏈式存儲法相對比較簡單,理解起來也很是容易,每個節點都有三個字段,一個字段存儲着該節點的值,另外兩個字段存儲着左右節點的引用。咱們順着跟字節就能夠很輕鬆的把整棵樹串起來,鏈式存儲法的結構大概長成這樣。

順序存儲法

順序存儲法是基於數組實現的,數組是一段有序的內存空間,若是咱們把跟節點的座標定位i=1,左節點就是 2 * i = 2,右節點 2 * i+ 1 = 3,以此類推,每一個節點都這麼算,而後就將樹轉化成數組了,反過來,按照這種規則咱們也能將數組轉化成一棵樹。看到這裏我想你必定看出了一些弊端, 若是這是一顆不平衡的二叉樹是否是會形成大量的空間浪費呢?沒錯,這就是爲何須要分徹底二叉樹和非徹底二叉樹。分別來看看這兩種樹基於數組的存儲模式。

徹底二叉樹順序存儲法

非徹底二叉樹順序存儲法

從圖中將樹轉化成數組以後能夠看出,徹底二叉樹用數組來存儲只浪費了一個下標爲0的存儲空間,二非徹底二叉樹則浪費了大量的空間。若是樹爲徹底二叉樹,用數組存儲比鏈式存儲節約空間,由於數組存儲不須要存儲左右節點的信息

上面咱們瞭解了二叉樹的定義、類型、存儲方式,接下來咱們一塊兒瞭解一下二叉樹的遍歷,二叉樹的遍歷也是面試中常常遇到的問題。

二叉樹遍歷

要了解二叉樹的遍歷,咱們首先須要實例化出一顆二叉樹,咱們採用鏈式存儲的方式來定義樹,實例化樹須要樹的節點信息,用來存放該節點的信息,由於咱們才用的是鏈式存儲,因此咱們的節點信息以下。

/** * 定義一棵樹 */
public class TreeNode {
    // 存儲值
    public int data;
    // 存儲左節點
    public TreeNode left;
    // 存儲右節點
    public TreeNode right;

    public TreeNode(int data) {
        this.data = data;
    }
}

複製代碼

定義完節點信息以後,咱們就能夠初始化一顆樹啦,下面是初始化樹的過程:

public static TreeNode buildTree() {
    // 建立測試用的二叉樹
    TreeNode t1 = new TreeNode(1);
    TreeNode t2 = new TreeNode(2);
    TreeNode t3 = new TreeNode(3);
    TreeNode t4 = new TreeNode(4);
    TreeNode t5 = new TreeNode(5);
    TreeNode t6 = new TreeNode(6);
    TreeNode t7 = new TreeNode(7);
    TreeNode t8 = new TreeNode(8);

    t1.left = t2;
    t1.right = t3;
    t2.left = t4;
    t4.right = t7;
    t3.left = t5;
    t3.right = t6;
    t6.left = t8;

    return t1;
}
複製代碼

通過上面步驟以後,咱們的樹就長成下圖所示的樣子,數字表明該節點的值。

有了樹以後,咱們就能夠對樹進行遍歷啦,二叉樹的遍歷有三種方式,前序遍歷、中序遍歷、後續遍歷三種遍歷方式,三種遍歷方式與節點輸出的順序有關係。下面咱們分別來看看這三種遍歷方式。

前序遍歷

前序遍歷:對於樹中的任意節點來講,先打印這個節點,而後再打印它的左子樹,最後打印它的右子樹。

爲了方便你們的理解,我基於上面咱們定義的二叉樹,對三種遍歷方式的執行流程都製做了動態圖,但願對你的閱讀有所幫助,咱們先來看看前序遍歷的執行流程動態圖。

理解了前序遍歷的概念和看完前序遍歷執行流程動態圖以後,你內心必定很想知道,在代碼中如何怎麼實現樹的前序遍歷?二叉樹的遍歷很是簡單,通常都是採用遞歸的方式進行遍歷,咱們來看看前序遍歷的代碼:

// 先序遍歷,遞歸實現 先打印自己,再打印左節點,在打印右節點
public static void preOrder(TreeNode root) {

    if (root == null) {
        return;
    }
    // 輸出自己
    System.out.print(root.data + " ");
    // 遍歷左節點
    preOrder(root.left);
    // 遍歷右節點
    preOrder(root.right);
}
複製代碼

中序遍歷

中序遍歷:對於樹中的任意節點來講,先打印它的左子樹,而後再打印它自己,最後打印它的右子樹。

跟前序遍歷同樣,咱們來看看中序遍歷的執行流程動態圖。

中序遍歷的代碼:

// 中序遍歷 先打印左節點,再輸出自己,最後輸出右節點
public static void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    inOrder(root.left);
    System.out.print(root.data + " ");
    inOrder(root.right);
}
複製代碼

後序遍歷

後序遍歷:對於樹中的任意節點來講,先打印它的左子樹,而後再打印它的右子樹,最後打印這個節點自己。

跟前兩種遍歷同樣,理解概念以後,咱們仍是先來看張圖。

後序遍歷的實現代碼:

// 後序遍歷 先打印左節點,再輸出右節點,最後才輸出自己
public static void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    postOrder(root.left);
    postOrder(root.right);
    System.out.print(root.data + " ");
}
複製代碼

二叉樹的遍歷仍是很是簡單的,雖然有三種遍歷方式,但都是同樣的,只是輸出的順序不同而已,通過了上面這麼多的學習,我相信你必定對二叉樹有很多的認識,接下來咱們來了解一種經常使用並且比較特殊的二叉樹:二叉查找樹

二叉查找樹

二叉查找樹又叫二叉搜索樹,從名字中咱們就可以知道,這種樹在查找方面必定有過人的優點,事實確實如此,二叉查找樹確實是爲查找而生的樹,可是它不只僅支持快速查找數據,還支持快速插入、刪除一個數據。那它是怎麼作到這些的呢?咱們先從二叉查找樹的概念開始瞭解。

二叉查找樹:在樹中的任意一個節點,其左子樹中的每一個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值。

難以理解?記不住?不要緊的,下面我定義了一顆二叉查找樹,咱們對着樹,來慢慢理解。

根據二叉查找樹的定義,每棵樹的左節點的值要小於這父節點,右節點的值要大於父節點。62節點的 全部左節點的值都要小於 62 ,全部右節點 的值都要大於 62 。對於這顆樹上的每個節點都要知足這個條件,咱們拿咱們樹上的另外一個節點 35 來講,它的右子樹上的節點值最大不能超過 47 ,由於 35 是 47 的左子樹,根據二叉搜索樹的規則,左子樹的值要小於節點值。

二叉查找樹既然名字中帶有查找兩字,那咱們就從二叉查找樹的查找開始學習二叉查找樹吧。

二叉查找樹的查找操做

因爲二叉查找樹的特性,咱們須要查找一個數據,先跟跟節點比較,若是值等於跟節點,則返回根節點,若是小於根節點,則必然在左子樹這邊,只要遞歸查找左子樹就行,若是大於,這在右子樹這邊,遞歸右子樹便可。這樣就可以實現快速查找,由於每次查找都減小了一半的數據,跟二分查找有點類似,快速插入、刪除都是居於這個特性實現的。

下面咱們用一幅動態圖來增強對二叉查找樹查找流程的理解,咱們須要在上面的這顆二叉查找樹中找出值等於 37 的節點,咱們一塊兒來看看流程圖是怎麼實現的。

二叉查找樹樹的查找操做

  • 一、先用 37 跟 62 比較,37 < 62 ,在左子樹中繼續查找
  • 2、左子樹的節點值爲 58,37 < 58 ,繼續在左子樹中查找
  • 3、左子樹的節點值爲 47,37 < 47,繼續在左子樹中查找
  • 4、左子樹的節點值爲 35,37 > 35,在右子樹中查找
  • 5、右子樹中的節點值爲 37,37 = 37 ,返回該節點

講完了查找的概念以後,咱們一塊兒來看看二叉查找樹的查找操做的代碼實現

/** * 根據值查找樹 * @param data&emsp;值 * @return */
public TreeNode find(int data) {
    TreeNode p = tree;
    while (p != null) {
        if (data < p.data) p = p.left;
        else if (data > p.data) p = p.right;
        else return p;
    }
    return null;
}
複製代碼

二叉查找樹的插入操做

插入跟查找差很少,也是從根節點開始找,若是要插入的數據比節點的數據大,而且節點的右子樹爲空,就將新數據直接插到右子節點的位置;若是不爲空,就再遞歸遍歷右子樹,查找插入位置。同理,若是要插入的數據比節點數值小,而且節點的左子樹爲空,就將新數據插入到左子節點的位置;若是不爲空,就再遞歸遍歷左子樹,查找插入位置。

假設咱們要插入 63 ,咱們用一張動態圖來看看插入的流程。

  • 一、63 > 62 ,在樹的右子樹繼續查找.
  • 二、63 < 88 ,在樹的左子樹繼續查找
  • 三、63 < 73 ,由於 73 是葉子節點,因此 63 就成爲了 73 的左子樹。

咱們來看看二叉查找樹的插入操做實現代碼

/** * 插入樹 * @param data */
public void insert(int data) {
    if (tree == null) {
        tree = new TreeNode(data);
        return;
    }

    TreeNode p = tree;

    while (p != null) {
        // 若是值大於節點的值,則新樹爲節點的右子樹
        if (data > p.data) {
            if (p.right == null) {
                p.right = new TreeNode(data);
                return;
            }
            p = p.right;
        } else { // data < p.data
            if (p.left == null) {
                p.left = new TreeNode(data);
                return;
            }
            p = p.left;
        }
    }
}
複製代碼

二叉查找樹的刪除操做

刪除的邏輯要比查找和插入複雜一些,刪除分一下三種狀況:

第一種狀況:若是要刪除的節點沒有子節點,咱們只須要直接將父節點中,指向要刪除節點的指針置爲 null。好比圖中的刪除節點 51。

第二種狀況:若是要刪除的節點只有一個子節點(只有左子節點或者右子節點),咱們只須要更新父節點中,指向要刪除節點的指針,讓它指向要刪除節點的子節點就能夠了。好比圖中的刪除節點 35。

第三種狀況:若是要刪除的節點有兩個子節點,這就比較複雜了。咱們須要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。而後再刪除掉這個最小節點,由於最小節點確定沒有左子節點(若是有左子結點,那就不是最小節點了),因此,咱們能夠應用上面兩條規則來刪除這個最小節點。好比圖中的刪除節點 88

前面兩種狀況稍微簡單一些,第三種狀況,我製做了一張動態圖,但願能對你有所幫助。

咱們來看看二叉查找樹的刪除操做實現代碼

public void delete(int data) {
    TreeNode p = tree; // p指向要刪除的節點,初始化指向根節點
    TreeNode pp = null; // pp記錄的是p的父節點
    while (p != null && p.data != data) {
        pp = p;
        if (data > p.data) p = p.right;
        else p = p.left;
    }
    if (p == null) return; // 沒有找到

    // 要刪除的節點有兩個子節點
    if (p.left != null && p.right != null) { // 查找右子樹中最小節點
        TreeNode minP = p.right;
        TreeNode minPP = p; // minPP表示minP的父節點
        while (minP.left != null) {
            minPP = minP;
            minP = minP.left;
        }
        p.data = minP.data; // 將minP的數據替換到p中
        p = minP; // 下面就變成了刪除minP了
        pp = minPP;
    }

    // 刪除節點是葉子節點或者僅有一個子節點
    TreeNode child; // p的子節點
    if (p.left != null) child = p.left;
    else if (p.right != null) child = p.right;
    else child = null;

    if (pp == null) tree = child; // 刪除的是根節點
    else if (pp.left == p) pp.left = child;
    else pp.right = child;
}
複製代碼

咱們上面瞭解了一些二叉查找樹的相關知識,因爲二叉查找樹在極端狀況下會退化成鏈表,例如每一個節點都只有一個左節點,這是時間複雜度就變成了O(n),爲了不這種狀況,又出現了一種新的樹叫平衡二叉查找樹,因爲本文篇幅有點長了,相信看到這裏的各位小夥伴已經有點疲憊了,關於平衡二叉查找樹的相關知識我就不在這裏介紹了。

最後

打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。

平頭哥的技術博文

參考資料

  • 數據結構與算法之美(極客時間)
相關文章
相關標籤/搜索