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

在算法面试中,数组(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,其中 ansi 等于 nums 中除 numsi 之外其余各元素的乘积。

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

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

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

ansi 的值其实可以拆解为两部分:

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

  • 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. 预处理:如"除自身以外数组的乘积",通过预先计算好前缀和后缀信息,避免了双重循环的重复计算。

相关推荐
lucas_AI15 小时前
1.2B 小模型赢过 235B 大模型:NaviDC-OCR 把文档解析卷明白了
人工智能·深度学习·算法
冻柠檬飞冰走茶16 小时前
PTA基础编程题目集 7-35有理数均值(C++语言实现)
开发语言·数据结构·c++·算法·均值算法
民乐团扒谱机16 小时前
【微实验】倒谱算法(Cepstrum)深度解析:原理、数学推导与代码实现
人工智能·算法·语音识别
Nil20817 小时前
leetcode 189轮转数组
数据结构·算法·leetcode
-dzk-17 小时前
【动态规划】LC 118.杨辉三角
算法·动态规划
恣逍信点18 小时前
《凌微经》静态自悖——变化之自因
人工智能·科技·算法·机器学习·业界资讯·拓扑学·哲学
吃着火锅x唱着歌18 小时前
LeetCode 3885.设计事件管理器
算法·leetcode·职场和发展
土司大王18 小时前
LeetCode hto100——字母异位词分组
java·算法·leetcode
Eloudy18 小时前
预词力:LLM 的唯一形式化能力
人工智能·算法·agent
依然鸣19 小时前
PTA团体程序设计天梯赛L1真题讲解L1-085-088
数据结构·c++·经验分享·算法·深度优先·pat考试