包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全。
工程示例下载地址: http://download.csdn.net/download/shuqin1984/6913555
包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全。
一、
场景与目标
在使用 IDE 开发软件时, IDE 会提供一种“智能提示”, 根据所输入的字符列出可能的词组; 在日常Web开发中,根据用户输入进行自动提示和补全,也能很好地改善使用体验。本文实现输入自动提示与补全功能。
输入自动补全功能实际上是“前缀匹配问题”, 即给定一个前缀以及一个单词列表, 找出所有包含该前缀的单词。
本文实现的功能是: 根据用户输入的关键字, 给出与之匹配的 Java 关键字。
二、 算法与设计
最简单直观的方案莫过于直接遍历单词列表, 检测每个单词是否包含前缀, 并返回。这样做的缺点是, 每次都要遍历单词列表, 效率非常低下。 一个更好的思路是, 先构建一个前缀匹配映射 Map
均为 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. 定义接口
package autocomplete;
import java.util.Set;
public interface PrefixMatcher {
Set
}
2. 定义抽象类
1 package autocomplete;
2
3 import java.util.Collections;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.Map;
7 import java.util.Set;
8
9 public abstract class AbstractPrefixMatcher implements PrefixMatcher {
10
11 protected final String[] javaKeywords = new String[] {
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
31
32 abstract void dynamicAddNew(String inputText);
33
34 public Set
35 Set
36 if (matchers == null) {
37 Set
38 input.add(inputText);
39 dynamicAddNew(inputText);
40 return input;
41 }
42 return matchers;
43 }
44
45 protected Map
46 return Collections.unmodifiableMap(prefixMatchers);
47 }
48
49 }
3. 简单的实现
package autocomplete;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class SimpleWordMatcher extends AbstractPrefixMatcher {
public SimpleWordMatcher() {
prefixMatchers = buildPrefixMatchers(javaKeywords);
}
/**
* 将输入的单词组转化为前缀匹配的映射
* @param keywords
* @return
*
* eg. {"abc", "acd", "bcd"} ===>
* {"a": ["abc", "acd"], "ab": ["abc"], "abc": ["abc"],
* "ac": ["acd"], "acd": ["acd"], "b": ["bcd"], "bc": ["bcd"], "bcd": ["bcd"]
* }
*/
public Map
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
if (matchers == null) {
matchers = new HashSet
}
matchers.add(keyword2);
prefixMatchers.put(prefix, matchers);
}
}
}
}
return prefixMatchers;
}
public static void main(String[] args) {
SimpleWordMatcher wordMatcher = new SimpleWordMatcher();
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));
}
}
@Override
void dynamicAddNew(String inputText) {
}
}
4. 性能更好的实现
package autocomplete;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class EffectiveWordMatcher extends AbstractPrefixMatcher {
public EffectiveWordMatcher() {
prefixMatchers = buildPrefixMatchers(javaKeywords);
}
static class Pair {
private String key;
private String value;
public Pair(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public String toString() {
return "<" + key + "," + value + ">";
}
}
private Map
List
return mergePairs(pairs);
}
/*
* 将 字符串数组转化为前缀匹配对
* eg. ["ab", "ac"] ===>
* [<"a","ab">, <"ab", "ab">, <"a", "ac">, <"ac", "ac">]
*/
private List
List
for (String keyword: javakeywords) {
int wordLen = keyword.length();
for (int i=1; i < wordLen; i++) {
String prefix = keyword.substring(0, i);
Pair pair = new Pair(prefix, keyword);
pairs.add(pair);
}
}
return pairs;
}
/*
* 将多个
* eg. [<"a", "abstract">, <"b", "boolean">, <"a", "assert">, <"b", "break">, <"c", "continue">] ===>
* {"a"=>["abstract", "assert", "b"=>["boolean", "break"], "c"=>["continue"]}
*/
private static Map
Map
if (pairs != null && pairs.size() > 0) {
for (Pair pair: pairs) {
String key = pair.getKey();
String value = pair.getValue();
Set
if (matchers == null) {
matchers = new HashSet
}
matchers.add(value);
result.put(key, matchers);
}
}
return result;
}
@Override
void dynamicAddNew(String inputText) {
if (checkValid(inputText)) {
List
Map
mergeMap(newPreixMatchers, prefixMatchers);
}
}
private boolean checkValid(String inputText) {
return false;
}
private Map
Set
Iterator
while (iter.hasNext()) {
Map.Entry
String key = entry.getKey();
Set
if (dest.containsKey(key)) {
dest.get(key).addAll(newMatchers);
}
else {
dest.put(key, newMatchers);
}
}
return dest;
}
public static void main(String[] args) {
EffectiveWordMatcher wordMatcher = new EffectiveWordMatcher();
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 使用
package servlets;
import java.io.IOException;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import autocomplete.EffectiveWordMatcher;
import autocomplete.PrefixMatcher;
public class AutoCompleteServlet extends HttpServlet {
protected PrefixMatcher wordMatcher = new EffectiveWordMatcher();
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF8");
String inputText = req.getParameter("inputText");
Set
StringBuilder sb = new StringBuilder();
for (String m: matchers) {
sb.append(m);
sb.append(' ');
}
sb.deleteCharAt(sb.length()-1);
resp.getWriter().print(sb.toString());
}
}
6. 前端交互
$(document).ready(function() {
var searchMatchers = function(keycode) {
var inputText = $('#inputText').val();
$.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();
var matchers = data.split(' ');
if (matchers.length > 0 && inputText != '') {
$('#resultRegion').append('')
$('#matchedKeywords').append('');
for (i=0; i var keyword = matchers[i]; $('#matchedKeywords').append(''); } $('#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) { var keycode = 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; }
输入自动补全功能演示,请输入Java关键字:
四、 效果图
五、 小结
在 EffectiveWordMatcher 中还可以支持根据用户输入动态添加关键字的功能, 但由于涉及到并发问题, 暂时没有做更多深入。 读者可以阅读源码自行完善, 我也会给出后续思考。
推荐阅读
发表评论