下一个排列:让顺序「刚好大一点」
把一个排列看成一串数字,按从左到右的优先级比大小,就有了它们的字典序 (lexicographic order)。给定其中一个,如何走到紧接着的下一个——既要变大,又要「变得尽量少」?答案是四个动作:从右找支点 (pivot)、找后继 (succ)、交换、翻转后缀。本页把这四步拆到原子级单步播放。
走出来的结果可以反复喂回去枚举全部排列;若想直接拿到「第 k 个」而非一个个走,见康托展开。
为什么是「找上升沿」? 一个排列的后缀若是递减的(如
),它已经是这几位能排出的最大顺序——在不动前面的前提下没法再变大。所以必须往左退到第一个能「抬一下」的位置:第一个满足 a[i] < a[i+1] 的 i,这就是支点。抬高它、再把它后面压到最小,整体就恰好大了「一档」。
四步各司其职。 找支点定位「哪一位需要进位」;找后继是右段里大于支点的最小者(右段递减,从右数第一个就是最小的那个),用它替换支点能让前缀只增大最少的量;交换后右段仍递减;翻转把右段从「最大」翻成「最小」。合起来:前缀进位最小、后缀取最小 = 整体的下一个。
整段递减没有支点。 像 5 4 3 2 1 已是字典序最大,找不到上升沿(i 退到 -1)。按「循环」约定,它的下一个回到最小排列 1 2 3 4 5——直接翻转整段即可。C++ 的 std::next_permutation 在这种情况返回 false 同时也把序列变回升序。
复杂度 O(n)。 三个动作(扫支点、扫后继、翻后缀)都是一趟线性扫描,不需要额外空间,全程就地修改。把它放进循环里反复调用,就能在 内按字典序枚举出全部排列。