java程序員要知道的大廠常見的算法面試題:字符串的包含

1.2 字符串的包含
題目描述

給定一長字符串a和一短字符串b。請問,如何最快地判斷出短字符串b中的所有字符是否都在長字符串a中?請編寫函數bool StringContain(string &a, string &b)實現此功能。

爲簡單起見,假設輸入的字符串只包含大寫英文字母。下面舉幾個例子。

如果字符串a是"ABCD",字符串b是"BAD",答案是true,因爲字符串b中的字母都在字符串a中,或者說b是a的真子集。
如果字符串a是"ABCD",字符串b是"BCE",答案是false,因爲字符串b中的字母E不在字符串a中。
如果字符串a是"ABCD",字符串b是"AA",答案是true,因爲字符串b中的字母A包含在字符串a中。
分析與解法

此題初看似乎很簡單,但要高效地實現並不輕鬆。而且,如果面試官步步緊逼,一個一個否決你想到的方法,要求你給出更快、更好的方案,恐怕就要費不少腦筋了。

解法一:蠻力輪詢

判斷短字符串b中的字符是否都在長字符串a中,最直觀也是最簡單的思路則是:輪詢短字符串b中的每一個字符,逐個與長字符串a中的每個字符進行比較,看是否都在字符串a中。

參考代碼如下:


bool StringContain(string &a, string &b)
{
   for (int i = 0; i < b.length(); ++i)
   {
     int j;
     for (j = 0; (j < a.length()) && (a[j] != b[i]); ++j)
       ;
     if (j >= a.length())
     {
       return false;
     }
   }
   return true;
}

如果n是長字符串a的長度,m是短字符串b的長度,那麼此算法需要O(nm)次比較。顯然,如果n和m很大,時間開銷太大,需要尋找更好的辦法。

解法二:排序後輪詢

如果允許排序,可以考慮先排序後輪詢。例如,可先對這兩個字符串中的字母進行排序,然後再對兩個字符串依次輪詢。

常規情況下,兩個字符串的排序需要O(mlogm)+O(nlogn)次操作,之後的線性掃描需要O(m+n)次操作。關於排序方法,可以採用最常用的快速排序。

參考代碼如下:


bool StringContain(string &a, string &b)
{
   sort(a.begin(), a.end());
   sort(b.begin(), b.end());
   for (int pa = 0, pb = 0; pb < b.length();)
   {
     while ((pa < a.length()) && (a[pa] < b[pb]))
     {
       ++pa;
     }
     if ((pa >= a.length()) || (a[pa] > b[pb]))
     {
       return false;
     }
     ++pb;
   }
   return true;
}

解法三:素數相乘

有沒有比排序後輪詢更好的方法呢?

首先,讓長字符串a中的每個字母與一個素數對應,如A對應2,B對應3,C對應5,……,依次類推。再遍歷長字符串a,把a中的每個字母對應的素數相乘,得到一個整數。然後,讓短字符串b中的字母也對應相應的素數,再用b中的每個字母對應的素數除上面得到的整數。如果結果有餘數,說明結果爲false,當即退出程序;如果整個過程中沒有餘數,則說明短字符串b是長字符串a的子集。

具體思路總結如下。

(1)按照從小到大的順序,用26個素數分別代替長字符串a中的所有字母。

(2)遍歷長字符串a,求得a中的所有字母對應的素數的乘積。

(3)遍歷短字符串b,判斷上一步得到的乘積能否被b中的字母對應的素數整除。

(4)輸出結果。

上述算法的時間複雜度爲O(m+n)。當然,最好情況下的時間複雜度爲O(n),即在遍歷短字符串b的第一個字母,與長字符串a中所有字符所對應的素數的乘積相除時,當即出現餘數,便直接退出程序,返回false。

bool StringContain(string &a, string &b)
{
   const int p[26] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 
             53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101 };
   int f = 1;
   for (int i = 0; i < a.length(); ++i)
   {
     int x = p[a[i] - 'A'];
     if (f % x)
     {
       f *= x;
     }
   }
   for (int i = 0; i < b.length(); ++i)
   {
     int x = p[b[i] - 'A'];
     if (f % x)
     {
       return false;
     }
   }
   return true;
}

這種素數相乘的方法看似可行,實則不可行,因爲素數相乘的結果會很大,從而導致整數溢出(前16個字母對應的素數相乘便會超出long long類型所能表示的最大整數範圍)。

解法四:位運算法

如果面試官繼續追問,到底有沒有更好的辦法呢?或許你絞盡腦汁能想到計數排序。但除了計數排序還有別的辦法嗎?

事實上,可以先把長字符串a中的所有字符都放入一個散列表(hash table)中,然後輪詢短字符串b,看b中的每個字符是否都在散列表裏,如果都在,說明長字符串a包含短字符串b;否則,說明不包含。

再進一步,可以用位運算(26位整數表示)爲長字符串a計算出一個“簽名”,再逐一將短字符串b中的字符放到a中進行查找。

參考代碼如下:

bool StringContain(string &a, string &b)
{
   int hash = 0;
   for (int i = 0; i < a.length(); ++i)
   {
     hash |= (1 << (a[i] - 'A'));
   }
   for (int i = 0; i < b.length(); ++i)
   {
     if ((hash & (1 << (b[i] - 'A'))) == 0)
     {
       return false;
     }
   }
   return true;
}

這個位運算方法的實質是用一個整數代替了散列表,它的空間複雜度爲O(1),時間複雜度爲O(n+m)。至此,算是比較完美地解決了這個字符串包含的問題。

但是,這樣真的完美了嗎?請讀者繼續思考。

舉一反三

變位詞

如果兩個字符串中的字符一樣,出現次數也一樣,只是出現的順序不一樣,則認爲這兩個字符串是兄弟字符串。例如,"bad"和"adb"即爲兄弟字符串。現提供一個字符串,請問如何在字典中迅速找到它的兄弟字符串?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章