二叉樹-你可能需要知道這些

一直以來,我都很少使用也避免使用到樹和圖,總覺得它們神秘而又復(fù)雜,但是樹在一些運(yùn)算和查找中也不可避免的要使用到,那么今天我們就鼓起勇氣來學(xué)習(xí)下樹,爭取在被問到和使用時(shí)不再那么慫。

1. 什么是二叉樹?

二叉樹也是一棵樹(這不是廢話么,雖說是廢話,但這是事實(shí)),但它的每個(gè)節(jié)點(diǎn)最多只有2個(gè)兒子(這個(gè)才是重點(diǎn))。

1.1 如何實(shí)現(xiàn)

在了解了二叉樹的概念后,我們?nèi)绾味x一顆樹的節(jié)點(diǎn)?so easy.

// 節(jié)點(diǎn)
public class BinaryNode {
    // 存放的信息
    Object data;
    // 左兒子
    BinaryNode left;
    // 右兒子
    BinaryNode right;
}

1.2 三種遍歷算法

構(gòu)造完樹之后,不可避免的就是讀取(或者叫做遍歷,不然存儲(chǔ)成這樣干哈啊)。
遍歷是對(duì)樹的一種最基本的運(yùn)算,所謂遍歷二叉樹,就是按一定的規(guī)則和順序走遍二叉樹的所有結(jié)點(diǎn),使每一個(gè)結(jié)點(diǎn)都被訪問一次,而且只被訪問一次。
由于二叉樹是非線性結(jié)構(gòu),因此,樹的遍歷實(shí)質(zhì)上是將二叉樹的各個(gè)結(jié)點(diǎn)轉(zhuǎn)換成為一個(gè)線性序列來表示。

對(duì)于樹的遍歷,按照訪問根節(jié)點(diǎn)的次序不同,主要有以下三種遍歷算法:

  1. 先序遍歷
  2. 后序遍歷
  3. 中序遍歷

除了上面三種遍歷,我們還會(huì)帶大家學(xué)習(xí)下深度優(yōu)先遍歷和廣度優(yōu)先遍歷。

我們首先給出一個(gè)假設(shè):
L:左子樹
D:根
R:右子樹

1.2.1 先序遍歷(DLR)

先序遍歷:根節(jié)點(diǎn)->左子樹->右子樹

算法的簡單實(shí)現(xiàn)如下:

    public static void DLR(BinaryNode node) {
        // 訪問根節(jié)點(diǎn)
        System.out.print(node.data + " ");
        // 遍歷左子樹
        if (node.left != null) {
            DLR(node.left);
        }
        // 遍歷右子樹
        if (node.right != null) {
            DLR(node.right);
        }
    }

1.2.2 后序遍歷(LRD)

后序遍歷:左子樹->右子樹->根節(jié)點(diǎn)

    public static void LRD(BinaryNode node) {
        // 遍歷左子樹
        if (node.left != null) {
            LRD(node.left);
        }
        // 遍歷右子樹
        if (node.right != null) {
            LRD(node.right);
        }
        // 訪問根節(jié)點(diǎn)    
        System.out.print(node.data + " ");
    }

1.2.3 中序遍歷(LDR)

中序遍歷:左子樹->根節(jié)點(diǎn)->右子樹

public static void LDR(BinaryNode node) {
        // 遍歷左子樹
        if (node.left != null) {
            LDR(node.left);
        }
        // 訪問根節(jié)點(diǎn)
        System.out.print(node.data + "");

        // 遍歷右子樹
        if (node.right != null) {
            LDR(node.right);
        }

    }

1.2.4 深度優(yōu)先遍歷

英文縮寫為DFS即Depth First Search.其過程簡要來說是對(duì)每一個(gè)可能的分支路徑深入到不能再深入為止,而且每個(gè)節(jié)點(diǎn)只能訪問一次。

深度優(yōu)先遍歷需要使用到棧這種數(shù)據(jù)結(jié)構(gòu),棧具有先進(jìn)后出的特點(diǎn)。

二叉樹

如上圖,我們來分析下深度優(yōu)先遍歷的過程。

  1. 首先根節(jié)點(diǎn)A入棧,stack(A)。
  2. 將A節(jié)點(diǎn)彈出,因?yàn)锳存在 B C兩個(gè)子節(jié)點(diǎn),根據(jù)定義和棧特點(diǎn),首先將C(右兒子)壓入棧中,然后將B(左兒子)壓入棧中,stack(C B)
  3. 彈出棧頂元素B節(jié)點(diǎn)彈出,將節(jié)點(diǎn) E 和 D壓入棧中,stack(C E D)。
  4. 彈出棧頂元素D,由于節(jié)點(diǎn)D只存在一個(gè)子節(jié)點(diǎn)H,因此H直接入棧,stack(C E H).
  5. 彈出棧頂元素H,元素H不存在子元素,stack(C E).
  6. 彈出棧頂元素E,元素E不存在子元素,stack(C).
  7. 彈出棧頂元素C,子節(jié)點(diǎn)G F分別入棧,stack(G F).
  8. F出棧,stack(G)。
  9. G出棧,stack()。
  10. 遍歷結(jié)束。

深度優(yōu)先遍歷的結(jié)果為: A B D H E C F G.

通過上面的分析,是不是覺得深度優(yōu)先遍歷其實(shí)也沒那么難啊,下面我們來動(dòng)手實(shí)現(xiàn)這段代碼(過程理解清楚了,代碼實(shí)現(xiàn)起來很簡單)。

private void depthFirst(AVLTreeNode<T> node) {
        if (node == null) {
            return;
        }

        Stack<AVLTreeNode> stack = new Stack<>();
        // 根節(jié)點(diǎn)入棧,然后進(jìn)行后續(xù)操作
        stack.push(node);

        while (!stack.isEmpty()) {
            AVLTreeNode root = stack.pop();
            // 彈出棧頂元素,進(jìn)行訪問。
            System.out.println(root.key + " ");
            // 首先將右節(jié)點(diǎn)入棧
            if (root.right != null) {
                stack.push(node.right);
            }
            // 然后左節(jié)點(diǎn)入棧
            if (root.left != null) {
                stack.push(node.left);
            }
        }

    }

