生成排列的算法彙總

概述

我以爲本身的算法思惟能力有些薄弱,因此基本上天天晚上都會抽空作1-2到 leetcode 算法題。這兩天遇到一個排列的問題——Next Permutation。而後我去搜索了一下生成排列的算法。這裏作一下總結。html

算法

目前,生成一個序列的排列經常使用的有如下幾種算法:c++

  • 暴力法(Brute Force)
  • 插入法(Insert)
  • 字典法(Lexicographic)
  • SJT算法(Steinhaus-Johnson-Trotter)
  • 堆算法(Heap)

下面依次介紹算法的內容,實現和優缺點。git

在介紹這些算法以前,咱們先作一些示例和代碼上的約定:github

  • 個人代碼實現是使用 Go 語言,且僅實現了求int切片的全部排列,其它類型自行擴展也不難。
  • 除非特殊說明,我假定輸入的int中無重複元素,有重複元素可自行去重,其中有個別算法可處理重複元素的問題。

完整代碼放在Github上。golang

暴力法

描述

暴力法是很直接的一種分治法:先生成 n-1 個元素的排列,加上第 n 個元素便可獲得 n 個元素的排列。算法步驟以下:算法

  • 將第 n 個元素依次交換到最後一個位置上
  • 遞歸生成前 n-1 個元素的排列
  • 加上最後一個元素即爲 n 個元素的排列

實現

算法實現也很簡單。這裏引入兩個輔助函數,拷貝和反轉切片,後面代碼都會用到:app

func copySlice(nums []int) []int {
  n := make([]int, len(nums), len(nums))
  copy(n, nums)
  return n
}

// 反轉切片nums的[i, j]範圍
func reverseSlice(nums []int, i, j int) {
  for i < j {
    nums[i], nums[j] = nums[j], nums[i]
    i++
    j--
  }
}

算法代碼以下:函數

func BruteForce(nums []int, n int, ans *[][]int) {
  if n == 1 {
    *ans = append(*ans, copySlice(nums))
    return
  }

  n := len(nums)
  for i := 0; i < n; i++ {
    nums[i], nums[n-1] = nums[n-1], nums[i]
    BruteForce(nums, n-1, ans)
    nums[i], nums[n-1] = nums[n-1], nums[i]
  }
}

做爲一個接口,須要作到儘量簡潔,第二個參數初始值就是前一個參數切片的長度。優化接口:性能

func bruteForceHelper(nums []int, n int, ans *[][]int) {
  // 生成排列邏輯
  ...
}

func BruteForce(nums []int) [][]int{
  ans := make([][]int, 0, len(nums))
  bruteForceHelper(nums, len(nums), &ans)
  return ans
}

優缺點

優勢:邏輯簡單直接,易於理解。學習

缺點:返回的排列數確定是n!,性能的關鍵在於係數的大小。因爲暴力法的每次循環都須要交換兩個位置上的元素,遞歸結束後又須要再交換回來,在n較大的狀況下,性能較差。

插入法

描述

插入法顧名思義就是將元素插入到一個序列中全部可能的位置生成新的序列。從 1 個元素開始。例如要生成{1,2,3}的排列:

  • 先從序列 1 開始,插入元素 2,有兩個位置能夠插入,生成兩個序列 12 和 21
  • 將 3 插入這兩個序列的全部可能位置,生成最終的 6 個序列
1
    12          21
123 132 312  213 231 321

實現

實現以下:

func insertHelper(nums []int, n int) [][]int {
  if n == 1 {
    return [][]int{[]int{nums[0]}}
  }

  var ans [][]int
  for _, subPermutation := range insertHelper(nums, n-1) {
    // 依次在位置0-n上插入
    for i := 0; i <= len(subPermutation); i++ {
      permutation := make([]int, n, n)
      copy(permutation[:i], subPermutation[:i])
      permutation[i] = nums[n-1]
      copy(permutation[i+1:], subPermutation[i:])
      ans = append(ans, permutation)
    }
  }

  return ans
}

func Insert(nums []int) [][]int {
  return insertHelper(nums, len(nums))
}

優缺點

優勢:一樣是簡單直接,易於理解。

缺點:因爲算法中有很多的數據移動,性能與暴力法相比下降了16%

字典法

