一次搞懂全排列——LeetCode四道Permutations问题详解

LeetCode中与Permutations相关的共有四题:
  31. Next Permutation
  46. Permutations
  47. Permutations II
  60. Permutation Sequence
  大致包括了所有全排列问题可能考到的题型。
  本文按序列出了解这四道题的详细思路和AC代码。在各题之间,尽可能地使用了不同的解法,使大家对各种方法能有个了解。

目录


下一个全排列数


题一描述:

原题链接:31. Next Permutation
  给定任一非空正整数序列,生成这些数所能排列出的下一个较大序列。若给出的序列为最大序列,则生成最小序列。

输入 → 输出
1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

题一解析:

概念:

这里,先考虑一个序列的最大最小情况。当一个序列为非递减序列时,它必然是该组数的最小的排列数;同理,当一个序列为非递增序列时,它必然是该组数的最大的排列数。

举例:

那么给定一个p(n)要如何才能生成p(n+1)呢?先来看下面的例子:
  我们用<a1,a2,...,am>来表示m个数的一种序列。设序列p(n)=<3,6,4,2>,根据定义可算得下一个序列p(n+1)=<4,2,3,6>。
  1. 观察p(n)可以发现,其子序列<6,4,2>已经为减序,那么这个子序列不可能通过交换元素位置得出更大的序列了。因此必须移动最高位3(即a1)的位置,且要在子序列<6,4,2>中找一个数来取代3的位置。
  2. 子序列<6,4,2>中6和4都比3大,但6大于4。如果用6去替换3得到的序列一定会大于4替换3得到的序列,因此只能选4。将4和3的位置对调后形成排列<4,6,3,2>。对调后得到的子序列<6,3,2>仍保持减序,即这3个数能够生成的最大的一种序列。
  3. 而4是第1次作为首位的,需要右边的子序列最小,因此4右边的子序列应为<2,3,6>,这样就得到了正确的一个序列p(n+1)=<4,2,3,6>。

结论:

由此,我们可以知道,本题的关键即是求出数组末尾的最长的非递增子序列。
  不妨假设在数组nums中,nums[k+1]...nums[n]均满足前一个元素大于等于后一个元素,即这一子序列非递增。
  那么,我们要做的,就是把nums[k]与其后序列中稍大于nums[k]的数交换,接着再逆序nums[k+1]...nums[n]即可。
  
  根据这个思路,可以得到如下的AC代码。

题一Java解答:

public class Solution {
    public void nextPermutation(int[] nums) {
        int len = nums.length;
        if (len<2)  return ;
        
        int[] res = new int [len];
        
        /* 从倒数第二个元素开始,从后向前,找到第一个满足(后元素>前元素)的情况
         * 此时,记录前元素下标k,则[k+1,n-1]为一个单调非增子序列
         * 那么,这里只需要将一个比nums[k]大的最小数与nums[k]交换
         */
        int lastEle = nums[len-1];
        int k = len-2;
        for (; k>=0; k--){
            if (lastEle > nums[k])  break;
            else {
                lastEle = nums[k];
                continue;
            }
        }
        
        // 当前排列为最大排列,逆序之
        if (k<0) {
            for (int i=0; i<(len+1)/2; i++) {
                swap(nums, i, len-1-i);
            }
        } else {
            // 在nums[k+1,n-1]中寻找大于nums[k]的最小数
            int index=0;
            for (int i=len-1; i>k; i--) {
                if (nums[i]>nums[k]) {
                    swap(nums, i, k);
                    index=i;
                    break;
                }
            }
            // index为0,表示当前nums[k]小于其后任意一个数,直接交换k与len-1
            if (index==0){
                swap(nums, k, len-1);
            }
            // 将nums[k+1,n-1]逆序
            for (int i=k+1; i<(k+len+2)/2; i++) {
                swap(nums, i, k+len-i);
            }
        }
        return ;
    }
    // 交换元素
    public void swap(int[] nums, int i, int j){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

无重复数字的全排列数


题二描述:

原题链接:46. Permutations
  给定一个无重复数字的序列,返回这些数所能排列出所有序列。

样例输入:
[1,2,3]

样例输出:
[   
    [1,2,3],
    [1,3,2],
    [2,1,3],
    [2,3,1],
    [3,1,2],
    [3,2,1]
]

题二解析:

这是很经典的全排列问题,本题的解法很多。因为这里的所有数都是相异的,故笔者采用了交换元素+DFS的方法来求解。
  下面列出我的AC代码。代码中附有中文注释,在此就不再赘述具体步骤。

题二Java解答:

public class Solution {
    
    // 最终返回的结果集
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    
    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        if (len==0||nums==null)  return res;

        // 采用前后元素交换的办法,dfs解题
        exchange(nums, 0, len);
        return res;
    }
    
    public void exchange(int[] nums, int i, int len) {
        // 将当前数组加到结果集中
        if(i==len-1) {
            List<Integer> list = new ArrayList<>();
            for (int j=0; j<len; j++){
                list.add(nums[j]);
            }
            res.add(list);
            return ;
        }
        // 将当前位置的数跟后面的数交换,并搜索解
        for (int j=i; j<len; j++) {
            swap(nums, i, j);
            exchange(nums, i+1, len);
            swap(nums, i, j);
        }
    }
    
    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

有重复数字的全排列数


题三描述:

原题链接:47. Permutations II
  给定一个含有重复数字的序列,返回这些数所能排列出的所有不同的序列。

样例输入:
[1,1,2]

样例输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

题三解析

题意:

本题与题二略有不同,给定的序列中含有重复元素,需要返回这些数所能排列出的所有不同的序列集合。

思路:

这里我们先考虑一下,它与第二题唯一的不同在于:在DFS函数中,做循环遍历时,如果与当前元素相同的一个元素已经被取用过,则要跳过所有值相同的元素。
  举个例子:对于序列<1,1,2,3>。在DFS首遍历时,1 作为首元素被加到list中,并进行后续元素的添加;那么,当DFS跑完第一个分支,遍历到1 (第二个)时,这个1 不再作为首元素添加到list中,因为1 作为首元素的情况已经在第一个分支中考虑过了。
  为了实现这一剪枝思路,有了如下的解题算法。

解题算法:

1. 先对给定的序列nums进行排序,使得大小相同的元素排在一起。
  2. 新建一个used数组,大小与nums相同,用来标记在本次DFS读取中,位置i的元素是否已经被添加到list中了。
  3. 根据思路可知,我们选择跳过一个数,当且仅当这个数与前一个数相等,并且前一个数未被添加到list中。
  根据以上算法,对题二的代码略做修改,可以得到如下的AC代码。
  (在处理一般性问题时,建议用此算法,毕竟题二只是特殊情况)

题三Java解答

public class Solution {
    
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len = nums.length;
        if(len==0||nums==null)  return res;
        
        boolean[] used = new boolean[len];
        List<Integer> list = new ArrayList<Integer>();
        
        Arrays.sort(nums);
        dfs(nums, used, list, len);
        return res;
    }
    
    public void dfs(int[] nums, boolean[] used, List<Integer> list, int len) {
        if(list.size()==len) {
            res.add(new ArrayList<Integer>(list));
            return ;
        }
        for (int i=0; i<len; i++) {
            // 当前位置的数已经在List中了
            if(used[i]) continue;
            // 当前元素与其前一个元素值相同 且 前元素未被加到list中,跳过该元素
            if(i>0 && nums[i]==nums[i-1] && !used[i-1])   continue;
            // 深度优先搜索遍历
            used[i]=true;
            list.add(nums[i]);
            dfs(nums, used, list, len);
            list.remove(list.size()-1);
            used[i]=false;
        }
    }
}

取特定位置的全排列字符串


题四描述:

原题链接:60. Permutation Sequence
  给定正整数n和k,要求返回在[1,2,...,n]所有的全排列中,第k大的字符串序列。

样例输入:
3  4

样例输出:
"231"

题四解析:

思路:

这里我们先考虑一个特殊情况,当n=4时,序列为[1,2,3,4],有以下几种情况:
  "1+(2,3,4)的全排列"
  "2+(1,3,4)的全排列"
  "3+(1,2,4)的全排列"
  "4+(1,2,3)的全排列"
  我们已经知道,对于n个数的全排列,有n!种情况。所以,3个数的全排列就有6种情况。
  
