交换两个变量值而不使用第三个变量


104

采访中提出的非常棘手的问题之一。

交换两个变量的值,例如a=10b=15

通常要交换两个变量值,我们需要第三个变量,例如:

temp=a;
a=b;
b=temp;

现在的要求是,不使用第三变量就交换两个变量的值。


76
哇。选择不当的面试问题恕我直言。这是一种在实践中很少有用的技术。它很有可能只会混淆编译器的优化器,从而导致代码效率不如“临时交换”。除非您在此采访的这个地方涉及大量数学方面的工作(例如:加密算法开发之类),否则我无法想象有任何充分的理由提出这样的问题。
Dan Moulding)2009年

10
确实,除了考生是否知道这个在生产代码中几乎没有用的特殊技巧之外,这个问题没有告诉您其他任何事情。我想您可能会遇到偶尔遇到的向导,他们会很快找到答案,但是像这样的人还不知道这个技巧,这很罕见。
ceo

2
也许他们想淘汰那些认为知道这些技巧才是好的程序员的人?另外,在阅读有关“异或”技巧时,请注意它何时会失败(IMO使它对于通用整数交换几乎完全无用)。
UncleBens

Answers:


155

使用异或交换算法

void xorSwap (int* x, int* y) {
    if (x != y) { //ensure that memory locations are different
       *x ^= *y;
       *y ^= *x;
       *x ^= *y;
    }
}


为什么要测试?

该测试是为了确保x和y具有不同的存储位置(而不是不同的值)。这是因为(p xor p) = 0,如果x和y共享相同的内存位置,则将1设置为0时,它们都将设置为0。当* x和* y都为0时,* x和* y上的所有其他xor运算将相等0(因为它们相同),这意味着该函数会将* x和* y都设置为0。

如果它们具有相同的值但存储位置不同,则一切正常

*x = 0011
*y = 0011
//Note, x and y do not share an address. x != y

*x = *x xor *y  //*x = 0011 xor 0011
//So *x is 0000

*y = *x xor *y  //*y = 0000 xor 0011
//So *y is 0011

*x = *x xor *y  //*x = 0000 xor 0011
//So *x is 0011


应该使用吗?

在一般情况下,不会。编译器将优化临时变量,并且鉴于交换是一个常见过程,它应为您的平台输出最佳机器代码。

以这个用C语言编写的快速测试程序为例。

#include <stdlib.h>
#include <math.h>

#define USE_XOR 

void xorSwap(int* x, int *y){
    if ( x != y ){
        *x ^= *y;
        *y ^= *x;
        *x ^= *y;
    }
}

void tempSwap(int* x, int* y){
    int t;
    t = *y;
    *y = *x;
    *x = t;
}


int main(int argc, char* argv[]){
    int x = 4;
    int y = 5;
    int z = pow(2,28); 
    while ( z-- ){
#       ifdef USE_XOR
            xorSwap(&x,&y);
#       else
            tempSwap(&x, &y);
#       endif
    }
    return x + y;    
}

编译使用:

gcc -Os main.c -o swap

xor版本需要

real    0m2.068s
user    0m2.048s
sys  0m0.000s

具有临时变量的版本的位置为:

real    0m0.543s
user    0m0.540s
sys  0m0.000s

1
可能值得解释测试的原因(即,如果x和y引用相同的对象,则三重xor方法将严重失败)。
dmckee ---前主持人小猫,

1
我不知道当代码被破坏时,如何获得如此多的支持。被测试的代码段中的两个交换都是完全错误的。仔细看
SoapBox

@SoapBox:非常好!XOR交换使x零且y保持不变,而临时交换使x的值均不变。哎呀!
德鲁·霍尔

4
@SoapBox好抓住。已修复并重新测试,我在时间上没有任何重大差异。
Yacoby

4
发问者说两个变量,而不是整数。:P
Plumenator 2010年

93

一般形式为:

A = A operation B
B = A inverse-operation B
A = A inverse-operation B 

但是,您必须潜在地注意溢出,并且并非所有操作都具有针对该操作定义的所有值正确定义的逆函数。例如*和/直到A或B为0

xor特别令人愉悦,因为它为所有int定义并且是它自己的逆


XOR(X,X)== 0,因此当要交换的两个值相等时,xor适用于int类型。我不是第一个指出这一点的人,有太多人(在这里和其他地方)都说过要单挑任何人以表示赞赏。
AlanK