1.2.5 廣度優(yōu)先遍歷

英文縮寫為BFS即Breadth FirstSearch。其過程檢驗(yàn)來說是對(duì)每一層節(jié)點(diǎn)依次訪問,訪問完一層進(jìn)入下一層,而且每個(gè)節(jié)點(diǎn)只能訪問一次。對(duì)于上面的例子來說,廣度優(yōu)先遍歷的 結(jié)果是:A,B,C,D,E,F,G,H(假設(shè)每層節(jié)點(diǎn)從左到右訪問)。

廣度優(yōu)先遍歷需要使用到隊(duì)列這種數(shù)據(jù)結(jié)構(gòu),隊(duì)列具有先進(jìn)先出的特點(diǎn)。

bfs.png

如上圖所示,我們來分析廣度優(yōu)先遍歷的過程。

  1. 首先將A節(jié)點(diǎn)插入隊(duì)列中,queue(A);
  2. 將A節(jié)點(diǎn)彈出,同時(shí)將A的子節(jié)點(diǎn)B,C插入隊(duì)列中,此時(shí)B在隊(duì)列首,C在隊(duì)列尾部,queue(B,C);
  3. 將B節(jié)點(diǎn)彈出,同時(shí)將B的子節(jié)點(diǎn)D,E插入隊(duì)列中,此時(shí)C在隊(duì)列首,E在隊(duì)列尾部,queue(C,D,E);
  4. 將C節(jié)點(diǎn)彈出,同時(shí)將C的子節(jié)點(diǎn)F,G插入隊(duì)列中,此時(shí)D在隊(duì)列首,G在隊(duì)列尾部,queue(D,E,F(xiàn),G);
  5. 將D節(jié)點(diǎn)彈出,同時(shí)將D節(jié)點(diǎn)的子節(jié)點(diǎn)H插入隊(duì)列中,此時(shí)E在隊(duì)列首,H在隊(duì)列尾部,queue(E,F(xiàn),G,H);
  6. E F G H分別彈出(這四個(gè)節(jié)點(diǎn)均不存在子節(jié)點(diǎn))。

廣度優(yōu)先遍歷結(jié)果為:A B C D E F G H

動(dòng)手來實(shí)現(xiàn)廣度優(yōu)先遍歷,代碼如下:

public void breadthFirst() {
        breadthFirst(root);
    }

    private void breadthFirst(AVLTreeNode<T> node) {
        if (node == null) {
            return;
        }

        Queue<AVLTreeNode> queue = new ArrayDeque<>();
        // 根節(jié)點(diǎn)入棧
        queue.add(node);

        while (!queue.isEmpty()) {
            AVLTreeNode root = queue.poll();
            System.out.print(node.key + " ");

            if (root.left != null) {
                queue.add(node.left);
            }

            if (root.right != null) {
                queue.add(node.right);
            }

        }


    }

1.3 示例

光說不練假把式,本章節(jié)我們來舉個(gè)簡單的例子來實(shí)戰(zhàn)下。

1.3.1 構(gòu)造一棵樹

表達(dá)式二叉樹

以上圖表達(dá)式二叉樹為例,我們來看下1.2章節(jié)三個(gè)遍歷算法的輸出結(jié)果:
先序遍歷:+ + a * b c * + * d e f g
中序遍歷:a + b * c + d * e + f * g
后序遍歷:a b c * + d e * f + g * +

1.3.2 根據(jù)表達(dá)式構(gòu)造樹

現(xiàn)在假如我們拿到了一個(gè)后綴表達(dá)式的輸入:a b c * + d e * f + g * + ,那么如何根據(jù)它來構(gòu)造樹呢?
我們前面已經(jīng)分析了后序遍歷算法(LRD),其實(shí)這就是一個(gè)逆序的過程。

我們一次一個(gè)符號(hào)讀入表達(dá)式,如果符號(hào)為操作數(shù),那么我們就新建一個(gè)節(jié)點(diǎn)并入棧;如果符號(hào)為操作符,我們就從棧中彈出兩個(gè)操作數(shù)T1和T2(T1先彈出)并形成一棵新的樹,該樹的根就是操作符,它的左右子樹分別為T2和T1,然后將該樹壓入棧中。

現(xiàn)在輸入為:a b + c d e + * *
很明顯前兩個(gè)為操作數(shù),因此創(chuàng)建兩個(gè)節(jié)點(diǎn)并將它們壓入棧中。

接著'+'被讀取,因此兩棵樹被彈出,一顆新的樹形成,并被壓入棧中。

然后,c、d、e分別被讀入,在每個(gè)節(jié)點(diǎn)被創(chuàng)建后,被壓入棧中。

然后'+'被讀入,因此我們彈出兩個(gè)節(jié)點(diǎn)形成一棵新的樹并壓入棧中。

繼續(xù)讀取,‘*’被讀入,我們彈出兩顆樹形成一棵新的樹并壓入棧中。

最后,讀入最后一個(gè)符號(hào),兩顆樹合并,最后的樹留在棧中。

下面,我們將上面的分析過程轉(zhuǎn)換為代碼:

    public static BinaryNode buildTreeFromLRD(String content) {
        Stack<BinaryNode> stack = new Stack();
        char[] chars = content.toCharArray();
        for (int index = 0; index < chars.length; index++) {
            char data = chars[index];
            if (data >= 'a' && data <= 'z') {
                BinaryNode numNode = new BinaryNode();
                numNode.data = String.valueOf(data);
                stack.push(numNode);
            } else {
                BinaryNode root = new BinaryNode();
                root.data = String.valueOf(data);
                root.right = stack.pop();
                root.left = stack.pop();
                stack.push(root);
            }
        }

        return stack.pop();
    }

1.3.3 求表達(dá)式的值

求表達(dá)式的值是我們學(xué)習(xí)中或者面試中一個(gè)不可避免的知識(shí)點(diǎn),本章節(jié)就結(jié)合二叉樹的遍歷,教你如何輕松實(shí)現(xiàn)表達(dá)式求值。