描述

該算法有個前提是序列必須是有升序排列的,固然也能夠微調對其它序列使用。它經過修改當前序列獲得下一個序列。咱們爲每一個序列定義一個權重,類比序列組成的數字的大小,序列升序排列時「權重」最小,降序排列時「權重」最大。下面是 1234 的排列按**「權重」由小到大:

1234
1243
1324
1342
1423
1432
2134
...

咱們觀察到一開始最高位都是 1,稍微調整一下後面三個元素的順序就可使得整個「權重」增長,類比整數。當後面三個元素已經逆序時,下一個序列最高位就必須是 2 了,由於僅調整後三個元素已經沒法使「權重」增長了。算法的核心步驟爲:

  • 對於當前的序列,找到索引i知足其後的元素徹底逆序。
  • 這時索引i處的元素須要變爲後面元素中大於該元素的最小值。
  • 而後剩餘元素升序排列,即爲當前序列的下一個序列。

該算法用於 C++ 標準庫中next_permutation算法的實現,見GNU C++ std::next_permutation

實現

func NextPermutation(nums []int) bool {
    if len(nums) <= 1 {
        return false
    }

    i := len(nums) - 1
    for i > 0 && nums[i-1] > nums[i] {
        i--
    }

    // 全都逆序了,達到最大值
    if i == 0 {
        reverse(nums, 0, len(nums)-1)
        return false
    }

    // 找到比索引i處元素大的元素
    j := len(nums) - 1
    for nums[j] <= nums[i-1] {
        j--
    }

    nums[i-1], nums[j] = nums[j], nums[i-1]
    // 將後面的元素反轉
    reverse(nums, i, len(nums)-1)
    return true
}

func lexicographicHelper(nums []int) [][]int {
    ans := make([][]int, 0, len(nums))
    ans = append(ans, copySlice(nums))
    for NextPermutation(nums) {
        ans = append(ans, copySlice(nums))
    }

    return ans
}

func Lexicographic(nums []int) [][]int {
    return lexicographicHelper(nums)
}

NextPermutation函數便可用於解決前文 LeetCode 算法題。其返回false表示已經到達最後一個序列了。

優缺點

優勢:NextPermutation能夠單獨使用,性能也不錯。

缺點:稍微有點難理解。

SJT算法

描述

SJT 算法在前一個排列的基礎上經過僅交換相鄰的兩個元素來生成下一個排列。例如,按照下面順序生成 123 的排列:

123(交換23) ->
132(交換13) ->
312(交換12) ->
321(交換32) ->
231(交換31) ->
213

一個簡單的方案是經過 n-1 個元素的排列生成 n 個元素的排列。例如咱們如今用 2 個元素的排列生成 3 個元素的排列。

2 個元素的排列只有 2 個: 1 2 和 2 1。

經過在 2 個元素的排列中全部不一樣的位置插入 3,咱們就能獲得 3 個元素的排列。

在 1 2 的不一樣位置插入 3 獲得:1 2 3,1 3 2 和 3 1 2。 在 2 1 的不一樣位置插入 3 獲得:2 1 3,2 3 1 和 3 2 1。

上面是插入法的邏輯,可是插入法因爲有大量的數據移動致使性能較差。SJT 算法不要求生成全部 n-1 個元素的排列。它記錄排列中每一個元素的方向。算法步驟以下:

  • 查找序列中可移動的最大元素。一個元素可移動意味着它的值大於它指向的相鄰元素。
  • 交換該元素與它指向的相鄰元素。
  • 修改全部值大於該元素的元素的方向。
  • 重複以上步驟直到沒有可移動的元素。

假設咱們須要生成序列 1 2 3 4 的全部排列。首先初始化全部元素的方向爲從右到左。第一個排列即爲初始序列:

<1 <2 <3 <4

全部可移動的元素爲 2,3 和 4。最大的爲 4。咱們交換 3 和 4。因爲此時 4 是最大元素,不用改變方向。獲得下一個排列:

<1 <2 <4 <3

4 仍是最大的可移動元素,交換 2 和 4,不用改變方向。獲得下一個排列:

<1 <4 <2 <3

4 仍是最大的可移動元素,交換 1 和 4,不用改變方向。獲得下一個排列:

<4 <1 <2 <3

