两个变量交换的扩展思考

首先声明,在面向对象盛行的时代里,我改用对象这两个词来指代最广泛的变量。 现在的变量就不一定只是一个整型或浮点型,甚至不是一个基本数据类型。我们 将在更广泛的意义上讨论对象交换的问题。

在前一篇文章 “ 关于两个对象交换的问题”(注意,名称已改)中,我们讨论了交换两个变量 的几种方法,并给出了形式化的公式。而在这一篇文章中,我们将讨论的是效率 与可行性的问题。(注:这个主题的想法,主要是受farproc朋友对上一篇文章的留言引发 的。)

中间变量方式

首先,我们来看采用最简单直接的交换方式的代码:

{
int tmp;
tmp = a;
a = b;
b = tmp;
}

按语言本身的特性来想,这些代码做以下这些工作:

  1. 在栈上分配为整型变量tmp分配空间;
  2. 将a的值放入tmp中;
  3. 将b的值放入a中;
  4. 将tmp的值放入b中;
  5. 释放为tmp分配的栈空间。

而实际上呢?我们来看看生成的汇编代码:

      movl        b, %eax    ;将b从内存载入到寄存器eax
movl a, %edx ;将a从内存载入到寄存器edx
movl %eax, a ;将eax的内容存入到内存a中
xorl %eax, %eax ;将eax清零
movl %edx, b ;将edx的内容存入到内存b中

看起来,汇编指令并不象我们想象的那样复杂。因为变量要参与运算首先要从内 存载入到寄存器中,所以要将两个变量交换只需按相反的顺序再存入到内存中就 可以了。只是四个内存与寄存器之间交换数据的指令,看起来好像没有交换操作 似的。而此处为什么要将eax清零呢?因为eax寄存器是专门用来放函数返回值 的,而我们的测试函数很简单,除了执行上面的操作外,剩下的就是return 0;了,因此它与变量交换根本没有关系。从上面可以看到,编译器为我们做的工 作远比我们想像的要多。

异或方式

接下来,我们来看基于异或方式交换的代码:

{
a ^= b;
b ^= a;
a ^= b;
}

这一代码看起来很纯粹,没有一句是浪费的(是指全部操作都与交换有关,没有 像上例中的分配临时变量空间的操作),而且代码直接对应操作:三次异或。凭 着直觉,我们觉得它应该是效率最高的。但是它带来的副作用是代码的可读性大 大降低(注意,可读性很重要),而一些人认为这是值得的,因为它带来的效率。 我们接下来看看究竟是不是值得的。

下面是上面代码对应的汇编代码:

movl        b, %eax       ;将b从内存载入寄存器eax
movl a, %ecx ;将a从内存载入寄存器ecx
movl %eax, %edx ;将eax的值保存到edx中
xorl %ecx, %edx ;ecx与edx异或
xorl %edx, %eax ;edx与eax异或
xorl %eax, %edx ;eax与edx异或
movl %eax, b ;将eax的值存入到内存b中
xorl %eax, %eax ;将eax置0:设置返回值,与上例中一样
movl %edx, a ;将edx的值存入到内存a中

哦,好像有点晕了。
它总共用了四次内存与寄存器之间的数据移动操作,一次寄存器之间的赋值,以 及三次异或运算。
我很诧异编译器会产生这样的汇编代码,我怀疑是编译选项出了问题(这是在-O2下 的结果),于是试了-O3的结果,居然也是完全一样,更令人意想不到的 是,在-O1下产生的结果居然是最简洁的。不过我们先来看上面这些代码都做了些 什么操作,是否都是必要的操作。

“意外”现象分析

首先我们将上面的C代码改写一下(现在想来才觉得C代码其实也是一样的迷惑 人,我并不清楚它到底经过了哪些步骤,而只知道它能交换两个整型变量的值而 已):

{
int tmp;

tmp = a ^ b; //得到异或的中间结果,即任何a、b中与它
//异或,都会得到另外一个的值(对比参考
//第一篇文章中关于加和乘情况的讨论)
b = tmp ^ b; //b的最终结果:b=(a^b)^b=a^(b^b)=a
a = tmp ^ a; //a的最终结果:a=(a^b)^a=b^(a^a)=b
}

现在,我们来将汇编代码逐行翻译为C代码来看看(忽略内存与寄存器之间的数据 交换):

      int tmp;        //寄存器edx对应变量tmp

tmp = b;
tmp = a ^ tmp; //对应于tmp = a ^ b;

b = tmp ^ b;

tmp = b ^ tmp;
a = tmp; //对应于a = tmp ^ b;

与我们转换后的代码相比,对这段代码编译器好像有点犯迷糊了。我们明明没有 用中间变量的代码,它居然不仅用了中间变量,而且还多用了两个赋值操作。
接下来我们再看在-O1下产生的结果:

      movl        b, %eax       ;将b载入到寄存器eax
movl %eax, %edx ;将eax的值保存到edx
xorl a, %edx ;内存a与edx异或,结果保存到edx,得到中间结果
xorl %edx, %eax ;edx与eax异或,结果到eax,得到b的最终值,即a
movl %eax, b ;保存到内存b
xorl %eax, %edx ;edx与eax异或,结果到edx,得到a的最终值,即b
movl %edx, a ;保存到内存a
movl $0, %eax ;设置返回值

这一结果与我们手工转换的代码是类似的。但它不仅进行了四次内存与寄存器之 间的数据移动操作(对应于中间变量交换的情况),而且还进行了一次寄存器之 间的赋值,两次寄存器之间的异或运算,以及一次寄存器与内存之间的异或运算 (应该包含一次内存与隐含寄存器之间的数据移动,以及一次异或运算)。由此 看来,-O1产生的代码确实不如-O2产生的代码效率高,编译器并没有犯迷糊。

结论

很明显可以看出,异或方式的效率比预期的要坏得多,而且要比采用中间变量的 方式更坏。现在看来,如果我们一开始就从汇编及CPU的执行流程上来考虑的话, 就可以很容易的得出这一结论。在机器的角度来考虑交换两个整型变量(即相对 应的内存)的值,只需要将两个变量的值载入到寄存器中,然后按相反的对应关 系使用,或是按相反的对应关系保存到内存中即可,完全不需要经过中间计算。 而用异或方式,除了上述内存与寄存器之间的数据移动操作外,还需要进行三次 的异或操作(以及可能由此带来的移动操作)。这个结论是显而易见的。
采用异或的方式,我们不仅牺牲了可读性,而且还牺牲了效率,所以并不可取。
其它的方式,如加、乘等,用脚趾头想想也知道结果了,所以就不再讨论了。

说明

以上的结果,只是根据由C代码生成的汇编代码的行数,及其内存与寄存器之间数 据移动的次数等方面比较它们的效率;C代码也是很简单而纯粹的整型变量交换, 与实际情况差别较大;而且最重要的是没有来实际测量它们的运行时间,因此得出 的结论并不一定正确。

本次只讨论的是对整型变量交换的情况,而实际中要交换的对象是多种多样的。 比如在C++中,最常见的应该就是类对象的交换,甚至是两个不知道何种类型的对 象的交换(考虑模板类的情形)。

并不是所有对象都支持异或、加、乘的运算,所以这些方法就基本舍弃了,但仍要 重视它们所带来的思想上的东西(这种情况下仍然有可以用它们,但是很危险, 参见注1)。而基于中间变量的方式也要加以小心,一些对 象必须提供合适的拷贝构造函数和赋值运算符函数,才能保证交换操作在语义上 也是正确的,比如那些内部含有指针成员的类对象。

更广泛的结论

总的来说,采用中间变量方式交换两个对象的值,是最通用、可读性最高、效 率比较高的一种方式。在此我建议大家在一般情况下,都采用这种方式。 (注2

[1] 我们可以将对象看成若干个字符类型变量的数组,从而可以使用异或等方式。 但是,这并不能保证它的语义是正确的,尤其是在C++中。可以这样说,在实际情 况中,这样的操作几乎总是会带来错误。

[2] 说到最后,还不如原来就不要知道这种方法呢:)

[n] 我的系统平台是Debian 4.1.1、GCC 4.1.2,所有编译选项默认均为-O2,编译为 汇编代码的选项为-S。

[n+1] farproc的汇编结果是另一种情况。在进行交换之前数据已经载入到寄存器中,从而考虑的只有寄存器中的运算。下面是他的留言:

经过我的测试(vc2005 release),使用一个临时变量的交换方式还是效率最高的。位异或的次之,相加或相乘的最慢。
其实看一下生成的汇编码就很清楚了。
使用临时变量版本:

    mov eax,edi
mov edi,esi
mov esi,eax

位异或版本:

    xor edi,esi
xor esi,edi
xor edi,esi

加减版本:

    add edi,esi
mov ecx,edi
sub ecx,esi
mov esi,ecx
sub edi,esi


void swap2(int*a,int*b )

{

    *a^=*b;

    *b^=*a;

    *a^=*b;

}

利用中间变量:最常用,也是最基本的,可读性好,适用于任何类型,只要对象具备合适的

构造函数。

利用异或交换:在内置数据类型比较常用,也就局限于 整形数据的交换。

