概述
項目需求中有一個需求,是用戶輸入的地址進行智能匹配,包含拼音匹配跟文字匹配,下面先展示一下需要實現的效果
其實看到這個需求,最開始的想法其實是很偷懶的,就是讓服務端寫一個接口,然后進行接口調用,不過在沒網的時候就尷尬了,輸入是沒有提示的,所以這種方式其實不大好,再加上城市地址庫一旦確定基本上就是不會輕易改變的,基于這幾點考慮,打算做一個本地搜索。
正文
確定實現方式之后,其實思路就比較清晰了,首先請求一次接口的數據,然后直接放在本地,再加上項目的需求,所以基本的功能點如下:
主要有以下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有很多重載方法,我們可以看一下
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進行查找,這樣的話,效率可能會高一下,感興趣的可以優化一下。