輸入自動提示與補全功能的設計與實現



 

      工程示例下載地址:  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.  定義抽象類    
 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<String, Set<String>> prefixMatchers = new HashMap<String, Set<String>>();
31 
32     abstract void dynamicAddNew(String inputText);
33     
34     public Set<String> obtainMatchedWords(String inputText) {
35         Set<String> matchers = prefixMatchers.get(inputText);
36         if (matchers == null) {
37             Set<String> input = new HashSet<String>();
38             input.add(inputText);
39             dynamicAddNew(inputText);
40             return input;
41         }
42         return matchers;
43     }
44     
45     protected Map<String, Set<String>> obtainPrefixMatchers() {
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<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 中還可以支持根據用戶輸入動態添加關鍵字的功能, 但由於涉及到並發問題, 暫時沒有做更多深入。 讀者可以閱讀源碼自行完善, 我也會給出后續思考。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2024 CODEPRJ.COM