算法刷题笔记:数组和矩阵的套路,看这篇就够了

13 阅读18分钟

算法刷题笔记:数组和矩阵的套路,看这篇就够了

第二篇:数组与矩阵

大家好,这是算法专栏的第二篇!今天我们一起来搞定数组和矩阵里最经典的题目。放心,我会把每道题的思路掰开揉碎讲,保证你看完也能自己写出来。

1. 最大子数组和

题目

给你一个整数数组 nums,找一个具有最大和的连续子数组,返回其最大和。

  • 示例:[-2,1,-3,4,-1,2,1,-5,4] → 输出 6(子数组 [4,-1,2,1] 的和最大)

思路过程

第一步:暴力解法(容易想到,但会超时)

// 枚举所有子数组的起点和终点
for (int i = 0; i < n; i++) {
    for (int j = i; j < n; j++) {
        int sum = 0;
        for (int k = i; k <= j; k++) {
            sum += nums[k];
        }
        max = Math.max(max, sum);
    }
}

问题:时间复杂度 O(n³),数据量大必超时。

第二步:优化到 O(n²)

// 用前缀和思想,内层循环用一个变量累加
for (int i = 0; i < n; i++) {
    int sum = 0;
    for (int j = i; j < n; j++) {
        sum += nums[j];  // 累加当前子数组和
        max = Math.max(max, sum);
    }
}

问题:虽然比暴力好一点,但还是太慢。

第三步:Kadane 算法(最优解!)

核心思想:负数只会拖后腿,及时止损!

想象你是个老板,开始攒钱(累加和)。如果累加和变成负数了,说明前面的子数组全是拖油瓶,不如从当前元素重新开始。

// 用一个变量 cur 记录"当前连续子数组的和"
int cur = nums[0];      // 初始化为第一个元素
int max = nums[0];      // 全局最大和

for (int i = 1; i < nums.length; i++) {
    // 两种选择:延续之前的子数组,或者从当前元素重新开始
    cur = Math.max(cur + nums[i], nums[i]);
    // 更新全局最大值
    max = Math.max(max, cur);
}
return max;

手动模拟一遍:

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

i=0: cur=-2, max=-2  (只有自己)
i=1: cur=max(-2+1, 1)=1, max=1  (前面的负数不要了,从1重新开始)
i=2: cur=max(1-3, -3)=-2, max=1
i=3: cur=max(-2+4, 4)=4, max=4  (到4这里爆发了!)
i=4: cur=max(4-1, -1)=3, max=4
i=5: cur=max(3+2, 2)=5, max=5
i=6: cur=max(5+1, 1)=6, max=6  ← 最终答案!
i=7: cur=max(6-5, -5)=1, max=6
i=8: cur=max(1+4, 4)=5, max=6

代码实现

class Solution {
    public int maxSubArray(int[] nums) {
        // 边界处理
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int cur = nums[0];  // 当前连续子数组的最大和
        int max = nums[0]; // 全局最大和
        
        // 从第二个元素开始遍历
        for (int i = 1; i < nums.length; i++) {
            // 状态转移:要么继续累加,要么从当前元素重新开始
            // 如果 cur 是负数,加上当前元素只会更小,所以不如重新开始
            cur = Math.max(cur + nums[i], nums[i]);
            
            // 记录遍历过程中的最大和
            max = Math.max(max, cur);
        }
        
        return max;
    }
}

复杂度分析

  • 时间复杂度:O(n),只遍历一遍数组
  • 空间复杂度:O(1),只用了两个变量

一句话总结

遇到负数就"断舍离",及时放弃拖累你的累加和,从头再来的贪心策略就是 Kadane 算法的精髓。

2. 合并区间

题目

给一些闭合区间,合并所有重叠的区间。

  • 示例:[[1,3],[2,6],[8,10],[15,18]][[1,6],[8,10],[15,18]]

思路过程

拿到题目先想:怎么判断两个区间重叠?

  • 区间 [a, b] 和 [c, d] 重叠 ⟺ c <= b(第二个区间的起点小于等于第一个区间的终点)

关键步骤:排序! 把区间按起点从小到大排序,这样重叠的区间就会排在一起,处理起来就简单多了。