當前 4 已經沒法移動了,3 成爲最大的可移動元素,交換 2 和 3。注意,元素 4 比 3 大,因此要改變元素 4 的方向。獲得下一個排列:

>4 <1 <3 <2

這時,元素 4 又成爲了最大的可移動元素,交換 4 和 1。注意,此時元素 4 方向已經變了。獲得下一個排列:

<1 >4 <3 <2

交換 4 和 3,獲得下一個排列:

<1 <3 >4 <2

交換 4 和 2:

<1 <3 <2 >4

這時元素 3 爲可移動的最大元素,交換 1 和 3,改變元素 4 的方向:

<3 <1 <2 <4

繼續這個過程,最後獲得的排列爲(強烈建議本身試試):

<2 <1 >3 >4

已經沒有可移動的元素了,算法結束。

實現

func getLargestMovableIndex(nums []int, dir []bool) int {
    maxI := -1
    l := len(nums)
    for i, num := range nums {
        if dir[i] {
            if i > 0 && num > nums[i-1] {
                if maxI == -1 || num > nums[maxI] {
                    maxI = i
                }
            }
        } else {
            if i < l-1 && num > nums[i+1] {
                if maxI == -1 || num > nums[maxI] {
                    maxI = i
                }
            }
        }
    }

    return maxI
}

func sjtHelper(nums []int, ans *[][]int) {
    l := len(nums)
    // true 表示方向爲從右向左
    // false 表示方向爲從左向右
    dir := make([]bool, l, l)
    for i := range dir {
        dir[i] = true
    }

    maxI := getLargestMovableIndex(nums, dir)
    for maxI >= 0 {
        maxNum := nums[maxI]
        // 交換最大可移動元素與它指向的元素
        if dir[maxI] {
            nums[maxI], nums[maxI-1] = nums[maxI-1], nums[maxI]
            dir[maxI], dir[maxI-1] = dir[maxI-1], dir[maxI]
        } else {
            nums[maxI], nums[maxI+1] = nums[maxI+1], nums[maxI]
            dir[maxI], dir[maxI+1] = dir[maxI+1], dir[maxI]
        }

        *ans = append(*ans, copySlice(nums))

        // 改變全部大於當前移動元素的元素的方向
        for i, num := range nums {
            if num > maxNum {
                dir[i] = !dir[i]
            }
        }

        maxI = getLargestMovableIndex(nums, dir)
    }
}

func Sjt(nums []int) [][]int {
    ans := make([][]int, 0, len(nums))
    ans = append(ans, copySlice(nums))
    sjtHelper(nums, &ans)
    return ans
}

優缺點

優勢:做爲一種算法思惟能夠學習借鑑。

缺點:性能不理想。

Heap算法

描述

Heap算法優雅、高效。它是從暴力法演化而來的,咱們前面提到暴力法性能差主要是因爲屢次交換,堆算法就是經過減小交換提高效率。

算法步驟以下:

  • 若是元素個數爲奇數,交換第一個和最後一個元素。
  • 若是元素個數爲偶數,依次交換第 i 個和最後一個元素。

Wikipedia上有詳細的證實,有興趣能夠看看。

實現

func heapHelper(nums []int, n int, ans *[][]int) {
    if n == 1 {
        *ans = append(*ans, copySlice(nums))
        return
    }

    for i := 0; i < n-1; i++ {
        heapHelper(nums, n-1, ans)
        if n&1 == 0 {
            // 若是是偶數,交換第i個與最後一個元素
            nums[i], nums[n-1] = nums[n-1], nums[i]
        } else {
            // 若是是奇數,交換第一個與最後一個元素
            nums[0], nums[n-1] = nums[n-1], nums[0]
        }
    }
    heapHelper(nums, n-1, ans)
}

// Heap 使用堆算法生成排列
func Heap(nums []int) [][]int {
    ans := make([][]int, 0, len(nums))
    heapHelper(nums, len(nums), &ans)
    return ans
}

Heap 算法很是難理解,並且很容易寫錯,我如今純粹是背下來了😂 。

優缺點

優勢:代碼實現簡單、高效。

缺點:很是難理解。

結語

本文介紹了幾種生成排列的算法,但願對你們有所幫助。

相關文章
相關標籤/搜索