二分搜索樹(帶動畫演示)

在理解二分搜索樹之前认然,我們先來看看二叉樹是什么。

1.1 二叉樹

二叉樹也是一種動態(tài)的數(shù)據(jù)結構酌伊。每個節(jié)點只有兩個叉腾窝,也就是兩個孩子節(jié)點,分別叫做左孩子居砖,右孩子虹脯,而沒有一個孩子的節(jié)點叫做葉子節(jié)點。每個節(jié)點最多有一個父親節(jié)點奏候,最多有兩個孩子節(jié)點(也可以沒有孩子節(jié)點或者只有一個孩子節(jié)點)循集。47左半邊的所有節(jié)點組合起來形成了47的左子樹,47右半邊所有節(jié)點結合起來形成了47的右子樹蔗草。如下圖所示:

1-1

綜合一下咒彤,涉及到的概念有:

根節(jié)點:二叉樹的起始節(jié)點,唯一沒有父親節(jié)點的節(jié)點咒精;
父親節(jié)點:每個節(jié)點只有一個父親節(jié)點蔼紧。如上圖47就是35的父親節(jié)點;
左右孩子節(jié)點:每個節(jié)點至多擁有兩個孩子節(jié)點狠轻,分別叫左孩子,右孩子彬犯;
左子樹右子樹:每個節(jié)點左邊或者右邊部分所有節(jié)點組合成的樹結構向楼。

1.2 二分搜索樹

1.2.1 性質

第一查吊,二分搜索樹是一顆二叉樹,滿足二叉樹的所有定義湖蜕;
第二逻卖,二分搜索樹每個節(jié)點的左子樹的值都小于該節(jié)點的值,每個節(jié)點右子樹的值都大于該節(jié)點的值昭抒。
第三评也,任意節(jié)點的每顆子樹都滿足二分搜索樹的定義。

1-2

1.2.2 意義

當我們看到二分搜索樹的定義時灭返,是否會去聯(lián)系這樣定義的意義何在呢盗迟?其實,二分搜索樹是在給數(shù)據(jù)做整理熙含,因為左右子樹的值和根節(jié)點存在大小關系罚缕,所以在查找元素時,我們于根節(jié)點進行對比后怎静,就能每次近乎一半的去除掉查找范圍邮弹,這就大大的加快了我們的查詢速度,插入元素時也是一樣蚓聘。

在圖1-2中腌乡,如果要查找元素55,那么和根節(jié)點47對比后夜牡,發(fā)現(xiàn)55比47大与纽,于是就往47右孩子60中去查詢,接著發(fā)現(xiàn)55比60小氯材,就往60左孩子中查詢渣锦,于是就找到了這個元素。想象一下氢哮,如果是一個鏈表袋毙,那么將一個一個查詢下去,速度可想而知冗尤。

其實在生活中听盖,這樣的例子也比比皆是,我們去超市買東西裂七,超市也把一二三樓賣的是啥寫的很清楚皆看,假如三樓賣的是生鮮果蔬,而我們要買今天的菜背零,那么我們就直接去三樓腰吟,一樓和二樓我們就可以不用去找了,大大加快了我們選購商品的速度。圖書館找書也是這樣的例子毛雇。所以二分搜索樹的意義也就在此嫉称,很多時候數(shù)據(jù)結構其實來源于生活,于生活中解決實際問題灵疮,這就是技術的力量和價值的體現(xiàn)织阅。

但是,為了達到這樣的高效性震捣,樹結構由此也需要每個節(jié)點之間具備可比較性荔棉。而鏈表數(shù)據(jù)結構就沒有這類要求,所以還是那句話蒿赢,有得必有失润樱。

到此我們就可以輕松定義出來二分搜索樹的基本代碼如下:

public class BST<E extends Comparable<E>> {

    private class Node {
        public E e;
        public Node left, right;

        public Node(E e) {
            this.e = e;
            left = right = null;
        }

    }

    private int size;
    private Node root;

}

1.3 添加元素

我們一起來看看在一個樹中插入元素的動畫演示過程,如下圖元素65插入樹結構所示诉植,元素在插入時祥国,不斷跟當前根節(jié)點進行對比,以來選擇插入到左子樹還是右子樹晾腔。

樹結構插入元素

通過動畫舌稀,我們對樹結構插入操作有了一個大致了解。現(xiàn)在我們來一步一步實現(xiàn)添加過程灼擂,將復雜問題簡單化:

1.3.1 第一步

當前狀態(tài):無節(jié)點壁查。

當然是從一棵空樹開始,這個時候來一個元素剔应,這時當然是賦值給root根節(jié)點睡腿。很自然,我們就想到如下代碼:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {

        }
    }

1.3.2 第二步

當前狀態(tài):有節(jié)點峻贮,但節(jié)點左孩子或右孩子為空或左右孩子都為空席怪。