  如果我们这里给定的k为14,那么它将会出现在:
    "3+(1,2,4)的全排列"
  这一情况中。

我们可以程式化地得到这个结果:取k=13(从0开始计数),(n-1)!=3!=6,k/(n-1)!=2,而3在有序序列[1,2,3,4]中的索引就是2。
  同理,我们继续计算,新的k=13%6=1,新的n=3,那么1/(n-1)!=2/2=0。在序列[1,2,4]中,索引0的数是1。那么,此时的字符串为"31"。
  继续迭代,新的k=1%2=1,新的n=2,那么k/(n-1)!=1/1=1。在序列[2,4]中,索引为1的数是4。那么,此时的字符串为"314"。最后在串尾添上仅剩的2,可以得到字符串"3142"。
  经过验算,此串确实是序列[1,2,3,4]的全排列数中第14大的序列。

解题算法:

1. 创建一个长度为n 的数组array,存放对应下标n的阶乘值。
  2. 再新建一个长度为n 的数组nums,初始值为nums[i]=i+1,用来存放待选的字符序列。
  3. 将得到的k减1后,开始迭代。迭代的规则是:迭代n次,每次选nums数组中下标为k/(n-1)!的数放在字符串的末尾,新的k=k%(n-1)!,新的n=n-1。
  4. 最后,返回得到的字符串。
  根据以上算法,可以得到如下的AC代码。

题四Java解答:

public class Solution {
    public String getPermutation(int n, int k) {

        StringBuilder sb = new StringBuilder();
        int[] array = new int[n+1];
        int sum = 1;
        array[0] = 1;

        // array[] = [1, 1, 2, 6, 24, ... , n!]
        for (int i=1; i<=n; i++){
            sum *= i;
            array[i] = sum;
        }

        // nums[] = [1, 2, 3, ... n]
        List<Integer> nums = new LinkedList<>();
        for (int i=0; i<n; i++){
            nums.add(i+1);
        }
        
        k--;
        for (int i=1; i<=n; i++){
            int index = k / array[n-i];
            sb.append("" + nums.get(index));
            nums.remove(index);
            k = k % array[n-i];
        }
        return sb.toString();
    }
}

行文仓促,文中如有不足或不当之处,欢迎拍砖指正。转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,660评论 0 33
  • 转载自:https://egoistk.github.io/2016/09/10/Java%E6%8E%92%E5...
    chad_it阅读 947评论 0 18
  • 昨晚十点做了40多分钟瑜伽,然后又听了一会儿瑜伽休息术,在蕙兰温柔的声音中入睡。 今天感觉身体很舒服,僵硬感消失。...
    心境如花阅读 230评论 0 0
  • 青春就像一次旅行,可能旅行会有很多次,去的地方也会是同一个,哪怕身边站着的人也是同一个,但每一次的感觉都只...
    Aybaby阅读 230评论 0 0