// 1. 按起点排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);

// 2. 遍历合并
int[] cur = intervals[0];  // 当前合并后的区间
for (int i = 1; i < intervals.length; i++) {
    if (cur[1] >= intervals[i][0]) {
        // 重叠了!合并:取终点较大的那个
        cur[1] = Math.max(cur[1], intervals[i][1]);
    } else {
        // 不重叠,把当前区间加入结果,cur 更新为新区间
        result.add(cur);
        cur = intervals[i];
    }
}
result.add(cur);  // 别忘了最后一个

代码实现

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals == null || intervals.length <= 1) {
            return intervals;
        }
        
        // 1. 按区间起点升序排序
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
        
        // 用于存储合并后的区间
        List<int[]> merged = new ArrayList<>();
        
        // 2. 用一个变量记录当前合并区间
        int[] cur = intervals[0];
        merged.add(cur);
        
        // 3. 遍历剩余区间
        for (int i = 1; i < intervals.length; i++) {
            int start = intervals[i][0];
            int end = intervals[i][1];
            
            // 如果当前区间起点 <= 合并区间的终点,说明重叠了
            if (cur[1] >= start) {
                // 合并:更新终点为较大值
                cur[1] = Math.max(cur[1], end);
            } else {
                // 不重叠,开始新的合并区间
                cur = intervals[i];
                merged.add(cur);
            }
        }
        
        return merged.toArray(new int[0][]);
    }
}

复杂度分析

  • 时间复杂度:O(n log n),排序是主要开销
  • 空间复杂度:O(n),存储结果

一句话总结

先按起点排序,再用贪心逐个合并重叠区间——简单来说就是"排好队,扎堆处理"。

3. 轮转数组

题目

把数组元素向右轮转 k 个位置。

  • 示例:[1,2,3,4,5,6,7], k=3[5,6,7,1,2,3,4]

思路过程

暴力解法: 创建新数组,把每个元素放到目标位置。时间 O(n),空间 O(n)。

能不能原地搞?试试三次翻转!

核心思想:把数组分成两部分,先整体翻转,再局部翻转。

[1,2,3,4,5,6,7], k=3 为例:

原始:        [1,2,3,4,5,6,7]
目标:        [5,6,7,1,2,3,4]

观察规律:
- 末尾 3 个 [5,6,7] 跑到前面了
- 前 4 个 [1,2,3,4] 跑到后面去了

翻转技巧:
1. 整体翻转:  [7,6,5,4,3,2,1]
2. 前k个翻转: [5,6,7,4,3,2,1]
3. 后n-k翻转: [5,6,7,1,2,3,4]  ← 完成!

代码实现

class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k = k % n;  // 防止 k 超过数组长度
        
        if (k == 0) return;  // 不用旋转
        
        // 1. 整体翻转
        reverse(nums, 0, n - 1);
        
        // 2. 前 k 个翻转
        reverse(nums, 0, k - 1);
        
        // 3. 后 n-k 个翻转
        reverse(nums, k, n - 1);
    }
    
    // 翻转数组指定区间的辅助方法
    private void reverse(int[] nums, int left, int right) {
        while (left < right) {
            int temp = nums[left];
            nums[left] = nums[right];
            nums[right] = temp;
            left++;
            right--;
        }
    }
}

复杂度分析

  • 时间复杂度:O(n),每个元素最多翻转两次
  • 空间复杂度:O(1),原地操作

一句话总结

三次翻转魔术:先打乱再局部还原,就能把末尾 k 个元素神奇地搬到前面来。

4. 除自身以外数组的乘积

题目

给你一个数组,返回一个新数组,其中每个位置是除自身以外所有元素的乘积。

  • 要求:不能用除法
  • 示例:[1,2,3,4][24,12,8,6](分别是 2×3×4, 1×3×4, 1×2×4, 1×2×3)

思路过程

暴力解法: 对每个元素,遍历其他所有元素相乘。O(n²),太慢。

更好的思路:前后缀分解

对于位置 i,答案 = (i 左边所有元素的乘积) × (i 右边所有元素的乘积)

举例:[1, 2, 3, 4]

位置 0:  左边乘积=1(无)   右边乘积=2×3×4=2424
位置 1:  左边乘积=1       右边乘积=3×4=1212
位置 2:  左边乘积=1×2=2   右边乘积=4=48
位置 3:  左边乘积=1×2×3=6 右边乘积=1(无)     → 6

空间优化:不用额外数组

结果数组先存前缀积,再反向遍历乘上后缀积。

int[] result = new int[n];

// 第一遍:计算前缀积(左边所有元素的乘积)
int left = 1;
for (int i = 0; i < n; i++) {
    result[i] = left;    // 先存,乘积不包括当前元素
    left *= nums[i];     // 更新,为下一个元素准备
}

// 第二遍:反向遍历,乘上后缀积(右边所有元素的乘积)
int right = 1;
for (int i = n - 1; i >= 0; i--) {
    result[i] *= right;  // 左边乘积 × 右边乘积 = 最终答案
    right *= nums[i];    // 更新,为上一个元素准备
}

代码实现

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] result = new int[n];
        
        // ========== 第一步:计算每个位置左边的乘积 ==========
        int left = 1;
        for (int i = 0; i < n; i++) {
            // result[i] 先存左边乘积(不包含自己)
            result[i] = left;
            // 更新 left,为下一个元素准备
            left *= nums[i];
        }
        
        // ========== 第二步:反向遍历,乘上右边的乘积 ==========
        int right = 1;
        for (int i = n - 1; i >= 0; i--) {
            // 左边乘积 × 右边乘积 = 最终答案
            result[i] *= right;
            // 更新 right,为上一个元素准备
            right *= nums[i];
        }
        
        return result;
    }
}

复杂度分析

  • 时间复杂度:O(n),遍历两遍
  • 空间复杂度:O(1)(结果数组不计入额外空间)

一句话总结

把"除自身外的乘积"拆成"左边乘积 × 右边乘积",两次遍历就能搞定。

5. 缺失的第一个正数

题目

给你一个未排序的整数数组,找出缺失的最小正整数

  • 示例:[3,4,-1,1]2(1 在数组中,但 2 缺失)
  • 示例:[7,8,9,11,12]1(1 不在数组中)

思路过程

关键洞察: 对于长度为 n 的数组,缺失的第一个正数一定在 [1, n+1] 范围内。

  • 如果 1~n 都在数组中,答案就是 n+1
  • 如果有缺失,答案在 1~n 之间

原地哈希:把数组当成哈希表用!

核心思想:如果数字 x 出现,就把它放到下标 x-1 的位置(前提是 x 在 1~n 范围内)。

for (int i = 0; i < n; i++) {
    // 如果 nums[i][1, n] 范围内
    // 就把它放到正确位置:交换到 nums[nums[i]-1] 处
    while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
        swap(nums, i, nums[i] - 1);
    }
}

// 最后再遍历一遍,第一个 nums[i] != i+1 的位置就是答案
for (int i = 0; i < n; i++) {
    if (nums[i] != i + 1) {
        return i + 1;
    }
}
return n + 1;  // 1~n 都存在

代码实现

class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        
        // 核心思想:把数字 x 放到下标 x-1 的位置
        for (int i = 0; i < n; i++) {
            // 不断交换,直到当前数字在正确位置,或者超出范围
            while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i] - 1]
                int correctPos = nums[i] - 1;
                int temp = nums[i];
                nums[i] = nums[correctPos];
                nums[correctPos] = temp;
            }
        }
        
        // 再次遍历,找到第一个不匹配的位置
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;  // 缺失的第一个正数
            }
        }
        
        // 如果 1~n 都存在,答案就是 n+1
        return n + 1;
    }
}

复杂度分析

  • 时间复杂度:O(n),每个数字最多交换两次
  • 空间复杂度:O(1),原地操作

一句话总结

用原地哈希把数字放到"本该属于它的位置",最后扫一遍看谁"不在家"——那个空位+1就是答案。

6. 矩阵置零

题目

如果矩阵中某个元素为 0,则把该元素所在行和列全部置为 0。要求原地修改。

思路过程

朴素想法:用两个数组标记

boolean[] rows = new boolean[m];
boolean[] cols = new boolean[n];
// 遍历找到所有 0,标记对应行和列
// 再遍历置零

