Leetcode 969 煎饼排序✨:翻转间的数组排序艺术

Leetcode 969 煎饼排序✨:翻转间的数组排序艺术

算法与数据结构片头

在算法的世界里,总有一些趣味十足的经典问题,煎饼排序便是其中之一!它以独特的前n位翻转规则为约束,让数组排序的过程变得像翻转煎饼一样充满巧思,既考验对算法逻辑的理解,也能锻炼编码实现的细节把控。今天,我们就一起拆解这道经典算法题,从解题思路到编码实现,再到性能优化,全方位解锁煎饼排序的奥秘~

一、问题初识🔍:什么是煎饼排序?

煎饼排序的核心规则十分简洁,但却充满约束性:我们只能对数组执行「翻转前n位」的操作,通过若干次这样的翻转,将一个无序数组调整为升序数组,并输出任意一组可行的翻转方案即可(答案不唯一)。

举个简单的例子🌰:

现有数组 [3,4,2,1],若第一次翻转前3位,数组会变为 [2,4,3,1]?不,是[4,2,3,1](原前三位3,4,2翻转后为2,4,3?纠正:原前三位3,4,2翻转后是2,4,3,会议示例中为[3,4,2,1]翻转前三位得到[4,2,3,1],核心是前n位元素逆序排列 );若再翻转前4位,整个数组逆序,就会变成[1,3,2,4]。我们的目标,就是通过这样的翻转操作,让数组最终成为[1,2,3,4]这样的有序数组。

这道题的魅力在于,看似简单的翻转操作,需要设计清晰的逻辑才能高效完成排序,而其答案的不唯一性,也给了我们充分的设计空间。

二、算法核心思路🧠:从大到小,逐个归位

面对「仅能翻转前n位」的约束,直接的升序排序思路会处处受限,那换个角度思考------从大到小对元素进行归位,这便是煎饼排序的核心解题思路,一步一步让每个元素找到自己的"正确座位"。

核心归位逻辑

对于数组中的任意一个元素(先从最大值开始,再到次大值,以此类推),通过两次翻转完成归位:

  1. 第一次翻转 :找到当前待归位元素的位置,翻转其位置之前的所有元素(前index+1位),让该元素移动到数组第一位

  2. 第二次翻转 :翻转前k位(k为该元素的正确位置序号),让该元素从第一位移动到正确的最终位置

直观步骤演示(以数组[3,4,2,1]为例)

为了更清晰理解,我们用图文结合 的方式展示最大值4的归位过程:

原数组:[3,4,2,1],最大值4的正确位置是第4位(数组下标为3)。

  1. 第一步:找到4的下标为1,翻转前1+1=2位,数组变为[4,3,2,1]4来到第一位;

  2. 第二步:翻转前4位,数组变为[1,2,3,4]4成功归位到最后一位。

💡 次大值及后续元素的归位逻辑完全一致:只需要在剩余未排序的子数组中,重复上述两次翻转操作即可,直到所有元素归位。

三、编码实现📝:C++代码拆解

理解了核心思路,编码实现就水到渠成了。整个实现过程分为三大核心模块:下标记录数组、翻转函数、主排序逻辑,再配合细节优化,让代码更高效、更健壮。

核心思路梳理

  1. index数组记录每个元素在原数组中的下标,方便快速查找待归位元素的位置,避免多次遍历数组;

  2. 编写通用的reverse翻转函数,实现「翻转数组前n位」的功能,并在翻转后更新index数组,保证下标记录的准确性;

  3. 从最大值开始遍历到最小值,对每个元素执行两次翻转(若需要),并记录每次的翻转步数,最终输出翻转方案。

关键代码实现(C++)

c++ 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 翻转数组前n位,并更新index数组
void reversePancake(vector<int>& arr, vector<int>& index, int n, vector<int>& res) {
    if (n == 1) return; // 翻转前1位无意义,直接返回
    res.push_back(n);   // 记录翻转的步数n
    int l = 0, r = n - 1;
    while (l < r) {
        swap(arr[l], arr[r]);
        // 更新index数组:交换后元素的下标同步更新
        index[arr[l]] = l;
        index[arr[r]] = r;
        l++;
        r--;
    }
}

// 煎饼排序主函数
vector<int> pancakeSort(vector<int>& arr) {
    vector<int> res; // 存储翻转方案
    int n = arr.size();
    vector<int> index(n + 1); // 元素值为1~n,下标从1开始更方便
    
    // 初始化index数组,记录每个元素的初始下标
    for (int i = 0; i < n; i++) {
        index[arr[i]] = i;
    }
    
    // 从最大值到最小值,逐个归位
    for (int i = n; i >= 1; i--) {
        // 优化:如果元素已在正确位置,无需处理
        if (index[i] == i - 1) continue;
        // 第一次翻转:将当前元素翻到第一位
        if (index[i] + 1 != 1) { // 避免无效翻转
            reversePancake(arr, index, index[i] + 1, res);
        }
        // 第二次翻转:将当前元素翻到正确位置
        if (i != 1) { // 避免无效翻转
            reversePancake(arr, index, i, res);
        }
    }
    return res;
}

