有了高效的散列表結構,爲什麼還要使用二叉樹?

面試三連

面試官: 知道二叉樹嗎?

小明: 知道一點…

面試官: 那你說一下什麼是二叉樹?

小明: 在計算機科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查找樹和二叉堆。

面試官: 哦,還了解二叉查找樹啊,二叉樹的查找效率並不是特別的高,相對於散列表來說慢很多,爲什麼還需要二叉樹呢?

小明: 不知道。

面試官: 好了,回去等通知吧

就這樣,小明失去了這次的工作機會,理由就是沒有回答出 有了散列表爲什麼還要使用二叉樹 ,我們一起着這個問題來看看下面講解的內容:二叉樹

什麼是樹

樹狀圖是一種數據結構,它是由n(n>=0)個有限節點組成一個具有層次關係的集合。把它叫做“樹”是因爲它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:

每個節點有零個或多個子結點;沒有父節點的結點稱爲根節點;每一個非根節點有且只有一個父節點;除了根節點外,每個子節點可以分爲多個不相交的子樹(百度百科)。

畫張圖,一起來理解一下
在這裏插入圖片描述
類似這種結構的就是樹結構,a是根節點同時也是b、c的父節點,b是d的父節點同時是a的子節點,d沒有子節點,所以它是葉子節點(我們把沒有子節點的節點稱爲葉子節點),c是e、f的父節點同時也是a的子節點,d、f沒有子節點,屬於葉子節點

上面是樹的基本介紹,其實,樹還有三個概念需要我們掌握:高度、深度、層

高度:節點到葉子節點的路徑(節點個數),從0開始計數

深度:根節點到某個節點所經歷的路徑(節點個數),從0開始計數

層:深度+1

概念很抽象,畫圖很重要,我們使用圖形表示一下什麼是高度、什麼是深度、以及什麼是層
在這裏插入圖片描述
解釋一下:高度從葉子節點開始計算,葉子結點高度爲0;深度從根節點開始計算,根節點深度爲0;層從根節點開始計算,根節點爲第1層。

二叉樹

什麼是二叉樹

在計算機科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查找樹和二叉堆。

簡單點來說就是一個節點只有兩個子節點,一個左節點,一個右節點,這樣的樹我們成爲二叉樹,雖然定義是這樣,但是二叉樹並不要求我們每個節點都需要滿足兩個子節點,有的只有左節點,有的只有右節點,他們也可以被稱爲二叉樹

我們來對比一下二叉樹和非二叉樹

在這裏插入圖片描述

在圖中,我們可以很清晰的看出,二叉樹中每個節點都只有兩個子節點,但是在非二叉樹中,我們發現有幾個節點都有3個子節點,這種結構只能稱爲普通樹結構。

在二叉樹中也有比較特殊的樹,主要有兩種:滿二叉樹完全二叉樹

滿二叉樹: 一個二叉樹,如果每一個層的節點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數爲K,且每層節點總數是2k-1 ,則它就是滿二叉樹

完全二叉樹:完全二叉樹是效率很高的數據結構,完全二叉樹是由滿二叉樹而引出來的。對於深度爲K的,有n個節點的二叉樹,當且僅當其每一個節點都與深度爲K的滿二叉樹中編號從1至n的節點一一對應時稱之爲完全二叉樹,簡單點來說,葉子節點在最底下兩層,並且最後一層葉子節點都靠左,除了最後一層,其他節點都要有兩個子節點

在這裏插入圖片描述

二叉樹的存儲結構

二叉樹是怎麼存儲的呢?其實他有兩種數據結構,一種是數組,一種是基於指針的鏈表,對於二叉樹而言使用鏈表存儲相對於數組存儲簡單的許多。

鏈表式的二叉樹

在這裏插入圖片描述
鏈表式存儲需要耗費額外的空間用來存儲子節點的指針,用於方便查找,我們只需要知道根節點的指針,就能把整棵樹都找出來,不知道你們有沒有發現,在葉子節點中也存儲了左右節點的指針,但是值爲null,當他們有新的子節點的時候就會將子節點的指針存放到對應的位置,這種存儲方式是我們經常使用的方式。

數組形式的二叉樹

在這裏插入圖片描述

數組下標 0 1 2 3 4 5 6 7 8 9 10 11
數據 a b c d e f g h

我們將根節點存放在數組下標爲 i=1的位置,那麼他的左子節點(b)所在的下標位置=2 × i=2 ,右子節點(c)= 2 × i- 1= 3,d的下標 = 2 × i(b的下標) = 4,e的下標= 2 × i(b的下標) - 1 = 5,以此類推,所以我們可以得出一個結論:左子節點下標=2 × 當前節點下標,右子節點下標 = 2 × 當前節點下標 - 1 。

仔細看一下上面的表格,數組下標以及數據,我們發現數組長度12,其中只有8個下標中有數據,4個地址中是空的,所以這種存儲方式會浪費大量的存儲空間,這樣一看是不是數組就不適合二叉樹呢?

顯然不是這樣的,數組這種結構也是可以用在二叉樹上的,只是有一個條件,當二叉樹爲完全二叉樹的時候使用數組存儲相對鏈表存儲更節省存儲空間,爲什麼這麼說呢?我們一起看看完全二叉樹的數組存儲。
在這裏插入圖片描述

數組下標 0 1 2 3 4 5 6 7 8 9 10
數據 a b c d e f g h i j

這是完全二叉樹使用數組存儲的結果,在表格中,數組長度爲11,其中10個地址都被使用了,只有下標爲0的地址處於未被使用狀態,這樣我們的數組得到了充分的使用,僅僅只是浪費了一個地址,相對於鏈表來說,更省空間,因爲鏈表需要額外的空間存儲左右子節點的指針信息,由於數組地址是連續的,它只需要記錄自己的地址即可,所以完全二叉樹(或者滿二叉樹)推薦使用數組結構存儲。

你可能會疑惑爲什麼完全二叉樹的葉子節點都是靠左而不是靠右,看了上面的分析你應該大致明白了吧,因爲靠左不會有數組空間的浪費,如果靠右的話,會導致數組中間至少有一個地址未被使用,所以完全二叉樹纔要求葉子結點都靠左。

二叉樹的遍歷

說了這麼多都只是對二叉樹的介紹,卻沒有講到怎麼使用二叉樹,接下來我們一起來看看二叉樹是如何遍歷的。

我們需要使用什麼方式將存儲好的二叉樹遍歷出來呢?我們常用的方法有三種,分別是:前序遍歷中序遍歷後序遍歷

  • 前序遍歷:對樹結構中的某個節點來說,先打印它自己,再打印他的左子節點,最後打印它的右子節點。
  • 中序遍歷:對樹結構中的某個節點來說,先打印它的左子節點,再打印它本身,最後打印它的右子節點。
  • 後序遍歷:對樹結構中的某個節點來說,先打印它的左子節點,再打印它的右子節點,最後打印它本身。

前序遍歷
在這裏插入圖片描述
我們從根節點開始打印,找到根節點(a)先打印它本身,然後判斷他是否存在左子節點,和明顯,b是它的做左子節點,所以再打印b,再通過b查找它的左子節點 —> d,打印d,尋找d的左子節點,沒有了,尋找d的右子節點,也沒有,往回走,然後查找b的右子節點 —> e,打印e,由於e也沒有左右子節點了,往回走,回到根節點,所以這個時候根節點a的左子節點全打印完了,這時候再去查找根節點a的右子節點 —>c,打印c,查找c的左子節點 —>f,打印f,f沒有左右子節點了直接往回走,再尋找c的右子節點 —> g,打印g,g沒有左右子節點,往回走,回到根節點,遍歷完成。

所以前序遍歷的最終結果:a, b, d, e, c, f, g

