轮转数组:从暴力到最优,一题掌握算法复杂度分析

轮转数组:从暴力到最优,一题掌握算法复杂度分析

LeetCode 189. 轮转数组 ------ 一道题学会三种解法,理解时间与空间复杂度的取舍


题目描述

题目链接189. 轮转数组 - 力扣(LeetCode)

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 1:

复制代码
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:

复制代码
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

提示:

  • 1 <= nums.length <= 10^5
  • -2^31 <= numsi <= 2^31 - 1
  • 0 <= k <= 10^5

进阶要求:

  • 尽可能想出更多解决方案,至少有三种不同方法
  • 尝试使用空间复杂度为 O(1) 的原地算法

为什么这道题值得反复琢磨?

这道题是学习算法复杂度分析的绝佳案例。我们即将展示三种解法,它们的复杂度分别是:

解法 时间复杂度 空间复杂度 结果
暴力法 O(n²) O(1) ❌ 超时
辅助数组 O(n) O(n) ✅ 通过
三次逆置 O(n) O(1) ✅ 最优

通过这道题,你能直观感受到:同样的功能,不同的算法设计,性能天差地别。这也是为什么我们要学习数据结构和算法------写出能通过面试、能处理大规模数据的代码。


解法一:暴力法(O(n²) ------ 超时)

思路

最直观的想法:每次将最后一个元素保存,然后将所有元素向后移动一位,最后把保存的元素放到数组开头。重复这个过程 k 次。

代码实现

c 复制代码
void rotate(int* nums, int numsSize, int k) {
    k = k % numsSize;  // 处理 k 大于数组长度的情况
    while (k--) {
        int end = nums[numsSize - 1];  // 保存最后一个元素
        for (int i = numsSize - 1; i > 0; i--) {
            nums[i] = nums[i - 1];      // 所有元素后移一位
        }
        nums[0] = end;                  // 放到开头
    }
}

复杂度分析

  • 时间复杂度O(n²)。外层循环执行 k 次,内层循环每次移动 n-1 个元素。当 k 接近 n 时,执行次数约为 n(n-1)/2。
  • 空间复杂度O(1)。只使用了常数个额外变量。

结果

超出时间限制 ❌ 当 n = 10^5 时,O(n²) 意味着 10^10 次操作,必然超时。


解法二:辅助数组法(O(n) 空间)

思路

创建一个新数组,直接计算出每个元素轮转后的最终位置。元素 nums[i] 应该放到新数组的 (i + k) % n 位置。

复制代码
原数组: [1, 2, 3, 4, 5, 6, 7], k = 3

i=0 → (0+3)%7 = 3 → 1 放到新数组下标3
i=1 → (1+3)%7 = 4 → 2 放到新数组下标4
...
i=4 → (4+3)%7 = 0 → 5 放到新数组下标0
i=5 → (5+3)%7 = 1 → 6 放到新数组下标1
i=6 → (6+3)%7 = 2 → 7 放到新数组下标2

结果: [5, 6, 7, 1, 2, 3, 4] ✅

代码实现

c 复制代码
void rotate(int* nums, int numsSize, int k) {
    int newArr[numsSize];
    for (int i = 0; i < numsSize; i++) {
        newArr[(i + k) % numsSize] = nums[i];
    }
    for (int i = 0; i < numsSize; i++) {
        nums[i] = newArr[i];
    }
}

复杂度分析

  • 时间复杂度O(n)。遍历两次数组,每次 O(n)。
  • 空间复杂度O(n)。需要一个与原数组大小相同的新数组。

结果

通过 ✅ 但空间复杂度不满足进阶要求。


解法三:三次逆置法(最优解,O(1) 空间)

核心思想

利用数组反转的特性,用三次反转完成轮转,不借助额外数组

观察目标:将数组向右轮转 k 步,相当于把后 k 个元素移到前面,前 n-k 个元素移到后面

关键洞察:三次反转可以实现这个效果

  1. 反转整个数组 → 后 k 个元素到了前面(但顺序是反的)
  2. 反转前 k 个元素 → 恢复这部分的正确顺序
  3. 反转后 n-k 个元素 → 恢复这部分的正确顺序

图解过程

复制代码
原数组: [1, 2, 3, 4, 5, 6, 7], k = 3

第1步:反转整个数组
[1, 2, 3, 4, 5, 6, 7] → [7, 6, 5, 4, 3, 2, 1]
                             ↑ 后3个元素到了前面,但顺序反了

