美文网首页
几种计算全排列的方法

几种计算全排列的方法

作者: 劫客轮蹲 | 来源:发表于2019-04-23 20:27 被阅读0次

基于golang实现,有非并发实现和并发实现

递归

全排列问题,比如打印1-9的共9个字母的全排列。先输出1,然后是2-9的全排列,输出2,然后1-9中去除2的全排列。于是很自然想到递归的方法。写出伪代码:


permutaion(prefix, set) {
    if set 为空
        print prefix

    for s in set:
        permuetaion(prefix+s, set - s)
}

go递归实现

func permutaionImpl(prefix []byte, s []byte, cur int) {
    if cur == len(s) {
        fmt.Println(prefix)
        return
    }

    for _, b := range s {
        exist := false
        for i := 0; i < cur; i++ {
            if prefix[i] == b {
                exist = true
                break
            }
        }

        if !exist {
            prefix[cur] = b
            permutaionImpl(prefix, s, cur+1)
        }

    }
}

func Permutation(s []byte) {
    //前缀slice
    p := make([]byte, len(s), len(s))
    permutaionImpl(p, s, 0)
}

并发1

刚才实现的是go单线程执行的,改成并发版本的, 参考rob pike讲Go Concurrency Patterns,多个goroutine通过channel连接起来。goroutine1发送1给goroutine2, goroutine2发送12给goroutine3,goroutine3发送123给goroutine4, 以此类推。

func prefixIncrement(in []byte, s []byte, next chan []byte) {
    for _, c := range s {
        exist := false
        for _, e := range in {
            if e == c {
                exist = true
                break
            }
        }
        if exist {
            continue
        }

        temp := make([]byte, 0)
        temp = append(temp, in...)
        temp = append(temp, c)
        next <- temp
    }
}

func permutaionConImpl(req chan []byte, out chan []byte, s []byte) {
    go func() {
        //递归退出条件: len(v) == len(s)-1
        v, ok := <-req
        if !ok {
            return
        }

        next := out
        if len(v) != len(s)-1 {
            next = make(chan []byte)
            permutaionConImpl(next, out, s)
        }

        prefixIncrement(v, s, next)
        for in := range req {
            prefixIncrement(in, s, next)
        }
        close(next)
    }()
}

// PermutationConcurrency  并发计算全排列
func PermutationConcurrency(s []byte) {
    req, out := make(chan []byte), make(chan []byte)

    //开启goroutine计算
    permutaionConImpl(req, out, s)

    over := make(chan struct{})
    
    //要开goroutine读取out,如果放在主函数中,会导致死锁。
    go func() {
        for res := range out {
            fmt.Println(res)
        }
        close(over)
    }()

    for _, c := range s {
        sl := []byte{c}
        req <- sl
    }
    close(req)

    <-over

}

并发2

并发1中把每个排列的阶段拆分到不同的goroutine, 从goroutine1开始每个goroutine越来越繁忙,最后一个goroutine要输出n!个slice,任务轻重很不均衡。于是也可以考虑让每个goroutine做同样的事情,比如下面的实现

func PermutationConcurrencyVertical(s []byte) {
    var sg sync.WaitGroup
    for _, c := range s {
        sg.Add(1)

        //要传参数进去,避免只取到最后一个值
        go func(k byte) {
            p := make([]byte, len(s), len(s))
            p[0] = k
            permutaionImpl(p, s, 1)
            defer sg.Done()
        }(c)
    }

    sg.Wait()
}

并发3

如果能提前知道要开几个goroutine,那就可以不用递归的方式创建goroutine了,代码逻辑会更清晰易懂。

func permutaionCal(left chan []byte, right chan []byte, s []byte) {
    for v := range left {
        prefixIncrement(v, s, right)
    }
    close(right)
}

func PermutaionChain(s []byte) {
    leftmost := make(chan []byte)
    left := leftmost
    right := leftmost

    for i := 0; i < len(s)-1; i++ {
        right = make(chan []byte)
        go permutaionCal(left, right, s)
        left = right
    }

    go func(c chan []byte) {
        for _, v := range s {
            c <- []byte{v}
        }
        close(c)
    }(leftmost)

    for v := range right {
        fmt.Println(v)
    }
}

对比

benchmark 4个byte的全排列:
递归 5000 308919 ns/op 768 B/op 24 allocs/op
并发1 5000 382600 ns/op 1774 B/op 93 allocs/op
并发3 5000 310740 ns/op 1675 B/op 92 allocs/op

benchmark 7个byte的全排列:
递归 20 62281150 ns/op 161372 B/op 5040 allocs/op
并发1 20 71853908 ns/op 275384 B/op 18781 allocs/op
并发3 20 84848114 ns/op 275663 B/op 18780 allocs/op

递归版本最快,分配内存最少,应为中间结果都是写入一个前缀slice的,通过cur下标来标记slice的结尾。并发版本要在不同的goroutine之间传递slice,分配的内存相对多一些。在这个例子中,并发没有表现出比单线程更快,原因可能是每个goroutine的计算过程较短,不能充分发挥出多核的优势,反而将时间浪费在了goroutine之间的消息传递上。

代码地址

https://github.com/amither/go-slice/tree/master/permutation

相关文章

  • 几种计算全排列的方法

    基于golang实现,有非并发实现和并发实现 递归 全排列问题,比如打印1-9的共9个字母的全排列。先输出1,然后...

  • 46. Permutations

    算法 1: 递归数组 的全排列,等价于全排列与可能的取值组合得到。 算法 2: 计算一个排列 按字典升序排列的紧...

  • leetcode指北---DFS

    DFS,也就是深搜,实质就是枚举。如果题目问的是一共多少种方法,多少种排列...尽管用。 全排列问题: 全排列:给...

  • 01行列式

    一、行列式的基本概念 二阶行列式的计算规则三阶行列式的计算-主对角线-副对角线 二、全排列与逆序数 123,全排列...

  • 八皇后问题

    最近接触到八皇后问题,看了下其中一种思路还挺简单,就是全排列然后筛选出斜边也满足的排列方法。比较麻烦的是全排列,特...

  • 排列组合

    排列(n>=r) 对有n个元素的集合S中的其中r个元素进行排列(n >= r)可以用如下几种方法来理解: 排列描述...

  • python数独的几个核心计算函数

    1.生成A9排列矩阵 1到9,9个数字的全排列可以说是数独的基础,根据统计学可以计算出9个数字的全排列是36288...

  • 全排列与字典序

    全排列 递归实现全排列; 首先来说递归算法实现全排列: 例如,对于{1,2,3,4}的例子进行全排列,其可以分解...

  • python计算阶乘的方法

    循环计算 reduce 递归 python学习——计算阶乘的几种方法_geerniya的博客-CSDN博客_pyt...

  • 全排列

    求全排列最简单的就是递归了123 的全排列共有 6 个, 123 的全排列等于以 1 开头 23 的全排列, 加上...

网友评论

      本文标题:几种计算全排列的方法

      本文链接:https://www.haomeiwen.com/subject/ssgbgqtx.html