我們的輸入一般為中綴表達(dá)式(我們二叉樹中序遍歷的結(jié)果),由于表達(dá)式可能存在(),因此可能會(huì)出現(xiàn)優(yōu)先級(jí)的問題。而如果將其轉(zhuǎn)換為后綴表達(dá)式,則無需考慮運(yùn)算符優(yōu)先級(jí)的問題。

因此本章節(jié)講解兩個(gè)部分的內(nèi)容:

  1. 將包含()的中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式
  2. 求解后綴表達(dá)的值

1.3.3.1 中綴->后綴

中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式時(shí),主要使用棧這種結(jié)構(gòu),并根據(jù)一定的規(guī)則進(jìn)行處理。
中綴表達(dá)式示例:a + b * c + ( d * e + f ) * g
期望的后綴表達(dá)式:a b c * + d e * f + g * +
我們使用op棧來存儲(chǔ)操作符,使用re棧來存儲(chǔ)操作數(shù)。


中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式最為重要的就遇到一個(gè)操作數(shù)或者運(yùn)算符的時(shí)候應(yīng)該怎么處理, 也就是優(yōu)先級(jí)的確認(rèn)。

  1. 遇到操作數(shù)的時(shí)候直接推進(jìn)re棧。

  2. 遇到運(yùn)算符的時(shí)候,需要比較該運(yùn)算符與op棧頭元素的運(yùn)算符的優(yōu)先級(jí),如果該運(yùn)算符優(yōu)先級(jí)高于棧頂運(yùn)算符(不包括等于)則將該運(yùn)算符推進(jìn)op, 否則彈出op棧的頭元素直到頭元素運(yùn)算符的優(yōu)先級(jí)低于該運(yùn)算符,最后將該運(yùn)算符推進(jìn)op中。

  3. 當(dāng)然有特殊情況,也就是遇到左括號(hào)“(”的時(shí)候直接推進(jìn)op, 當(dāng)其他運(yùn)算符與左括號(hào)比較優(yōu)先級(jí)的時(shí)候可認(rèn)為左括號(hào)的優(yōu)先級(jí)最低。遇到右括號(hào)的時(shí)候,不斷彈出棧的頭元素直到遇到左括號(hào),左括號(hào)彈出后不入re棧(后綴表達(dá)式不需要括號(hào))。

  4. 接下來我們詳細(xì)討論每種情況:
    “+”:優(yōu)先級(jí)是最低的,幾乎所有運(yùn)算符都要彈出(包括加號(hào)自身),除了左括號(hào)。
    “-”:與加號(hào)優(yōu)先級(jí)相同,同上。
    “*”:優(yōu)先級(jí)稍高,只有遇到乘號(hào),除號(hào),以及冪號(hào)的時(shí)候才需要彈出。
    “/”:與乘號(hào)優(yōu)先級(jí)相同,同上。
    “^”:優(yōu)先級(jí)最高,只有遇到它自身的時(shí)候才需要彈出。


  1. 首先符號(hào)a被讀入,它被存入re棧;然后‘+’被讀入并被放入op棧中;接下來b讀入并流向re棧。
    op棧:+
    re棧:a b

  2. ‘ * ’被讀入,因?yàn)閛p棧棧頂元素[+]優(yōu)先級(jí)比[ * ]低,因此re棧無輸出,且*入op棧。
    op棧:+ *
    re棧:a b c

  3. ‘+’被讀入。因?yàn)閇+]優(yōu)先級(jí)低于op棧棧頂元素[ * ],因此將[ * ]號(hào)從op棧彈出并輸出到re棧;現(xiàn)在op棧剩下了[+],由于讀入的[+]不比op棧中的[+]優(yōu)先級(jí)低而是具備相同的優(yōu)先級(jí),因此彈出op棧中的[+]并輸出到re棧,并將讀取到的[+]輸入到op棧。
    op棧:+
    re棧:a b c * +

  4. ‘(’被讀入。直接輸入到op棧中。
    op棧:+ (
    re棧:a b c * +

  5. d被讀入。直接將d輸出到re棧中。
    op棧:+ (
    re棧:a b c * + d

  6. ‘*’被讀入。由于除非在處理閉括號(hào)否則開括號(hào)不會(huì)從棧中彈出,因此[ * ]直接輸出到op棧。
    op棧:+ ( *
    re棧:a b c * + d

  7. ‘+’被讀入。比較優(yōu)先級(jí)'+'<' * ',‘ * ’從op棧彈出輸出至re棧,‘+’棧輸出至op棧。
    op棧:+ ( +
    re棧:a b c * + d *

  8. f被讀入。直接將f輸出到re棧中。
    op棧:+ ( +
    re棧:a b c * + d * f

  9. ‘)’被讀入。遇到了),因此將op棧中元素彈出,直到最近的(彈出。
    op棧:+
    re棧:a b c * + d * f +

  10. ' * '被讀入。' * '>'+',*號(hào)直接入op棧。
    op棧:+ *
    re棧:a b c * + d * f +

  11. g被讀入。直接將g輸出到re棧。
    op棧:+ *
    re棧:a b c * + d * f + g

  12. 表達(dá)式已經(jīng)讀取完畢,將op棧中的元素出棧,并輸出到re棧中。
    op棧:
    re棧:a b c * + d * f + g * +

過程已經(jīng)清楚了,那么下面讓我們動(dòng)手來實(shí)現(xiàn)這個(gè)轉(zhuǎn)換過程。(只處理了簡單的加減乘除和()的情況)

   /**
     * 將中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式
     *
     * @param midStat 中綴表達(dá)式
     * @return 轉(zhuǎn)換后的后綴表達(dá)式
     */
    public static String convertMid2Suffix(String midStat) {
        Stack<Character> op = new Stack<>();
        Stack<Character> re = new Stack<>();
        char[] chars = midStat.toCharArray();
        for (int index = 0; index < chars.length; index++) {
            char data = chars[index];
            if (data >= 'a' && data <= 'z' || data >= '0' && data <= '9') {
                // 操作數(shù)直接進(jìn)入re棧
                re.push(data);
            } else {
                // 棧為空時(shí),直接入棧
                if (op.size() == 0) {
                    op.push(data);
                    continue;
                }


                // 處理運(yùn)算符優(yōu)先級(jí)
                while (op.size() > 0) {
                    // peek方法只讀取,不出棧
                    Character exist = op.peek();

                    // 處理特殊情況,讀入的為左括號(hào)時(shí),直接入op棧
                    if (data == '(') {
                        op.push(data);
                        break;
                    }

                    // 讀入為右括號(hào)時(shí),op出棧輸出到re棧,直到最近的左括號(hào)彈出為止
                    if (data == ')') {
                        if (exist == '(') {
                            op.pop();
                            break;
                        }

                        re.push(exist);
                        op.pop();
                        continue;
                    }

                    if (exist == '(') {
                        op.push(data);
                        break;
                    }


                    int compareResult = compareOp(data, exist);
                    // 當(dāng)前讀入的運(yùn)算符大于op棧棧頂?shù)倪\(yùn)算符,直接輸入到op棧
                    if (compareResult > 0) {
                        op.push(data);
                        break;
                    }

                    // 讀入運(yùn)算符與op棧棧頂運(yùn)算符優(yōu)先級(jí)相同
                    if (compareResult == 0) {
                        // op棧棧頂運(yùn)算符彈出,并輸出至re棧中
                        re.push(exist);
                        op.pop();
                        if (op.size() == 0) {
                            op.push(data);
                            break;
                        }
                    }

                    if (compareResult < 0) {
                        // op棧棧頂運(yùn)算符彈出,并輸出至re棧中
                        re.push(exist);
                        op.pop();
                        if (op.size() == 0) {
                            op.push(data);
                            break;
                        }
                    }

                }

            }
        }

        while(op.size()>0){
            re.push(op.pop());
        }

        StringBuilder result = new StringBuilder();
        while (re.size() > 0) {
            result.append(re.pop());
        }

        return result.reverse().toString();

    }
    
    public static int compareOp(Character left, Character right) {

        String pmOp = "-+";
        String mdOp = "*/";

        if (left == right) {
            return 0;
        }


        if (pmOp.indexOf(left) >= 0 && pmOp.indexOf(right) >= 0) {
            return 0;
        }

        if (mdOp.indexOf(left) >= 0 && mdOp.indexOf(right) >= 0) {
            return 0;
        }

        if (pmOp.indexOf(left) >= 0 && mdOp.indexOf(right) >= 0) {
            return -1;
        }

        if (mdOp.indexOf(left) >= 0 && pmOp.indexOf(right) >= 0) {
            return 1;
        }

        return 0;
    }

1.3.3.2 后綴表達(dá)式求值

在上個(gè)章節(jié),我們詳細(xì)分析了如何將我們?nèi)粘R姷乃阈g(shù)表達(dá)式轉(zhuǎn)換為后綴表達(dá)式,接下來我們來分析下如何求解后綴表達(dá)式的值。

后綴表達(dá)式:6 5 2 3 + 8 * + 3 + *
中綴表達(dá)式為:6 * ((5+(2+3) * 8)+3)

照例我們來分析下求解的過程:

  1. 6523 入棧
    re棧:6 5 2 3

  2. +讀入。此時(shí)2 3出棧,進(jìn)行+運(yùn)算,并將結(jié)果入棧。
    re棧:6 5 5

  3. 8讀入。
    re棧:6 5 5 8

  4. *讀入。此時(shí)5 8出棧,進(jìn)行 * 運(yùn)算后將結(jié)果入棧。
    re棧:6 5 40

  5. +讀入。此時(shí)5 40出棧,進(jìn)行+運(yùn)算后入棧。
    re棧:6 45

  6. 3讀入。
    re棧:6 45 3

  7. +讀入。此時(shí)45 3出棧,進(jìn)行+運(yùn)算后入棧。
    re棧:6 48

  8. '*' 入棧。此時(shí)6 48 出棧,進(jìn)行 * 運(yùn)算后結(jié)果入棧。
    re棧:288

  9. 288出棧,運(yùn)算結(jié)束。

讓我們動(dòng)手來實(shí)現(xiàn)這段代碼吧。

public static Integer computeSuffixStat(String statement) {
        Stack<Integer> re = new Stack<>();

        char[] chars = statement.toCharArray();
        for (int index = 0; index < chars.length; index++) {
            char charData = chars[index];
            if (charData >= '0' && charData <= '9') {
                Integer data = Integer.parseInt(String.valueOf(charData));
                re.push(data);
            }


            if (charData == '+') {
                Integer result = re.pop() + re.pop();
                re.push(result);
            }

            if (charData == '-') {
                Integer aft = re.pop();
                Integer pre = re.pop();

                Integer result = pre - aft;
                re.push(result);
            }

            if (charData == '*') {
                Integer result = re.pop() * re.pop();
                re.push(result);
            }

            if (charData == '/') {
                Integer aft = re.pop();
                Integer pre = re.pop();
                Integer result = pre / aft;
                re.push(result);

            }
        }
        System.out.println(re.pop());
        return re.pop();
    }

2.2 完全二叉樹

定義:若設(shè)二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結(jié)點(diǎn)數(shù)都達(dá)到最大個(gè)數(shù),第 h 層所有的結(jié)點(diǎn)都連續(xù)集中在最左邊,這就是完全二叉樹。

完全二叉樹有如下特點(diǎn):

  1. 只允許最后一層有空缺結(jié)點(diǎn)且空缺在右邊,即葉子結(jié)點(diǎn)只能在層次最大的兩層上出現(xiàn)。
  2. 對(duì)任一結(jié)點(diǎn),如果其右子樹的深度為j,則其左子樹的深度必為j或j+1。 即度為1的點(diǎn)只有1個(gè)或0個(gè)。

2.2.1 分析

出于簡便起見,完全二叉樹通常采用數(shù)組而不是鏈表存儲(chǔ),其存儲(chǔ)結(jié)構(gòu)如下:
var tree:array[1..n]of longint;{n:integer;n>=1}
對(duì)于tree[i],有如下特點(diǎn):
(1)若i為奇數(shù)且i>1,那么tree的左兄弟為tree[i-1];
(2)若i為偶數(shù)且i<n,那么tree的右兄弟為tree[i+1];
(3)若i>1,tree的父親節(jié)點(diǎn)為tree[i div 2];
(4)若2i<=n,那么tree的左孩子為tree[2i];若2i+1<=n,那么tree的右孩子為tree[2i+1];
(5)若i>n div 2,那么tree[i]為葉子結(jié)點(diǎn)(對(duì)應(yīng)于(3));
(6)若i<(n-1) div 2.那么tree[i]必有兩個(gè)孩子(對(duì)應(yīng)于(4))。
(7)滿二叉樹一定是完全二叉樹,完全二叉樹不一定是滿二叉樹。
完全二叉樹第i層至多有2(i-1)個(gè)節(jié)點(diǎn),共i層的完全二叉樹最多有2i-1個(gè)節(jié)點(diǎn)。

2.2.2 滿二叉樹

除最后一層無任何子節(jié)點(diǎn)外,每一層上的所有結(jié)點(diǎn)都有兩個(gè)子結(jié)點(diǎn)二叉樹。
國內(nèi)教程定義:一個(gè)二叉樹,如果每一個(gè)層的結(jié)點(diǎn)數(shù)都達(dá)到最大值,則這個(gè)二叉樹就是滿二叉樹。也就是說,如果一個(gè)二叉樹的層數(shù)為K,且結(jié)點(diǎn)總數(shù)是(2^k) -1 ,則它就是滿二叉樹。

滿二叉樹是一種特俗的完全二叉樹。

2.3 ADT-二叉查找樹

二叉樹的一個(gè)重要的應(yīng)用就是它們在查找中的使用。假設(shè)每個(gè)節(jié)點(diǎn)存儲(chǔ)一項(xiàng)數(shù)據(jù)。

使二叉樹成為二叉查找樹的性質(zhì)是:對(duì)于二叉樹中的每個(gè)節(jié)點(diǎn)X,它的左子樹中所有項(xiàng)的值都小于X中的項(xiàng),它的右子樹中所有項(xiàng)的值大于X中的項(xiàng)。注意,這意味著該樹所有的元素可以使用某種一致的方式排序。

二叉查找樹的平均深度為O(logN),所以一般不必?fù)?dān)心棧空間被用盡,我們一般使用遞歸來處理查找、刪除等操作。

二叉查找樹要求所有項(xiàng)均能排序,這就要求我們實(shí)現(xiàn)Comparable接口。該接口告訴我們,樹中任意兩項(xiàng)都可以使用compareTo方法進(jìn)行比較。

2.3.1 二叉查找樹定義

根據(jù)二叉查找樹的要求,我們首先讓BinaryNode實(shí)現(xiàn)Comparable接口。

// 樹節(jié)點(diǎn)
class BinaryNode implements Comparable {

    Integer element;

    BinaryNode left;

    BinaryNode right;

    public BinaryNode{

    }

    public BinaryNode(Integer element, BinaryNode left, BinaryNode right) {
        this.element = element;
        this.left = left;
        this.right = right;
    }

    @Override
    public int compareTo(@NonNull Object o) {
        return this.element - (Integer) o;
    }
}

下面我們完成對(duì)BinarySearchTree的定義(只定義關(guān)鍵輪廓,具體接口后續(xù)進(jìn)行分析)。

public class BinarySearchTree {

    //定義樹的根節(jié)點(diǎn)
    BinaryNode root;

    public BinarySearchTree() {
        this.root = null;
    }

    public void makeEmpty() {
        this.root = null;
    }

    public boolean isEmpty() {
        return this.root == null;
    }

    // 判斷是否包含某個(gè)元素
    public boolean contains(Integer x) {
        //TODO:后續(xù)講解
        return false;
    }

    // 查找最小值
    public BinaryNode findMin(){
        //TODO:后續(xù)講解
        return null;
    }

    // 查找最大值
    public BinaryNode findMax(){
        //TODO:后續(xù)講解
        return null;
    }

    // 按照順序插入值
    public void insert(Integer x){
        //TODO:后續(xù)講解
    }

    // 刪除某個(gè)值
    public void remove(Integer x){
        //TODO:后續(xù)講解
    }
    
    // 打印樹
    public void printTree(){
        //TODO:后續(xù)講解
    }
}

2.3.2 contains-查找

如果在樹T中包含項(xiàng)X的節(jié)點(diǎn),那么該操作返回true,否則返回false。

樹的結(jié)構(gòu)使得這種操作變得非常簡單,如果T為空集,直接返回false;否則我們就對(duì)T的左子樹或右子樹進(jìn)行遞歸查找,直到找到該項(xiàng)X。

代碼實(shí)現(xiàn)如下:

/**
     * 是否包含某個(gè)元素
     * @param x 待查找對(duì)象
     * @return 查找結(jié)果
     */
    public boolean contains(Integer x) {
        // 首次查找從根節(jié)點(diǎn)開始
        return contains(x,root);
    }

    private boolean contains(Integer x, BinaryNode node) {
        // 根節(jié)點(diǎn)為空的情況,不需要再查找
        if (node == null) {
            return false;
        }
        
        // 與當(dāng)前節(jié)點(diǎn)進(jìn)行比較
        int compareResult = x.compareTo(node.element);

        // 小于當(dāng)前節(jié)點(diǎn)的值,就遞歸遍歷左子樹
        if (compareResult < 0) {
            return contains(x, node.left);
        } 
        // 大于當(dāng)前節(jié)點(diǎn)的值,就遞歸遍歷右子樹
        else if (compareResult > 0) {
            return contains(x, node.right);
        } 
        // 等于當(dāng)前節(jié)點(diǎn)值,直接返回
        else {
            return true;
        }
    }

2.3.3 查找最小值節(jié)點(diǎn)

從根開始并且只要有左子樹就向左進(jìn)行,終止點(diǎn)就是最小元素節(jié)點(diǎn)。

    // 查找最小值
    public BinaryNode findMin() {
        return findMin(root);
    }

    private BinaryNode findMin(BinaryNode node) {
        // 當(dāng)前節(jié)點(diǎn)為null,直接返回null
        if (node == null) {
            return null;
        }

        // 不存在左子樹,返回當(dāng)前節(jié)點(diǎn)
        if (node.left == null) {
            return node;
        }

        // 遞歸遍歷左子樹
        return findMin(node.left);

    }

2.3.4 查找最大值節(jié)點(diǎn)

從根開始并且只要有右子樹就向右進(jìn)行,終止點(diǎn)就是最大元素節(jié)點(diǎn)。作為與findMin的對(duì)比,findMax方法我們拋棄了常用的遞歸,使用了常見的while循環(huán)來查找。

    // 查找最大值
    public BinaryNode findMax() {
        return findMax(root);
    }

    private BinaryNode findMax(BinaryNode node) {
        if (node != null) {
            while (node.right != null) {
                node = node.right;
            }
        }
        return node;
    }

2.3.5 insert-插入

插入操作在概念上是簡單的,為了將X插入樹T中,我們像contains一樣沿著樹查找。
如果找到X,那么我們可以什么都不做,也可以做一些更新操作。
否則,就將X插入到遍歷路徑上的最后一個(gè)節(jié)點(diǎn)。

代碼實(shí)現(xiàn)如下:

public void insert(Integer x) {
        root = insert(x, root);
    }

    // 返回的插入節(jié)點(diǎn)的根節(jié)點(diǎn)
    private BinaryNode insert(Integer x, BinaryNode node) {
        // 如果當(dāng)前節(jié)點(diǎn)為null,新建節(jié)點(diǎn)返回
        if (node == null) {
            return new BinaryNode(x, null, null);
        }
        // 與當(dāng)前節(jié)點(diǎn)比較
        int compareResult = x.compareTo(node.element);

        // 小于當(dāng)前節(jié)點(diǎn)值,遞歸插入左子樹,并將返回值設(shè)置為當(dāng)前節(jié)點(diǎn)的left
        if (compareResult < 0) {
            node.left = insert(x, node.left);
        }


         // 大于當(dāng)前節(jié)點(diǎn)值,遞歸插入右子樹,并將返回值設(shè)置為當(dāng)前節(jié)點(diǎn)的right
        if (compareResult > 0) {
            node.right = insert(x, node.right);
        }

        // 等于當(dāng)前的值,不做任何處理
        if (compareResult == 0) {
            // do some update or do noting
        }
        
        return node;

    }

2.3.6 remove-刪除

正如許多數(shù)據(jù)結(jié)構(gòu)一樣,最難的操作是remove,一旦我們發(fā)現(xiàn)了要?jiǎng)h除的元素,就要考慮幾種可能的情況:

  1. 當(dāng)待刪除的節(jié)點(diǎn)為葉子節(jié)點(diǎn)時(shí),直接刪除。
  2. 當(dāng)待刪除節(jié)點(diǎn)只有一個(gè)兒子節(jié)點(diǎn)時(shí),把兒子節(jié)點(diǎn)代替該節(jié)點(diǎn)的位置,然后刪除該節(jié)點(diǎn)。
  3. 當(dāng)待刪除的節(jié)點(diǎn)有兩個(gè)兒子節(jié)點(diǎn)時(shí),一般的刪除策略是用其右子樹的最小的數(shù)據(jù)代替該節(jié)點(diǎn)的數(shù)據(jù)并遞歸刪除那個(gè)節(jié)點(diǎn)(現(xiàn)在它是空的);因?yàn)橛易訕涞淖钚」?jié)點(diǎn)不可能有左兒子,所以第二次Delete要容易。
    // 刪除某個(gè)值
    public void remove(Integer x) {
        remove(x, root);
    }

    private BinaryNode remove(Integer x, BinaryNode node) {
        if (node == null) {
            return null;
        }

        int compareResult = x.compareTo(node.element);

        if (compareResult < 0) {
            node.left = remove(x, node.left);
        }

        if (compareResult > 0) {
            node.right = remove(x, node.right);
        }

        if (compareResult == 0) {

            if (node.left != null && node.right != null) {

                node.element = findMin(node.right).element;

                node.right = remove(node.element, node.right);

            } else {

                node = (node.left != null) ? node.left : node.right;

            }

        }


        return node;

    }

