第k个排列问题(自用)

📃题目描述:

给定参数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开头的组里,并且应该是这个组里的第一个;现在确定了第一个数字,第二位的第一个肯定是最小的那个,那第三位只能是剩下那个了。把这个分组的逻辑转换成编程语言就可以了,利用循环来做即可。

### 自用应用程序开发入门指南 开发一个自用的应用程序可以根据目标平台的不同而有所差异。以下是针对iOS和Android两个主流平台的开发方法介绍。 #### iOS 应用开发 对于初学者来说,Objective-C 是一种常见的编程语言用于iOS应用开发[^1]。尽管Swift逐渐成为苹果生态系统的首选语言,但学习Objective-C仍然有助于理解iOS开发的基础原理。以下是一个简单的Objective-C代码片段展示如何创建一个基本视图控制器: ```objective-c #import <UIKit/UIKit.h> @interface ViewController : UIViewController @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 50)]; label.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); label.textAlignment = NSTextAlignmentCenter; label.text = @"Hello, World!"; [self.view addSubview:label]; } @end ``` 这段代码展示了如何在一个ViewController中添加并显示一个标签控件。 #### Android 应用开发 如果计划开发跨平台或者专注于安卓设备上的应用,则可以考虑使用HTML5 Plus 或 Native.js 技术来构建应用[^2][^3]。下面是一段利用Native.js调用安卓原生功能的例子: ```javascript var obj = plus.android.import("android.content.Intent"); var mainActivity = plus.android.runtimeMainActivity(); mainActivity.startActivity(obj); ``` 此脚本通过JavaScript操作安卓系统中的Intent类实例化对象`obj`,从而启动一个新的活动组件(Activity)。这表明即使是在Web前端背景下的开发者也能借助此类工具轻松接入移动操作系统底层能力。 #### 总结 无论是选择基于Apple官方支持的语言如Objective-C/Swift进行封闭环境内的定制化工作还是采用开放标准兼容多端运行模式的技术方案比如HBuilderX所倡导的方式都各有优劣需根据实际需求权衡取舍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值