千家信息网

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

发表于:2025-01-22 作者:千家信息网编辑
千家信息网最后更新 2025年01月22日,如何用Go语言生成一个排列,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。算法目前,生成一个序列的排列常用的有以下几种算法
千家信息网最后更新 2025年01月22日如何用Go语言生成一个排列

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

算法

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

暴力法(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 < 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 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 算法非常难理解,而且很容易写错,我现在纯粹是背下来了

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

元素 算法 序列 生成 移动 方向 最大 位置 性能 暴力 两个 权重 代码 优缺点 步骤 优点 缺点 不同 三个 不用 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 司法局法制主题展厅软件开发 数据库版本怎么填 paas物联网云服务器 三级网络技术考试大纲 网络安全保障函 棋牌软件开发广告语 实况8联机对战是哪个服务器 应届毕业生笔试题软件开发 为何总是显示微信无法找到服务器 贯彻网络安全审查办法 国家网络安全产品的质量标准 电商的软件开发费如何做阿红 淄博电商软件开发推荐 东北电力大学网络安全大赛 漯河网络安全工程师零基础 国外网络安全体系现状 roblox进服务器 互联网网络安全是哪一层特殊需求 理光扫描仪服务器响应错误 网络安全绘画人物素材 通用云存储服务器软件 福建服务器散热片云主机 mysql 跨服务器 浙江晶曙互联网科技有限公司 网络安全比自由重要辩论 木鱼跳跳网络技术有限公司怎么样 柬埔寨java软件开发 湖北工业大学移动软件开发 和平精英之前谁把服务器充爆过 服务器内置管理员密码忘记了
0