[LeetCode][14]Longest Common Prefix解析 兩種演算法和底層原始碼的深入對比-Java實現
Q:
Write a function to find the longest common prefix string amongst an array of strings.
A:
這題的大概意思就是說給你一組字串找出其中最長的哪個通用的前綴出來。這個東西不難找,但是如何找的又快又好不簡單。其實這題本來就是easy題,但是卻讓我聯想到了《資料結構與演算法分析》上的一道題目,那道題目是這樣的:
給一個8900個字的字典,從中間找出類似abc、bbc、abb這樣單詞中只有一個字母不同的單詞進行分類,最差的演算法,用的大家都想到的遍歷演算法進行分類耗時90多秒,更新到第四種演算法的時候只用4秒鐘了(最後還有一種3秒鐘的方法)。他用的最關鍵的改變就是把單詞切出來一個字母然後存在TreeMap裡,如果TreeMap有的話就在裡面的size上+1,最後找出size大於2的值,當然我之前說過有一種3秒鐘的方法就是使用的hashmap。
關於hashmap和treemap的區別我會在我的資料結構與演算法分析複習的部分講。如果有興趣請關注我的部落格。
那我們來想一想我們可不可以用這種方法增加速度呢?
首先我們需要找出字首的範圍,有三種選擇:
1、找出最小的字元,遍歷每個字母當作字首。O(n)
2、找出第一個字元,遍歷每個字母當作字首O(1)
3、找出前兩個字元的共同部分,遍歷字母當作字首O(min(a.length,b.length).
對於我們得到的字首範圍我們可以得到她們的數量size。
遍歷size來分割接下來的字串,比如說[0,1],[0,2],[0,3]這樣O(size*n)
最後找出這麼多字元中誰的size==總String數量並且她的值最大就是我們要的最長字首了O(size)
這樣我們最後最好的時間複雜度的情況應該是O(size*n)並且0<size<=n,size為整數,也就是說O(size*n)>O(n)
那麼我們來看第二種方法:
首先使用前兩個對比,得出通用字首prefix。O(prefix.length)
使用通用字首根接下來的遍歷,得出跟prefix的通用字首並賦值給他自己O(n)
最後返回prefix
她的時間複雜度是O(prefix.length*n)
O(prefix.length*n)和O(size*n)誰大誰小真的不好比較。我們來分析一下
如果size的確定使用第三種辦法,那麼應該是跟第二種方法中的第一次取得prefix相等即O(prefix.length)=O(min(a.length,b.length).
對於第二種方法之後的prefix.length肯定小於第一次的prefix.length。但是對於第一種方法而言第一次的prefix.length等於之後的size。選取第一種裡面的1和2的話size肯定是大的。所以經過對比分析第二種方法時間複雜度更小。
所以我們使用第二種方法計算,程式碼如下:
public static void main(String[] args){
String[] strs= {"aa","a"};
System.out.println(method(strs));
}
private static String method(String[] strs) {
// TODO Auto-generated method stub
if(strs.length==0)
return "";
String prefix =strs[0];
for(int i = 1;i<strs.length;i++){
//char thisc = prefix.charAt(0);
if(prefix.length()>strs[i].length()){
prefix = prefix.substring(0, strs[i].length());
}
for(int j = 0;j< prefix.length();j++){
if(prefix.charAt(j)!=strs[i].charAt(j)){//自負不相等擷取prefix
prefix = prefix.substring(0, j);
}
}
}
return prefix;
}
當然這個是原版程式碼,後來發現要5ms,我在leetcode上參考了一下,修改成瞭如下程式碼:
public static void main(String[] args){
String[] strs= {"aa","a"};
System.out.println(method(strs));
}
private static String method(String[] strs) {
// TODO Auto-generated method stub
if(strs.length==0)
return "";
String prefix =strs[0];
for(int i = 1;i<strs.length;i++){
while(strs[i].indexOf(prefix) != 0){
prefix = prefix.substring(0,prefix.length()-1);
}
}
return prefix;
}
這樣耗時1ms,問題就在一個方法上String.indexOf,為什麼這個方法效率那麼高呢?
我們看一下原始碼:
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
預設引數:
value, 0, value.length,str.value, 0, str.value.length, 0
我們可以看出來這段程式碼的思路就是遍歷到第一個字元開始然後對比,都相同就返回index否則返回-1.
那我們根據這個思路修改一下原本的程式碼試試。
if(strs.length==0)
return "";
String prefix =strs[0];
for(int i = 1;i<strs.length;i++){
//if(prefix.length()>strs[i].length()){
// prefix = prefix.substring(0, strs[i].length());
//}
int max= Math.min(prefix.length(), strs[i].length());
int j = 0;
for(;j< max&&prefix.charAt(j)==strs[i].charAt(j);j++){}
prefix = prefix.substring(0, j);
}
return prefix;
最後我把原始碼改成這樣也還有4ms,然後我仔細對比原始碼發現問題應該出在String.charat()上面了,程式碼如下:
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
裡面有兩個判斷非常耗費時間,我們改成如下程式碼
if(strs.length==0)
return "";
String prefix =strs[0];
for(int i = 1;i<strs.length;i++){
//if(prefix.length()>strs[i].length()){
// prefix = prefix.substring(0, strs[i].length());
//}
char[] str = strs[i].toCharArray();
int max= Math.min(prefix.length(), str.length);
int j = 0;
for(;j< max&&prefix.charAt(j)==str[j];j++){}
prefix = prefix.substring(0, j);
}
return prefix;
就變成了2ms,果然問題出在這裡,如果我們不使用tochararray()方法的話就應該是1ms了,這說明我們的思路和演算法都沒有錯,底層還有很多需要注意的東西