中序遍歷
在這裏插入圖片描述
後序遍歷
在這裏插入圖片描述
中序遍歷和後序遍歷的過程我就不寫了,可以參考一下前序遍歷,自己思考一下,如果不明白,可以在評論中留言,我會將他們補充完成。

這麼講解你有沒有對二叉樹的遍歷有一點點理解了呢?這只是概念,下面我們一起使用代碼實現一遍二叉樹的前中後序遍歷(java代碼實現)

NodeTree.java

package binarytree;

public class NodeTree {

    /**
     * 數據
     */
    private Object data;

    /**
     * 左子節點
     */
    private NodeTree leftNodeTree;

    /**
     * 右子節點
     */
    private NodeTree rightNodeTree;



    public NodeTree (Object data, NodeTree leftNodeTree,NodeTree rightNodeTree){
        this.data = data;
        this.leftNodeTree = leftNodeTree;
        this.rightNodeTree = rightNodeTree;
    }

    public Object getData() {
        return data;
    }

    public NodeTree getLeftNodeTree() {
        return leftNodeTree;
    }

    public NodeTree getRightNodeTree() {
        return rightNodeTree;
    }
}

Test.java

package binarytree;

import java.util.ArrayList;
import java.util.List;

public class Test {

    private static  NodeTree rootNode = null;

    private static List<Object> list = new ArrayList<>();


    static {
        //數據初始化,方便樹結構的打印,初始化的數據結構與上面的前中後序打印的圖一樣

        //b節點的左子節點
        NodeTree leftNodeThirdL = new NodeTree("d",null,null);
        //b節點的右子節點
        NodeTree leftNodeThirdR = new NodeTree("e",null,null);
        //根節點的左子節點 b
        NodeTree leftNodeSecond = new NodeTree("b",leftNodeThirdL,leftNodeThirdR);

        //c節點的左子節點
        NodeTree rightNodeThirdL = new NodeTree("f",null,null);
        //c節點的右子節點
        NodeTree rightNodeThirdR = new NodeTree("g",null,null);
        //根節點的右子節點 c
        NodeTree rightNodeSecond = new NodeTree("c",rightNodeThirdR,rightNodeThirdL);
        
        //根節點 a
        rootNode = new NodeTree("a",leftNodeSecond,rightNodeSecond);
    }


    /**
     * 前序遍歷
     * @param rootNode
     */
    private static void preTraversal(NodeTree rootNode){
        if(rootNode != null){
            list.add(rootNode.getData());
            if(null != rootNode.getLeftNodeTree()){
                preTraversal(rootNode.getLeftNodeTree());
            }
            if(null != rootNode.getRightNodeTree()){
                preTraversal(rootNode.getRightNodeTree());
            }
        }
    }

    /**
     * 中序遍歷
     * @param rootNode
     */
    private static void inorderTraversal(NodeTree rootNode){
        if(rootNode != null){
            if(null != rootNode.getLeftNodeTree()){
                inorderTraversal(rootNode.getLeftNodeTree());
            }
            list.add(rootNode.getData());

            if(null != rootNode.getRightNodeTree()){
                inorderTraversal(rootNode.getRightNodeTree());
            }
        }
    }



    /**
     * 後序遍歷
     * @param rootNode
     */
    private static void afterTraversal(NodeTree rootNode){
        if(rootNode != null){
            if(null != rootNode.getLeftNodeTree()){
                afterTraversal(rootNode.getLeftNodeTree());
            }
            if(null != rootNode.getRightNodeTree()){
                afterTraversal(rootNode.getRightNodeTree());
            }
            list.add(rootNode.getData());
        }
    }


    /**
     * 前中後序遍歷
     * @param args
     */
    public static void main(String[] args) {
        preTraversal(rootNode);
        System.out.println("前序遍歷結果:"+list);

        list = new ArrayList<>();
        inorderTraversal(rootNode);
        System.out.println("中序遍歷結果:"+list);

        list = new ArrayList<>();
        afterTraversal(rootNode);
        System.out.println("後序遍歷結果:"+list);

    }
}

打印結果

前序遍歷結果:[a, b, d, e, c, f, g]
中序遍歷結果:[d, b, e, a, f, c, g]
後序遍歷結果:[d, e, b, f, g, c, a]

Process finished with exit code 0

這個結果與我們畫圖分析的結果是一致的,並且代碼的實現也不是很複雜,不過如果你們仔細看上面的圖形的話,你可能就會思考一個問題,二叉樹的遍歷時間複雜度是多少

答案是:O(n),原因也很簡單,遍歷的時間複雜度只會和二叉樹中數據的多少(或者高度)有關,在遍歷過程中有些節點被訪問了兩次,按道理來說時間複雜度應該是O(2n),爲什麼是O(n)? 時間複雜度是隨着n的變化而變化,對於2這個常數來說,是可以忽略的,因爲他不是影響時間複雜度的關鍵因素,所以二叉樹的遍歷時間複雜度爲:O(n)。

到現在爲止我還是沒有講爲什麼要使用二叉樹,前面都是鋪墊,下面的東西纔是重點,請拿好筆記本,做好筆記。

二叉查找樹

概念

二叉排序樹(Binary Sort Tree),又稱二叉查找樹(Binary Search Tree),亦稱二叉搜索樹。

特點: 在樹的任意一個節點,它的左子節點的值都要小於當前節點,右子節點的值都要大於當前節點,我們將這樣的樹結構成爲二叉查找樹

插入一組從小到大的數據
在這裏插入圖片描述

插入一組從大到小的數據:8,7,6,5,4,3,2,1
在這裏插入圖片描述

插入一組無序的數據:5,3,2,4,1,7,6,8
在這裏插入圖片描述
中序遍歷:5,3,2,4,1,7,6,8
在這裏插入圖片描述

這就是二叉查找樹的基本結構,爲什麼交二叉查找樹?就是因爲它支持很快的查找能力,同時在刪除和插入速度方面也很高效。

查找與插入

如果我們需要在二叉查找樹中查找某個元素,我們只需要從根節點開始,首先判斷查找的值是否等於根節點的值,如果等於,直接返回,說明找到了,小於根節點的值,那麼就往根節點的左子節點中遞歸查詢,知道查到爲止,如果查找的值大於根節點的值,那麼往根節點的右子節點中遞歸查找,直到查到爲止,是不是有點二分的味道?

我們來看個例子,我們需要在一組數據中查找4這個元素
在這裏插入圖片描述

插入:插入之前也需要查找,它需要查找到自己應該插入的位置,插入時我們將第一個插入的數據作爲根節點,之後的插入的數據都會和根節點做對比,如果比根節點數據小,那麼就往根節點的左子節點中插入,判斷根節點的左子節點是否爲空,爲空直接插入,否則再次遞歸查詢,直到找到插入的位置位置,反之,如果插入的數據大於根節點數據,那麼判斷根節點是否存在右子節點,如果不存在,直接插入,如果存在,繼續遞歸查詢,找到插入的位置進行插入。

NodeTree.java

package binarytree;

public class NodeTree {

    /**
     * 數據
     */
    private Integer data;

    /**
     * 左子節點
     */
    private NodeTree leftNodeTree;

    /**
     * 右子節點
     */
    private NodeTree rightNodeTree;


    public NodeTree (Integer data){
        this.data = data;
        this.leftNodeTree = null;
        this.rightNodeTree = null;
    }

    public Integer getData() {
        return data;
    }

    public NodeTree getLeftNodeTree() {
        return leftNodeTree;
    }

    public NodeTree getRightNodeTree() {
        return rightNodeTree;
    }

    public void setLeftNodeTree(NodeTree leftNodeTree) {
        this.leftNodeTree = leftNodeTree;
    }

    public void setRightNodeTree(NodeTree rightNodeTree) {
        this.rightNodeTree = rightNodeTree;
    }

