算法 第五章 单词查找树5.2

5.2 单词查找树

我们可以利用字符串的性质进行字符串的查找,以便用于字符串作为被查找的键的一般应用程序。

具体来说,本节的算法在一般应用场景中(甚至对于巨型的符号表)都能取得以下性能:

  • 查找命中所需的时间与被查找的键的长度成正比;
  • 查找未命中只需要检查若干个字符;
image-20200824140322606.png

这份API和第三章的符号表API有以下不同:

  • 将泛型的Key类型转换为了具体的类型String。
  • 添加了三个方法:longestPrefixOf()、keysWithPrefix()和keysThatMatch().

从字符串的排序算法可以看到,指定字符串的字母表是十分重要的。对小型字母表的简单而高效的实现不适用于大型字母表,这是因为后者消耗的空间太多。

下面使用 she sells sea shells by the shore这几个键作为示例描述以下三个方法。

  • longestPrefixOf() 接受一个字符串参数并返回符号表中该字符串的前缀中最长的键,对于以上所有键,longestPrefixOf("shell")的结果是she,longestPrefixOf("shellsort")的结果是shells。
  • keysWithPrefix()接受一个字符串参数并返回符号表中所有以该字符串作为前缀的键,对于以上所有键,keysWithPrefix("she")结果是she和shells,keysWithPrefix("se")结果是sells和sea。
  • keysThatMatch()接受一个字符串参数并返回符号表中所有和该字符串匹配的键,其中参数点(‘.’)可以匹配任何字符。对于以上所有键,keysThatMatch(".he")的结果是she和the,keysThatMatch("s...")结果是she和sea。

5.2.1 单词查找树

它由字符串键中的所有字符构造而成,允许使用被查找键中的字符进行查找。

5.2.1.1 基本性质

和各种查找树一样,单词查找树也是由链接的节点所组成的数据结构,这些链接可能为空。也可能指向其他结点,称为它的父节点(只有一个节点除外,根节点,没有任何节点指向根节点)。每个节点含有R条链接,其中R为字母表的大小。单词查找树一般都含有大量的空链接,因此在绘制一颗单词查找树的时候一般会忽略空链接。尽管链接指向的是节点,但是也可以看成链接指向的是另一颗单词查找树,它的根节点就是被指向的节点。每个链接都对应着一个字符--因为每条链接都只能指向一个节点,所以可以用链接所对应的字符标记被指向的节点(根节点除外,因为没有链接指向它)。每个节点也含有一个对应的值,可以是空也可以是符号表中的某个键所关联的值。具体来说,我们将每个键所关联的值保存在该键的最后一个字母所对应的节点中。

image-20200824143102875.png

5.2.1.2 单词查找树中的查找操作

在单词查找树中查找给定字符串是一个很简单的过程,它是以被查找的键中的字符为导向的。单词查找树中的每个节点都包含了下一个可能出现的所有字符的链接。从根节点开始,首先经过的是键的首字母所对应的链接;在下一个节点中沿着第二个字符所对应的链接继续前进;在第二个节点中沿着第三个字符所对应的链接向前,如此这般直到最后一个字母所指向的节点或者遇到了一条空链接。这时可能出现下面三种情况。

  • 键的尾字符所对应的结点值非空(如图查找shells和she的示例)。这是一次命中的查找-键所对应的值就是键的卫子夫所对应的结点保存的值。
  • 键的尾字符所对应的结点中的值为空。(如查找shell)这是一次未命中的查找-符号表中不存在被查找的键
  • 查找结束于一条空链接(如查找shore)。这也是未命中的查找
image-20200824143657535.png

5.2.1.3 单词查找树中的插入操作

在插入之前要进行一次查找:在单词查找树中意味着沿着被查找的键的所有字符到达树中表示尾字符的结点或一个空链接。此时会遇到以下两种情况。

  • 在到达键尾字符之前就遇到了一个空链接。在这种情况下,单词查找树中不存在与键的尾字符对应的结点,因此需要为键中还未被检查的每个字符创建一个对应的结点并且将键的值保存到最后一个字符的结点中
  • 在遇到空链接之前就遇到了尾字符。在这种情况下,和关联数组一样,将该结点的值设为键所对应的值(无论该值是否非空)
image-20200824144117253.png

5.2.1.4 结点的表示

在开头提过,我们为单词查找树所绘出的图像和在程序中构造的数据结构并不完全一致,因为我们没有画出空链接,将空链接考虑进来将会突出单词查找树的以下重要性质:

  • 每个结点都含有R个链接,对应着每个可能出现的字符
  • 字符和键均隐式地保存在数据结构中

例如下面:所有的键都由小写字母组成,每个结点都含有一个值和26个链接。第一条链接指向的子单词查找树中的所有键的首字母都是a,第二条链接指向的子单词查找树中的所有键的首字母都是b。

image-20200824144431964.png

在单词查找树中,键是由根节点到含有非空值的结点的路径所隐式表示的。例如,sea关联的值是2,因为根节点的第19条链接(指向所有以s开头的键组成的子单词查找树)非空,下一个结点中的第5条链接(指向所有以se开头的键组成的子单词数)非空,第三个结点中的第1条链接(指向所有以sea开头的键组成的子单词查找树)的值为2。数据结构既没有保存字符串sea也没有保存字符s、e和a。事实上,数据结构没有存储任何字符串或字符,保存了链接数组和值。因为参数R的作用的重要性,所以将基于含有R个字符的字母表的单词查找树称为R向单词查找树

有了这些预备知识后,实现单词查找树就比较简单了

/**
 * 基于单词查找树的符号表
 */