2.4 AVL樹

AVL樹是高度平衡的而二叉樹。它的特點(diǎn)是:AVL樹中任何節(jié)點(diǎn)的兩個(gè)子樹的高度最大差別為1。

先來明確幾個(gè)概念:

平衡因子:將二叉樹上節(jié)點(diǎn)的左子樹高度減去右子樹高度的值稱為該節(jié)點(diǎn)的平衡因子BF(Balance Factor)。

最小不平衡子樹:距離插入節(jié)點(diǎn)最近的,且平衡因子的絕對(duì)值大于1的節(jié)點(diǎn)為根的子樹。

最小平衡樹

左邊二叉樹的節(jié)點(diǎn)45的BF = 1,插入節(jié)點(diǎn)43后,節(jié)點(diǎn)45的BF=2。
節(jié)點(diǎn)45是距離插入點(diǎn)43最近的BF不在[-1,1]范圍內(nèi)的節(jié)點(diǎn),因此以節(jié)點(diǎn)45為根的子樹為最小不平衡子樹。

2.4.1 AVL樹節(jié)點(diǎn)實(shí)現(xiàn)

public class AVLTreeNode<T extends Comparable> {

    // 存儲(chǔ)的數(shù)據(jù)-用于排序
    T key;

    // 節(jié)點(diǎn)高度-用于計(jì)算父節(jié)點(diǎn)的BF
    int height;

