拉賓-卡普模式搜索算法 Rabin-Karp算法

在許多數(shù)據(jù)結(jié)構(gòu)與算法(DSA)問題中,一個(gè)常見任務(wù)是比較字符串——無論是在句子中尋找單詞、檢測重復(fù)詞,還是檢查較大文本中的模式。
可以把這想象成在一大段文字里找一個(gè)短語——手動(dòng)檢查每個(gè)字符會(huì)很累。電腦也是這樣,如果不小心,長文本會(huì)變得非常慢。

參考編程資源
https://pan.quark.cn/s/7f7c83756948
參考
https://pan.quark.cn/s/bda57957c548

參考編程資源
參考

這正是Rabin-Karp算法發(fā)揮作用的地方。

它由Michael Rabin和Richard Karp于1987年開發(fā),作為一種巧妙的方法,利用哈希技術(shù)加快模式匹配。它不是逐個(gè)比較字符,而是將字符串轉(zhuǎn)換成數(shù)字(哈希)并進(jìn)行比較——就像商店里用條形碼而不是讀取完整產(chǎn)品名稱一樣。

簡而言之,拉賓-卡普是:

字符串匹配算法
使用滾動(dòng)哈希來查找文本中所有模式的出現(xiàn)
針對(duì)需要多圖案或重復(fù)匹配的情況進(jìn)行了優(yōu)化
這為許多現(xiàn)代算法在抄襲檢測、子字符串搜索等領(lǐng)域奠定了基礎(chǔ)。

目錄

為什么需要拉賓-卡普?
拉賓-卡普的關(guān)鍵思想
實(shí)用示例以更好地理解
拉賓-卡普的局限性
需要雙哈希
Rabin-Karp 能解決的問題類型
為什么需要拉賓-卡普?
假設(shè)你想查找“代碼”這個(gè)詞是否出現(xiàn)在一篇大文章中。
最簡單的方法?從第一個(gè)字母開始,逐一檢查每組4個(gè)字母。這被稱為“天真方法”,效果不錯(cuò)——但它每次都要檢查每個(gè)字符,如果文本較長,這會(huì)非常緩慢。

樸素方法的時(shí)間復(fù)雜度為O(n × m),其中:

n = 文本長度
m = 圖案長度
現(xiàn)在想象一下,試圖找出多個(gè)模式(比如檢查一份文檔中的100個(gè)不同單詞)。用天真的方式做這件事會(huì)變得非常緩慢。

Rabin-Karp 通過使用滾動(dòng)哈希改進(jìn)了這一點(diǎn)。

它不是每次都比較實(shí)際字符,而是將模式和文本部分轉(zhuǎn)換成數(shù)字(哈希值),然后對(duì)這些數(shù)字進(jìn)行比較。這樣,大多數(shù)情況下它能在不看每個(gè)角色的情況下檢測不匹配,從而更快且可擴(kuò)展。

拉賓-卡普的關(guān)鍵思想
Rabin-Karp的核心思想是將字符串轉(zhuǎn)換為數(shù)值哈希,這樣我們就能比較數(shù)字而非字符。這大大加快了整個(gè)過程。
為了高效實(shí)現(xiàn)這一點(diǎn),我們使用滾動(dòng)哈希,通過更新上一個(gè)哈希,使我們能在常數(shù)時(shí)間內(nèi)計(jì)算下一個(gè)子串的哈希值。所以我們不再重新檢查每個(gè)字符,而是滑動(dòng)窗口調(diào)整哈希值。
我們先計(jì)算該模式的哈希值,然后將其與文本所有子串的哈希值進(jìn)行比較。如果哈希匹配,我們會(huì)逐字符快速檢查以確認(rèn)(以避免哈希碰撞導(dǎo)致的錯(cuò)誤)。
這樣,大多數(shù)比較都是用數(shù)字完成的,使模式匹配變得快速且可擴(kuò)展,尤其是對(duì)于大型文本或多個(gè)圖案。

在Rabin-Karp中,哈希值是如何計(jì)算的?
Rabin-Karp 中的哈希值是通過滾動(dòng)哈希函數(shù)計(jì)算的,這使得哈希值在圖案滑動(dòng)文本時(shí)能夠高效地更新。滾動(dòng)哈希讓我們不必為每個(gè)子串重新計(jì)算整個(gè)哈希,而是可以移除舊字符的貢獻(xiàn),并在常數(shù)時(shí)間內(nèi)添加新的字符。

字符串通過多項(xiàng)式滾動(dòng)哈希轉(zhuǎn)換為數(shù)值哈希。對(duì)于長度為 n 的字符串 s,哈希值計(jì)算為:

=>哈希(s) = (s[0] * p(n-1)+ s[1] * p(n-2)+ ... + s[n-1] * p0) %mod

其中:

S[i] 是第i個(gè)字符的數(shù)值('a' = 1, 'b' = 2, ..., 'z' = 26)
p 是一個(gè)小質(zhì)數(shù)(通常為 31 或 37)
Mod 是一個(gè)較大的質(zhì)數(shù)(比如 1E9 + 7),以避免溢出并減少哈希碰撞
這種方法允許我們利用預(yù)計(jì)算的冪和前置哈希在常數(shù)時(shí)間內(nèi)計(jì)算子串的哈希值。
哈希遞歸關(guān)系
設(shè) preHash[i] 表示前綴子串 s[0...i] 的哈希值。

則遞歸為:preHash[i] = preHash[i - 1] * base + s[i]

其中:

p[0] = s[0]
S[i] 是第i個(gè)字符的數(shù)值('a' = 1, 'b' = 2, ..., 'z' = 26)
底數(shù)是選定的質(zhì)數(shù)(通常為31或37)
所有作都在模 mod 下進(jìn)行,以避免溢出
如何從L到R計(jì)算哈希值?
要得到任意子串 s[L... 的哈希值R] 高效地使用前綴哈希。

首先,我們預(yù)計(jì)算前綴哈希,其中前綴[i]存儲(chǔ)子字符串s[0...i]的哈希值。同時(shí),我們還預(yù)先計(jì)算了基數(shù)的冪(如基數(shù)1、基數(shù)2等),以便快速計(jì)算。
然后,利用模運(yùn)算,我們可以用以下公式在常數(shù)時(shí)間內(nèi)計(jì)算任意范圍的哈希值:

hash(L, R) = (前綴[R] - 前綴[L-1] * power(R - L + 1) + mod) % mod

這里,

前綴[R]是從0到R的哈希值
前綴[L-1] * 冪(R - L + 1)去除前L字符的貢獻(xiàn)

  • mod 確保結(jié)果保持正值,然后再應(yīng)用 % mod
class GfG {
    private final int mod = (int)1e9 + 7;
    private final int base = 31;
    private int[] hash;
    private int[] power;

    // modular addition
    private int add(int a, int b) {
        a += b;
        if (a >= mod) a -= mod;
        return a;
    }

    // modular subtraction
    private int sub(int a, int b) {
        a -= b;
        if (a < 0) a += mod;
        return a;
    }

    // modular multiplication
    private int mul(int a, int b) {
        return (int)((1L * a * b) % mod);
    }

    // convert character to int 
    // ('a' = 1, ..., 'z' = 26)
    private int charToInt(char c) {
        return c - 'a' + 1;
    }

    // constructor: precomputes prefix hashes and powers
    public GfG(String s) {
        int n = s.length();
        hash = new int[n];
        power = new int[n];

        hash[0] = charToInt(s.charAt(0));
        power[0] = 1;

        for (int i = 1; i < n; i++) {
            hash[i] = add(mul(hash[i - 1], base), charToInt(s.charAt(i)));
            power[i] = mul(power[i - 1], base);
        }
    }

    // get hash of substring s[l...r] in O(1)
    public int getSubHash(int l, int r) {
        int h = hash[r];
        if (l > 0) {
            h = sub(h, mul(hash[l - 1], power[r - l + 1]));
        }
        return h;
    }
}

時(shí)間復(fù)雜度:O(n),前綴哈希和冪在字符串的單次處理中預(yù)先計(jì)算完成,耗時(shí)線性時(shí)間。一旦構(gòu)建完成,任何子字符串哈希都可以在 O(1) 時(shí)間內(nèi)檢索。
輔助空間:O(n),兩個(gè)大小為n的數(shù)組用于存儲(chǔ)基的前綴哈希和冪次,導(dǎo)致線性額外空間使用。

實(shí)用示例以更好地理解
給定兩個(gè)字符串:文本(文本)和模式(模式),由小寫英文字母表組成,找到所有以0為基礎(chǔ)的起始索引,其中模式作為文本中的子字符串出現(xiàn)。

示例:

輸入:text = “geeksforgeeks”,pattern = “geeks”
輸出:[0, 8]
解釋:字符串“geeks”出現(xiàn)在文本索引0和8。

輸入:文本 = “aabaacaadaaba”,模式 = “aaba”
輸出:[0, 9, 12]
解釋:

圖片
用于模式搜索的KMP算法
在樸素字符串匹配算法中,我們逐一檢查文本中每個(gè)大小大小的子字符串是否等于該模式。

與樸素算法類似,Rabin-Karp 算法也檢查每個(gè)子串。但與樸素算法不同,Rabin Karp算法會(huì)將模式的哈希值與當(dāng)前文本子串的哈希值匹配,只有當(dāng)哈希值匹配時(shí),才會(huì)開始匹配單個(gè)字符。因此,Rabin Karp 算法需要計(jì)算以下字符串的哈希值。

圖案本身
文本中所有長度為 m 的子串,也就是模式的大小。

import java.util.ArrayList;

class RabinKarpHash {
    private final int mod = 1000000007;
    private final int base = 31;
    private int[] hash;
    private int[] power;

    // modular addition
    private int add(int a, int b) {
        a += b;
        if (a >= mod) a -= mod;
        return a;
    }

    // modular subtraction
    private int sub(int a, int b) {
        a -= b;
        if (a < 0) a += mod;
        return a;
    }

    // modular multiplication
    private int mul(int a, int b) {
        return (int)((1L * a * b) % mod);
    }

    // convert character to int 
    // ('a' = 1, ..., 'z' = 26)
    private int charToInt(char c) {
        return c - 'a' + 1;
    }

    // constructor: precomputes prefix hashes and powers
    public RabinKarpHash(String s) {
        int n = s.length();
        hash = new int[n];
        power = new int[n];

        hash[0] = charToInt(s.charAt(0));
        power[0] = 1;

        for (int i = 1; i < n; ++i) {
            hash[i] = add(mul(hash[i - 1], base), charToInt(s.charAt(i)));
            power[i] = mul(power[i - 1], base);
        }
    }

    // get hash of substring s[l...r] in O(1)
    public int getSubHash(int l, int r) {
        int h = hash[r];
        if (l > 0) {
            h = sub(h, mul(hash[l - 1], power[r - l + 1]));
        }
        return h;
    }
}

class GfG {
    // Rabin-Karp search using hash class
    public static ArrayList<Integer> searchPattern(String text, String pattern) {
        int n = text.length(), m = pattern.length();
        RabinKarpHash textHash = new RabinKarpHash(text);
        RabinKarpHash patHash = new RabinKarpHash(pattern);

        int patternHash = patHash.getSubHash(0, m - 1);
        ArrayList<Integer> result = new ArrayList<>();

        for (int i = 0; i <= n - m; i++) {
            int subHash = textHash.getSubHash(i, i + m - 1);
            if (subHash == patternHash) {
                result.add(i);
            }
        }

        return result;
    }

    public static void main(String[] args) {
        String txt = "geeksforgeeks";
        String pat = "geek";

        ArrayList<Integer> positions = searchPattern(txt, pat);
        for (int idx : positions) {
            System.out.print(idx + " ");
        }
        System.out.println();
    }
}

輸出
0 8
時(shí)間復(fù)雜度:O(n + m),我們計(jì)算文本和模式的前綴哈希和冪,O(n + m)。然后,我們將窗口滑過文本,每個(gè)子字符串的哈希值以 O(1) 進(jìn)行比較。
輔助空間:O(n + m),我們存儲(chǔ)文本和模式的前綴哈希和冪數(shù)組,取用 O(n + m) 空間。

拉賓-卡普的局限性
可能發(fā)生哈希碰撞——不同的子串可能擁有相同的哈希值。
需要逐角色檢查以確認(rèn)匹配。
如果不小心處理,存在減量溢流的風(fēng)險(xiǎn)。
性能取決于良好的哈希函數(shù)和素模數(shù)。
與像KMP這樣更簡單的算法相比,常數(shù)因子略高一些。
為什么單哈希會(huì)失敗(哈希碰撞):

使用單一哈希函數(shù)時(shí),總有可能兩個(gè)不同的子串產(chǎn)生相同的哈希值。這被稱為哈希碰撞。

