不得不感叹,code确实是一门艺术,即便你学的再多,写的再多,对于最简单操作可能还只是仅仅"完成了这个功能"而已,怎样高效,优美的完成确着实需要一番造诣.比如怎样求两个int类型的平均数并返回,今天再将最近看到的几种交换数据的方法总结一下,希望能从中略微领略一下程序的艺术.
今天要说的任务很简单,是每个学习c/c++初学者第一个遇到的问题:交换两个数据.
严格期间,我们将条件阐述清楚:给定两个int型变量a,b,交换a,b的内容.
下面就来一步一步的用越发优美的方法来解决这个问题.
首先来说一个错误的做法,这个做法是很多初学者想到的
a=b;
b=a;
"既然是a,b交换,那就把b给a,同时把a给b不久行了吗?"
问题就在于计算机无法"同时"来完成这两条语句,程序必须确定一个先后执行的顺序才可以,所以上面这段代码执行的结果就是a,b里面都是原来b变量的内容.
如果觉得不好理解话,就拿两个杯子,红杯子里面是雪碧,蓝杯子里面是可乐,现在想让红杯子里面装可乐,蓝杯子里面装雪碧,你总不会把可乐从蓝杯子倒进红杯子,同时又能吧雪碧从红杯子倒进蓝杯子里吧?
或许对于刚才这个问题,你马上就会想到解决方法:借助第三个杯子,首先把雪碧倒进第三个杯子,然后把可乐倒进红杯子(这时已经空了),最后再把雪碧从第三个杯子倒到蓝杯子里.
同样的思路可以放到程序里,杯子在这就可以看作是内存,杯子中的内容就是内存里的数据,所以要完成交换操作,我们需要引入第三个变量,设为c,类比刚才倒饮料的过程,我们可以得到如下代码:
c=a;
a=b;
b=c;
如此就能成功完成交换两个数的任务了.
问题来了,既然数据交换前后都只需要两个变量,我在交换的时候却需要引入第三个变量,那这个变量到底是不是必须的呢?有没有方法不使用新变量就完成交换的任务呢?
居然是可以的,答案貌似不可思议,但注意到我们刚才的思路里将a,b独立来看的,整个操作过程中也没有新的数产生.不过用下面的方法:
a=a+b;
b=a-b;
a=a-b;
确实可以完成交换的操作.原因就是我们虽然没引入第三个变量,却引入了第三个数据(a+b),这样我们只需要另外a,b其中的一个数据就可以确定另外一个了.这就是他的巧妙之处,用两个变量再借助运算就完成了三个变量的存储工作,这样交换也就不在话下了.
这种方法比较新奇了,细究他的可行性可以知道关键在于+,-是一对可逆的运算,既然这样,我们也可以用另外的可逆运算来取代+,-,比如我们可以用乘除,幂和开方等等.只是他们的准确性和速度和加减是没法比较的.
然而,还有一个很神奇的运算可以完成上述功能-异或,异或的逆运算不是别的,正是异或本身,所以上面的代码可以这样来改写
a=a^b;
b=a^b;
a=a^b;
到了这一步,交换这个问题从数学的角度已经找到很优美的方法了,那么从计算机的角度来看,这种方法同第一个申请变量的方法(称为方法A)效率上有多少差别呢?
方法A经汇编后代码如下(VC6)
10: c = a;
0040186A mov eax,dword ptr [ebp-4]
0040186D mov dword ptr [ebp-0Ch],eax
11: a = b;
00401870 mov ecx,dword ptr [ebp-8]
00401873 mov dword ptr [ebp-4],ecx
12: b = c;
00401876 mov edx,dword ptr [ebp-0Ch]
00401879 mov dword ptr [ebp-8],edx
一共有6步,涉及到六次访存操作.
后面这种方法汇编的代码如下:
10: a = a^b;
0040186A mov eax,dword ptr [ebp-4]
0040186D xor eax,dword ptr [ebp-8]
00401870 mov dword ptr [ebp-4],eax
11: b = a^b;
00401873 mov ecx,dword ptr [ebp-4]
00401876 xor ecx,dword ptr [ebp-8]
00401879 mov dword ptr [ebp-8],ecx
12: a = a^b;
0040187C mov edx,dword ptr [ebp-4]
0040187F xor edx,dword ptr [ebp-8]
00401882 mov dword ptr [ebp-4],edx
一共有9步,并且有9次访存操作,这样来看,后面一种方法虽然节省了一个内存,但是时间开销却比第一种方法高50%.
时间与空间的矛盾在程序中是永远存在的,想找到一个时间空间双优化的算法是极难的事~
不过我们继续接着这个时间效率不怎么高的方法说下去,因为在这种方法中还没完全发挥c/c++的威力
考虑到=运算符的返回值是右操作数的内容,所以上述式子可以写成如下形式
a=a^(b=b^(a=a^b));
什么?看起来很乱?那就再请^=出场,上述式子改写成
a^=b^=a^=b;
记住了,上面这个式子可是完成的交换a,b数据的工作,非常的对称和优美,不过在效率上并没有任何改进.仅仅在形式上的艺术而已,你可以写在你的代码中作为一种炫耀~~
如果我们硬是蛮不讲理的要求时间空间双丰收,希望时间上更优,并且也不要那个多余的第三者,有没有办法呢?
对于高级语言,能做的恐怕只有这么多了,想要达到上述目标,只有祭出汇编语言这一利剑了:
首先铺垫一下,在c/c++中嵌入汇编的方法是
_asm
{
......
}
这样,交换的工作可以这样来完成
_asm
{
mov eax,a
mov ebx,b
mov a,ebx
mov b,eax
}
因为x86在一条语句中最多支持一个访存操作,a,b的交换各需要一次读和一次写操作,因此最少需要四次访存操作,如此,上面这段代码已经没有任何多余的时间和空间了.
但是这个方法还不完美!
软肋就是它的稳定性,在这段代码中我们使用了eax和ebx两个寄存器,可天知道在你代码的上下文里这两个寄存器是否已经有了其他的作用,如果因此这个操作误改了里面的数据,会直接导致你程序错误!
因此为了让这段代码稳定工作,你需要事先将eax和ebx的内容存到存储器里,事后在覆盖回去,这样一来又多了4次访存操作.时间上的优势再次消失了......
ok,至此,c/c++中对两个数的交换方法讨论完毕,我们一共得到三个可行的方法
方法A:
c=a;
a=b;
b=c;
方法B:
a^=b^=a^=b;
方法C:
_asm //需要补充eax和ebx的暂存操作
{
mov eax,a
mov ebx,b
mov a,ebx
mov b,eax
}
上述方法都是可行的,按照时间效率来进行比较有 A>C>B,按照空间效率进行比较有B=C>A,这里有一个例外,就是在能够确定寄存器使用情况下,不需要补充eax和ebx暂存操作的方法C是一个时间空间都有优势的方法~那什么情况下能够确定寄存器的使用情况呢?可以将交换操作单独写成一个函数,这样就能防止额外的寄存器使用了(需要加上函数调用的额外开销).
当然,在现代的计算机编程中,内存的使用量都是极大的,多4个B完全可以忽略,倒是时间的考量越发严格,假如在一个需要进行大量交换操作,同时又需要保证稳定性的程序中,方法A却是最好的选择.
没想到讨论到最后,看起来最朴实的却是最优秀的~