[通用技术]在不同语言中用协程实现全排列算法(C++/Lua/Python/C#)
我这里实现全排列的基本算法如下(C++):
1 #include <algorithm> 2 #include <iostream> 3 #include <vector> 4 5 void perm(std::vector<int>& v, int pos = 0) 6 { 7 if (pos == v.size()) { 8 for (int i = 0; i < v.size(); ++i) { 9 std::cout << v[i] << ',';10 }11 std::cout << std::endl;12 }13 for (int i = pos; i < v.size(); ++i) {14 std::swap(v[i], v[pos]);15 perm(v, pos + 1);16 std::swap(v[i], v[pos]);17 }18 }19 20 int main()21 {22 std::vector<int> v;23 for (int i = 1; i <= 4; ++i) v.push_back(i);24 perm(v);25 }
这是一种最简洁的算法,即,在每轮递归中,依次选择一项数据放在当前参考位置,然后移动参考位置,并进入更深一级递归。
这里的C++实现,简单,但是灵活性不够。上面的例子,对全排列的每个状态,只能够打印输出。当然,为perm函数添加一个函数指针,用不同的函数行为来替代打印操作,可以稍微提高灵活性。但,我这里所说的灵活性受限,主要是指,上面的算法只能一口气处理所有的全排列状态,不能中断这个过程。是的,你也可以为上面的perm函数提供一个容器,收集所有排列状态,待顶层perm函数返回后,再对搜集的结果进行进一步处理,但是,当排列状态过多时,搜集状态的容器会占用很大的内存。能不能把数组的所有排列状态看作一个集合,然后用迭代器去访问这整个集合?迭代器的灵活性能够满足我的需要,最好,迭代过程中,也没有数据堆积,不会出现内存占用太多的情况。
C++的STL中有符合要求的算法:std::next_permutation。
使用如下:
1 #include <algorithm> 2 #include <iostream> 3 #include <vector> 4 5 int main() 6 { 7 std::vector<int> v; 8 for (int i = 1; i <= 4; ++i) v.push_back(i); 9 do {10 for (int i = 0; i < v.size(); ++i) {11 std::cout << v[i] << ',';12 }13 std::cout << std::endl;14 } while (std::next_permutation(v.begin(), v.end()));15 }
next_permutation这个函数,能够满足我的需求。它灵活,快速,并且没有多余的内存占用。不过,我关注的焦点不在它,因为它采用了一种更复杂的算法。具体的算法实现,可以自行查看该函数的源码,另外,侯捷《STL源码剖析》一书中,对该算法的原理有清楚的解释。
我希望的是,算法如第一例般简明易懂,使用起来又有如迭代器般灵活。
为了这个目标,我想到协程。
C/C++的话,Windows下有CreateFiber等一组函数能够创建和切换协程。由于Lua中的协程和Windows的Fiber使用大同小异,这里我就直接转到Lua的协程-coroutine了。
1 function _perm(arr, pos) 2 pos = pos or 1 3 if pos == #arr then 4 coroutine.yield(arr) 5 end 6 for i = pos, #arr do 7 arr[i], arr[pos] = arr[pos], arr[i] 8 _perm(arr, pos + 1) 9 arr[i], arr[pos] = arr[pos], arr[i]10 end11 end12 13 function perm(arr)14 return coroutine.wrap(function()15 _perm(arr)16 end)17 end18 19 for i in perm({1, 2, 3, 4}) do20 table.foreach(i, function(_, v) io.write(v, ',') end)21 print()22 end
可以看到,上面的_perm函数和C++版的perm几乎相同,只是把打印数组的语句换成了coroutine.yield。
_perm实现了算法的主体部分,而perm则是将_perm包装成协程迭代器后返回给外部使用。这个实现,既保持了算法的清晰性,又得到了极高的灵活度。
当然,协程不是没有开销的,协程其实是另外开辟了一个栈的空间,在上面返回的coroutine迭代其销毁前,这份额外的栈内存一直存在。但是,肯定比收集所有状态再逐一访问的方案更优,因为,协程中的栈帧最多不过n层,而前者的内存占用是n!。
之前我还测试过,将STL中的next_permutation算法移植到Lua中,再和协程版本比效率,结果是协程版本输了,但两者的用时也较接近。毕竟协程是一种用空间/时间换清晰性,控制软件复杂度的方案,不应该对它的性能苛求太多。当然,只针对全排列这个问题,采用next_permutation算法是最好了。
本来,说过Lua的协程版全排列算法后,我这篇文章也该结束了,但事实上,由于在不同的语言中,协程的用法不尽相同,一些语言中的协程在使用上会有一些局限,因此,部分语言的协程在解决特定问题时,可能会遇到点麻烦。
我在很长的时间里,都没意识到,C#的yield return其实是协程设施,直到我最近学Python时,看到有人指出,Python的协程就是send和yield,我才恍然大悟。
在需要自动生成迭代器时,yield这货,它表现得极为犀利,也因此,各种语言入门书,但凡讲yield,都用迭代器的生成作为示例。事实上,yield在实现类似Linq to object这样的多级延迟迭代的函数库时,确实能够发挥及其强大的威力。
和Lua/Win-API版本的协程相比,C#/Python中的协程更为高效,因为它的实现并没有借助栈,内存占用更少,但是,也因此,后者的协程在用法上有了一个限制:yield应该放在同一个函数的函数体中,即是说,在Python中,不能在外层函数中yield后,又简单的进入内层函数再yield。这是因为,外层函数对内层函数的调用,并不会执行内层函数函数体的代码,这个调用的结果,只是一个表达式,一个迭代器对象。
这是一段Lua代码:
1 function bar() 2 coroutine.yield(2) 3 end 4 5 function foo() 6 coroutine.yield(1) 7 bar() 8 coroutine.yield(3) 9 end10 11 for i in coroutine.wrap(foo) do12 print(i)13 end
它输出的是1,2,3.
这是一段Python代码:
1 def bar(): 2 yield 2 3 4 def foo(): 5 yield 1 6 bar() 7 yield 3 8 9 for i in foo():10 print i
它输出1,3.
因为二者的协程实现方案是不同的!Python中的bar()调用,只会返回一个迭代器,而不会执行任何bar函数体中的语句!
其实说到这里,结论已经很明显了:在C#/Python中,如果需要跨多个函数帧来yield的话,不能简单的调用含yield的内层函数,而是应该代之以这种语句:
1 for i in foo(): yield i
这里的foo就是含yield的内层函数。不管foo内部的yield语句后面有没有表达式,即,不管上面的i是否为None,都应该在含yield的外层函数中,将foo()调用替换乘上面的语句,这样才能榨干foo,执行foo中应该被执行的每条语句。
在发现C#的yield return就是协程之前,我一度认为yield是不能用于解决全排列这个问题的,直到我认真思考,得出上面这个结论之后。这样看来,yield这种协程方案,功能是和Lua的coroutine等效的了。
这是Python的全排列实现:
1 def perm(arr, pos = 0): 2 if pos == len(arr): 3 yield arr 4 5 for i in range(pos, len(arr)): 6 arr[pos], arr[i] = arr[i], arr[pos] 7 for _ in perm(arr, pos + 1): yield _ 8 arr[pos], arr[i] = arr[i], arr[pos] 9 10 for i in perm([1,2,3,4]):11 print i
当然,C#也一样:
1 static void Swap<T>(ref T a, ref T b) 2 { 3 T t = a; 4 a = b; 5 b = t; 6 } 7 8 static IEnumerable<int[]> Perm(int[] arr, int pos) 9 {10 if (pos == arr.Length)11 {12 yield return arr;13 }14 for (int i = pos; i < arr.Length; ++i)15 {16 Swap(ref arr[i], ref arr[pos]);17 foreach (var j in Perm(arr, pos + 1)) yield return j;18 Swap(ref arr[i], ref arr[pos]);19 }20 }21 22 static void Main(string[] args)23 {24 foreach (var i in Perm(new int[] { 1, 2, 3, 4 }, 0))25 {26 Console.WriteLine(string.Join(",", i.Select(j=>j.ToString()).ToArray()));27 }28 }
尽管有不能利用多核心充分发挥硬件性能这个缺陷,但还是不得不说,协程实在是用线性代码实现异步逻辑之居家旅行杀人越货的必备神器啊。
转载于:https://www.cnblogs.com/cbscan/archive/2012/01/17/2325095.html
[通用技术]在不同语言中用协程实现全排列算法(C++/Lua/Python/C#)相关推荐
- go 怎么等待所有的协程完成_GO语言基础进阶教程:Go语言的协程——Goroutine
Go语言的协程--Goroutine 进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程) 进程进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为&qu ...
- Go语言中协程的概念和基本使用
为什么80%的码农都做不了架构师?>>> Go协程(Goroutine)是与其他函数同时运行的函数.可以认为Go协程是轻量级的线程.与创建线程相比,创建Go协程的成本很小.因此 ...
- 浅尝Go语言的协程实现
文章目录 为什么需要协程 协程的本质 协程如何在线程中执行 GMP调度模型 协程并发 为什么需要协程 协程的本质是将一段数据的运行状态进行打包,可以在线程之间调度,所以协程就是在单线程的环境下实现的应 ...
- C语言关于协程的探究
协程是一种用户级的轻量级线程,它可以在单线程里多个函数并发地执行协程,可以在主任务进行的同时,执行一些分支任务,以协助程序达到最终的效果,我们可以将协程成为用户态线程,但它与线程又有所区别. 协程和线 ...
- C语言中协程(coroutine)实现
C语言协程库实现说明 代码实现 1. 当前支持的功能概览 1.1 创建任意数量协程并在协程中yield #include <stdio.h> #include <stdlib.h&g ...
- Go语言入门到实战——14.Go语言的协程机制以及并发机制
Go语言入门到实战--00主目录 在上一讲中我们学习了Go语言的包的知识已经依赖管理. 协程(groutine)是一种更加轻量级的线程(thread). 一.协程与线程得到比较 1.对于java而言, ...
- C语言实现协程----初探
协程定义 协程,从编程的角度看来,可以理解为用户自己能控制和调度的线程,可以理解为用户级别的线程.一个线程可以有多个协程,一个进程也可以有多个协程. 协程实现 协程的实现有多种方式 setjmp和lo ...
- go语言ants协程池
参考链接:大数据Ants go ants学习 比如多文件处理,常用 package patternhandleimport ("fmt""sync"" ...
- python协程异步原理_简单介绍Python的Tornado框架中的协程异步实现原理
Tornado 4.0 已经发布了很长一段时间了, 新版本广泛的应用了协程(Future)特性. 我们目前已经将 Tornado 升级到最新版本, 而且也大量的使用协程特性. 很长时间没有更新博客, ...
最新文章
- DIY自己的AI助理,萝莉御姐暖男霸道总裁全凭你定义,微软小冰团队发布新框架...
- 【Java报错】mapper传入array\collection\list类型的参数时报BindingException:Parameter `` not found(问题复现+3种解决方法)
- EL toString()功能,对象转换字符串
- 牛客网 【每日一题】5月21日题目 图的遍历
- 大学物理实验电学基本参数的测量实验报告_大学物理电学实验报告
- linux内核head.S文件分析
- 第1关:HDFS的基本操作
- 如何建立一个利于SEO的网站
- 哪些NPM仓库更易遭供应链攻击?研究员给出了预测指标
- python_sorted()详解
- Yandex浏览器 - 可以在手机上安装chrome插件的浏览器
- 2021-05-05 数组、 元组、字典、字符串常见操作
- P2071 座位安排(二分图最大匹配)
- 创业者觉得苦逼得真正原因
- 【选型指南】频谱分析仪配件 衰 减 器 选型的3个重要参数
- 3.完成ODS层数据采集操作
- eutran和utran区别_EUTRAN接口与功能.ppt
- 华瑞IT教育|校园成人礼满满的青春味
- 入侵报警的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
- AI算法工程师 | 02人工智能基础-Python基础(二)语言特性_控制语句_切片_数据类型