代码随想录:数组篇

P1: 704. 二分查找

题目核心要求

升序、无重复 的整型数组 nums 中查找目标值 target

  • 存在则返回对应下标;
  • 不存在返回 -1。

示例

  • 输入:nums = [-1,0,3,5,9,12], target = 9 → 输出:4
  • 输入:nums = [-1,0,3,5,9,12], target = 2 → 输出:-1

核心思路:循环不变量(区间定义)

二分查找的核心是明确区间定义(循环不变量),所有边界处理必须贴合该定义。主流两种区间定义对应两种写法,时间复杂度均为 O(log n),空间复杂度 O(1)。


写法1:左闭右闭 [left, right](C++ 版本)

核心规则

  1. 循环条件:while (left <= right)left == right 时区间仍有效,包含1个元素);
  2. 边界更新:
    • nums[mid] > targetright = mid - 1(排除已验证的 mid);
    • nums[mid] < targetleft = mid + 1(排除已验证的 mid)。

完整代码

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 左闭右闭,right指向最后一个元素
        
        while (left <= right) {
            // 防溢出:等价于 (left+right)/2,避免大数相加越界
            int mid = left + (right - left) / 2;
            
            if (nums[mid] > target) {
                right = mid - 1;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid; // 找到目标,返回下标
            }
        }
        return -1; // 未找到目标
    }
};

写法2:左闭右开 [left, right)(C++ 版本)

核心规则

  1. 循环条件:while (left < right)left == right 时区间为空,无效);
  2. 边界更新:
    • nums[mid] > targetright = mid(右区间开,天然排除 mid);
    • nums[mid] < targetleft = mid + 1(左区间闭,排除 mid)。

完整代码

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 左闭右开,right指向数组长度(越界位置)
        
        while (left < right) {
            int mid = left + (right - left) / 2;
            
            if (nums[mid] > target) {
                right = mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
};

关键注意点

  1. mid 计算 :优先用 left + (right - left)/2,避免 (left+right)/2 导致整数溢出;
  2. 循环条件 :左闭右闭用 <=,左闭右开用 <,需严格贴合区间定义;
  3. 边界更新 :左闭右闭需 mid±1,左闭右开仅右边界更新为 mid

P2: 27. 移除元素

题目核心要求

原地 移除数组 nums 中所有值等于 val 的元素,返回移除后数组的新长度:

  • 仅使用 O(1) 额外空间,直接修改输入数组;
  • 元素顺序可改变,无需考虑新长度后的数组元素。

示例

  • 输入:nums = [3,2,2,3], val = 3 → 输出:2(前2个元素为 [2,2]);
  • 输入:nums = [0,1,2,2,3,0,4,2], val = 2 → 输出:5(前5个元素为 [0,1,3,0,4])。

核心解法

数组元素在内存中连续存储,无法直接删除,只能通过覆盖实现"移除",以下是3种核心解法(优先掌握双指针法)。


解法1:暴力解法(效率低,仅作理解)

核心思路

两层循环:外层遍历数组,找到等于 val 的元素;内层将该元素后方所有元素向前移动一位,覆盖目标值。

  • 时间复杂度:O(n²),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int size = nums.size();
        for (int i = 0; i < size; i++) {
            if (nums[i] == val) { // 找到目标值,后续元素前移
                for (int j = i + 1; j < size; j++) {
                    nums[j - 1] = nums[j];
                }
                i--;  // 前移后当前下标需回退,避免漏检
                size--; // 数组有效长度减1
            }
        }
        return size;
    }
};

解法2:快慢双指针法(最优,推荐)

核心思路

单循环完成两层循环的工作,定义两个指针:

  • 快指针:遍历数组,寻找非 val 的元素(新数组的有效元素);
  • 慢指针:指向新数组的下标,接收快指针找到的有效元素。
  • 时间复杂度:O(n),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow = 0; // 慢指针:新数组下标
        for (int fast = 0; fast < nums.size(); fast++) { // 快指针:遍历原数组
            if (nums[fast] != val) { // 找到有效元素,覆盖慢指针位置
                nums[slow++] = nums[fast];
            }
        }
        return slow; // 慢指针最终值 = 新数组长度
    }
};

总结

  1. 数组"移除"元素的本质是覆盖,而非真正删除;
  2. 快慢双指针法是最优解(O(n) 时间,O(1) 空间),需熟练掌握;
  3. 暴力解法仅作理解,实际开发中优先使用双指针法。

P3: 977. 有序数组的平方

题目核心要求

给一个非递减排序 的整数数组 nums,返回由每个数字的平方组成的新数组,要求新数组也按非递减顺序排序。

示例

  • 输入:nums = [-4,-1,0,3,10] → 输出:[0,1,9,16,100]
  • 输入:nums = [-7,-3,2,3,11] → 输出:[4,9,9,49,121]

核心解法

解法1:暴力排序法(基础思路,效率较低)

核心思路
  1. 遍历数组,将每个元素替换为自身的平方;
  2. 调用排序函数对平方后的数组排序。
  • 时间复杂度:O(n + nlogn)(遍历O(n) + 排序O(nlogn)),空间复杂度:O(1)(原地修改)。
C++ 代码
cpp 复制代码
#include <vector>
#include <algorithm> // sort函数头文件
using namespace std;

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        // 1. 每个元素平方
        for (int i = 0; i < nums.size(); i++) {
            nums[i] *= nums[i];
        }
        // 2. 排序
        sort(nums.begin(), nums.end());
        return nums;
    }
};

解法2:双指针法(最优,推荐)

核心思路

数组本身有序,负数平方后可能成为最大值,因此平方后的最大值一定出现在数组两端(最左/最右)。利用双指针从两端向中间遍历,新数组从后往前填充最大值:

  • 左指针 i:指向数组起始位置;
  • 右指针 j:指向数组末尾位置;
  • 结果数组指针 k:从末尾开始,依次存放当前最大的平方值。
  • 时间复杂度:O(n)(仅一次遍历),空间复杂度:O(n)(需要额外存储结果数组)。
C++ 代码
cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int n = nums.size();
        vector<int> result(n, 0); // 初始化等长结果数组
        int i = 0, j = n - 1, k = n - 1; // i左指针,j右指针,k结果数组指针
        
        while (i <= j) { // 注意i<=j,需处理最后一个元素
            int leftSquare = nums[i] * nums[i];
            int rightSquare = nums[j] * nums[j];
            
            if (leftSquare > rightSquare) { // 左指针平方更大,存入结果数组
                result[k--] = leftSquare;
                i++;
            } else { // 右指针平方更大,存入结果数组
                result[k--] = rightSquare;
                j--;
            }
        }
        return result;
    }
};

总结

  1. 暴力法思路简单但效率低,仅适合理解基础逻辑;
  2. 双指针法利用数组"有序"的特性,将时间复杂度优化至O(n),是最优解;
  3. 双指针法的核心逻辑:平方后的最大值必在数组两端,从后往前填充结果数组。

209. 长度最小的子数组

题目核心要求

给定由正整数组成的数组 nums 和正整数 target,找出和 ≥ target最短连续子数组,返回其长度;若无符合条件的子数组,返回 0。

示例

  • 输入:target = 7, nums = [2,3,1,2,4,3] → 输出:2(最短子数组为 [4,3])。

核心解法

解法1:暴力解法(超时,仅作理解)

核心思路

两层循环枚举所有连续子数组:

  1. 外层循环固定子数组起始位置 i
  2. 内层循环从 i 开始累加,直到和 ≥ target,记录当前子数组长度。
  • 时间复杂度:O(n²)(数据量较大时超时),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
#include <vector>
#include <climits> // INT32_MAX 头文件
using namespace std;

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = INT32_MAX; // 初始化为极大值
        for (int i = 0; i < nums.size(); i++) { // 子数组起始位置
            int sum = 0;
            for (int j = i; j < nums.size(); j++) { // 子数组终止位置
                sum += nums[j];
                if (sum >= target) { // 找到符合条件的子数组
                    result = min(result, j - i + 1);
                    break; // 最短子数组,无需继续累加
                }
            }
        }
        return result == INT32_MAX ? 0 : result; // 无符合条件则返回0
    }
};

解法2:滑动窗口(最优,推荐)

核心思路

单循环+双指针实现"窗口"的滑动,将时间复杂度降至 O(n):

  1. 右指针 j:遍历数组,作为窗口终止位置,累加元素和;
  2. 左指针 i:窗口起始位置,当窗口和 ≥ target 时,右移左指针缩小窗口,同时更新最短长度。
  • 核心逻辑:窗口和满足条件时,尽可能缩小窗口以找到最短长度。
  • 时间复杂度:O(n)(每个元素最多被左右指针各访问一次),空间复杂度:O(1)。
C++ 代码
cpp 复制代码
#include <vector>
#include <climits>
using namespace std;

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = INT32_MAX;
        int sum = 0; // 窗口内元素和
        int i = 0; // 窗口起始位置
        for (int j = 0; j < nums.size(); j++) { // j为窗口终止位置
            sum += nums[j];
            // 窗口和满足条件,缩小窗口找最短长度
            while (sum >= target) {
                int subLen = j - i + 1; // 当前窗口长度
                result = min(result, subLen);
                sum -= nums[i++]; // 左指针右移,缩小窗口
            }
        }
        return result == INT32_MAX ? 0 : result;
    }
};

总结

  1. 暴力解法思路简单但效率低(O(n²)),无法通过大数据量测试;
  2. 滑动窗口是最优解(O(n)),核心是"右指针扩展窗口、左指针收缩窗口",通过单循环实现双指针移动;
  3. 滑动窗口的关键:窗口和满足条件时,尽可能缩小窗口以找到最短长度。

59. 螺旋矩阵II

题目核心要求

给定正整数 n,生成一个 n×n 的正方形矩阵,矩阵包含 1 到 的所有元素,且元素按顺时针螺旋顺序排列,返回该矩阵。

示例

  • 输入:3 → 输出:[[1,2,3],[8,9,4],[7,6,5]]

核心解法(模拟填充,循环不变量原则)

核心思路

本题无复杂算法,核心是模拟顺时针填充矩阵的过程,关键要遵守「左闭右开」的循环不变量原则,避免边界条件混乱:

  1. 按「上行从左到右 → 右列从上到下 → 下行从右到左 → 左列从下到上」的顺序,由外向内逐圈填充;
  2. 每圈填充时,每条边都遵循左闭右开(拐角处留给下一条边填充);
  3. n 为奇数,矩阵中心位置需单独赋值。
  • 时间复杂度:O(n²)(遍历填充整个矩阵),空间复杂度:O(1)(仅额外使用少量变量)。

C++ 代码

cpp 复制代码
#include <vector>
using namespace std;

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n, vector<int>(n, 0)); // 初始化n×n矩阵
        int startx = 0, starty = 0; // 每圈填充的起始位置
        int loop = n / 2; // 循环圈数(偶数n填n/2圈,奇数最后单独处理中心)
        int mid = n / 2; // 奇数n的中心位置
        int count = 1; // 填充的数值
        int offset = 1; // 控制每条边的填充长度(每圈收缩1位)
        int i, j;

        while (loop--) {
            i = startx;
            j = starty;

            // 1. 填充上行:从左到右(左闭右开)
            for (; j < n - offset; j++) res[i][j] = count++;
            // 2. 填充右列:从上到下(左闭右开)
            for (; i < n - offset; i++) res[i][j] = count++;
            // 3. 填充下行:从右到左(左闭右开)
            for (; j > starty; j--) res[i][j] = count++;
            // 4. 填充左列:从下到上(左闭右开)
            for (; i > startx; i--) res[i][j] = count++;

            // 下一圈起始位置内移1位
            startx++;
            starty++;
            // 下一圈每条边的填充长度收缩1位
            offset++;
        }

        // 奇数n,单独填充中心位置
        if (n % 2) res[mid][mid] = count;
        return res;
    }
};

总结

  1. 解题核心是遵守「左闭右开」的循环不变量原则,统一边界处理规则,避免逻辑混乱;
  2. 按「上→右→下→左」的顺序逐圈填充,每圈结束后起始位置内移、填充长度收缩;
  3. 奇数阶矩阵需单独处理中心位置,偶数阶矩阵无需额外操作。
相关推荐
uesowys2 小时前
Apache Spark算法开发指导-K-means
算法·spark·kmeans
alanesnape2 小时前
Valgrind 测试详解--检测内存泄漏的好工具
c语言·c++·算法
你怎么知道我是队长2 小时前
C语言---排序算法10---基数排序法
算法
YGGP2 小时前
【Golang】LeetCode 56. 合并区间
算法·leetcode·职场和发展
_F_y2 小时前
回文串系列动态规划附马拉车算法原理及实现
算法·动态规划
你怎么知道我是队长2 小时前
C语言---排序算法12---计数排序法
c语言·算法·排序算法
fu的博客2 小时前
【数据结构2】带头结点·单向链表实现
数据结构·算法·链表
近津薪荼2 小时前
优选算法——前缀和(6):和可被 K 整除的子数组
c++·算法
lifallen2 小时前
线性基 (Linear Basis)
数据结构·算法