Java數(shù)據(jù)結構——二叉搜索樹

查找算法

不管是之前學過的數(shù)組矩动、鏈表有巧、隊列、還是棧悲没,這些線性結構中篮迎,如果想在其中查找一個元素,效率是比較慢的示姿,只有O(N)甜橱,因此如果你的需求是實現(xiàn)快速查找,那么就需要新的算法設計栈戳,也需要新的數(shù)據(jù)結構支持岂傲。

還記得最先介紹的那個二分查找算法嗎?它的查找效率能夠達到 O(\log{N})子檀,是不是還不錯镊掖?不過呢乃戈,它需要對數(shù)組事先排好序,而排序的成本是比較高的亩进。那么有沒有一個折中的辦法呢症虑?有,那就是接下來要給大家介紹的二叉搜索樹

1. 二叉搜索樹

二叉搜索樹(也稱二叉排序樹)是符合下面特征的二叉樹:

  1. 樹節(jié)點增加 key 屬性归薛,用來比較誰大誰小谍憔,key 不可以重復
  2. 對于任意一個樹節(jié)點,它的 key 比左子樹的 key 都大主籍,同時也比右子樹的 key 都小习贫,例如下圖所示


    image.png

輕易看出要查找 7 (從根開始)自然就可應用二分查找算法,只需三次比較

  • 與 4 比崇猫,較之大沈条,向右找
  • 與 6 比,較之大诅炉,繼續(xù)向右找
  • 與 7 比,找到

查找的時間復雜度與樹高相關屋厘,插入涕烧、刪除也是如此。

  • 如果這棵樹長得還不賴(左右平衡)上圖汗洒,那么時間復雜度均是 O(\log{N})
  • 當然议纯,這棵樹如果長得丑(左右高度相差過大)下圖,那么這時是最糟的情況溢谤,時間復雜度是 O(N)
image.png

注:

  • 二叉搜索樹 - 英文 binary search tree瞻凤,簡稱 BST
  • 二叉排序樹 - 英文 binary ordered tree 或 binary sorted tree

定義節(jié)點

static class BSTNode {
    int key; // 若希望任意類型作為 key, 則后續(xù)可以將其設計為 Comparable 接口
    Object value;
    BSTNode left;
    BSTNode right;

    public BSTNode(int key) {
        this.key = key;
        this.value = key;
    }

    public BSTNode(int key, Object value) {
        this.key = key;
        this.value = value;
    }

    public BSTNode(int key, Object value, BSTNode left, BSTNode right) {
        this.key = key;
        this.value = value;
        this.left = left;
        this.right = right;
    }
}

查詢

遞歸實現(xiàn)

public Object get(int key) {
    return doGet(root, key);
}

private Object doGet(BSTNode node, int key) {
    if (node == null) {
        return null; // 沒找到
    }
    if (key < node.key) {
        return doGet(node.left, key); // 向左找
    } else if (node.key < key) {
        return doGet(node.right, key); // 向右找
    } else {
        return node.value; // 找到了
    }
}

非遞歸實現(xiàn)

public Object get(int key) {
    BSTNode node = root;
    while (node != null) {
        if (key < node.key) {
            node = node.left;
        } else if (node.key < key) {
            node = node.right;
        } else {
            return node.value;
        }
    }
    return null;
}

Comparable

如果希望讓除 int 外更多的類型能夠作為 key,一種方式是 key 必須實現(xiàn) Comparable 接口世杀。

public class BSTTree2<T extends Comparable<T>> {
    static class BSTNode<T> {
        T key; // 若希望任意類型作為 key, 則后續(xù)可以將其設計為 Comparable 接口
        Object value;
        BSTNode<T> left;
        BSTNode<T> right;

        public BSTNode(T key) {
            this.key = key;
            this.value = key;
        }

        public BSTNode(T key, Object value) {
            this.key = key;
            this.value = value;
        }

        public BSTNode(T key, Object value, BSTNode<T> left, BSTNode<T> right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

    BSTNode<T> root;

    public Object get(T key) {
        return doGet(root, key);
    }

    private Object doGet(BSTNode<T> node, T key) {
        if (node == null) {
            return null;
        }
        int result = node.key.compareTo(key);
        if (result > 0) {
            return doGet(node.left, key);
        } else if (result < 0) {
            return doGet(node.right, key);
        } else {
            return node.value;
        }
    }

}

還有一種做法不要求 key 實現(xiàn) Comparable 接口阀参,而是在構造 Tree 時把比較規(guī)則作為 Comparator 傳入,將來比較 key 大小時都調(diào)用此 Comparator 進行比較瞻坝,這種做法可以參考 Java 中的 java.util.TreeMap

最小

遞歸實現(xiàn)

public Object min() {
    return doMin(root);
}

public Object doMin(BSTNode node) {
    if (node == null) {
        return null;
    }
    // 左邊已走到頭
    if (node.left == null) { 
        return node.value;
    }
    return doMin(node.left);
}

非遞歸實現(xiàn)

public Object min() {
    if (root == null) {
        return null;
    }
    BSTNode p = root;
    // 左邊未走到頭
    while (p.left != null) {
        p = p.left;
    }
    return p.value;
}

最大

遞歸實現(xiàn)

public Object max() {
    return doMax(root);
}

public Object doMax(BSTNode node) {
    if (node == null) {
        return null;
    }
    // 右邊已走到頭
    if (node.left == null) { 
        return node.value;
    }
    return doMin(node.right);
}

非遞歸實現(xiàn)

public Object max() {
    if (root == null) {
        return null;
    }
    BSTNode p = root;
    // 右邊未走到頭
    while (p.right != null) {
        p = p.right;
    }
    return p.value;
}

新增

遞歸實現(xiàn)

public void put(int key, Object value) {
    root = doPut(root, key, value);
}

private BSTNode doPut(BSTNode node, int key, Object value) {
    if (node == null) {
        return new BSTNode(key, value);
    }
    if (key < node.key) {
        node.left = doPut(node.left, key, value);
    } else if (node.key < key) {
        node.right = doPut(node.right, key, value);
    } else {
        node.value = value;
    }
    return node;
}
  • 若找到 key蛛壳,走 else 更新找到節(jié)點的值
  • 若沒找到 key,走第一個 if所刀,創(chuàng)建并返回新節(jié)點
    • 返回的新節(jié)點衙荐,作為上次遞歸時 node 的左孩子或右孩子
    • 缺點是,會有很多不必要的賦值操作

非遞歸實現(xiàn)

public void put(int key, Object value) {
    BSTNode node = root;
    BSTNode parent = null;
    while (node != null) {
        parent = node;
        if (key < node.key) {
            node = node.left;
        } else if (node.key < key) {
            node = node.right;
        } else {
            // 1. key 存在則更新
            node.value = value;
            return;
        }
    }
    // 2. key 不存在則新增
    if (parent == null) {
        root = new BSTNode(key, value);
    } else if (key < parent.key) {
        parent.left = new BSTNode(key, value);
    } else {
        parent.right = new BSTNode(key, value);
    }
}

前驅后繼

image.png

一個節(jié)點的前驅(前任)節(jié)點是指比它小的節(jié)點中浮创,最大的那個

一個節(jié)點的后繼(后任)節(jié)點是指比它大的節(jié)點中忧吟,最小的那個

例如上圖中

  • 1 沒有前驅,后繼是 2
  • 2 前驅是 1斩披,后繼是 3
  • 3 前驅是 2溜族,后繼是 4
  • ...

簡單的辦法是中序遍歷讹俊,即可獲得排序結果,此時很容易找到前驅后繼

要效率更高斩祭,需要研究一下規(guī)律劣像,找前驅分成 2 種情況:

image.png
  1. 節(jié)點有左子樹,此時前驅節(jié)點就是左子樹的最大值摧玫,圖中屬于這種情況的有
    • 2 的前驅是1
    • 4 的前驅是 3
    • 6 的前驅是 5
    • 7 的前驅是 6
  2. 節(jié)點沒有左子樹耳奕,若離它最近的祖先自從左而來,此祖先即為前驅诬像,如
    • 3 的祖先 2 自左而來屋群,前驅 2
    • 5 的祖先 4 自左而來,前驅 4
    • 8 的祖先 7 自左而來坏挠,前驅 7
    • 1 沒有這樣的祖先芍躏,前驅 null

找后繼也分成 2 種情況

image.png
  1. 節(jié)點有右子樹,此時后繼節(jié)點即為右子樹的最小值降狠,如
    • 2 的后繼 3
    • 3 的后繼 4
    • 5 的后繼 6
    • 7 的后繼 8
  2. 節(jié)點沒有右子樹对竣,若離它最近的祖先自從右而來,此祖先即為后繼榜配,如
    • 1 的祖先 2 自右而來否纬,后繼 2
    • 4 的祖先 5 自右而來,后繼 5
    • 6 的祖先 7 自右而來蛋褥,后繼 7
    • 8 沒有這樣的祖先临燃,后繼 null
public Object predecessor(int key) {
    BSTNode ancestorFromLeft = null;
    BSTNode p = root;
    while (p != null) {
        if (key < p.key) {
            p = p.left;
        } else if (p.key < key) {
            ancestorFromLeft = p;
            p = p.right;
        } else {
            break;
        }
    }

    if (p == null) {
        return null;
    }
    // 情況1 - 有左孩子
    if (p.left != null) {
        return max(p.left);
    }
    // 情況2 - 有祖先自左而來
    return ancestorFromLeft != null ? ancestorFromLeft.value : null;
}


public Object successor(int key) {
    BSTNode ancestorFromRight = null;
    BSTNode p = root;
    while (p != null) {
        if (key < p.key) {
            ancestorFromRight = p;
            p = p.left;
        } else if (p.key < key) {
            p = p.right;
        } else {
            break;
        }
    }

    if (p == null) {
        return null;
    }
    // 情況1 - 有右孩子
    if (p.right != null) {
        return min(p.right);
    }
    // 情況2 - 有祖先自右而來
    return ancestorFromRight != null ? ancestorFromRight.value : null;
}

刪除

要刪除某節(jié)點(稱為 D),必須先找到被刪除節(jié)點的父節(jié)點烙心,這里稱為 Parent

