全排列生成算法笔记

我们知道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++);
        }
    }
}

引用:

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值