public class TrieST {
    private static final int R = 256; //基数
    private Node root;              //单词查找树的根节点

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

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

    public Node get(Node x, String key, int d) {
        //返回以x作为根节点的子单词查找树中与key相关联的值
        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);
    }

    private Node put(Node x, String key, Value val, int d) {
        //如果key存在于以x为根节点的子单词查找树中则更新与它相关联的值
        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;
    }
}

5.2.1.5 大小

size()方法实现有以下三种显而易见的选择。

  • 即时实现:用一个实例变量N保存键的数量
  • 更加即时的实现:用结点的实例变量保存子单词查找树中键的数量,在递归的put()和delete()方法调用之后更新它们。
  • 延时递归实现:遍历单词查找树,返回结果
/**
     * 单词查找树的延时递归方法
     *
     * @return 返回一共有多少个键
     */
    public int size() {
        return size(root);
    }

    private int size(Node x) {
        if (x == null) return 0;
        int cnt = 0;
        if (x.val != null) cnt++;
        for (char c = 0; c < R; c++) {
            cnt += size(x.next[c]);
        }
        return cnt;
    }

5.2.1.6 查找所有键

因为字符和键是隐式地表示在单词查找树中。在单词查找树中,不仅要能够在数据结构中找到这些键,还需要显示的表示它们。我们用一个类似于size()的私有递归方法collect()来完成这个任务,它维护了一个字符串来保存从出发路径上的一系列字符。每当我们在collect()调用访问一个结点的时候,方法的第一个参数就是该结点,第二个参数就是和该结点相关联的字符串(从根节点到该结点路径上的所有字符)。在访问一个结点时,如果值为空,就将它和关联的字符串加入队列中,然后递归的访问它的链接数组所指向的所有可能的字符结点。每次调用之前,都将链接所对应的字符附加到当前键的末尾作为符号表中所有的键。用collect()方法为API中的keys()和keysWithPrefix()方法收集符号表中所有的键。要实现keys方法,可以以空字符串作为参数调用keysWithPrefix()方法。要实现KeysWithPrefix()方法,可以先调用get()找出给定前缀所对应的单词查找树(如果不存在则返回null),再使用collect()方法完成任务。

image-20200824150326540.png
image-20200824150333587.png

 /**
     * 返回所有的键
     *
     * @return 返回单词查找树中的所有的键
     */
    public Iterable keys() {
        return keysWithPrefix("");
    }

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

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

5.2.1.7 通配符匹配

我们可以用一个类似的过程实现keysThatMatch(),但需要collect()添加一个参数来指定匹配的模式。如果模式中含有通配符,就需要递归调用处理所有的链接,否则只需要考虑处理指定的字符即可。

/**
     * 通配符匹配
     *
     * @param pat 通配符
     * @return 和通配符匹配的键
     */
    public Iterable keyThatMatch(String pat) {
        Queue q = new ArrayDeque<>();
        collect(root, "", pat, q);
        return q;
    }

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

5.2.1.8 最长前缀

为了找到给定字符串的最长前缀,需要一个类似于get()的递归方法。它会记录查找路径上所找到的最长键的长度。查找会在被查找的字符串结束或者遇到空链接终止

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

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

5.2.1.9 删除操作

从单词查找树删除一个键值对的第一步是,找到所对应的结点并将它的值设为null。如果该结点含有一个非空结点,则不需要任何其他操作。如果它的所有链接均为空,则删除这个结点,如果删除它使得它的父节点也为空,则删除它的父节点,以此类推。

public void delete(String key) {
        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,key,d+1);
        }
        if(x.val!=null) return x;
        for(char c=0;c
image-20200824151450388.png

5.2.2 单词查找树的性质

image-20200824151531564.png

5.2.2.1 最坏情况下查找和插入操作的时间界限

image-20200824151615459.png

5.2.2.2 查找未命中的预期时间界限

image-20200824151649481.png

5.2.2.3 空间

image-20200824151708225.png

5.2.3 三向单词查找树

为了避免R向单词查找树过度的空间消耗,我们学习另一种数据的表示方法:三向单词查找树(TST)。在三向单词查找树中,每个结点都含有一个字符、三条链接和一个值。这三条链接分别对应着当前字母小于、等于和大于结点字母的所有键。在R向单词查找树中,树的节点含有R条链接,每个非空链接的索引隐式的表示了他所对应的字符。在等价的三向单词查找树中,字符是显示地保存在节点中--只有沿着中间链接前进的时候才会根据字符找到表中的键。

查找与插入操作

用三向单词查找树实现符号表API中的查找和插入操作很简单。在查找的时候,我们首先比较首字母和根节点的字母。如果键的首字母较小,就选择左链接;如果较大,就选择右链接;如果相等,就选择中链接。然后,递归地使用相同的算法。如果遇到了空链接或者当前结束时点的值为空,那么查找未命中。如果结束时键非空,那么查找命中

image-20200824153233700.png
image-20200824153242328.png

基于三向单词查找树的符号表

/**
 * 三向单词查找树
 */
public class TST {
    private Node root;  //树的根节点

    private class Node {
        char c;                 //字符
        Node left, mid, right;    //左中右子三向单词查找树
        Value val;              //和字符串相关联的值
    }

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

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

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

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

5.2.4 三向单词查找树的性质

5.2.4.1 空间

image-20200824153409848.png

5.2.4.2 查找成本

image-20200824153434936.png

5.2.5 应使用字符串符号表的哪种实现

image-20200824153512708.png

你可能感兴趣的:(算法 第五章 单词查找树5.2)