    // 左兒子 & 右兒子
    AVLTreeNode<T> left;
    AVLTreeNode<T> right;
    
    public AVLTreeNode() {
    }

    public AVLTreeNode(T key, AVLTreeNode<T> left, AVLTreeNode<T> right) {
        this.key = key;
        this.left = left;
        this.right = right;
        this.height = 0;
    }

}

節(jié)點(diǎn)的定義還是比較簡單的,相對(duì)于之前的定義多了一個(gè)height屬性用于計(jì)算父節(jié)點(diǎn)的BF。

2.4.2 AVL樹定義

public class AVLTree<T extends Comparable> {
    // 定義樹的根節(jié)點(diǎn)
    AVLTreeNode<T> root;

    public AVLTree() {
        root = null;
    }
}

我們之定義了一個(gè)根節(jié)點(diǎn),后續(xù)分析逐漸豐富其該有的操作。

2.4.3 獲取樹的高度

    public int height() {
        return height(root);
    }

    private int height(AVLTreeNode<T> tree) {
        if (tree != null){
            return tree.height;
        }
        return 0;
    }

本文章將空樹的高度定義為0,高度以樹的層次為準(zhǔn),根節(jié)點(diǎn)的高度為1,依次類推。

2.4.4 AVL失衡調(diào)整

如果在AVL樹中進(jìn)行插入或刪除節(jié)點(diǎn)后,可能導(dǎo)致AVL樹失去平衡。這種不平衡可能出現(xiàn)在下面四種情況中:

  1. 對(duì)a的左兒子的左子樹進(jìn)行一次插入。(LL)
  2. 對(duì)a的左兒子的右子樹進(jìn)行一次插入。(LR)
  3. 對(duì)a的右兒子的左子樹進(jìn)行一次插入。(RL)
  4. 對(duì)a的右兒子的右子樹進(jìn)行一次插入。(RR)

其中1、4是關(guān)于a點(diǎn)的鏡像對(duì)稱,2、3是關(guān)于a點(diǎn)的鏡像對(duì)稱。

第一種情況(1、4)需要通過對(duì)樹的一次單旋轉(zhuǎn)完成調(diào)整。
第二種情況(2、3)需要通過對(duì)樹的一次雙旋轉(zhuǎn)完成調(diào)整。

LL旋轉(zhuǎn)

在左子樹上插入左孩子導(dǎo)致AVL樹失衡,"根的左子樹的高度"比"根的右子樹的高度"大2。
針對(duì)該情況,我們需要進(jìn)行單右旋轉(zhuǎn)來完成對(duì)樹的調(diào)整。

