1. 程式人生 > >一次搞懂全排列——LeetCode四道Permutations問題詳解

一次搞懂全排列——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)呢?先來看下面的例子:
  我們用

結論:

  由此,我們可以知道,本題的關鍵即是求出陣列末尾的最長的非遞增子序列。
  不妨假設在陣列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;
    }
}

無重複數字的全排列數

題二描述:

樣例輸入:
[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();
    }
}
 

 
 行文倉促,文中如有不足或不當之處,歡迎拍磚指正。轉載請註明出處。

相關推薦

排列——LeetCodePermutations問題

  LeetCode中與Permutations相關的共有四題:   31. Next Permutation   46. Permutations   47. Permutations II   60. Permutation Sequence   

排列、組合、子集問題

>微信搜一搜:【bigsai】 獲取更多肝貨知識 > 春風十里,感謝有你 ## 前言 Hello,大家好,我是bigsai,long time no see!在刷題和麵試過程中,我們經常遇到一些排列組合類的問題,而全排列、組合、子集等問題更是非常經典問題。本篇文章就帶你徹底搞懂全排列! **求

建模語言UML

Unified Modeling Language (UML)又稱統一建模語言或標準建模語言,它是一個支援模型化和軟體系統開發的圖形化語言,為軟體開發的所有階段提供模型化和視覺化支援,包括由需求分析到規格,到構造和配置。 UML分類 (1)靜態模型(系統結構): 用例圖、類圖、物件圖、構件圖、部署圖 (2)

SpringMVC原理

@[toc] # 前言 前面幾篇文章,學習了Spring IOC、Bean例項化過程、AOP、事務的原始碼和設計思想,瞭解了Spring的整體執行流程,但如果是web開發,那麼必不可少的還有Spring MVC,本篇主要分析在請求呼叫過程中SpringMVC的實現原理,通過本篇要搞懂它是怎麼解決請求、引數、返

Spring Web零xml配置原理以及父子容器關係

# 前言 在使用Spring和SpringMVC的老版本進行開發時,我們需要配置很多的xml檔案,非常的繁瑣,總是讓使用者自行選擇配置也是非常不好的。基於**約定大於配置**的規定,Spring提供了很多註解幫助我們簡化了大量的xml配置;但是在使用SpringMVC時,我們還會使用到**WEB-INF/we

SpringBoot核心原理(自動配置、事件驅動、Condition)

@[TOC] # 前言 SpringBoot是Spring的包裝,通過自動配置使得SpringBoot可以做到開箱即用,上手成本非常低,但是學習其實現原理的成本大大增加,需要先了解熟悉Spring原理。如果還不清楚Spring原理的,可以先檢視博主之前的文章,本篇主要分析SpringBoot的啟動、自動配置、

Spring代理建立及AOP鏈式呼叫過程

@[toc] # 前言 AOP,也就是面向切面程式設計,它可以將公共的程式碼抽離出來,動態的織入到目標類、目標方法中,大大提高我們程式設計的效率,也使程式變得更加優雅。如事務、操作日誌等都可以使用AOP實現。這種織入可以是**在執行期動態生成代理物件**實現,也可以在**編譯期**、**類載入時期**靜態織入

資料結構與演算法隨筆之------二叉樹的遍歷(二叉樹的種遍歷)

二叉樹的遍歷 二叉樹的遍歷(traversing binary tree)是指從根結點出發,按照某種次序依次訪問二叉樹中所有的結點,使得每個結點被訪問依次且僅被訪問一次。 遍歷分為四種,前序遍歷,中序遍歷,後序遍歷及層序遍歷 前序 中

LeetCode 排列 問題

LeetCode中與Permutations相關的共有四題:   31. Next Permutation   46. Permutations   47. Permutations II   60. Permutation Sequence   大致包括了所有

Tensorflow實現Mask R-CNN實例分割通用框架,檢測,分割和特征點定位定(多圖)

優點 設計 orf 時間 rcnn 超越 rain 沒有 add Mask R-CNN實例分割通用框架,檢測,分割和特征點定位一次搞定(多圖) 導語:Mask R-CNN是Faster R-CNN的擴展形式,能夠有效地檢測圖像中的目標,同時還能為每個實例生成一個

各種 Docker 網絡 - 每天5分鐘玩轉 Docker 容器技術(72)

docker 教程 容器 前面各小節我們先後學習了 Docker Overaly,Macvaln,Flannel,Weave 和 Calico 跨主機網絡方案。目前這個領域是百家爭鳴,而且還有新的方案不斷湧現。本節將從不同維度比較各種網絡方案,大家在選擇的時候可以參考。CloudMan 的建議是:

深度學習--李宏毅教程分享

最好 的語音 電機 存在 aabb 工程學 bad 成功 並不是 原標題:【286頁幹貨】一天搞懂深度學習(臺灣資料科學年會課程) 本文是2016 臺灣資料科學年會前導課程“一天搞懂深度學習”的全部講義PPT(共268頁),由臺灣大學電機工程學助理教授李宏毅

安裝多版本php的個雷區,你踩著了嗎

path start cgi 命令執行 mysq -c tool port 一鍵 記一次安裝多版本的php的四個雷區,你踩著了嗎 需求:公司需要在同一臺服務器上安裝不同版本的php,而這一臺的服務上已經安裝了php.7.1,現需要同

分鐘 JavaScript this 指向問題

www ava tro ref 函數定義 htm 所在 就是 一個 關於Javascript的this指向問題,網絡上有很多分析文章,寫的很好,比如這裏和這裏 我這裏做一個簡單的總結。 箭頭函數的 this 箭頭函數內的this指向外層函數定義時所在的作用域。如果沒有外層函

輸出個數組的排列

bubuko .com return else inf its 遞歸 lse mes 方法一(插入法): python實現: #-*- coding:utf-8 -*- if __name__==‘__main__‘: l=[1,2,3,4,5] l_

Java 線程中斷

回復 代碼 信號 過程 執行 except 實例 二維 微信公眾 在之前的一文《如何"優雅"地終止一個線程》中詳細說明了 stop 終止線程的壞處及如何優雅地終止線程,那麽還有別的可以終止線程的方法嗎?答案是肯定的,它就是我們今天要分享的——線程中斷。 下面的這斷代碼大家應

k近鄰(k-NN)演算法(

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

:詞法作用域、動態作用域、回撥函式、閉包

不管什麼語言,我們總要學習作用域(或生命週期)的概念,比如常見的稱呼:全域性變數、包變數、模組變數、本地變數、區域性變數等等。不管如何稱呼這些作用域的範圍,實現它們的目的都一樣: (1)為了避免名稱衝突; (2)為了限定變數的生命週期(本文以變數名說事,其它的名稱在規則上是一樣的)

Python裝飾器是款神奇的神器!你知道怎麼用嗎?它!

    進群:548377875  即可獲取小編精心準備的教程以及大量的PDF呢! 1.引子 #功能函式 def add(x,y): return x+y #裝飾函式 def logger(fn): print('frist') x =

Redis

1什麼是Redis? Redis 是一個基於記憶體的高效能key-value資料庫。 (有空再補充,有理解錯誤或不足歡迎指正) 2Reids有哪些特點? Redis本質上是一個Key-Value型別的記憶體資料庫,很像memcached,整個資料庫統統載入在記憶體當中進行操作,