看了這篇文章,不再怕關於樹的面試題了

基礎知識就像是一座大樓的地基,它決定了咱們技術的高度java

在面試中,關於樹的問題是不少的,例如簡單點的會問你關於樹的前中後序的遍歷順序是怎樣的?難點會讓你手寫關於樹的算法題,又或是在Java後端面試中也會涉及到一些樹的知識,例如在HashMap中產生哈希衝突生成的鏈表到必定條件下爲何要轉成紅黑樹?,爲何要用紅黑樹而不用B+樹呢?在Mysql中索引的存儲爲何用B+樹而不用其餘樹等等。其實這些東西咱們在平常開發過程當中都會用到,其實每一個程序員都不甘心天天工做只是CRUD,那麼這些數據結構其實就是內功,咱們學會了它在平常工做分析問題,選用什麼集合,排序怎麼排,這些問題中咱們會多一些選擇。git

什麼是樹

樹(英語:tree)是一種抽象數據類型(ADT)或是實現這種抽象數據類型的數據結構,用來模擬具備樹狀結構性質的數據集合。它是由n(n>0)個有限節點組成一個具備層次關係的集合。把它叫作「樹」是由於它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。程序員

再完備的定義也沒有圖直觀,咱們先來看一下圖(這裏借用數據結構與算法做者王爭老師的圖,下面看到相似風格的圖都是如此)。github

咱們能夠看到這裏的樹和咱們現實生活的樹是十分相像的,其中每一個元素咱們稱之爲節點,而用直線相連的關係咱們稱之爲父子關係。那麼什麼才能稱之爲樹呢?web

  • 每一個節點都只有有限個子節點或無子節點
  • 沒有父節點的節點稱爲根節點
  • 每個非根節點有且只有一個父節點
  • 除了根節點外,每一個子節點能夠分爲多個不相交的子樹
  • 樹裏面沒有環路

樹的分類有不少,可是咱們經常使用的仍是二叉樹(每一個節點最多含有兩個子樹的樹稱爲二叉樹),下圖中所示其實就是二叉樹。面試

二叉樹中也會有分類,其中上圖中②是滿二叉樹(除了葉子節點之外,每一個節點都有左右兩個子節點),上圖③是徹底二叉樹(其它各層的節點數目均已達最大值,最後的全部節點從左向右連續地緊密排列,這樣的二叉樹被稱爲徹底二叉樹),這裏關鍵點是從左到右排列。那麼爲何會有徹底二叉樹呢?這裏涉及到了樹的兩種存儲方式,一種直觀的感覺就是直接鏈表存儲。Node節點設置左右子節點兩個元素。代碼以下。算法

1class TreeNode <T>{
2    public T data;
3    public TreeNode leftNode;
4    public TreeNode rightNode;
5
6    TreeNode(T data){
7        this.data = data;
8    }
9}
複製代碼

另外一種方式就是基於數組順序存儲,咱們把樹從根節點開始放入數組下標爲1的索引處,接下來一次從左到右放入。sql

序號 左節點位置 右節點位置
1 2 3
2 4 5
3 6 7
…… …… ……
n 2n 2n-1

這裏若是是將根節點放在索引爲0的地方開始的話,那麼左節點就是2n+1,右節點就是2n+2後端

依據上面的表格,咱們應該可以獲得對於每一個節點n來講,它的左節點位置就是2n,它的右節點位置就是2n+1。這樣咱們就可以將徹底二叉樹存儲在數組的結構中去了。若是是非徹底二叉樹也用數組存儲的話,那麼數組的中間會產生許多的空洞,形成內存的浪費。api

用數組存儲數據的優勢在於無需向鏈表那樣存儲左右子節點的指針,節省空間。

二叉樹的遍歷

上面咱們簡單瞭解了樹的定義,接下來咱們學習一下二叉樹的遍歷(前序遍歷、中序遍歷、後序遍歷),這也是面試中常常被問到的點。

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