问题:空间复杂度 O(m+n),能不能更省?

原地标记:用矩阵本身记录!

用第一行和第一列作为"标记位":

  • matrix[i][0] = 0 表示第 i 行需要置零
  • matrix[0][j] = 0 表示第 j 列需要置零

注意事项: 第一行和第一列既是"标记位"也是"被标记对象",所以要先单独记录它们原本是否含 0。

// 1. 先记录第一行和第一列原本是否含 0
boolean row0HasZero = false, col0HasZero = false;
for (int j = 0; j < n; j++) if (matrix[0][j] == 0) row0HasZero = true;
for (int i = 0; i < m; i++) if (matrix[i][0] == 0) col0HasZero = true;

// 2. 用第一行和第一列标记其他位置
for (int i = 1; i < m; i++) {
    for (int j = 1; j < n; j++) {
        if (matrix[i][j] == 0) {
            matrix[i][0] = 0;   // 标记行
            matrix[0][j] = 0;   // 标记列
        }
    }
}

// 3. 根据标记置零(跳过第一行和第一列)
for (int i = 1; i < m; i++) {
    for (int j = 1; j < n; j++) {
        if (matrix[i][0] == 0 || matrix[0][j] == 0) {
            matrix[i][j] = 0;
        }
    }
}

// 4. 最后处理第一行和第一列
if (row0HasZero) Arrays.fill(matrix[0], 0);
if (col0HasZero) {
    for (int i = 0; i < m; i++) matrix[i][0] = 0;
}

代码实现

class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        
        // 记录第一行和第一列原本是否含 0
        boolean firstRowZero = false;
        boolean firstColZero = false;
        
        // 检查第一行
        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) {
                firstRowZero = true;
                break;
            }
        }
        
        // 检查第一列
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                firstColZero = true;
                break;
            }
        }
        
        // 用第一行和第一列标记其他需要置零的位置
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] == 0) {
                    matrix[i][0] = 0;    // 标记第 i 行
                    matrix[0][j] = 0;    // 标记第 j 列
                }
            }
        }
        
        // 根据标记置零(从 1 开始,避免覆盖第一行/列的标记)
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {
                    matrix[i][j] = 0;
                }
            }
        }
        
        // 处理第一行和第一列
        if (firstRowZero) {
            for (int j = 0; j < n; j++) matrix[0][j] = 0;
        }
        if (firstColZero) {
            for (int i = 0; i < m; i++) matrix[i][0] = 0;
        }
    }
}

复杂度分析

  • 时间复杂度:O(m×n),多次遍历但总共还是线性
  • 空间复杂度:O(1),只用了几个布尔变量

一句话总结

用矩阵的"边边"当记事本,把需要置零的行和列先记下来,最后再统一处理——聪明地复用空间。

7. 螺旋矩阵

题目

给你一个 m×n 的矩阵,按顺时针螺旋顺序返回所有元素。

  • 示例:[[1,2,3],[4,5,6],[7,8,9]][1,2,3,6,9,8,7,4,5]

思路过程

模拟遍历:设定边界,逐层收缩!

想象成"剥洋葱",用四个边界指针:

  • topbottom:上下边界
  • leftright:左右边界

每剥一圈,按"右→下→左→上"的顺序遍历一圈,然后边界向内收缩。

遍历顺序:
→→→→
←   ↓
↑ ← ←
(每步后边界收缩)

终止条件:边界相遇

while (left <= right && top <= bottom) {
    // 1. 从左到右遍历上边
    for (int j = left; j <= right; j++) res.add(matrix[top][j]);
    top++;
    
    // 2. 从上到下遍历右边
    for (int i = top; i <= bottom; i++) res.add(matrix[i][right]);
    right--;
    
    // 3. 从右到左遍历下边(要检查是否还有)
    if (top <= bottom) {
        for (int j = right; j >= left; j--) res.add(matrix[bottom][j]);
        bottom--;
    }
    
    // 4. 从下到上遍历左边(要检查是否还有)
    if (left <= right) {
        for (int i = bottom; i >= top; i--) res.add(matrix[i][left]);
        left++;
    }
}

