1. 程式人生 > 實用技巧 >豁出去了,二叉搜尋樹最強學習攻略!

豁出去了,二叉搜尋樹最強學習攻略!

1、我想深入的學習

2、想給大家講明白,讓大家也深入學習

二叉搜尋樹

為啥要學習二叉搜尋樹

今天咱們要說的是二叉搜尋樹,在學習這個之前,建議之前還沒有看過我寫的樹和二叉樹的朋友一定要去看看再來!否則你會有點暈乎。

這裡先給大傢伙普及一下,想必有些人不太清楚,我們之前講了什麼是樹,然後接著說了二叉樹,這裡為啥開始說二叉搜尋樹了?

說到樹這個資料結構,其實蠻重要的,你肯定聽說過紅黑樹,因為jdk1.8中的hashmap就是陣列加連結串列加紅黑樹,然後你肯定還聽過AVL樹,是不是聽著就感覺高大上,然後學資料庫MySQL索引的時候,你一定會接觸B樹,對還有B+樹,同樣的高大上(其實還有B-樹)

如果你之前不瞭解,看到這些有種被勸退的感覺,我這裡簡單先給梳理一下,以後咱們都會詳細講解的:

“其實很多高深的東西都是在原有基礎之上發展而來,我們最開始學習樹,瞭解了啥是樹,然後開始對樹做一些規定,然後產生了二叉樹來應用於某些特定的場景,然後我們對二叉樹再給定一些要求,然後產生了二叉搜尋樹,在使用二叉搜尋樹的過程中發現了一些問題,然後優化優化,再加一些新規定,就產生了AVL樹,AVL樹主要解決的就是二叉搜尋樹的平衡問題,然後在AVL樹的基礎上再優化設計,又出來了紅黑樹,也就是說,紅黑樹也是一種平衡二叉搜尋樹,主語B樹也是在原有基礎的樹之上發展而來的一種結構,進而有B+和B-”

不知道上面說的那些你明白了不,總結一下就是:

1、這些看起來各式各樣的樹都是一步步發展而來的,我們應該尋根溯源,從最開始的一步步來學習

2、這些都屬於高階資料結構,都有一些特定的應用場景,是我們需要格外注意的

也就是說,我們想要學習AVL樹和紅黑樹這些就要先知道啥是二叉搜尋樹,然後你就會接觸二叉搜尋樹的平衡問題,自然過渡到紅黑樹這種。

二叉搜尋樹是個啥

首先,我們必須搞清楚二叉搜尋樹是個啥?第一點,肯定的那就是它一定是樹結構,也就是說二叉搜尋樹是基本的基礎資料結構,看清楚了:

二叉搜尋樹是基礎資料結構,不是什麼演算法

它是在原有二叉樹上增加一些規則從而形成的,也就是本來人家是個二叉樹,然後再給它指定一些規則,加了這些規則之後,它就不同於一般的二叉樹了,人家有了新名字,就叫做二叉搜尋樹

那加了人什麼規則呢?二叉搜尋樹有如下規定:

  1. 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值;
  3. 任意節點的左、右子樹也分別為二叉查詢樹;

不知道你是否看明白了?我來結合圖看看:

這就是一棵二叉搜尋樹,然後我們來一次解讀它的三條規則:

1、比如8是根節點,左子樹不為空,左子樹上所有節點的值均小於8,你看看是不是,也就是說這個8左邊的樹都比它小

2、同樣的,這個8的右子樹上的所有節點的值都比8大

3、然後每個節點的左右子樹也滿足上述1和2兩條

我一般是這樣理解的:

你看啊,我們這裡以每一個節點來敘述物件來說,也就是比左邊的都大,比右邊的都小,這裡要記住我們針對的是某一個節點來說,比如我們拿8這個根節點來說,這個8是不是比它左邊的數都要大,但是比它右邊的數都小

不知道大家明白嗎?這裡還有注意的就是:

這裡說的左邊指的是左子樹上的節點的值

比如這裡的7,千萬別抬槓說“慶哥,慶哥,這個7不是在8的右邊嗎?不信你看”

哈哈,千萬不要這樣想哦。

