使用递归解决全排列问题(含重复元素)

2,618 阅读6分钟

参考链接

全排列算法思路解析


为了更好地理解,我们使用剑指offer上的一道题目来开始:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。(输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。)


对于这种问题,我们很容易想到用枚举法,ok,let's start。通过枚举这种方式,虽然这种方式很笨拙,但也很有效,可以帮助我们更好的理清思路。对于这个问题,需要分两种情况进行讨论:包含重复元素或者不包含重复元素。


不包含重复元素

给定字符串abcd,对其进行全排列。
a开头为例

                   a
             /     |     \
           b       c       d
          / \      /\      / \
         /   \    /  \     /  \ 
         c   d    b   d    b   c
        |    |   |   |    |    |
        d    c   d   b    c    b
        |    |   |   |    |    |
    ----------------------------------
    | abcd  acdc acbd acdb adbc  adcb|
    ----------------------------------

类似地,我们可以以b ,c,d打头。
根据上面的经验,我们很容易得出这样的思路,

  • abcd四个字母进行全排列时,先确定第1位(从1开始计数)。
  • 对后边三位进行全排列,先确定第2位。
  • 对后面两位进行全排列,先确定第3位。
  • 到最后一位时,因为只剩下一个字母,对一个字母全排列只有一种情况,那就是自身。

根据上面的分析,很容易想到使用递归函数。
要使用递归,必须考虑两个问题:递归终止条件和问题规模的缩小。
递归终止条件
当只剩下,一个字母时,递归到底。
如何缩小问题规模? 根据前面的树图,通过不断确定前n位,也就是固定前面n位,保持不变。一开始,n=1,也就是保持a不变,对b,c,d全排列;然后n=2,保持前2位不变,对后面两位进行全排列;接着n=3,保持前3位不变,对后面一位进行全排列。当n=4时问题规模达到最小,问题解决。


伪代码

    // arr即表示问题的输入,from 表示从哪里开始全排列。
    function permutation(arr,from){
        if(from===arr.length-1){ // 当from到达arr最后一个元素时,问题规模达到最小
            //...do something here
        }
        // ...something need to do...here
        // 缩小问题规模
        permutation(arr,from+1)
    }

我们现在做这样的一个假设,假设给定的一些序列中第一位都不相同,那么就可以认定说这些序列一定不是同一个序列,这是一个很显然的问题。有了上面的这一条结论,我们就可以同理得到如果在第一位相同,可是第二位不同,那么在这些序列中也一定都不是同一个序列。
那么,这个问题可以这样来看。对

我们获得了在第一个位置上的所有情况之后(注:是所有的情况),对每一种情况,抽去序列 T中的第一个位置,那么对于剩下的序列可以看成是一个全新的序列
序列T1可以认为是与之前的序列毫无关联了。同样的,我们可以对这个T1进行与T直到T中只一个元素为止,这样我们就获得了所有的可能性。

示意图如下

这里写图片描述 这里写图片描述

通过上面的示意图,我们可以得到这样的思路,每一位的所有情况就是与其后面的元素进行交换位置。(因为位于前面位置的元素已经交换过一次,所以不需要考虑前面的元素。)

根据上面的分析,我们再来完善我们刚才写的代码:

    // arr即表示问题的输入,from 表示从哪里开始全排列。
    var ret = [] // 定义数组存储排列结果
    function permutation(arr,from){
        if(from===arr.length-1){ // 当from到达arr最后一个元素时,问题规模达到最小
            ret.push(arr.join(''))
        }
        // ...something need to do...here
        // 缩小问题规模
        for(var i=from;i<arr.length;i++){
            swap(arr,from,i)//每一位都与第一位发生交换,i===from时,也就是自己和自己交换,并不会发生值的变化,所以我们可以直接这样处理,不需要让i从from+1开始,当然也可以从i=from+1开始循环。
            permutation(arr,from+1) // 固定位置后移
            swap(arr,from,i)// 这一步的操作是为了在循环体中的第一步,我们对arr,进行了位置交换,对数组产生了影响。arr的顺序发生了变化,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下,所以每次交换后,要还原,确保初始状态一致。
        }
    }
    // 位置交换
    function swap(list, m, n) {
        var temp = list[m];
        list[m] = list[n];
        list[n] = temp;
    }

包含重复元素

abca为例进行分析

               a                 |           b      |           c
           /   |   \             |       /     \    |        /    \
            b   c    a           |       a      c   |       a      b
          /\    /\   /\          |      /\      |   |     /  \      |
         c  a   a  b  b  c       |     a  c     a   |     a   b     a
        |  |   |  |  |  |        |     |  |     |   |     |   |     |
         a  c   b  a  c  b       |     c  a     a   |     b   a     a

思路分析:

  • abca四个字母进行全排列时,先确定第1位(从1开始计数)。第1位有4中情况,因为a出现过一次,第二次出现时,结果与第一次全排列结果一样,所以省去。
  • 对后边三位进行全排列,先确定第2位。同样判断是否出现相同元素,出现则跳过。
  • 对后面两位进行全排列,先确定第3位。同样判断是否出现相同元素,出现则跳过。
  • 到最后一位时,因为只剩下一个字母,对一个字母全排列只有一种情况,那就是自身。 对与包含重复元素与包含重复元素的思路极其类似,只是多了一步判断,也就是判断当前元素之前是不是开过头。
    function permutation(arr,from){
        if(from===arr.length-1){
            ret.push(arr.join(''))
        }
        for(var i=from;i<arr.length;i++){
            if(!hasAppeared(arr,from,i)){
                 swap(arr,from,i)// 这一步的操作是将元素i作为打头元素,也就是以元素i作为开头,在此之前,我们需要对i是否打过头进行判断。也就是看看再[from,i-1]之间看看arr[i]是否出现过。
                permutation(arr,from+1)
                swap(arr,from,i)   
            }
        }
    }
    
    function hasAppeared(arr,from,to){
        if(i>from){
            for(var i=from;i<to;i++){
                if(arr[i]===arr[to]){
                    return true
                }
            }
        }
        return false
    }

解题代码

function Permutation(str) {
    // write code here
    if(str.length===0){
        return []
    }
    var res = []
    perm(str.split(''),0,str.length-1)
    return res.sort();
    function perm(arr, from, to) {
        if (from === to) {
            res.push(arr.join(''))
            return;
        }
        for (var i = from; i <=to; i++) {
            if (!isRepeat(arr, from, i)) {
                swap(arr, from, i)
                perm(arr, from + 1, to)
                swap(arr, from, i)
            }
        }
    }
    function isRepeat(list, from, to) {
        if (to > from) {
            for (var i = from; i < to; i++) {
                if (list[i] === list[to]) {
                    return true;
                }
            }
        }
        return false;
    }
    function swap(list, m, n) {
        var temp = list[m];
        list[m] = list[n];
        list[n] = temp;
    }
}