Заміна двох змінних значень без використання третьої змінної


104

Одне з дуже складних питань, яке задавали в інтерв'ю.

Обміняйте значеннями двох змінних, таких як a=10і b=15.

Як правило, для заміни двох значень змінних нам потрібна 3-я змінна типу:

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

Тепер вимога полягає в тому, щоб поміняти значення двох змінних без використання 3-ї змінної.


76
Ого. Невдало обране інтерв'ю, питання IMHO. Це техніка, яка рідко, якщо взагалі корисна на практиці. Є хороший шанс, що це лише заплутає оптимізатор компілятора, що призведе до менш ефективного коду, ніж "тимчасовий своп". Якщо це місце, з яким ви опитувались, не втягується в дуже важкі для математики речі (подумайте: розробка алгоритму шифрування тощо), я не можу уявити жодної вагомої причини задати таке питання.
Дан Ліплення

10
Дійсно, це питання не означає нічого, крім того, чи знає кандидат цей конкретний трюк, який є майже марним у виробничому коді. Я припускаю, що ти можеш натрапити на випадкового майстра, який розгадає це на льоту, але такі люди, які ще не знають хитрість, швидше за все, є досить рідкісними.
ceo

2
Можливо, вони хочуть відлучити людей, які думають, що знання таких хитрощів - це те, що робить хорошого програміста? Крім того, читаючи про xor-трюк, зверніть увагу на те, коли він вийде з ладу (що IMO робить його майже абсолютно непридатним для заміни цілих чисел загального призначення).
UncleBens

Відповіді:


155

Використання алгоритму заміни xor

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 мають одне і те ж місце пам’яті, коли для одного встановлено 0, обидва встановлюються 0. Коли обидва * x і * y дорівнюють 0, всі інші операції xor на * x і * y будуть рівними 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


Чи слід це використовувати?

У загальних випадках немає. Компілятор оптимізує тимчасову змінну та враховуючи, що заміна є звичайною процедурою, вона повинна виводити оптимальний машинний код для вашої платформи.

Візьмемо для прикладу цю програму швидкого тестування, написану на С.

#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
Можливо, варто пояснити, чому тест (тобто те, що потрійний підхід xor жахливо виходить з ладу, якщо x і y посилаються на один і той же об'єкт).
dmckee --- колишнє кошеня модератора

1
Я не знаю, як це отримало стільки оновлень, коли код так зламаний. Обидві свопи в тестованому сегменті коду абсолютно невірні. Придивіться.
SoapBox

@SoapBox: Дуже хороший момент! XOR swap нулі x & залишає y недоторканим, а temp своп залишає обидва зі значенням x. Ого!
Дрю Холл

4
@SoapBox Хороший улов. Виправлено та повторно перевірено, і я не отримую жодної суттєвої різниці в термінах.
Yacoby

4
Опитуючий сказав дві змінні, а не вставки. : P
Плюменатор

93

загальна форма:

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

однак ви повинні потенційно стежити за переповненнями, а також не всі операції мають зворотну сторону, яка добре визначена для всіх значень, визначених операцією. наприклад * і / працюйте, поки A або B не дорівнює 0

xor особливо приємний, оскільки він визначений для всіх ints та є його власною оберненою


XOR (X, X) == 0, тому xor працює для ints EXCEPT, коли два значення, що змінюються, рівні. Я не перший, хто вказав на це, і занадто багато людей сказали це (тут і деінде), щоб виділити когось на заслугу.
AlanK

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

16
Що робити, якщо a+bпереливається?
Алок Сінгал

2
@Alok: Це не прийнято в міркуваннях, що це непрактично :)
Дор

4
Якщо б мають ті ж, основні, розмірів цілочисельних типів (як int, unsigned short...), він все ще працює, врешті-решт, навіть з переливом, тому що якщо а + Ь переповнюється, то а - Ь буде Underflow. З цими основними цілими типами значення просто перевертаються.
Патрік Джонмайєр

10
У C використовувати це для unsignedцілих типів добре і працює завжди. Підписані типи можуть викликати невизначене поведінку при переповненні.
jpalecek

1
@Shahbaz: Навіть якщо підписані цілі числа зберігаються у доповненнях 2, поведінка щодо переповнення все ще не визначена.
Кіт Томпсон

79

Ще ніхто не запропонував використовувати std::swap.

std::swap(a, b);

Я не використовую жодних тимчасових змінних, і залежно від типу aта bреалізації може бути спекалізація, яка не має жодної. Реалізація повинна бути написана, знаючи, чи підходить "фокус" чи ні. Немає сенсу намагатися другий здогад.

Загалом, я, мабуть, хотів би зробити щось подібне, оскільки це буде працювати для типів класів, що дозволяє ADL знаходити кращу перевантаження, якщо це можливо.

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

Звичайно, реакція інтерв'юера на цю відповідь може багато сказати про вакансію.


