普林斯頓《算法》筆記(三)

官方網站 官方代碼


第三章 查找


3.1 符號表 (Symbol Tables)

符號表是一種存儲鍵值對 (key-value pairs) 的數據結構,其主要目的是將鍵 (key) 和值 (value) 聯繫起來。主要支持兩種操做:插入 (put) ,即將一組新的鍵值對存入存入表中;查找 (get) ,即根據給定的鍵獲得相應的值。java

下表列出了符號表的典型應用:算法




符號表是一種典型的抽象數據類型,它表明一組清晰定義的值以及對這些值相應的操做, 使得咱們可以將類型的實現和使用區別開來。下圖是一種泛型符號表API:數組


鍵的等價性 數據結構

在Java中全部對象都繼承了一個 equals() 方法,Java也爲其標準數據類型如Integer、Double和String以及其餘一些更加複雜的類型,如File和URL,實現了 equals() 方法。若是是自定義的鍵則須要重寫 equals() 方法 (如1.2.5.8中的Date類型),這時最好使用不可變 (immutable) 的數據類型做爲鍵,如 Integer, Double, String, java.io.File等。若是鍵是Comparable對象,則 a.compareTo(b) == 0a.equals(b) 是等價的。函數


有序符號表 (Ordered symbol tables) 性能

典型的應用程序中,鍵都是Comparable對象,所以能夠用 a.compareTo(b) 來比較a和b兩個鍵。這樣就能經過Comparable接口帶來的鍵的有序性來更好地實現put() 和 get() 方法,由此也能定義更多實用操做,以下表所示。通常只要見到類的聲明中含有泛型變量 Key extends Comparable<Key>,則說明這段程序實在實現這份API。網站


符號表用例一: this

public static void main(String[] args)
{
    ST<String, Integer> st;
    st = new ST<String, Integer>();
    for (int i = 0; !.StdIn.isEmpty(); i++)
    {
        String key = StdIn.readString();
        st.put(key, i);
    }
    for (String s : st.keys())
        StdOut.println(s + " " + st.get(s));
}




符號表用例二: spa

下列FrequencyCounter 用例統計了標準輸入中各個單詞的出現頻率,而後將頻率最高的且不小於指定長度的單詞打印出來。設計

public class FrequencyCounter
{
    public static void main(String[] args)
    {
        int minlen = Integer.parseInt(args[0]);
        ST<String, Integer> st = new ST<String, Integer>();
        while (!StdIn.isEmpty())
        {
            String word = StdIn.readString();
            if (word.length() < minlen) continue;
            if (!st.contains(word)) st.put(word, 1);
            else                    st.put(word, st.get(word) + 1);
        }
        String max = "";
        st.put(max, 0);
        for (String word : st.keys())
            if (st.get(word) > st.get(max))
                max = word;
        StdOut.println(max + " " + st.get(max));
    }
}

/***************************************
% java FrequancyCounter 1 < tinyTale.txt
it 10
***************************************/



無序鏈表中的順序查找 (Sequential Search)

順序查找經過get()方法會順序地搜索鏈表查找給定的鍵,並返回相應的值;put() 方法一樣順序地搜索給定的鍵,若是找到則更新相關聯的值,不然建立一個新結點並將其插入到鏈表的開頭。

