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位」的约束,直接的升序排序思路会处处受限,那换个角度思考------从大到小对元素进行归位,这便是煎饼排序的核心解题思路,一步一步让每个元素找到自己的"正确座位"。
核心归位逻辑
对于数组中的任意一个元素(先从最大值开始,再到次大值,以此类推),通过两次翻转完成归位:
-
第一次翻转 :找到当前待归位元素的位置,翻转其位置之前的所有元素(前
index+1位),让该元素移动到数组第一位; -
第二次翻转 :翻转前
k位(k为该元素的正确位置序号),让该元素从第一位移动到正确的最终位置。
直观步骤演示(以数组[3,4,2,1]为例)
为了更清晰理解,我们用图文结合 的方式展示最大值4的归位过程:
原数组:[3,4,2,1],最大值4的正确位置是第4位(数组下标为3)。
-
第一步:找到
4的下标为1,翻转前1+1=2位,数组变为[4,3,2,1],4来到第一位; -
第二步:翻转前4位,数组变为
[1,2,3,4],4成功归位到最后一位。
💡 次大值及后续元素的归位逻辑完全一致:只需要在剩余未排序的子数组中,重复上述两次翻转操作即可,直到所有元素归位。
三、编码实现📝:C++代码拆解
理解了核心思路,编码实现就水到渠成了。整个实现过程分为三大核心模块:下标记录数组、翻转函数、主排序逻辑,再配合细节优化,让代码更高效、更健壮。
核心思路梳理
-
用
index数组记录每个元素在原数组中的下标,方便快速查找待归位元素的位置,避免多次遍历数组; -
编写通用的
reverse翻转函数,实现「翻转数组前n位」的功能,并在翻转后更新index数组,保证下标记录的准确性; -
从最大值开始遍历到最小值,对每个元素执行两次翻转(若需要),并记录每次的翻转步数,最终输出翻转方案。
关键代码实现(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;
}
代码关键细节讲解
- index数组的设计✨:
由于煎饼排序的数组元素通常为1~n的正整数(若不是可做映射处理),我们将index数组的大小设为n+1,元素值作为index数组的下标 ,对应存储该元素在原数组中的位置。这样做的好处是:O(1)时间查找任意元素的下标,无需遍历数组,大幅提升效率。
- reversePancake翻转函数🔄:
该函数不仅完成数组前n位的翻转,还会同步更新index数组 ------因为数组元素交换后,其下标也发生了变化,若不更新,后续查找会出现错误。同时,函数中直接记录翻转步数到结果数组res中,简化主逻辑。
- 主排序逻辑📌:
从最大值n遍历到最小值1,对每个元素先判断是否已在正确位置(index[i] == i-1),若是则直接跳过;若不是,执行两次翻转操作,完成归位。
四、优化技巧⚡:避免无效操作,提升效率
煎饼排序的核心思路实现后,还存在一些无效的翻转操作,这些操作不仅不会改变数组状态,还会增加结果数组的冗余,因此我们需要做针对性优化,让代码更高效。
优化点1:跳过已归位的元素
如果当前待归位元素的下标已经等于其正确位置(index[i] == i-1),说明该元素已经在最终位置,无需进行任何翻转操作,直接continue进入下一个元素的处理。
优化点2:避免翻转前1位
翻转数组的前1位,数组状态完全不变,属于无意义操作。因此在第一次翻转(翻到第一位)和第二次翻转(翻到正确位置)时,分别判断index[i]+1 != 1和i != 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的正整数,该如何修改代码实现煎饼排序呢?欢迎在评论区交流~