	@Override
    public String toString() {
        return "NodeTree{" +
                "data=" + data +
                ", leftNodeTree=" + leftNodeTree +
                ", rightNodeTree=" + rightNodeTree +
                '}';
    }
}

Test.java

package binarytree;

public class Test {

    private static NodeTree rootNode = null;

    /**
     * 搜索次數
     */
    private static int seaCount = 0;


    /**
     * 插入
     * @param data
     */
    private static void insert(int data) {
        if (rootNode == null) {
            //插入根節點
            rootNode = new NodeTree(5);
            return ;
        }
        NodeTree tree = rootNode;
        while (tree != null){
            //比較數據和當前節點值的大小
            if (data < tree.getData()) {
                //小於當前節點的值,判斷左子節點是否有數據,沒有數據就直接插入到左子節點中
                if (tree.getLeftNodeTree() == null) {
                    tree.setLeftNodeTree(new NodeTree(data));
                    return ;
                }
                //否者,獲取到當前節點的左子節點,繼續遞歸插入
                tree = tree.getLeftNodeTree();
            } else {
                //插入的值比當前節點值大
                //判斷當前節點是否存在右子節點,如果不存在,直接將數據插入到當前節點的右子節點中
                if (tree.getRightNodeTree() == null) {
                    tree.setRightNodeTree(new NodeTree(data));
                    return ;
                }
                //如果存在,獲取到當前節點的右子節點,繼續遞歸插入
                tree = tree.getRightNodeTree();
            }
        }
    }

    /**
     * 搜索
     * @param data
     * @return
     */
    private static NodeTree search(int data){
        if(rootNode == null){
            seaCount++;
            return null;
        }
        if(rootNode.getData() == data){
            seaCount++;
            return rootNode;
        }
        NodeTree tree = rootNode;
        while (null != tree ){
            seaCount++;
            if(data < tree.getData()){
                tree = tree.getLeftNodeTree();
            }else if(data > tree.getData()){
                tree = tree.getRightNodeTree();
            }else{
                return tree;
            }
        }

        return null;
    }


    /**
     * 
     *
     * @param args
     */
    public static void main(String[] args) {
        //  5,3,2,4,1,7,6,8
        insert(5);
        insert(3);
        insert(2);
        insert(1);
        insert(4);
        insert(7);
        insert(6);
        insert(8);
        System.out.println("插入完成");


        System.out.println("搜索開始-======");

        NodeTree search = search(4);
        System.out.println(search);
        System.out.println("搜索了"+seaCount+" 次");


    }
}

在這裏插入圖片描述
上面的代碼主要實現了插入並查找某個元素
在這裏插入圖片描述
對比一下上面查到的動圖,查找了三次就找到了4這個元素,我們使用代碼實現也是值搜索了3次就查找到了4,所以代碼實現的沒有毛病。

刪除節點(鏈表形式)

查找插入我放到了一起講,爲什麼要將刪除單獨分開講呢?這還不明顯嗎嗎,因爲它比較複雜啊。

如果我們現在需要刪除二叉樹中的某個節點,這個時候不是一個簡單的刪除就搞定了,而是要視情況而定,大致有以下三種情況。
1.刪除的節點屬於葉子結點:沒有子節點,這個時候只需要將他刪除即可,C語言只需要刪除父節點中指向需要刪除子節點指針即可,高級語言(java)只需要將父節點中需要刪除子節點的引用即可,刪除下圖的4 。

2.刪除的節點有一個子節點(左子節點或者右子節點都一樣,沒有特殊要求):我們只需要修改父節點中指向需要刪除的指針,讓它指向需要刪除節點的子節點,刪除下圖中的2 。

3.刪除的節點中含有兩個子節點(左子節點 && 右子節點):這種方式稍微麻煩一點,但是也無需過多擔心,第一步,我們需要找到需要刪除節點右子節點中最小的節點,第二步,將最小的節點替換到爲需要刪除的節點上,第三步,刪除最小的節點,這個最小節點應該是葉子節點,所以只要按照 1 的情況刪除即可,刪除下圖中的10 。
在這裏插入圖片描述