import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class SequentialSearchST<Key, Value>
{
    private int n;
    private Node first;

    private class Node
    {
        private Key key;
        private Value val;
        private Node next;

        public Node(Key key, Value val, Node next)
        {
            this.key = key;
            this.val = val;
            this.next = next;
        }
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        for (Node x = first; x != null; x = x.next)
            if (key.equals(x.key))
                return x.val;
        return null;
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        { delete(key); return; }

        for (Node x = first; x != null; x = x.next)
        {
            if (key.equals(x.key))
            { x.val = val; return; }
        }
        first = new Node(key, val, first);
        n++;
    }
    
    public void delete(Key key)
    { first = delete(first, key); }

    private Node delete(Node x, Key key)
    {
        if (key.equals(x.key))
        { n--; return x.next; }
        x.next = delete(x.next, key);
        return x;
    }

    public Iterable<Key> keys()
    {
        Queue<Key> queue = new Queue<Key>();
        for (Node x = first; x != null; x = x.next)
            queue.enqueue(x.key);
        return queue;
    }

    public static void main(String[] args)
    {
        SequentialSearchST<String, Integer> st = new SequentialSearchST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}



有序數組中的二分查找 (Binary Search)

下列實現使用兩個數組分別保存key 和 value,這樣的數據結構是一對平行的數組。須要建立一個Key類型的Comparable對象的數組和一個Value 類型的Object對象的數組,並在構造函數中將它們轉化爲Key[]Value[]

put() 方法能夠保證數組中Comparable 類型的鍵有序,具體是使用rank() 方法獲得鍵的具體位置,而後將全部更大的鍵向後移動一格來騰出位置並插入新的鍵值。

這份實現的核心在於rank()方法,它返回表中小於給定鍵的鍵的數量。如下是遞歸版本的代碼:

public int rank(Key key, int lo, int hi)
{
    if (hi < lo) return lo;
    int mid = (lo + hi) / 2;
    int cmp = key.compareTo(keys[mid]);
    if (cmp < 0) return rank(key, lo, mid-1);
    else if (cmp > 0) return rank(key, mid+1, hi);
    else return mid;
}

import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class BinarySearchST<Key extends Comparable<Key>, Value>
{
    private static final int INIT_CAPACITY = 2;
    private Key[] keys;
    private Value[] vals;
    private int n = 0;

    public BinarySearchST()
    { this(INIT_CAPACITY); }

    public BinarySearchST(int capacity)
    {
        keys = (Key[]) new Comparable[capacity];
        vals = (Value[]) new Object[capacity];
    }

    private void resize(int capacity)
    {
        assert capacity >= n;
        Key[]    tempk = (Key[])   new Comparable[capacity];
        Value[]  tempv = (Value[]) new Object[capacity];
        for (int i = 0; i < n; i++)
        {
            tempk[i] = keys[i];
            tempv[i] = vals[i];
        }
        keys = tempk;
        vals = tempv;
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        if (isEmpty()) return null;
        int i = rank(key);
        if (i < n && keys[i].compareTo(key) == 0) return vals[i];
        return null;
    }

    public int rank(Key key)
    {
        int lo = 0, hi = n-1;
        while (lo <= hi)
        {
            int mid = lo + (hi - lo) / 2;
            int cmp = key.compareTo(keys[mid]);
            if      (cmp < 0) hi = mid - 1;
            else if (cmp > 0) lo = mid + 1;
            else return mid; 
        }
        return lo;
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        { delete(key); return; }

        int i = rank(key);

        if (i < n && keys[i].compareTo(key) == 0)
        { vals[i] = val; return; }

        if (n == keys.length) resize(2*keys.length);

        for (int j = n; j > i; j--)
        {
            keys[j] = keys[j-1];
            vals[j] = vals[j-1];
        }

        keys[i] = key;
        vals[i] = val;
        n++;
    }

    public void delete(Key key)
    {
        int i = rank(key);

        if (i == n || keys[i].compareTo(key) != 0)
        { return; }

        for (int j = i; j < n-1; j++)
        {
            keys[j] = keys[j+1];
            vals[j] = vals[j+1];
        }

        n--;
        keys[n] = null;
        vals[n] = null;

        if (n > 0 && n == keys.length/4) resize(keys.length/2);  
    }

    public void deleteMin()
    { delete(min()); }

    public void deleteMax()
    { delete(max()); }

    public Key min()
    { return keys[0]; }

    public Key max()
    { return keys[n-1]; }

    public Key select(int k)
    { return keys[k]; }

    public Key floor(Key key)
    {
        int i = rank(key);
        if (i < n && key.compareTo(keys[i]) == 0) return keys[i];
        if (n == 0) return null;
        else return keys[i-1];
    }

    public Key ceiling(Key key)
    {
        int i = rank(key);
        if (i == n) return null;
        else return keys[i];
    }

    public int size(Key lo, Key hi)
    {
        if (lo.compareTo(hi) > 0) return 0;
        if (contains(hi)) return rank(hi) - rank(lo) + 1;
        else              return rank(hi) - rank(lo);
    }

    public Iterable<Key> keys()
    { return keys(min(), max()); }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        if (lo.compareTo(hi) > 0) return queue;
        for (int i = rank(lo); i < rank(hi); i++)
            queue.enqueue(keys[i]);
        if (contains(hi)) queue.enqueue(keys[rank(hi)]);
        return queue;
    }

    public static void main(String[] args)
    { 
        BinarySearchST<String, Integer> st = new BinarySearchST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }

        System.out.println(st.size());
        st.delete("S");
        System.out.println(st.floor("Z"));
        System.out.println(st.select(2));
        System.out.println(st.size());

        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


  • Sequential search 和 binary search 的成本模型,插入都是線性級別的,對於大數組來講太慢了。咱們須要一種更復雜的數據結構如二叉查找樹 (binary search tree)來實現對數級別的插入和查找操做。


要支持高效的插入操做,須要一種鏈式結構,但單鏈表是沒法使用binary search的,由於binary search的高效性來源於可以經過索引快速取得任何子數組的中間元素 (但獲得一條鏈表的中間元素的惟一方法是沿鏈表遍歷)。下一節的二叉查找樹 (binary search tree)能夠將binary search的效率和鏈表的靈活性結合起來。下表是本章各類符號表實現的優缺點概覽:






3.2 二叉查找樹 (Binary Search Tree)

二叉樹 (binary tree):每一個結點最多有兩個子樹的樹結構。

二叉查找樹 (binary search tree):二叉查找樹的每一個結點包含一個Comparable鍵和一個值,且每一個結點的鍵 (key)都大於其左子樹中的任意結點的鍵而小於右子樹的任意結點的鍵。是一種結合了鏈表插入的靈活性和有序數組查找的高效性的符號表實現。


3.2.1 查找

查找分爲兩種狀況:若是含有該鍵的結點存在表中,則返回相應的值;若不存在則返回null。

3.2.2 插入

插入一樣分兩種狀況:若是該鍵的結點在表中,則更新值;若不存在則增長新結點。

二叉查找樹可以保持鍵的有序性,所以它能夠做爲實現有序符號表中衆多方法的基礎。

3.2.3 floor 和 ceiling

3.2.4 Selection

Selection操做是要找到排名爲k的鍵 (即樹中正好有k個小於它的鍵)。

3.2.5 Rank

rank()select()的逆方法,返回給定鍵的排名。

3.2.6 刪除最大鍵和刪除最小鍵

3.2.7 刪除操做

3.2.8 範圍查找 (range queries)

要實現可以返回給定範圍內鍵的keys方法,首先要對二叉樹進行中序遍歷 (inorder traversal)。以下所示:

private void print(Node x)
{
    if (x == null) return;
    print(x.left);
    StdOut.println(x.key);
    print(x.right);
}

範圍查找主要經過將全部給定範圍內的鍵加入隊列 Queue 來實現。


  • 下列二叉查找樹的實現中樹由 Node 對象組成,每一個對象含有一對鍵值,兩條連接 (link) 和一個結點計數器N。每一個Node對象都是一顆含有N個結點的子樹的根結點。
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class BST<Key extends Comparable<Key>, Value>
{
    private Node root;

    private class Node
    {
        private Key key;
        private Value val;
        private Node left, right;
        private int N;

        public Node(Key key, Value val, int N)
        { this.key = key; this.val = val; this.N = N; }
    }

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

    private int size(Node x)
    {
        if (x == null) return 0;
        else           return x.N;
    }

    public Value get(Key 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;
    }

    public void put(Key key, Value val)
    { 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.N = size(x.left) + size(x.right) + 1;
        return x;
    }

    public Key min()
    { return min(root).key; }

    private Node min(Node x)
    {
        if (x.left == null) return x;
        return min(x.left);
    }

    public Key max()
    { return max(root).key; }

    private Node max(Node x)
    {
        if (x.right == null) return x;
        return max(x.right);
    }

    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;
    }

    public Key select(int k)
    { return select(root, k).key; }

    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)
    { return rank(key, root); }

    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);
    }

    public void deleteMin()
    { root = deleteMin(root); }

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

    public void delete(Key key)
    { root = delete(root, key); }

    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.N = size(x.left) + size(x.right) + 1;
        return x;
    }

    public Iterable<Key> keys()
    { return keys(min(), max()); }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        keys(root, queue, lo, hi);
        return queue;
    }

    private void keys(Node x, Queue<Key> queue, Key lo, Key hi)
    {
        if (x == null) return;
        int cmplo = lo.compareTo(x.key);
        int cmphi = hi.compareTo(x.key);
        if (cmplo < 0) keys(x.left, queue, lo, hi);
        if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
        if (cmphi >0 ) keys(x.right, queue, lo, hi);
    }

    public static void main(String[] args)
    {
        BST<String, Integer> st = new BST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


各類符號表實現的比較:






3.3 平衡查找樹 (Balanced Search Tree)

一、 2-3查找樹

一顆2-3查找樹或爲一顆空樹,或由如下結點組成:

  • 2- 結點,含有一個鍵(key) (及其對應的值) 和兩條連接 (link),左連接指向的的鍵都小於該結點,右連接指向的鍵都大於該結點。
  • 3- 結點,含有兩個鍵 (及其對應的值) 和三條連接,左連接指向的鍵都小於該結點,中連接指向的鍵位於該結點兩個鍵之間,右連接指向的鍵都大於該結點。

同時將指向一顆空樹的連接成爲空連接 (null link)。


查找和各類插入操做

這部分直接看圖可比文字描述清晰多了。


下圖總結了將一個4- 結點分解爲一顆2-3 樹可能的6種狀況。2-3 樹插入算法的根本在於這些變換都是局部的:除了相關的結點和連接外沒必要修改或檢查樹的其餘部分。



這些局部變換不會影響樹的全局有序性和平衡性:任意空連接到根結點的路徑長度都是相等的。如下圖舉例,變換前根結點到全部空連接的路徑長度爲h,變換後仍然爲h。只有當根結點被分解爲3個2- 結點時,路徑長度纔會加1。


下圖顯示了2-3樹的平衡性,對於升序插入10個鍵,二叉查找樹會變成高度爲9,而2-3 樹的高度則爲2 。



二、紅黑二叉查找樹 (Red Black BST)

紅黑樹的紅連接(red link) 將兩個2- 結點鏈接起來構成一個3- 結點,黑連接則是2-3 樹的普通連接。紅黑樹知足如下條件:

  • 紅連接均爲左連接
  • 沒有任何一個結點同時和兩條紅連接相連
  • 樹是完美黑色平衡的,即任意空連接到根結點的路徑上的黑連接的數量相同




顏色表示

這裏約定空連接爲黑色。



旋轉


各類插入操做


全部插入步驟均可以歸結爲一下三種操做:

  • 若是右子結點是紅色而左子結點是黑色,進行左旋轉。
  • 若是左子結點是紅色且它的子結點也是紅色,進行右旋轉。
  • 若是左右子結點均爲紅色,則進行顏色變換。

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class RedBlackBST<Key extends Comparable<Key>, Value>
{
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private Node root;

    private class Node
    {
        private Key key;
        private Value val;
        private Node left, right;
        private boolean color;
        private int size;

        public Node(Key key, Value val, boolean color, int size)
        {
            this.key = key;
            this.val = val;
            this.color = color;
            this.size = size;
        }
    }

    private boolean isRed(Node x)
    {
        if (x == null) return false;
        return x.color == RED;
    }

    private int size(Node x)
    {
        if (x == null) return 0;
        return x.size;
    }

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

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

    public Value get(Key key)
    { return get(root, key); }

    private Value get(Node x, Key key)
    {
        while (x != null)
        {
            int cmp = key.compareTo(x.key);
            if      (cmp < 0) x = x.left;
            else if (cmp > 0) x = x.right;
            else              return x.val;
        }
        return null;
    }

    public void put(Key key, Value val)
    {
        root = put(root, key, val);
        root.color = BLACK;
    }

    private Node put(Node h, Key key, Value val)
    {
        if (h == null) return new Node(key, val, RED, 1);

        int cmp = key.compareTo(h.key);
        if      (cmp < 0) h.left = put(h.left, key, val);
        else if (cmp > 0) h.right = put(h.right, key, val);
        else              h.val = val;

        if (isRed(h.right) && !isRed(h.left))       h = rotateLeft(h);
        if (isRed(h.left)  && isRed(h.left.left))   h = rotateRight(h);
        if (isRed(h.left)  && isRed(h.right))       flipColors(h);
        h.size = size(h.left) + size(h.right) + 1;

        return h;
    }
    
    public void deleteMin()
    {
        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;

        root = deleteMin(root);
        if (!isEmpty()) root.color = BLACK;
    }

    private Node deleteMin(Node h)
    {
        if (h.left == null) return null;

        if (!isRed(h.left) && !isRed(h.left.left))
            h = moveRedLeft(h);

        h.left = deleteMin(h.left);
        return balance(h);
    }

    private Node rotateLeft(Node h)
    {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = x.left.color;
        x.left.color = RED;
        x.size = h.size;
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }

    private Node rotateRight(Node h) {
        // assert (h != null) && isRed(h.left);
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = x.right.color;
        x.right.color = RED;
        x.size = h.size;
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }

    public Key min() {
        return min(root).key;
    } 

    // the smallest key in subtree rooted at x; null if no such key
    private Node min(Node x) { 
        // assert x != null;
        if (x.left == null) return x; 
        else                return min(x.left); 
    } 

 
    public Key max() {
        return max(root).key;
    } 

    private Node max(Node x) { 
        if (x.right == null) return x; 
        else                 return max(x.right); 
    } 


    private void flipColors(Node h)
    {
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
    }

    private Node moveRedLeft(Node h)
    {
        flipColors(h);
        if (isRed(h.right.left))
        {
            h.right = rotateRight(h.right);
            h = rotateLeft(h);
            flipColors(h);
        }
        return h;
    }

    private Node balance(Node h)
    {
        if (isRed(h.right))             h = rotateLeft(h);
        if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
        if (isRed(h.left) && isRed(h.right)) flipColors(h);

        h.size = size(h.left) + size(h.right) + 1;
        return h;
    }

    public Iterable<Key> keys()
    {
        if (isEmpty()) return new Queue<Key>();
        return keys(min(), max());
    }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        keys(root, queue, lo, hi);
        return queue;
    }

    private void keys(Node x, Queue<Key> queue, Key lo, Key hi)
    {
        if (x == null) return;
        int cmplo = lo.compareTo(x.key);
        int cmphi = hi.compareTo(x.key);
        if (cmplo < 0) keys(x.left, queue, lo, hi);
        if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
        if (cmphi > 0) keys(x.right, queue, lo, hi);
    }

    public static void main(String[] args)
    {
        RedBlackBST<String, Integer> st = new RedBlackBST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }    
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
        StdOut.println(); 
    }
}

