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 实现的字典序排列算法,字典序的一个生成算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php支持cs吗,关于composer、
- 下一篇: php怎么关闭oracle连接,PHP