為什么會(huì)發(fā)生這種事?
=> 我們計(jì)算的是模大數(shù)的哈希(例如,10^9 + 7)
=> 但由于可能的子串?dāng)?shù)量巨大,不同子串取模后可能會(huì)意外得到相同的哈希。

結(jié)果:如果兩個(gè)不同的子串具有相同的哈希值,算法可能會(huì)錯(cuò)誤報(bào)告匹配(假陽性)。在這種情況下,Rabin-Karp需要驗(yàn)證實(shí)際角色才能確認(rèn)匹配——這會(huì)拖慢性能。

需要雙哈希
為了降低碰撞概率,我們使用雙重哈?!从?jì)算兩個(gè)不同的獨(dú)立哈希值:基值(p1, p2)和模數(shù)(mod1, mod2)

它的幫助:

=> 現(xiàn)在,只有當(dāng)兩個(gè)子串的哈希值相匹配時(shí),才被視為相等。
=> 兩個(gè)不同子串在兩個(gè)哈希函數(shù)中碰撞的概率極低——大約為1/(mod1 x mod2),幾乎可以忽略不計(jì)。

import java.util.ArrayList;

public class RabinKarpHash {
    private final int mod1 = (int) 1e9 + 7;
    private final int mod2 = (int) 1e9 + 9;
    private final int base1 = 31;
    private final int base2 = 37;

    private int[] hash1, hash2;
    private int[] power1, power2;

    // modular addition
    private int add(int a, int b, int mod) {
        a += b;
        if (a >= mod) a -= mod;
        return a;
    }

    // modular subtraction
    private int sub(int a, int b, int mod) {
        a -= b;
        if (a < 0) a += mod;
        return a;
    }

    // modular multiplication
    private int mul(int a, int b, int mod) {
        return (int) ((1L * a * b) % mod);
    }

    // convert character to int
    private int charToInt(char c) {
        return c - 'a' + 1;
    }

    // constructor: precomputes both prefix hashes and powers
    public RabinKarpHash(String s) {
        int n = s.length();
        hash1 = new int[n];
        hash2 = new int[n];
        power1 = new int[n];
        power2 = new int[n];

        hash1[0] = charToInt(s.charAt(0));
        hash2[0] = charToInt(s.charAt(0));
        power1[0] = 1;
        power2[0] = 1;

        for (int i = 1; i < n; ++i) {
            hash1[i] = add(mul(hash1[i - 1], base1, mod1),
                           charToInt(s.charAt(i)), mod1);

            power1[i] = mul(power1[i - 1], base1, mod1);

            hash2[i] = add(mul(hash2[i - 1], base2, mod2),
                           charToInt(s.charAt(i)), mod2);

            power2[i] = mul(power2[i - 1], base2, mod2);
        }
    }

    // get double hash of substring s[l...r]
    public ArrayList<Integer> getSubHash(int l, int r) {
        int h1 = hash1[r];
        int h2 = hash2[r];
        if (l > 0) {
            h1 = sub(h1, mul(hash1[l - 1], power1[r - l + 1], mod1), mod1);
            h2 = sub(h2, mul(hash2[l - 1], power2[r - l + 1], mod2), mod2);
        }

        ArrayList<Integer> res = new ArrayList<>();
        res.add(h1);
        res.add(h2);
        return res;
    }
}

時(shí)間復(fù)雜度:O(n),前綴哈希和冪在字符串的單次處理中預(yù)先計(jì)算完成,耗時(shí)線性時(shí)間。一旦構(gòu)建完成,任何子字符串哈希都可以在 O(1) 時(shí)間內(nèi)檢索。
輔助空間:O(n),兩個(gè)大小為n的數(shù)組用于存儲(chǔ)基的前綴哈希和冪次,導(dǎo)致線性額外空間使用。

Rabin-Karp 能解決的問題類型
模式匹配——查找大文本中所有模式的出現(xiàn)
抄襲檢測——通過檢查常見子字符串來比較文檔
多重模式搜索——高效地同時(shí)搜索多個(gè)模式
子字符串比較——使用哈希值快速比較子字符串
回文和DP問題——用于基于哈希的優(yōu)化
檢測重復(fù)子串——查找字符串中的重復(fù)序列
最長的常見前綴/后綴——在常數(shù)時(shí)間內(nèi)使用預(yù)先計(jì)算的哈希值

參考編程資源
https://pan.quark.cn/s/7f7c83756948
參考
https://pan.quark.cn/s/bda57957c548

參考編程資源
參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容