代码实现

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> result = new ArrayList<>();
        if (matrix == null || matrix.length == 0) {
            return result;
        }
        
        int m = matrix.length;      // 行数
        int n = matrix[0].length; // 列数
        
        // 四个边界指针
        int top = 0, bottom = m - 1;
        int left = 0, right = n - 1;
        
        while (left <= right && top <= bottom) {
            // 1. 从左到右,遍历上边
            for (int col = left; col <= right; col++) {
                result.add(matrix[top][col]);
            }
            top++; // 上边界向下收缩
            
            // 2. 从上到下,遍历右边
            for (int row = top; row <= bottom; row++) {
                result.add(matrix[row][right]);
            }
            right--; // 右边界向左收缩
            
            // 3. 从右到左,遍历下边(需要确认还有剩余)
            if (top <= bottom) {
                for (int col = right; col >= left; col--) {
                    result.add(matrix[bottom][col]);
                }
                bottom--; // 下边界向上收缩
            }
            
            // 4. 从下到上,遍历左边(需要确认还有剩余)
            if (left <= right) {
                for (int row = bottom; row >= top; row--) {
                    result.add(matrix[row][left]);
                }
                left++; // 左边界向右收缩
            }
        }
        
        return result;
    }
}

复杂度分析

  • 时间复杂度:O(m×n),每个元素访问一次
  • 空间复杂度:O(1),除了输出数组

一句话总结

用四个边界指针"框住"当前层,顺时针绕一圈后边界收缩,层层剥洋葱直到全部遍历完。

8. 旋转图像

题目

把 n×n 矩阵顺时针旋转 90 度,要求原地修改。

  • 示例:[[1,2,3],[4,5,6],[7,8,9]][[7,4,1],[8,5,2],[9,6,3]]

思路过程

两步走:转置 + 翻转

观察旋转前后的对应关系:

旋转前:          旋转后:
1 2 3           7 4 1
4 5 68 5 2
7 8 9           9 6 3

可以发现:
- 原位置 (i,j) 旋转后到了 (j, n-1-i)
- 等价于:先转置(关于主对角线对称交换),再水平翻转

第一步:转置

1 2 3          1 4 7
4 5 62 5 8   (matrix[i][j] 和 matrix[j][i] 交换)
7 8 9          3 6 9

第二步:水平翻转

1 4 7          7 4 1
2 5 88 5 2   (每行首尾交换)
3 6 9          9 6 3

代码实现

class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        
        // ========== 第一步:转置(关于主对角线镜像)==========
        // 只需要遍历上三角(不包括对角线)
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                // 交换 matrix[i][j] 和 matrix[j][i]
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }
        
        // ========== 第二步:水平翻转(每行首尾交换)==========
        for (int i = 0; i < n; i++) {
            int left = 0, right = n - 1;
            while (left < right) {
                int temp = matrix[i][left];
                matrix[i][left] = matrix[i][right];
                matrix[i][right] = temp;
                left++;
                right--;
            }
        }
    }
}

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)

一句话总结

旋转 90° = 转置(对角线镜像)+ 水平翻转(左右交换),两步搞定!

9. 搜索二维矩阵 II

题目

在一个行列分别升序的矩阵中搜索目标值。

  • 示例:
[
  [1,  4,  7, 11, 15],
  [2,  5,  8, 12, 19],
  [3,  6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
  • 找 5 → true,找 20 → false

思路过程

关键观察:四个角的特点

位置特点能否作为起点
左上角行最小、列最小❌ 无法排除
右下角行最大、列最大❌ 无法排除
右上角行最大、列最小✅ 可以排除
左下角行最小、列最大✅ 可以排除

从右上角出发:

右上角元素是当前行的最大值、当前列的最小值:

  • 如果 当前值 == target → 找到!
  • 如果 当前值 > target → 这一列都不行,排除列,向左
  • 如果 当前值 < target → 这一行都不行,排除行,向下

每次比较都能排除一整行或一整列!

int row = 0;           // 从第一行开始
int col = n - 1;       // 从最后一列开始(右上角)

while (row < m && col >= 0) {
    if (matrix[row][col] == target) return true;
    if (matrix[row][col] > target) {
        col--;  // 当前值太大,向左移动(排除这一列)
    } else {
        row++;  // 当前值太小,向下移动(排除这一一行)
    }
}
return false;

代码实现

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0) {
            return false;
        }
        
        int m = matrix.length;      // 行数
        int n = matrix[0].length;   // 列数
        
        // 从右上角开始搜索
        int row = 0;
        int col = n - 1;
        
        while (row < m && col >= 0) {
            int current = matrix[row][col];
            
            if (current == target) {
                // 找到了!
                return true;
            } else if (current > target) {
                // 当前值大于目标值,说明这一列都不行(因为列是升序的)
                col--;  // 向左移动
            } else {
                // 当前值小于目标值,说明这一行都不行(因为行是升序的)
                row++;  // 向下移动
            }
        }
        
        return false;
    }
}