這就是二叉搜尋樹了,看定義,我們覺得其實挺好理解的,另外啊,關於二叉搜尋樹的基本概念,你還需要知道這些:

二叉搜尋樹,英文名稱是:Binary Search Tree,我們一般就簡稱BST,另外它還有其他的名字,比如二叉排序樹,二叉查詢樹,記住這指的都是一樣的,我這裡習慣說成二叉搜尋樹。

最後再總結一下,啥是二叉搜尋樹:

二叉搜尋樹是基本的資料結構,是具有以下性質的二叉樹,當然也可以是空樹:

  1. 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值;
  3. 任意節點的左、右子樹也分別為二叉查詢樹;

以上說了這麼多,其實目的就一個,讓你明白什麼是二叉搜尋樹,它的概念本身比較簡單,可能剛接觸的話會感覺比較迷糊。

假如說現在你已經掌握了什麼是二叉搜尋樹,那我們接下來看看下一個問題:二叉搜尋樹的優勢在哪?

二叉索樹的優勢

資料的邏輯機構和物理結構

我們在說二叉搜尋樹的時候,需要先來回顧一下之前學習關於資料的邏輯結構和物理結構,我們知道,對於資料的儲存該選擇什麼樣的資料結構,那就要取決於資料的邏輯結構和物理結構。

所謂資料的邏輯結構指的就是資料之間具有什麼樣的關係,一般如下:

  1. 一對一(採用線性資料結構)
  2. 一對多(採用樹結構)
  3. 多對多(採用圖結構)

然後我們還需要知道的是,對於某種資料結構來說,我們實現的方式有兩種:

  1. 順序儲存(陣列)
  2. 鏈式儲存(連結串列)

也就是說啊,當我們確定了要採用樹結構了,我們下一個要考慮的問題就是看看這個樹結構要採取順序儲存還是鏈式儲存,那這個就要有資料的物理結構來決定了。

所謂的資料的物理結構一般就是指的資料在記憶體中的存放形式:

  1. 集中存放(順序儲存)
  2. 分散存放(鏈式儲存)

這些知識,你是需要提前知道的。

二叉搜尋樹的儲存結構

首先既然是樹,那肯定存放的是具有一對多關係的資料,這個毋庸置疑的,我們就不需要再去考慮資料的邏輯結構這一塊了,也就是說剩下的重點就是要考慮資料結構的儲存結構了,也就是看看是採取順序儲存還是鏈式儲存了,當然了,我們這裡根據資料的物理結構來判斷,可能採取順序儲存,也有可能採取鏈式儲存。

不過這裡你要記住了,二叉搜尋樹通常情況下采用的鏈式儲存,也就是使用連結串列的形式,那我們就知道了,連結串列的的話對於更新的操作效率比較高,因為只需要改動相應指標指向即可,不用進行挪動什麼的(陣列),那麼二叉搜尋樹採用鏈式儲存自然有這個優點:

所以,對於二叉搜尋樹而言,它相比較其他資料結構來說在查詢,插入和刪除等效率比較高,時間複雜度為

這裡可能有人會有疑問了,連結串列是在更新操作效率比較高,查詢的話不應該是陣列更快嗎?連結串列的查詢需要依次遍歷,時間複雜度不是O(n)嗎?

確實如此,但是這裡在二叉搜尋樹這塊就變了,我們應該還記得二叉搜尋樹上的節點資料的特點吧,也就是我們上面方分析的,比左邊的大,比右邊的小,我們查詢的時候根節點開始,進行的就是二分查詢啊,因為比當前節點小的數都在左邊,大的都在右邊,而二分查詢的時間複雜度為

所以在查詢這塊,二叉搜尋樹的效率還是可以的。

二叉搜尋樹相關操作的時間複雜度

然後我們來看下關於二叉搜搜索樹的相關操作的時間複雜度,這裡有一張圖片挺好的,來自維基百科:

可以看到,對於二叉搜尋樹的搜尋,插入和刪除來說,時間複雜度都是

不過這個都是平均,這裡還有個最差的情況降低到了O(n)的情況,這是怎麼回事呢?我們看下,對於二叉搜尋樹來說可能有這樣的情況:

就是比如有個有序的序列,資料大小一次增大或者增小,就比如上面的資料一次變大,這樣實際上就會退化成連結串列了,這個時候搜尋,插入和刪除的時間複雜度最差就退化到O(n)了。

那這個時候怎麼辦呢?這就需要進行優化,也就是需要旋轉,像AVL和紅黑樹就解決了這個問題,就是二叉搜尋樹的自平衡,這樣就可以把最差的時間複雜度也優化到

簡單小結

到了這裡,我們基本上把二叉搜尋樹一些基本的概念知識點都介紹的差不多了,當然,可能有遺漏,大家可以留言說一下,我後續還會發文進行補充的。

其實吧,我們再講二叉搜尋樹的話,接下來就是講關於二叉搜尋樹的實現了,也就是二叉搜尋樹如何實現查詢啊,增加和刪除啊,重要還有二叉搜尋樹的遍歷等等,這些其實就要涉及程式碼了,上面說的那些都是停留在二叉搜尋樹的概念性認識,就是讓你瞭解二叉搜尋樹,接下的重點就是我們需要自己手動實現一下二叉搜尋樹,這樣你才能真正的理解這個資料結構。

二叉搜尋樹該如何進行插入操作

就是不會寫程式碼

很多人剛開始學習資料結構的時候,都有這種感覺,我看概念的話覺得都懂了,但是真的讓我去實現,自己寫程式碼的話就會感到無從下手。

這其實也是我們學習程式設計都會遇到的一個問題,就是我看視訊或者看書覺得自己都可以看得懂啊,但是讓自己寫程式碼的話卻寫不出來,所以啊,你看的懂和能寫出來完全是兩碼事。

所以啊,平常學習程式設計一定要多敲程式碼

很多人在接觸連結串列也就是鏈式儲存這塊的時候,會迷惑這個節點咋表示啊,有點抽象啊,還有指標指向,這些看起來比較抽象,用程式碼?怎麼搞啊?

就拿今天說的二叉搜尋樹來說吧,我們要實現它的很重要的一步就是要確定這個節點怎麼表示啊,這個怎麼搞啊,有點懵啊,我們先來看畫圖怎麼表示的,比如這裡有一個二叉搜尋樹:

你就比如這個,我們該怎麼用程式碼表示呢?

節點該怎麼表示

首先啊,你看,我們直觀來看,是不是每個節點是一個數據,然後還有指標指向,就是那些箭頭,所以最基本的一個節點包括:

  1. 資料元素
  2. 箭頭(也就是指標,一個節點有兩個)

簡單來看是不是就是這些,要記住,這是節點包含這些內容,那麼這個節點是一個整塊的內容,該怎麼表示,在java中不就可以使用一個類來表示嘛,也即是這樣:

class Node {
        

    }

然後就是包含的裡面的東西,首先是資料,這個資料這裡暫定為整型型別,然後我們在類裡面定義這個資料元素:

class Node {
        int element;
    }

接下來就是資料指標的表示了,很多人會疑惑這個該怎麼表示,你想啊,這個指標指向的不也是個節點嘛,這裡的節點已經是個Node物件了,那麼一個節點裡面儲存的指標不就是指向另外一個節點嘛,這不就是儲存的另一個物件的引用地址嘛,所以我們可以直接在類裡面宣告節點物件,也即是這樣:

class Node {
        int element;
        Node left;
        Node right;
        Node parent;
    }

咋樣,是不是看明白了,然後這裡還需要記錄一個父節點,因為後續的插入啥的要根據父節點來操作,然後我們還需要新增建構函式:

class Node {
        int element;
        Node left;
        Node right;
        Node parent;
        public Node(int element,Node parent) {
            this.element = element;
            this.parent = parent;
        }
    }

如此一來,我們就表示了二叉搜尋樹的節點,接下來就是新增節點的操作。

二叉搜尋樹新增操作

在此之前,我們需要設定兩個引數,一個是代表節點個數,一個是代表根節點:

 private int size;
 private Node root;

這個應該好理解吧,然後就是新增操作:

    /**
     * 新增節點
     */
    public void add(int element) {
        //新增第一個節點
        if (root == null) {
            root = new Node(element, null);
            size++;
            return;

        }
     }

