《算法》筆記 14 - 單詞查找樹

  • R向單詞查找樹
    • 數據結構
    • 查找
    • 插入
    • 查找全部鍵
    • 通配符匹配
    • 最長前綴
    • 刪除
    • R向單詞查找樹的性質
  • 三向單詞查找樹
    • 三向單詞查找樹的性質

同字符串的排序同樣,利用字符串的性質開發的查找算法也比通用的算法更有效,這些算法能夠用於在以字符串做爲被查找鍵的場合。這類算法在面對巨量的數據時,仍然能夠取得這樣的性能:查找命中所需的時間與被查找的鍵的長度成正比;而查找未命中時只需檢查若干個字符。這樣的性能是至關驚人的,也是算法研究的最高成就之一,這些算法成了建成如今可以便捷、快速地訪問海量信息所依賴的基礎設施的重要因素。node

R向單詞查找樹

數據結構

單詞查找樹(Trie)是用於字符串鍵查找的數據結構。與以前的查找樹相似,它也是由連接的結點所組成的數據結構,這些連接可能爲空,也可能指向其餘結點。
結點的數據結構爲:c++

private static class Node {
    private Object val;
    private Node[] next = new Node[R];
}

每一個節點都只有一個或0個指向它的結點(父結點),只有根結點不會有父結點。每一個節點都含有R條連接,R爲字母表的大小,若是字符都由26個小寫英文字母構成,則R爲26;若是字符屬於ASCII字符集,則R=128;DNA研究中用4個字母表示4個鹼基,R=4。
R條連接對應可能出現的字符,這其中會有大量的空連接,鍵是由從根節點到含有非空值的結點的路徑所隱式表示的。每一個結點也含有一個相應的值,能夠是空也能夠是符號表中的某個鍵所關聯的值。值爲空的結點在符號表中沒有對應的鍵,它們的存在是爲了簡化單詞查找樹中的查找操做,每一個鍵所關聯的值保存在給定鍵的最後一個字母所對應的結點中。
也將基於含有R個字符的字母表的單詞查找樹稱爲R向單詞查找樹算法

查找

在單詞查找樹中查找給定字符串鍵所對應的值時,是以被查找的鍵中的字符爲導向的。單詞查找樹中的每一個結點都包含了下一個可能出現的全部字符的連接。從根結點開始,首先通過的是鍵的首字母所對應的連接,在下一個結點沿着第二個字符所對應的連接繼續前進,以此類推,直到找到鍵的最後一個字母所指向的結點,或者遇到了一條空連接。shell

  • 若是鍵的尾字符所對應的結點中的值非空,則查找命中,鍵所對應的值就是鍵的尾字符中所存儲的值;
  • 若是鍵的尾字符所對應的結點中的值爲空,或者查找的過程當中遇到了空連接,則查找未命中。
public Value get(String key) {
    Node x = get(root, key, 0);
    if (x == null)
        return null;
    return (Value) x.val;
}

private Node get(Node x, String key, int d) {
    if (x == null)
        return null;
    if (d == key.length())
        return x;
    char c = key.charAt(d);
    return get(x.next[c], key, d + 1);
}

插入

插入的時候,也須要先進行一次查找數組

  • 若是在到達鍵的尾字符以前就遇到了一個空連接,這種狀況下單詞查找樹中不存在與鍵的尾字符對應的結點,所以須要爲鍵中還未被檢查的每一個字符建立一個對應的結點,並將鍵的值保存到最後一個字符的結點中;
  • 若是在遇到空連接以前就到達了鍵的尾字符,則將該結點的值設爲鍵所對應的值。
public void put(String key, Value val) {
    root = put(root, key, val, 0);
}

public Node put(Node x, String key, Value val, int d) {
    if (x == null)
        x = new Node();
    if (d == key.length()) {
        x.val = val;
        return x;
    }

    char c = key.charAt(d);
    x.next[c] = put(x.next[c], key, val, d + 1);
    return x;
}