/*******************************
*  % more tinyST.txt
 *  S E A R C H E X A M P L E
 *  
 *  % java RedBlackBST < tinyST.txt
 *  A 8
 *  C 4
 *  E 12
 *  H 5
 *  L 11
 *  M 9
 *  P 10
 *  R 3
 *  S 0
 *  X 7
 *******************************/

紅黑樹的性質

  • 全部基於紅黑樹的符號表實現都能保證操做的運行時間爲對數級別。
  • 一顆大小爲N的紅黑樹的高度不會超過 \(2lgN\)。紅黑樹的最壞狀況是它所對應的2-3 樹最左邊的路徑結點均爲3-結點而其他均爲2-結點,這樣最左邊的路徑長度是隻包含2- 結點的路徑長度(~ \(lgN\))的兩倍。


各符號表實現的比較






3.4 散列表 (Hash Table)

散列表(Hash table,也叫哈希表),是根據經過鍵(key) 直接進行訪問值 (value)的數據結構。也就是說,它經過把鍵映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。

給定表M,存在函數f(key),對任意給定的鍵 (key),代入函數後若能獲得包含該關鍵字的記錄在表中的地址(即索引),則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。


散列函數

若是有一個可以保存M個鍵值對的數組,就須要一個能將任意鍵轉化爲該數組範圍內的索引([0, M-1])的散列函數。散列函數應具備一下特色:

  • 一致性 —— 等價的鍵必然產生相等的散列值
  • 高效性 —— 計算簡便
  • 均勻性 —— 對於任意鍵映射到每一個索引都是等機率的



