php 实现的字典序排列算法,字典序的一个生成算法
字典序的一個(gè)生成算法。
最近在LeetCode刷題,刷到一個(gè)題,鏈接:
https://leetcode-cn.com/problems/permutation-sequence/
這個(gè)題要求得長度為n的字典序列的第k個(gè)排列。
我們知道,字典序列是一個(gè)長度為n(n>=1),元素為1~n的無重復(fù)整數(shù)序列。
之前還真沒仔細(xì)了解過如何按照順序,從小到大生成這個(gè)序列。這次就探究一下。
我先在紙上枚舉了n=3、4、5這幾種簡單的序列的生成,從中找到規(guī)律,然后推理出一般方法。
以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
當(dāng)我們擁有了從第m個(gè)排列到m+1個(gè)排列的生成方法時(shí),就可以寫一個(gè)算法findNext(),通過k-1次生成排列,就可以求出第k次的排列。
那么接下來就是尋找字典序的規(guī)律:
我們能夠知道 如果當(dāng)前字典序排列為M,假設(shè)M的下一個(gè)字典序?yàn)镹,N也有下一個(gè)字典序O,那么有以下推論:
1. N = findNext(M)
2. O = findNext(N)
3. M < N < O
所以可得:N是大于M的最小的排列
既然我們要生成這樣的一個(gè)排列,那么就要盡可能變動位數(shù)更低的數(shù)去增大序列:
以 findNext(1243)為例,為了盡可能變動位數(shù)更低的數(shù)去增大序列,由于“43”已經(jīng)是降序排列的子序列,無法通過變動“4”這個(gè)位及更低的位去增大序列,那么只能從上一位“2”去增大序列,所以我們要從“43”這個(gè)降序序列中找到一個(gè)最的數(shù)“3”,換到“2”的位置,把“2”放入降序序列中,然后重新按照升序排序,這樣就生成了“1324”,即1324 = findNext(1243)
所以我們有以下思路:
1. 從最低位開始尋找最長的遞減序列L的最高位i
2. 如果i是最高位,證明已經(jīng)是最大的字典序,算法結(jié)束;如果不是,取i的上一位j,從L中找到大于j的最小值k,然后交換jk位置
3. 對L進(jìn)行升序排序,把L變?yōu)樽钚⌒蛄?/p>
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){
//找到交換點(diǎn)的位置
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){
//找到逆序內(nèi)大于目標(biāo)值的最小值
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;
}
}
//就剩一個(gè),只能和它換了
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));
}
}
該算法能夠計(jì)算出長度為n的字典序的第k個(gè)排列。
后來啊,我想了想,這個(gè)方法有些慢,畢竟k次移動,只有最后一次是有意義的,之前的k-1次移動都是白白浪費(fèi)了運(yùn)算。于是打算優(yōu)化一下算法。
從優(yōu)化生成字典序的方法開始吧,上面的算法,移動次數(shù)很多,這次優(yōu)化可以采用回溯法處理。思路如下圖所示:(本圖來自Leetcode該題優(yōu)秀題解,自己不想畫圖了,借用一哈)
生成字典序的優(yōu)化思路如下:
1. 構(gòu)造一個(gè)1~n的升序序列N
2. 從小到大逐個(gè)取N中的數(shù),遞歸放入空序列M中
3. 當(dāng)該序列的數(shù)全部用光時(shí),記錄該序列,拿走M(jìn)中尾數(shù)字,回溯
4. 來到上一層,證明該層剛才遞歸用的數(shù)字已經(jīng)用過了,從M尾部拿出,從N中取更大的一個(gè)數(shù),遞歸放入M,回到步驟2
5. 當(dāng)最外層使用了N中最大的數(shù),并且回溯之后,證明所有序列已經(jīng)生成,算法結(jié)束。
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數(shù)組是為了記錄該位置的數(shù)字是否使用過。
有了這個(gè)遞歸思路之后,通過剪枝的操作,就可以快速定位第k個(gè)字典序所在的分支,直接找到并返回。
以n = 4, k = 9為例 所求序列為 L
以1開頭的序列一共有 3*2 = 6個(gè)
因?yàn)閗 = 9 > 6
所以L肯定不以1開頭。
以2開頭的序列也有 3*2 = 6 個(gè)
以2為開頭的序列應(yīng)該是第7個(gè)至第12個(gè)
因?yàn)?7 < k < 12
所以L以2開頭。
以21開頭的序列一共有 2*1 = 2個(gè)
以21開頭的序列應(yīng)該是第7個(gè)至第8個(gè)
因?yàn)?8 < k
所以L不以21開頭
以23開頭的序列一共有 2*1 = 2個(gè)
以23開頭的序列應(yīng)該是第9個(gè)至第10個(gè)
因?yàn)?k == 9
所以L以23開頭且是23開頭的第一個(gè)數(shù),就是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;
//判斷當(dāng)前的這個(gè)值,是否在這個(gè)分支內(nèi)
if(inner_k <= list[num]){
array.add(i);
used.put(i, true);
arrange(array, used, inner_k, list);
}
else {//不在就切換到下一個(gè)分支,去掉之前的個(gè)數(shù)
inner_k = inner_k - list[num];
}
}
}
}
public static void main(String[] args) {
System.out.println(getPermutation(3, 2));
}
}
為了不再多構(gòu)造一個(gè)int數(shù)組來存遞增數(shù)列,將boolean數(shù)組升級為HashMap,兼具int數(shù)組與used數(shù)組的功能。
由于每層遍歷從1開始,可能會遇到已經(jīng)用過的數(shù),這種情況下,不能剪枝,因?yàn)榧糁χ会槍€沒有用過的數(shù)的分支,所以要先判斷該數(shù)是否用過,再判斷是否需要剪枝。
該算法不使用遞歸:
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));
}
}
兩種優(yōu)化算法均為O(n^2)時(shí)間復(fù)雜度。
總結(jié)
以上是生活随笔為你收集整理的php 实现的字典序排列算法,字典序的一个生成算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php支持cs吗,关于composer、
- 下一篇: php怎么关闭oracle连接,PHP