查找全部鍵

單詞查找樹中的字符是被隱式地表示的,查找的時候須要顯式地將它們表示出來,並加入到隊列中。查找基於一個叫作collect的方法,它的參數中包含了一個字符串,用來保存從根結點出發的路徑上的一系列字符。每當在collect()調用中訪問一個結點時,方法的第一個參數就是這個結點,第二個參數是從根節點到這個結點的路徑上的全部字符。若是結點的值非空,就將和它相關聯的字符串加入隊列中,而後遞歸地訪問它的連接數組所指向的全部可能的字符結點。在每次調用collect以前,都將連接對應的字符附加到當前鍵的末尾做爲參數。要實現keys()方法,能夠用空字符做爲參數調用keysWithPrefix()方法。要實現keysWithPrefix(),則能夠先調用get()方法找出給定前綴所對應的單詞查找子樹,再使用collect()。數據結構

public Iterable<String> keys() {
    return keysWithPrefix("");
}

public Iterable<String> keysWithPrefix(String pre) {
    Queue<String> q = new Queue<String>();
    collect(get(root, pre, 0), pre, q);
    return q;
}

private void collect(Node x, String pre, Queue<String> q) {
    if (x == null)
        return;
    if (x.val != null)
        q.enqueue(pre);
    for (char c = 0; c < R; c++) {
        collect(x.next[c], pre + c, q);
    }
}

通配符匹配

通配符匹配的過程相似keysWithPrefix,但須要爲collect添加一個用於指定匹配模式的參數。模式中用'.'來表示通配符,若是模式中含有通配符,就須要用遞歸調用處理全部的連接,不然就只須要處理模式中指定字符的連接便可。性能

public Iterable<String> keysThatMatch(String pat) {
    Queue<String> q = new Queue<String>();
    collect(root, "", pat, q);
    return q;
}

private void collect(Node x, String pre, String pat, Queue<String> q) {
    int d = pre.length();
    if (x == null)
        return;
    if (d == pat.length() && x.val != null)
        q.enqueue(pre);
    if (d == pat.length())
        return;
    char next = pat.charAt(d);
    for (char c = 0; c < R; c++) {
        if (next == '.' || next == c)
            collect(x.next[c], pre + c, pat, q);
    }
}

最長前綴

longestPrefixOf方法會找出與給定字符串匹配的最長前綴。好比對於鍵by,she, shells,longestPrefixOf("shell")的結果爲she。要找到最長前綴,須要一個相似於get的遞歸方法來記錄查找路徑上所找到的最長鍵的長度,並在遇到值非空的結點時更新它,而後在被查找的字符串結束或者遇到空連接時終止查找。code

public String longestPrefixOf(String s) {
    int length = search(root, s, 0, 0);
    return s.substring(0, length);
}

public int search(Node x, String s, int d, int length) {
    if (x == null)
        return length;
    if (x.val != null)
        length = d;
    if (d == s.length())
        return length;
    char c = s.charAt(d);
    return search(x.next[c], s, d + 1, length);
}

刪除

要從單詞查找樹中刪除一個鍵值對,首先須要找到鍵所對應的結點並將它的值設爲空。而後分兩種狀況:排序

  • 若是這個結點還含有一個指向某個子結點的非空連接,就不須要在進行別的操做;
  • 若是這個結點的全部連接都爲空,那馬就須要從樹中刪除這個結點;若是刪除這個結點後,使得它的父結點的全部連接也都成了空,就要繼續刪除它的父結點,依次類推。
public void delete(String key) {
    root = delete(root, key, 0);
}

private Node delete(Node x, String key, int d) {
    if (x == null)
        return null;
    if (d == key.length())
        x.val = null;
    else {
        char c = key.charAt(d);
        x.next[c] = delete(x.next[c], key, d + 1);
    }

    if (x.val != null)
        return x;
    for (char c = 0; c < R; c++)
        if (x.next[c] != null)
            return x;
    return null;
}