單右旋轉(zhuǎn).png

圖中左邊是旋轉(zhuǎn)之前的樹,右邊是旋轉(zhuǎn)之后的樹。從中可以發(fā)現(xiàn),旋轉(zhuǎn)之后的樹又變成了AVL樹,而且該旋轉(zhuǎn)只需要一次即可完成。
對(duì)于LL旋轉(zhuǎn),你可以這樣理解為:LL旋轉(zhuǎn)是圍繞"失去平衡的AVL根節(jié)點(diǎn)"進(jìn)行的,也就是節(jié)點(diǎn)4;而且由于是LL情況,就將節(jié)點(diǎn)4進(jìn)行一次順時(shí)針旋轉(zhuǎn)。

代碼實(shí)現(xiàn):

    /**
     * 進(jìn)行一次單右旋轉(zhuǎn)
     *
     * @param node 最小失衡樹根節(jié)點(diǎn)
     */
    private AVLTreeNode<T> rightRotation(AVLTreeNode<T> node) {
        AVLTreeNode<T> left = node.left;
        node.left = left.right;
        left.right = node;
        // 更新高度
        node.height = Math.max(height(node.left), height(node.right)) + 1;
        left.height = Math.max(height(left.left), height(left.right)) + 1;
        return left;
    }

RR旋轉(zhuǎn)

在右子樹插入右孩子導(dǎo)致AVL失衡時(shí),我們需要進(jìn)行單左旋調(diào)整。旋轉(zhuǎn)圍繞最小失衡子樹的根節(jié)點(diǎn)進(jìn)行。

單左旋轉(zhuǎn).png

    /**
     * 進(jìn)行一次單左旋轉(zhuǎn)
     *
     * @param node 最小失衡樹根節(jié)點(diǎn)
     */
    private AVLTreeNode<T> leftRotation(AVLTreeNode<T> node) {
        AVLTreeNode<T> right = node.right;
        node.right = right.left;
        right.left = node;

        // 更新高度
        node.height = Math.max(height(node.left), height(node.right)) + 1;
        right.height = Math.max(height(right.left), height(right.right)) + 1;

        return right;

    }

RL旋轉(zhuǎn)

“在右子樹上插入左孩子導(dǎo)致AVL樹失衡",此時(shí)我們需要進(jìn)行先右旋后左旋的調(diào)整。

左+右.png
    /**
     * 先右旋后左旋
     *
     * @param node 失衡樹根節(jié)點(diǎn)
     * @return 旋轉(zhuǎn)后的根節(jié)點(diǎn)
     */
    private AVLTreeNode<T> rightLeftRotation(AVLTreeNode<T> node) {
        node.right = rightRoation(node.right);

        return leftRoation(node);

    }

LR旋轉(zhuǎn)

“在左子樹上插入右孩子導(dǎo)致AVL樹失衡",此時(shí)我們需要進(jìn)行先左旋后右旋的調(diào)整。

右+左.png
    /**
     * 先左旋后右旋
     *
     * @param node 失衡樹根節(jié)點(diǎn)
     * @return 旋轉(zhuǎn)后的根節(jié)點(diǎn)
     */
    private AVLTreeNode<T> leftRightRotation(AVLTreeNode<T> node) {
        node.left = leftRoation(node.left);

        return rightLeftRoation(node);

    }

2.4.5 插入

代碼實(shí)現(xiàn)如下:

    public void insert(T key) {
        root = insert(root, key);
    }

    private AVLTreeNode<T> insert(AVLTreeNode<T> root, T key) {

        if (root == null) {
            root = new AVLTreeNode(key, null, null);
        } else {
            int cmp = key.compareTo(root.key);


            // 插入左子樹的情況
            if (cmp < 0) {
                root.left = insert(root.left, key);

                if (height(root.left) - height(root.right) == 2) {
                    // 情況二:插入到左子樹的左孩子節(jié)點(diǎn)上,進(jìn)行右旋
                    if (key.compareTo(root.left.key) < 0) {
                        root = rightRoation(root);
                    }

                    //情況四:插入到左子樹的右孩子節(jié)點(diǎn)上,進(jìn)行先左后右旋轉(zhuǎn)
                    else {
                        root = leftRightRoation(root);
                    }
                }


            }

            // 插入右子樹的情況
            if (cmp > 0) {
                root.right = insert(root.right, key);
                if (height(root.right) - height(root.left) == 2) {
                    // //情況一:插入右子樹的右節(jié)點(diǎn),進(jìn)行左旋
                    if (key.compareTo(root.right.key) > 0) {
                        root = leftRoation(root);
                    }
                    // 情況三:插入右子樹的左節(jié)點(diǎn),進(jìn)行先右再左旋轉(zhuǎn)
                    else {
                        root = rightLeftRoation(root);
                    }
                }

            }


            if (cmp == 0) {
                System.out.println("無法添加相同的節(jié)點(diǎn)");
            }


        }

        root.height = Math.max(height(root.left), height(root.right)) + 1;

        return root;


    }