咱們用代碼表示一下更加直觀。

 1/**
2@Description: 前序遍歷
3@Param: [treeNode]
4@return: void
5@Author: hu_pf
6@Date: 2019/11/26
7*/

8private static void frontOrder(TreeNode<String> treeNode){
9    if (treeNode == null){
10        return;
11    }
12    System.out.printf(treeNode.data);
13    frontOrder(treeNode.leftNode);
14    frontOrder(treeNode.rightNode);
15}
16
17/**
18@Description: 中序遍歷
19@Param: [treeNode]
20@return: void
21@Author: hu_pf
22@Date: 2019/11/26
23*/

24private static void middleOrder(TreeNode<String> treeNode){
25    if (treeNode == null){
26        return;
27    }
28    middleOrder(treeNode.leftNode);
29    System.out.printf(treeNode.data);
30    middleOrder(treeNode.rightNode);
31}
32
33/**
34@Description: 後序遍歷
35@Param: [treeNode]
36@return: void
37@Author: hu_pf
38@Date: 2019/11/26
39*/

40private static void afterOrder(TreeNode<String> treeNode){
41    if (treeNode == null){
42        return;
43    }
44    afterOrder(treeNode.leftNode);
45    afterOrder(treeNode.rightNode);
46    System.out.printf(treeNode.data);
47}
複製代碼

二叉查找樹

二叉查找樹中,每一個節點的值都大於左子樹節點的值,小於右子樹節點的值

二叉查找樹就是動態的支持數據的增刪改查,並且仍是自然有序的,咱們只要經過中序遍歷那麼的到的數據就是有序的數據了。

二叉查找樹的插入

咱們只須要從根節點開始,依次比較要插入的數據和節點的關係便可。若是插入的數據比當前節點大,而且其右節點沒有數據則放到其右節點上去,若是其右節點有數據,那麼久再次遍歷其右子樹。若是插入數據比當前數據小的話過程相似。

用代碼表示以下。

 1private void insertTree(int data){
2    if (this.rootNode == null){
3        rootNode = new TreeNode(data);
4        return;
5    }
6    TreeNode<Integer> p = rootNode;
7    while (p!=null){
8        Integer pData = p.data;
9        if (data>=pData){
10            if (p.rightNode == null){
11                p.rightNode = new TreeNode(data);
12                break;
13            }
14            p = p.rightNode;
15        }else {
16            if (p.leftNode == null){
17                p.leftNode = new TreeNode(data);
18                break;
19            }
20            p = p.leftNode;
21        }
22    }
23}
複製代碼

二叉查找樹的查找

