
本文介绍一种基于阶乘数制(factoradic)的高效算法,可在不枚举所有排列的前提下,直接计算给定有序元素序列的第n个字典序排列,适用于含重复元素的场景,并提供可运行的递归与迭代python实现。
本文介绍一种基于阶乘数制(factoradic)的高效算法,可在不枚举所有排列的前提下,直接计算给定有序元素序列的第n个字典序排列,适用于含重复元素的场景,并提供可运行的递归与迭代python实现。
在处理排列问题时,若只需获取第 N 个(从 0 开始计数)字典序排列,暴力生成全部排列再取索引的方式在元素较多时会迅速失效(时间与空间复杂度均为 O(n!))。而利用阶乘数制(Factoradic)原理,我们可以将排列索引“解码”为一组位置选择序列,从而逐位构造目标排列——这正是所谓“跳过生成、直算结果”的核心思想。
其关键洞察在于:对于长度为 n 的序列,以第 i 个元素作为首项的所有排列共有 (n−1)! 个;因此,第 N 个排列的首个元素,即为排序后候选列表中索引为 N // (n−1)! 的元素;余数 N % (n−1)! 则递归决定剩余 n−1 个元素的排列顺序。
例如,对输入 [3, 3, 3, 3, 6, 6, 6, 9, 9](已排序),求第 27 个排列(0-indexed,即 n = 27):
- 总长 len = 9,首层分块大小为 8! = 40320 —— 显然远大于 27,说明首元素仍在第一个等价组内;
- 实际应按去重后的有序唯一值及其频次动态调整分块大小(原答案未显式处理重复,但代码逻辑天然支持:因 values 是列表,rest.pop(index) 操作基于当前可用元素的顺序,只要输入 values 已按字典序排好,结果即为字典序第 N 排列);
- 因此,nthPerm([3,3,3,3,6,6,6,9,9], 27) 将逐步选取:
- 第1位:27 // 8! = 0 → 取 rest[0] == 3,剩余 [3,3,3,6,6,6,9,9]
- 第2位:27 % 40320 = 27,27 // 7! = 0 → 再取 3
- ……依此类推,最终得到 3,6,9,3,3,6,6,9,9(即 369336699),与预期一致。
以下是两种推荐实现:
✅ 递归版本(清晰易懂):
from math import factorial
def nth_perm(values, n):
if len(values) <= 1:
return values[:]
sub_count = factorial(len(values) - 1)
idx, n_remaining = divmod(n, sub_count)
rest = list(values)
first = rest.pop(idx)
return [first] + nth_perm(rest, n_remaining)✅ 迭代版本(避免栈溢出,推荐生产使用):
from math import factorial
def nth_perm(values, n):
result = []
rest = list(values)
total = factorial(len(values))
for _ in range(len(values)):
total //= len(rest) # 当前每位对应子排列数
idx, n = divmod(n, total)
result.append(rest.pop(idx))
return result⚠️ 重要注意事项:
- 输入 values 必须是已排序的列表(如 [3,3,3,3,6,6,6,9,9]),否则无法保证字典序正确性;
- 本方法默认按标准字典序生成排列,重复元素会被自然处理(无需额外去重逻辑);
- 索引 n 从 0 开始(即第 0 个是首个排列),最大有效值为 factorial(len(values)) - 1;
- 若需支持含重复元素的唯一排列计数(即跳过重复排列),则需改用多重集合排列公式调整分块大小(如 sub_count = factorial(k) // (∏ factorial(freq_i))),但上述代码在输入已排序前提下,仍能正确返回字典序中第 N 个实际出现的排列(含重复)。
掌握该算法后,你即可在 O(n²) 时间内(pop(i) 为 O(n))直接定位任意大规模排列,彻底摆脱全量生成瓶颈。


















