查找算法——二叉查找樹

1、二叉查找樹的定義與特性

  二叉查找樹(Binary Search Tree),也稱爲二叉搜索樹、有序二叉樹(ordered binary tree)或排序二叉樹(sorted binary tree),是指一棵空樹或者具備下列性質的二叉樹:node

  • 若任意節點的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;
  • 若任意節點的右子樹不空,則右子樹上全部節點的值均大於它的根節點的值;
  • 任意節點的左、右子樹也分別爲二叉查找樹;
  • 沒有鍵值相等的節點。

 
  二叉查找樹相比於其餘數據結構的優點在於查找、插入的時間複雜度較低,爲O(log n)。中序遍歷二叉查找樹可獲得一個關鍵字的有序序列一個無序序列能夠經過構造一棵二叉查找樹變成一個有序序列,構造樹的過程即將無序序列中元素逐個插入到二叉查找樹的過程。每次插入的新的結點都是二叉查找樹上新的葉子結點,在進行插入操做時,沒必要移動其它結點,只需改動某個結點的指針,由空變爲非空便可搜索、插入、刪除的複雜度等於樹高,指望 O(log n),最壞O(n)(數列有序,樹退化成線性表)。數據結構

  下面構造的二叉查找樹由於要實現選擇(select)排名(rank)兩個操做,因此在Node中新增長了一個int類型字段size,表示該結點所做爲根節點所表明的樹當中全部結點的個數(包括其自己),則對任意結點總有size(x)=size(x.left)+size(x.right)+1(1表明的是x結點自己),當咱們後面在進行插入、刪除相關操做的都要考慮到了對N的進行更新。還設置了size函數來返回size,以下所示less

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

    // return number of key-value pairs in BST rooted at x
    private int size(Node x) {
        if (x == null) return 0;
        else return x.size;
    }

2、 二叉查找樹的查找與插入

1.查找操做

//Returns the value associated with the given key.
    public Value get(Key key) {
        if (key == null) throw new IllegalArgumentException("calls get() with a null key");
        return get(root, key);
    }
    private Value get(Node x, Key key) {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) return get(x.left, key);
        else if (cmp > 0) return get(x.right, key);
        else              return x.val;
    }

  查找操做的遞歸實現:若是樹是空的,則查找未命中;若是被查找的鍵與根節點的鍵相等,查找命中。若是被查找的鍵較小就選擇在左子樹中(遞歸地)繼續查找,較大則選擇右子樹。函數

2.插入操做

public void put(Key key, Value val) {
            if (key == null) throw new IllegalArgumentException("calls put() with a null key");
            root = put(root, key, val);
        }
    private Node put(Node x, Key key, Value val) {
        if (x == null) return new Node(key, val, 1);
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) x.left  = put(x.left,  key, val);
        else if (cmp > 0) x.right = put(x.right, key, val);
        else   x.val   = val;
        //下面兩行時總會執行的
        x.size = 1 + size(x.left) + size(x.right);
        return x;
    }

  函數功能:在node做爲根節點所表示的二叉查找樹當中,將key和val生成新結點插入進去,已有相同key時則是更新對應的值爲val,而後將根節點返回。其實這個函數當中並不會有下面在刪除操做中存在的根節點被更換的問題,而插入操做自己也不須要像查找(要返回查找到的結點)或者選擇(返回排名爲k的結點)、排名(返回key對應結點在樹中的排名)、取整(要返回一個key取整在樹中可以找到的結點)的要有一個返回值,之因此返回根節點只是爲了更新結點當中的size,在Node不須要size屬性時,徹底能夠將返回值設置成爲void便可代碼留待後面寫下
  傳入參數:node、key和val。
  返回值:返回一個根節點,該根節點多是隻是根據參數key找到了子樹上某個結點更新了值或者根據key在該根節點子樹上的相應位置插入了一個新結點。this

  其中1234主要是邏輯實現,0是主要爲了邏輯實現與完善遞歸。指針

  1. 若是當前樹爲null則返回以key和val爲鍵值對的新節點
  2. 若是key小於當前節點的鍵,則將鍵值對放到當前節點的左子樹當中去,由於左子樹多是null,到達會被插入新結點,因此還要接收一下
  3. 若是key大於當前節點的鍵,則將鍵值對放到當前節點的右子樹當中去,由於右子樹多是null,到達會被插入新結點,因此還要接收一下
  4. 若是key等於當前節點的鍵,則將當前節點的值更新爲要插入的鍵值對中的值
  5. 無論key是大於小於等於,都要講當前節點的結點數目進行更新一下,以防進行了插入後,左或者右子樹的結點數目發生了變化。而後將當前結點返回。

3、 二叉查找樹的向上取整和向下取整

1.向下取整

public Key floor(Key key) {
        Node x = floor(root, key);
        if (x == null) return null;
        return x.key;
    } 

    private Node floor(Node x, Key key) {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if (cmp == 0) return x;
        if (cmp <  0) return floor(x.left, key);
        Node t = floor(x.right, key); 
        if (t != null) return t;
        else return x; 
    }

  向下取整是獲得鍵相比給定的key變小了一點的結點,向上取整是獲得鍵相比給定的key變大了一點的結點
  向下取整:若是給定鍵的key小於二叉樹的根節點的鍵,那麼小於等於key的最大鍵floor(key)必定在根節點的左子樹當中,這是由於原本就小於了你再變小一點那就只能是根節點左子樹當中的結點了,因此在左子樹當中;若是給定鍵的key大於二叉樹的根節點的鍵,那麼只有當根節點右子樹當中存在小於等於key的結點時,小於等於key的最大鍵纔會出如今右子樹當中,這是由於原本是大於的,若是變小了一點,那麼可能會變成根節點或者根節點左子樹中的結點。而且當小於的時候向下取整可能不存在,而當大於的時候向下取整必定存在。
  函數功能:在根節點x所表明的二叉查找樹當中找到 鍵值等於向下取整後的key 的結點,找不到則返回null
  傳入參數:二叉查找樹根節點x和要進行向下取整的鍵key
  返回值:在根節點x表明的樹中找到的key向下取整的結點或者null(未找到)code

  向下取整函數的遞歸邏輯(向上取整相似就不在重複,上下互換、左右互換、大小互換便可):排序

  1. 根節點爲null則返回null
  2. 若是key小於x.key,由於向下取整必定存在左子樹的結點當中或者在左子樹中不存在也是整顆子樹中不存在,因此那麼就返回在左子樹中找到的key的向下取整。
  3. 若是key大於x.key,這個時候向下取整必定存在了,在右子樹中找不到的話那就是x自己了,因此先接收在右子樹當中找到的key的向下取整,接受完判斷是否爲null,如不是則返回,如是則將x返回。

4、 二叉查找樹的選擇與排名

public Key select(int k) {
        if (k < 0 || k >= size()) {
            throw new IllegalArgumentException("argument to select() is invalid: " + k);
        }
        Node x = select(root, k);
        return x.key;
    }
    // Return key of rank k. 
    private Node select(Node x, int k) {
        if (x == null) return null; 
        int t = size(x.left); 
        if      (t > k) return select(x.left,  k); 
        else if (t < k) return select(x.right, k-t-1); 
        else            return x; 
    } 
    
    
    public int rank(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to rank() is null");
        return rank(key, root);
    } 
    // Number of keys in the subtree less than key.
    private int rank(Key key, Node x) {
        if (x == null) return 0; 
        int cmp = key.compareTo(x.key); 
        if      (cmp < 0) return rank(key, x.left); 
        else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right); 
        else              return size(x.left); 
    }

1.選擇操做

  函數功能:在根節點所表明的二叉查找樹當中找到名次爲k的結點,而後將其返回,如找不到則返回的是null
  傳入參數:排名名次k,二叉查找樹根節點x
  返回值:找到的排名爲k的鍵,找不到則返回null
  方法思路:假設咱們想找到排名爲k的鍵(即對於該鍵而言在樹中正好有k個小於它的鍵)。若是左子樹中的結點數t大於k,那麼咱們就繼續(遞歸地)在左子樹中查找排名爲k的鍵;若是t等於k,咱們就返回根節中的鍵;若是t小於k,咱們就(遞歸地)在右子樹中查找排名爲(k-t-1,由於要除去本來的根節點及其左子樹上的結點個數之和)的鍵。遞歸

2.排名操做

  傳入參數:要進行排名的鍵值Key(若是輸入的不是二叉樹中存在的結點的鍵那麼就會返回0,其實改爲-1應該更好些)
  返回值:給定鍵的排名
  方法思路:實現與select()相似:若是給定鍵和根節點的鍵相等,咱們返回左子樹的結點總數t;若是給定的鍵小於根節點,咱們會返回該鍵在左子樹中的排名(遞歸計算);若是給定的鍵大於根節點,咱們會返回t+1(根節點)加上它在右子樹當中的排名(遞歸計算)。ci


5、 二叉查找樹的刪除操做

1.刪除最大鍵和最小鍵

  對於delMin()方法(刪除最小鍵所對應的鍵值對),遞歸方法接收一個指向結點的引用,並返回指向一個結點的引用,這樣的話咱們就能將這個返回值賦給被做爲參數傳進來的那個引用變量,就能將對樹結構進行的刪除正確地反映。
  什麼意思呢,咱們在刪除的過程當中可能會刪除掉根節點,也就是說根節點換了,但函數外部那個本來的根節點的引用變量仍是指向被刪除的那個根節點,怎麼辦?就是隻能將新的根節點(固然根節點也可能沒被刪掉,仍是舊的,被刪掉的狀況是更少的可是是必須考慮的)返回賦值給那個根節點引用變量,以下面的root = deleteMin(root),這樣就能正確地將刪除操做反映出來了。