在這裏插入圖片描述

是不是看着有點懵?別擔心,我們還有動圖
在這裏插入圖片描述

這樣看是不是清晰多了,我們在使用代碼來實現一下刪除。

/**
     * 刪除節點
     * @param data
     */
    private static  void delete(int data){

        NodeTree tree = rootNode;
        //tree的父節點
        NodeTree treeP = null;

        if(null == tree){
            return ;
        }
        //找到需要刪除的節點
        while (null != tree && tree.getData() != data){
            treeP = tree;
            if(data < tree.getData()){
                tree = tree.getLeftNodeTree();
            }else{
                tree = tree.getRightNodeTree();
            }
        }
        //判斷需要刪除的節點是否包含子節點
        //如果刪除的節點有兩個子節點:左子節點與右子節點
        if( null != tree.getLeftNodeTree() && null != tree.getRightNodeTree()){
            //找到要刪除節點右子節點中最小的節點數據
            NodeTree minNode = tree.getRightNodeTree();
            NodeTree minNodeP = tree;
            while(null != minNode.getLeftNodeTree()){
                minNodeP = minNode;
                minNode = minNode.getLeftNodeTree();
            }
            //數據交換 將需要刪除節點的右子節點中最小節點的值替換到需要刪除的節點上
            tree.setData(minNode.getData());
            tree = minNode;
            treeP = minNodeP;
        }


        //刪除的節點沒有子節點(葉子結點)或者只有一個子節點
        NodeTree childTree = null;
        if(null != tree.getLeftNodeTree()){
            childTree = tree.getLeftNodeTree();
        }else if(null != tree.getRightNodeTree()){
            childTree = tree.getRightNodeTree();
        }

        if(null == treeP){
            rootNode = childTree;
        }else if(treeP.getLeftNodeTree() == tree){
            treeP.setLeftNodeTree(childTree);
        }else{
            treeP.setRightNodeTree(childTree);
        }
    }
public static void main(String[] args) {
        //  5,3,2,4,1,15,10,20,8,12,13,11

        int[] arr = {5,3,2,4,1,15,10,20,8,12,13,11};
        for(int i = 0; i<arr.length;++i){
            insert(arr[i]);
        }
        System.out.println("插入完成");
        delete(4);
        delete(2);
        delete(10);
        System.out.println(rootNode);
    }

刪除完成之後的數據
在這裏插入圖片描述
15的左子節點原本應該是10,但是現在變成了11,而11所在的位置已被刪除,數據4直接被刪除,數據2被刪除之後改變了數據3的左子節點的指針,指向數據1,這就是二叉樹的三種刪除情況,尤其是最後一種刪除的節點包含左右子節點,這種不是很好理解,代碼看起來也比較抽象,難理解,不過可以多看幾遍,然後自己動手走幾遍,相信就能理解的。

“邏輯刪除”

上面的刪除是不是很麻煩?還有一種很簡單的方式同樣可以以實現刪除邏輯,那就是邏輯刪除,將需要刪除的數據做一下標記,區分開來那些已經被刪除,哪些正在被使用,和數據庫的邏輯刪除性質一樣,很簡單,就不做代碼展示了。

如何插入重複數據

插入一個數據(a1)時,如果查找到一個和插入數據相同的數據(a2),那麼我們就將a1存放到a2的右子節點中,右子節點的邏輯處理和之前將的邏輯一致,由於a2的右子節點肯定大於a2,所以我們需要將a1存放到a2的右子節點的左子節點中。
在這裏插入圖片描述
那我們查找的時候需要怎麼做呢?和之前的查找一樣,只不過在找到第一個數據之後不要停止,需要繼續往右子節點查找,理由是可能會存在重複數據,繼續往右子節點查找是爲了將重複的數據也一起查找出來。

刪除操作其實也是之前的邏輯沒有什麼區別,只是需要多一個步驟,那就是將需要刪除的數據先查找出來,然後再執行刪除操作。

