输入法核心数据结构及算法的设计
突然想到了去年腾讯招实习生时笔试的附加题目,就是让你给出一个输入法的设计方案,要求能够提供智能的输入提示,具体题目要求我不太记得了,简单岂见我们只考虑英文输入法~
问题的关键在于如何做到智能输入提示呢?由于很多单词是共享前缀的,才使得用户输入过程中,智能地提示可供选择的词组。因此,创建共享前缀树用于存储单词词条,并在用户输入过程中实时地查寻前缀相同的词条是一个非常好的思路。如果你学过数据结构的话,就知道这种结构叫“键树”。
输入法只需要实现三个非常重要的函数即可:
/** * 插入一个单词 * * @param term * 要插入的单词 */ public void insert(String term); /** * 删除一个单词 * * @param term * 要删除的单词 */ public void delete(String term); /** * 获取提示的词条 * * @param preTerm * 用户已输入的词条 * @return 提示词条 */ public List<String> promptsTerms(String preTerm);
当然在考虑删除某个单词的时候,由于有很多单词共享相同的前缀,所以不能轻易删除,具体删除的情况可以分为三种:完全独立型、完全共享型、部分共享型。大家自己去思考吧,还是很简单的,我就不说了~
其中一种实现思路可以为每个单词节点设计一个计数器,delete的时候就把计数减1,当计数为0时就删除。但这种方法有个缺陷:即每个节点都要多设置一个计数的空间,节点当然不少,无形中对空间会造成比较大的浪费。而且对于输入法而言delete函数并不常用,为了一个极少使用的函数浪费这么多空间肯定是不合适的。特别是对于手机而言,本身内存就已经很少了,这样的设计肯定是bad smell。那么,我们采用另外一种方法去解决了这个问题~
废话我就不太多说了,我觉得用代码描述更为直观,大家有什么问题直接研究代码吧~
package com.algorithm; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; /** * 共享前缀树PrefixTree * * E-mail:530025983@qq.com 2011-5-28 * * @author MONKEY_D_MENG * */ public class PrefixTree { /* 利用Hash表能够O(1)时间内找到下一个字符的位置 */ private Map<Byte, PrefixTree> wordTable = new HashMap<Byte, PrefixTree>(); /* 到这个节点为止是否是一个完整的单词 */ private boolean isWord = false; /** * 插入一个单词 * * @param term * 要插入的单词 */ public void insert(String term) { if (null == term) { return; } /* 将单词转化为字符串处理 */ byte[] termChars = term.getBytes(); PrefixTree current = this; for (byte ch : termChars) { if (!current.wordTable.containsKey(ch)) { PrefixTree next = new PrefixTree(); current.wordTable.put(ch, next); current = next; } else { current = (PrefixTree) current.wordTable.get(ch); } } /* 设置到该节点止为单词的标志 */ current.isWord = true; } /** * 删除一个单词 * * @param term * 要删除的单词 */ public void delete(String term) { if (null == term) { return; } /* 将单词转化为字符串处理 */ byte[] termChars = term.getBytes(); PrefixTree current = this; PrefixTree fromDelete = this; PrefixTree next = this; int indexDelete = 0; int index = 0; for (index = 0; index < termChars.length; ++index) { if (!current.wordTable.containsKey(termChars[index])) { return; } else { current = (PrefixTree) current.wordTable.get(termChars[index]); } /* 记录要删除的节点位置,注意不能到最后才判断,因为要删除这个单词最后的节点的isWord肯定被转为了true */ if (current.isWord && index != termChars.length – 1) { fromDelete = current; indexDelete = index + 1; } } /* 有后缀节点,说明被其他单词共享了前缀 */ if (current.wordTable.size() != 0) { current.isWord = false; return; } /* 删除所有不共享的后缀节点 */ for (current = fromDelete, index = indexDelete; index < termChars.length; ++index) { next = current.wordTable.get(termChars[index]); current.wordTable.remove(termChars[index]); current = next; current.isWord = false; } } /** * 获取提示的词条 * * @param preTerm * 用户已输入的词条 * @return 提示词条 */ public List<String> promptsTerms(String preTerm) { if (null == preTerm) { return null; } PrefixTree current = this; List<String> words = new ArrayList<String>(); Stack<Byte> stack = new Stack<Byte>(); /* 将单词转化为字符串处理 */ byte[] termChars = preTerm.getBytes(); for (byte ch : termChars) { if (!current.wordTable.containsKey(ch)) { return null; } else { current = (PrefixTree) current.wordTable.get(ch); } } /* 如果本身输入的已经是一个单词了,需要先记录,由于后面处理时要加前缀,这个地方只给个空串即可 */ if (current.isWord) { words.add(“”); } /* 递归地遍历,寻找所有匹配的单词 */ this.traversalTree(current, stack, words); /* 将每个单词都加上前缀 */ for (int index = 0; index < words.size(); ++index) { words.set(index, preTerm + words.get(index)); } return words; } /** * 递归生成所有后缀的单词 * * @param current * 当前的根节点 * @param words * 单词列表 */ private void traversalTree(PrefixTree current, Stack<Byte> stack, List<String> words) { for (byte ch : current.wordTable.keySet()) { stack.push(ch); /* 遇到是单词的标志 */ if (current.wordTable.get(ch).isWord) { /* 记录单词 */ words.add(stackToWord(stack)); } /* 如果还有后续节点则递归 */ if (current.wordTable.get(ch).wordTable.size() != 0) { this.traversalTree(current.wordTable.get(ch), stack, words); } stack.pop(); } } /** * 输出从栈底到栈顶的一条路径,即为一个单词 * * @param stack * 栈 * @return 栈中所存储的单词 */ private String stackToWord(Stack<Byte> stack) { String word = “”; for (byte ch : stack) { word += (char) ch; } return word; } } package com.algorithm; import java.util.List; /** * 测试代码 * * E-mail:530025983@qq.com 2011-5-28 * * @author MONKEY_D_MENG * */ public class Main { public static void main(String[] args) { PrefixTree prefixTree = new PrefixTree(); List<String> words; prefixTree.insert(“mon”); prefixTree.insert(“money”); prefixTree.insert(“monkey”); prefixTree.insert(“monkey1”); prefixTree.insert(“monkey2”); prefixTree.insert(“monkey3”); prefixTree.insert(“monkey4”); prefixTree.insert(“monkey5”); prefixTree.delete(“money”); words = prefixTree.promptsTerms(“mon”); for (String word : words) { System.out.println(word); } } }