這一步,我們已經(jīng)有了根節(jié)點了纤控,那么將在else中寫邏輯挂捻。由下面動畫我們知道,這時需要判斷兩個條件:

第一船万,插入元素與當前節(jié)點大小的比較刻撒;
第二,程序當前所在節(jié)點下左右節(jié)點是否為空耿导,如果為空声怔,那么就把添加的節(jié)點插入在這個位置上。

二分搜索樹添加元素第二步

接下來舱呻,我們將用遞歸的方式進行元素的添加醋火,我們先解決在某一節(jié)點上添加元素這個子問題。那么下面代碼塊中 add(root, e);這句代碼把根節(jié)點傳入,就是在根節(jié)點上添加元素胎撇,那么代碼如下:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {
            add(root, e);
        }
    }

    // 在node為根節(jié)點的樹中介粘,添加元素e
    private void add(Node node, E e) {
        if (node.e.compareTo(e) == 0) {
            return;
        }

        // 添加到左孩子的位置,
        // 條件:添加的元素小于node節(jié)點的元素晚树,node節(jié)點的元素的左孩子為空
        if (e.compareTo(node.e) < 0 && node.left == null) {
            node.left = new Node(e);
            size++;
            return;
        }

       // 添加到右孩子的位置
        if (e.compareTo(node.e) > 0 && node.right == null) {
            node.right = new Node(e);
            size++;
            return;
        }

    }

1.3.3 第三步

當前狀態(tài):有節(jié)點,左右孩子都不為空雅采。

如果當前節(jié)點左右孩子都不為空爵憎,那么就遞歸的把當前節(jié)點的左孩子或右孩子傳遞進add方法。如下:

    public void add(E e) {
        if (root == null) {
            root = new Node(e);
            size++;
        } else {
            add(root, e);
        }
    }

    private void add(Node node, E e) {
        if (node.e.compareTo(e) == 0) {
            return;
        }

        if (e.compareTo(node.e) < 0 && node.left == null) {
            node.left = new Node(e);
            size++;
            return;
        }

        if (e.compareTo(node.e) > 0 && node.right == null) {
            node.right = new Node(e);
            size++;
            return;
        }

         // 遞歸調用婚瓜,如果當前節(jié)點左右節(jié)點不為空宝鼓,則進入下一輪遞歸的調用。
        if (e.compareTo(node.e) < 0) {
            add(node.left, e);
        } else {
            add(node.right, e);
        }
    }

換一種思考方式

我們如果仔細觀察上面的邏輯就會發(fā)現(xiàn)巴刻,只要添加元素愚铡,那么這個位置肯定是空節(jié)點,用遞歸的話胡陪,我們的循環(huán)終止條件就可以用當前位置是否為空來判斷沥寥,代碼瞬間簡單了許多,如下:


    public void add(E e) {
        root = addAnother(root, e);
    }

    // 返回插入新節(jié)點后柠座,二分搜索樹的根
    private Node addAnother(Node node, E e) {

        if (node == null) {
            size++;
            return new Node(e);
        }

        if (e.compareTo(node.e) < 0) {
            node.left = addAnother(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = addAnother(node.right, e);
        }

        return node;
    }

1.4 樹的遍歷

樹的遍歷分為前序遍歷邑雅,中序遍歷,后序遍歷妈经,層序遍歷淮野。我們依次來講一講。

1.4.1 前序遍歷

前序遍歷吹泡,就是我們在遞歸調用前骤星,先做我們的邏輯(這里的邏輯就是打印一下當前元素),前序遍歷代碼如下:

    private void preOrder(Node node) {
        if (node == null) {
            return;
        }

        System.out.println(node.e);
        preOrder(node.left);
        preOrder(node.right);

    }

我們以一個節(jié)點為例子爆哑,如節(jié)點后還有節(jié)點洞难,遞歸會循環(huán)處理。以下樹結構最終打印的結果就是47泪漂,35廊营,60。

image.png

1.4.2 中序遍歷與后續(xù)遍歷

那么接下來就是中序遍歷萝勤,中序遍歷就是把對元素的操作操作放在了中間露筒,先不斷遞歸左孩子,然后對元素進行操作敌卓,最后遞歸右孩子慎式。所以很容易的推理出來,中序遍歷是對元素從小到大的排序遍歷。這個性質可以作為判斷二分搜索樹的一個條件瘪吏。

同理可以推出還有后續(xù)遍歷癣防,這里就不再贅述了。

1.4.3 層序遍歷

最后我們一起來聊聊層序遍歷掌眠,顧名思義蕾盯,層序遍歷就是對樹結構一層一層的遍歷。要做到這一點蓝丙,我們可以很方便的想到利用隊列來實現(xiàn)這一過程级遭。我們每次從隊列中取出一個元素時,接著就把該元素的左右兩孩子推入隊列中渺尘,然后依次取出元素挫鸽,以此來做到按層把元素遍歷一遍。

一起來看看動畫爸爸給我們的演示鸥跟,最直觀丢郊。

層序遍歷

由動畫爸爸很容易看出,依次打印的順序分別為:47医咨,35枫匾,60,32腋逆,45婿牍,55,65惩歉,完美完成層序遍歷等脂。

然后我來奉上代碼媽媽:

    public void levelOrder() {
        Queue<Node> q = new LinkedList<>();
        // 把根節(jié)點放入隊列
        q.add(root);

        while (!q.isEmpty()) {
            // 移除并返回隊列中的第一個節(jié)點
            Node front = q.remove();
            System.out.print(front.e + "  ");

            // 把移除元素的左右孩子推出隊列
            if (front.left != null) {
                q.add(front.left);
            }

            if (front.right != null) {
                q.add(front.right);
            }
        }
    }

1.5 刪除元素

1.5.1 后繼節(jié)點

接下來,我們聊聊元素的刪除撑蚌。在此之前上遥,我們先得引入一個概念,元素的前驅或者后繼節(jié)點争涌。

后繼節(jié)點:一個節(jié)點右子樹中粉楚,最小的節(jié)點為該節(jié)點的后繼節(jié)點。后繼節(jié)點是比該節(jié)點所有大的元素中最小的元素亮垫。

之所以要引入這個概念模软,原因是在刪除元素時,我們需要找一個元素替代被刪除位置的元素饮潦,但是由于二分搜索樹的特性燃异,不能隨便找元素過來代替,必須得找一個和被刪除元素最接近的元素來替代其位置继蜡。所以找前驅或后繼替代都可以回俐。

比如說逛腿,如下圖,47的后繼節(jié)點就是55仅颇,右子樹中最小的元素单默。

image.png

1.5.2 刪除元素的過程

我們分情況討論,待刪除元素可能有以下三種情形忘瓦。

第一種搁廓,只有左孩子;
第二種政冻,只有右孩子枚抵;
第三種,左右孩子都有明场;(當然還有一種情況就是為葉子節(jié)點,那么直接刪除即可)

刪除元素-左子樹為空

同理很容易就能推出右子樹為空時的刪除過程李丰。

接下來我們來看看左右子樹不為空的情況苦锨。這種情況動畫演示比較簡單,我們看看整體的過程趴泌。

刪除元素-左右子樹不為空

第一步,找到待刪除元素;
第二步碌嘀,找到待刪除元素的后繼節(jié)點便锨;
第三步,把待刪除元素的左子樹賦值給后繼節(jié)點的左子樹吉捶,把待刪除元素的右子樹最小元素刪除后夺鲜,賦值給后繼節(jié)點的右子樹。

整體代碼如下:

   public void remove(E e) {
        root = remove(root, e);
   }

   private Node remove(Node node, E e) {
        if (node == null) {
            return null;
        }

        if (e.compareTo(node.e) < 0) {
            node.left = remove(node.left, e);
            return node;
        } else if (e.compareTo(node.e) > 0) {
            node.right = remove(node.right, e);
            return node;
        } else { // 找到了待刪除元素
            if (node.left == null) {
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }

            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // 后繼節(jié)點
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;
            return successor;

        }

    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末呐舔,一起剝皮案震驚了整個濱河市币励,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌珊拼,老刑警劉巖食呻,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異澎现,居然都是意外死亡仅胞,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門剑辫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來干旧,“玉大人,你說我怎么就攤上這事揭斧±掣铮” “怎么了峻堰?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盅视。 經(jīng)常有香客問我捐名,道長,這世上最難降的妖魔是什么闹击? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任镶蹋,我火速辦了婚禮,結果婚禮上赏半,老公的妹妹穿的比我還像新娘贺归。我一直安慰自己,他們只是感情好断箫,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布拂酣。 她就那樣靜靜地躺著,像睡著了一般仲义。 火紅的嫁衣襯著肌膚如雪婶熬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天埃撵,我揣著相機與錄音赵颅,去河邊找鬼。 笑死暂刘,一個胖子當著我的面吹牛饺谬,可吹牛的內容都是我干的。 我是一名探鬼主播谣拣,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼募寨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芝发?” 一聲冷哼從身側響起绪商,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辅鲸,沒想到半個月后格郁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡独悴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年例书,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刻炒。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡决采,死狀恐怖,靈堂內的尸體忽然破棺而出坟奥,到底是詐尸還是另有隱情树瞭,我是刑警寧澤拇厢,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站晒喷,受9級特大地震影響孝偎,放射性物質發(fā)生泄漏。R本人自食惡果不足惜凉敲,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一衣盾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爷抓,春花似錦势决、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渤昌,卻和暖如春据悔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背耘沼。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留朱盐,地道東北人群嗤。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像兵琳,于是被迫代替她去往敵國和親狂秘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容