php 实现的字典序排列算法,字典序的一个生成算法
字典序的一个生成算法。
最近在LeetCode刷题,刷到一个题,链接:
https://leetcode-cn.com/problems/permutation-sequence/
这个题要求得长度为n的字典序列的第k个排列。
我们知道,字典序列是一个长度为n(n>=1),元素为1~n的无重复整数序列。
之前还真没仔细了解过如何按照顺序,从小到大生成这个序列。这次就探究一下。
我先在纸上枚举了n=3、4、5这几种简单的序列的生成,从中找到规律,然后推理出一般方法。
以n=4为例,字典序从小到大生成如下:
1234 → 1243 → 1324 → 1342 → 1423 → 1432 → 2134 → 2143 → 2314 → 2341 → 2413 → 2431 → 3124 → 3142 → 3214 → 3241 → 3412 → 3421 → 4123 → 4132 → 4213 → 4231 → 4312 → 4321
当我们拥有了从第m个排列到m+1个排列的生成方法时,就可以写一个算法findNext(),通过k-1次生成排列,就可以求出第k次的排列。
那么接下来就是寻找字典序的规律:
我们能够知道 如果当前字典序排列为M,假设M的下一个字典序为N,N也有下一个字典序O,那么有以下推论:
1. N = findNext(M)
2. O = findNext(N)
3. M < N < O
所以可得:N是大于M的最小的排列
既然我们要生成这样的一个排列,那么就要尽可能变动位数更低的数去增大序列:
以 findNext(1243)为例,为了尽可能变动位数更低的数去增大序列,由于“43”已经是降序排列的子序列,无法通过变动“4”这个位及更低的位去增大序列,那么只能从上一位“2”去增大序列,所以我们要从“43”这个降序序列中找到一个最的数“3”,换到“2”的位置,把“2”放入降序序列中,然后重新按照升序排序,这样就生成了“1324”,即1324 = findNext(1243)
所以我们有以下思路:
1. 从最低位开始寻找最长的递减序列L的最高位i
2. 如果i是最高位,证明已经是最大的字典序,算法结束;如果不是,取i的上一位j,从L中找到大于j的最小值k,然后交换jk位置
3. 对L进行升序排序,把L变为最小序列
Java代码如下:
public class GetPermutation {
public static String getPermutation(int n, int k) {
if(n <= 0 || k <= 0){
return "";
}
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = i + 1;
}
for (int i = 1; i < k; i++) {
findNext(array);
}
return intArrayToString(array);
}
public static void findNext(int[] array){
if(array != null && array.length > 1){
int left_exchange_index = -1;
//找到最长逆序的上一位
for (int i = array.length - 1; i > 0; i--) {
if(array[i - 1] < array[i]){
left_exchange_index = i - 1;
break;
}
}
//如果还有更大的序列
if(left_exchange_index != -1){
//找到交换点的位置
int right_exchange_index = findExchangeIndex(array, left_exchange_index);
//交换
exchange(array, left_exchange_index, right_exchange_index);
//对交换后的序列升序排序
sortRight(array, left_exchange_index + 1);
}
}
}
public static int findExchangeIndex(int[] array, int left_exchange_index){
int left = left_exchange_index + 1;
int right = array.length - 1;
int temp = array[left_exchange_index];
int middle = (left + right) / 2;
while(left < right){
//找到逆序内大于目标值的最小值
if(array[middle] > temp && array[middle + 1] < temp){
return middle;
}else if(array[middle] < temp){
right = middle - 1;
middle = (left + right) / 2;
}else {//array[middle + 1] > temp
left = middle + 1;
middle = (left + right) / 2;
}
}
//就剩一个,只能和它换了
if(left == right){
return left;
}
return -1;
}
public static void exchange(int[] array, int left, int right){
int temp = array[left];
array[left] = array[right];
array[right] = temp;
}
public static void sortRight(int[] array, int left){
Arrays.sort(array, left, array.length);
}
public static String intArrayToString(int[] array){
StringBuffer temp = new StringBuffer();
for(int value : array){
temp.append(value);
}
return temp.toString();
}
public static void main(String[] args) {
System.out.println(getPermutation(4, 9));
}
}
该算法能够计算出长度为n的字典序的第k个排列。
后来啊,我想了想,这个方法有些慢,毕竟k次移动,只有最后一次是有意义的,之前的k-1次移动都是白白浪费了运算。于是打算优化一下算法。
从优化生成字典序的方法开始吧,上面的算法,移动次数很多,这次优化可以采用回溯法处理。思路如下图所示:(本图来自Leetcode该题优秀题解,自己不想画图了,借用一哈)
生成字典序的优化思路如下:
1. 构造一个1~n的升序序列N
2. 从小到大逐个取N中的数,递归放入空序列M中
3. 当该序列的数全部用光时,记录该序列,拿走M中尾数字,回溯
4. 来到上一层,证明该层刚才递归用的数字已经用过了,从M尾部拿出,从N中取更大的一个数,递归放入M,回到步骤2
5. 当最外层使用了N中最大的数,并且回溯之后,证明所有序列已经生成,算法结束。
Java代码如下:
class Solution {
public List> permute(int[] nums) {
List> res = new ArrayList<>();
List temp = new ArrayList<>();
boolean[] used = new boolean[nums.length];
arrange(res, used, nums, temp);
return res;
}
public static void arrange(List> res, boolean[] used, int[] nums, List temp){
if(temp.size() == nums.length){
res.add(new ArrayList<>(temp));
return;
}
for(int i = 0; i < used.length; i++){
if(used[i] == false){
used[i] = true;
temp.add(nums[i]);
arrange(res, used, nums, temp);
used[i] = false;
temp.remove(temp.size() - 1);
}
}
return;
}
}
使用的used数组是为了记录该位置的数字是否使用过。
有了这个递归思路之后,通过剪枝的操作,就可以快速定位第k个字典序所在的分支,直接找到并返回。
以n = 4, k = 9为例 所求序列为 L
以1开头的序列一共有 3*2 = 6个
因为k = 9 > 6
所以L肯定不以1开头。
以2开头的序列也有 3*2 = 6 个
以2为开头的序列应该是第7个至第12个
因为 7 < k < 12
所以L以2开头。
以21开头的序列一共有 2*1 = 2个
以21开头的序列应该是第7个至第8个
因为 8 < k
所以L不以21开头
以23开头的序列一共有 2*1 = 2个
以23开头的序列应该是第9个至第10个
因为 k == 9
所以L以23开头且是23开头的第一个数,就是2314
求解完毕。
将剪枝操作放在递归之前,即可求解,Java代码如下:
public class GetPermutation_better {
public static String getPermutation(int n, int k) {
int[] list = new int[]{1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
int k_inner = k;
Map used = new HashMap<>(16);
for (int i = 1; i <= n; i++) {
used.put(i, false);
}
List array = new ArrayList<>();
arrange(array, used, k_inner, list);
StringBuffer buffer = new StringBuffer();
for(int temp : array){
buffer.append(temp);
}
return buffer.toString();
}
public static void arrange(List array, Map used, int k, int[] list){
if(array.size() == used.size()) {
return;
}
int inner_k = k;
for (int i = 1; i <= used.size(); i++) {
if(used.get(i)){
continue;
}else {
int num = used.size() - array.size() - 1;
//判断当前的这个值,是否在这个分支内
if(inner_k <= list[num]){
array.add(i);
used.put(i, true);
arrange(array, used, inner_k, list);
}
else {//不在就切换到下一个分支,去掉之前的个数
inner_k = inner_k - list[num];
}
}
}
}
public static void main(String[] args) {
System.out.println(getPermutation(3, 2));
}
}
为了不再多构造一个int数组来存递增数列,将boolean数组升级为HashMap,兼具int数组与used数组的功能。
由于每层遍历从1开始,可能会遇到已经用过的数,这种情况下,不能剪枝,因为剪枝只针对还没有用过的数的分支,所以要先判断该数是否用过,再判断是否需要剪枝。
该算法不使用递归:
public class GetPermutation_best {
public static String getPermutation(int n, int k) {
int[] list = new int[]{1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
int k_inner = k;
Map used = new HashMap<>(16);
for (int i = 1; i <= n; i++) {
used.put(i, false);
}
List array = new ArrayList<>();
arrange(array, used, k_inner, list);
StringBuffer buffer = new StringBuffer();
for(int temp : array){
buffer.append(temp);
}
return buffer.toString();
}
public static void arrange(List array, Map used, int k, int[] list){
int inner_k = k;
for (int i = 1; i < used.size(); i++) {
int num = used.size() - array.size() - 1;
int integer = inner_k / list[num];
int rest = inner_k % list[num];
int index;
if(rest == 0){
index = integer;
inner_k = list[num];
}else {
index = integer + 1;
inner_k = rest;
}
array.add(getNum(index, used));
}
array.add(getNum(1, used));
}
public static int getNum(int index, Map used){
int counter = 0;
for (int i = 1; i <= used.size(); i++) {
if(!used.get(i)){
counter++;
}
if(index == counter){
used.put(i, true);
return i;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(getPermutation(3, 3));
}
}
两种优化算法均为O(n^2)时间复杂度。
php 实现的字典序排列算法,字典序的一个生成算法相关推荐
- 曲线的生成算法实现_PCGPlanet1-地形生成算法简介
比较常用的地形生成算法有三种: 四叉树算法,GeoMipmap算法,移动立方体算法 目前市面游戏采用的方案基本都是以这三种算法为基础实现的,下面依次进行介绍 四叉树算法 很经典的算法,在没有GPU的时 ...
- C#,人工智能,机器学习,聚类算法,训练数据集生成算法、软件与源代码
摘要:本文简述了人工智能的重要分支--机器学习的核心算法之一--聚类算法,并用C#实现了一套完全交互式的.可由用户自由发挥的,适用于聚类算法的训练数据集生成软件--Clustering.用户使用鼠标左 ...
- java 随机数生成算法_Java随机数的生成算法
Java中随机数的生成算法主要有3种 1.Math.random()//产生一个0-1之间的随机数,类型为double类型 2.new Random() random.nextInextInt(100 ...
- java不规则算法_分布式id生成算法 snowflake 详解
背景 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识.如在支付流水号.订单号等,随者业务数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息,数据库的自增ID显然不能满足需 ...
- java 唯一id生成算法_唯一ID生成算法剖析
在业务开发中,大量场景需要唯一ID来进行标识:用户需要唯一身份标识:商品需要唯一标识:消息需要唯一标识:事件需要唯一标识-等等,都需要全局唯一ID,尤其是分布式场景下. 唯一ID有哪些特性或者说要求呢 ...
- dga (Domain Generation Algorithm) 域名 生成算法 简介
目录 一.引言 二.背景 三.检测 四.发展 五.总结 一.引言 恶意软件如今已经发展为威胁网络安全的头号公敌,为了逃避安全设施的检测,其制作过程也越来越复杂,其中一个典型做法是在软件中集成DGA(D ...
- 红包随机算法微信群红包随机算法
文章目录 1.前言 2.参考微信群红包算法 3.一个可用的随机算法 参考文献 1.前言 因疫情影响,部门 2021 年会以线上直播的形式进行,通过微信小程序展开.为活跃年会氛围,年会直播间会有抢红包环 ...
- 计算机图形学直线算法论文,《计算机图形学》中直线生成算法的教学心得
摘要:<计算机图形学>是计算机科学与技术专业一门重要的专业课,其中直线生成算法是教学重点之一.该文通过分析几种直线生成算法的特点,阐述了理论教学和实践教学的重点和难点,总结了教学的体会和心 ...
- 迷宫生成算法和迷宫寻路算法
迷宫生成算法和迷宫寻路算法 大学二年级的时候,作为对栈这个数据结构的复习,我制作了一个迷宫生成算法的小程序,当时反响十分好,过了几天我又用自己已经学的DirectX技术制作了DirectX版的程序.这 ...
最新文章
- 雪花算法 Java 版
- 常见的canvas优化——模糊问题、旋转效果
- PHP 单例模式继承的实现方式
- 吐槽一下现在的代码编辑器
- 【计算理论】上下文无关语法 ( 语法组成 | 规则 | 语法 | 语法示例 | 约定的简写形式 | 语法分析树 )
- java基础之访问控制符
- 2014年9月21日_随笔,jdic,ETL,groovy,Nutz好多东西想学
- 【转】Postman系列二:Postman中get接口实战讲解(接口测试介绍,接口测试流程,头域操作)
- (转)iOS编程高性能之路-自动化编译脚本(1)
- 力扣-二叉树的层序遍历
- python的顶级库_三大用于数据科学的顶级Python库
- Linux下挂载NTFS格式文件系统
- 论网络安全(观直播后感)
- BP神经网络预测模型
- web渗透测试----30、0day漏洞
- 极限中0除以常数_第七讲 极限存在准则和两个重要极限
- web前端的css示例
- [学习笔记]后缀数组
- android软件无法联网,关于android软件wifi联网无法使用的问题
- RLException: [xx.launch] is neither a launch file in package [x] nor is [x] a launch file name的解决方法
热门文章
- 再分享 5 个 vs 调试技巧
- 用.NetCore 编译国产老牌PHP论坛DiscuzX ,世界上最好的语言从此属于.Net 的一员
- 使用BeetleX在Linux下部署.NET多站点服务
- 关于技术文章“标题党”一事我想说两句
- 2019 微软Build大会预告:值得开发者期待的是哪些?
- 如何在 ASP.Net Core 中使用 Consul 来存储配置
- 【ASP.NET Core】给路由规则命名有何用处
- WebSocket In ASP.NET Core(二)
- android 特效绘图,Android绘图机制与处理技巧——Android图像处理之图形特效处理...
- mysql事务操作_mysql的事务操作