包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全。
一、
场景与目标
在使用 IDE 开发软件时, IDE 会提供一种“智能提示”, 根据所输入的字符列出可能的词组; 在日常Web开发中,根据用户输入进行自动提示和补全,也能很好地改善使用体验。本文实现输入自动提示与补全功能。
输入自动补全功能实际上是“前缀匹配问题”, 即给定一个前缀以及一个单词列表, 找出所有包含该前缀的单词。
本文实现的功能是: 根据用户输入的关键字, 给出与之匹配的 Java 关键字。
二、 算法与设计
最简单直观的方案莫过于直接遍历单词列表, 检测每个单词是否包含前缀, 并返回。这样做的缺点是, 每次都要遍历单词列表, 效率非常低下。 一个更好的思路是, 先构建一个前缀匹配映射 Map>, key 是每一个单词中所包含的前缀, value 是包含该 key 的所有单词列表。 那么, 问题就转化为给定一个单词列表 list, 将其转换为 Map> , 这里 Word, Prefix, Matcher
均为 String 类型。
一种思路是, 遍历每一个单词包含的每一个前缀, 找出所有包含该前缀的单词。
for word in words
for prefix in word(0,i)
for word in words
if (word.startWith(prefix)) {
result.put(prefix, result.get(prefix).add(word));
}
显然, 其效率是 O(总前缀数*总单词数), 在单词列表比较大的情况下, 其效率是比较低的。 要想避免这种嵌套遍历, 就必须充分利用每一次遍历,获取充分的信息。
另一种思路是, 先找出每个单词中所包含的前缀匹配对, 再将这些前缀匹配对合并为最终的前缀匹配映射。 类似 Map - Reduce 方式。
for word in words
for prefix in word(0,i)
pairs.add(new Pair(prefix, word))
mergePairs(pairs)
其效率是O(总的前缀数)。
三、 代码设计与实现
下面给出代码设计与实现。 注意到, 这是通过多次小步重构达到的结果, 而不是一次性实现。 具体是, 先写出一个最简单的实现, 可以把应用跑起来; 然后, 思考更有效率的实现, 最后进行了抽象。
1. 定义接口
packageautocomplete;importjava.util.Set;public interfacePrefixMatcher {
SetobtainMatchedWords(String inputText);
}
2. 定义抽象类
1 packageautocomplete;2
3 importjava.util.Collections;4 importjava.util.HashMap;5 importjava.util.HashSet;6 importjava.util.Map;7 importjava.util.Set;8
9 public abstract class AbstractPrefixMatcher implementsPrefixMatcher {10
11 protected final String[] javaKeywords = newString[] {12 "abstract", "assert",13 "boolean", "break", "byte",14 "case", "catch", "char", "class", "const", "continue",15 "default", "do", "double",16 "else", "enum", "extends",17 "final", "finally", "float", "for",18 "goto",19 "if", "implements", "import", "instanceof", "int", "interface",20 "long",21 "native", "new",22 "package", "private", "protected", "public",23 "return",24 "strictfp", "short", "static", "super", "switch", "synchronized",25 "this", "throw", "throws", "transient", "try",26 "void", "volatile",27 "while"
28 };29
30 protected Map> prefixMatchers = new HashMap>();31
32 abstract voiddynamicAddNew(String inputText);33
34 public SetobtainMatchedWords(String inputText) {35 Set matchers =prefixMatchers.get(inputText);36 if (matchers == null) {37 Set input = new HashSet();38 input.add(inputText);39 dynamicAddNew(inputText);40 returninput;41 }42 returnmatchers;43 }44
45 protected Map>obtainPrefixMatchers() {46 returnCollections.unmodifiableMap(prefixMatchers);47 }48
49 }
3. 简单的实现
packageautocomplete;importjava.util.HashMap;importjava.util.HashSet;importjava.util.Map;importjava.util.Set;public class SimpleWordMatcher extendsAbstractPrefixMatcher {publicSimpleWordMatcher() {
prefixMatchers=buildPrefixMatchers(javaKeywords);
}/*** 将输入的单词组转化为前缀匹配的映射
*@paramkeywords
*@return*
* eg. {"abc", "acd", "bcd"} ===>
* {"a": ["abc", "acd"], "ab": ["abc"], "abc": ["abc"],
* "ac": ["acd"], "acd": ["acd"], "b": ["bcd"], "bc": ["bcd"], "bcd": ["bcd"]
* }*/
public Map>buildPrefixMatchers(String[] keywords) {
HashMap> prefixMatchers = new HashMap>();for(String keyword: keywords) {int wordLen =keyword.length();for (int i=1; i < wordLen; i++) {
String prefix= keyword.substring(0, i);for(String keyword2: javaKeywords) {if(keyword2.startsWith(prefix)) {
Set matchers =prefixMatchers.get(prefix);if (matchers == null) {
matchers= new HashSet();
}
matchers.add(keyword2);
prefixMatchers.put(prefix, matchers);
}
}
}
}returnprefixMatchers;
}public static voidmain(String[] args) {
SimpleWordMatcher wordMatcher= newSimpleWordMatcher();
MapUtil.printMap(wordMatcher.obtainPrefixMatchers());
String[] prefixes= new String[] {"a", "b", "c", "d", "e", "f", "g", "i","l", "n", "p", "r", "s", "t", "v", "w", "do", "finally"};for(String prefix: prefixes) {
System.out.println(wordMatcher.obtainMatchedWords(prefix));
}
}
@OverridevoiddynamicAddNew(String inputText) {
}
}
4. 性能更好的实现
packageautocomplete;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.HashSet;importjava.util.Iterator;importjava.util.List;importjava.util.Map;importjava.util.Set;public class EffectiveWordMatcher extendsAbstractPrefixMatcher {publicEffectiveWordMatcher() {
prefixMatchers=buildPrefixMatchers(javaKeywords);
}static classPair {privateString key;privateString value;publicPair(String key, String value) {this.key =key;this.value =value;
}publicString getKey() {returnkey;
}publicString getValue() {returnvalue;
}publicString toString() {return "";
}
}private Map>buildPrefixMatchers(String[] javakeywords) {
List pairs =strarr2pairs(javakeywords);returnmergePairs(pairs);
}/** 将 字符串数组转化为前缀匹配对
* eg. ["ab", "ac"] ===>
* [, , , ]*/
private Liststrarr2pairs(String[] javakeywords) {
List pairs = new ArrayList();for(String keyword: javakeywords) {int wordLen =keyword.length();for (int i=1; i < wordLen; i++) {
String prefix= keyword.substring(0, i);
Pair pair= newPair(prefix, keyword);
pairs.add(pair);
}
}returnpairs;
}/** 将多个 合并为一个映射
* eg. [, , , , ] ===>
* {"a"=>["abstract", "assert", "b"=>["boolean", "break"], "c"=>["continue"]}*/
private static Map> mergePairs(Listpairs) {
Map> result = new HashMap>();if (pairs != null && pairs.size() > 0) {for(Pair pair: pairs) {
String key=pair.getKey();
String value=pair.getValue();
Set matchers =result.get(key);if (matchers == null) {
matchers= new HashSet();
}
matchers.add(value);
result.put(key, matchers);
}
}returnresult;
}
@OverridevoiddynamicAddNew(String inputText) {if(checkValid(inputText)) {
List newpairs = strarr2pairs(newString[] {inputText});
Map> newPreixMatchers =mergePairs(newpairs);
mergeMap(newPreixMatchers, prefixMatchers);
}
}private booleancheckValid(String inputText) {return false;
}private Map> mergeMap(Map> src, Map>dest) {
Set>> mapEntries =src.entrySet();
Iterator>> iter =mapEntries.iterator();while(iter.hasNext()) {
Map.Entry> entry =iter.next();
String key=entry.getKey();
Set newMatchers =entry.getValue();if(dest.containsKey(key)) {
dest.get(key).addAll(newMatchers);
}else{
dest.put(key, newMatchers);
}
}returndest;
}public static voidmain(String[] args) {
EffectiveWordMatcher wordMatcher= newEffectiveWordMatcher();
MapUtil.printMap(wordMatcher.obtainPrefixMatchers());
String[] prefixes= new String[] {"a", "b", "c", "d", "e", "f", "g", "i","l", "n", "p", "r", "s", "t", "v", "w", "do", "finally"};for(String prefix: prefixes) {
System.out.println(wordMatcher.obtainMatchedWords(prefix));
}
}
}
5. Servlet 使用
packageservlets;importjava.io.IOException;importjava.util.Set;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importautocomplete.EffectiveWordMatcher;importautocomplete.PrefixMatcher;public class AutoCompleteServlet extendsHttpServlet {protected PrefixMatcher wordMatcher = newEffectiveWordMatcher();public voiddoGet(HttpServletRequest req, HttpServletResponse resp)throwsServletException, IOException {
doPost(req, resp);
}public voiddoPost(HttpServletRequest req, HttpServletResponse resp)throwsServletException, IOException {
resp.setContentType("text/plain;charset=UTF8");
String inputText= req.getParameter("inputText");
Set matchers =wordMatcher.obtainMatchedWords(inputText);
StringBuilder sb= newStringBuilder();for(String m: matchers) {
sb.append(m);
sb.append(' ');
}
sb.deleteCharAt(sb.length()-1);
resp.getWriter().print(sb.toString());
}
}
6. 前端交互
输入自动补全功能演示$.ajax(
{
url:'servlets/AutoCompleteServlet',
data: {'inputText': inputText },
dataType:'text',
timeout:10000,
success:function(data) {if(keycode== 13) {//Enter
$('#inputText').val($('#matchedKeywords').val());
$('#resultRegion').empty();return;
}if(keycode== 38 ||keycode== 40) {//上下箭头
$('#matchedKeywords').trigger('focus');return;
}
$('#resultRegion').empty();varmatchers=data.split(' ');if(matchers.length> 0 &&inputText!= '') {
$('#resultRegion').append('')
$('#matchedKeywords').append('' + '' + '');for(i=0; i
$('#matchedKeywords').append('' +keyword+ '');
}
$('#matchedKeywords').attr('size', matchers.length+1);
$('#matchedKeywords').height(20*(matchers.length+1));
$('#matchedKeywords').click(function() {
$('#inputText').val($('#matchedKeywords').val());
$('#resultRegion').empty();
});
}
}
}
);
}
$(this).bind("keyup",function(eventObj) {varkeycode=eventObj.which;
searchMatchers(keycode);
});
});
#main{margin:15% 20% 0% 25%;
}#inputText{width:450px;height:30px;
}#matchedKeywords{width:450px;height:25px;
}#resultRegion{text-align:left;margin:0 0 0 128px;
}