概述
專案需求中有一個需求,是使用者輸入的地址進行智慧匹配,包含拼音匹配跟文字匹配,下面先展示一下需要實現的效果
其實看到這個需求,最開始的想法其實是很偷懶的,就是讓服務端寫一個介面,然後進行介面呼叫,不過在沒網的時候就尷尬了,輸入是沒有提示的,所以這種方式其實不大好,再加上城市地址庫一旦確定基本上就是不會輕易改變的,基於這幾點考慮,打算做一個本地搜尋。
正文
確定實現方式之後,其實思路就比較清晰了,首先請求一次介面的資料,然後直接放在本地,再加上專案的需求,所以基本的功能點如下: 主要有以下2點:
- 對介面返回的資料進行排序
- 根據排序進行分組
- 對使用者的輸入進行智慧匹配
排序的實現
提到排序,其實首先會點到Java中的兩個介面Comparable跟Comparator
Comparable
public interface Comparable<T> {
public int compareTo(T o);
}
複製程式碼
Comparable實際上就只是個介面,定義了一個compareTo方法,挺簡單的,不過在使用的時候需要注意一下幾點:
- 兩個元素排序:需要實現compareTo方法,並且有一個int返回值,表明返回的結果,具體比較的規則可以根據需求自己定義,可以實現相同型別的引數進行比較.
- 多個元素排序:這裡用地比較多的情況就是排序,JDK提供了一個工具類Arrays,呼叫Arrays.sort(Object[] a);只需要傳入的陣列實現了Comparable介面即可對傳入的陣列進行排序,這個時候我們注意到,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.
- 兩個元素:直接傳入物件,即可比較
- 多個元素: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的快速漢字轉拼音庫。
以武漢為例
- 用tinypinyin將所有的城市名稱轉換成拼音,用3個欄位分別儲存W,WH,WUHAN,其中W用來進行排序分組,WH是用來進行簡拼匹配,WUHAN是用來進行全拼匹配
- 將城市列表的資料根據首字母安裝ABCD的順序進行排序,對於無法獲取拼音的通過"#"進行標識
- 然後再進行二次分組,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());
}
}
複製程式碼
查詢演算法
先定義一下查詢規則
- 如果是漢字,則採用精準查詢
- 如果是字母,當字母數量較小(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進行查詢,這樣的話,效率可能會高一下,感興趣的可以優化一下。