【alg4-字符串】单词查找树

单词查找树

基本性质

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

结点表示

单词查找树具有以下性质:

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

在单词查找树中,键是由从根结点到含有非空值的结点的路径所隐式表示的。数据结构不会存储任何字符串或字符,它保存了链接数组和值。因为参数R的作用的重要性,所以将基于含有R个字符的字母表的单词查找树称为R向单词查找树

基本操作

查找操作

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

  • 键的尾字符所对应的结点中的值非空。这是一次命中的查找——键所对应的值就是键的尾字符所对应的结点中保存的值。
  • 键的尾字符所对应的结点中的值为空。这是一次未命中的查找——符号表中不存在被查找的键。
  • 查找结束于一条空链接。这也是一次未命中的查找。

插入操作

在单词查找树中意味着沿着被查找的键的所有字符到达树中表示尾字符的结点或者一个空链接。此时可能会出现以下两种情况。

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

取得大小操作

size()方法的实现有以下三种选择:

  • 即时实现:用一个实例变量N保存键的数量。
  • 更加即时的实现:用结点的实例变量保存子单词查找树中键的数量,在递归的put()和delete()方法调用之后更新它们。
  • 延时递归实现:它会遍历单词查找树中的所有结点并记录非空值结点的总数。

查找所有键

在单词查找树中,递归方法collect()维护了一个字符串用来保存从根结点出发的路径上的一系列字符。每当我们在collect()调用中访问一个结点时,方法的第一个参数就是该结点,第二个参数则是和该结点相关联的字符串(从根结点到该结点的路径上的所有字符)。在访问一个结点时,如果它的值非空,我们就将和它相关联的字符串加入队列之中,然后(递归地)访问它的链接数组所指向的所有可能的字符结点。在每次调用之前,都将链接对应的字符附加到当前键的末尾作为调用的参数键。

通配符匹配

可以用类似上述的过程来实现。如果模式中含有通配符,就需要递归调用处理所有的链接,否则就只需要处理模式中指定字符的链接即可。

最长前缀

为了找到给定字符串的最长键前缀,就需要使用一个类似于get()的递归方法。它会记录查找路径上所找到的最长键的长度(将它作为递归方法的参数在遇到值非空的结点时更新它)。查找会在被查找的字符串结束或是遇到空链接时终止。

删除操作

从一个单词查找树中删去一个键值对的第一步是,找到键所对应的结点并将它的值设为空(null)。如果该结点含有一个非空的链接指向某个子结点,那么就不需要再进行其他操作了。如果它的所有链接均为空,那就需要从数据结构中删去这个结点。如果删去它使得它的父结点的所有链接也均为空,就需要继续删除它的父结点,依此类推。

性质

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

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

在单词查找树中查找一个键或是插入一个键时,访问数组的次数最多为键的长度加1。无论使用的是什么算法和数据结构,在检查完要查找的键中的所有字符之前都是无法判断是否已找到该键。从实际角度来说,这个保证也很重要,因为它和符号表中键的数量无关。

查找为命中的预期时间界限

假设我们正在单词查找树中查找一个键,发现根结点中与被查找键的第一个字符所对应的链接为空。此时只检查了一个结点就知道了该键不存在于表中。这种情况是很常见的:单词查找树的最重要的性质之一就是未命中的查找一般都只需要检查很少的几个结点。可以得出以下结论:
字母表的大小为R,在一棵由N个随机键构造的单词查找树中,未命中查找平均所需检查的结点数量为~log®(N)(R为对数的底)。
从实际角度来说,该命题说明的最重要的一点就是,查找未命中的成本与键的长度无关。

空间

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

  • 当所有键均较短时,链接的总数接近于RN;
  • 当所有键均较长时,链接的总数接近于RNw;
  • 因此,缩小R能够节省大量的空间。

不要使用这个算法处理来自于大型字母表的大量长键。它所构造的单词查找树所需要的空间与R和所有键的字符总数之积成正比。但是,如果你能够负担得起这么庞大的空间,单词查找树的性能是无可匹敌的。

实现

API

API功能
TrieST()创建一个单词查找树
void put(String key, Value val)向表中插入键值对
Value get(String key)键key所对应的值(如果键不存在则返回null)
void delete(String key)删除键key(和它的值)
boolean contains(String key)表中是否保存着key的值
boolean isEmpty()符号表是否为空
String longestPrefixOf(String s)s的前缀中最长的键
Iterable<String> keysWithPrefix(String s)所有以s为前缀的键
Iterable<String> keysThatMatch(String s)所有和s匹配的键(其中“.”能够匹配任意字符)
int size()键值对的数量
Iterable<String> keys()符号表中的所有键

代码

package section5_2;

import java.util.LinkedList;
import java.util.Queue;

public class TrieST<Value> {

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

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

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

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

    public Iterable<String> keysWithPrefix(String pre) {
        Queue<String> q = new LinkedList<>();
        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.offer(pre);
        for (char c = 0;c < R;c++) {
            collect(x.next[c],pre + c,q);
        }
    }

    public Iterable<String> keysThatMatch(String pat) {
        Queue<String> q = new LinkedList<>();
        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.offer(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);
            }
        }
    }

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

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

    public static void main(String[] args) {
        String[] a = {
                "she",
                "sells",
                "sea",
                "shells",
                "by",
                "the",
                "shore"
        };
        TrieST<Integer> trie = new TrieST<>();
        for (int i = 0;i < a.length;i++) {
            trie.put(a[i],i);
        }
        System.out.println("test get:");
        System.out.println(trie.get("shells"));
        System.out.println(trie.get("shell"));
        System.out.println("test keysWithPrefix:");
        for (String s : trie.keysWithPrefix("sh")) {
            System.out.print(s + " ");
        }
        System.out.println();
        System.out.println("test keysThatMatch:");
        for (String s : trie.keysThatMatch("s..")) {
            System.out.print(s + " ");
        }
        System.out.println();
        System.out.println("test longestPrefixOf:");
        System.out.println(trie.longestPrefixOf("shell"));
        System.out.println("test delete:");
        System.out.println("before deleting:");
        for (String s : trie.keys()) {
            System.out.print(s + " ");
        }
        System.out.println();
        trie.delete("shore");
        System.out.println("after delete:");
        for (String s : trie.keys()) {
            System.out.print(s + " ");
        }
        System.out.println();
    }
}

输出:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值