最近看了一下河西朝雄先生的演算法實習應用,裡面探討到核對字串的處理,因此在這邊
就提一下一般的核對法及Boyer-Moore演算法,最後則是Java String indexOf原始碼的比對方式
,測試一下它們的執行時間。
基本上,字串的核對在這邊我們的例題是只針對英文字母的字串來處理(不是中文字orz),
如下:
1. 一般核對法
基本上我們要核對text字串是否有符合key的部分,最簡單的方式為先看key的長度
,這邊為5,那麼一開始從text的0 ~ 5字串位置開始核對,即
text.substring(0 , 5) => 取出"Hello",與"array"比對結果不符合!!
再來你無法保證1 ~ 6的位置取出的字串會不符合,再來只能摸摸鼻子繼續努力!
text.substring(1 , 6) => 取出"ello!",與"array"比對結果還是不符合!!
就這樣一路比對下去,不放過任何一個機會!
PS. 字串的index位置從0(字元陣列)開始,以endIndex - startIndex來表示要取的長度
若0 ~ 5,表示0開始,取長度5-0 = 5,以此類推!
從上面的source code會得到如下的結果,
2. Boyer-Moore 法
這個方法,一樣先從text的 0 ~ 5的字串位置取出來與key比對,不過有些許差異,
text.substring(0 , 5) => 取出"Hello",與"array"比對結果不符合!!
但是下一次決定要去取text的字串位置就不一定是從1 ~ 6囉!
會根據當下第5個字元是啥,上面是"Hello" => 'o',那麼表示該字元與要核對的字串
"array"是毫無任何關係,因此想都不用想地可以向後推進5個字元,以6 ~ 10的位置來比對
PS. 由於取1 ~ 6的位置還是會包含第5個位置的'o',那麼也就不用核對囉!
text.substring(5 , 10) => 取出"! arr",與"array"比對結果還是不符合!!
不過,"! arr"的最後一個字元為'r',這個字元與"array"有關係,因此接下來只要推進
2個字元(為什麼?下面會提到)就有可能會得到與字串"array"相符合的字串!
text.substring(7 , 12) => 取出"array",與"array"比對結果符合!!
再來,"array"的最後一個字元'y',這個字元雖然與"array"有關係,不過由於它已經是
key的最後一個字元,因此視為與其他非相關的字元一樣碰到它也是要推進5個字元!
因此,採用Boyer-Moore法,核對的方式是不固定的次數,但若以第一種方法來看的話
假設text長度為39,那麼至少得比對34次(39 - 5(key length) = 34)之多!
而我們事先要知道碰到'r'只需前進2個字元,若是'h'字元需前進5個字元,因此要先建立這個
前進陣列。而為什麼'r'只要前進兩個字元呢? 因為就"array"來說,若上一個是取到"! arr",那
麼我可以猜測出下兩個字元可能為剩下的"ay",如此只需前進2個字元。
PS. 若key字串內有兩個相同的字元,如"array",則'a'字元的前進長度以越後面的字元為主
,即一開始'a' => 4,但後來又有個'a' => 1,那麼以較小的值為主,比較保險!
在這邊我建立的record array長度為127,record的index為字元ascii的十進位數值大小,
如'a' 換算ascii為97 => record[97] = 1。
從上面的source code會得到如下的結果,
在執行時間上明顯比第一種方法快,有可能有幾種原因:
1. while(p < n){ } 並非執行34次(第一種則是)
2. 迴圈執行次數少,相對地if(key.equals(find))的equals的比對次數也會減少。請想想
當你呼叫Java equals的method,其實就是利用Java幫你去比對equals內的字元是否與
該字串本身是相等的!
equals method的片段程式碼如下:
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
n => 字串本身的長度
v1 => 字串本身的字元陣列
v2 => 另一個字串轉換後的字元陣列
因此多呼叫equals等於在多花比對時間!
3. Java indexOf source code
我們都知道Java String class的indexOf本身就有提供字串搜尋的功能,會回傳當下搜尋字串
的第一個字元位置!
整理了一下他的source code處理的概念,套用到這個字串核對的例子,如下:
1. 首先取得key與text相符的第一個字元,如下
while(++i <= max && text.charAt(i) != first);
2. 接著,假設取得的第一個字元,i = 7,會進入此迴圈比對接續的字元
for (int k = 1 ; j < end && text.charAt(j) == key.charAt(k); k++, j++);
若接續的第2 ~ 5的字元有任一一個不符就跳出,若成功的一一核對完畢,
那麼表示j = end,就符合if(j == end)的條件!
從上面的source code會得到如下的結果,
從上面的三種字串核對的方法比較結果,發現第三種方法是最快的,再來是2 , 1
,第三種會比第二種更快有可能是,indexOf內完全沒有額外用到equals字串比對,完全
是自己本身的字元比對
當然,就空間複雜度而言,最差的首當第一種方法,就substring的使用頻率最高,
由於substring回傳回來的字串是重新new一個String object。如下:
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
,除非他substring取的範圍為0 ~ 字串的長度,才回傳他自己本身XDDD
因此substring應該少用之比較好!
最後,關於執行效率與空間複雜度好壞的討論純屬個人的推論,僅供參考XDD
就提一下一般的核對法及Boyer-Moore演算法,最後則是Java String indexOf原始碼的比對方式
,測試一下它們的執行時間。
基本上,字串的核對在這邊我們的例題是只針對英文字母的字串來處理(不是中文字orz),
如下:
String text = "Hello! array. Learning array knowledge.";
String key = "array";
假設被核對的字串為變數text,欲核對的字串為key,接下來以三種方法來實作之。1. 一般核對法
基本上我們要核對text字串是否有符合key的部分,最簡單的方式為先看key的長度
,這邊為5,那麼一開始從text的0 ~ 5字串位置開始核對,即
text.substring(0 , 5) => 取出"Hello",與"array"比對結果不符合!!
再來你無法保證1 ~ 6的位置取出的字串會不符合,再來只能摸摸鼻子繼續努力!
text.substring(1 , 6) => 取出"ello!",與"array"比對結果還是不符合!!
就這樣一路比對下去,不放過任何一個機會!
PS. 字串的index位置從0(字元陣列)開始,以endIndex - startIndex來表示要取的長度
若0 ~ 5,表示0開始,取長度5-0 = 5,以此類推!
public void stringMatchMethod1(String key, String text){
int n = text.length()-key.length();
int keyLen = key.length();
long startTime = System.currentTimeMillis();
for(int i = 0 ; i < n ; i++){
String find = text.substring(i, i+keyLen);
if(key.equals(find)){
System.out.println("*** Match:"+find+" => "+text.substring(i));
}else{
System.out.println("Not Match:"+find);
}
}
float time = (System.currentTimeMillis()-startTime) /1000.0f;
System.out.println("執行時間:"+time+"秒");
}
從上面的source code會得到如下的結果,
Not Match:Hello Not Match:ello! Not Match:llo! Not Match:lo! a Not Match:o! ar Not Match:! arr Not Match: arra *** Match:array => array. Learning array knowledge. Not Match:rray. Not Match:ray. Not Match:ay. L Not Match:y. Le Not Match:. Lea Not Match: Lear Not Match:Learn Not Match:earni Not Match:arnin Not Match:rning Not Match:ning Not Match:ing a Not Match:ng ar Not Match:g arr Not Match: arra *** Match:array => array knowledge. Not Match:rray Not Match:ray k Not Match:ay kn Not Match:y kno Not Match: know Not Match:knowl Not Match:nowle Not Match:owled Not Match:wledg Not Match:ledge 執行時間:0.005秒
2. Boyer-Moore 法
這個方法,一樣先從text的 0 ~ 5的字串位置取出來與key比對,不過有些許差異,
text.substring(0 , 5) => 取出"Hello",與"array"比對結果不符合!!
但是下一次決定要去取text的字串位置就不一定是從1 ~ 6囉!
會根據當下第5個字元是啥,上面是"Hello" => 'o',那麼表示該字元與要核對的字串
"array"是毫無任何關係,因此想都不用想地可以向後推進5個字元,以6 ~ 10的位置來比對
PS. 由於取1 ~ 6的位置還是會包含第5個位置的'o',那麼也就不用核對囉!
text.substring(5 , 10) => 取出"! arr",與"array"比對結果還是不符合!!
不過,"! arr"的最後一個字元為'r',這個字元與"array"有關係,因此接下來只要推進
2個字元(為什麼?下面會提到)就有可能會得到與字串"array"相符合的字串!
text.substring(7 , 12) => 取出"array",與"array"比對結果符合!!
再來,"array"的最後一個字元'y',這個字元雖然與"array"有關係,不過由於它已經是
key的最後一個字元,因此視為與其他非相關的字元一樣碰到它也是要推進5個字元!
因此,採用Boyer-Moore法,核對的方式是不固定的次數,但若以第一種方法來看的話
假設text長度為39,那麼至少得比對34次(39 - 5(key length) = 34)之多!
而我們事先要知道碰到'r'只需前進2個字元,若是'h'字元需前進5個字元,因此要先建立這個
前進陣列。而為什麼'r'只要前進兩個字元呢? 因為就"array"來說,若上一個是取到"! arr",那
麼我可以猜測出下兩個字元可能為剩下的"ay",如此只需前進2個字元。
PS. 若key字串內有兩個相同的字元,如"array",則'a'字元的前進長度以越後面的字元為主
,即一開始'a' => 4,但後來又有個'a' => 1,那麼以較小的值為主,比較保險!
在這邊我建立的record array長度為127,record的index為字元ascii的十進位數值大小,
如'a' 換算ascii為97 => record[97] = 1。
public void createTable(String key){
int len = key.length();
for(int i = 0 ; i < records.length ; i++){
records[i] = len;
}
for(int i = 0 ; i < len-1 ; i++){ //last char set default value
records[key.charAt(i)] = len-i-1;
}
}
public void stringMatchMethod2(String key, String text){
createTable(key);
int n = text.length();
int m = key.length();
int p = m-1;
long startTime = System.currentTimeMillis();
while(p < n){
String find = text.substring(p-m+1, p+1);
if(key.equals(find)){
System.out.println("*** Match:"+find+" => "+text.substring(p-m+1));
}else{
System.out.println("Not Match:"+find);
}
p = p + records[text.charAt(p)];
}
float time = (System.currentTimeMillis()-startTime) /1000.0f;
System.out.println("執行時間:"+time+"秒");
}
從上面的source code會得到如下的結果,
Not Match:Hello Not Match:! arr *** Match:array => array. Learning array knowledge. Not Match:. Lea Not Match: Lear Not Match:earni Not Match:ng ar Not Match: arra *** Match:array => array knowledge. Not Match: know Not Match:ledge 執行時間:0.002秒
在執行時間上明顯比第一種方法快,有可能有幾種原因:
1. while(p < n){ } 並非執行34次(第一種則是)
2. 迴圈執行次數少,相對地if(key.equals(find))的equals的比對次數也會減少。請想想
當你呼叫Java equals的method,其實就是利用Java幫你去比對equals內的字元是否與
該字串本身是相等的!
equals method的片段程式碼如下:
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
n => 字串本身的長度
v1 => 字串本身的字元陣列
v2 => 另一個字串轉換後的字元陣列
因此多呼叫equals等於在多花比對時間!
3. Java indexOf source code
我們都知道Java String class的indexOf本身就有提供字串搜尋的功能,會回傳當下搜尋字串
的第一個字元位置!
整理了一下他的source code處理的概念,套用到這個字串核對的例子,如下:
public void stringMatchMethod3(String key, String text){
int n = text.length();
int m = key.length();
char first = key.charAt(0);
int max = n - m;
long startTime = System.currentTimeMillis();
for(int i = 0 ; i < max ; i++){
if(text.charAt(i) != first){
while(++i <= max && text.charAt(i) != first);
}
if(i <= max){
int j = i + 1; //second char start
int end = j + m - 1;
for (int k = 1 ; j < end && text.charAt(j) == key.charAt(k); k++, j++);
if(j == end)
System.out.println("*** Match:"+text.substring(i, end));
else
System.out.println("Not Match:"+text.substring(i, end));
}
}
float time = (System.currentTimeMillis()-startTime) /1000.0f;
System.out.println("執行時間:"+time+"秒");
}
上面的程式是indexOf method的實作方法(Java Source code),步驟如下:1. 首先取得key與text相符的第一個字元,如下
while(++i <= max && text.charAt(i) != first);
2. 接著,假設取得的第一個字元,i = 7,會進入此迴圈比對接續的字元
for (int k = 1 ; j < end && text.charAt(j) == key.charAt(k); k++, j++);
若接續的第2 ~ 5的字元有任一一個不符就跳出,若成功的一一核對完畢,
那麼表示j = end,就符合if(j == end)的條件!
從上面的source code會得到如下的結果,
*** Match:array Not Match:ay. L Not Match:arnin *** Match:array Not Match:ay kn 執行時間:0.001秒========================結論==========================
從上面的三種字串核對的方法比較結果,發現第三種方法是最快的,再來是2 , 1
,第三種會比第二種更快有可能是,indexOf內完全沒有額外用到equals字串比對,完全
是自己本身的字元比對
當然,就空間複雜度而言,最差的首當第一種方法,就substring的使用頻率最高,
由於substring回傳回來的字串是重新new一個String object。如下:
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
,除非他substring取的範圍為0 ~ 字串的長度,才回傳他自己本身XDDD
因此substring應該少用之比較好!
最後,關於執行效率與空間複雜度好壞的討論純屬個人的推論,僅供參考XDD
留言
張貼留言