同字符串的排序同样,利用字符串的性质开发的查找算法也比通用的算法更有效,这些算法能够用于在以字符串做为被查找键的场合。这类算法在面对巨量的数据时,仍然能够取得这样的性能:查找命中所需的时间与被查找的键的长度成正比;而查找未命中时只需检查若干个字符。这样的性能是至关惊人的,也是算法研究的最高成就之一,这些算法成了建成如今可以便捷、快速地访问海量信息所依赖的基础设施的重要因素。node
单词查找树(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; }
单词查找树的连接结构和键的插入或删除顺序无关,对于任意给定的一组键,它们的单词查找树都是惟一的,这与以前全部的其它查找树都不相同。递归
在单词查找树中查找或插入一个键时,访问数组的次数最多为键的长度加1,由于get()和put()都使用了一个指示字符位置的参数d,它的初始值为0,每次递归都会加1,当长度等于键的长度时递归中止,此时访问了数组d+1,若是查找未命中,访问次数会更少。这说明在单词查找树中查找一个键所需的时间与树的大小无关,只与键的长度有关。
关于单词查找树占用的空间,与树中的连接总数有关。设w为键的平均长度,R为字符集的大小,N为键的总数,则一颗单词查找树中的连接总数在RN到RNw之间。
有一些经验性的规律:当全部键都较短时,连接的总数接近于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之间。