二叉樹的查找的話就比較簡單了,拿要找的數據和根節點相比較,若是查找的數據比它小則在左子樹進行查找,若是查找的數據比它大則在右子樹進行查找。

 1private TreeNode findTreeNode(int data){
2
3    if (this.rootNode == null){
4        return null;
5    }
6
7    TreeNode<Integer> p = rootNode;
8    while (p != null){
9        if (p.data == datareturn p;
10        if (data >= p.data) p = p.rightNode;
11        else p = p.leftNode;
12    }
13    return null;
14}
複製代碼

二叉查找樹的刪除

二叉查找樹的刪除操做比較複雜,須要考慮三種狀況。

  • 刪除的節點無子節點:直接刪除便可
  • 刪除的節點只有一個節點:父節點的引用換成其子節點便可
  • 刪除的節點有兩個節點:那麼咱們須要想了,左子節點數據<父節點數據<右子節點數據,此時咱們若是刪除了父節點的話,那麼就須要從左節點或者右節點找到一個節點移動過來佔據此位置,而且移動完之後還要保持一樣的大小關係,那麼移動哪一個呢?移動左子節點最大的節點,或者右子節點最小的節點。這裏你們須要細細品味一下。爲何要這樣移動。

要找到右子節點最小的節點,只要找到右子節點哪一個沒有左節點就表明那個節點是最小的,相反的若是要找到左子節點最大的節點,只要找到左子節點哪一個沒有右節點就表明那個節點是最大的。

接下來咱們看一下刪除的代碼

 1private void deleteTreeNode(int data){
2
3    if (this.rootNode == null ) return;
4
5    TreeNode<Integer> treeNode = rootNode;
6    TreeNode<Integer> treeNodeParent = null;
7    while (treeNode != null){
8        if (treeNode.data == databreak;
9        treeNodeParent = treeNode;
10        if (data >= treeNode.data) treeNode = treeNode.rightNode;
11        else treeNode = treeNode.leftNode;
12    }
13
14    // 沒有找到節點
15    if (treeNode == nullreturn;
16
17    TreeNode<Integer> childNode = null;
18    // 1. 刪除節點沒有子節點
19    if (treeNode.leftNode == null && treeNode.rightNode == null){
20        childNode = null;
21        if (treeNodeParent.leftNode == treeNode) treeNodeParent.leftNode = childNode;
22        else treeNodeParent.rightNode = childNode;
23        return;
24    }
25
26    // 2. 刪除節點只有一個節點
27    if ((treeNode.leftNode !=null && treeNode.rightNode==null)||(treeNode.leftNode ==null && treeNode.rightNode!=null)){
28        // 若是此節點是左節點
29        if (treeNode.leftNode !=null)  childNode = treeNode.leftNode;
30        // 若是此節點是右節點
31        else childNode = treeNode.rightNode;
32        if (treeNodeParent.leftNode == treeNode) treeNodeParent.leftNode = childNode;
33        else treeNodeParent.rightNode = childNode;
34        return;
35    }
36
37
38    // 3. 刪除的節點有兩個子節點都有,這裏咱們演示的是找到右子節點最小的節點
39    if (treeNode.leftNode !=null && treeNode.rightNode!=null){
40        TreeNode<Integer> minNode = treeNode.rightNode;
41        TreeNode<Integer> minNodeParent = treeNode;
42        while (minNode.leftNode!=null){
43            minNodeParent = minNode;
44            minNode = minNode.leftNode;
45        }
46        treeNode.data = minNode.data;
47        if (minNodeParent.rightNode != minNode) minNodeParent.leftNode = minNode.rightNode;
48        else minNodeParent.rightNode = minNode.rightNode;
49    }
50}
複製代碼

刪除的代碼比較複雜,這裏還有一個簡單的作法,就是將其標記爲刪除。這樣也就無需移動數據,在插入數據的時候判斷是否刪除便可。可是會比較佔用內存。

平衡二叉樹——紅黑樹

平衡二叉樹(Balanced Binary Tree)具備如下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹。

上面咱們講了什麼是查找二叉樹,那麼查找二叉樹在一些極端狀況下有可能變成一個鏈表,因此再進行查找的話效率就會變低,而平衡二叉樹就是爲了解決這個問題出現的,即讓整棵樹看起來比較均勻,左右子節點的數量大體相同。這樣就不會再極端狀況下變成鏈表了。那麼一般狀況下咱們經常使用的平衡二叉樹就是紅黑樹

這裏我不講解紅黑樹實現的代碼,由於實在是太麻煩了,大概說一下他在Java後端的應用場景。Java的HashMap的結構實際上是數組加鏈表的結構,若是一個槽的鏈表過多的話也會影響性,因此當鏈表長度爲8的時候就會自動轉換爲紅黑樹,以增長查詢性能。接下來我會結合我自身面試的狀況給你們說一下紅黑樹在面試中常常被問到的點。

  • HashMap何時後會將鏈表轉換爲紅黑樹?鏈表長度爲8的狀況
  • 在什麼狀況下紅黑樹會轉換爲鏈表?紅黑樹的節點爲6的時候
  • 爲何鏈表要轉換爲紅黑樹?答出鏈表查詢性能很差便可
  • 爲何不用其餘的樹?關鍵點,平衡樹,答出紅黑樹的優勢便可
  • 爲何不用B+樹?關鍵點,磁盤,內存。紅黑樹多用在內部排序,即全放在內存中的。B+樹多用於外存上時,B+樹也被稱之爲一個磁盤友好的數據結構

正常狀況在面試中面試官是不會讓你手寫紅黑樹之類的,我在面試中大概碰到的就上面幾個,只要答出紅黑樹的優勢以及應用場景差很少就夠了。

關於樹的排序

是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子節點的鍵值或索引老是小於(或者大於)它的父節點。

在面試中其實也會常常碰到一些排序的算法,例如堆排序或者堆排序的一些應用例如前求TopN的數據。其實堆的結構也是樹。堆是具備如下性質的徹底二叉樹:每一個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每一個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。

其中第 1 個和第 2 個是大頂堆,第 3 個是小頂堆,第 4 個不是堆。既然堆本質上是一個徹底二叉樹,那麼咱們徹底能夠用數組來存堆的數據。那麼當前節點是n的話,其左子節點就是2n,其右子節點就是2n+1。

 1class Heap{
2    // 存放堆中的數據,用數組來裝
3    private int [] a;
4
5    // 堆中能存放的最大數
6    private int n;
7
8    // 堆中已經存放的數量
9    private int count;
10
11    public Heap(int capacity){
12        a = new int [capacity + 1];
13        this.n = capacity;
14        count = 0;
15    }
16}
複製代碼

如何在堆中插入數據

接下來咱們如何動態的將數據插入到堆中呢?

例如咱們上圖中的在堆中插入元素22,那麼此時應該怎麼作來保持他還是一個堆呢?此時咱們上圖中演示的大頂堆,那麼就要保證大頂堆的概念要求,每一個節點的值都大於等於其左右子節點的值。那麼咱們將其放入到數組最後一個位置,而後和它的父節點(n/2就是他的父節點)進行比較,若是大於它的父節點,就和父節點調換位置,繼續和其父節點比較,直到它小於其父節點就中止。代碼實現以下。

 1public void insert(int data){
2    if (count >= n) return;
3
4    count++;
5    // 把數據放到數組中
6    a[count] = data;
7
8    int index = count;
9    // 開始進行比較,先判斷跳出條件
10    // 1. 首先能想到的是插入的數據知足了大(小)頂堆的數據要求
11    // 2. 加上極值條件,及是一顆空樹狀況
12    while (index/2>0 && a[index]>a[index/2]){
13        swap(a,index,index/2);
14        index = index/2;
15    }
16}
17
18private void swap(int [] a, int i , int j){
19    int swap = a[i];
20    a[i] = a[j];
21    a[j] = swap;
22}
複製代碼

如何刪除堆頂元素

咱們直到大小頂堆的根節點值是最大的或者最小的。那麼若是刪除了堆頂元素,接下來就要從左右子節點選個最大或者最小的放入到根節點,而後以此類推。那麼咱們按照咱們剛纔分析的若是進行移動的話,可能會產生數組的空洞。

咱們能夠將根節點移除之後,而後再將數組最後的值給移到根節點,而後再進行依次比較換位置,這樣就不會產生數組空洞了。

代碼以下

 1public void removeMax(){
2
3    if (count == 0return;
4
5    // 將最後的元素移動到堆頂
6    a[1] = a[count];
7
8    count--;
9
10    heapify(a,count,1);
11}
12
13private void heapify(int [] a,int n,int i){
14    // 定義何時結束,當前節點與其左右子節點進行比對,若是當前節點是最大的則跳出循環(表明當前節點已是最大的)
15    while (true){
16        int maxIndex = i;
17        // 找到三個節點中最大節點的索引值
18        if (2*i<= n && a[i]<a[2*i]) maxIndex = 2*i; // 判斷當前節點,是否小於左節點
19        if (2*i+1<= n && a[maxIndex]<a[2*i+1]) maxIndex = 2*i+1;// 判斷最大節點是否小於右節點
20        // 若是當前節點已是最大節點就中止交換並中止循環
21        if (maxIndex == i )break;
22        // 找到中最大值的位置,並交換位置
23        swap(a,i,maxIndex);
24        i = maxIndex;
25    }
26}
複製代碼

堆排序

咱們對於傳進來一個無序的數組如何利用堆來進行排序呢。那麼既然是利用堆來排序,那麼咱們第一步確定是先將此數組變成一個堆結構。

建堆

如何將一個無序的數組變成一個堆結構呢?第一種咱們很容易就能想到的就是依次調用咱們上面的插入的方法,可是這樣所用的內存會加倍,即咱們會新建一個內存進行存儲這個堆。那麼若是咱們想無需新增內存直接再原有的數組將其變成堆,該如何作呢?

這裏數組是否是堆,那麼如何將其變成堆呢?這裏咱們採用從下往上建堆的方法,什麼意思呢?其實就是遞歸的思想,咱們先將下面的建好堆,而後依次往上傳遞。咱們先堆化標號爲4的樹,而後再堆化標號爲3的,而後堆化標號爲2的,而後堆化標號爲1的。依次進行。到最後咱們就獲得了一個堆。下圖中畫圓圈表明了其堆化的樹的範圍。代碼以下

 1public void buildHeap(int[] a,int n){
2    for (int i =n/2;i>=1;i--){
3        heapify(a,n,i);
4    }
5}
6
7private void heapify(int [] a,int n,int i){
8// 定義何時結束,當前節點與其左右子節點進行比對,若是當前節點是最大的則跳出循環(表明當前節點已是最大的)
9while (true){
10    int maxIndex = i;
11    // 找到三個節點中最大節點的索引值
12    if (2*i<= n && a[i]<a[2*i]) maxIndex = 2*i; // 判斷當前節點,是否小於左節點
13    if (2*i+1<= n && a[maxIndex]<a[2*i+1]) maxIndex = 2*i+1;// 判斷最大節點是否小於右節點
14    // 若是當前節點已是最大節點就中止交換並中止循環
15    if (maxIndex == i )break;
16    // 找到中最大值的位置,並交換位置
17    swap(a,i,maxIndex);
18    i = maxIndex;
19}
20}
複製代碼

排序

排序的話就簡單了,由於堆的根節點是最大或者最小的元素,因此第一種咱們依然可以想到的是直接將其根節點的值拿出來後放到另外一個數組,而後刪掉堆頂元素,而後再拿掉堆頂元素,而後再刪除依次類推。這樣有一個缺點仍是佔用內存,由於咱們又從新定義了一個數組,那麼有沒有辦法不佔用內存,直接在原有的數組中進行排序呢?

第二種辦法就是不須要藉助另外一個數組,怎麼作呢?這個過程優勢相似於刪除堆頂元素,不一樣的是刪除堆頂元素咱們直接堆頂元素丟棄了,而排序的話咱們須要將堆頂元素和最後一個元素進行互換,互換完之後而後再將其堆化,而後再將堆頂元素與最後一個元素的前一個元素互換,而後再堆化,以此類推。

如何求前TopN的數據

相信微博你們都用,那麼其中的前十熱搜是如何實時的顯現呢?其實求前TopN的數據用到的基本上都是堆,咱們能夠創建一個大小爲N的小頂堆,若是是靜態數據的話,那麼就依次從數組中取出元素與小頂堆的堆頂元素進行對比,若是比堆頂元素大,那麼就丟棄堆頂元素,而後將數組中元素放入到堆頂,而後堆化。依次進行,最後獲得的就是TopN大的數據了。若是是動態數據的話和靜態數據也同樣,也是進行不斷的進行比對。最後若是想要TopN大的數據直接將此堆返回便可。

接下來我用動圖演示一下是怎麼求得的TopN數據。

數組爲: 1 2 3 9 6 5 4 3 2 10 15
求得Top5

本文代碼地址

有感興趣的能夠關注一下我新建的公衆號,搜索[程序猿的百寶袋]。或者直接掃下面的碼也行。

參考

相關文章
相關標籤/搜索