编程小知识之生成排列

63 篇文章 1 订阅
21 篇文章 1 订阅

本文简述了两种生成排列的方法

生成排列的方法不少,一种经典的方法就是采用递归,假设我们需要求解 1 ~ n 的所有排列,那么我们假设已经求解了 1 ~ n - 1 的所有排列,然后对于求解出的每一种排列(1 ~ n - 1 的一种排列),我们将 n 放置于该排列中可能的 n 个空位上,即可完成 1 ~ n 的排列求解.

说的有些抽象,举个实际的例子:

假设我们要求解 1 ~ 3 这三个数字的所有排列,并且我们已经求解出了 1 ~ 2 的所有排列,求解出的排列如下所示:

1 , 2 2 , 1 \begin{aligned} 1, 2 \\ 2, 1 \end{aligned} 1,22,1

其中每个排列都有 3 个空位:

e m p t y   1 , e m p t y   2   e m p t y e m p t y   2 , e m p t y   1   e m p t y \begin{aligned} \boxed{empty}\ 1, \boxed{empty}\ 2\ \boxed{empty} \\ \boxed{empty}\ 2, \boxed{empty}\ 1\ \boxed{empty} \end{aligned} empty 1,empty 2 emptyempty 2,empty 1 empty

我们将 3 依序填入每个空位即可完成 1 ~ 3 排列的求解:

3 , 1 , 2 1 , 3 , 2 1 , 2 , 3 3 , 2 , 1 2 , 3 , 1 2 , 1 , 3 \begin{aligned} \boxed{3}, 1, 2 \\ 1, \boxed{3}, 2 \\ 1, 2, \boxed{3} \\ \boxed{3}, 2, 1 \\ 2, \boxed{3}, 1 \\ 2, 1, \boxed{3} \end{aligned} 3,1,21,3,21,2,33,2,12,3,12,1,3


求解排列的另外一种思路,就是给每个可能的排列规定一个大小次序,仍然拿 1 ~ 3 的排列举例,我们可以规定:

1 , 2 , 3 &lt; 1 , 3 , 2 &lt; 2 , 1 , 3 &lt; 2 , 3 , 1 &lt; 3 , 1 , 2 &lt; 3 , 2 , 1 \begin{aligned} &amp; 1, 2, 3 &lt; 1, 3, 2 &lt; 2, 1, 3 &lt; 2, 3, 1 &lt; 3, 1, 2 &lt; 3, 2, 1 \end{aligned} 1,2,3<1,3,2<2,1,3<2,3,1<3,1,2<3,2,1

即排列 1 , 2 , 3 1, 2, 3 1,2,3 最小,排列 3 , 2 , 1 3, 2, 1 3,2,1 最大,扩展到 1 ~ n 的排列来讲,如果给定一个现有排列(初始即为 1 ~ n 的顺序排列: 1 , 2 , 3 , . . . , n 1, 2, 3, ..., n 1,2,3,...,n),我们生成大于该排列的最小排列,依次进行下去便可生成所有排列.

那么如何生成大于该排列的最小排列呢?方法如下:

假设当前给定的排列为 a 1 , a 2 , a 3 , a 4 , . . . a n a_1, a_2, a_3, a_4, ... a_n a1,a2,a3,a4,...an

  1. 从后( a n a_n an 开始)往前查找,找到第一个满足 a i &lt; a i + 1 a_i &lt; a_{i+1} ai<ai+1 的元素
  2. 再次从后( a n a_n an 开始)往前查找,找到第一个满足 a j &gt; a i a_j &gt; a_i aj>ai 的元素( a i a_i ai 为上一步骤的结果元素)
  3. 交换 a i a_i ai a j a_j aj
  4. a i + 1 a_{i+1} ai+1 a n a_n an 的元素重新排序

(这里我们只给出了过程描述,更多细节大家可以参考相关书籍)

有了算法步骤,我们便可以进行代码实现了,过程中有两点值得一提:

  • 如果第 1 步中找不到满足 a i &lt; a i + 1 a_i &lt; a_{i+1} ai<ai+1 的元素怎么办?这种情况会出现在元素逆序的排列中(譬如排列: 3 , 2 , 1 3, 2, 1 3,2,1),处理方法很简单,我们直接跳过 2, 3 步骤,直接执行第 4 步中的排序即可,由于此时元素是逆序的,我们直接反转(reverse)排列元素即可完成排序(排序之后排列又回到了"原点").
  • 如果正常进行了 1, 2, 3 这三个步骤,第 4 步中的重新排序操作我们仍然可以通过直接反转(reverse)排列元素来完成:

考虑第 1 步骤的操作,当找到目标元素 a i a_i ai 时,我们可知以下元素关系:

a i &lt; a i + 1 &gt; a i + 2 &gt; a i + 3 &gt; . . . &gt; a n a_i &lt; a_{i+1} &gt; a_{i+2} &gt; a_{i+3} &gt; ... &gt; a_n ai<ai+1>ai+2>ai+3>...>an

考虑第 2 步骤的操作,当找到目标元素 a j a_j aj 时,我们可知以下元素关系:

. . . &gt; a j − 1 &gt; a j &gt; a i &gt; a j + 1 &gt; . . . ... &gt; a_{j-1} &gt; a_j &gt; a_i &gt; a_{j+1} &gt; ... ...>aj1>aj>ai>aj+1>...

第 3 步骤我们执行了 a i a_i ai a j a_j aj 的交换,根据之前的元素关系,我们可知:

a j &lt; a i + 1 &gt; a i + 2 &gt; . . . &gt; a j − 1 &gt; a i &gt; a j + 1 &gt; . . . &gt; a n \boxed{a_j} &lt; a_{i+1} &gt; a_{i+2} &gt; ... &gt; a_{j-1} &gt; \boxed{a_i} &gt; a_{j+1} &gt; ... &gt; a_n aj<ai+1>ai+2>...>aj1>ai>aj+1>...>an

所以 a i + 1 a_{i+1} ai+1 a n a_n an 间的元素在 1, 2, 3 步骤之后仍然是逆序关系,自然我们就可以通过直接反转(reverse)这些元素来对它们进行排序了,最终的示例代码如下:

// C#
public static List<int> NextPermutation(List<int> curPermutation)
{
    var swapIndex = -1;

    var index = curPermutation.Count - 2;
    while (index >= 0)
    {
        if (curPermutation[index] < curPermutation[index + 1])
        {
            swapIndex = index;
            break;
        }
        --index;
    }

    if (swapIndex >= 0)
    {
        // valid swap index, swap with the rest smallest big number
        var swapElement = curPermutation[swapIndex];
        var smallIndex = swapIndex + 1;
        for (int i = curPermutation.Count - 1; i >= swapIndex + 2; --i)
        {
            if (curPermutation[i] > swapElement)
            {
                smallIndex = i;
                break;
            }
        }

        // do swap
        curPermutation[swapIndex] = curPermutation[smallIndex];
        curPermutation[smallIndex] = swapElement;
    }

    // do sort
    var startIndex = swapIndex + 1;
    var endIndex = curPermutation.Count - 1;
    while (endIndex > startIndex)
    {
        var temp = curPermutation[startIndex];
        curPermutation[startIndex] = curPermutation[endIndex];
        curPermutation[endIndex] = temp;
        ++startIndex;
        --endIndex;
    }

    return curPermutation;
}
以下是C语言实现代码: ```c #include <stdio.h> // 计算n的阶乘 int factorial(int n) { int res = 1; for (int i = 1; i <= n; i++) { res *= i; } return res; } // 交换两个数 void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } // 字典序全排列 void permutation(int n) { int p[n]; // 初始化为1,2,3,...,n for (int i = 0; i < n; i++) { p[i] = i + 1; } int count = 0; do { count++; printf("%d\t", count); for (int i = 0; i < n; i++) { printf("%d", p[i]); } printf("\n"); } while (next_permutation(p, n)); } // 下一个字典序排列 int next_permutation(int *p, int n) { // 从后往前找到第一个相邻的逆序对 int i = n - 2; while (i >= 0 && p[i] >= p[i + 1]) { i--; } // 如果找不到逆序对,说明已经是最后一个排列,返回0 if (i < 0) { return 0; } // 从i的右侧找到最小的大于p[i]的数 int j = n - 1; while (j > i && p[j] <= p[i]) { j--; } // 交换p[i]和p[j] swap(&p[i], &p[j]); // 反转i右侧的元素 int k = i + 1, l = n - 1; while (k < l) { swap(&p[k], &p[l]); k++; l--; } return 1; } int main() { int n; printf("请输入一个整数n(1<=n<=9):"); scanf("%d", &n); int count = factorial(n); printf("%d!共有%d个全排列,输出如下:\n", n, count); permutation(n); return 0; } ``` 该程序使用了字典序全排列算法,先将1到n初始化为一个数组p,然后按照字典序的顺序依次输出全排列,直到输出了n!个全排列。在每次输出时,先输出当前排列的编号,然后再输出该排列的元素。在next_permutation函数中,实现了求下一个字典序排列的算法。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值