您的位置:首页 > 其它

输入自动提示与补全功能的设计与实现

2014-02-11 17:46 603 查看
工程示例下载地址: http://download.csdn.net/download/shuqin1984/6913555

包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全。

一、
场景与目标


在使用 IDE 开发软件时, IDE 会提供一种“智能提示”, 根据所输入的字符列出可能的词组; 在日常Web开发中,根据用户输入进行自动提示和补全,也能很好地改善使用体验。本文实现输入自动提示与补全功能。

输入自动补全功能实际上是“前缀匹配问题”, 即给定一个前缀以及一个单词列表, 找出所有包含该前缀的单词。

本文实现的功能是: 根据用户输入的关键字, 给出与之匹配的 Java 关键字。

二、 算法与设计

最简单直观的方案莫过于直接遍历单词列表, 检测每个单词是否包含前缀, 并返回。这样做的缺点是, 每次都要遍历单词列表, 效率非常低下。 一个更好的思路是, 先构建一个前缀匹配映射 Map<Prefix, List<Matcher>>, key 是每一个单词中所包含的前缀, value 是包含该 key 的所有单词列表。 那么, 问题就转化为给定一个单词列表 list<Word>, 将其转换为 Map<Prefix, List<Matcher>> , 这里 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. 定义接口

package autocomplete;

import java.util.Set;

public interface PrefixMatcher {

Set<String> obtainMatchedWords(String inputText);
}


2. 定义抽象类

package autocomplete;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public abstract class AbstractPrefixMatcher implements PrefixMatcher {

protected final String[] javaKeywords = new String[] {
"abstract", "assert",
"boolean", "break",    "byte",
"case", "catch", "char", "class", "const",    "continue",
"default",    "do", "double",
"else", "enum",    "extends",
"final", "finally",    "float", "for",
"goto",
"if", "implements",    "import", "instanceof", "int",  "interface",
"long",
"native", "new",
"package",    "private",    "protected",  "public",
"return",
"strictfp",    "short", "static", "super",    "switch",  "synchronized",
"this",    "throw", "throws", "transient", "try",
"void",    "volatile",
"while"
};

protected Map<String, Set<String>> prefixMatchers = new HashMap<String, Set<String>>();

abstract void dynamicAddNew(String inputText);

public Set<String> obtainMatchedWords(String inputText) {
Set<String> matchers = prefixMatchers.get(inputText);
if (matchers == null) {
Set<String> input = new HashSet<String>();
input.add(inputText);
dynamicAddNew(inputText);
return input;
}
return matchers;
}

protected Map<String, Set<String>> obtainPrefixMatchers() {
return Collections.unmodifiableMap(prefixMatchers);
}

}


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<String, Set<String>> buildPrefixMatchers(String[] keywords) {
HashMap<String, Set<String>> prefixMatchers = new HashMap<String, Set<String>>();

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<String> matchers = prefixMatchers.get(prefix);
if (matchers == null) {
matchers = new HashSet<String>();
}
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<String, Set<String>> buildPrefixMatchers(String[] javakeywords) {
List<Pair> pairs = strarr2pairs(javakeywords);
return mergePairs(pairs);
}

/*
* 将 字符串数组转化为前缀匹配对
* eg. ["ab", "ac"] ===>
*     [<"a","ab">, <"ab", "ab">, <"a", "ac">, <"ac", "ac">]
*/
private List<Pair> strarr2pairs(String[] javakeywords) {
List<Pair> pairs = new ArrayList<Pair>();
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;
}

/*
* 将多个 <key,value> 合并为一个映射
* eg. [<"a", "abstract">, <"b", "boolean">, <"a", "assert">, <"b", "break">, <"c", "continue">] ===>
*     {"a"=>["abstract", "assert", "b"=>["boolean", "break"], "c"=>["continue"]}
*/
private static Map<String, Set<String>> mergePairs(List<Pair> pairs) {
Map<String, Set<String>> result = new HashMap<String, Set<String>>();
if (pairs != null && pairs.size() > 0) {
for (Pair pair: pairs) {
String key = pair.getKey();
String value = pair.getValue();
Set<String> matchers = result.get(key);
if (matchers == null) {
matchers = new HashSet<String>();
}
matchers.add(value);
result.put(key, matchers);
}
}
return result;
}

@Override
void dynamicAddNew(String inputText) {
if (checkValid(inputText)) {
List<Pair> newpairs = strarr2pairs(new String[] {inputText});
Map<String, Set<String>> newPreixMatchers = mergePairs(newpairs);
mergeMap(newPreixMatchers, prefixMatchers);
}
}

private boolean checkValid(String inputText) {
return false;
}

private Map<String, Set<String>> mergeMap(Map<String, Set<String>> src, Map<String, Set<String>> dest) {
Set<Map.Entry<String, Set<String>>> mapEntries = src.entrySet();
Iterator<Map.Entry<String, Set<String>>> iter = mapEntries.iterator();
while (iter.hasNext()) {
Map.Entry<String, Set<String>> entry = iter.next();
String key = entry.getKey();
Set<String> newMatchers = entry.getValue();
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<String> matchers = wordMatcher.obtainMatchedWords(inputText);
StringBuilder sb = new StringBuilder();
for (String m: matchers) {
sb.append(m);
sb.append(' ');
}
sb.deleteCharAt(sb.length()-1);
resp.getWriter().print(sb.toString());
}

}


6. 前端交互

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>输入自动补全功能演示</title>
<script type="text/javascript" src="jquery-1.10.2.min.js"></script>
<script>

$(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('<select id="matchedKeywords"></select>')
$('#matchedKeywords').append('<option value="' + '' + '">' + '' + '</option>');
for (i=0; i<matchers.length; i++) {
var keyword = matchers[i];
$('#matchedKeywords').append('<option value="' + keyword + '">' + keyword + '</option>');
}
$('#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);
});
});

</script>
<style type="text/css">

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

</style>
</head>
<body>
<center>
<div id="main">
<h3> 输入自动补全功能演示,请输入Java关键字: </h3>
<input type="text" name="inputText" id="inputText" value=""/><br/>
<div id="resultRegion"></div>
</div>
</center>
</body>
</html>


四、 效果图



五、 小结
在 EffectiveWordMatcher 中还可以支持根据用户输入动态添加关键字的功能, 但由于涉及到并发问题, 暂时没有做更多深入。 读者可以阅读源码自行完善, 我也会给出后续思考。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: