實現城市列表的排序及模糊查詢

wustor發表於2017-11-06

概述

專案需求中有一個需求,是使用者輸入的地址進行智慧匹配,包含拼音匹配跟文字匹配,下面先展示一下需要實現的效果

位址列排序搜尋

其實看到這個需求,最開始的想法其實是很偷懶的,就是讓服務端寫一個介面,然後進行介面呼叫,不過在沒網的時候就尷尬了,輸入是沒有提示的,所以這種方式其實不大好,再加上城市地址庫一旦確定基本上就是不會輕易改變的,基於這幾點考慮,打算做一個本地搜尋。

正文

確定實現方式之後,其實思路就比較清晰了,首先請求一次介面的資料,然後直接放在本地,再加上專案的需求,所以基本的功能點如下: 主要有以下2點:

  • 對介面返回的資料進行排序
  • 根據排序進行分組
  • 對使用者的輸入進行智慧匹配

排序的實現

提到排序,其實首先會點到Java中的兩個介面Comparable跟Comparator

Comparable
public interface Comparable<T> {

    public int compareTo(T o);
}

複製程式碼

Comparable實際上就只是個介面,定義了一個compareTo方法,挺簡單的,不過在使用的時候需要注意一下幾點:

  1. 兩個元素排序:需要實現compareTo方法,並且有一個int返回值,表明返回的結果,具體比較的規則可以根據需求自己定義,可以實現相同型別的引數進行比較.
  2. 多個元素排序:這裡用地比較多的情況就是排序,JDK提供了一個工具類Arrays,呼叫Arrays.sort(Object[] a);只需要傳入的陣列實現了Comparable介面即可對傳入的陣列進行排序,這個時候我們注意到,Arrays.sort有很多過載方法,我們可以看一下
    Arrays.sort的過載方法

有很多我們熟悉的基本型別,int,byte,char,這些貌似跟Comparable沒有什麼關係,不過由於Java是物件導向的,所以對於基本型別有一個裝箱拆箱操作,當看到基本型別的時候,應該多跟他們的包裝類聯絡起來,那就隨便找幾個,int的包裝類Integer進行byte的包裝類Byte ,以及char的包裝類Character 來檢視一下:

public final class Integer extends Number implements Comparable<Integer>{
      public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }

}
public final class Byte extends Number implements Comparable<Byte>{
      public int compareTo(Byte anotherByte) {
        return compare(this.value, anotherByte.value);
    }

}
public finalclass Character implements java.io.Serializable, Comparable<Character>{
     public int compareTo(Character anotherCharacter) {
        return compare(this.value, anotherCharacter.value);
    }

}
複製程式碼

原來,他們的包裝類都實現了Comparable介面,所以理清了,可以直接呼叫Arrays的sort方法對這些基本型別進行排序,當然,這裡的排序都是基於包裝類自身實現的排序演算法,是固定不變的,如果是我們自定義的物件的話,需要重寫compare方法。

Comparator
public interface Comparator<T> {
   
    int compare(T o1, T o2);

    boolean equals(Object obj);
 
  
}
複製程式碼

Comparator的方法比Comparable要多地多,這裡選擇了compare跟equals兩個方法,compare很好理解,用來比較兩個物件,equals是用來比較兩個comparator的,如果傳入的物件也是一個Comparator並且他們的排序規則也是一樣的,則equals方法返回true,否則返回false.

  1. 兩個元素:直接傳入物件,即可比較
  2. 多個元素:Collections提供了sort方法,傳入一個list,跟一個comparator
   public static <T> void sort(List<T> list, Comparator<? super T> c) {
        if (list.getClass() == ArrayList.class) {
            Arrays.sort(((ArrayList) list).elementData, 0, list.size(), (Comparator) c);
            return;
        }

        Object[] a = list.toArray();
        Arrays.sort(a, (Comparator)c);
        ListIterator<T> i = list.listIterator();
        for (int j=0; j<a.length; j++) {
            i.next();
            i.set((T)a[j]);
        }
    }

複製程式碼

然後方法裡面還是呼叫了Arrays.sort,畢竟集合也是陣列,最終還是呼叫了陣列的排序方法。

對比分析
  • Comparator是在類的外部進行排序,Comparable是在類的內部進行排序
  • Comparator比較適合對於多個類進行排序,只需要實現一個Comparator就可以,Comparable則需要在每個類中實現Comparable介面

開始排序

排序通常的做法是對字母進行排序,但是介面返回的是文字,所以需要將文字轉換成拼音,並且拿到首字母,才能進行排序,這裡用到了一個第三方庫TinyPinyin,適用於Java和Android的快速漢字轉拼音庫。

以武漢為例

  1. 用tinypinyin將所有的城市名稱轉換成拼音,用3個欄位分別儲存W,WH,WUHAN,其中W用來進行排序分組,WH是用來進行簡拼匹配,WUHAN是用來進行全拼匹配
  2. 將城市列表的資料根據首字母安裝ABCD的順序進行排序,對於無法獲取拼音的通過"#"進行標識
  3. 然後再進行二次分組,ABCD各位一大組,插入一個titleA,titleB,titleC,通過不同的type來在Recyclerview中進行type區分

這些其實沒什麼難度,下面貼一下Comparator的程式碼,自定義了compare方法,

        @Override
        public int compare(CityBean c1, CityBean c2) {
            if (c1.getPinyinFirst().equals("#")) {
                return 1;
            } else if (c2.getPinyinFirst().equals("#")) {
                return -1;
            }
            return c1.getPinyinFirst().compareTo(c2.getPinyinFirst());
        }
    }
複製程式碼

查詢演算法

先定義一下查詢規則

  1. 如果是漢字,則採用精準查詢
  2. 如果是字母,當字母數量較小(3個以內)的時候,優先進行簡拼,然後全拼,字母較多,使用全拼查詢

正則匹配查詢演算法

   public static void find(String inputStr, List<CityBean> old, List<CityBean> target) {
        if (RegexUtils.isEnglishAlphabet(inputStr)) {
            //拼音模糊匹配
            findByEN(inputStr, old, target);
        } else {
            //含有中文精準匹配
            findByCN(inputStr, old, target);
        }

    }
複製程式碼

中文匹配

private static void findByCN(String inputStr, List<CityBean> mBodyDatas, List<CityBean> searchResult) {
        for (int i = 0; i < mBodyDatas.size(); i++) {
            CityBean cityBean = mBodyDatas.get(i);
            if (!TextUtils.isEmpty(cityBean.getRegionName()) && cityBean.getRegionName().contains(inputStr)) {
                searchResult.add(cityBean);
            }
        }
    }

複製程式碼

字母匹配

    private static void findByEN(String inputStr, List<CityBean> mBodyDatas, List<CityBean> searchResult) {
        //把輸入的內容變為大寫
        String searPinyin = PinYinUtil.transformPinYin(inputStr);
        //搜尋字串的長度
        int searLength = searPinyin.length();
        //搜尋的第一個大寫字母
        for (int i = 0; i < mBodyDatas.size(); i++) {
            CityBean cityBean = mBodyDatas.get(i);
            //如果輸入的每一個字母都和名字的首字母一樣,那就可以匹配比如:武漢,WH
            if (cityBean.getMatchPin().contains(searPinyin)) {
                searchResult.add(cityBean);
            } else {
                boolean isMatch = false;
                //先去匹配單個字,比如武漢WU,HAN.輸入WU,肯定匹配第一個
                for (int j = 0; j < cityBean.getNamePinyinList().size(); j++) {
                    String namePinyinPer = cityBean.getNamePinyinList().get(j);
                    if (!TextUtils.isEmpty(namePinyinPer) && namePinyinPer.startsWith(searPinyin)) {
                        //符合的話就是當前字匹配成功
                        searchResult.add(cityBean);
                        isMatch = true;
                        break;
                    }
                }
                if (isMatch) {
                    continue;
                }
//                根據拼音包含來實現,比如武漢:WUHAN,輸入WUHA或者WUHAN。
                if (!TextUtils.isEmpty(cityBean.getNamePinYin()) && cityBean.getNamePinYin().contains(searPinyin)) {
                    //這樣的話就要從每個字的拼音開始匹配起
                    for (int j = 0; j < cityBean.getNamePinyinList().size(); j++) {
                        StringBuilder sbMatch = new StringBuilder();
                        for (int k = j; k < cityBean.getNamePinyinList().size(); k++) {
                            sbMatch.append(cityBean.getNamePinyinList().get(k));
                        }
                        if (sbMatch.toString().startsWith(searPinyin)) {
                            //匹配成功
                            int length = 0;
                            //比如輸入是WUH,或者WUHA,或者WUHAN,這些都可以匹配上
                            for (int k = j; k < cityBean.getNamePinyinList().size(); k++) {
                                length = length + cityBean.getNamePinyinList().get(k).length();
                                if (length >= searLength) {
                                    break;
                                }
                            }
                            //有可能重複匹配
                            if (!searchResult.contains(cityBean))
                                searchResult.add(cityBean);
                        }
                    }
                }

            }
        }
    }

複製程式碼

由於我是在記憶體中進行匹配查詢的,這樣雖然效率比較高,但是進行匹配的時候,過多地使用了for迴圈,整體的效能不是很好,後續會嘗試著通過Sqlite進行查詢,這樣的話,效率可能會高一下,感興趣的可以優化一下。

原始碼下載

相關文章