  1. 刪除節(jié)點沒有左孩子膜廊,將右孩子托孤給 Parent
  2. 刪除節(jié)點沒有右孩子,將左孩子托孤給 Parent
  3. 刪除節(jié)點左右孩子都沒有淫茵,已經(jīng)被涵蓋在情況1爪瓜、情況2 當中,把 null 托孤給 Parent
  4. 刪除節(jié)點左右孩子都有痘昌,可以將它的后繼節(jié)點(稱為 S)托孤給 Parent钥勋,設 S 的父親為 SP,又分兩種情況
    1. SP 就是被刪除節(jié)點辆苔,此時 D 與 S 緊鄰算灸,只需將 S 托孤給 Parent
    2. SP 不是被刪除節(jié)點,此時 D 與 S 不相鄰驻啤,此時需要將 S 的后代托孤給 SP菲驴,再將 S 托孤給 Parent

非遞歸實現(xiàn)

/**
 * <h3>根據(jù)關鍵字刪除</h3>
 *
 * @param key 關鍵字
 * @return 被刪除關鍵字對應值
 */
public Object delete(int key) {
    BSTNode p = root;
    BSTNode parent = null;
    while (p != null) {
        if (key < p.key) {
            parent = p;
            p = p.left;
        } else if (p.key < key) {
            parent = p;
            p = p.right;
        } else {
            break;
        }
    }
    if (p == null) {
        return null;
    }
    // 刪除操作
    if (p.left == null) {
        shift(parent, p, p.right); // 情況1
    } else if (p.right == null) {
        shift(parent, p, p.left); // 情況2
    } else {
        // 情況4
        // 4.1 被刪除節(jié)點找后繼
        BSTNode s = p.right;
        BSTNode sParent = p; // 后繼父親
        while (s.left != null) {
            sParent = s;
            s = s.left;
        }
        // 4.2 刪除和后繼不相鄰, 處理后繼的后事
        if (sParent != p) {                
            shift(sParent, s, s.right); // 不可能有左孩子
            s.right = p.right;
        }
        // 4.3 后繼取代被刪除節(jié)點
        shift(parent, p, s);
        s.left = p.left;
    }
    return p.value;
}

/**
 * 托孤方法
 *
 * @param parent  被刪除節(jié)點的父親
 * @param deleted 被刪除節(jié)點
 * @param child   被頂上去的節(jié)點
 */
// 只考慮讓 n1父親的左或右孩子指向 n2, n1自己的左或右孩子并未在方法內(nèi)改變
private void shift(BSTNode parent, BSTNode deleted, BSTNode child) {
    if (parent == null) {
        root = child;
    } else if (deleted == parent.left) {
        parent.left = child;
    } else {
        parent.right = child;
    }
}

遞歸實現(xiàn)

public Object delete(int key) {
    ArrayList<Object> result = new ArrayList<>();
    root = doDelete(root, key, result);
    return result.isEmpty() ? null : result.get(0);
}

public BSTNode doDelete(BSTNode node, int key, ArrayList<Object> result) {
    if (node == null) {
        return null;
    }
    if (key < node.key) {
        node.left = doDelete(node.left, key, result);
        return node;
    }
    if (node.key < key) {
        node.right = doDelete(node.right, key, result);
        return node;
    }
    result.add(node.value);
    if (node.left != null && node.right != null) {
        BSTNode s = node.right;
        while (s.left != null) {
            s = s.left;
        }
        s.right = doDelete(node.right, s.key, new ArrayList<>());
        s.left = node.left;
        return s;
    }
    return node.left != null ? node.left : node.right;
}

說明

