二分查找高效斷定子序列

讀完本文,你能夠去力扣拿下以下題目:java

392.判斷子序列git

-----------github

二分查找自己不難理解,難在巧妙地運用二分查找技巧。對於一個問題,你可能都很難想到它跟二分查找有關,好比前文 最長遞增子序列 就藉助一個紙牌遊戲衍生出二分查找解法。算法

今天再講一道巧用二分查找的算法問題:如何斷定字符串 s 是不是字符串 t 的子序列(能夠假定 s 長度比較小,且 t 的長度很是大)。舉兩個例子:數組

s = "abc", t = "ahbgdc", return true.指針

s = "axc", t = "ahbgdc", return false.code

題目很容易理解,並且看起來很簡單,但很難想到這個問題跟二分查找有關吧?索引

1、問題分析

首先,一個很簡單的解法是這樣的:遊戲

bool isSubsequence(string s, string t) {
    int i = 0, j = 0;
    while (i < s.size() && j < t.size()) {
        if (s[i] == t[j]) i++;
        j++;
    }
    return i == s.size();
}

其思路也很是簡單,利用雙指針 i, j 分別指向 s, t,一邊前進一邊匹配子序列:leetcode

gif

讀者也許會問,這不就是最優解法了嗎,時間複雜度只需 O(N),N 爲 t 的長度。

是的,若是僅僅是這個問題,這個解法就夠好了,不過這個問題還有 follow up

若是給你一系列字符串 s1,s2,... 和字符串 t,你須要斷定每一個串 s 是不是 t 的子序列(能夠假定 s 較短,t 很長)。

boolean[] isSubsequence(String[] sn, String t);

你也許會問,這不是很簡單嗎,仍是剛纔的邏輯,加個 for 循環不就好了?

能夠,可是此解法處理每一個 s 時間複雜度仍然是 O(N),而若是巧妙運用二分查找,能夠將時間複雜度下降,大約是 O(MlogN)。因爲 N 相對 M 大不少,因此後者效率會更高。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

2、二分思路

二分思路主要是對 t 進行預處理,用一個字典 index 將每一個字符出現的索引位置按順序存儲下來:

int m = s.length(), n = t.length();
ArrayList<Integer>[] index = new ArrayList[256];
// 先記下 t 中每一個字符出現的位置
for (int i = 0; i < n; i++) {
    char c = t.charAt(i);
    if (index[c] == null) 
        index[c] = new ArrayList<>();
    index[c].add(i);
}

好比對於這個狀況,匹配了 "ab",應該匹配 "c" 了:

按照以前的解法,咱們須要 j 線性前進掃描字符 "c",但藉助 index 中記錄的信息,能夠二分搜索 index[c] 中比 j 大的那個索引,在上圖的例子中,就是在 [0,2,6] 中搜索比 4 大的那個索引:

這樣就能夠直接獲得下一個 "c" 的索引。如今的問題就是,如何用二分查找計算那個剛好比 4 大的索引呢?答案是,尋找左側邊界的二分搜索就能夠作到。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,所有發佈在 labuladong的算法小抄,持續更新。建議收藏,按照個人文章順序刷題,掌握各類算法套路後投再入題海就如魚得水了。

3、再談二分查找

在前文 二分查找詳解 中,詳解了如何正確寫出三種二分查找算法的細節。二分查找返回目標值 val 的索引,對於搜索左側邊界的二分查找,有一個特殊性質:

val 不存在時,獲得的索引剛好是比 val 大的最小元素索引

什麼意思呢,就是說若是在數組 [0,1,3,4] 中搜索元素 2,算法會返回索引 2,也就是元素 3 的位置,元素 3 是數組中大於 2 的最小元素。因此咱們能夠利用二分搜索避免線性掃描。

// 查找左側邊界的二分查找
int left_bound(ArrayList<Integer> arr, int tar) {
    int lo = 0, hi = arr.size();
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (tar > arr.get(mid)) {
            lo = mid + 1;
        } else {
            hi = mid;
        } 
    }
    return lo;
}

以上就是搜索左側邊界的二分查找,等會兒會用到,其中的細節能夠參見前文《二分查找詳解》,這裏再也不贅述。

4、代碼實現

這裏以單個字符串 s 爲例,對於多個字符串 s,能夠把預處理部分抽出來。

boolean isSubsequence(String s, String t) {
    int m = s.length(), n = t.length();
    // 對 t 進行預處理
    ArrayList<Integer>[] index = new ArrayList[256];
    for (int i = 0; i < n; i++) {
        char c = t.charAt(i);
        if (index[c] == null) 
            index[c] = new ArrayList<>();
        index[c].add(i);
    }
    
    // 串 t 上的指針
    int j = 0;
    // 藉助 index 查找 s[i]
    for (int i = 0; i < m; i++) {
        char c = s.charAt(i);
        // 整個 t 壓根兒沒有字符 c
        if (index[c] == null) return false;
        int pos = left_bound(index[c], j);
        // 二分搜索區間中沒有找到字符 c
        if (pos == index[c].size()) return false;
        // 向前移動指針 j
        j = index[c].get(pos) + 1;
    }
    return true;
}

算法執行的過程是這樣的:

可見藉助二分查找,算法的效率是能夠大幅提高的。

_____________

個人 在線電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 算法倉庫 已經得到了 70k star,歡迎標星!

相關文章
相關標籤/搜索