第二章、字符串是否包含及相關問題擴展

 

程序員編程藝術:第二章、字符串是否包含及相關問題擴展


作者:July,yansha。
時間:二零一一年四月二十三日。
致謝:老夢,nossiac,Hession,Oliver,luuillu,雨翔,啊菜,及微軟100題實現小組所有成員。

微博:http://weibo.com/julyweibo
出處:http://blog.csdn.net/v_JULY_v
-------------------------------------------

目錄
曲之前奏
第一節、一道倆個字符串是否包含的問題
  1.1、O(n*m)的輪詢方法
  1.2、O(mlogm)+O(nlogn)+O(m+n)的排序方法
  1.3、O(n+m)的計數排序方法
第二節
  2.1、O(n+m)的hashtable的方法
  2.2、O(n+m)的數組存儲方法
第三節、O(n)到O(n+m)的素數方法
第四節、字符串是否包含問題的繼續補充
  4.1、Bit-map
  4.2、移位操作
第五節、字符串相關問題擴展
  5.1、字符串匹配問題
  5.2、在字符串中查找子串
    擴展:在一個字符串中找到第一個只出現一次的字符
  5.3、字符串轉換爲整數
  5.4、字符串拷貝

前奏

    前一章,請見這:程序員面試題狂想曲:第一章、左旋轉字符串。本章裏出現的所有代碼及所有思路的實現,在此之前,整個網上都是沒有的

    文中的思路,聰明點點的都能想到,巧的思路,大師也已奉獻了。如果你有更好的思路,歡迎提供。如果你對此狂想曲系列有任何建議,歡迎微博上交流或來信指導。任何人,有任何問題,歡迎隨時不吝指正。

    如果此狂想曲系列對你有所幫助,我會非常之高興,並將讓我有了永久堅持寫下去的動力。謝謝。


第一節、一道倆個字符串是否包含的問題
1.0、題目描述:
假設這有一個各種字母組成的字符串,假設這還有另外一個字符串,而且這個字符串裏的字母數相對少一些。從算法是講,什麼方法能最快的查出所有小字符串裏的字母在大字符串裏都有?

比如,如果是下面兩個字符串:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPOM
答案是true,所有在string2裏的字母string1也都有。
  
