幾種回文算法的比較


前言

這是我的第一篇博文,獻給算法。

學習和研究算法可以讓人變得更加聰明。

算法的目標是以更好的方法完成任務。

更好的方法的具體指標是:

  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)。

 

結果頗讓人費解,為什么馬拉車算法和自己嘗試的算法跑不過原始的算法?

我所能理解到的原因是這樣的:

由於文本隨機產生,產生長回文的可能性非常小,所以試圖捕捉規律減少重復判斷的那些代碼的功效沒有發揮出來。另一方面,由於考慮的更多,代碼變復雜了,每個循環執行的指令條數就增加了,故而產生復雜算法跑不過原始算法的結果。

這里也驗證了一個常識:代碼越精簡,執行指令條數越少,程序運行就越快。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM