在《編程珠璣》一書的第二章提到了一個變位詞問題,變位詞指的是一個單詞可以通過改變其他單詞中字母的順序來得到,也叫做兄弟單詞,如army->mary。由變位詞可以引申出幾個演算法問題,包括字串包含問題,比較兩個字串是否是變位詞,以及找出字典中變位詞集合的問題。
一、字串包含問題
(1) 問題描述:存在字串1和字串2,假設字串2相對較短,如何快速地判定字串2中的字元都存在於字串1裡(假定字串只包含字母)?
(2) 舉例:字串1為ABCDEFGHIJK,字串2為ABCDE,則字串1包含字串2,因為字串2中包含的字母在字串1中也都有。
(3) 解決方案:
思路一
最直接的思路就是針對字串2中的每個字元,輪詢字串1進行比較,看是否在字串1裡面。很明顯這種的時間效率為O(n*m)。
/************************************************************************* > File Name: test.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<string> using namespace std; void Compare(string long_str, string short_str) { int i,j; for(i=0; i<short_str.size(); ++i) { for(j=0; j<long_str.size(); ++j) { if(long_str[j] == short_str[i]) { break; } } if(j == long_str.size()) { cout << "false" << endl; return; } } cout << "true" << endl; return; } int main() { string l = "ABCDEFGHIJK"; string s = "ABCDEF"; Compare(l, s); return 0; }
思路二
這裡由於假定了字串只包含字母,所以我們可以用一個額外的數組flag[26]作為26個字元標識位,先遍曆長字串將對應的標識位置1,再遍曆短字串,如果對應的標示位都是1,則包含;否則不包含。這種方法的時間複雜度為O(n+m),為了提高空間效率,這裡不使用數組而使用26個bit位來作為標示位(bitset容器)。
/************************************************************************* > File Name: test1.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<bitset> #include<string> using namespace std; bool Compare(string long_str, string short_str) { bitset<26> flag; for(int i=0; i<long_str.size(); ++i) { // flag.set(n)置第n位為1 flag.set(long_str[i]-'A'); } for(int i=0; i<short_str.size(); ++i) { // flag.test(n)判斷第n位是否為1 if(!flag.test(short_str[i]-'A')) return false; } return true; } int main() { string l = "ABCDEFGHIJK"; string s = "ABCDEZ"; if(Compare(l, s)) cout << "true" << endl; else cout << "false" << endl; return 0; }
這種方法還可以進行最佳化,例如如果長字串的首碼就為短字串,那麼我們可以不需要n+m次,而只需要2m次。具體實現請自己思考。
思路三
給每個字母分配一個素數,從2開始到3,5,7...遍曆長字串,求得每個字元對應素數的乘積。然後遍曆短字串,判斷該乘積能否被短字串中的字元對應的素數整除,如果除的結果存在餘數,則說明有不匹配的字母;如果整個過程都沒有餘數,則說明短字串中的字母在長字串裡都有。這種方法的時間複雜度也是O(n+m),需要26個額外空間存素數,但是這種方法有一個缺點就是需要跟大整數打交道,因為乘積可能非常大。(這裡我們使用<cstdint>標頭檔中定義的int64_t和uint64_t)
/************************************************************************* > File Name: test2.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<string> #include<stdint.h> //#include<cstdint> // C++11 using namespace std; bool Compare(string long_str, string short_str) { unsigned int primeNum[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}; /* int64_t和uint64_t分別表示64位的有符號和無符號整形數 */ /* 在不同位元機器的平台下通用,都是64位 */ uint64_t ch = 1; for(int i=0; i<long_str.size(); ++i) { ch = ch*primeNum[long_str[i]-'A']; } for(int i=0; i<short_str.size(); ++i) { if(ch%primeNum[short_str[i]-'A'] != 0) return false; } return true; } int main() { string l = "ABCDEFGHIJK"; string s = "ABCDEK"; if(Compare(l, s)) cout << "true" << endl; else cout << "false" << endl; return 0; }
二、比較兩個字串是否為變位詞
(1) 問題描述:如果兩個字串的字元一樣,但是順序不一樣,被認為是兄弟字串,問如何在迅速匹配兄弟字串(如,bad和adb就是兄弟字串)。
(2) 注意:第一點中討論了字串包含問題,但是不要以為兩個字串互相包含就是(變位詞)兄弟字串了,例如aabcde和edcba互相包含,但它們不是變位詞。
(3) 解決方案:
思路一
給每個字母分配一個素數,可以通過判斷兩個字串的素數乘積是否相等。跟上述素數法一樣,時間複雜度也是O(n+m),需要跟大整數打交道。
/************************************************************************* > File Name: test3.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<string> #include<stdint.h> //#include<cstdint> // C++11 using namespace std; bool Compare(string s1, string s2) { unsigned int primeNum[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}; uint64_t ch = 1; for(int i=0; i<s1.size(); ++i) { ch = ch*primeNum[s1[i]-'a']; } for(int i=0; i<s2.size(); ++i) { ch = ch/primeNum[s2[i]-'a']; } if(ch == 1) return true; else return false; } int main() { string s1 = "abandon"; string s2 = "banadon"; if(Compare(s1, s2)) cout << "They are brother words!" << endl; else cout << "They aren't brother words!" << endl; return 0; }
思路二
將兩個字串按照字母表順序排序,看排序後的字串是否相等,如果相等則是兄弟字串(變位詞)。這種方法的時間效率根據你使用的排序演算法不同而不同。當然,你可以自己寫排序演算法,這裡我們使用C++的STL中的sort()函數對字串進行排序。
/************************************************************************* > File Name: test4.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<algorithm> #include<string> using namespace std; // 自訂序函數(二元謂詞) bool myfunction(char i, char j) { return i > j; } bool Compare(string s1, string s2) { // 採用泛型演算法對s1,s2排序,sort()採用的是快速排序演算法 sort(s1.begin(), s1.end(), myfunction); sort(s2.begin(), s2.end(), myfunction); if(!s1.compare(s2)) // 相等返回0 return true; else return false; } int main() { string s1 = "abandon"; string s2 = "banadon"; if(Compare(s1, s2)) cout << "They are brother words!" << endl; else cout << "They aren't brother words!" << endl; return 0; }
三、字典找出所有變位詞集合(重點)
(1) 問題描述:給定一個英語字典,找出其中的所有變位詞集合。
(2) 解決方案:
思路一
對於這個問題,最快想到的最直接的方法就是針對每一個單詞跟字典中的其他單詞進行比較。然而,假設一次比較至少花費1微秒的時間,則擁有二十萬單詞的字典將花費:200000單詞 x 200000比較/單詞 x 1微秒/比較 = 40000x10^6秒 = 40000秒 ≈ 11.1小時。比較的次數太多了,導致效率低下,我們需要找出效率更高的方法。
思路二
標識字典中的每一個單詞,使得在相同變位詞類中的單詞具有相同的的標識,然後集中具有相同標識的單詞。將每個單詞按照字母表排序,排序後得到的字串作為該單詞的標識。那麼對於該問題的解題過程可以分為三步:第一步,讀入字典檔案,對單詞進行排序得到標識;第二步,將所有的單詞按照其標識的順序排序;第三步,將同一個變位詞類中的各個單詞放到同一行中。
這裡出現了標識-單詞(key-value)對,我們很容易想到C++中的關聯容器map,使用map的好處就是:
動態管理記憶體,容器大小動態改變;
單詞與它的標識一一對應,對於相同標識(key)的單詞直接加在值(value)後面;
無需根據標識排序,因為map會自動按關鍵字有序(預設升序)。
所以,在將每個單詞及其標識存入map以後,就可以直接遍曆輸出了,每一個map元素就是一個變位詞集合。
C++實現代碼如下:
/************************************************************************* > File Name: test5.cpp > Author: SongLee ************************************************************************/ #include<iostream> #include<fstream> // file I/O #include<map> // map #include<string> // string #include<algorithm> // sort using namespace std; /* *map是C++中的關聯容器 * 按關鍵字有序 * 關鍵字不可重複 */ map<string, string> word; /* 自訂比較函數(用於排序) */ bool myfunction(char i, char j) { return i < j; } /* *對每個單詞排序 *排序後字串作為關鍵字,原單詞作為值 *存入map中 */ void sign_sort(const char* dic) { // 檔案流 ifstream in(dic); if(!in) { cout << "Couldn't open file: " + string(dic) << endl; return; } string aword; string asign; while(in >> aword) { asign = aword; sort(asign.begin(), asign.end(), myfunction); // 若標識不存在,建立一個新map元素,若存在,加在值後面 word[asign] += aword + " "; } in.close(); } /* *寫入輸出檔案 */ void write_file(const char* file) { ofstream out(file); if(!out) { cout << "Couldn't create file: " + string(file) << endl; return; } map<string, string>::iterator begin = word.begin(); map<string, string>::iterator end = word.end(); while(begin != end) { out << begin->second << "\n"; ++begin; } out.close(); } int main() { string dic; string outfile; cout << "Please input dictionary name: "; cin >> dic; cout << "Please input output filename: "; cin >> outfile; sign_sort(dic.c_str()); write_file(outfile.c_str()); return 0; }
附:(2012.5.6百度實習筆試題)一個單詞交換字母位置,可得另一個單詞,如army->mary,成為兄弟單詞。提供一個單詞,在字典中找到它的兄弟。描述資料結構和查詢過程。
解題思路:如果不允許進行預先處理,那麼我們只能順序遍曆整個字典,計算每個單詞的標識與給定單詞的標識比較。如果允許進行預先處理,我們可以如上述思路二將所有單詞加入一個map,然後輸出關鍵字(給定單詞的標識)對應的值,值中就包含了該單詞的所有兄弟單詞。
相信本文所述執行個體有助於讀者更好的掌握C++下資料結構與演算法的實現技巧。