从零开始写算法——普通数组类题:数组操作中的“翻转技巧”与“前后缀分解”

在算法面试中,数组(Array)类题目往往看似简单,实则暗藏玄机。暴力解法通常很容易想到,但要在 O(n) 时间复杂度和 O(1) 空间复杂度(或受限条件)下完成,就需要一些巧妙的数学逻辑。

今天整理了两道非常经典的数组题目:轮转数组除自身以外数组的乘积,重点分析它们背后的解题思路。

一、轮转数组 (Rotate Array)

1. 题目简述

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

2. 解题思路:三次反转法

如果使用额外的数组来暂存数据,空间复杂度是 O(n),但这题通常考察的是原地操作,O(1)空间复杂度。

核心技巧在于"翻转"。我们可以把数组想象成两部分:需要移动到前面的尾部元素 和 需要后移的头部元素。

通过整体和局部的三次翻转,刚好可以将位置对调。

步骤演示:

假设数组为 [1, 2, 3, 4, 5, 6, 7],k = 3。

  1. 整体翻转[7, 6, 5, 4, 3, 2, 1]

    • 此时,原本在尾部的 5, 6, 7 跑到了最前面,但顺序是反的。
  2. 翻转前 k 个[5, 6, 7, 4, 3, 2, 1]

    • k 个元素顺序恢复正常。
  3. 翻转剩余部分[5, 6, 7, 1, 2, 3, 4]

    • 后半部分元素顺序恢复正常。

    • 完成!

3. 代码实现 (C++)

C++代码:

cpp 复制代码
class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        // 核心思路:整体反转 + 两次局部反转
        // 坑点注意:k 可能大于数组长度,必须取模
        k %= nums.size();
        
        // 1. 翻转全部
        reverse(nums.begin(), nums.end());
        // 2. 翻转前 k 个
        reverse(nums.begin(), nums.begin() + k);
        // 3. 翻转剩余部分
        reverse(nums.begin() + k, nums.end());
    }
};

4. 复杂度分析

  • 时间复杂度 :O(n)。reverse 函数本质是遍历交换,三次操作依然是线性时间。(reverse底层是依靠双指针实现的复杂度为O(n))。

  • 空间复杂度:O(1)。没有使用额外的数组空间,仅用了常数个变量。


二、除自身以外数组的乘积 (Product of Array Except Self)

1. 题目简述

给你一个整数数组 nums,返回数组 ans,其中 ans[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

要求:不能使用除法,且在 O(n) 时间内完成。

2. 解题思路:前缀积与后缀积

如果能用除法,算出所有数的乘积再除以 nums[i] 即可(需考虑 0 的情况)。但题目禁止除法,我们就需要转换思路。

ans[i] 的值其实可以拆解为两部分:

我们可以利用空间换时间的思想,预处理出两个数组:

  • pre 数组pre[i] 表示 i 左边所有元素的乘积。

  • suf 数组suf[i] 表示 i 右边所有元素的乘积。

最后遍历一遍,将两者相乘即可。

3. 代码实现 (C++)

C++代码:

cpp 复制代码
class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> suf(n);
        vector<int> pre(n);
        vector<int> ans;
        
        // 初始化边界:第一个数左边没数,视为1;最后一个数右边没数,视为1
        pre[0] = 1;
        suf[n - 1] = 1;
        
        // 1. 计算前缀积
        for (int i = 1; i < n; ++i) {
            pre[i] = pre[i - 1] * nums[i - 1]; 
        }
        
        // 2. 计算后缀积 (从右向左遍历)
        for (int i = n - 2; i >= 0; --i) {
            suf[i] = suf[i + 1] * nums[i + 1]; 
        }
        
        // 3. 组合结果
        for (int i = 0; i < n; ++i) {
            ans.push_back(pre[i] * suf[i]);
        }
        
        return ans;
    }
};

4. 复杂度分析

  • 时间复杂度:O(n)。我们进行了三次独立的 for 循环遍历,总体是线性的。

  • 空间复杂度 :O(n)。使用了 presuf 两个辅助数组。

    • 进阶提示:如果题目要求 O(1) 空间(输出数组不计入空间),可以先用输出数组 ans 存前缀积,再用一个变量动态维护后缀积来计算,从而省去 presuf 数组。

三、总结

这两道题虽然解法不同,但都体现了优化算法的常见方向:

  1. 找规律:如"轮转数组"中的反转规律,通过数学变换避免模拟移动的开销。

  2. 预处理:如"除自身以外数组的乘积",通过预先计算好前缀和后缀信息,避免了双重循环的重复计算。

相关推荐
逝雪Yuki42 分钟前
简单多源BFS问题
算法·leetcode·bfs·广度优先遍历
curry____30343 分钟前
study in PTA(高精度算法与预处理)(2025.12.3)
数据结构·c++·算法·高精度算法
ChoSeitaku1 小时前
高数强化NO6|极限的应用|连续的概念性质|间断点的定义分类|导数与微分
人工智能·算法·分类
代码游侠1 小时前
学习笔记——栈
开发语言·数据结构·笔记·学习·算法
自然语1 小时前
人工智能之数字生命-情绪
人工智能·算法
Ayanami_Reii1 小时前
进阶数据结构应用-维护序列
数据结构·算法·线段树
_w_z_j_1 小时前
mari和shiny() (多状态dp数组)
算法
CoderYanger1 小时前
C.滑动窗口-越长越合法/求最短/最小——2904. 最短且字典序最小的美丽子字符串
java·开发语言·数据结构·算法·leetcode·1024程序员节
Tim_101 小时前
【算法专题训练】33、堆
算法