第2步:反转前 k=3 个元素
[7, 6, 5, 4, 3, 2, 1] → [5, 6, 7, 4, 3, 2, 1]
                             ↑ 前3个元素顺序恢复

第3步:反转后 n-k=4 个元素
[5, 6, 7, 4, 3, 2, 1] → [5, 6, 7, 1, 2, 3, 4] ✅
                              ↑ 后4个元素顺序恢复

最终结果: [5, 6, 7, 1, 2, 3, 4] ✅

代码实现

c 复制代码
// 反转数组中 [left, right] 区间的元素
void reverse(int* nums, int left, int right) {
    while (left < right) {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}

void rotate(int* nums, int numsSize, int k) {
    k = k % numsSize;  // 关键:处理 k 大于数组长度的情况
    reverse(nums, 0, numsSize - 1);        // 反转整个数组
    reverse(nums, 0, k - 1);               // 反转前 k 个
    reverse(nums, k, numsSize - 1);        // 反转后 n-k 个
}

复杂度分析

  • 时间复杂度O(n)。每个元素被反转两次,总计 O(2n) = O(n)。
  • 空间复杂度O(1)。只使用常数个变量,原地操作。

为什么 k = k % numsSize 是必要的?

轮转 n 次(n 为数组长度)数组恢复原样。所以实际有效轮转次数是 k % n。这个操作还能避免 numsSize - k - 1 出现负数下标。

数学正确性证明(选读)

向右轮转 k 个位置后,下标 i 的元素移动到下标 (i + k) % n。三次反转法的正确性可以分两类讨论:

  • i < n-k(原本在前半部分):第一次反转后移动到 n-1-i,第三次反转(范围 kn-1)后移动到 (n-1-i) - k = n-1-i-k?等等,这里推导比较复杂。直接记住结论:三次反转法已被证明能够正确地将每个元素移动到目标位置。

三种解法对比总结

解法 时间复杂度 空间复杂度 是否通过 适用场景
暴力法 O(n²) O(1) ❌ 超时 小数据量
辅助数组 O(n) O(n) 对空间无要求
三次逆置 O(n) O(1) ✅ 最优 面试要求原地操作

延伸思考:环状替换法

除了上述三种方法,还有环状替换法。它同样达到 O(n) 时间和 O(1) 空间,但实现更复杂。

核心思路:从位置 0 开始,把 nums[0] 放到 (0+k)%n,再把被覆盖的元素继续放到它的目标位置......直到回到起点。如果还有未访问元素,从下一个位置继续。

c 复制代码
void rotate(int* nums, int numsSize, int k) {
    k = k % numsSize;
    int count = gcd(k, numsSize);  // 需要遍历的环的数量
    for (int start = 0; start < count; start++) {
        int current = start;
        int prev = nums[start];
        do {
            int next = (current + k) % numsSize;
            int tmp = nums[next];
            nums[next] = prev;
            prev = tmp;
            current = next;
        } while (start != current);
    }
}

这种方法虽然也是 O(1) 空间,但需要理解环的个数与 gcd 的关系,面试中不如三次逆置法直观。


总结

这道题告诉我们:

  1. 暴力法不一定能用:O(n²) 在大数据量下必然超时
  2. 空间换时间:辅助数组法虽然通过,但 O(n) 空间可能不是最优
  3. 最优解往往需要巧思:三次逆置法通过反转操作实现了原地 O(n) 轮转

在面试中,从暴力法开始分析复杂度,然后逐步优化到三次逆置法,是展示你算法思维的最佳路径。


📌 如果觉得有用,欢迎点赞、收藏、转发!更多算法题解,持续更新中......

相关推荐
樱桃读报僵尸2 小时前
说说 LLMRouter,Agent 执行过程中怎么动态的选择 LLM
算法
gis开发之家2 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码
进击的丸子3 小时前
虹软人脸服务器SDK-C++语言Demo实操指南
后端·算法
兰令水3 小时前
hot100【acm版】【2026.7.25/26打卡-java版本】
java·算法·排序算法
音视频工程实战4 小时前
PromptQL 新手入门与实战指南
数据库·sql·算法
wabs6664 小时前
关于图论【卡码网104.建造最大岛屿的思考】
数据结构·算法·图论
玖玥拾4 小时前
LeetCode 27 移除元素
算法·leetcode
冻柠檬飞冰走茶5 小时前
PTA基础编程题目集 7-15 计算圆周率(C语言实现)
c语言·开发语言·数据结构·算法
问商十三载5 小时前
AI 引擎生成式优化两种思路怎么选?2026 对比分析附选型方法
人工智能·算法