R向單詞查找樹的性質

單詞查找樹的連接結構和鍵的插入或刪除順序無關,對於任意給定的一組鍵,它們的單詞查找樹都是惟一的,這與以前全部的其它查找樹都不相同。遞歸

在單詞查找樹中查找或插入一個鍵時,訪問數組的次數最多爲鍵的長度加1,由於get()和put()都使用了一個指示字符位置的參數d,它的初始值爲0,每次遞歸都會加1,當長度等於鍵的長度時遞歸中止,此時訪問了數組d+1,若是查找未命中,訪問次數會更少。這說明在單詞查找樹中查找一個鍵所需的時間與樹的大小無關,只與鍵的長度有關。

關於單詞查找樹佔用的空間,與樹中的連接總數有關。設w爲鍵的平均長度,R爲字符集的大小,N爲鍵的總數,則一顆單詞查找樹中的連接總數在RN到RNw之間。

  • 若是每一個鍵的首字母都不相同,那麼每一個鍵中的每一個字母都有一個結點,一個鍵含有Rw個連接,共N個鍵,含有RNw個連接;
  • 若是鍵的首字母都相同,且都處於一個分支,那麼樹將只含有這一條分支,共RN個連接。

有一些經驗性的規律:當全部鍵都較短時,連接的總數接近於RN;而當全部鍵都較長時,連接的總數接近於RNw,因此縮小R或w可以節省大量的空間。並且在實際應用中,使用單詞查找樹以前,首先了解將要插入的全部鍵的性質是很是重要的。

三向單詞查找樹

R向單詞查找樹雖然檢索速度很快,但空間佔用也很是大,尤爲是對於比較大的字符集和比較長的鍵,這將消耗很是大的空間。三向單詞查找樹可避免這個問題。
在三向單詞查找樹中,每一個節點都含有一個字符,三條連接和一個值。這三條連接分別對應着當前字母小於、等於、大於節點字母的全部鍵。只有沿着中間連接前進時纔會找到待查找的鍵。

在三向單詞查找樹中查找鍵時,首先將鍵的首字母和根結點進行比較,若是首字母較小,就選擇左連接,若是首字母較大,就選擇右連接,首字母與根節點字符相等,就選擇中連接,而後遞歸查找,直到遇到一個空連接或者當鍵結束時結點的值爲空,則查找未命中;若是在鍵結束時結點的值非空,則查找命中。

public class TST<Value> {
    private Node root;

    private class Node {
        char c;
        Node left, mid, right;
        Value val;
    }

    public Value get(String key) {
        Node node = get(root, key, 0);
        if (node == null)
            return null;
        return node.val;
    }

    private Node get(Node x, String key, int d) {
        if (x == null)
            return null;
        char c = key.charAt(d);
        if (c < x.c)
            return get(x.left, key, d);
        else if (c > x.c)
            return get(x.right, key, d);
        else if (d < key.length() - 1)
            return get(x.mid, key, d);
        else
            return x;
    }

    public void put(String key, Value val) {
        root = put(root, key, val, 0);
    }

    private Node put(Node x, String key, Value val, int d) {
        char c = key.charAt(d);
        if (c < x.c)
            x.left = put(x.left, key, val, d);
        else if (c > x.c)
            x.right = put(x.right, key, val, d);
        else if (d < key.length() - 1)
            x.mid = put(x.mid, key, val, d);
        else
            return x.val = val;
        return x;
    }

}

三向單詞查找樹的性質

三向單詞查找樹能夠看做是R向單詞查找樹的緊湊表示,但三向單詞查找樹的形狀是與鍵的插入順序有關的,並且空間佔用要比R向單詞查找樹小不少。 三向單詞查找樹的每一個結點只含有3個連接,樹的連接總數在3N到3Nw之間。

相關文章
相關標籤/搜索