звичайно, в C ++ 0x свопі будуть використовуватись посилання rvalue, тому буде ще краще!
jk.

16
Усі хочуть показати блискучий ксьор чи інший хитрість, який вони дізналися, за допомогою різних застережень про те, як ним користуватися, але це неминуче найкраща відповідь.

2
чому б не std :: swap (a, b); ? Я маю на увазі, чому використання?
Дінаїз

2
@Dinaiz без визначених std::користувачем swapреалізацій для визначених користувачем типів також доступні для виклику (це означає, що "це буде працювати для типів класів, що дозволяє ADL знайти кращу перевантаження, якщо це можливо").
Кайл Странд

std::swapвикористовує додаткову тимчасову пам'ять у своїй реалізації ні? cplusplus.com/reference/utility/swap
Ninja

19

Як вже зазначає manu, алгоритм XOR є популярним, який працює для всіх цілих значень (що включає в себе покажчики, з деякою удачею та кастингом). Для повноти хотілося б згадати ще один менш потужний алгоритм з додаванням / відніманням:

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

Тут ви повинні бути обережними щодо переливів / переливів, але в іншому випадку це працює так само добре. Ви можете навіть спробувати це на поплавках / дублях, якщо XOR заборонено на них.


" всі цілі значення (що включає покажчики тоді) " - Що? Ні, покажчики не є цілими числами, і ви не можете 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

Ах, я розумію. Я шукав у стандарті С згадування змінної як іменника, а не лише значення non-const. Я знайшов його в n1124, в розділі для циклів, що визначають область "змінних", оголошених у ініціалізаторі. Тоді я зрозумів, що це питання 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:"
сподівався

Як і в ряді інших відповідей, це має невизначений поведінка , якщо a+bабо a-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

Якщо ви трохи змінили питання, щоб задати про два регістри складання замість змінних, ви можете також використовувати xchgоперацію як один варіант, а операцію стека - як іншу.


1
Яку xchgоперацію ви маєте на увазі? Питання не вказало архітектуру процесора.
Кіт Томпсон

1

Розглянемо a=10, b=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=10і b=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=4і , b=9а потім:

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

Будь ласка, додайте коротке пояснення до своєї відповіді для майбутніх відвідувачів.
Микола Михайлов

1
У цьому ми беремо введення двох цілих чисел від користувача. Тоді ми використовуємо операцію по бітовому xor, дві поміняємо їх. Скажімо, у нас є два інтергери a = 4 і b = 9, то тепер a = a ^ b -> 13 = 4 ^ 9 b = a ^ b -> 4 = 13 ^ 9 a = a ^ b -> 9 = 13 ^ 9
Наем Уль Хассан

1

Ось ще одне рішення, але єдиний ризик.

код:

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

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

будь-яке значення в розташуванні a + 1 буде замінено.


2
Це була корисна відповідь, можливо, значення зникло.
Бібі Тахіра

1
Кілька видів невизначеної поведінки. *(&a+1)цілком може бути b. void main()недійсний. <conio.h>є нестандартним (і ви навіть не користуєтесь ним).
Кіт Томпсон

Код був протестований з іншими вихідними параметрами, які виключали, як я вже згадував ризик. Ризик пам’яті перекрито .. Це так .. Але це було корисним заміною двох змінних без участі третьої.
DareDevil

Найгірше - це те, що воно може спрацьовувати кілька разів, тому ви можете не відразу зрозуміти, що він повністю порушений і вийде з ладу під час оптимізації, інших компіляторів тощо
avl_sweden

1

Звичайно, відповідь С ++ повинна бути 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


Вам не потрібно переконатися, що значення відрізняються.
користувач253751

0

Ви можете зробити .... по-простому ... в межах одного рядка Logic

#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 Після swap 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 Після swap a = 20 b = 10

Програма 3: Використання побітового оператора XOR:

Бітовий оператор XOR може використовуватися для обміну двома змінними. XOR з двох чисел x і y повертає число, яке має всі біти як 1, де б біти x і y не відрізнялися. Наприклад, XOR 10 (In Binary 1010) і 5 (In Binary 0101) - це 1111, а XOR 7 (0111) і 5 (0101) - (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) Коли ми використовуємо покажчики на змінну і робимо своп функцій, всі вищевказані методи провалюються, коли обидва вказівника вказують на одну і ту ж змінну. Давайте подивимось, що буде в цьому випадку, якщо обидва вказують на одну і ту ж змінну.

// Бітовий метод на основі XOR

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

Як ви бачите (і, як багато хто говорив), поміняти місцями (xor) потрібно багато часу, ніж інший варіант, використовуючи змінну temp.


0

R не вистачає паралельного призначення, запропонованого Едсгером У. Дійкстра в "Дисципліні програмування" , 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.
Бо Персон


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

Це не визначене поведінка, якщо будь-яка операція переповнюється або переповнюється. Він також може втратити точність, якщо предмети мають плаваючу точку.
Кіт Томпсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.