如果是下面兩個字符串:  
String 1: ABCDEFGHLMNOPQRS   
String 2: DCGSRQPOZ  
答案是false,因爲第二個字符串裏的Z字母不在第一個字符串裏。

    點評:
    1、題目描述雖長,但題意簡單明瞭,就是給定一長一短的倆個字符串A,B,假設A長B短,現在,要你判斷B是否包含在字符串A中,即B?(-A。

    2、題意雖簡單,但實現起來並不輕鬆,且當如果面試官步步緊逼,一個一個否決你能想到的方法,要你給出更好、最好的方案時,你恐怕就要傷不少腦筋了。

    ok,在繼續往下閱讀之前,您最好先想個幾分鐘,看你能想到的最好方案是什麼,是否與本文最後實現的方法一致。


1.1、O(n*m)的輪詢方法

判斷string2中的字符是否在string1中?:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPOM

    判斷一個字符串是否在另一個字符串中,最直觀也是最簡單的思路是,針對第二個字符串string2中每一個字符,一一與第一個字符串string1中每個字符依次輪詢比較,看它是否在第一個字符串string1中。

    假設n是字符串string1的長度,m是字符串string2的長度,那麼此算法,需要O(n*m)次操作,拿上面的例子來說,最壞的情況下將會有16*8 = 128次操作

    我們不難寫出以下代碼:

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. int CompareSting(string LongSting,string ShortSting)  
  5. {  
  6.     for (int i=0; i<ShortString.length(); i++)  
  7.     {  
  8.         for (int j=0; j<LongString.length(); j++)  //O(n*m)  
  9.         {  
  10.             if (LongString[i] == ShortString[j])  //一一比較  
  11.             {  
  12.                 break;  
  13.             }  
  14.               
  15.         }  
  16.         if (j==LongString.length())  
  17.         {  
  18.             cout << "false" << endl;  
  19.             return 0;  
  20.         }  
  21.     }  
  22.     cout << "true" << endl;  
  23.     return 1;  
  24. }  
  25.   
  26. int main()   
  27. {   
  28.     string LongString="ABCDEFGHLMNOPQRS";  
  29.     string ShortString="DCGSRQPOM";  
  30.     compare(LongString,ShortString);  
  31.     return 0;  
  32. }    

 上述代碼的時間複雜度爲O(n*m),顯然,時間開銷太大,我們需要找到一種更好的辦法。

(網友acs713在本文評論下指出:個人的代碼風格不規範,的確如此,後來看過<<代碼大全>>之後,此感尤甚。個人會不斷完善和規範此類代碼風格。有任何問題,歡迎隨時指正。謝謝大家。)

 

1.2、O(mlogm)+O(nlogn)+O(m+n)的排序方法
    一個稍微好一點的方案是先對這兩個字符串的字母進行排序,然後同時對兩個字串依次輪詢。兩個字串的排序需要(常規情況)O(m log m) + O(n log n)次操作,之後的線性掃描需要O(m+n)次操作

    同樣拿上面的字串做例子,將會需要16*4 + 8*3 = 88加上對兩個字串線性掃描的16 + 8 = 24的操作。(隨着字串長度的增長,你會發現這個算法的效果會越來越好)

    關於採用何種排序方法,我們採用最常用的快速排序,下面的快速排序的代碼用的是以前寫的,比較好懂,並且,我執意不用庫函數的qsort代碼。唯一的問題是,此前寫的代碼是針對整數進行排序的,不過,難不倒我們,稍微改一下參數,即可,如下:

  1. //copyright@ 2011 July && yansha  
  2. //July,updated,2011.04.23.  
  3. #include <iostream>  
  4. #include <string>  
  5. using namespace std;  
  6.   
  7. //以前的註釋,還讓它保留着  
  8. int partition(string &str,int lo,int hi)   
  9. {  
  10.     int key = str[hi];      //以最後一個元素,data[hi]爲主元  
  11.     int i = lo - 1;  
  12.     for(int j = lo; j < hi; j++) ///注,j從p指向的是r-1,不是r。  
  13.     {  
  14.         if(str[j] <= key)  
  15.         {  
  16.             i++;  
  17.             swap(str[i], str[j]);  
  18.         }  
  19.     }  
  20.     swap(str[i+1], str[hi]);    //不能改爲swap(&data[i+1],&key)  
  21.     return i + 1;   
  22. }  
  23.   
  24. //遞歸調用上述partition過程,完成排序。  
  25. void quicksort(string &str, int lo, int hi)  
  26. {  
  27.     if (lo < hi)  
  28.     {  
  29.         int k = partition(str, lo, hi);  
  30.         quicksort(str, lo, k - 1);  
  31.         quicksort(str, k + 1, hi);  
  32.     }  
  33. }  
  34.   
  35. //比較,上述排序O(m log m) + O(n log n),加上下面的O(m+n),  
  36. //時間複雜度總計爲:O(mlogm)+O(nlogn)+O(m+n)。  
  37. void compare(string str1,string str2)  
  38. {  
  39.     int posOne = 0;  
  40.     int posTwo = 0;  
  41.     while (posTwo < str2.length() && posOne < str1.length())  
  42.     {  
  43.         while (str1[posOne] < str2[posTwo] && posOne < str1.length() - 1)  
  44.             posOne++;  
  45.         //如果和str2相等,那就不能動。只有比str2小,才能動。  
  46.           
  47.         if (str1[posOne] != str2[posTwo])  
  48.             break;  
  49.           
  50.         //posOne++;     
  51.         //歸併的時候,str1[str1Pos] == str[str2Pos]的時候,只能str2Pos++,str1Pos不可以自增。  
  52.         //多謝helloword指正。  
  53.   
  54.         posTwo++;  
  55.     }  
  56.                   
  57.     if (posTwo == str2.length())  
  58.         cout << "true" << endl;  
  59.     else  
  60.         cout << "false" << endl;  
  61. }  
  62.   
  63. int main()   
  64. {   
  65.     string str1 = "ABCDEFGHLMNOPQRS";  
  66.     string str2 = "DCGDSRQPOM";    
  67.     //之前上面加了那句posOne++之所以有bug,是因爲,@helloword:  
  68.     //因爲str1如果也只有一個D,一旦posOne++,就到了下一個不是'D'的字符上去了,  
  69.     //而str2有倆D,posTwo++後,下一個字符還是'D',就不等了,出現誤判。  
  70.   
  71.     quicksort(str1, 0, str1.length() - 1);  
  72.     quicksort(str2, 0, str2.length() - 1);  //先排序  
  73.     compare(str1, str2);                    //後線性掃描  
  74.     return 0;  
  75. }  
    

1.3、O(n+m)的計數排序方法

    此方案與上述思路相比,就是在排序的時候採用線性時間的計數排序方法,排序O(n+m),線性掃描O(n+m),總計時間複雜度爲:O(n+m)+O(n+m)=O(n+m)

    代碼如下:

  1. #include <iostream>  
  2. #include <string>  
  3. using namespace std;  
  4.   
  5. // 計數排序,O(n+m)  
  6. void CounterSort(string str, string &help_str)  
  7. {  
  8.     // 輔助計數數組  
  9.     int help[26] = {0};  
  10.   
  11.     // help[index]存放了等於index + 'A'的元素個數  
  12.     for (int i = 0; i < str.length(); i++)  
  13.     {  
  14.         int index = str[i] - 'A';  
  15.         help[index]++;  
  16.     }  
  17.   
  18.     // 求出每個元素對應的最終位置  
  19.     for (int j = 1; j < 26; j++)  
  20.         help[j] += help[j-1];  
  21.   
  22.     // 把每個元素放到其對應的最終位置  
  23.     for (int k = str.length() - 1; k >= 0; k--)  
  24.     {  
  25.         int index = str[k] - 'A';  
  26.         int pos = help[index] - 1;  
  27.         help_str[pos] = str[k];  
  28.         help[index]--;  
  29.     }  
  30. }  
  31.   
  32. //線性掃描O(n+m)  
  33. void Compare(string long_str,string short_str)  
  34. {  
  35.     int pos_long = 0;  
  36.     int pos_short = 0;  
  37.     while (pos_short < short_str.length() && pos_long < long_str.length())  
  38.     {  
  39.         // 如果pos_long遞增直到long_str[pos_long] >= short_str[pos_short]  
  40.         while (long_str[pos_long] < short_str[pos_short] && pos_long < long_str.length  
  41.   
  42. () - 1)  
  43.             pos_long++;  
  44.           
  45.         // 如果short_str有連續重複的字符,pos_short遞增  
  46.         while (short_str[pos_short] == short_str[pos_short+1])  
  47.             pos_short++;  
  48.   
  49.         if (long_str[pos_long] != short_str[pos_short])  
  50.             break;  
  51.           
  52.         pos_long++;  
  53.         pos_short++;  
  54.     }  
  55.       
  56.     if (pos_short == short_str.length())  
  57.         cout << "true" << endl;  
  58.     else  
  59.         cout << "false" << endl;  
  60. }  
  61.   
  62. int main()  
  63. {  
  64.     string strOne = "ABCDAK";  
  65.     string strTwo = "A";  
  66.     string long_str = strOne;  
  67.     string short_str = strTwo;  
  68.   
  69.     // 對字符串進行計數排序  
  70.     CounterSort(strOne, long_str);  
  71.     CounterSort(strTwo, short_str);  
  72.   
  73.     // 比較排序好的字符串  
  74.     Compare(long_str, short_str);  
  75.     return 0;  
  76. }  

 不過上述方法,空間複雜度爲O(n+m),即消耗了一定的空間。有沒有在線性時間,且空間複雜度較小的方案列?

 

第二節、尋求線性時間的解法
2.1、O(n+m)的hashtable的方法
    上述方案中,較好的方法是先對字符串進行排序,然後再線性掃描,總的時間複雜度已經優化到了:O(m+n),貌似到了極限,還有沒有更好的辦法列?

    我們可以對短字串進行輪詢(此思路的敘述可能與網上的一些敘述有出入,因爲我們最好是應該把短的先存儲,那樣,會降低題目的時間複雜度),把其中的每個字母都放入一個Hashtable裏(我們始終設m爲短字符串的長度,那麼此項操作成本是O(m)或8次操作)。然後輪詢長字符串,在Hashtable裏查詢短字符串的每個字符,看能否找到。如果找不到,說明沒有匹配成功,輪詢長字符串將消耗掉16次操作,這樣兩項操作加起來一共只有8+16=24次。
    當然,理想情況是如果長字串的前綴就爲短字串,只需消耗8次操作,這樣總共只需8+8=16次。

    或如夢想天窗所說: 我之前用散列表做過一次,算法如下:
 1、hash[26],先全部清零,然後掃描短的字符串,若有相應的置1,
 2、計算hash[26]中1的個數,記爲m 
 3、掃描長字符串的每個字符a;若原來hash[a] == 1 ,則修改hash[a] = 0,並將m減1;若hash[a] == 0,則不做處理 
 4、若m == 0 or 掃描結束,退出循環。

    代碼實現,也不難,如下:

  1. //copyright@ 2011 yansha  
  2. //July、updated,2011.04.25。   
  3. #include <iostream>  
  4. #include <string>  
  5. using namespace std;  
  6.   
  7. int main()  
  8. {  
  9.     string str1="ABCDEFGHLMNOPQRS";  
  10.     string str2="DCGSRQPOM";  
  11.   
  12.     // 開闢一個輔助數組並清零  
  13.     int hash[26] = {0};  
  14.   
  15.     // num爲輔助數組中元素個數  
  16.     int num = 0;  
  17.   
  18.     // 掃描短字符串  
  19.     for (int j = 0; j < str2.length(); j++)  
  20.     {  
  21.         // 將字符轉換成對應輔助數組中的索引  
  22.         int index = str1[j] - 'A';  
  23.   
  24.         // 如果輔助數組中該索引對應元素爲0,則置1,且num++;  
  25.         if (hash[index] == 0)  
  26.         {  
  27.             hash[index] = 1;  
  28.             num++;  
  29.         }  
  30.     }  
  31.   
  32.     // 掃描長字符串  
  33.     for (int k = 0; k < str1.length(); k++)  
  34.     {  
  35.         int index = str1[k] - 'A';  
  36.   
  37.         // 如果輔助數組中該索引對應元素爲1,則num--;爲零的話,不作處理(不寫語句)。  
  38.         if(hash[index] ==1)  
  39.         {  
  40.             hash[index] = 0;  
  41.             num--;  
  42.             if(num == 0)    //m==0,即退出循環。  
  43.                 break;  
  44.         }  
  45.     }  
  46.   
  47.     // num爲0說明長字符串包含短字符串內所有字符  
  48.     if (num == 0)  
  49.         cout << "true" << endl;  
  50.     else  
  51.         cout << "false" << endl;  
  52.     return 0;  
  53. }  

 

2.2、O(n+m)的數組存儲方法

    有兩個字符串short_str和long_str。
    第一步:你標記short_str中有哪些字符,在store數組中標記爲true。(store數組起一個映射的作用,如果有A,則將第1個單元標記true,如果有B,則將第2個單元標記true,... 如果有Z, 則將第26個單元標記true)
    第二步:遍歷long_str,如果long_str中的字符包括short_str中的字符則將store數組中對應位置標記爲false。(如果有A,則將第1個單元標記false,如果有B,則將第2個單元標記false,... 如果有Z, 則將第26個單元標記false),如果沒有,則不作處理。
    第三步:此後,遍歷store數組,如果所有的元素都是false,也就說明store_str中字符都包含在long_str內,輸出true。否則,輸出false。

    舉個簡單的例子好了,如abcd,abcdefg倆個字符串,
    1、先遍歷短字符串abcd,在store數組中想對應的abcd的位置上的單元元素置爲true,
    2、然後遍歷abcdefg,在store數組中相應的abcd位置上,發現已經有了abcd,則前4個的單元元素都置爲false,當我們已經遍歷了4個元素,等於了短字符串abcd的4個數目,所以,滿足條件,退出。
    (不然,繼續遍歷的話,我們會發現efg在store數組中沒有元素,不作處理。最後,自然,就會發現store數組中的元素單元都是false的。)
    3、遍歷store數組,發現所有的元素都已被置爲false,所以程序輸出true。

    其實,這個思路和上一節中,O(n+m)的hashtable的方法代碼,原理是完全一致的,且本質上都採用的數組存儲(hash表也是一個數組),但我並不認爲此思路多此一舉,所以仍然貼出來。ok,代碼如下:

  1. //copyright@ 2011 Hession  
  2. //July、updated,2011.04.23.  
  3. #include<iostream>  
  4. #include<string.h>  
  5. using namespace std;  
  6.   
  7. int main()  
  8. {  
  9.     char long_ch[]="ABCDEFGHLMNOPQRS";  
  10.     char short_ch[]="DEFGHXLMNOPQ";  
  11.     int i;  
  12.     bool store[58];  
  13.     memset(store,false,58);    
  14.       
  15.     //前兩個 是  遍歷 兩個字符串, 後面一個是  遍歷 數組  
  16.     for(i=0;i<sizeof(short_ch)-1;i++)  
  17.         store[short_ch[i]-65]=true;  
  18.       
  19.     for(i=0;i<sizeof(long_ch)-1;i++)  
  20.     {  
  21.         if(store[long_ch[i]-65]!=false)  
  22.             store[long_ch[i]-65]=false;  
  23.     }  
  24.     for(i=0;i<58;i++)  
  25.     {  
  26.         if(store[i]!=false)  
  27.         {  
  28.             cout<<"short_ch is not in long_ch"<<endl;  
  29.             break;    
  30.         }          
  31.         if(i==57)  
  32.             cout<<"short_ch is in long_ch"<<endl;  
  33.     }  
  34.       
  35.     return 0;  
  36. }  

 

第三節、O(n)到O(n+m)的素數方法

    我想問的是,還有更好的方案麼?
    你可能會這麼想:O(n+m)是你能得到的最好的結果了,至少要對每個字母至少訪問一次才能完成這項操作,而上一節最後的倆個方案是剛好是對每個字母只訪問一次。

    ok,下面給出一個更好的方案:
    假設我們有一個一定個數的字母組成字串,我給每個字母分配一個素數,從2開始,往後類推。這樣A將會是2,B將會是3,C將會是5,等等。現在我遍歷第一個字串,把每個字母代表的素數相乘。你最終會得到一個很大的整數,對吧?
    然後——輪詢第二個字符串,用每個字母除它。如果除的結果有餘數,這說明有不匹配的字母。如果整個過程中沒有餘數,你應該知道它是第一個字串恰好的子集了。

思路總結如下:
1.定義最小的26個素數分別與字符'A'到'Z'對應。
2.遍歷長字符串,求得每個字符對應素數的乘積。
3.遍歷短字符串,判斷乘積能否被短字符串中的字符對應的素數整除。
4.輸出結果。

    至此,如上所述,上述算法的時間複雜度爲O(m+n),時間複雜度最好的情況爲O(n)(遍歷短的字符串的第一個數,與長字符串素數的乘積相除,即出現餘數,便可退出程序,返回false),n爲長字串的長度,空間複雜度爲O(1)。如你所見,我們已經優化到了最好的程度。

    不過,正如原文中所述:“現在我想告訴你 —— Guy的方案(不消說,我並不認爲Guy是第一個想出這招的人)在算法上並不能說就比我的好。而且在實際操作中,你很可能仍會使用我的方案,因爲它更通用,無需跟麻煩的大型數字打交道。但從”巧妙水平“上講,Guy提供的是一種更、更、更有趣的方案。”

    ok,如果你有更好的思路,歡迎在本文的評論中給出,非常感謝。

  1. #include <iostream>  
  2. #include <string>  
  3. #include "BigInt.h"  
  4. using namespace std;  
  5.   
  6. // 素數數組  
  7. int primeNumber[26] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,  
  8.                         61, 67, 71, 73, 79, 83, 89, 97, 101};  
  9.   
  10. int main()  
  11. {  
  12.     string strOne = "ABCDEFGHLMNOPQRS";  
  13.     string strTwo = "DCGSRQPOM";  
  14.   
  15.     // 這裏需要用到大整數  
  16.     CBigInt product = 1;   //大整數除法的代碼,下頭給出。  
  17.   
  18.     // 遍歷長字符串,得到每個字符對應素數的乘積  
  19.     for (int i = 0; i < strOne.length(); i++)  
  20.     {  
  21.         int index = strOne[i] - 'A';  
  22.         product = product * primeNumber[index];  
  23.     }  
  24.   
  25.     // 遍歷短字符串  
  26.     for (int j = 0; j < strTwo.length(); j++)  
  27.     {  
  28.         int index = strTwo[j] - 'A';  
  29.   
  30.         // 如果餘數不爲0,說明不包括短字串中的字符,跳出循環  
  31.         if (product % primeNumber[index] != 0)  
  32.             break;  
  33.     }  
  34.   
  35.     // 如果積能整除短字符串中所有字符則輸出"true",否則輸出"false"。  
  36.     if (strTwo.length() == j)  
  37.         cout << "true" << endl;  
  38.     else  
  39.         cout << "false" << endl;  
  40.     return 0;  
  41. }  
     上述程序待改進的地方:
1.只考慮大些字符,如果考慮小寫字符和數組的話,素數數組需要更多素數
2.沒有考慮重複的字符,可以加入判斷重複字符的輔助數組。

以下的大整數除法的代碼,雖然與本題目無多大關係,但爲了保證文章的完整性,我還是決定把它貼出來,

代碼如下(點擊展開):

    說明:此次的判斷字符串是否包含問題,來自一位外國網友提供的gofish、google面試題,這個題目出自此篇文章:http://www.aqee.net/2011/04/11/google-interviewing-story/,文章記錄了整個面試的過程,比較有趣,值得一讀。

    擴展:正如網友安逸所說:其實這個問題還可以轉換爲:a和b兩個字符串,求b串包含a串的最小長度。包含指的就是b的字串包含a中每個字符。

 

第四節、字符串是否包含問題的繼續補充
    updated:
本文發佈後,得到很多朋友的建議和意見,其中nossiac,luuillu等倆位網友除了給出具體的思路之外,還給出了代碼,徵得同意,下面,我將引用他們的的思路及代碼,繼續就這個字符串是否包含問題深入闡述。

    4.1、在引用nossiac的思路之前,我得先給你介紹下什麼是Bit-map?
    Oliver:所謂的Bit-map就是用一個bit位來標記某個元素對應的Value, 而Key即是該元素。由於採用了Bit爲單位來存儲數據,因此在存儲空間方面,可以大大節省。

    如果看了以上說的還沒明白什麼是Bit-map,那麼我們來看一個具體的例子,假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置爲0,如下圖:

    然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1(可以這樣操作:p+(i/8)|(0x01<<(i%8))當然了這裏的操作涉及到Big-ending和Little-ending的情況,這裏默認爲Big-ending),因爲是從零開始的,所以要把第五位置爲一(如下圖):

    接着再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完所有的元素,將相應的位置爲1,這時候的內存的Bit位的狀態如下:

    最後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。

    代碼示例

  1. //位圖的一個示例  
  2. //copyright@ Oliver && July   
  3. //http://blog.redfox66.com/post/2010/09/26/mass-data-4-bitmap.aspx  
  4. //July、updated,2011.04.25.  
  5.   
  6. #include <memory.h>  
  7. #include <stdio.h>  
  8. //定義每個Byte中有8個Bit位  
  9. #define BYTESIZE 8   
  10.   
  11. void SetBit(char *p, int posi)  
  12. {      
  13.     for(int i=0; i < (posi/BYTESIZE); i++)       
  14.     {         
  15.         p++;      
  16.     }        
  17.     *p = *p|(0x01<<(posi%BYTESIZE)); //將該Bit位賦值1     
  18.     return;  
  19. }    
  20.   
  21. void BitMapSortDemo()   
  22. {     
  23.     //爲了簡單起見,我們不考慮負數     
  24.     int num[] = {3,5,2,10,6,12,8,14,9};    
  25.   
  26.     //BufferLen這個值是根據待排序的數據中最大值確定的    
  27.     //待排序中的最大值是14,因此只需要2個Bytes(16個Bit)      
  28.     //就可以了。      
  29.     const int BufferLen = 2;      
  30.     char *pBuffer = new char[BufferLen];   
  31.       
  32.     //要將所有的Bit位置爲0,否則結果不可預知。      
  33.     memset(pBuffer,0,BufferLen);      
  34.       
  35.     for(int i=0;i<9;i++)      
  36.     {          
  37.         //首先將相應Bit位上置爲1          
  38.         SetBit(pBuffer,num[i]);     
  39.     }        
  40.       
  41.     //輸出排序結果       
  42.     for(i=0;i<BufferLen;i++)   //每次處理一個字節(Byte)      
  43.     {           
  44.         for(int j=0;j<BYTESIZE;j++)   //處理該字節中的每個Bit位       
  45.         {              
  46.             //判斷該位上是否是1,進行輸出,這裏的判斷比較笨。           
  47.             //首先得到該第j位的掩碼(0x01<<j),將內存區中的               
  48.             //位和此掩碼作與操作。最後判斷掩碼是否和處理後的              
  49.             //結果相同             
  50.             if((*pBuffer&(0x01<<j)) == (0x01<<j))          
  51.             {                
  52.                 printf("%d ",i*BYTESIZE + j);        
  53.             }    
  54.         }    
  55.         pBuffer++;     
  56.     }  
  57.     printf("/n");  
  58. }     
  59.   
  60. int main()   
  61. {     
  62.     BitMapSortDemo();     
  63.     return 0;  
  64. }   

    位圖總結
      1、可進行數據的快速查找,判重,刪除,一般來說數據範圍是int的10倍以下
      2、使用bit數組來表示某些元素是否存在,比如8位電話號碼
      3、Bloom filter(日後介紹)可以看做是對bit-map的擴展

    問題實例
    1)已知某個文件內包含一些電話號碼,每個號碼爲8位數字,統計不同號碼的個數。 
       8位最多99 999 999,大概需要99m個bit,大概10幾m字節的內存即可。 (可以理解爲從0-99 999 999的數字,每個數字對應一個Bit位,所以只需要99M個Bit==12MBytes,這樣,就用了小小的12M左右的內存表示了所有的8位數的電話)
    2)2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。
      將bit-map擴展一下,用2bit表示一個數即可,0表示未出現,1表示出現一次,2表示出現2次及以上,在遍歷這些數的時候,如果對應位置的值是0,則將其置爲1;如果是1,將其置爲2;如果是2,則保持不變。或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map,都是一樣的道理。

    ok,介紹完了什麼是bit-map,接下來,咱們回到正題,來看下nossiac關於此字符串是否包含問題的思路(http://www.shello.name/me/?p=64

每個字母的ASCII碼值,可以對應一個位圖中的位。 
先遍歷第一個字符串,生成一個“位圖字典”。

用僞代碼表示就是:

 dictionary = 0
 for x in String1:
  dictionary |= 0x01<<x-'a'

紅色部分就是構造位圖字典的過程,剛好能運用ASCII碼值完成,取巧,呵呵,比較愜意。

然後,我們遍歷第二個字符串,用查字典的方式較檢,僞代碼爲:

 for x in String2:
  if dictionary != dictionary|0x01<<x-'a':
  print("NO")
 else:
  print("YES")

what?還不夠明白,ok,看yansha對此思路的具體闡述吧:
此思路是位操作的典型應用:
dictionary = 0
for x in String1:
   dictionary |= 0x01 << (x - 'a');

分析如下:
dictionary是一個32位的int,初始化爲0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0


dictionary |= 0x01 << (x - 'a')則是把字符映射到dictionary當中的某一位;

比方String1 = "abde";
1、當x爲‘a’時,x-‘a’爲0,所以0x01<<0 爲0x01。
那麼dictionary |= 0x01,也就是將dictionary的第一位置1。
此時dictionary爲:

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

2、當x爲‘b’時,x-‘b’爲1,所以0x01<<1 爲0x02。
那麼dictionary |= 0x02,也就是將dictionary的第二位置1。
此時dictionary爲:

1

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

3、當x爲‘d’時,x-‘d’爲3,所以0x01<<3 爲0x08。
那麼dictionary |= 0x08,也就是將dictionary的第四位置1。
此時dictionary爲:

1

1

0

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

4、當x爲‘e’時,x-‘e’爲4,所以0x01<<4 爲0x10。
那麼dictionary |= 0x10,也就是將dictionary的第五位置1。
此時dictionary爲:

1

1

0

1

1

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

其他字符依此類推,比較過程也類似。對於128個字符的ASCII碼而言,顯然一個32位的整形是不夠的。

OK,算法完成。時間複雜度爲 O(m+n),空間複雜度爲O(1)。

    然後,代碼可以編寫如下:

  1. //copyright@ nossiac  
  2. //July、updated,2011.04.24。  
  3. #include <stdio.h>  
  4. #include <string.h>  
  5.   
  6. #define getbit(x) (1<<(x-'a'))  
  7.   
  8. void a_has_b(char * a, char * b)  
  9. {  
  10.     int i = 0;  
  11.     int dictionary = 0;  
  12.     int alen = strlen(a);  
  13.     int blen = strlen(b);  
  14.       
  15.     for(i=0;i<alen;i++)  
  16.         dictionary |= getbit(a[i]);  
  17.       
  18.     for(i=0;i<blen;i++)  
  19.     {  
  20.         if(dictionary != (dictionary|getbit(b[i])))  
  21.             break;  
  22.     }  
  23.       
  24.     if(i==blen)  
  25.         printf("YES! A has B!/n");  
  26.     else  
  27.         printf("NiO!  Char at %d is not found in dictionary!/n",i);  
  28. }  
  29.   
  30. int main()  
  31. {  
  32.     char * str1="abcdefghijklmnopqrstuvwxyz";  
  33.     char * str2="akjsdfasdfiasdflasdfjklffhasdfasdfjklasdfjkasdf";  
  34.     char * str3="asdffaxcfsf";  
  35.     char * str4="asdfai";  
  36.       
  37.     a_has_b(str1, str2);  
  38.     a_has_b(str1, str3);  
  39.     a_has_b(str3, str4);  
  40.       
  41.     return 0;  
  42. }  

     4.2、還可以如luuillu所說,判斷字符串是否包含,採用移位的方法(此帖子第745樓http://topic.csdn.net/u/20101126/10/b4f12a00-6280-492f-b785-cb6835a63dc9_8.html?seed=423056362&r=72955051#r_72955051,他的代碼編寫如下:

  1. //copyright@ luuillu  
  2. //July、updated,2011.04.24。  
  3. #include <iostream>      
  4. using namespace std;  
  5.   
  6. //判斷  des 是否包含在 src 中  
  7. bool compare(char *des,char * src)  
  8. {  
  9.      unsigned  index[26]={1,2,4,8,16,32,64,128,256,512,1024,1<<11,  
  10.                           1<<12,1<<13,1<<14,1<<15,1<<16,1<<17,1<<18,1<<19,  
  11.                           1<<20,1<<21,1<<22,1<<23,1<<24,1<<25};    //2的n次冪  
  12.   
  13.      unsigned   srcdata=0;  
  14.      unsigned    desdata=0;  
  15.   
  16.      while( *src)  
  17.          srcdata|=index[(*src++)-'A'];  
  18.      while(*des)  
  19.          desdata|=index[(*des++)-'A'];  
  20.   
  21.      return     (srcdata|desdata) == srcdata    ;  
  22.    
  23. }  
  24.   
  25. int main()  
  26. {   
  27.     char *src="ABCDEFGHLMNOPQRS";     
  28.     char *des="DCGSRQPOM";     
  29.     cout<<compare(des,src)<<endl;     
  30.     return 0;     
  31. }  

    第四節總結:正如十一文章在本文評論裏所提到的那樣,上面的位圖法,hash,還有bitmap三者之間並沒有本質上的區別,只是形式上不同而已。

 

第五節、字符串相關問題擴展與闡述

5.1、字符串匹配問題
題目描述:
假設兩個字符串中所含有的字符和個數都相同我們就叫這兩個字符串匹配,比如:abcda和adabc,
由於出現的字符個數都是相同,只是順序不同,所以這兩個字符串是匹配的。

要求高效實現下面的函數: boolen Is_Match(char *str1,char *str2)。

    分析:可以看出,此字符串的匹配問題,是與上述字符串包含的問題相類似的,這個問題可以先排序再比較,也可以利用hash表進行判斷。這裏給出一種hash表的方法,原理已在上文中闡明瞭,代碼如下:

  1. //copyright@ 2011 yansha   
  2. //July、updated,2011.04.24。  
  3. #include <iostream>  
  4. #include <string>  
  5. using namespace std;  
  6.   
  7. bool Is_Match(const char *strOne,const char *strTwo)  
  8. {  
  9.     int lenOfOne = strlen(strOne);  
  10.     int lenOfTwo = strlen(strTwo);  
  11.   
  12.     // 如果長度不相等則返回false  
  13.     if (lenOfOne != lenOfTwo)  
  14.         return false;  
  15.   
  16.     // 開闢一個輔助數組並清零  
  17.     int hash[26] = {0};  
  18.       
  19.     // 掃描字符串  
  20.     for (int i = 0; i < strlen(strOne); i++)  
  21.     {  
  22.         // 將字符轉換成對應輔助數組中的索引  
  23.         int index = strOne[i] - 'A';  
  24.           
  25.         // 輔助數組中該索引對應元素加1,表示該字符的個數  
  26.         hash[index]++;  
  27.     }  
  28.   
  29.     // 掃描字符串  
  30.     for (int j = 0; j < strlen(strTwo); j++)  
  31.     {  
  32.         int index = strTwo[j] - 'A';  
  33.           
  34.         // 如果輔助數組中該索引對應元素不爲0則減1,否則返回false  
  35.         if (hash[index] != 0)  
  36.             hash[index]--;  
  37.         else  
  38.             return false;  
  39.     }  
  40.     return true;  
  41. }  
  42.   
  43. int main()  
  44. {  
  45.     string strOne = "ABBA";  
  46.     string strTwo = "BBAA";  
  47.       
  48.     bool flag = Is_Match(strOne.c_str(), strTwo.c_str());  
  49.       
  50.     // 如果爲true則匹配,否則不匹配  
  51.     if (flag == true)  
  52.         cout << "Match" << endl;  
  53.     else  
  54.         cout << "No Match" << endl;  
  55.     return 0;  
  56. }  
 

5.2、在字符串中查找子串
題目描述:
給定一個字符串A,要求在A中查找一個子串B。
如A="ABCDF",要你在A中查找子串B=“CD”。

    分析:比較簡單,相當於實現strstr庫函數,主體代碼如下:

  1. //copyright@ 2011 July && luoqitai  
  2. //string爲模式串,substring爲要查找的子串  
  3. int strstr(char *string,char *substring)   
  4. {  
  5.     int len1=strlen(string);  
  6.     int len2=strlen(substring);  
  7.     for (int i=0; i<=len1-len2; i++)   //複雜度爲O(m*n)  
  8.     {  
  9.         for (int j=0; j<len2; j++)  
  10.         {  
  11.             if (string[i+j]!=substring[j])  
  12.                 break;  
  13.         }  
  14.         if (j==len2)  
  15.             return i+1;  
  16.     }  
  17.     return 0;  
  18. }   

    上述程序已經實現了在字符串中查找第一個子串的功能,時間複雜度爲O(n*m),繼續的優化可以先對兩個字符串進行排序,然後再查找,也可以用KMP算法,複雜度爲O(m+n)。具體的,在此不再贅述(多謝hlm_87指正)。 

    擴展:還有個類似的問題:第17題(字符串):題目:在一個字符串中找到第一個只出現一次的字符。如輸入abaccdeff,則輸出b。代碼,可編寫如下(測試正確):

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. //查找第一個只出現一次的字符,第1個程序  
  5. //copyright@ Sorehead && July  
  6. //July、updated,2011.04.24.  
  7. char find_first_unique_char(char *str)  
  8. {  
  9.     int data[256];  
  10.     char *p;  
  11.       
  12.     if (str == NULL)  
  13.         return '/0';  
  14.       
  15.     memset(data, 0, sizeof(data));    //數組元素先全部初始化爲0  
  16.     p = str;  
  17.     while (*p != '/0')  
  18.         data[(unsigned char)*p++]++;  //遍歷字符串,在相應位置++,(同時,下標強制轉換)  
  19.       
  20.     while (*str != '/0')  
  21.     {  
  22.         if (data[(unsigned char)*str] == 1)  //最後,輸出那個第一個只出現次數爲1的字符  
  23.             return *str;  
  24.           
  25.         str++;  
  26.     }  
  27.       
  28.     return '/0';  
  29. }  
  30.   
  31. int main()  
  32. {  
  33.     char *str = "afaccde";  
  34.     cout << find_first_unique_char(str) << endl;  
  35.     return 0;  
  36. }  
  當然,代碼也可以這麼寫(測試正確): 
  1. //查找第一個只出現一次的字符,第2個程序  
  2. //copyright@ yansha  
  3. //July、updated,2011.04.24.  
  4. char FirstNotRepeatChar(char* pString)  
  5. {  
  6.     if(!pString)  
  7.         return '/0';  
  8.       
  9.     const int tableSize = 256;  
  10.     int hashTable[tableSize] = {0}; //存入數組,並初始化爲0  
  11.       
  12.     char* pHashKey = pString;  
  13.     while(*(pHashKey) != '/0')  
  14.         hashTable[*(pHashKey++)]++;  
  15.       
  16.     while(*pString != '/0')  
  17.     {  
  18.         if(hashTable[*pString] == 1)  
  19.             return *pString;  
  20.           
  21.         pString++;  
  22.     }  
  23.     return '/0';  //沒有找到滿足條件的字符,退出  
  24. }  

5.3、字符串轉換爲整數
題目:輸入一個表示整數的字符串,把該字符串轉換成整數並輸出。
例如輸入字符串"345",則輸出整數345。

    分析:此題看起來,比較簡單,每掃描到一個字符,我們把在之前得到的數字乘以10再加上當前字符表示的數字。這個思路用循環不難實現。然其背後卻隱藏着不少陷阱,正如zhedahht 所說,有以下幾點需要你注意:
    1、由於整數可能不僅僅之含有數字,還有可能以'+'或者'-'開頭,表示整數的正負。如果第一個字符是'+'號,則不需要做任何操作;如果第一個字符是'-'號,則表明這個整數是個負數,在最後的時候我們要把得到的數值變成負數。
    2、如果使用的是指針的話,在使用指針之前,我們要做的第一件是判斷這個指針是不是爲空。如果試着去訪問空指針,將不可避免地導致程序崩潰(此第2點在下面的程序不需注意,因爲沒有用到指針)。
    3、輸入的字符串中可能含有不是數字的字符。
每當碰到這些非法的字符,我們就沒有必要再繼續轉換。
    4、溢出問題。由於輸入的數字是以字符串的形式輸入,因此有可能輸入一個很大的數字轉換之後會超過能夠表示的最大的整數而溢出。

    總結以上四點,代碼可以如下編寫:

  1. //字符串轉換爲整數  
  2. //copyright@ yansha  
  3. #include <iostream>  
  4. #include <string>  
  5. using namespace std;  
  6.   
  7. int str_2_int(string str)  
  8. {  
  9.     if (str.size() == 0)  
  10.         exit(0);  
  11.       
  12.     int pos = 0;  
  13.     int sym = 1;  
  14.       
  15.     // 處理符號  
  16.     if (str[pos] == '+')  
  17.         pos++;  
  18.     else if (str[pos] == '-')  
  19.     {  
  20.         pos++;  
  21.         sym = -1;  
  22.     }  
  23.       
  24.     int num = 0;  
  25.     // 逐位處理  
  26.     while (pos < str.length())  
  27.     {  
  28.         // 處理數字以外的字符  
  29.         if (str[pos] < '0' || str[pos] > '9')  
  30.             exit(0);  
  31.           
  32.         num = num * 10 + (str[pos] - '0');  
  33.           
  34.         // 處理溢出  
  35.         if (num < 0)  
  36.             exit(0);      
  37.         pos++;  
  38.     }  
  39.       
  40.     num *= sym;   
  41.     return num;  
  42. }  
  43.   
  44. int main()  
  45. {  
  46.     string str = "-3450";  
  47.     int num = str_2_int(str);  
  48.     cout << num << endl;  
  49.     return 0;  
  50. }  

    @helloword:這個的實現非常不好,當輸入字符串參數爲非法時,不是拋出異常不是返回error code,而是直接exit了。直接把進程給終止了,想必現實應用中的實現都不會這樣。建議您改改,不然拿到面試官那,會被人噴死的。ok,聽從他的建議,借用zhedahht的代碼了:

  1. //http://zhedahht.blog.163.com/blog/static/25411174200731139971/  
  2. enum Status {kValid = 0, kInvalid};  
  3. int g_nStatus = kValid;  
  4.   
  5. int StrToInt(const char* str)  
  6. {  
  7.     g_nStatus = kInvalid;  
  8.     long long num = 0;  
  9.       
  10.     if(str != NULL)  
  11.     {  
  12.         const char* digit = str;  
  13.           
  14.         // the first char in the string maybe '+' or '-'  
  15.         bool minus = false;  
  16.         if(*digit == '+')  
  17.             digit ++;  
  18.         else if(*digit == '-')  
  19.         {  
  20.             digit ++;  
  21.             minus = true;  
  22.         }  
  23.           
  24.         // the remaining chars in the string  
  25.         while(*digit != '/0')  
  26.         {  
  27.             if(*digit >= '0' && *digit <= '9')  
  28.             {  
  29.                 num = num * 10 + (*digit - '0');  
  30.                   
  31.                 // overflow    
  32.                 if(num > std::numeric_limits<int>::max())  
  33.                 {  
  34.                     num = 0;  
  35.                     break;  
  36.                 }  
  37.                   
  38.                 digit ++;  
  39.             }  
  40.             // if the char is not a digit, invalid input  
  41.             else  
  42.             {  
  43.                 num = 0;  
  44.                 break;  
  45.             }  
  46.         }  
  47.           
  48.         if(*digit == '/0')  
  49.         {  
  50.             g_nStatus = kValid;  
  51.             if(minus)  
  52.                 num = 0 - num;  
  53.         }  
  54.     }  
  55.       
  56.     return static_cast<int>(num);  
  57. }  

updated:

yansha看到了上述helloword的所說的後,修改如下:

  1. #include <iostream>  
  2. #include <string>  
  3. #include <assert.h>  
  4. using namespace std;  
  5.   
  6. int str_2_int(string str)  
  7. {  
  8.     assert(str.size() > 0);  
  9.       
  10.     int pos = 0;  
  11.     int sym = 1;  
  12.       
  13.     // 處理符號  
  14.     if (str[pos] == '+')  
  15.         pos++;  
  16.     else if (str[pos] == '-')  
  17.     {  
  18.         pos++;  
  19.         sym = -1;  
  20.     }  
  21.       
  22.     int num = 0;  
  23.     // 逐位處理  
  24.     while (pos < str.length())  
  25.     {  
  26.         // 處理數字以外的字符  
  27.         assert(str[pos] >= '0');  
  28.         assert(str[pos] <= '9');  
  29.           
  30.         num = num * 10 + (str[pos] - '0');  
  31.           
  32.         // 處理溢出  
  33.         assert(num >= 0);  
  34.           
  35.         pos++;  
  36.     }  
  37.       
  38.     num *= sym;  
  39.       
  40.     return num;  
  41. }  
  42.   
  43. int main()  
  44. {  
  45.     string str = "-1024";  
  46.     int num = str_2_int(str);  
  47.     cout << num << endl;  
  48.     return 0;  
  49. }  
  

5.4、字符串拷貝
題目描述:
    要求實現庫函數strcpy,

原型聲明:extern char *strcpy(char *dest,char *src); 
功能:把src所指由NULL結束的字符串複製到dest所指的數組中。  
說明:src和dest所指內存區域不可以重疊且dest必須有足夠的空間來容納src的字符串。  
返回指向dest的指針。

    分析:如果編寫一個標準strcpy函數的總分值爲10,下面給出幾個不同得分的答案:

  1. //2分  
  2. void strcpy( char *strDest, char *strSrc )  
  3. {  
  4.     while( (*strDest++ = * strSrc++) != '/0' );  
  5. }   
  6.   
  7. //4分  
  8. void strcpy( char *strDest, const char *strSrc )   
  9. {  
  10.     //將源字符串加const,表明其爲輸入參數,加2分  
  11.     while( (*strDest++ = * strSrc++) != '/0' );  
  12. }   
  13.   
  14. //7分  
  15. void strcpy(char *strDest, const char *strSrc)   
  16. {  
  17.     //對源地址和目的地址加非0斷言,加3分  
  18.     assert( (strDest != NULL) && (strSrc != NULL) );  
  19.     while( (*strDest++ = * strSrc++) != '/0' );  
  20. }   
  21.   
  22. //10分  
  23. //爲了實現鏈式操作,將目的地址返回,加3分!  
  24. char * strcpy( char *strDest, const char *strSrc )   
  25. {  
  26.     assert( (strDest != NULL) && (strSrc != NULL) );  
  27.     char *address = strDest;   
  28.     while( (*strDest++ = * strSrc++) != '/0' );   
  29.     return address;  
  30. }   
發佈了62 篇原創文章 · 獲贊 29 · 訪問量 77萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章