前言
這是我的第一篇博文,獻給算法。
學習和研究算法可以讓人變得更加聰明。
算法的目標是以更好的方法完成任務。
更好的方法的具體指標是:
1. 花費更少的執行時間。
2. 花費更少的內存。
在對方法的不斷尋找,對規律的不斷探索中,個人的思考能力能夠被加強。當敏捷的思考能力成為一種固有特征時,人就變得聰明起來。
研究算法其實是研究事物的規律。對事物的變化規律掌握的越准確、越細致、越深入,就能找到更好的算法。
在探索規律的過程當中,一定會經歷失敗。但是這種失敗是值得的,因為它可以為解決其它問題提供基礎。
回文算法:
回文指從左往右和從由往左讀到相同內容的文字。比如: aba,abba,level。
回文具有對稱性。
回文算法的目標是把最長的回文從任意長度的文本當中尋找出來。比如:從123levelabc中尋找出level。
框架代碼
框架代碼包含除核心算法代碼的所有其他部分代碼。
1. main()函數,使用隨機數產生10M長度的字符串。然后調用核心算法代碼。
2. 時間函數,用於統計並比較不同算法耗時的差別。
#include <vector> #include <iostream> #include <string> #include <minmax.h> #include <time.h> #include <Windows.h> #include <random> #include <assert.h> using namespace std; __int64 get_local_ft_time(){ SYSTEMTIME st; __int64 ft; GetLocalTime(&st); SystemTimeToFileTime(&st, (LPFILETIME) &ft); return ft; } int diff_ft_time_ms(__int64 subtracted, __int64 subtraction){ return (int)((subtracted - subtraction) / 10000); } int main() { int length = 1024 * 1024 * 10; LPSTR s = new char[length + 1]; srand(time(NULL)); for (int i = 0; i < length; i++){ s[i] = (char) ((rand() % 26) + 'a'); } palindrome_raw(s, length); Manacher(s, length); palindrome_zjs(s, length); delete [] s; //cin.get(); }
回文算法: 原始算法
原始算法指按照回文的原始定義,利用數據的對稱性(s[i - x] = s[i + x])來尋找回文的算法。
void palindrome_raw(LPSTR t, int length) { cout << "palindrome_raw" << endl; __int64 start = get_local_ft_time(); int max = 0; // 最長回文的起點 int l_max = 1; // 最長回文的長度(l: length, 長度的意思) for (int i = 1; i < length; i++) { // i為對稱點 int d = 1; // d為回文擴展半徑 while (i - d >= 0 && i + d < length && t[i - d] == t[i + d]){ // 以i為中心對稱。aba d++; } d--; if (2 * d + 1 > l_max){ max = i - d; l_max = 2 * d + 1; } // 循環結束時d總不滿足判斷條件,所以減1 d = 0; // d為回文擴展半徑 while (i - d >= 0 && i + 1 + d < length && t[i - d] == t[i + 1 + d]){ // 以i后面空隙為中心對稱。abba d++; } d--; if (2 * (d + 1) > l_max){ max = i - d; l_max = 2 * (d + 1); } } char c = t[max + l_max]; t[max + l_max] = 0; cout << t + max << endl; t[max + l_max] = c; __int64 end = get_local_ft_time(); cout << "處理時間: " << diff_ft_time_ms(end, start) << "ms" << endl; }
算法說明:
對每個數據位置i, 分別尋找
1. 以i為對稱點的回文。比如文本: aba,以b對稱。
2. 以i與i+1直接的空隙對稱的回文。比如文本abba,以bb之間的空隙對稱。
所以,對每個點輪詢兩次。
回文算法: 馬拉車(Manacher)算法
馬拉車算法使用空間換取時間,把每個點的回文半徑存儲起來。為了避免輪詢兩次,算法把原始文本的每個字符讓固定字符(比如#)前后包圍起來,這樣,對於原始文本aba和abba,處理后的文本變成#a#b#a#和#a#b#b#a#,這樣,無論對於#a#b#a#和#a#b#b#a#,總有中心對稱點m,從而避免了對稱點落在字符的間隙中的情況。
算法把回文半徑存儲起來,在一個已經確定的大的回文當中,右半部分的點的回文與已經確定的左邊部分的點回文具有對稱性,所以節省掉一部分輪詢的時間。這里說的某點的回文,指以該點為中心對稱的回文。
如上圖,以m點對稱的回文其半徑已經確定是p[m],那么對於m點右側的i點,總有一個沿m點對稱的j點。由於m點回文的對稱性,j點的回文與i點的回文在m回文的區域是一定對稱的。這是馬拉車算法規律的基礎。
代碼引用自: https://www.cnblogs.com/grandyang/p/4475985.html。源代碼使用string和vector類,調試發現訪問類中的數據是耗時的主要原因,所以將類數據改成更接近機器指令的數組,實測發現效率增長有百倍之多。這也是一個教訓,評估算法不能通過高級的類去訪問數據。
void Manacher(LPSTR s, int length_raw) { cout << "Manacher" << endl; int length = 2 * length_raw + 1; LPSTR t = new char[length + 1]; for (int i = 0; i < length_raw; i ++) { t[2 * i] = '#'; t[2 * i + 1] = s[i]; } t[length - 1] = '#'; t[length] = 0; int * p = new int[length]; ZeroMemory(p, length * 4); __int64 start = get_local_ft_time(); int mx = 0, id = 0, resLen = 0, resCenter = 0; for (int i = 1; i < length; ++i) { int p_i = mx > i ? min(p[2 * id - i], mx - i) : 1; while (t[i + p_i] == t[i - p_i]) ++p_i; if (mx < i + p_i) { mx = i + p_i; id = i; } if (resLen < p_i) { resLen = p_i; resCenter = i; } p[i] = p_i; } char c = s[(resCenter - resLen) / 2 + resLen - 1]; s[(resCenter - resLen) / 2 + resLen - 1] = 0; cout << s + (resCenter - resLen) / 2 << endl; s[(resCenter - resLen) / 2 + resLen - 1] = c; __int64 end = get_local_ft_time(); cout << "處理時間: " << diff_ft_time_ms(end, start) << "ms" << endl; delete [] t; delete [] p; }
回文算法: 自己嘗試的算法
把文本數據看做函數曲線,則有下面的規律:
1. 遞增或者遞減的區間內,一定沒有對稱性。
2. 恆值區間,一定有對稱性。
3. 遞增、遞減的屬性變化時,在最高點或最低點(拐點),可能存在對稱性。
4. 遞增或者遞減變化成恆值時,一定沒有對稱性。
根據以上的規律,寫出相應的代碼:
void palindrome_zjs(LPSTR t, int length) { cout << "palindrome_zjs" << endl; __int64 start = get_local_ft_time(); int l = 0; // 起點l(left,左邊的意思) int s = 0; // 符號s(sign, 符號的意思),代表上升,下降或者平坦 (1, -1, 0) int max = 0; // 最長回文的起點 int l_max = 1; // 最長回文的長度(l: length, 長度的意思) for (int r = 1; r < length; r++) { // 終點r(right, 右邊的意思) int s_n = t[r] == t[r - 1] ? 0 : t[r] > t[r - 1] ? 1 : -1;// 上升、下降或者不變? if (s_n == s) { // 處在遞增、遞減或者恆值的階段中,此時不作處理 ; } else if(s_n == 0){ // 由遞增、遞減變成不變 l = r - 1; // 新線段的起點 s = s_n; // 增減屬性 } else if (s == 0) { // 不變的區域結束。恆值區總是自對稱,比如aa, aaa int i = 1; int right = r - 1; // right指向最后一個恆值區的位置 while (l - i >= 0 && right + i < length && t[l - i] == t[right + i]){ // 沿恆值區向左右擴展即可。 i++; } i--; // 循環結束時i總不滿足判斷條件,所以減1 if (right + i - (l - i) + 1 > l_max){ max = l - i; l_max = right + i - max + 1; } l = r; // 新線段的起點 s = s_n; // 增減屬性 } else if (s_n != 0) { // 遞增變成遞減,或者遞減變成遞增 int i = 1; int c = r - 1; // c是拐點(最低或者最高點)。 while (c - i >= 0 && c + i < length && t[c - i] == t[c + i]){ // 拐點為對稱點。 i++; } i--; // i總不滿足條件,所以減1 if (2 * i + 1 > l_max){ max = c - i; l_max = 2 * i + 1; // + 1是加拐點本身 } l = r; // 新線段的起點 s = s_n; // 增減屬性 } assert(1); } char c = t[max + l_max]; t[max + l_max] = 0; cout << t + max << endl; t[max + l_max] = c; __int64 end = get_local_ft_time(); cout << "處理時間: " << diff_ft_time_ms(end, start) << "ms" << endl; }
幾種算法的比較
算法 格外的內存 運算時間(10M字節的隨機文本)
原始算法 不需要 30ms
馬拉車算法 2倍的文本 110ms
自己的代碼 不需要 60ms
10M個數據,耗時在100ms左右(100M條指令),算法的時間級數似乎都是O(n)。
結果頗讓人費解,為什么馬拉車算法和自己嘗試的算法跑不過原始的算法?
我所能理解到的原因是這樣的:
由於文本隨機產生,產生長回文的可能性非常小,所以試圖捕捉規律減少重復判斷的那些代碼的功效沒有發揮出來。另一方面,由於考慮的更多,代碼變復雜了,每個循環執行的指令條數就增加了,故而產生復雜算法跑不過原始算法的結果。
這里也驗證了一個常識:代碼越精簡,執行指令條數越少,程序運行就越快。