如何用Go语言生成一个排列

79次阅读
没有评论

共计 5293 个字符,预计需要花费 14 分钟才能阅读完成。

如何用 Go 语言生成一个排列,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面丸趣 TV 小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

算法

目前,生成一个序列的排列常用的有以下几种算法:

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

下面依次介绍算法的内容,实现和优缺点。

在介绍这些算法之前,我们先做一些示例和代码上的约定:

我的代码实现是使用 Go 语言,且仅实现了求 int 切片的所有排列,其它类型自行扩展也不难。除非特殊说明,我假定输入的 int 中无重复元素,有重复元素可自行去重,其中有个别算法可处理重复元素的问题。

完整代码放在 Github 上。

暴力法描述

暴力法是很直接的一种分治法:先生成 n-1 个元素的排列,加上第 n 个元素即可得到 n 个元素的排列。算法步骤如下:

将第 n 个元素依次交换到最后一个位置上递归生成前 n-1 个元素的排列加上最后一个元素即为 n 个元素的排列

实现

算法实现也很简单。这里引入两个辅助函数,拷贝和反转切片,后面代码都会用到:

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 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 23,132 和 31 2。在 2 1 的不同位置插入 3 得到:2 13,231 和 32 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 算法非常难理解,而且很容易写错,我现在纯粹是背下来了

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注丸趣 TV 行业资讯频道,感谢您对丸趣 TV 的支持。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-19发表,共计5293字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)