這段程式碼的邏輯也很簡單,你想啊,我們剛開始肯定是個空樹,那麼你新增的第一個節點理所應當的稱為根節點,然後節點個數加一,這個不難理解啊,接下來我們看後續的新增操作:

//新增的不是第一個節點
        //找到父節點
        Node parent = null;
        Node node = root;
        int cmp = 0;

        //插入的節點與父節點進行比較
        while (node != null) {
             cmp = compare(element, node.element);

            //向左向右之前儲存父節點
            parent = node;

            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else { //相等
                node.element = element;
                return;
            }
        }

這些程式碼就需要好好說道說道了,不然我真怕有些夥伴看不懂,首先我們先看這個圖:

假設這裡的根節點79已經新增上去,我們新增第二個節點的時候,是不是需要與根節點的79進行比較,看看是比79大還是小,從而決定該把新插入的第二個節點放在79的左邊還是右邊,然後目前這個樹只有一個根節點79,它是沒有父節點的,對應的就是這些程式碼:

//新增的不是第一個節點
        //找到父節點
        Node parent = null;
        Node node = root;
        int cmp = 0;

這個node就代表當前節點,此時就是根節點啊,這裡還定義一個整型變數cmp,主要是後續為了比較兩個節點根絕cmp的值來確定兩個值的大小。

接下來就是核心程式碼:(目的就是找到新插入節點的父節點)

//插入的節點與父節點進行比較
        while (node != null) {
             cmp = compare(element, node.element);

            //向左向右之前儲存父節點
            parent = node;

            if (cmp > 0) {
                node = node.right;
            } else if (cmp < 0) {
                node = node.left;
            } else { //相等
                node.element = element;
                return;
            }
        }

繼續看這個圖:

假如我們要插入一個新的節點,是不是需要先與根節點78比較,看看是大還是小,比較有三種結果,如果是等於的情況,就直接覆蓋當前節點的資料值,如果是大於或者小於,那都需要下移,將新插入的節點資料與33或者88比較,而此時的節點(需要與新插入節點比較的那個節點)node就變成了33或者88了,而無論是33還是88,79都是他們的父節點(其實是父節點的資料值,我這裡為了敘述方便)。

所以需要把33或者88所在的節點程式設計當前節點node好與當前節點比較,在此之前需要先記錄著父節點,比如這裡新增加的節點的資料值比79大,那需要與79的右節點的資料值比較,圖上是88,假如這裡是空呢?我們是不是就需要把新插入的節點放在88的位置。自然而然,79就是我們插入新的節點的父節點,這就達到了我們的目的。

ps:這裡可能沒那麼好理解,記住核心,你插入新節點,就需要需要當前節點比較,比當前節點大就去與當前節點的右子節點比較,小就去與當前節點的左子節點比較,直到找到左右子節點為空,就是新節點的位置,在此之前也記錄了父節點的位置,後續就可以根絕父節點插入新節點了

怕你們不理解

然後這句程式碼: node = node.right;執行結果node肯定是空,因為我們預設79的右節點是空的,那麼這段程式碼就迴圈結束,也就進入以下程式碼:

// 看看插入到父節點的哪個位置
        Node newNode = new Node(element, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;

經過上面的步驟,我們找到了新插入節點的父節點,剩下的就是根據是比父節點資料大還是小,然後存放到父節點的左子節點還是右子節點,這個好理解!

然後就是那個比較方法了:

   /**
     * @param e1
     * @param e2
     * @return 0代表相同,大於0,e1大,否則e2大
     */
    private int compare(int e1, int e2) {
        return e1-e2;
    }

至此,關於二叉搜尋樹的簡單插入就實現了,當然,上述實現肯定有不足之處,比如有些地方需要判空等等,這裡只是提供一個思路和簡單實現,後續我們會慢慢完善的。

好了,今天的文章就到這裡,後續我們會繼續探討二叉搜尋樹的其他知識,歡迎持續關注!

有相同愛好的可以進來一起討論哦:企鵝群號:1046795523

學習視訊資料:http://www.makeru.com.cn/live/1392_1164.html?s=143793