🎶二分 · 双指针 · 滑动窗口 · 螺旋矩阵:数组算法四题拆解

数组算法四题精讲

二分查找 · 双指针 · 滑动窗口 · 螺旋矩阵 LeetCode 704 · 27 · 977 · 209 · 59


01 二分查找(LeetCode 704)

给定一个升序排列的有序数组和目标值 target,返回目标值的下标,找不到返回 -1。

暴力解法从头扫到尾,时间复杂度 O(n)。二分查找利用数组有序的特性,每次把搜索范围缩小一半,效率远高于暴力。

核心思路

用左右两个指针圈定搜索范围,每次取中间值和 target 比较,根据大小决定往左半区还是右半区继续找。

ini 复制代码
nums = [-1, 0, 3, 5, 9, 12]   target = 9

left=0                 right=5
  ↓                      ↓
[-1,  0,  3,  5,  9,  12]
           ↑
         mid=2   nums[2]=3 < 9  → 往右找

              left=3    right=5
                ↓         ↓
[              5,   9,   12]
                    ↑
                  mid=4  nums[4]=9 == 9  → 找到!返回4

边界条件:left < right 还是 left <= right?

这是二分查找最容易搞混的地方,取决于你定义的区间是左闭右开 还是左闭右闭

左闭右开 [left, right) 的规则:

  • right = nums.length(不是 length-1),因为右边是开区间取不到
  • while(left < right),left == right 时区间为空,退出
  • nums[mid] > target 时,right = mid(不是 mid-1,因为右边取不到)
java 复制代码
int left = 0;
int right = nums.length;          // 左闭右开,right不包含

while (left < right) {            // 相等时区间为空,退出
    int mid = (right + left) / 2;
    if (nums[mid] == target) {
        return mid;
    } else if (nums[mid] < target) {
        left = mid + 1;           // mid已判断过,左边从mid+1开始
    } else {
        right = mid;              // 右开区间,right=mid即排除mid
    }
}
return -1;

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


02 移除元素(LeetCode 27)

给一个数组和值 val,原地移除所有等于 val 的元素,返回剩余元素个数。不能开新数组,要在原数组上操作。

核心思路:快慢双指针

用两个指针 fast 和 slow,fast 负责遍历每个元素,slow 负责指向下一个有效位置。

  • 遇到不等于 val 的元素就写入 slow 位置,slow 前进
  • 遇到等于 val 的直接跳过,slow 不动
ini 复制代码
nums = [3, 2, 2, 3]   val = 3

fast=0  slow=0
  3 == val → 跳过,slow不动,fast++

fast=1  slow=0
  2 != val → nums[slow]=2,slow++,fast++

fast=2  slow=1
  2 != val → nums[slow]=2,slow++,fast++

fast=3  slow=2
  3 == val → 跳过

结果:nums = [2, 2, ...]  返回 slow=2
javascript 复制代码
let slow = 0
for (let fast = 0; fast < nums.length; fast++) {
    if (nums[fast] != val) {       // fast找到有效元素
        nums[slow] = nums[fast]    // 写入slow位置
        slow++                     // slow往前走
    }
    // fast不管怎样都会++(for循环自动)
}
return slow                        // slow就是有效元素个数

关键点:

  • fast 每次都走,slow 只有遇到有效元素才走
  • slow 同时也是有效元素的计数器,最终返回 slow 即可
  • 原地操作,没有开新数组,空间复杂度 O(1)

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


03 有序数组的平方(LeetCode 977)

给一个按非递减顺序排列的整数数组(包含负数),返回每个数字平方后按非递减顺序排列的新数组。

关键点:数组有负数,负数平方后可能很大。最大的平方一定在数组两端(最左或最右),因此用双指针从两端往中间收。

核心思路:对撞双指针 + 从后往前填

用 i 指向左端,j 指向右端,k 指向结果数组的末尾。每次比较两端的平方,把较大的放入 resultk,然后 k--,对应指针也收缩。

ini 复制代码
nums = [-4, -1, 0, 3, 10]

i=0  j=4  k=4
(-4)²=16   (10)²=100  → 100更大,result[4]=100,j--,k--

i=0  j=3  k=3
(-4)²=16   (3)²=9     → 16更大,result[3]=16,i++,k--

i=1  j=3  k=2
(-1)²=1    (3)²=9     → 9更大,result[2]=9,j--,k--

i=1  j=2  k=1
(-1)²=1    (0)²=0     → 1更大,result[1]=1,i++,k--

i=2  j=2  k=0
(0)²=0                → result[0]=0

结果:[0, 1, 9, 16, 100] ✅
javascript 复制代码
const result = []
let k = nums.length - 1           // 结果数组从末尾开始填

for (let i = 0, j = nums.length - 1; i <= j;) {
    if (nums[i] * nums[i] < nums[j] * nums[j]) {
        result[k] = nums[j] * nums[j]   // 右边更大
        j--
        k--
    } else {
        result[k] = nums[i] * nums[i]   // 左边更大或相等
        i++
        k--
    }
}
return result

时间复杂度:O(n)  空间复杂度:O(n)


04 长度最小的子数组(LeetCode 209)

给定数组和目标值 target,找出总和 ≥ target 的最小连续子数组,返回其长度,不存在则返回 0。

核心思路:滑动窗口

用左右两个指针维护一个窗口:

  • right 不断往右扩张,把新元素加进窗口
  • 当窗口内的和 ≥ target 时,记录当前长度,然后 left 往右收缩,尝试找更小的满足条件的窗口
ini 复制代码
nums = [2, 3, 1, 2, 4, 3]   target = 7

right扩张:2→5→6→8 ≥ 7
窗口 [2,3,1,2]  长度4  记录minLen=4

left收缩:sum-=2=6 < 7,继续扩张

right扩张:6+4=10 ≥ 7
left收缩:sum-=3=7 ≥ 7,窗口[1,2,4]  长度3  更新minLen=3
left收缩:sum-=1=6 < 7,停止收缩

right扩张:6+3=9 ≥ 7
left收缩:sum-=2=7 ≥ 7,窗口[4,3]  长度2  更新minLen=2
left收缩:sum-=4=3 < 7,停止

最终 minLen = 2
javascript 复制代码
let left = 0
let sum = 0
let minLen = 10000000              // 初始设一个极大值

for (let right = 0; right < nums.length; right++) {
    sum += nums[right]             // 扩张右边界

    while (sum >= target) {        // 满足条件就收缩左边界
        minLen = Math.min(minLen, right - left + 1)
        sum -= nums[left]          // 移出左边元素
        left++
    }
}

return minLen === 10000000 ? 0 : minLen   // 没找到返回0

关键点:

  • right 用 for 循环控制,left 用 while 控制,两者不对称
  • 用 while 而不是 if:满足条件要持续收缩,直到不满足为止
  • 每次收缩都记录一次长度,保证不遗漏更小的窗口

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


05 螺旋矩阵 II(LeetCode 59)

给定 n,生成一个 n×n 的矩阵,按螺旋顺序填入 1 到 n²。

这道题没有特别的算法,考的是边界控制。关键是坚持"左闭右开"原则,每条边不走最后一个格,留给下一条边的起点,保持一致性。

核心思路

每一圈分四条边:上、右、下、左。用 startx、starty 记录当前圈的起点,end 控制每条边走多远,每走完一圈,起点内移、范围缩小、圈数减少。

ini 复制代码
n=4 的螺旋顺序(每条边左闭右开):

 →  →  →  ↓
 1   2   3   4
↑           ↓
12  13  14   5
↑           ↓
11  16  15   6
↑   ←   ←   ↓
10   9   8   7

startx=starty=0,end=1,每圈后 start++,end++,test--
javascript 复制代码
let startx = 0, starty = 0       // 每圈起点
let end = 1                      // 每条边结束位置偏移
let count = 1                    // 填入的数字
let test = Math.floor(n / 2)     // 需要转几圈

while (test) {
    // 上边:从左往右(列变,行固定为startx)
    for (j = starty; j < n - end; j++) nums[startx][j] = count++
    // 右边:从上往下(行变,j停在n-end)
    for (i = startx; i < n - end; i++) nums[i][j] = count++
    // 下边:从右往左(列变,i停在n-end)
    for (;           j > starty; j--) nums[i][j] = count++
    // 左边:从下往上(行变,j停在starty)
    for (;           i > startx; i--) nums[i][j] = count++

    startx++; starty++            // 下一圈起点内移
    end++                         // 每条边走的范围缩小
    test--                        // 圈数减一
}

// n为奇数时,中心格单独处理
if (n % 2 != 0) nums[Math.floor(n/2)][Math.floor(n/2)] = n * n

关键点:

  • 坚持左闭右开:每条边不走最后一个格,留给下一条边
  • 四条边共用 i 和 j,上一条边结束时 i/j 的位置,正好是下一条边的起点
  • n 为奇数时中心格永远走不到,要单独填入 n²
  • 转几圈 = Math.floor(n/2),n=4 转2圈,n=5 转2圈+中心

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


总结:解题方法对照

题目 方法 核心思路
二分查找 二分查找 有序数组每次砍掉一半范围
移除元素 快慢双指针 fast找有效元素,slow负责写入
有序数组的平方 对撞双指针 两端往中间收,从后往前填
长度最小的子数组 滑动窗口 right扩张,满足条件left收缩
螺旋矩阵 II 模拟 坚持左闭右开,边界统一不出错
相关推荐
海清河晏1111 小时前
字符串匹配:BF算法与KMP算法
数据结构·算法·visual studio
wandertp1 小时前
对信号处理及滤波器的理解---基于robomaster机器人嵌入式控制系统
arm开发·stm32·算法·信号处理
z小猫不吃鱼1 小时前
15 InstructGPT 论文精读:SFT + RLHF 如何让模型听懂指令?
人工智能·深度学习·算法·机器学习·语言模型·自然语言处理·gpt-3
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
见合八方2 小时前
【滤波器】热调谐FP滤波器
人工智能·算法
古城小栈2 小时前
cargo-pprof:Rust性能调优
人工智能·算法·rust
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
零陵上将军_xdr2 小时前
后端转全栈学习-Day3-JavaScript 基础-1
开发语言·javascript·学习
GISHUB2 小时前
Express + TypeScript + ESM 后端服务搭建教程
javascript·typescript·express