  1. ArrayList<Object> result 用來保存被刪除節(jié)點的值
  2. 第二、第三個 if 對應沒找到的情況骑冗,繼續(xù)遞歸查找和刪除赊瞬,注意后續(xù)的 doDelete 返回值代表刪剩下的先煎,因此需要更新
  3. 最后一個 return 對應刪除節(jié)點只有一個孩子的情況,返回那個不為空的孩子巧涧,待刪節(jié)點自己因沒有返回而被刪除
  4. 第四個 if 對應刪除節(jié)點有兩個孩子的情況薯蝎,此時需要找到后繼節(jié)點,并在待刪除節(jié)點的右子樹中刪掉后繼節(jié)點谤绳,最后用后繼節(jié)點替代掉待刪除節(jié)點返回占锯,別忘了改變后繼節(jié)點的左右指針

找小的

public List<Object> less(int key) {
    ArrayList<Object> result = new ArrayList<>();
    BSTNode p = root;
    LinkedList<BSTNode> stack = new LinkedList<>();
    while (p != null || !stack.isEmpty()) {
        if (p != null) {
            stack.push(p);
            p = p.left;
        } else {
            BSTNode pop = stack.pop();
            if (pop.key < key) {
                result.add(pop.value);
            } else {
                break;
            }
            p = pop.right;
        }
    }
    return result;
}

找大的

public List<Object> greater(int key) {
    ArrayList<Object> result = new ArrayList<>();
    BSTNode p = root;
    LinkedList<BSTNode> stack = new LinkedList<>();
    while (p != null || !stack.isEmpty()) {
        if (p != null) {
            stack.push(p);
            p = p.left;
        } else {
            BSTNode pop = stack.pop();
            if (pop.key > key) {
                result.add(pop.value);
            }
            p = pop.right;
        }
    }
    return result;
}

但這樣效率不高,可以用 RNL 遍歷

注:

  • Pre-order, NLR
  • In-order, LNR
  • Post-order, LRN
  • Reverse pre-order, NRL
  • Reverse in-order, RNL
  • Reverse post-order, RLN
public List<Object> greater(int key) {
    ArrayList<Object> result = new ArrayList<>();
    BSTNode p = root;
    LinkedList<BSTNode> stack = new LinkedList<>();
    while (p != null || !stack.isEmpty()) {
        if (p != null) {
            stack.push(p);
            p = p.right;
        } else {
            BSTNode pop = stack.pop();
            if (pop.key > key) {
                result.add(pop.value);
            } else {
                break;
            }
            p = pop.left;
        }
    }
    return result;
}

找之間

public List<Object> between(int key1, int key2) {
    ArrayList<Object> result = new ArrayList<>();
    BSTNode p = root;
    LinkedList<BSTNode> stack = new LinkedList<>();
    while (p != null || !stack.isEmpty()) {
        if (p != null) {
            stack.push(p);
            p = p.left;
        } else {
            BSTNode pop = stack.pop();
            if (pop.key >= key1 && pop.key <= key2) {
                result.add(pop.value);
            } else if (pop.key > key2) {
                break;
            }
            p = pop.right;
        }
    }
    return result;
}
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缩筛,一起剝皮案震驚了整個濱河市消略,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞎抛,老刑警劉巖艺演,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異桐臊,居然都是意外死亡胎撤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門断凶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哩照,“玉大人,你說我怎么就攤上這事懒浮。” “怎么了识藤?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵砚著,是天一觀的道長。 經(jīng)常有香客問我痴昧,道長稽穆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任赶撰,我火速辦了婚禮舌镶,結果婚禮上,老公的妹妹穿的比我還像新娘豪娜。我一直安慰自己餐胀,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布瘤载。 她就那樣靜靜地躺著否灾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸣奔。 梳的紋絲不亂的頭發(fā)上墨技,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天惩阶,我揣著相機與錄音,去河邊找鬼扣汪。 笑死断楷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的崭别。 我是一名探鬼主播冬筒,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼紊遵!你這毒婦竟也來了账千?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤暗膜,失蹤者是張志新(化名)和其女友劉穎匀奏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體学搜,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡娃善,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瑞佩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聚磺。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炬丸,靈堂內(nèi)的尸體忽然破棺而出瘫寝,到底是詐尸還是另有隱情,我是刑警寧澤稠炬,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布焕阿,位于F島的核電站,受9級特大地震影響首启,放射性物質(zhì)發(fā)生泄漏暮屡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一毅桃、第九天 我趴在偏房一處隱蔽的房頂上張望褒纲。 院中可真熱鬧,春花似錦钥飞、人聲如沸莺掠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汁蝶。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掖棉,已是汗流浹背墓律。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幔亥,地道東北人耻讽。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像帕棉,于是被迫代替她去往敵國和親针肥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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