📃题目描述:
给定参数n,从1到n会有n个整数:1,2,3,…,n,这n个数字共有n!种排列。
按大小顺序升序列出所有排列的情况,
当n=3时,所有排列如下:
“123” “132” “213” “231” “312” “321”
给定n和k,返回第k个排列。
1️⃣解法一:递归(暴力全排列)
🤔核心思路:
先生成 1~n 所有数字的全排列(按字典序),当生成的排列数量达到 k 时停止,取第 k 个排列作为结果。本质是 “遍历所有可能,找到目标”。
👣具体步骤:
(1)初始化准备:
- 用数组
nums
存储 1~n 的数字(初始待排列的全部数字); - 用列表
result
存储生成的排列(字符串形式,如 “123”); - 用
current
记录当前正在拼接的排列(如递归中逐步拼接 “1”→“12”→“123”)。
(2)递归生成排列(核心逻辑):
A.终止条件:若nums
为空(所有数字都已拼接到current
),则将current
加入result
,表示生成了一个完整排列;
B.循环选数:遍历当前nums
中的每个数字,每次选一个数字拼接到current
后:
- 生成新数组
newNums
:删除已选中的数字(避免重复使用); - 递归调用:用
newNums
和更新后的current
继续生成后续排列;
C.提前终止:若result
的大小达到 k(已找到第 k 个排列),直接返回,避免生成多余排列(优化暴力法的无效计算)。
(3)取结果:生成的result
按字典序排列,第 k-1 个元素(0 基索引)就是目标的 “第 k 个排列”。
💻代码实现:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class TheKPermutation {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n=sc.nextInt();
sc.nextLine();
int k=sc.nextInt();
sc.close();
// 如果 n 等于 1,则直接输出 1 并结束程序
if (n == 1) {
System.out.println("1");
return;
}
// 初始化结果列表
List<String> result = new ArrayList<>();
// 初始化 nums 数组,存储 1 到 n 的整数
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// 递归函数,用于生成所有排列
System.out.println(getPermutation(n, k));
}
public static void generatePermutations(int[] nums, String current, List<String> result, int k) {
// 如果数字数组为空,将当前结果添加到结果列表中
if (nums.length == 0) {
result.add(current);
return;
}
// 遍历当前数字数组
for (int i = 0; i < nums.length; i++) {
// 取出一个数字
int num = nums[i];
// 创建新的数字数组,删除当前数字
int[] newNums = new int[nums.length - 1];
for (int j = 0; j < i; j++) {
newNums[j] = nums[j];
}
for (int j = i + 1; j < nums.length; j++) {
newNums[j - 1] = nums[j];
}
// 递归调用函数,传递更新后的数字数组和结果字符串
generatePermutations(newNums, current + num, result, k);
// 如果结果列表长度等于k,直接返回
if (result.size() == k) return;
}
}
这种解法的优点是逻辑简单,数据量小的时候可以直接使用,很容易理解。缺点也显而易见,效率很低,当n很大的时候,生成全排列就不妥了。
那么,有没有更好的方法呢避免暴力解决,降低时间复杂度呢?
2️⃣解法二:阶乘定位法
🔎问题本质
我们再来分析下这个题目,n 个不同数字(1~n)的全排列有 n!
种,且按字典序排列(如 n=3 时,排列顺序是 123、132、213、231、312、321)。我们需要找到 “第 k 个排列”,那我们为什么不直接去找这个“第k个”呢?所以这个题目的关键就是:每确定一位数字,就能通过阶乘计算出该位数字对应的 “分组”,从而快速定位,无需遍历所有排列。
💯核心前置知识:阶乘的分组作用
以 n=3 为例,3!=6
个排列可按「第一位数字」分成 3 组,每组有 2!=2
个排列(因为确定第一位后,剩下 2 个数字的排列数是 2!):
- 第一位 = 1:对应排列 [123, 132] → 共 2! 个
- 第一位 = 2:对应排列 [213, 231] → 共 2! 个
- 第一位 = 3:对应排列 [312, 321] → 共 2! 个
同理:若确定了前两位(如 1、2),剩下 1 个数字的排列数是 1!=1
,即只有 1 种可能(123)。
规律:确定第 i 位数字后,剩余 n-i
个数字的排列数为 (n-i)!
,这就是分组的 “大小”—— 通过这个大小,可计算当前位数字的索引。
💻代码实现:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class TheKPermutation {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n=sc.nextInt();
sc.nextLine();
int k=sc.nextInt();
sc.close();
// 如果 n 等于 1,则直接输出 1 并结束程序
if (n == 1) {
System.out.println("1");
return;
}
// 初始化结果列表
List<String> result = new ArrayList<>();
// 初始化 nums 数组,存储 1 到 n 的整数
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// 递归函数,用于生成所有排列
System.out.println(getPermutation(n, k));
}
public static String getPermutation(int n, int k) {
// 存储当前可以选择的数字(初始为1-n)
List<Integer> numbers = new ArrayList<>();
// 存储0-n的阶乘的值
int[] factorial = new int[n + 1];
StringBuilder sb = new StringBuilder();
// 初始化阶乘数组
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
numbers.add(i);
}
k--; // 因为是list,所以列表的索引需要转换为0-based索引
// i表示“当前要确定第i位”(从1开始,对应高位到低位)
for (int i = 1; i <= n; i++) {
// 用k除以组大小(剩余n-i个数字的排列数),得到当前位数字在可选列表中的索引
int index = k / factorial[n - i];
sb.append(numbers.get(index));
numbers.remove(index);
// 更新k(缩小范围到当前组内的位置),
k -= index * factorial[n - i]; // 减去“前面所有组的总个数”,k变为当前组内偏移量
}
return sb.toString();
}
}
🤔代码拆解(帮助理解):
代码的核心逻辑是「从高位到低位,依次确定每一位数字」,共 3 个关键步骤:初始化、调整 k 为 0 基索引、逐位确定数字。
步骤 1:初始化(准备阶乘数组和可选数字列表)
List<Integer> numbers = new ArrayList<>(); // 存储当前可选的数字(初始是1~n)
int[] factorial = new int[n + 1]; // 存储0! ~ n! 的阶乘值
StringBuilder sb = new StringBuilder(); // 拼接最终的第k个排列
// 1. 计算阶乘 + 初始化可选数字列表
factorial[0] = 1; // 0! = 1(数学定义:0个元素的排列数是1)
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i; // 递推计算i!(如1! = 1*1, 2! = 1!*2, ...)
numbers.add(i); // 可选数字列表初始化为 [1,2,...,n]
}
- 阶乘数组作用:快速获取「剩余数字的排列数」(如 n=3 时,factorial [2] = 2! = 2,即确定第一位后每组的大小)。
- 可选数字列表作用:每次确定一位数字后,就从列表中移除该数字(避免重复使用)。
阶段 2:将 k 从 “1 基” 转为 “0 基” 索引(关键!)
k--; // 转换为0-based索引
因为我们用的是list,它的索引从0开始,所以要转换成0基!
阶段 3:逐位确定数字(从高位到低位)
循环n次(每次确定 1 位,共 n 位),核心是「计算当前位数字的索引 → 取数字 → 更新可选列表和 k」:
for (int i = 1; i <= n; i++) { // i表示“当前要确定第i位”(从1开始,对应高位到低位)
// 步骤1:计算当前位数字的索引
int groupSize = factorial[n - i]; // 剩余n-i个数字的排列数 = 每组的大小
int index = k / groupSize; // 用k除以组大小,得到当前位数字在可选列表中的索引
// 步骤2:取数字并拼接
sb.append(numbers.get(index)); // 将当前位数字加入结果
numbers.remove(index); // 从可选列表中删除该数字(避免重复)
// 步骤3:更新k(缩小范围到当前组内的位置)
k -= index * groupSize; // 减去“前面所有组的总个数”,k变为当前组内的偏移量
}
🎯示例拆解(n=3,k=3 → 目标排列 213)
1.初始化:
factorial = [1,1,2,6],numbers = [1,2,3],k=3→k=2(0 基)。
2.第一次循环(确定第 1 位,i=1):
- groupSize = factorial [3-1] = factorial [2] = 2(每组 2 个排列);
- index = 2 / 2 = 1(k=2 对应第 1 个索引,可选列表 [1,2,3] 的第 1 位是 2);
- 拼接 2,numbers 变为 [1,3];
- k = 2 - 1*2 = 0(更新后 k 是当前组内的偏移量,即第 0 个位置)。
3.第二次循环(确定第 2 位,i=2):
- groupSize = factorial [3-2] = factorial [1] = 1(每组 1 个排列);
- index = 0 / 1 = 0(可选列表 [1,3] 的第 0 位是 1);
- 拼接 1,numbers 变为 [3];
- k = 0 - 0*1 = 0。
4.第三次循环(确定第 3 位,i=3):
- groupSize = factorial[3-3] = factorial[0] = 1;
- index = 0 / 1 = 0(可选列表 [3] 的第 0 位是 3);
- 拼接 3,最终结果为 "213"。
📔总结:
说白了这个算法,就是通过先分组,然后定位的数学方法来解决问题。还是以n=3,k=3为例,确定第1位时,后面2位还有(3-1)!=2种排列,所以k=3不在这个组里,1后面跟的两个,2后面跟的两个,k=3应该在以2开头的组里,并且应该是这个组里的第一个;现在确定了第一个数字,第二位的第一个肯定是最小的那个,那第三位只能是剩下那个了。把这个分组的逻辑转换成编程语言就可以了,利用循环来做即可。