85
a = a + b
b = a - b // b = a
a = a - b

16
如果a+b溢出怎么办?
Alok Singhal 2010年

2
@Alok:那不是采取考虑,因此是不切实际:)
多尔

4
如果a和b具有相同的基本大小的整数类型(如intunsigned short...),即使在发生溢出的情况下,它最终仍会起作用,因为如果a + b溢出,则a-b将下溢。使用这些基本的整数类型,值仅会翻转。
Patrick Johnmeyer

10
在C语言中,在unsigned整数类型上使用此命令是可以的,并且始终有效。对于带符号的类型,它将在溢出时调用未定义的行为。
jpalecek'4

1
@Shahbaz:即使有符号整数存储在2的补码中,溢出的行为仍然不确定。
基思·汤普森

79

没有人建议使用std::swap

std::swap(a, b);

不使用任何临时变量,具体取决于的类型ab实现可能也没有具体化。应该在知道“技巧”是否合适的情况下编写实现。试图进行第二次猜测没有任何意义。

更笼统地说,我可能想做这样的事情,因为它将对使ADL尽可能找到更好的重载的类类型起作用。

using std::swap;
swap(a, b);

当然,访调员对此回答的反应可能说明了很多空缺。


当然在C ++ 0x交换中将使用右值引用,因此会更好!
jk。

16
每个人都想炫耀他们学到的闪亮的异或技巧,以及关于如何使用它的各种警告,但这无疑是最好的答案。

2
为什么不std :: swap(a,b); ?我的意思是为什么要使用?
Dinaiz

2
不带@的std::用户swap定义类型的用户定义实现的@Dinaiz 也可以调用(这意味着“它将对类类型起作用,使ADL尽可能找到更好的重载”)。
凯尔·斯特兰德

std::swap在实现中使用额外的临时内存吗?cplusplus.com/reference/utility/swap
Ninja

19

正如manu已经指出的那样,XOR算法是一种流行的算法,适用于所有整数值(包括指针,然后包括运气和强制转换)。为了完整起见,我想提一下另一个功能较差的加/减算法:

A = A + B
B = A - B
A = A - B

在这里,您必须小心上溢/下溢,但否则它也可以正常工作。您甚至可以在floats / doubles上尝试这样做,以防在它们上不允许XOR。


所有整数值(然后包括指针) ”-什么?不,指针不是整数,并且您不能对指针值进行异或操作。您也不能对浮点值进行异或。但是加法/减法在上溢或下溢时具有不确定的行为(对于带符号或浮点类型),可能会丢失浮点的精度,并且无法应用于指针或其他非数字类型。
基思·汤普森

@KeithThompson-好的,浮点精度的损失是真的,但是根据情况,这是可以接受的。至于指针-好吧,我从未听说过编译器/平台无法将指针自由转换为整数并再次返回(当然要小心选择正确大小的整数)。但是,我不是C / C ++专家。我知道这违反了标准,但是我给人的印象是,这种行为在所有方面都非常一致。
Vilx-

1
@ Vilx-:为什么使用临时变量很容易避免精度损失,或者std::swap()为什么?当然,您可以指针转换为整数,然后再次返回(假设存在足够大的整数类型,但不能保证),但这不是您所建议的。您暗示指针整数,而不是它们可以转换为整数。是的,接受的答案不适用于指针。查尔斯·贝利的回答使用std::swap真的是唯一正确的。
基思·汤普森

1
首先,问题是“如何在不使用第三个变量的情况下进行交换”,而给出的原因是“面试问题”。我只是给了另一个可能的答案。其次,好的,我很抱歉暗示指针是整数。我更新了答案,希望更加明确和正确。如果我还是错的话,请纠正我(或自己更新答案)。第三,我删除了有关已接受答案的部分。它不在任何地方使用指针到整数的强制转换,并且可以正常工作(据我所知)。
Vilx-

@KeithThompson:这个问题可以修改为关于如何std::swap在不使用第三个变量的情况下实现。
jxh

10

愚蠢的问题应有适当的答案:

void sw2ap(int& a, int& b) {
  register int temp = a; // !
  a = b;
  b = temp;
}

register关键字的唯一好用法。


1
用存储类寄存器声明的对象不是“变量”吗?同样,我也不认为这是寄存器的一种好用法,因为如果您的编译器无法对其进行优化,那么即使尝试也有什么意义,您应该接受所得到的垃圾,或者自己编写程序集;-)但是你说,一个狡猾的问题值得一个狡猾的答案。
史蒂夫·杰索普

1
几乎所有现代编译器都忽略了寄存器存储类,因为它们比您更容易了解经常访问的内容。
ceo

6
请注意,此答案仅适用于采访者,而不适用于编译器。特别是利用这样一种事实,即询问此类问题的面试官并不真正理解C ++。因此,他们不能拒绝这个答案。(并且没有标准答案; ISO C ++谈论对象而不是变量)。
MSalters,2009年

啊,我明白了。我在C标准中一直在寻找将变量作为名词提及,而不仅仅是非常量含义。我在n1124的for循环部分中找到了一个,该循环定义了初始化程序中声明的“变量”的范围。然后我意识到问题是C ++,并且没有费心寻找C ++是否在任何地方都做了相同的错字。
史蒂夫·杰索普

3

使用第三个变量交换两个数字是这样的,

int temp;
int a=10;
int b=20;
temp = a;
a = b;
b = temp;
printf ("Value of a", %a);
printf ("Value of b", %b);

交换两个数字而不使用第三个变量

int a = 10;
int b = 20;
a = a+b;
b = a-b;
a = a-b;
printf ("value of a=", %a);
printf ("value of b=", %b);

2
#include<iostream.h>
#include<conio.h>
void main()
{
int a,b;
clrscr();
cout<<"\n==========Vikas==========";
cout<<"\n\nEnter the two no=:";
cin>>a>>b;
cout<<"\na"<<a<<"\nb"<<b;
a=a+b;
b=a-b;
a=a-b;

cout<<"\n\na="<<a<<"\nb="<<b;
getch();
}

1
cout << "Enter the two no=:"我期望阅读之后cout << "Now enter the two no in reverse order:"
user253751

与其他几个答案一样,如果a+ba-b溢出,则具有未定义的行为。此外,它void main()是无效的并且<conio.h>是非标准的。
基思·汤普森

2

由于原始解决方案是:

temp = x; y = x; x = temp;

您可以使用以下方法使其成为两层衬板:

temp = x; y = y + temp -(x=y);

然后使用以下命令使其成为一个衬板:

x = x + y -(y=x);

1
未定义的行为。y在没有中间序列点的情况下以相同的表达式进行读写。
基思·汤普森

1

如果稍微改变一下要询问2个汇编寄存器而不是变量的问题,则还可以将xchg操作用作一个选项,将堆栈操作用作另一个选项。


1
xchg您指的是什么操作?该问题未指定CPU架构。
基思·汤普森

1

考虑a=10b=15

使用加减法

a = a + b //a=25
b = a - b //b=10
a = a - b //a=15

使用除法和乘法

a = a * b //a=150
b = a / b //b=10
a = a / b //a=15

1
溢出时有未定义的行为。
基思·汤普森

1
同样,在C ++中,当值不是同一类型时,它可能具有不良行为。例如a=10b=1.5
马修·科尔

1
#include <iostream>
using namespace std;
int main(void)
{   
 int a,b;
 cout<<"Enter a integer" <<endl;
 cin>>a;
 cout<<"\n Enter b integer"<<endl;
 cin>>b;

  a = a^b;
  b = a^b;
  a = a^b;

  cout<<" a= "<<a <<"   b="<<b<<endl;
  return 0;
}

更新:在这种情况下,我们从用户那里输入两个整数。然后,我们使用按位XOR操作来交换它们。

假设我们有两个整数a=4b=9,然后:

a=a^b --> 13=4^9 
b=a^b --> 4=13^9 
a=a^b --> 9=13^9

请为您的答案添加简短的说明,以供将来的访问者使用。
Nikolay Mihaylov

1
在此,我们从用户那里输入两个整数。然后我们使用按位异或运算将它们交换两次。假设我们有两个整数a = 4和b = 9,那么现在a = a ^ b-> 13 = 4 ^ 9 b = a ^ b-> 4 = 13 ^ 9 a = a ^ b-> 9 = 13 ^ 9
Naeem Ul Hassan'8

1

这是另一种解决方案,但有一个风险。

码:

#include <iostream>
#include <conio.h>
void main()
{

int a =10 , b =45;
*(&a+1 ) = a;
a =b;
b =*(&a +1);
}

位置a + 1处的任何值都将被覆盖。


2
这是一个有用的答案,也许价值消失了。
Bibi Tahira

1
几种不确定的行为。*(&a+1)可能是bvoid main()是无效的。<conio.h>是非标准的(您甚至不使用它)。
基思·汤普森

该代码已与其他输出参数一起进行了测试,因为我提到了风险,因此将其排除在外。覆盖内存的风险..在那里。.但是,在不涉及第三个变量的情况下交换两个变量非常有用。
DareDevil '16

最糟糕的部分是它实际上可能会工作一段时间,因此您可能不会立即意识到它已完全损坏,并且在优化,其他编译器等情况下会失败
。– avl_sweden

1

当然,C ++答案应该是 std::swap

但是,以下实现中也没有第三个变量swap

template <typename T>
void swap (T &a, T &b) {
    std::pair<T &, T &>(a, b) = std::make_pair(b, a);
}

或者,作为单线:

std::make_pair(std::ref(a), std::ref(b)) = std::make_pair(b, a);

0
#include <stdio.h>

int main()
{
    int a, b;
    printf("Enter A :");
    scanf("%d",&a);
    printf("Enter B :");
    scanf("%d",&b);
    a ^= b;
    b ^= a;
    a ^= b;
    printf("\nValue of A=%d B=%d ",a,b);
    return 1;
}

0

那是正确的XOR交换算法

void xorSwap (int* x, int* y) {
   if (x != y) { //ensure that memory locations are different
      if (*x != *y) { //ensure that values are different
         *x ^= *y;
         *y ^= *x;
         *x ^= *y;
      }
   }
}

您必须确保内存位置不同,并且实际值也不同,因为A XOR A = 0


您不必确保值是不同的。
user253751

0

您可以...以简单的方式...在一行逻辑内

#include <stdio.h>

int main()
{
    int a, b;
    printf("Enter A :");
    scanf("%d",&a);
    printf("Enter B :");
    scanf("%d",&b);
    int a = 1,b = 2;
    a=a^b^(b=a);
    printf("\nValue of A=%d B=%d ",a,b);

    return 1;
}

要么

#include <stdio.h>

int main()
{
    int a, b;
    printf("Enter A :");
    scanf("%d",&a);
    printf("Enter B :");
    scanf("%d",&b);
    int a = 1,b = 2;
    a=a+b-(b=a);
    printf("\nValue of A=%d B=%d ",a,b);

    return 1;
}

1
未定义的行为。在这两个版本中,b都是在同一表达式中读取和修改的,没有中间的序列点。
基思·汤普森


0

最好的答案是使用XOR,并且在一行中使用它会很酷。

    (x ^= y), (y ^= x), (x ^= y);

x,y是变量,并且它们之间的逗号引入了序列点,因此它不依赖于编译器。干杯!


0

让我们看一个简单的c示例,它不使用第三个变量就交换两个数字。

程序1:

#include<stdio.h>
#include<conio.h>
main()
{
int a=10, b=20;
clrscr();
printf("Before swap a=%d b=%d",a,b);
a=a+b;//a=30 (10+20)
b=a-b;//b=10 (30-20)
a=a-b;//a=20 (30-10)
printf("\nAfter swap a=%d b=%d",a,b);
getch();
}

输出:

交换之前a = 10 b = 20交换之后a = 20 b = 10

程序2:使用*和/

让我们看另一个示例,使用*和/交换两个数字。

#include<stdio.h>
#include<conio.h>
main()
{
int a=10, b=20;
clrscr();
printf("Before swap a=%d b=%d",a,b);
a=a*b;//a=200 (10*20)
b=a/b;//b=10 (200/20)
a=a/b;//a=20 (200/10)
printf("\nAfter swap a=%d b=%d",a,b);
getch();
}

输出:

交换之前a = 10 b = 20交换之后a = 20 b = 10

程序3:使用按位XOR运算符:

按位XOR运算符可用于交换两个变量。两个数字x和y的XOR返回一个数字,其中x和y的位不同时,所有位都为1。例如,10(在Binary 1010中)和5(在Binary 0101中)的XOR为1111,而7(0111)和5(0101)的XOR为(0010)。

#include <stdio.h>
int main()
{
 int x = 10, y = 5;
 // Code to swap 'x' (1010) and 'y' (0101)
 x = x ^ y;  // x now becomes 15 (1111)
 y = x ^ y;  // y becomes 10 (1010)
 x = x ^ y;  // x becomes 5 (0101)
 printf("After Swapping: x = %d, y = %d", x, y);
 return 0;

输出:

交换后:x = 5,y = 10

计划4:

还没有人建议使用std :: swap。

std::swap(a, b);

我不使用任何临时变量,并且根据a和b的类型,实现的专业化可能不会。应该在知道“技巧”是否合适的情况下编写实现。

上述方法存在的问题:

1)如果一个数字为0,则乘积和除法将不起作用,因为乘积变为0,而与另一个数字无关。

2)两种算术解决方案都可能导致算术溢出。如果x和y太大,则加法和乘法可能超出整数范围。

3)当我们使用指向变量的指针并进行函数交换时,当两个指针都指向同一个变量时,上述所有方法都会失败。让我们看看如果两种情况都指向同一个变量,将会发生什么情况。

//基于位异或的方法

x = x ^ x; // x becomes 0
x = x ^ x; // x remains 0
x = x ^ x; // x remains 0

//基于算术的方法

x = x + x; // x becomes 2x
x = x  x; // x becomes 0
x = x  x; // x remains 0

让我们看下面的程序。

#include <stdio.h>
void swap(int *xp, int *yp)
{
    *xp = *xp ^ *yp;
    *yp = *xp ^ *yp;
    *xp = *xp ^ *yp;
}

int main()
{
  int x = 10;
  swap(&x, &x);
  printf("After swap(&x, &x): x = %d", x);
  return 0;
}

输出

交换之后(&x,&x):x = 0

在许多标准算法中可能需要将变量本身交换。例如,请参见QuickSort的此实现,我们可以在其中交换变量。通过在交换之前设置条件可以避免上述问题。

#include <stdio.h>
void swap(int *xp, int *yp)
{
    if (xp == yp) // Check if the two addresses are same
      return;
    *xp = *xp + *yp;
    *yp = *xp - *yp;
    *xp = *xp - *yp;
}
int main()
{
  int x = 10;
  swap(&x, &x);
  printf("After swap(&x, &x): x = %d", x);
  return 0;
}

输出

交换后(&x,&x):x = 10


0

也许不在主题之列,但是如果您知道要在两个不同的值之间交换单个变量,则可以执行数组逻辑。每次运行此代码行,它将在1到2之间交换值。

n = [2, 1][n - 1]

0

您可以这样做:

std::tie(x, y) = std::make_pair(y, x);

或交换两个以上变量时使用make_tuple:

std::tie(x, y, z) = std::make_tuple(y, z, x);

但是我不确定内部std :: tie是否使用临时变量!


0

在javascript中:

function swapInPlace(obj) {
    obj.x ^= obj.y
    obj.y ^= obj.x
    obj.x ^= obj.y
}

function swap(obj) {
    let temp = obj.x
    obj.x = obj.y
    obj.y = temp
}

注意两个选项的执行时间。

通过运行此代码,我对其进行了测量。

console.time('swapInPlace')
swapInPlace({x:1, y:2})
console.timeEnd('swapInPlace') // swapInPlace: 0.056884765625ms

console.time('swap')
swap({x:3, y:6})
console.timeEnd('swap')        // swap: 0.01416015625ms

如您所见(和很多人所述),就地交换(异或)比使用temp变量的其他选项要花很多时间。


0

R缺少Edsger W. Dijkstra在1976年的《程序设计学》中提出的并发分配,第4章,第29页。这将提供一个优雅的解决方案:

a, b    <- b, a         # swap
a, b, c <- c, a, b      # rotate right

-1
a = a + b - (b=a);

这很简单,但是可能会发出警告。


5
警告是它不起作用?我们不知道b=a是在执行之前还是之后执行a + b
Bo Persson


-1
second_value -= first_value;
first_value +=  second_value;
second_value -= first_value;
second_value *= -1;

如果任何操作上溢或下溢,则具有未定义的行为。如果对象是浮点的,它也会失去精度。
基思·汤普森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.