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

标签: 功能 设计 | 发表时间:2014-02-12 01:46 | 作者:shuqin1984
出处:http://blog.csdn.net


      工程示例下载地址:  http://download.csdn.net/download/shuqin1984/6913555
      包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全。

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

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

        二、 算法与设计
       最简单直观的方案莫过于直接遍历单词列表, 检测每个单词是否包含前缀, 并返回。这样做的缺点是, 每次都要遍历单词列表, 效率非常低下。 一个更好的思路是, 先构建一个前缀匹配映射 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 中还可以支持根据用户输入动态添加关键字的功能, 但由于涉及到并发问题, 暂时没有做更多深入。 读者可以阅读源码自行完善, 我也会给出后续思考。

 
作者:shuqin1984 发表于2014-2-11 17:46:05 原文链接
阅读:100 评论:0 查看评论

相关 [功能 设计] 推荐:

用户积分功能的设计

- - 四火的唠叨
文章系本人原创,转载请保持完整性并注明出自 《四火的唠叨》. 有一个SNS应用,用户在使用的过程中积累积分,例如登陆+3点,个人空间每次浏览+1点,结交每个朋友+5点等等. 同时,很重要的一点是,用户需要看到自己的积分累计有多少,能够根据积分划分用户等级,在自己的空间展示积分. 在用户量比较大的情况下(例如超过三千万),这是一个比较典型的读写都很频繁的问题,而且写入的次数可能和读取的次数差别不大(大多数SNS应用中,读次数远超写次数的场景居多,例如用户的状态信息,更新一次以后有成千上万的访问).

怎样设计密码重设功能

- - php.js.cn
当我们设计一个带有用户注册/登录功能的网站的时候,一个必须的功能就是重设密码. 重设密码功能有很多种设计方式,比如发送一个新密码到用户邮箱等. 不过今天我要介绍一个我经常用实现方式. 用户忘记密码,来到密码重设界面. 用户输入Email地址,点击重设密码按钮. 用户收到一封密码重设邮件,里面有重设密码的链接,此链接有过期时间.

谈 DevOps 平台设计:版本号相关功能的设计

- - IT瘾-dev
在设计 DevOps 平台时,笔者认为版本号的管理是一个绕不开的课题. 可是,行业里似乎很少人提这个事,笔者觉得要谈一谈,所以就有了这篇文章. 一万个人的眼里有一万个“版本号”. 笔者这三年在同一家公司里,换岗换了四个团队. 团队的成员组成各异,有的团队都是在大型跨国企业跳槽过来的,有的团队大部人都是刚毕业的.

浅谈社区小游戏功能设计

- 锋 - 互联网的那点事
社区游戏功能的设计方法,很大程度能够决定这个功能能否被社区用户所接受认可,而且能够减少开发的时间成本. 功能设计分为两个阶段:功能玩法设计及功能完善. 两个阶段必不可少,且设计的重心也各有不同. 功能设计玩法的方法总结八个字:“目标,荣耀,互动,惊喜”. 而功能完善的方法总结六个字:“情绪,表现,人性”.

Andy Rubin:Ice Cream Sandwich 的 Face Unlock 功能由 PittPatt 设计

- 云飞风起 - Engadget 中国版
当你仍在为智能型手机支持脸部辨识解锁功能而兴奋的时候,Google Mobile SVP Andy Rubin 在香港 Asia D: All Things Digital 接受访问时指出,Face Unlock 正是来自今年年初收购的 PittPatt. 虽然今天早上发表 Android 4.0 Ice Cream Sandwich 时,Face Unlock 一度失败、无法进行脸部解锁功能,但 Andy Rubin 在现场再次示范时,成功进行有关解锁功能,而他也请主持 Walt Mossberg 尝试用其脸部来解锁(当然是失败收场),以确定其准确度,看来我们真的不必担心太多.

扁平化设计――UI的功能比外观更重要

- - 互联网的一些事-关注互联网产品管理,交流产品设计、用户体验心得
  现在大家都在谈“扁平”设计,扁扁的Windows 8动态砖、扁扁的Google+、Google Now,甚至是即将亮相的iOS 7系统预估也将会是扁平的设计,但,到底为什么?.    UI的功能比外观更重要.   首先,有人说用户界面(UI)外观怎样一点都不重要. 有道理!用户界面最重的应该是功能与便利性,对吧?.

如何设计“找回用户帐号”功能

- 雪冬 - 酷壳 - CoolShell.cn
因为《腾讯帐号申诉的用户体验》一文中好多人觉得腾讯申诉是世界级先进的,并让我拿出一个找回用户的帐号的功能来. 本来不想写的,因为大家看看其它系统的就行的,但是,很明显有些人就是很懒,也不会思考,而且不会观察,所以,我就只好写下这篇科普性常识性的文章. 在行文之前,我得先感谢腾讯公司的至少30名员工在《腾讯帐号申诉的用户体验》一文后的回帖(我STFG(Search The Fucking Google)看到了你们使用的那个固定IP在各个大学论坛上的腾讯的招聘广告),我感谢你们主要有两点:.

Elasticsearch项目实战,商品搜索功能设计与实现!

- - 掘金后端
SpringBoot实战电商项目mall(30k+star)地址: github.com/macrozheng/…. 上次写了一篇 《Elasticsearch快速入门,掌握这些刚刚好. 》,带大家学习了下Elasticsearch的基本用法,这次我们来篇实战教程,以 mall项目中的商品搜索为例,把Elasticsearch用起来.

iOS最大问题已非系统设计 功能性成最大软肋

- - cnBeta全文版
美国知名媒体《商业内幕》特约撰稿人杰-耶罗(Jay Yarow)日前撰文表示,苹果iOS操作系统目前存在着更深层次的问题,许多第三方应用软件的设计已经远远超越了苹果内置应用,因此在乔纳森-艾维 (Jony Ive)领导下的iOS 7软件设计团队仅仅对应用外观作出改进恐怕还远远不够. 目前,有不少消息指出苹果正在针对下一代驱动iPhone、iPad和iPod Touch的iOS操作系统进行重新设计.

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

- - CSDN博客Web前端推荐文章
      工程示例下载地址:  http://download.csdn.net/download/shuqin1984/6913555.       包含一个简短而完整的Web示例, 演示如何根据用户输入的字符进行自动提示和补全.          一、  场景与目标        在使用 IDE 开发软件时, IDE 会提供一种“智能提示”, 根据所输入的字符列出可能的词组; 在日常Web开发中,根据用户输入进行自动提示和补全,也能很好地改善使用体验.