時間複雜度分析

二叉查找樹的時間複雜度應該是和樹的高度有關的,爲什麼這麼說呢?你可以仔細看看上面的插入、查找、刪除操作就可以得到結論了,所以二叉查找樹的時間複雜度=O(h) ,h:樹的高度。

那樹的高度怎麼計算出來呢?之前我一直只知道結論,log2nlog_2n,直到看到了一片很有深度的文章(王爭哥寫的數據結構算法)之後我終於知道這個高度是怎麼算出來的了。

對於滿二叉樹或者完全二叉樹而言,每層的節點數都有着這樣的一個規律:m = 2k-1(m:每層的節點數,k:層數)

那麼這棵樹最後一層有:2k-1個節點(k爲層數),但是呢,我們的樹又不可能百分百是滿二叉樹,所以還有一種情況那就是最後一層只有一個節點,所以最後一層的節點數應該是:1 到 2k-1之間, 我們假設那麼總節點數n的範圍應該在

n >= 1+2+4+8+…+2(k-2) +1
n <= 1+2+4+8+…+2(k-2) + 2(k-1)

1:第一層節點數;2:第二層節點數;4第三層節點數;2(k-2) 倒數第二層節點數: 2(k-1) :最後一層節點數。

你們發現什麼了嗎?沒錯這兩個等式都是等比數列,還記得等比數列的求和公式嗎?
formulaformula
具體我就不計算了,直接給出最終結果:k的取值範圍在[log2(n+1)log_2(n+1), log2nlog_2n +1],完全二叉樹的層數小於等於log2nlog_2n +1,又因爲高度 = 層數-1,所以樹的高度小於等於 log2nlog_2n

我們再將樹的高度嵌套回我們開始的公式:最好時間複雜度 = O(h) = O(log2nlog_2n)

這是最好情況,最壞情況不用我分析大家也知道,肯定是:O(n),所以二叉查找樹的時間複雜度在: O(log2nlog_2n) - O(n)之間,極度不穩定。

平衡二叉樹

定義很簡單:在二叉樹中每個節點的左右子節點高度不能相差1。這就是平衡二叉樹的概念,我們一起來對比一下平衡二叉樹和非平衡二叉樹。
在這裏插入圖片描述
其實我們經常看到的完全二叉樹和滿二叉樹都是平衡二叉樹,爲什麼會定義一個平衡二叉樹呢?就是爲了讓樹結構分佈的更加均勻,對於某個節點來說,不會出現左右子節點極度不平衡的情況,這樣可以儘可能的平衡查找、刪除、插入的時間。

開篇解答

有了高效的散列表結構,爲什麼還要使用二叉樹?

如果瞭解散列表的話都知道,它的刪除、查找、新增的時間複雜度能做到:O(1),而我們剛剛討論的二叉樹在最好情況下時間複雜度纔是 O(log2nlog_2n),爲什麼還推薦使用二叉樹呢?

1.雖然散列表的查找、刪除很快,但是它也存在很慢的時候,什麼時候呢?擴容與hash碰撞的時候,擴容會導致數據拷貝,hash碰撞會導致循環計算新的hash值,這個計算可能會讓你計算一天都沒有計算出新的hash值(雖然不太可能,這裏誇張了)。

2.有序性:如果要求我們將數據有序輸出,由於散列表是無序的,所以需要將數據先進行排序,在進行輸出,而二叉查找樹只需要進行一次中序遍歷即可。

3.散列表的結構設計要比二叉查找樹複雜的多,維護起來更加困難。

所以我們設計一個平衡二叉樹,在某些方面確實是比散列表強一點的,比如jdk1.8中 HashMap在hash衝突的時候也是引用了接近平衡二叉查找樹的紅黑樹,也足以說明二叉樹的優勢。

文章很長,需要細細品味,想當初我學習二叉樹的時候也是花了不少功夫呢,冰凍三尺非一日之寒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章