//Removes the smallest key and associated value from the symbol table.
    public void deleteMin() {
        if (isEmpty()) throw new NoSuchElementException("Symbol table underflow");
        root = deleteMin(root);
        assert check();
    }

    private Node deleteMin(Node x) {
        if (x.left == null) return x.right;
        x.left = deleteMin(x.left);
        x.size = size(x.left) + size(x.right) + 1;
        return x;
    }

  函數功能:刪除根節點x表明的二叉查找樹中鍵值最小的結點,而後將「新」的根節點x返回
  傳入參數:根節點x
  返回值:進行刪除鍵值最小的結點以後的"新"的根節點

  1. 判斷當前結點有沒有左子樹,沒有的話,那麼當前結點就是最小結點,那麼刪去以後的新的根節點就是當前結點的右子樹因此直接返回該結點的右子樹便可。有的話繼續下面的步驟。
  2. 左子樹存在,則要刪除的結點就在左子樹上,因此直接刪除左子樹的最小結點而後將左子樹的"新"的根節點返回並從新給當前結點的左子樹進行賦值。
  3. 左子樹既然刪除了一個結點,那麼結點數目確定發生了變化,因此要更新一下結點個數。
  4. 刪除而且更新結點數目完畢,能夠返回「新結點」了。

 
  刪除最大鍵與刪除最小鍵的原理和實現均類似,在此再也不贅述。

2.刪除指定鍵

  對於刪除指定鍵而言,若是指定鍵對應的結點x只有一個子結點或者沒有子結點,咱們就能夠採用和上面刪除最大鍵最小鍵的方式同樣來刪除。但對於有兩個子結點的結點x,要採用在刪除結點x後用它的後繼結點填補它的位置。由於x有一個右子結點,所以它的後繼結點就是其右子樹中的最小結點,這樣的替換仍能保證樹的有序性,由於x.key和它的後繼結點之間不存在其餘的鍵。咱們能用四個簡單的步驟完成將x替換爲它的後繼結點的任務
  ps:對於左右子樹任一或者同時不存在的狀況已經用刪除最大最小鍵的方法解決,在這裏不用再考慮了,必定是有右子樹的。

//Removes the specified key and its associated value from this symbol table     
    public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("calls delete() with a null key");
        root = delete(root, key);
        assert check();
    }

    private Node delete(Node x, Key key) {
        if (x == null) return null;

        int cmp = key.compareTo(x.key);
        if      (cmp < 0) x.left  = delete(x.left,  key);
        else if (cmp > 0) x.right = delete(x.right, key);
        else { 
            if (x.right == null) return x.left;
            if (x.left  == null) return x.right;
            Node t = x;
            x = min(t.right);
            x.right = deleteMin(t.right);
            x.left = t.left;
        } 
        x.size = size(x.left) + size(x.right) + 1;
        return x;
    }

  找到要刪除的結點後的操做

  1. 若是結點左子樹爲空,則直接將右子樹做爲新的根節點返回便可。如結點右子樹爲空,則直接將左子樹做爲新的根節點返回便可。如下流程都是在左右子樹都存在的狀況下進行的了。
  2. 將指向x即將被刪除的結點的連接保存爲t;
  3. 將x指向他的後繼結點min(t.right);
  4. 將x的右連接(本來指向一顆全部結點都大於x.key的二叉查找樹)指向deleteMin(t.right),也就是在刪除後的全部結點都仍然大於x.key的子二叉查找樹。其實這個時候x已是min(t.right)了,但還未從t.right這棵樹中刪去並指向刪除後的t.right。
  5. 將x的左鏈接(本爲空,由於它本來是min(t.right))設爲t.left(其下全部鍵都小於被刪除的結點和它的後繼結點)。

 
  以上就是在找到指定鍵值相應節點後的操做,能夠看出是一個順序化流程,並不涉及遞歸,但在找到這個相應結點的過程當中仍是用到了遞歸,並在找到後對結點的N進行了更新,刪除操做整體流程以下:
  函數功能:對指定根結點x所表示的樹,刪除指定key在樹中對應的結點,而後將該樹「新」的根節點返回
  傳入參數:根節點x,要刪除的結點的指定鍵key
  返回值:進行刪除以後的"新"的根節點(也可能並無進行任何刪除操做,在沒找到指定鍵的狀況下)

  1. 若是根結點爲空則返回null,不然繼續。
  2. 判斷指定鍵與根結點的鍵的大小,指定鍵更大,則將右子樹在刪除key對應結點後的新的根結點賦值給x.right(由於有可能刪除的就是x.right自己)。
  3. 指定鍵更小,則將左子樹在刪除key對應結點後的新的根節點賦值給x.left(由於有可能刪除的就是x.left自己)。
  4. 指定鍵與根結點的鍵值相等,則說明當前結點就是要刪除的結點,去執行找到要刪除的結點後的操做
  5. 對根節點的N進行更新,而後返回根節點x。
相關文章
相關標籤/搜索