煎饼排序 图解题解
这道题到底在问什么
- 输入
- arr=[3,2,4,1]
- 输出
- [3,4,2,3,2] (5 次翻转后变成 [1,2,3,4])
- 输入
- arr=[1,2,3]
- 输出
- [] (本来就有序,不用翻)
最优解:为什么这么做
一句话答案:LeetCode 969 煎饼排序:只能翻前 k 张,就每轮把没排好那段里的最大值先翻到顶、再翻到底沉入正确位置,记下每次的 k,n−1 轮排完,时间 O(n²)、空间 O(1)。
只能翻前 k 张,最后要我们交出什么
给一个 1 到 n 的排列(每个数恰好出现一次)arr。一次操作叫「煎饼翻转」:挑一个 k,把前 k 个元素整段反转。要返回一串 k 值,按顺序翻完让 arr 升序。题面 arr=[3,2,4,1],一种答案 [3,4,2,3,2],翻 5 次变 [1,2,3,4];arr=[1,2,3] 本就有序,返回空串。答案不唯一,翻转次数不超 10n 都算对。
为什么不能像普通排序那样随手换两个数
普通排序想换哪两个位置就换,这题的手被绑住——唯一动作是反转前缀,想挪中间某个数,得连它前面一整串一起翻。穷举所有翻转组合去凑有序,序列可以很长、可能性指数级膨胀,根本试不完。得找一套照着走就一定排好的翻法,让每个数固定几步落位。
怎么用两次翻转,把一张饼精准送到底
前缀翻转藏着一根杠杆:你没法直接把某张饼放到末位,却能借下标 0 中转。想让最大的饼落到最末位,先翻一个前缀把它带到最顶(下标 0),再翻一个更长的前缀把它一路带到末位,两翻就位。
于是整体走贪心加模拟:每一轮只挑当下最该做的一件事做、照规则一轮轮往下推,盯住还没排好那段前缀里的最大值,沉到这段末尾,排好的尾巴长一节、前缀缩一格,n−1 轮后全部归位。只管最大值:它沉到底后不再挪动,剩下的短前缀又是同一问题的缩小版。
一轮里的两次翻转,k 该记成几
具体到一轮,设未排前缀是下标 0 到 i。从右往左找到这轮最大值所在下标 j,分三种情形:已在末位(j 等于 i)什么都不做;在中间(j 落在 0 和 i 之间),先翻前 j+1 张送到顶、再翻前 i+1 张送到底;已在顶(j 等于 0)省掉第一翻,直接翻前 i+1 张送到底。记进答案的是「张数」j+1、区间 [0, j],写成 j 就翻错一段。
[3,2,4,1] 五次翻转,k 序列一步步长出来
第 1 轮前缀是下标 0 到 3,最大值 4 在下标 2、不在末位:先翻前 3 张(记 3),[3,2,4,1] 变 [4,2,3,1],4 到顶;再翻前 4 张(记 4)变 [1,3,2,4],4 落到下标 3 归位,答案 [3,4]。第 2 轮前缀缩到下标 0 到 2,最大值 3 在下标 1:先翻前 2 张(记 2)、再翻前 3 张(记 3)得 [2,1,3,4],3 归位到下标 2,答案 [3,4,2,3]。
第 3 轮前缀只剩下标 0 到 1,2 在下标 0——已在顶,省掉第一翻,直接翻前 2 张(记 2)变 [1,2,3,4],2 落到下标 1、1 顺势回下标 0,整摞升序。把记下的张数连起来就是 [3,4,2,3,2],共 5 次翻转,对上题面。
n−1 轮各扫一遍,边界上别多翻那一下
一共 n−1 轮,每轮扫一遍前缀找最大再翻,各 O(n),合起来时间 O(n²);翻转原地进行、空间 O(1)。每轮最多翻两次,总次数不超过 2(n−1),落在 10n 里。最大值若已在末位,整轮都该跳过,硬翻只会把排好的尾巴搅乱。它若已在顶,就得省掉第一翻,多翻那一张等于没翻,还会往答案塞个废数。碰上本就有序的数组,每轮都撞见「已在末位」,一次不翻、返回空串,就是题面第二个例子。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记牢这套「找最大,翻到顶,再翻到底」,下面每一轮都在套它。
- 4这就是要排的一摞煎饼,数组 [3,2,4,1]。我们一轮一轮地把当前最大的那张归位,紫色框出的就是还没排好的前缀。
- 5第 1 轮,未排前缀是 [下标 0 到 3]。先看下标 0 的 3,暂时当作最大。
- 6看下标 1 的 2,比 3 小,最大还是 3。
- 7看下标 2 的 4,比刚才的 3 大,最大刷新成 4。
- 8看下标 3 的 1,比 4 小,最大还是 4。
- 9这轮最大的饼是 4,它最终该躺在下标 3(前缀的最末位)。分两步翻过去。
- 10第一翻:翻最上面 3 张(k=3),把 4 先翻到最顶。高亮的就是要翻的区间。
- 11翻完前 3 张,4 现在到了最顶(下标 0)。
- 12第二翻:翻最上面 4 张(k=4),把顶上的 4 一路翻到下标 3。
- 13翻完前 4 张,4 归位在下标 3(蓝色),已排好的尾巴又长了一节。
- 14第 2 轮,未排前缀是 [下标 0 到 2]。先看下标 0 的 1,暂时当作最大。
- 15看下标 1 的 3,比刚才的 1 大,最大刷新成 3。
- 16看下标 2 的 2,比 3 小,最大还是 3。
- 17这轮最大的饼是 3,它最终该躺在下标 2(前缀的最末位)。分两步翻过去。
- 18第一翻:翻最上面 2 张(k=2),把 3 先翻到最顶。高亮的就是要翻的区间。
- 19翻完前 2 张,3 现在到了最顶(下标 0)。
- 20第二翻:翻最上面 3 张(k=3),把顶上的 3 一路翻到下标 2。
- 21翻完前 3 张,3 归位在下标 2(蓝色),已排好的尾巴又长了一节。
- 22第 3 轮,未排前缀是 [下标 0 到 1]。先看下标 0 的 2,暂时当作最大。
- 23看下标 1 的 1,比 2 小,最大还是 2。
- 24这轮最大是 2,而且它已经在最顶(下标 0)了,第一次翻转可以省掉,直接做第二翻。
- 25第二翻:翻最上面 2 张(k=2),把顶上的 2 一路翻到下标 1。
- 26翻完前 2 张,2 归位在下标 1。最小的 1 也顺势落到下标 0,整摞全好了。
- 27全部升序排好![1,2,3,4]。一路记录的翻转张数连起来就是答案:k 序列 3,4,2,3,2,一共 5 次翻转。
⚠️ 容易写错的地方
✗ 错:把 k 当成下标
✓ 对:k 是「张数」,翻的是前 k 张即区间 [0, k-1]
最大在下标 j 要翻到顶,记的 k 是 j+1 不是 j,差一位会翻错段
✗ 错:最大已在顶还多翻一次
✓ 对:j=0 时跳过第一翻,直接做第二翻
多翻 k=1 等于没翻,纯属浪费;写成翻 [0,0] 还会污染答案序列
✗ 错:最大已在末位仍硬翻
✓ 对:j=i 时整轮跳过
它已归位,再翻会把已排好的部分打乱
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def pancakeSort(self, arr: List[int]) -> List[int]:
def reverse(arr, j):
i = 0
while i < j:
arr[i], arr[j] = arr[j], arr[i]
i, j = i + 1, j - 1
n = len(arr)
ans = []
for i in range(n - 1, 0, -1):
j = i
while j > 0 and arr[j] != i + 1:
j -= 1
if j < i:
if j > 0:
ans.append(j + 1)
reverse(arr, j)
ans.append(i + 1)
reverse(arr, i)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> pancakeSort(vector<int>& arr) {
int n = arr.size();
vector<int> ans;
for (int i = n - 1; i > 0; --i) {
int j = i;
for (; j > 0 && arr[j] != i + 1; --j)
;
if (j == i) continue;
if (j > 0) {
ans.push_back(j + 1);
reverse(arr.begin(), arr.begin() + j + 1);
}
ans.push_back(i + 1);
reverse(arr.begin(), arr.begin() + i + 1);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> pancakeSort(int[] arr) {
int n = arr.length;
List<Integer> ans = new ArrayList<>();
for (int i = n - 1; i > 0; --i) {
int j = i;
for (; j > 0 && arr[j] != i + 1; --j)
;
if (j < i) {
if (j > 0) {
ans.add(j + 1);
reverse(arr, j);
}
ans.add(i + 1);
reverse(arr, i);
}
}
return ans;
}
private void reverse(int[] arr, int j) {
for (int i = 0; i < j; ++i, --j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
}复杂度
时间
O(n²)
共 n-1 轮,每轮扫一遍前缀找最大并翻转,都是 O(n)
空间
O(1)
原地翻转,只用几个下标变量;答案数组不计入额外空间
翻转次数
≤ 2(n-1)
每轮最多两翻,远在 10n 的限制内
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 煎饼排序 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这套贪心的翻转次数会不会超出题目允许的 10n?+
不会。每个元素归位最多用两翻,一共 n−1 轮,所以翻转次数不超过 2(n−1)。题目允许 10 倍长度也就是 10n,2(n−1) 比它小得多,永远不会超限。也正因为不追求最少,实现起来才这么直白——只要每轮稳稳把当前最大值沉到底即可。
为什么记进答案的是 j+1 而不是下标 j?+
因为 k 是「翻几张」,不是下标。最大值停在下标 j,要把下标 0 到 j 这一整段(一共 j+1 张)翻过来才能让它到顶,所以记的是张数 j+1、对应区间 [0, j]。要是顺手记成 j,翻的就成了前 j 张、区间 [0, j−1],最大值那张根本没被翻到,整轮全乱。同理送到底那一翻记的是 i+1,不是 i。
能不能求出「最少翻转次数」的解?+
本解只保证次数在限制内,不追求最少。求精确最少翻转次数的「煎饼数」是个很难的组合问题,至今没有已知的高效精确算法,面试里通常只要这套 O(n²) 的简单贪心就够了。若真要更少的翻转,一般也只能靠搜索加剪枝,规模稍大就吃不消。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 煎饼排序 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。