將整數散列的最經常使用方法是除留餘數法(modular hashing)。選擇大小爲素數M的數組,對於任意正整數k,計算k除以M的餘數。若是M不是素數,散列值可能沒法均勻分佈。


Java中全部數據類型都繼承一個 hashCode() 方法,能返回一個32位整數。每一種數據類型的hashCode()方法必須與equals()是一致的。若是 a.equals(b),那麼a.hashCode()的返回值與b.hashCode()的返回值必須是一致的。若是兩個對象的hashCode()方法的返回值不一樣,那麼這兩個對象是不一樣的。若是兩個對象的hashCode()方法的返回值相同,這兩個對象也可能不一樣,還須要equals()方法判斷。


hashCode() 的返回值轉化爲一個數組索引: 由於須要的是數組索引而不是一個32位整數,所以在實現中會將默認的hashCode()方法和除留餘數法結合起來產生一個0到M-1的整數:

private int hash(Key x)
{ return (x.hashCode() & 0x7fffffff) % M; }

這段代碼會將符號位(sign bit)屏蔽(將一個32位整數變爲一個31位非負整數),而後計算它除以M的餘數。



基於拉鍊法(separate chaining) 的散列表

一個散列函數能將鍵轉化爲數組索引,散列算法的第二步是碰撞處理(collision resolution),也就是處理兩個或多個鍵的散列值相同的狀況。拉鍊法將大小爲M的數組中的每一個元素指向一條鏈表,鏈表的每一個結點存儲了一些鍵值對,同一條鏈表中鍵的散列值都同樣,爲該鏈表在散列表中的索引,以下圖所示:

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.SequentialSearchST;
import edu.princeton.cs.algs4.Queue;

public class SeparateChainingHashST<Key, Value>
{
    private static final int INIT_CAPACITY = 4;
    private int N;
    private int M;
    private SequentialSearchST<Key, Value>[] st;

    public SeparateChainingHashST()
    { this(INIT_CAPACITY); }

    public SeparateChainingHashST(int M)
    {
        this.M = M;
        st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[M];
        for (int i = 0; i < M; i++)
            st[i] = new SequentialSearchST<Key, Value>();
    }

    private void resize(int chains)
    {
        SeparateChainingHashST<Key, Value> temp = new SeparateChainingHashST<Key, Value>(chains);
        for (int i = 0; i < M; i++)
        {
            for (Key key : st[i].keys())
            { temp.put(key, st[i].get(key)); }
        }
        this.M = temp.M;
        this.N = temp.N;
        this.st = temp.st;
    }

    private int hash(Key key)
    { return (key.hashCode() & 0x7fffffff) % M; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        int i = hash(key);
        return st[i].get(key);
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        {
            delete(key);
            return;
        }

        if (n >= 10*M) resize(2*M);

        int i = hash(key);
        if (!st[i].contains(key)) N++;
        st[i].put(key, val);
    }

    public void delete(Key key)
    {
        int i = hash(key);
        if (st[i].contains(key)) N--;
        st[i].delete(key);

        if (M > INIT_CAPACITY && N <= 2*M) resize(M/2);
    }

    public Iterable<Key> keys()
    {
        Queue<Key> queue = new Queue<Key>();
        for (int i = 0; i < M; i++)
        {
            for (Key key : st[i].keys())
                queue.enqueue(key);
        }
        return queue;
    }

    public static void main(String[] args)
    {
        SeparateChainingHashST<String, Integer> st = new SeparateChainingHashST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }

        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


  • 拉鍊法中鏈表的平均長度爲\(N/M\) ,一張含有M條鏈表和N個鍵的散列表中,查找和插入操做所需的比較次數爲 \(\sim N/M\) ,這樣就比 sequential search 快了M倍。
  • 散列表的大小: M若是太大,則有存在許多空鏈表浪費內存;若是過小則鏈表太長而浪費查找時間。典型的作法是選擇 \(M \sim N / 5\)
  • 散列最主要的目的是將鍵均勻地散佈開來,所以計算散列後鍵的順序信息就消失了,所以不大適合實現有序方法。在鍵的順序不是特別重要的應用中,拉鍊法多是最快的(也是應用最普遍) 的符號表實現。



基於線性探測法 (linear probing) 的散列表

實現散列表的另外一種方式是用大小爲M的數組保存N個鍵值對,其中M > N。咱們須要依靠數組中的空位來解決碰撞衝突,基於這種策略的全部方法統稱爲開放地址(open addressing) 散列表。

線性探測法是開放地址散列表的一種。先使用散列函數鍵在數組中的索引,檢查其中的鍵和被查找的鍵是否相同。若是不一樣則繼續查找 (將索引擴大,到達數組結尾時返回數組的開頭),知道找到該鍵或遇到一個空元素。

開放地址類散列表的核心思想是將內存做爲散列表中的空元素,而不是將其當作鏈表,這些空元素能夠做爲查找結束的標誌。下列實現中使用了並行數組,一條保存鍵,一條保存值。

刪除操做:如何線性探測表中刪除一個鍵? 直接將該鍵所在的位置設爲null是不行的,由於這會使得以後的元素沒法被找到。可行的方法是將簇中被刪除鍵右側全部的鍵從新插入散列表。

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class LinearProbingHashST<Key, Value>
{
    private static final int INIT_CAPACITY = 4;
    private int N;
    private int M;
    private Key[] keys;
    private Value[] vals;

    public LinearProbingHashST()
    { this(INIT_CAPACITY); }

    public LinearProbingHashST(int capacity)
    {
        M = capacity;
        N = 0;
        keys = (Key[])   new Object[M];
        vals = (Value[]) new Object[M];
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    private int hash(Key key)
    { return (key.hashCode() & 0x7fffffff) % M; }

    private void resize(int capacity)
    {
        LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
        for (int i = 0; i < m; i++)
        {
            if (keys[i] != null)
            { temp.put(keys[i], vals[i]); }
        }
        keys = temp.keys;
        vals = temp.vals;
        M    = temp.M;
    }

    public void put(Key key, Value val)
    {
        if (N >= M/2) resize(2*M);
        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % M)
        {
            if (keys[i].equals(key))
            {
                vals[i] = val;
                return;
            }
        }
        keys[i] = key;
        vals[i] = val;
        n++;
    }

    public Value get(Key key)
    {
        for (int i = hash(key); keys[i] != null; i = (i + 1) % M)
        {
            if (keys[i].equals(key)) return vals[i];    
        }
        return null;
    }

    public Iterable<Key> keys() {
        Queue<Key> queue = new Queue<Key>();
        for (int i = 0; i < m; i++)
            if (keys[i] != null) queue.enqueue(keys[i]);
        return queue;
    }

    public static void main(String[] args) { 
        LinearProbingHashST<String, Integer> st = new LinearProbingHashST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++) {
            String key = StdIn.readString();
            st.put(key, i);
        }

        // print keys
        for (String s : st.keys()) 
            StdOut.println(s + " " + st.get(s)); 
    }
}



散列表和平衡查找樹的比較:

  • Java中的java.util.TreeMapjava.util.HashMap分別是基於紅黑樹和拉鍊法的散列表的符號表實現。
  • 相對於二叉查找樹,散列表的優勢在於代碼更簡單,且查找時間最優。二叉查找樹的優勢是抽象結構更簡單(不須要設計散列函數),紅黑樹能夠保證在最壞狀況下的性能且可以支持的操做更多(如排名、選擇、排序和範圍查找)。 大多數時候的第一選擇是散列表,在其餘元素更重要時纔會選擇紅黑樹。


各類符號表實現的比較 2:

/

相關文章
相關標籤/搜索