我们知道n的全排列组合共有n!
——>如何将这n!全排列的组合打印出来?
为次,我们简单起见,假设对元素全排列的集合是从1到n的简单正整数集合{1,2,…,n}。
一、递归算法
首先递归的思想涉及到减治法的概念。
PS:减治法与分治法
1、减治法:把一个问题分成一个小问题来解决——>缩小(扩大)问题
- 增量法:自底而上的扩大问题——>迭代
- 减治法:自顶而下的缩小问题——>递归
- 3种主要的变化形式:
- 减去一个常量,规模n-1。(插入排序)
- 减去一个常量因子,规模n/2。(折半查找)
- 减去得规模是可变的,规模动态可变。(欧几里得求公因数算法)
- 注:子问题的状态要与原问题一致,不光看规模。如插入排序的状态是有序的。——>状态一致是减治的关键
- 如:选择、冒泡排序不是自顶向下的减治法,原问题与子问题状态不是有序一致的,而是蛮力法。
2、分治法:把一个问题分成多个小问题来解决——>多个同一类型的子问题,规模最好相同。(合并排序、快速排序)
- 注:分治法和减常量因子的减治法的区别。虽然缩小的规模都是n/2。但是①分治法对n/2个每个子问题都处理后合并;②减治法只处理缩小后的一个子问题。
言归正传,有了减治法的概念,全排列的递归算法就自然想到运用减治法的思想。将每次递归看成是一个状态点(子问题),一般是自顶而下不改变排序数组大小只交换组内元素,我这加了自底而上的动态改变数组大小的方法。
1、自顶而下的缩减
设全排列R(n1,n2,n3…..nn),可以化简为分别以n1,n2,n3……开始的全排列,即有状态点 n1R1(n2,n3…..nn),n2R2(n1,n3…..nn),n3R3(n1,n2,…..nn)……nnR(n1,n2,n3…..)。然后接着缩减递归直到R只有一个元素。
——>问题很好想,只是如何缩减,如何交换元素呢?
全排列数组的大小不变。每次形成新的状态点只是前缀元素(索引index)与待排列(元素下标i)的元素交换。这样前缀元素索引后面的集合就是缩减后新的状态点,然后继续递归。(注意每次递归出栈后需要回溯交换元素。)直到索引遍历到n,即状态点只有一个元素,则是递归出口,打印全排列。
一个n为3的全排列递归流程图,如下:
2、自底而上的扩大
对排序数组规模依次扩大,直到n个全排序集合。
每次扩大对数组增加一个新元素,并依次从左到右插入子问题数组。
两个递归代码(java)如下:
//FullPermutation_RecursionAlgorithm
public class Main {
static int count = 1;// 计数
/**
* 向上(增量)递归
*
* 如:3全排列
* 第一次状态:1
* 第二次状态:12,21
* 第三次状态:312,132,123;321,231,213
* @param list
* 每个数组相当于一个状态点,改变数组的大小
* @param maxLength
* 递归边界,即待排序数组长度
*/
private static void up(int list[], int maxLength) {
int n = list.length;
if (n == maxLength) {
System.out.print("第" + count++ + "轮:");
for (int i = 0; i < n; i++) {
System.out.print(list[i]);
}
System.out.print("\n");
} else {
// 数组(状态点)增量操作。如若12,则增加3元素——>123,132,312;若21也增加3元素——>213,231,321
for (int i = n; i >= 0; i--) {
int[] new_list = new int[n + 1];
for (int j = n - 1; j >= i; j--) {
new_list[j + 1] = list[j];
}
new_list[i] = n + 1;
for (int j = 0; j < i; j++) {
new_list[j] = list[j];
}
up(new_list, maxLength);
}
}
}
//初始化数组
private static int[] initList(int n) {
int[] list = new int[n];
for (int i = 0; i < n; i++) {
list[i] = i + 1;
}
return list;
}
// 交换
private static void swap(int[] list, int i, int j) {
int temp;
temp = list[i];
list[i] = list[j];
list[j] = temp;
}
/**
* 向下(缩量)递归
*
* 设全排列R(n1,n2,n3.....nn),可以化简为分别以n1,n2,n3……开始的全排列。
* 即 n1R1(n2,n3.....nn),n2R2(n1,n3.....nn),n3R3(n1,n2,.....nn)……nnR(n1,n2,n3.....)
* @param list 这里的数组大小不变,以交换元素的形式改变数组(状态点),
* @param index 交换index与i(index<=i<=n)de1元素,保持前缀不变
*/
private static void down(int[] list, int index) {
int n = list.length;
if (index >= n) {
System.out.print("第" + count++ + "轮:");
for (int i = 0; i < n; i++) {
System.out.print(list[i]);
}
System.out.print("\n");
return;
}
for (int i = index; i < n; i++) {
swap(list, index, i);
down(list, index + 1);
swap(list, index, i);
}
}
/**
* 两个递归方法求n的全排列
* @param args
*/
public static void main(String[] args) {
System.out.print("输入全排序长度(正整数n):");
Scanner input = new Scanner(System.in);
int n = input.nextInt();// 输入
/*方法一:向上递归
// 状态起点从只有一个元素1开始
* int list[] = { 1 };
* up(list,n);
*/
//方法二:向下递归
down(initList(n), 0);
}
}
二、Johnson-Trotter算法
方向:给每个元素增加方向数组。左移就是该元素与左边相邻的元素交换;右移就是该元素与右边相邻的元素交换。
——>不是任何元素都可以在方向随便交换的。
可移动元素:其移动方向的相邻元素应该小于该元素,才可以移动。
Johnson-Trotter算法伪代码:
//输入:一个正整数n
//输出:{1,...,n}的所有排列的列表
初始化:将第一个排列初始化为12..n,方向全部向左
while 存在一个可移动元素 do
求最大的可移动元素K
把k和它定义的方向下的相邻元素互换
调转所有大于k的元素的方向
将新排列添加到列表中
例:n=3的全排列:
元素:123, 132, 312, 321, 231, 213
方向:-1-1-1,-1-1-1,-1-1-1,1-1-1,-11-1,-1-11
(-1代表左边,1代表右边)
代码(java)如下:
//FullPermutation_JohnsonTrotterAlgorithm
public class Main {
/**
* 寻找可以移动的元素,其移动方向的元素应该小于该元素。在所有满足条件的元素中,找到其中的最大者
* @param list 全排列数组
* @param direction 方向数组(-1代表左边,1代表右边)
* @return
*/
private static int findMovedMaxIndex(int[] list, int[] direction) {
int i = list.length - 1;
int max = -1;
int maxIndex = -1;
while (i >= 0) {
int j = i + direction[i];
if (j >= 0 && j <= list.length - 1) {
if (list[j] < list[i]) {
if (max < list[i]) {
max = list[i];
maxIndex = i;
}
}
}
--i;
}
if (maxIndex >= 0) {
return maxIndex;
} else {
return -1;
}
}
/**
* 找到最大可移动元素后,与移动的元素交换。
* 并把所有大于可移动元素的元素方向调转
* @param list
* @param direction
* @param index
*/
private static void swap(int[] list, int[] direction, int index) {
int next = direction[index] + index;
// 交换list的元素
int temp = list[index];
list[index] = list[next];
list[next] = temp;
// 交换direction的方向
temp = direction[index];
direction[index] = direction[next];
direction[next] = temp;
// 转变所有大于list[index]的元素的方向
for (int i = 0; i < list.length; i++) {
// 此时的index互换后为next
if (list[i] > list[next]) {
direction[i] = -direction[i];
}
}
}
// 打印
private static void print(int[] list, int count) {
System.out.print("第" + count + "轮:");
for (int i = 0; i < list.length; i++) {
System.out.print(list[i]);
}
System.out.print("\n");
}
public static void main(String[] args) {
System.out.print("输入全排序长度(正整数n):");
Scanner input = new Scanner(System.in);
int n = input.nextInt();// 输入
// 初始化全排列数组
int[] list = new int[n];
for (int i = 0; i < list.length; i++) {
list[i] = i + 1;
}
// 初始化方向数组,全部指向左边(-1代表左边,1代表右边)
int[] direction = new int[n];
for (int i = 0; i < direction.length; i++) {
direction[i] = -1;
}
int count = 1;
print(list, count++);
int index;
while ((index = findMovedMaxIndex(list, direction)) >= 0) {
swap(list, direction, index);
print(list, count++);
}
}
}
性能:这个算法是生成排列的最有效的算法之一。该算法的运行时间和排列的数量是呈正比的,也就是说属于O(n!)。
但是该算法的排列次序不是很自然。下面字典序将按照升序排列。
三、字典序算法
例:n=3的字典序:
123,132,213,231,312,321
——如何找到后续的字典序呢?
我们看下伪代码:
//输入:一个正整数n
//输出:字典序下{1,...,n}的所有排列的列表
初始化:将第一个排列初始化为12..n
while 最后一个1排列有两个连续升序的元素 do
找出是的ai<ai+1的最大的i //最长递减后缀ai+1>ai+2>...>an
找到使得ai<aj的最大索引j //j>=i+1,因为ai<ai+1
交换ai和aj //☆就是将ai和后缀中大于它的最小元素进行交换,以使ai增大
将ai+1到an的后缀颠倒 //使其变为递增序列
根据上面的思想,可以发现362541后面,跟着364125,接着364152。
代码(java)如下:
/**
* LexicographicPermute
* @Title: Main.java
* @Description: TODO 字典序排列
* @author ZhangJing
* @date 2017年5月18日 下午4:03:54
*
*/
public class Main {
/**
* //交换list[a],list[b]
* @param list
* @param i
* @param j
*/
private static void swap(int[] list, int i, int j) {
int temp = 0;
temp = list[i];
list[i] = list[j];
list[j] = temp;
}
/**
* 查找i,使A[i+1]>A[i+2]>...>A[n],但A[i]<A[i+1]。
* @param list 待排序数组
* @return 最小元素索引i
*/
private static int findMinOutsideDecreasingSuffix(int[] list){
int n = list.length;
int i = n-1;
while(i>0){
if(list[i-1] < list[i]){
return i-1;
}else {
i=i-1;
}
}
return -1;
}
/**
* 找到后缀中大于ai的最小元素,并交换
* @param list
* @param i
*/
private static void findMinByMaxInDecreasingSuffixAndSwap(int list[], int i) {
int n = list.length;
int j = i+1;
int minIndex = -1;
while(j<n){
if(list[j]<list[i]){
break;
}
j= j+1;
}
if(j>n){
minIndex = n;
}else {
minIndex = j-1;
}
swap(list, i, minIndex);
}
private static void sortSuffix(int[] list, int i){
int n = list.length;
for (int j=i+1; j < n; j++) {
for(int k=j+1; k<n; k++){
if(list[k]<list[j]){
swap(list, k, j);
}
}
}
}
//打印
private static void print(int[] list, int count) {
System.out.print("第" + count + "轮:");
for(int i=0; i<list.length; i++){
System.out.print(list[i]);
}
System.out.print("\n");
}
//检查排列数组中是否有连续的两个升序
private static boolean isNext(int[] list) {
for (int i = list.length-1; i > 0; i--) {
if(list[i] > list[i-1]){
return true;
}
}
return false;
}
public static void main(String[] args) {
System.out.print("输入全排序长度(正整数n):");
Scanner input = new Scanner(System.in);
int n = input.nextInt();// 输入
int origin[] = new int[n];
int count =1;
//初始化原始数组
for (int i = 0; i < origin.length; i++) {
origin[i] = i+1;
}
print(origin, count++);
while(isNext(origin)) {
int index = findMinOutsideDecreasingSuffix(origin);
findMinByMaxInDecreasingSuffixAndSwap(origin, index);
sortSuffix(origin, index);
print(origin, count++);
}
}
}
引用:
- 算法设计与分析基础(4.3.1减治法>减常量方法>生成排列)
- 全排列生成算法(一)(二)(三)