复杂度分析

  • 时间复杂度:O(m + n),最多走完一行 + 一列
  • 空间复杂度:O(1)

一句话总结

右上角是"十字路口":比它大就向左走(排除一列),比它小就向下走(排除一行),每步都能排除一片区域。

10. 回文子串

题目

给定一个字符串,返回其中回文子串的个数。

  • 示例:"aaa"6(分别是 a, a, a, aa, aa, aaa)

思路过程

中心扩展法:枚举每个可能的回文中心!

回文串的特点是"两边对称",所以可以从中心向两边扩展。

中心有两种情况:

  1. 单中心:aba,中心是 'b'
  2. 双中心:aa,中心是两个 'a' 之间
枚举中心:
  a   b   a
  ↑   ↑   ↑
  |   中心   |
单中心(1个字符)  双中心(2个字符)
int count = 0;
for (int i = 0; i < s.length(); i++) {
    // 1. 以单字符为中心向两边扩展
    count += expandAroundCenter(s, i, i);
    
    // 2. 以双字符为中心向两边扩展
    count += expandAroundCenter(s, i, i + 1);
}

// 辅助函数:从中心向两边扩展
int expandAroundCenter(String s, int left, int right) {
    int count = 0;
    while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
        count++;      //找到一个回文串
        left--;        //向左扩展
        right++;       //向右扩展
    }
    return count;
}

为什么这样不重不漏?

  • 每个回文串都有唯一的"中心"
  • 单中心回文串对应奇数长度
  • 双中心回文串对应偶数长度
  • 枚举所有中心,就能找到所有回文串

代码实现

class Solution {
    public int countSubstrings(String s) {
        int count = 0;
        int n = s.length();
        
        // 枚举每个位置作为回文中心
        for (int i = 0; i < n; i++) {
            // 情况1:以单字符为中心(奇数长度回文)
            count += expandAroundCenter(s, i, i);
            
            // 情况2:以双字符为中心(偶数长度回文)
            count += expandAroundCenter(s, i, i + 1);
        }
        
        return count;
    }
    
    // 从中心向两边扩展,统计以 (left, right) 为中心的所有回文串
    private int expandAroundCenter(String s, int left, int right) {
        int count = 0;
        
        // 满足条件:两边没越界,且字符相等(是回文)
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            count++;    // 找到一个回文串,计数 +1
            left--;     // 向左扩展
            right++;    // 向右扩展
        }
        
        return count;
    }
}

复杂度分析

  • 时间复杂度:O(n²),每个中心最多扩展 O(n)
  • 空间复杂度:O(1)

一句话总结

每个回文串都有一个"对称中心",枚举所有可能的中心(单个字符或两个字符之间),向两边扩展找所有回文。

总结

今天这些题覆盖了数组和矩阵的经典技巧:

题号题目核心技巧
53最大子数组和Kadane 算法:负数断舍离
56合并区间先排序,再贪心合并
189轮转数组三次翻转魔术
238除自身乘积前后缀分解
41缺失的第一个正数原地哈希
73矩阵置零用边边做标记
54螺旋矩阵边界收缩模拟
48旋转图像转置+翻转
240搜索二维矩阵从右上角出发
647回文子串中心扩展法

下期预告:链表专题!跟着我一起刷算法题,咱们下期见!

如果觉得有用,点个赞再走呗~ 有问题评论区见!