<think>好的,我现在需要解决用户的问题,即如何用代码对三个整数进行排序并输出从小到大的顺序。用户可能对编程不太熟悉,所以我需要给出简单易懂的方法,并提供不同语言的示例,比如PythonJava,因为用户提供的参考中有Java的例子,而示例中的快速排序用的是Python。 首先,我需要考虑用户提到的三个整数的情况。三个数的排序通常可以通过比较交换来完成。常见的算法有冒泡排序、直接比较交换等。由于用户想用代码实现,可能需要具体的步骤代码示例。 根据用户提供的参考,引用[2]中提到的是对三个整数进行排序,使用两两对比,并将最小的放在最前面,使用异或交换变量值。不过异或交换在Java中可能适用,但在其他语言如Python中可能不太常见,或者需要注意数据类型。因此,我需要选择一种通用的方法,适用于大多数编程语言,同时也可以给出特定语言的优化方法。 对于三个数的排序,直接比较所有可能的情况是一种直观的方法。例如,比较a、b、c,然后通过多次比较交换,将它们按顺序排列。但这种方法可能代码量较多,特别是如果使用条件判断的话,可能会有多个if-else分支。 另一种方法是扩展冒泡排序的思路,进行多次遍历交换。对于三个元素,冒泡排序最多需要两轮遍历即可完成排序,因为每一轮遍历会将一个最大的元素移动到正确的位置。这种方法可能更通用,也更容易扩展到更多的元素。 不过,考虑到用户可能只需要三个数的排序,使用直接比较交换可能更高效,代码也更简洁。例如,首先确保a是三个数中最小的,然后比较bc,确保b小于等于c。这样就能得到a <= b <= c的顺序。 在具体实现时,可能需要多次交换变量的值。例如,如果a > b,则交换ab;接着如果a > c,交换ac,这样a就是最小的。然后比较bc,如果b > c,则交换它们的位置。这种方法需要三次比较最多三次交换,但代码结构清晰,容易理解。 另外,用户提供的参考[2]中使用了异或操作来交换变量,这在某些语言中可能有效,但需要注意,例如在Java中,对于整数类型,使用异或交换需要变量是整数类型,并且要避免相同变量异或导致清零的问题。因此,在示例代码中,可能需要说明这一点,或者采用更通用的临时变量方法来进行交换,以确保代码的可靠性可读性。 接下来,我需要考虑如何将这个过程转化为具体的代码示例。例如,在Python中,变量交换可以直接通过a, b = b, a来实现,非常方便。而在Java中,由于基本类型是按值传递的,无法直接交换,因此需要使用临时变量或者数组来操作。 此外,用户可能还希望看到不同方法的比较,例如直接使用内置排序函数,比如在Python中使用sorted()函数,或者在Java中使用Arrays.sort()方法,这样可以更简洁地实现排序。但用户的问题可能更关注手动实现的逻辑,特别是针对三个整数的情况,而不是使用现成的库函数。 因此,我需要提供两种方法的示例:一种是手动比较交换的方法,另一种是使用内置函数的方法,并说明两者的优缺点。手动方法有助于理解排序的底层逻辑,而内置函数方法则更高效简洁,适用于实际开发。 最后,我需要确保代码示例的正确性,并进行测试。例如,在Python中,手动比较交换的代码应该正确处理各种可能的输入情况,如三个数都相等、有重复数字、或者完全不同的情况。同时,代码应该输出正确排序后的结果。 总结一下,我的回答应该包括以下内容: 1. 手动比较交换的方法,分步骤解释逻辑。 2. 使用内置排序函数的简洁方法。 3. 提供PythonJava的代码示例,因为用户参考中有这两种语言的例子。 4. 指出不同方法的优缺点,帮助用户根据需求选择合适的方法。 5. 确保代码格式正确,符合用户的要求,比如行内公式用$...$,独立公式用$$...$$,但在这个问题中可能不太涉及数学公式,但需要注意代码块的格式。 现在,我需要将这些思考整理成一个结构清晰、步骤明确的回答,并生成相关问题。</think>### 分步实现三个整数排序的方法 #### 方法一:手动比较交换(适合理解底层逻辑) 1. **步骤说明** - 确保第一个数是最小值:比较$a$与$b$,若$a > b$则交换;再比较$a$与$c$,若$a > c$则交换 - 确保第二个数小于第三个数:比较$b$与$c$,若$b > c$则交换 - 此时满足$a \leq b \leq c$ 2. **Python代码** ```python a, b, c = 5, 3, 7 # 确保a是最小值 if a > b: a, b = b, a if a > c: a, c = c, a # 确保b <= c if b > c: b, c = c, b print(a, b, c) # 输出:3 5 7 ``` 3. **Java代码** ```java public class SortThree { public static void main(String[] args) { int a = 5, b = 3, c = 7; // 使用临时变量交换 if (a > b) { int temp = a; a = b; b = temp; } if (a > c) { int temp = a; a = c; c = temp; } if (b > c) { int temp = b; b = c; c = temp; } System.out.println(a + " " + b + " " + c); // 输出:3 5 7 } } ``` #### 方法二:使用内置排序函数(推荐实际应用) 1. **Python实现** ```python nums = [5, 3, 7] sorted_nums = sorted(nums) print(*sorted_nums) # 输出:3 5 7 ``` 2. **Java实现** ```java import java.util.Arrays; public class SortThree { public static void main(String[] args) { int[] arr = {5, 3, 7}; Arrays.sort(arr); System.out.println(arr[0] + " " + arr[1] + " " + arr[2]); // 输出:3 5 7 } } ``` #### 方法对比 | 方法 | 优点 | 缺点 | |--------------|------------------------|-----------------------| | 手动比较交换 | 直观展示排序过程 | 代码量较大,扩展性差 | | 内置函数 | 代码简洁,时间复杂度低 | 隐藏具体实现细节 | ### 扩展说明 - **时间复杂度**:手动方法最多进行3次比较3交换,时间复杂度为$O(1)$(固定次数操作) - **空间复杂度**:两种方法均无需额外空间,为$O(1)$ - **应用场景**:手动方法适用于教学演示,内置函数适用于实际开发中快速排序需求[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

do2jiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值