[LeetCode: 3. 无重复字符的最长子串] | 刷题打卡

191 阅读3分钟

双指针算法

算法介绍

双指针算法其实不是一种特定的算法, 更像是一种思想. 它十分巧妙, 应用也十分广泛, 只要你发现当前问题具备单调性, 那么你就可以尝试使用这种思想来优化现有的解决方案. 通常用于提高性能(经典的从O(n^2)优化成O(n)).

从上面的描述你可以看出, 双指针是用于优化我们原有的解决方案的. 也就是说, 对于某一个问题, 我们都会先想出一个最朴素的作法(直观的作法), 然后再对其进行优化. 但是因为双指针这个四星比较抽象, 所以我们还是利用题目来帮助大家理解.

适用场景

我们先说一下双指针的适用场景, 对于某一个问题, 如果你发现了具备单调性, 那么你就可以尝试使用双指针进行优化.

具体案例

无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度
eg: input: "abcabcbb", 最长子串是"abc", 故返回答案3

原题链接: leetcode: 3. 无重复字符的最长子串

对于这道题, 我们很容易就想到的一种做法是, 遍历所有字符, 求出以当前字符结尾的最长无重复连续子串的长度. 然后去个max就ok. 具体代码如下:

var lengthOfLongestSubstring = function(s) {
    let ans = 0;
    for(let i = 0; i < s.length; i ++) {
        let j = i, hash = {};
        while(j >= 0 && !hash[s[j]]) {
            hash[s[j]] = true;
            j --;
        }
        if(ans < i - j) ans = i - j;
    }
    return ans;
};

可以发现, 该算法的时间复杂度是O(n ^ 2)的, 需要进行俩重循环. 接下来我们就来思考一下本题是否具备我们上面所说的单调性.

对于每一个字符(下面用i来代表该字符的下标)来说, 都会对应着存在一个在它左边的字符(下面用j来代表该字符的下标)距离它最远, 且满足s[j ~ i]区间中不存在重复字符的情况. 那么, i和j的具备一种关系: 当i向右走的时候, j只可能不动或者向右走而不可能向左走.

下面我们就来论证一下它们的关系是否是正确的, 如果是正确的, 那么我们就可以说它具备单调性(建议论证过程大家可以画个图理解一下)

想象字符串是一个数轴, 在j ~ i这个区间是以s[i]字符结尾的无重复字符的最长子串. 这个区间有什么特点呢? 只要j往左移动一位, 那么该区间就会具有重复字符, 即j就是一个左边界. 而当i右移会如何呢? 它意味这个区间会引入一个新的字符, 这个新字符有俩种可能:

  • 它是原区间未出现过的字符
  • 它是原区间存在过的字符

为了保证区间具备无重复字符这一特点, 对这俩种情况我们有不同的处理方案:

  1. 引入未出现过的字符, 那么j不需要变化.
  2. 引入原区间存在过的字符, 那么j就应该向后移动, 直到不存在重复字符为止.

至此, 我们就可以得出结论, 当i增加的时候, j只可能增加而不可能减少. 即具备单调性.

既然证明出了具备单调性, 我们就可以使用双指针来进行优化, 具体的实现思路其实我们在上面已经透露了. 就是动态维护一个区间, 左端点是j, 右端点是i. 让该区间一直都具备它是以s[i]字符结尾的无重复字符的最长子串这一性质即可. 代码如下:

var lengthOfLongestSubstring = function(s) {
    let ans = 0, hash = {}; // hash: 用于记录字符出现的次数
    for(let i = 0, j = 0; i < s.length; i ++) {
        if(hash[s[i]] === undefined) hash[s[i]] = 0; // 这里是为了解决undefined与number类型做运算为NaN的情况.
        hash[s[i]] ++;
        while(hash[s[i]] > 1) hash[s[j ++]] --; // 由于原区间是不存在重复字符的, 所以如果引入新字符导致有重复字符的话, 那么肯定是新字符搞的鬼. 
        if(ans < i - j + 1) ans = i - j + 1; // 为什么是 i - j + 1, 主要是因为最后i和j是我们维护这个区间的左右端点, 大家拿笔算一下就可以了.
    }
    return ans; // 最后将答案返回.
};

好的最后让我们来分析一下时间复杂度, 虽然代码看上去有俩重循环, 但是在我们的思路中我们可以看到, i和j都是始终向后移动的, 最多i和j就是从头走到尾, 也就是遍历俩次数组. 时间复杂度是O(2n). 但通常常数不算, 所以我们的时间复杂度是O(n)的. 至此, 我们就将双指针是如何一步步优化原有方案的过程给大家讲完了.

总结

双指针相对来说还是比较简单的, 主要是需要自己动笔在纸上画一画, 自行分析一遍. 切忌只在脑中思考, 那样只会导致你的头发在不停的"燃烧"... 然后就是多做几道习题巩固一下, Leetcode上面有双指针的tag. 可以很好的帮助你们熟练的掌握这个思想.

额外题目

鉴于双指针比较抽象, 所以多准备了一道题目帮助大家进一步掌握双指针算法. : leetcode: 167. 两数之和 II - 输入有序数组

本文正在参与「掘金 3 月闯关活动」,点击查看活动详情