智能拨号匹配算法

本文出自 “NashLegend” 博客,请务必保留此出处http://nashlegend.blog.51cto.com/5635342/1566108 

完整源码在我的github上 https://github.com/NashLegend/QuicKid  

智能拨号是指,呃不用解释了,国内拨号软件都带的大家都知道,就是输入姓名拼音的一部分就可快速搜索出联系人的拨号方式。如下图

wKioL1RQ4j_Bu7A3AAE0u2jIJJM541.jpg


    智能匹配,很容易想到的就是先把九宫格输入键盘上输入的数字转换成可能的拼音组合,然后再用这些可能的拼音与联系人列表中的姓名拼音一一匹配,取匹配度最高的排到最前,但是这有一个问题就是数组对应的可能的拼音组合实在是点儿多,跑一下下面的代码就知道了。如果想智能一些的话还要先剔除一些不可能的拼音组合实在有点麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public  static  HashMap<Character, String[]> keyMaps;
 
public  static  void  main(String[] args) {
     keyMaps =  new  HashMap<Character, String[]>();
     keyMaps.put( '0' new  String[ 0 ]);
     keyMaps.put( '1' new  String[ 0 ]);
     keyMaps.put( '2' new  String[] {  "a" "b" "c"  });
     keyMaps.put( '3' new  String[] {  "d" "e" "f"  });
     keyMaps.put( '4' new  String[] {  "g" "h" "i"  });
     keyMaps.put( '5' new  String[] {  "j" "k" "l"  });
     keyMaps.put( '6' new  String[] {  "m" "n" "o"  });
     keyMaps.put( '7' new  String[] {  "p" "q" "r" "s"  });
     keyMaps.put( '8' new  String[] {  "t" "u" "v"  });
     keyMaps.put( '9' new  String[] {  "w" "x" "y" "z"  });
 
     List<String> lss = getPossibleKeys( "726" );
     System.out.println(lss.size());
}
 
public  static  ArrayList<String> getPossibleKeys(String key) {
     ArrayList<String> list =  new  ArrayList<String>();
     if  (key.length() >  0 ) {
         if  (key.contains( "1" ) || key.contains( "0" )) {
             list.add(key);
         else  {
             int  keyLen = key.length();
             String[] words;
             if  (keyLen ==  1 ) {
                 words = keyMaps.get(key.charAt( 0 ));
                 for  ( int  i =  0 ; i < words.length; i++) {
                     list.add(words[i]);
                 }
             else  {
                 ArrayList<String> sonList = getPossibleKeys(key.substring(
                         0 , key.length() -  1 ));
                 words = keyMaps.get(key.charAt(key.length() -  1 ));
                 for  ( int  i =  0 ; i < words.length; i++) {
                     for  (Iterator<String> iterator = sonList.iterator(); iterator
                             .hasNext();) {
                         String sonStr = iterator.next();
                         list.add(sonStr + words[i]);
                     }
                 }
             }
         }
     }
     return  list;
}


    所以可以反过来想,为什么一定要匹配拼音呢。其实我们可以匹配数字,将姓名的拼音转化成九宫格上的数字,比如张三就是94264,726。用输入的数字来匹配这些数字,匹配次数将大大减少。匹配出的数值越高,匹配度越强。


下面先定义一下几个匹配规则

  1. 完全匹配用来匹配姓名和电话号码。指输入字符串与联系人内某一匹配项完全匹配。无加减分项。

    PanZhiHui-->PanZhiHui

  2. 前置首字母完全匹配用来匹配姓名。指输入字符串与联系人前几个首字母完全匹配。用来匹配姓名。是前置首字母溢出匹配的特殊形式。 无加分项,减分项为不匹配的首字母个数。

    PZH-->PanZhiHui。+2-0
    PZ-->PanZhiHui。+2-1

  3. 前置首字母溢出匹配用来匹配姓名。指在匹配首字母的情况下,还匹配了某一个或者几个首字母后一段连贯的字符串。加分项为匹配到的首字母个数,减分项为不匹配的首字母个数。

    PanZH-->PanZhiHui。+1-0
    PZhiHui-->PanZhiHui。+1-0
    PZHui-->PanZhiHui。+1-0
    PZHu-->PanZhiHui。+1-0
    PZhi-->PanZhiHui。+1-1

  4. 前置段匹配用来匹配姓名。指一个长度为N的连贯字符与联系人内某一匹配项的前N个字符完全匹配。是前置首字母溢出匹配的特殊形式。

    panzh-->PanZhiHui

  5. 后置首字母完全匹配用来匹配姓名。指输入字符串匹配除第一个首字母以外的其他几个连续首字母。 无加分项,减分项为不匹配的首字母个数。

    ZH-->PanZhiHui

  6. 后置首字母溢出匹配用来匹配姓名。后置首字母完全匹配的情况下,还匹配了某一个或者几个首字母后一段连贯的字符串。加分项为匹配的首字母的数量,减分项为不匹配的首字母个数。

    ZHu-->PanZhiHui。+1-0
    Zh-->PanZhiRui。+1-1

  7. 后置段匹配用来匹配姓名。指有一串长度为N的连贯字符与与联系人内某一匹配项的后半部的一段N个字符串匹配,且此连贯字符的开头位置必须是某一首字母位置。是后置首字母溢出匹配的特殊形式,同时意味着后置首字母溢出匹配事实上不需要加分项,只要保证后置首字母完全匹配的加分项比它大就足够了

    ZhiHui/Zhi/Hui-->PanZhiHui

  8. 后置无头匹配用来匹配姓名和电话号码。指一串连贯字符在前7种全部未匹配成功的情况下,却被包含在字符串里。加分项为-index,减分项为长度差

    hiHui-->PanZhiHui

每个规则都有一个基础数值,以及加分减分项,基本数值不同。取减分项为0.001,加分项为1。至于为什么,在下一段。

查询时匹配以上8种,其他情况不匹配。

匹配的原则是匹配尽可能多的单词。

上面这些名字完全是临时胡编乱造的好么 0.0j_0004.gif

排列规则

  1. 查询出的列表将按匹配度排序,匹配度是一个float(当然double也一样),优先级别从高到低如下(减分项足够小以至于高优先级的匹配度无论如何减分都仍然会高于下面的优先级,因此减分项事实上只用来区别同一优先级中不同联系人匹配程度的高低)。

  • 完全匹配,对应的基础数值为4000。

  • 前置首字母完全匹配、前置首字母溢出匹配、前置段匹配,这三个其实都可以视作前置首字母溢出匹配,对应的基础数值为3000。(当只有一个字母时,按规则#1算)

  • 后置首字母完全匹配、后置首字母溢出匹配、后置段匹配,这三个其实都可以视作后置首字母溢出匹配对应的基础数值为2000。(当只有一个字母时,按规则#5算)

  • 后置无头匹配。对应的基础数值为1000。(可以考虑摒弃此匹配,没有人会这么按,而按键出错的可能性导致无头匹配的可能性又极小,往往不是想要的结果)



输入的一列查询字符串将同时与联系人的名字和电话匹配。对于一个联系人,他的名字可能有多种发音,这时候要取匹配度最高的。对于一个联系人,他可能有两个甚至更多的电话号码,匹配的时候要分别匹配,而不是单独取匹配度最高的。




    


    好了,先写一个类Contact。

    添加几个常量,看字面意思应该看得懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static  final  int  Match_Type_Name =  1 ;
static  final  int  Match_Type_Phone =  2 ;
 
static  final  int  Level_Complete =  4 ;
static  final  int  Level_Fore_Acronym_Overflow =  3 ;
static  final  int  Level_Back_Acronym_Overflow =  2 ;
static  final  int  Level_Headless =  1 ;
static  final  int  Level_None =  0 ;
 
static  final  float  Match_Level_None =  0 ;
static  final  float  Match_Level_Headless =  1000 ;
static  final  float  Match_Level_Back_Acronym_Overflow =  2000 ;
static  final  float  Match_Level_Fore_Acronym_Overflow =  3000 ;
static  final  float  Match_Level_Complete =  4000 ;
static  final  float  Match_Score_Reward =  1 ;
static  final  float  Match_Miss_Punish =  0 .001f;
static  final  int  Max_Reward_Times =  999 ;
static  final  int  Max_Punish_Times =  999 ;

    再添加下面几条字段

1
2
3
     List<ArrayList<String>> fullNameNumber =  new  ArrayList<ArrayList<String>>();
     List<String> fullNameNumberWithoutSpace =  new  ArrayList<String>();
     List<String> abbreviationNumber =  new  ArrayList<String>();

    fullNameNumber是一个二维的ArrayList,它存放的是将一个联系人打散后数字后的List。比如张三的fullNameString就是{{94264,726}},之所以是二维的,原因是有可能姓名是含有多音字……

    fullNameNumberWithoutSpace是联系人姓名的全拼对应的数字,比如张三就是{94264726},之所以是二维的,原因是有可能姓名是含有多音字……

    abbreviationNumber是联系人姓名首字母对应的数字,比如张三对应的就是{97},之所以是二维的,原因是有可能姓名是含有多音字……

    在设置了Contact的名字后上面三个字段将同时生成数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     synchronized  public  void  initPinyin() {
             String trimmed = name.replaceAll( " " "" );
             //将姓名转化为拼音
             String fullNamesString = HanyuPinyinHelper.hanyuPinYinConvert(trimmed,  false );
             for  (Iterator<String> iterator = fullNamesString.iterator(); iterator
                     .hasNext();) {
                 String str = iterator.next();
                 ArrayList<String> lss =  new  ArrayList<String>();
                 String[] pinyins = TextUtil.splitIgnoringEmpty(str,  " " );
                 String abbra =  "" ;
                 String fullNameNumberWithoutSpaceString =  "" ;
                 for  ( int  i =  0 ; i < pinyins.length; i++) {
                     String string = pinyins[i];
                     String res = convertString2Number(string);
                     abbra += res.charAt( 0 );
                     fullNameNumberWithoutSpaceString += res;
                     lss.add(res);
                 }
                 abbreviationNumber.add(abbra);
                 fullNameNumberWithoutSpace
                         .add(fullNameNumberWithoutSpaceString);
                 fullNameNumber.add(lss);
             }
     }

给它一个match方法。下面调用的xxxMatch()方法都是针对四种不同种类的匹配的对应方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public  float  match(String reg) {
     // 无法通过第一个字母来判断是不是后置匹配
     // 但是可以通过第一个字母判断是不是前置匹配
     // match的原则是匹配尽可能多的字符
     // 事实上前五种匹配方式都可以使用crossMatch来实现
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     if  (!TextUtils.isEmpty(reg)) {
         boolean  checkBack = !canPrematch(reg);
         if  (!checkBack) {
             if  ((scoreAndHits = completeMatch(reg)).score == 0f) {
                 if  ((scoreAndHits = foreAcronymOverFlowMatch(reg)).score == 0f) {
                     checkBack =  true ;
                 }
             }
         }
         if  (checkBack) {
             if  ((scoreAndHits = backAcronymOverFlowMatch(reg)).score == 0f) {
                 scoreAndHits = backHeadlessParagraphMatch(reg);
             }
         }
     }
     scoreAndHits.reg = reg;
     matchValue = scoreAndHits;
     return  scoreAndHits.score;
}

所有的xxxMatch返回的结果是一个自定义类ScoreAndHits。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public  static  class  ScoreAndHits {
     public  float  score = 0f;
     public  int  nameIndex;
     public  ArrayList<PointPair> pairs =  new  ArrayList<PointPair>();
     public  int  matchType = Match_Type_Name;
     public  int  matchLevel = Level_None;
     public  String reg =  "" ;
 
     public  ScoreAndHits( int  nameIndex,  float  score,
             ArrayList<PointPair> pairs) {
         this .nameIndex = nameIndex;
         this .score = score;
         this .pairs = pairs;
     }
}

nameIndex是匹配到了第几个拼音。score是匹配度。pairs是指匹配到的数字在对应的二维list中的位置,用来将来高亮显示匹配的字符用的。如果完全匹配的话,就用不到pairs了。


几个匹配方法的具体内容看下一篇,超过字数限制,写不开了j_0012.gif

1.完全匹配

    完全匹配很简单了,只要判断string是否相等就行了。这里要判断所有拼音和所有号码。如果拼音已经符合,就不再判断号码。反正是一个人……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private  ScoreAndHits completeMatch(String reg) {
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     for  ( int  i =  0 ; i < fullNameNumberWithoutSpace.size(); i++) {
         String str = fullNameNumberWithoutSpace.get(i);
         if  (reg.equals(str)) {
             scoreAndHits.nameIndex = i;
             scoreAndHits.score = Match_Level_Complete;
             scoreAndHits.pairs.add( new  PointPair(i, - 1 ));
             scoreAndHits.matchLevel = Level_Complete;
             return  scoreAndHits;
         }
     }
     for  ( int  i =  0 ; i < phones.size(); i++) {
         PhoneStruct phone = phones.get(i);
         if  (reg.equals(phone.phoneNumber)) {
             scoreAndHits.nameIndex = i;
             scoreAndHits.score = Match_Level_Complete;
             scoreAndHits.pairs.add( new  PointPair(i, - 1 ));
             scoreAndHits.matchType = Match_Type_Phone;
             scoreAndHits.matchLevel = Level_Complete;
             return  scoreAndHits;
         }
     }
     // 走到这里说明没有匹配
     return  new  ScoreAndHits(- 1 , 0f,  new  ArrayList<PointPair>());
}

2.前置首字母溢出匹配。(能不能想个好听的名字j_0004.gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
private  ScoreAndHits foreAcronymOverFlowMatch(String reg) {
         // 因为有可能是多音字,所以这个方法用来对比不同拼音的匹配度,并取最大的那个
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     for  ( int  i =  0 ; i < fullNameNumber.size(); i++) {
         ArrayList<String> names = fullNameNumber.get(i);
         ScoreAndHits tmpscore = foreAcronymOverFlowMatch(names, reg);
         if  (tmpscore.score > scoreAndHits.score) {
             scoreAndHits = tmpscore;
             scoreAndHits.nameIndex = i;
         }
     }
     scoreAndHits.matchLevel = Level_Fore_Acronym_Overflow;
     return  scoreAndHits;
}
 
// 在第一个字母确定的情况下,第二个字母有可能有三种情况
// 一、在第一个字母所在单词的邻居位置charAt(x+1);
// 二、在第二个单词的首字母处
// 三、以上两种情况皆不符合,不匹配,出局
 
private  ScoreAndHits foreAcronymOverFlowMatch(ArrayList<String> names,
         String reg) {
     // 用来得出某一个拼音的匹配值。
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     if  (names.get( 0 ).charAt( 0 ) == reg.charAt( 0 )) {
             //其实crossWords()方法才是求匹配值的方法,lol
         OverflowMatchValue value = crossWords(names, reg,  0 0 0 );
         int  cross = crossWords(names, reg,  0 0 0 ).crossed;
         if  (cross >  0 ) {
             scoreAndHits.score = Match_Level_Fore_Acronym_Overflow + cross
                     * Match_Score_Reward - (names.size() - cross)
                     * Match_Miss_Punish;
             scoreAndHits.pairs = value.pairs;
         }
 
     }
     return  scoreAndHits;
}
 
/**
  * 返回一串字符能跨越另一串字符的长度,根据上面的匹配规则,要尽可能的多匹配单词。若要保证
  * 能匹配最长的长度,只要保证下一个字符开始的一段字符能匹配最长的长度即可,换名话说,
  * 如果想要让96758匹配最长的字符串,那么只要保证6758能匹配最长的字符串即可,
  * 然后758,再然后58……。例如,名字叫PanAnNing,输入pan,那么应该匹配三个首字母,
  * PAN,而不是第一姓的拼音Pan.这是一个递归。
 
 
  * @param names
  * @param regString
  *            匹配字符串
  * @param listIndex
  *            匹配到的list的第listIndex个单词
  * @param strIndex
  *            匹配到第listIndex个单词中的第strIndex个字母
  * @param regIndex
  *            regchar的匹配位置,比如匹配到了96758的7上,也就是regIndex==2.
  * @return
  */
private  OverflowMatchValue crossWords(ArrayList<String> names,
         String regString,  int  listIndex,  int  strIndex,  int  regIndex) {
         //在进入此方法时,第listIndex个单词的第strIndex的字母肯定是
         // 与regString的第regIndex个字母相等的
     OverflowMatchValue result =  new  OverflowMatchValue( 0 false );
     OverflowMatchValue reser =  new  OverflowMatchValue( 0 false ); //返回如果匹配到本单词的下一个字母能得到的匹配值
     OverflowMatchValue impul =  new  OverflowMatchValue( 0 false ); //返回如果匹配到下一个单词的第一个字母的匹配值
     
     // 仍然以【名字叫PanAnNing,输入pan(其实对比的是数字,这里转化成字母为了方便)】举例
     // 假设这时listIndex,strIndex,regIndex都是0,所以现在匹配的是p字母,它毫无疑问对应姓名的第一个P,
     // 那么下一步应该怎么做呢,由上面所说【保证下一个字符开始的一段字符能匹配最长的长度即可】
     // 也就是说,我们输入的pan中的第二个字母a匹配哪个位置将得到最优结果。这个盒子中显然有两种情况。
     // 一是匹配姓氏Pan中的a,另一个是匹配名字AnNing中的A。
     // reser就表示如果a匹配到Pan中的a最终的匹配值。
     // impul就表示如果a匹配到AnNing中的A得到的最终的匹配值。
     
     if  (regIndex < regString.length() -  1 ) {
         //如果还没匹配到最后一个字母,也就是regString还没匹配到最后一个,那么将检测如
         //果将regString的下一个字母放到哪里将得到最优结果
         char  nextChar = regString.charAt(regIndex +  1 );
         if  (listIndex < names.size() -  1
                 && nextChar == names.get(listIndex +  1 ).charAt( 0 )) {
             impul = crossWords(names, regString, listIndex +  1 0 ,
                     regIndex +  1 );
         }
         if  (strIndex < names.get(listIndex).length() -  1
                 && nextChar == names.get(listIndex).charAt(strIndex +  1 )) {
             reser = crossWords(names, regString, listIndex, strIndex +  1 ,
                     regIndex +  1 );
         }
         //如果上面两个条件都不成立,那么就表示本次匹配失败
     else  {
         result =  new  OverflowMatchValue((strIndex ==  0 ) ?  1  0 true );
         result.pairs.add( 0 new  PointPair(listIndex, strIndex));
     }
 
     if  (reser.matched || impul.matched) {
         //如果其中任意一个方式可以匹配,那么结果最大的那个就是最优结果
         if  (impul.crossed > reser.crossed) {
             result = impul;
         else  {
             result = reser;
         }
         result.matched =  true ;
         result.crossed = ((strIndex ==  0 ) ?  1  0 )
                 + Math.max(result.crossed, result.crossed);
         result.pairs.add( 0 new  PointPair(listIndex, strIndex));
     }
     return  result;
}
 
static  class  OverflowMatchValue {
     public  int  crossed =  0 ;
     public  boolean  matched =  false ;
     public  ArrayList<PointPair> pairs =  new  ArrayList<PointPair>();
 
     public  OverflowMatchValue( int  c,  boolean  m) {
         this .crossed = c;
         this .matched = m;
     }
}

3.后置首字母溢出匹配。(能不能想个好听的名字j_0004.gif

跟前置首字母溢出匹配基本一样,只不过匹配的第一个字母不再是姓的首字母。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private  ScoreAndHits backAcronymOverFlowMatch(String reg) {
         //跟上面差不多
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     for  ( int  i =  0 ; i < fullNameNumber.size(); i++) {
         ArrayList<String> names = fullNameNumber.get(i);
         ScoreAndHits tmp = backAcronymOverFlowMatch(names, reg);
         if  (tmp.score > scoreAndHits.score) {
             scoreAndHits = tmp;
             scoreAndHits.nameIndex = i;
         }
     }
     scoreAndHits.matchLevel = Level_Back_Acronym_Overflow;
     return  scoreAndHits;
}
 
private  ScoreAndHits backAcronymOverFlowMatch(ArrayList<String> names,
         String reg) {
     int  score =  0 ;
     int  punish =  0 ;
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , 0f,
             new  ArrayList<PointPair>());
     // 有可能会调用多次crossWords,取决于名字的长度。这是跟前面的不同
     for  ( int  i =  0 ; i < names.size(); i++) {
         String string = (String) names.get(i);
         if  (string.charAt( 0 ) == reg.charAt( 0 )) {
             OverflowMatchValue value = crossWords(names, reg, i,  0 0 );
             int  cross = value.crossed;
             int  lost = names.size() - cross;
             if  (cross > score || cross == score && punish > lost) {
                 scoreAndHits.pairs = value.pairs;
                 score = cross;
                 punish = lost;
             }
         }
     }
     if  (score >  0 ) {
         scoreAndHits.score = Match_Level_Back_Acronym_Overflow + score
                 * Match_Score_Reward - punish * Match_Miss_Punish;
         return  scoreAndHits;
     else  {
         return  new  ScoreAndHits(- 1 , 0f,  new  ArrayList<PointPair>());
     }
 
}

4.后置无头匹配。(难听就难听了,反正就那个意思)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private  ScoreAndHits backHeadlessParagraphMatch(String reg) {
     // TODO,如果此人有两个相似的号码,那么就只能匹配出一个来了,这是很显然不对的
     int  punish =  0 ;
     ScoreAndHits scoreAndHits =  new  ScoreAndHits(- 1 , -1f,
             new  ArrayList<PointPair>());
     scoreAndHits.matchLevel = Level_Headless;
     scoreAndHits.matchType = Match_Type_Phone;
     // 不匹配姓名
     for  ( int  i =  0 ; i < phones.size(); i++) {
         PhoneStruct phone = phones.get(i);
         int  sco = phone.phoneNumber.indexOf(reg);
         if  (sco >=  0 ) {
             int  lost = phone.phoneNumber.length() - reg.length();
             if  (scoreAndHits.score < sco || sco == scoreAndHits.score
                     && punish > lost) {
                 scoreAndHits.score = sco;
                 scoreAndHits.nameIndex = i;
                 punish = lost;
             }
             //pairs.add放到判断外面是因为有可能匹配到同一个人的多个手机号码。
             scoreAndHits.pairs.add( new  PointPair(i, sco));
         }
     }
     if  (scoreAndHits.score >=  0 ) {
         scoreAndHits.score = Match_Level_Headless - scoreAndHits.score
                 * Match_Score_Reward - punish * Match_Miss_Punish;
     }
     return  scoreAndHits;
}
 
//表示电话号码的一个静态类,将过滤掉开头的+86以及系统可能自动生成的“-”以及其他非数字的字符以便于搜索
public  static  class  PhoneStruct {
     public  String phoneNumber;
     public  int  phoneType;
     public  String displayType;
 
     public  PhoneStruct(String number,  int  type) {
         phoneNumber = number.replaceAll( "^\\+86" "" ).replaceAll( "[\\]+" ,
                 "" );
         phoneType = type;
     }
}




到这里已经可以得出输入字符串与联系人的匹配度了,剩下的事情就是调用和显示了,但是这不是本文的重点

有了匹配算法,下面是如何将搜索联系人并显示出来以及高亮显示匹配到的字符串。


1.搜索并显示联系人


    显示列表当然是使用ListView,使用自定义的ContactAdapter,ContactAdapter继承BaseAdapter,并实现了Filterable接口,此接口中有一个getFilter方法,返回一个过滤用的类Filter,Filter需要自己实现,我们就是通过这个Filter实现搜索的。


    Filter类有两个方法,publishResults和performFiltering方法,其中publishResults运行在UI线程,而performFiltering运行在其他线程,搜索的过程就在performFiltering中执行。


    下面是自己实现的Filter类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Override
public  Filter getFilter() {
     return  filter;
}
 
// 上一次搜索的字符串
private  String preQueryString =  "" ;
 
private  Filter filter =  new  Filter() {
     @Override
     protected  void  publishResults(CharSequence constraint,
             FilterResults results) {
         if  (results !=  null ) {
             if  (results.count >  0 ) {
                 notifyDataSetChanged();
             else  {
                 notifyDataSetInvalidated();
             }
         }
     }
 
     @Override
     synchronized  protected  FilterResults performFiltering(CharSequence constraint) {
         if  (TextUtils.isEmpty(constraint)
                 || preQueryString.equals(constraint)) {
             return  null ;
         }
         String queryString = constraint.toString();
         FilterResults results =  new  FilterResults();
         int  preLength = preQueryString.length();
         int  queryLength = queryString.length();
         ArrayList<Contact> baseList =  new  ArrayList<Contact>();
         ArrayList<Contact> resultList =  new  ArrayList<Contact>();
         if  (preLength >  0  && (preLength == queryLength -  1 )
                 && queryString.startsWith(preQueryString)) {
             //如果本次搜索的字符串是上次搜索的字符串开头,那么将只在contacts里面搜索(contacts是当前列表的数据集合)
             baseList = contacts;
         else  {
             //过滤所有联系人
             baseList = AllContacts;
         }
 
         for  (Iterator<Contact> iterator = baseList.iterator(); iterator
                 .hasNext();) {
             Contact contact = (Contact) iterator.next();
             if  (contact.match(queryString) >  0 ) {
                 resultList.add(contact);
             }
         }
         sortContact(resultList); // 这是ContactAdapter中的方法,将ContactAdapter的数据换成resultList。
         preQueryString = queryString;
         results.values = resultList;
         results.count = resultList.size();
         setContacts(resultList);
         return  results;
     }
};

    如果用户搜索的手速十分快的话将会带来线程同步的问题。在执行performFiltering的时候有可能正在执行ContactAdapter的getView方法,而match()方法是有可能改变Contact的数据的,这将导致显示出错。比如未匹配到结果的话,Contact的匹配结果的nameIndex会是-1,如果在上次搜索中某用户成功匹配,nameIndex=0,就意味着将取用户的第一种拼音组合做为匹配结果,但是如果手速过快,在执行getView之前就进行了下一次搜索,那么有可能这个联系人不再匹配,这里的nameIndex将会是-1,取第-1个拼音的时候就会报错。这里的解决方法很简单,并没有做过多的保证同步的工作(让getView,publishResults和performFiltering不互相打断貌似是很困难的),所以如果发现nameIndex不对,就直接不显示这个拼音,因为用户操作非常之快,他是无法发现也没必要关心这几十毫秒的显示不正常的。


    还有一个线程同步的问题,在notifyDataSetChanged之后,adapter会顺序执行getView,但是在getView的时候,setContacts可能又会执行,从而改变了contacts的长度,contacts.get(position)可能会发生越界的问题,因此这时候getView要捕获这个错误,返回一个空view,跟上次一样,空view存在时间很短,不会有人注意的……


    搜索某个单词的时候,使用getFilter.filter(queryString)即可实现搜索。剩下的不用多说,都是普通的adapter和listview的问题。


2.高亮显示匹配的字符串


    高亮显示匹配的字符串使用户知道是如何匹配的。比如输入pan得出结果PanAnNing的时候,高亮的是三个首字母PanAnNing.高亮这里用的是SpannableStringBuilder。


    高亮方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
if  (contact.matchValue.matchLevel == Contact.Level_Complete) {
     //如果是完全匹配,那么只要全部高亮对应的姓名拼音或者电话号码就OK了
     if  (contact.matchValue.matchType == Contact.Match_Type_Name) {
         String str = contact.fullNamesString.get(
                 contact.matchValue.nameIndex).replaceAll( " " "" );
         SpannableStringBuilder builder =  new  SpannableStringBuilder(
                 str);
         ForegroundColorSpan redSpan =  new  ForegroundColorSpan(
                 Color.RED);
         builder.setSpan(redSpan,  0 , str.length(),
                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
         pinyinTextView.setText(builder);
     else  {
         shouldDisplayMorePhones =  false ;
         String str = contact.getPhones().get(
                 contact.matchValue.nameIndex).phoneNumber;
         SpannableStringBuilder builder =  new  SpannableStringBuilder(
                 str);
         ForegroundColorSpan redSpan =  new  ForegroundColorSpan(
                 Color.RED);
         builder.setSpan(redSpan,  0 , str.length(),
                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
         phoneTextView.setText(builder);
     }
else  if  (contact.matchValue.matchLevel == Contact.Level_Headless) {
     //如果是后置无头匹配,那么高亮从strIndex开始的regString长度的一串就行了
     shouldDisplayMorePhones =  false ;
     String str = contact.getPhones().get(
             contact.matchValue.nameIndex).phoneNumber;
     SpannableStringBuilder builder =  new  SpannableStringBuilder(str);
     ForegroundColorSpan redSpan =  new  ForegroundColorSpan(Color.RED);
     builder.setSpan(redSpan,
             contact.matchValue.pairs.get( 0 ).strIndex,
             contact.matchValue.pairs.get( 0 ).strIndex
                     + contact.matchValue.reg.length(),
             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     phoneTextView.setText(builder);
     for  ( int  i =  1 ; i < contact.matchValue.pairs.size(); i++) {
         int  idx = contact.matchValue.pairs.get(i).listIndex;
         PhoneStruct phoneStruct = contact.getPhones().get(idx);
         PhoneView phoneView =  new  PhoneView(getContext());
         phoneView.setPhone(phoneStruct, contact.matchValue.reg);
         phoneViews.addView(phoneView);
     }
else  {
     // 剩下的情况就是两个首字母匹配了。首字母匹配到的字符串位置不是连续的
     // 匹配到的字母一个一个记录在contact.matchValue.pairs里面
     // 所以要先将contact.matchValue.pairs里的一个个不连续的字母连接成几个字符串
     String str = contact.fullNamesString.get(
             contact.matchValue.nameIndex).replaceAll( " " "" );
     ArrayList<PointPair> pa = getColoredString(
             contact.fullNameNumber
                     .get(contact.matchValue.nameIndex),
             contact.matchValue.pairs,  "#FF0000" );
     SpannableStringBuilder builder =  new  SpannableStringBuilder(str);
     for  (Iterator<PointPair> iterator = pa.iterator(); iterator
             .hasNext();) {
         PointPair pointPair = iterator.next();
         builder.setSpan( new  ForegroundColorSpan(Color.RED),
                 pointPair.listIndex, pointPair.strIndex,
                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
     }
     pinyinTextView.setText(builder);
}
 
// getColoredString是将PointPairs列表单个的字符转化成几个字符串范围。这时候返回的PointPair的listIndex
// 变成了字符串开关的位置,strIndex变成了长度。builder.setSpan将使这几段范围内的字符高亮
private  ArrayList<PointPair> getColoredString(ArrayList<String> strings,
         ArrayList<PointPair> pairs, String color) {
     int  k =  0 ;
     int  idx = - 1 ;
     int  crtHead = - 1 ;
     int  crtTail = - 1 ;
     ArrayList<PointPair> ps =  new  ArrayList<PointPair>();
     for  ( int  i =  0 ; i < strings.size(); i++) {
         String str = strings.get(i);
         for  ( int  j =  0 ; j < str.length() && k < pairs.size(); j++) {
             idx++;
             if  (pairs.get(k).listIndex == i && pairs.get(k).strIndex == j) {
                 if  (crtHead == - 1 ) {
                     crtHead = idx;
                     crtTail = idx +  1 ;
                 else  {
                     if  (crtTail == idx) {
                         crtTail = idx +  1 ;
                     }
                 }
                 k++;
             else  {
                 if  (crtHead != - 1 ) {
                     ps.add( new  PointPair(crtHead, crtTail));
                     crtHead = - 1 ;
                     crtTail = - 1 ;
                 }
             }
         }
     }
     if  (crtHead != - 1 ) {
         ps.add( new  PointPair(crtHead, crtTail));
         crtHead = - 1 ;
         crtTail = - 1 ;
     }
     return  ps;
}

北京大学oj题目,已提交AC。原题目如下: Description Businesses like to have memorable telephone numbers. One way to make a telephone number memorable is to have it spell a memorable word or phrase. For example, you can call the University of Waterloo by dialing the memorable TUT-GLOP. Sometimes only part of the number is used to spell a word. When you get back to your hotel tonight you can order a pizza from Gino's by dialing 310-GINO. Another way to make a telephone number memorable is to group the digits in a memorable way. You could order your pizza from Pizza Hut by calling their ``three tens'' number 3-10-10-10. The standard form of a telephone number is seven decimal digits with a hyphen between the third and fourth digits (e.g. 888-1200). The keypad of a phone supplies the mapping of letters to numbers, as follows: A, B, and C map to 2 D, E, and F map to 3 G, H, and I map to 4 J, K, and L map to 5 M, N, and O map to 6 P, R, and S map to 7 T, U, and V map to 8 W, X, and Y map to 9 There is no mapping for Q or Z. Hyphens are not dialed, and can be added and removed as necessary. The standard form of TUT-GLOP is 888-4567, the standard form of 310-GINO is 310-4466, and the standard form of 3-10-10-10 is 310-1010. Two telephone numbers are equivalent if they have the same standard form. (They dial the same number.) Your company is compiling a directory of telephone numbers from local businesses. As part of the quality control process you want to check that no two (or more) businesses in the directory have the same telephone number. Input The input will consist of one case. The first line of the input specifies the number of telephone numbers in the directory (up to 100,000) as a positive integer alone on the line. The remaining lines list the telephone numbers in the directory, with each number alone on a line. Each telephone number consists of a string composed of decimal digits, uppercase letters (excluding Q and Z) and hyphens. Exactly seven of the characters in the string will be digits or letters. Output Generate a line of output for each telephone number that appears more than once in any form. The line should give the telephone number in standard form, followed by a space, followed by the number of times the telephone number appears in the directory. Arrange the output lines by telephone number in ascending lexicographical order. If there are no duplicates in the input print the line: No duplicates. Sample Input 12 4873279 ITS-EASY 888-4567 3-10-10-10 888-GLOP TUT-GLOP 967-11-11 310-GINO F101010 888-1200 -4-8-7-3-2-7-9- 487-3279 Sample Output 310-1010 2 487-3279 4 888-4567 3
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值