第五章 字符串(二) - 《算法》读书笔记

第五章 字符串(二)

5.2 单词查找树

  • API中添加了以下三个方法:
public class StringST<Value>
String longestPrefixOf(String s) s的前缀中最长的键
Iterable<String> keysWithPrefix(String s) 所有以s为前缀的键
Iterable<String> keysTharMatch(String s) 所有和s匹配的键(其中"."能够匹配任意字符)

5.2.1 单词查找树

5.2.1.1 基本性质

  • 单词查找树是由链接结点所组成的数据结构,这些链接可能为空

  • 每个结点都含有R条链接,其中R为字母表的大小

  • 我们将每个键所关联的值保存在该键的最后一个字母所对应的结点中

  • 值为空的链接在符号表中没有对应的键,它们的存在是为了简化单词查找树中的查找操作

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

  • 从根结点开始检查某条路径上的所有结点,可能会遇到三种情况:
    • 键的尾字符所对应的结点中的值非空:这是一次命中的查找,键所对应的值就是键的尾字符所对应的结点中保存的值
    • 键的尾字符所对应的结点中的值为空:这是一次未命中的查找,符号表中不存在被查找的键
    • 查找结束于一条空链接:这也是一次未命中的查找

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

  • 在插入之前进行一次查找,可能会出现两种情况:
    • 在到达键的尾字符之前就遇到了一个空链接:在这种情况下,单词查找树中不存在与键的尾字符对应的结点,因此需要为键中还未被检查的每个字符创建一个对应的结点并将键的值保存到最后一个字符的结点中
    • 在遇到空链接之前就到达了键的尾字符:在这种情况下,将该结点的值设为键所对应的值(无论该值是否为空)

5.2.1.4 结点的表示

  • R向单词查找树:基于含有R个字符的字母表的单词查找树,具体实现如下:
public class TrieST<Value>{
    private static int R = 256;
    private Node root;
    
    private static class Node{
        private Object val; //Java不支持泛型数组,因此必须使用Object
        private Node[] next = new Node[R];
    }
    
    private 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);
    }
    
    private void put(String key, Value val){
        root = put(root, key, val, 0);
    }
    private 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;
    }
}

5.2.1.6 查找所有键

  • 使用collect方法将当前结点下的所有键加入队列中
  • keysWithPrefix则是先get前缀所在的结点,再调用collect方法
  • 查找所有键的keys方法就是查找前缀为空的所有键
public Iterable<String> keys(){
    return keysWithPrefix("");
}
public Iterable<String> keysWithPrefix(String pre){
    Queue<String> q = new Queue<String>();
    collect(get(root, pre, 0), String pre, Queue<String> 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);
}

5.2.1.7 通配符匹配

  • 如果模式中含有通配符,collect方法中就需要递归调用处理所有的链接,否则就只需要处理模式中指定字符的链接即可

5.2.1.8 最长前缀

  • 在查找的时候记录路径上所找到的最长键的长度(将它作为递归方法的参数,在遇到值非空的结点时更新它)

5.2.1.9 删除操作

  • 找到键所对应的结点,并将它的值设置为空
  • 如果它的所有链接均为空,那就需要从数据结构中删去这个结点
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;
}

5.2.2 单词查找树的性质

单词查找树的链表结构(形状)和键插入或删除顺序无关;对于任意给定的一组键,其单词查找树都是唯一的。

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

在单词查找树中查找一个键或是插入一个键时,访问数组的次数最多为键的长度加1。

  • 访问数组的次数和符号表中键的数量无关

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

字母表的大小为R,在一棵由N个随机键构造的单词查找树中,未命中查找平均所需检查的结点数量为~logRN

  • 查找未命中的成本与键的长度无关

5.2.2.3 空间

一棵单词查找树中的链表总数在RN到RNw之间,其中w为键的平均长度。

5.2.2.4 单向分支

  • 长键在单词查找树中通常有一条长长的尾巴,占用了大量的空间,因此不要使用上述算法处理来自于大型字母表的大量长键

5.2.3 三向单词查找树

  • 三向单词查找树(TST):每个结点都含有一个字符、三条链接和一个值,这三条链接分别对应着当前字母小于、等于和大于结点字母的所有键
  • 字符显式地保存在结点中,只有沿着中键链接前进时才会根据字符找到表中的键

5.2.4 三向单词查找树的性质

5.2.4.1 空间

由N个平均长度为w的字符串构造的三向单词查找树中的链接总数在3N到3Nw之间。

5.2.4.2 查找成本

在一棵由N个随机字符串构造的三向单词查找树中,查找未命中的平均需要比较字符~lnN次。除lnN此外,一次插入或命中的查找会比较一次被查找的键中的每个字符。

5.2.4.7 单向分支

  • 通过将键的尾字母变为叶子结点,并在内部结点中消除单向分支,来提高三向单词查找树的空间利用率

由N个随机字符串构造的根结点进行了Rt向分支,且不含有外部单向分支的三向单词查找树中,一次插入或查找操作平均需要进行约lnN-tlnR次字符比较。

posted @ 2021-02-22 23:16  一天到晚睡觉的鱼  阅读(79)  评论(0)    收藏  举报