// 测试主函数
int main() {
    vector<int> arr = {3,4,2,1};
    vector<int> res = pancakeSort(arr);
    cout << "翻转方案:";
    for (int num : res) {
        cout << num << " ";
    }
    cout << endl;
    cout << "排序后数组:";
    for (int num : arr) {
        cout << num << " ";
    }
    return 0;
}

代码关键细节讲解

  1. index数组的设计✨:

由于煎饼排序的数组元素通常为1~n的正整数(若不是可做映射处理),我们将index数组的大小设为n+1元素值作为index数组的下标 ,对应存储该元素在原数组中的位置。这样做的好处是:O(1)时间查找任意元素的下标,无需遍历数组,大幅提升效率。

  1. reversePancake翻转函数🔄:

该函数不仅完成数组前n位的翻转,还会同步更新index数组 ------因为数组元素交换后,其下标也发生了变化,若不更新,后续查找会出现错误。同时,函数中直接记录翻转步数到结果数组res中,简化主逻辑。

  1. 主排序逻辑📌:

从最大值n遍历到最小值1,对每个元素先判断是否已在正确位置(index[i] == i-1),若是则直接跳过;若不是,执行两次翻转操作,完成归位。

四、优化技巧⚡:避免无效操作,提升效率

煎饼排序的核心思路实现后,还存在一些无效的翻转操作,这些操作不仅不会改变数组状态,还会增加结果数组的冗余,因此我们需要做针对性优化,让代码更高效。

优化点1:跳过已归位的元素

如果当前待归位元素的下标已经等于其正确位置(index[i] == i-1),说明该元素已经在最终位置,无需进行任何翻转操作,直接continue进入下一个元素的处理。

优化点2:避免翻转前1位

翻转数组的前1位,数组状态完全不变,属于无意义操作。因此在第一次翻转(翻到第一位)和第二次翻转(翻到正确位置)时,分别判断index[i]+1 != 1i != 1,避免此类无效操作。

优化效果

经过上述优化后,对于已经有序的数组 (如[1,2,3,4]),算法会直接跳过所有操作,结果数组为空,实现了最优的时间复杂度。

五、算法性能分析📊

时间复杂度

  • 初始化index数组:O(n),仅需一次遍历;

  • 归位每个元素时,最多执行两次翻转操作,每次翻转的时间复杂度为O(k) (k为翻转的前n位长度),总共有n个元素,因此翻转的总时间复杂度为O(n²)

  • 整体时间复杂度:O(n²),这是煎饼排序的最优时间复杂度(受限于翻转规则)。

空间复杂度

  • 额外使用了index数组和结果数组res,空间复杂度为O(n),属于常数级额外空间,空间效率较高。

适用场景

煎饼排序是一种基于翻转操作的排序算法,适用于对排序操作有特殊约束的场景(仅能翻转前n位),虽然时间复杂度为O(n²),不如快速排序、归并排序等高效排序算法,但在特定约束下是最优解,同时其趣味化的解题思路,也是算法学习中锻炼逻辑思维的经典案例。

六、总结🎯

煎饼排序以其独特的翻转规则,成为算法学习中一道经典的"思维题",其核心解题思路**「从大到小,逐个归位」** 打破了常规的升序排序思维,让我们学会在约束条件下换角度思考问题。

从思路设计到编码实现,再到细节优化,我们完成了煎饼排序的全流程拆解:用index数组实现元素下标的快速查找,用通用翻转函数实现核心操作,用三次优化避免无效操作,最终实现了高效、健壮的煎饼排序代码。

其实算法的魅力就在于此,看似复杂的问题,只要找到核心逻辑,一步步拆解,就能化繁为简。希望这篇文章能让你对煎饼排序有清晰的理解,也能在后续的算法学习中,养成多角度思考、重细节实现的习惯~

✨ 最后留一个小思考:如果数组元素不是1~n的正整数,该如何修改代码实现煎饼排序呢?欢迎在评论区交流~

相关推荐
炸膛坦客9 小时前
单片机/C/C++八股:(二十)指针常量和常量指针
c语言·开发语言·c++
I_LPL9 小时前
hot100贪心专题
数据结构·算法·leetcode·贪心
颜酱9 小时前
DFS 岛屿系列题全解析
javascript·后端·算法
发现一只大呆瓜10 小时前
React-彻底搞懂 Redux:从单向数据流到 useReducer 的终极抉择
前端·react.js·面试
WolfGang00732110 小时前
代码随想录算法训练营 Day16 | 二叉树 part06
算法
炸膛坦客10 小时前
单片机/C/C++八股:(十九)栈和堆的区别?
c语言·开发语言·c++
零雲10 小时前
java面试:了解抽象类与接口么?讲一讲它们的区别
java·开发语言·面试
uzong10 小时前
Skill 被广泛应用,到底什么是 Skill,今天详细介绍一下
人工智能·后端·面试
发现一只大呆瓜11 小时前
React-路由监听 / 跳转 / 守卫全攻略(附实战代码)
前端·react.js·面试