二分查找高效断定子序列

读完本文,你能够去力扣拿下以下题目: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,欢迎标星!