2.4.6 刪除

刪除節(jié)點(diǎn)也可能導(dǎo)致AVL樹的失衡,實(shí)際上刪除節(jié)點(diǎn)和插入節(jié)點(diǎn)是一種互逆的操作:

刪除右子樹的節(jié)點(diǎn)導(dǎo)致AVL樹失衡時(shí),相當(dāng)于在左子樹插入節(jié)點(diǎn)導(dǎo)致AVL樹失衡,即情況情況二或情況四。
刪除左子樹的節(jié)點(diǎn)導(dǎo)致AVL樹失衡時(shí),相當(dāng)于在右子樹插入節(jié)點(diǎn)導(dǎo)致AVL樹失衡,即情況情況一或情況三。
另外,AVL樹也是一棵二叉排序樹,因此在刪除節(jié)點(diǎn)時(shí)也要維護(hù)二叉排序樹的性質(zhì)。

代碼實(shí)現(xiàn)如下:

public AVLTreeNode<T> remove(T key) {
        return remove(root, key);
    }

    private AVLTreeNode<T> remove(AVLTreeNode<T> node, T key) {

        if (node != null) {
            int cmp = key.compareTo(node.key);

            if (cmp == 0) {

                if (node.left != null && node.right != null) {

                    // 左子樹比右子樹高,在左子樹上選擇節(jié)點(diǎn)進(jìn)行替換
                    if (height(node.left) > height(node.right)) {
                        //使用左子樹最大節(jié)點(diǎn)來代替被刪節(jié)點(diǎn),而刪除該最大節(jié)點(diǎn)
                        AVLTreeNode<T> leftMax = findMax(node.left);
                        node.key = leftMax.key;
                        remove(node.left, leftMax.key);
                    }
                    //在右子樹上選擇節(jié)點(diǎn)進(jìn)行替換
                    else {
                        //使用最小節(jié)點(diǎn)來代替被刪節(jié)點(diǎn),而刪除該最小節(jié)點(diǎn)
                        AVLTreeNode<T> rightMin = findMin(node.right);
                        node.key = rightMin.key;
                        remove(node.right, rightMin.key);
                    }

                } else {
                    AVLTreeNode<T> tmp = node;
                    node = (node.left != null) ? node.left : node.right;
                    tmp = null;
                }


            }

            if (cmp > 0) {

                node.right = remove(node.right, key);

                if (height(node.left) - height(node.right) == 2) {
                    //相當(dāng)于在左子樹上插入右節(jié)點(diǎn)造成的失衡(情況四)
                    if (height(node.left.right) > height(node.left.left))
                        node = leftRightRotation(node);
                    else//相當(dāng)于在左子樹上插入左節(jié)點(diǎn)造成的失衡(情況二)
                        node = rightRotation(node);
                }

            }

            if (cmp < 0) {
                node.left = remove(node.left, key);
                if (height(node.right) - height(node.left) == 2) {
                    //相當(dāng)于在右子樹上插入左節(jié)點(diǎn)造成的失衡(情況三)
                    if (height(node.right.left) > height(node.right.right))
                        node = rightLeftRotation(node);
                    else//相當(dāng)于在右子樹上插入右節(jié)點(diǎn)造成的失衡(情況一)
                        node = leftRotation(node);
                }

            }

            return node;

        }


        return null;
    }

    // 查找最大值
    public AVLTreeNode findMax() {
        return findMax(root);
    }

    private AVLTreeNode<T> findMax(AVLTreeNode<T> node) {
        if (node != null) {
            while (node.right != null) {
                node = node.right;
            }

        }
        return node;
    }

    private AVLTreeNode<T> findMin(AVLTreeNode<T> node) {
        // 當(dāng)前節(jié)點(diǎn)為null,直接返回null
        if (node == null) {
            return null;
        }

        // 不存在左子樹,返回當(dāng)前節(jié)點(diǎn)
        if (node.left == null) {
            return node;
        }

        // 遞歸遍歷左子樹
        return findMin(node.left);

    }


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • 樹的概述 樹是一種非常常用的數(shù)據(jù)結(jié)構(gòu),樹與前面介紹的線性表,棧,隊(duì)列等線性結(jié)構(gòu)不同,樹是一種非線性結(jié)構(gòu) 1.樹的定...
    Jack921閱讀 4,473評(píng)論 1 31
  • 基于樹實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),具有兩個(gè)核心特征: 邏輯結(jié)構(gòu):數(shù)據(jù)元素之間具有層次關(guān)系; 數(shù)據(jù)運(yùn)算:操作方法具有Log級(jí)的平...
    yhthu閱讀 4,299評(píng)論 1 5
  • 什么是二叉樹? 引用自百度百科:在計(jì)算機(jī)科學(xué)中,二叉樹是每個(gè)節(jié)點(diǎn)最多有兩個(gè)子樹的樹結(jié)構(gòu)。通常子樹被稱作“左子樹”(...
    AnICoo1閱讀 1,388評(píng)論 0 1
  • 1.什么是二叉樹? 在計(jì)算機(jī)科學(xué)中,二叉樹是每個(gè)節(jié)點(diǎn)最多有兩個(gè)子樹的樹結(jié)構(gòu)。通常子樹被稱作“左子樹”和“右子樹”,...
    zcaaron閱讀 1,279評(píng)論 2 15
  • 去年二叉樹算法的事情鬧的沸沸揚(yáng)揚(yáng),起因是Homebrew 的作者 @Max Howell 在 twitter 上發(fā)...
    Masazumi柒閱讀 1,609評(píng)論 0 8