1.22 LeetCode总结(基本算法)_双指针

一、单序列双指针

1.1 反转字符串

本质是相向双指针。

344. 反转字符串

c 复制代码
void reverseString(char *s, int sSize)
{
    int right = sSize - 1;
    for (int left = 0; left < right; left++) {
        char tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;
        right--;
    }
}

1.2 相向双指针

两个指针 left=0, right=n−1,从数组的两端开始,向中间移动,这叫相向双指针。上面的滑动窗口相当于同向双指针。

2697. 字典序最小回文串

c 复制代码
char *makeSmallestPalindrome(char *s)
{
    int n = strlen(s);
    int l = 0, r = n - 1;
    while (l < r) {
        if (s[l] != s[r]) {
            // 统一改成更小的字符,保证字典序最小
            char minCh = (s[l] < s[r]) ? s[l] : s[r];
            s[l] = minCh;
            s[r] = minCh;
        }
        l++;
        r--;
    }
    return s;
}

1.3 同向双指针

两个指针的移动方向相同(都向右,或者都向左)。

611. 有效三角形的个数

c 复制代码
// 升序排序
int cmp(const void* a, const void* b)
{
    return *(int*)a - *(int*)b;
}
int triangleNumber(int* nums, int numsSize)
{
    if (numsSize < 3) {
        return 0;
    }
    qsort(nums, numsSize, sizeof(int), cmp);
    int ans = 0;
    for (int k = 2; k < numsSize; k++) {
        int left  = 0;
        int right = k - 1;
        while (left < right) {
            if (nums[left] + nums[right] > nums[k]) {
                // left 到 right-1 全部合法, 因为 nums[left] 本身是最小的
                ans += right - left;
                right--;
            } else { 
                // 当前最小的左 + 当前最大的右都不够,说明这个 left 搭配任何小于 right 的数都不可能满足条件
                // 只能 left++,换更大的左数试试。
                left++;
            }
        }
    }
    return ans;
}

二、双序列双指针

什么是「双序列双指针」

两个独立有序序列,各用一个指针遍历,互相配合完成合并、筛选、插入、匹配操作,就是双序列双指针。

2.1 双指针

2109. 向字符串添加空格

本题两个序列:

序列 1:原字符串 s,指针 i(遍历原字符串每个字符下标)

序列 2:空格位置数组 spaces,指针 p(遍历要插入空格的原下标)

两个指针各自只往前走,不回头、不回退,属于典型双序列同向双指针

c 复制代码
char *addSpaces(char *s, int *spaces, int spacesSize)
{
    int len = strlen(s);
    // 新字符串长度 = 原长度 + 空格个数 + 结束符
    char *res = (char*)malloc(len + spacesSize + 1);
    int idx = 0;
    int p = 0; // spaces指针
    for (int i = 0; i < len; i++) {
        // 当前位置需要插入空格
        if (p < spacesSize && i == spaces[p]) {
            res[idx++] = ' ';
            p++;
        }
        res[idx++] = s[i];
    }
    res[idx] = '\0';
    return res;
}
int main() {
    char s[] = "LeetcodeHelpsMeLearn";
    int spaces[] = {8,13,15};
    char* ans = addSpaces(s, spaces, 3);
    printf("%s\n", ans);
    free(ans);
    return 0;
}

2.2 子序列

思路

两个指针 i(遍历子串s)、j(遍历母串t):

如果 si == tj:匹配成功,两个指针同时右移;

如果不相等:只移动母串指针 j;

最终如果 i 走完了s全部字符,说明是子序列,返回true,否则false。

392. 判断子序列

c 复制代码
bool isSubsequence(char *s, char *t)
{
    int i = 0, j = 0;
    int lenS = strlen(s);
    int lenT = strlen(t);
    while (i < lenS && j < lenT) {
        if (s[i] == t[j]) {
            i++;
        }
        j++;
    }
    return i == lenS;
}

三、三指针

2367. 等差三元组的数目

核心观察(单调性)

因为数组递增:

如果差值偏小,只能把靠右的指针右移,放大差值;

如果差值偏大,只能把靠左的指针右移,缩小差值;

三个指针永远只往右走,没有回头

c 复制代码
int arithmeticTriplets(int* nums, int numsSize, int diff)
{
    if (numsSize < 3) return 0;
    int ans = 0;
    int i = 0, j = 1, k = 2;
    while (k < numsSize) {
        int d1 = nums[j] - nums[i];
        int d2 = nums[k] - nums[j];
        if (d1 == diff && d2 == diff) {
            ans++;
            i++;
            j++;
            k++;
        } else if (d1 < diff) {
            // 保证 j 不能追上 k
            if (j + 1 < k) {
                j++;
            } else {
                k++;
            }
        } else if (d2 < diff) {
            k++;
        } else {
            i++;
        }
    }
    return ans;
}

四、分组循环

适用场景:按照题目要求,数组会被分割成若干组,每一组的判断/处理逻辑是相同的。

核心思想:

外层循环负责遍历组之前的准备工作(记录开始位置),和遍历组之后的统计工作(更新答案最大值)。

内层循环负责遍历组,找出这一组最远在哪结束。

这个写法的好处是,各个逻辑块分工明确,也不需要特判最后一组(易错点)。以我的经验,这个写法是所有写法中最不容易出 bug 的,推荐大家记住。

485. 最大连续 1 的个数

c 复制代码
int findMaxConsecutiveOnes(int *nums, int numsSize)
{
    int maxLen = 0;
    int i = 0;
    // 外层循环:遍历所有分组
    while (i < numsSize) {
        // 内循环1:跳过所有0,找到连续1的起始位置
        while (i < numsSize && nums[i] == 0) {
            i++;
        }
        int start = i;
        // 内循环2:遍历当前这一段连续的1
        while (i < numsSize && nums[i] == 1) {
            i++;
        }
        // 计算当前分组长度,更新最大值
        int curLen = i - start;
        if (curLen > maxLen) {
            maxLen = curLen;
        }
    }
    return maxLen;
}