Permuter deux variables sans en utiliser une troisième

En informatique, il est une opération courante de vouloir permuter deux variables, et même si certain langage implémente cette fonctionnalité en leur sein (Python par exemple), on voit souvent les programmeurs et programmeuses recoder la permutation.

Permutation classique avec variables temporaires

Si nous devions imaginer une implémentation de la permutation, la première qui nous vient à l’esprit est surement celle-ci.

Imaginons que nous ayons 2 melons en mains, un melon vert dans la main droite et un melon jaune dans la main gauche, j’aimerais échanger de mains mes melons. Malheureusement les melons sont trop lourds et je ne peux qu’en mettre un dans chaque main. La meilleure solution, et vous en conviendrez que c’est celle que vous utiliseriez dans ce cas, consiste (1) à poser un des melons sur une table ce qui permet de libérer une main, (2) de changer de main le melon qui n’a pas été posé, et (3) de finalement reprendre le melon posé avec l’autre main. Ça se passe donc en 3 temps.

Prenons les variables a et b que nous voulons permuter.

Le pseudo-code suivant permet de permuter nos variables.

a = 5
b = 3
temp = a
a = b
b = temp

La variable temp est une variable dite temporaire, dans le sens où elle n’est utilisée que pour permettre la permutation, comme la table dans mon analogie fruitière. Elle permet de garder l’information comme on peut le voir dans le dérouler de l’exécution ci-dessous :

Ligne a b temp
2 5 3 0
3 5 3 5
4 3 3 5
5 3 5 5

Mais le titre de ce billet c’est "sans variable temporaire". Or là, on en utilise une.

Tous doux ! Effectivement, et j’y viens, on peut permuter sans variable temporaire. En fait on peut le faire d’au moins deux façons en utilisant des propriétés mathématiques.

Permutation SANS variable temporaire

Bon on y est, deux façons donc !

On peut faire une permutation sans variable temporaire avec la propriété de deux opérations.

L’opération de l’addition (et soustraction)

La propriété intéressante de l’addition est la suivante.

a=a+bbb=a+baa = a+b-b\\ b = a+b-a

Bon avec seulement ça, ce n’est pas sûr que vous voyez où nous allons, alors je vais éclaircir un peu.

a=(a+b)bb=(a+b)aa = (a+b)-b\\ b = (a+b)-a

(a+b)(a+b) peut nous servir de variable temporaire.

Soit l’algorithme suivant :

a = 3
b = 5
a = a + b
b = a - b
a = a - b

On remarque qu’il n’y a pas de variable temporaire. Mais la permutation est-elle bien effective ? Pour s’en convaincre, écrivons l’exécution de l’algorithme avec les variables indicées de leur valeur au ième temps.

Ligne a b
2 a0a_0 b0b_0
3 a1=a0+b0a_1 = a_0 + b_0 b1=b0b_1 = b_0
4 a2=a1a_2 = a_1 b2=a1b1=a0+b0b0=a0b_2 = a_1 - b_1 = a_0 + b_0 - b_0 = a_0
5 a3=a2b2=a1a0=a0+b0a0=a_3 = a_2 - b_2 = a_1 -a_0 = a_0 + b_0 - a_0 = b₀ b3=b2=b_3 = b_2 = a₀

À la fin, on a bien a3=b0a_3 = b_0 et b3=a0b_3 = a_0. La permutation est bien effective.

Cette méthode peut poser problème selon la taille des objets a et b. Les nombres entiers étant bornés dans leur implémentation, cette borne peut etre dépassé à l’addition de a et b, ce qui causera un bug de l’algorithme.

L’opération XOR

keuwa ?

XOR est une opération binaire dite "bits à bits" appelés aussi le ou exclusif dont la valeur renvoie 1 quand les 2 bits sont différents. Sa table de vérité est :

p q p XOR q
0 0 0
0 1 1
1 0 1
1 1 0

Ainsi 1101  XOR  0101=10001101 \ \ XOR \ \ 0101 = 1000

La propriété intéressante de XOR qu’on va utiliser est que :

p=(p  XOR  q)  XOR  qp = (p \ \ XOR \ \ q) \ \ XOR\ \ q\\

ce qui peut se montrer facilement avec une table de vérité.

p q p XOR q (p XOR q) XOR q
0 0 0 0
0 1 1 0
1 0 1 1
1 1 0 1

La première et la quatrième colonnes sont égales.

et aussi de manière plus anecdotique que p  XOR  q=q  XOR  pp \ \ XOR\ \ q = q \ \ XOR \ \ p

Soit l’algorithme suivant :

a = 0011
b = 1001
a = a XOR b
b = a XOR b
a = a XOR b

À la fin de cet algorithme, on a

b=(a  XOR  b)  XOR  bb=ab = (a \ \ XOR \ \ b) \ \ XOR \ \ b \\ \Rightarrow b = a

et

a=(a  XOR  b)  XOR  ((a  XOR  b)  XOR  b)a=(a  XOR  b)  XOR  aa=ba = (a \ \ XOR \ \ b) \ \ XOR \ \ ((a \ \ XOR \ \ b) \ \ XOR \ \ b)\\ \Rightarrow a = (a \ \ XOR \ \ b) \ \ XOR \ \ a\\ \Rightarrow a = b

On a bien la permutation qui a été effectué.

Cependant cela nécessite l’existence d’une fonction XOR pour les types utilisés.


La permutation est simple, mais parfois c’est dans les choses simples, qu’on se casse le plus la tête.

Merci pour votre lecture.

25 commentaires

Le contenu de ce billet ne me pose aucun soucis, mais il serait peut-être utile de préciser que ces deux méthodes "sans variables temporaires" sont toutes les deux plus lentes et moins lisibles que la méthode de départ, et qu’elles sont surtout là pour la culture ou pour faire des énigmes, qu’on ne conseille pas de les utiliser en pratique.

Existe-t-il des langages (utilisé !) sans XOR ? C’est une instruction processeur, ça m’étonne qu’une langage ne la propose pas de base.

Haskell peut-être ?

Est-il possible de modifier l’algorithme 1 pour éviter le bug ? Je pense que oui, en utilisant aba - b ou bab - a.

+1 -0

Moi ma question, c’est à quoi ça sert de permuter 2 variables ? Ça vous arrive souvent d’en avoir besoin ?

Phigger

En vrai, non. C’est pas un besoin que je rencontre couramment. Sauf pour implémenter la suite de Fibonacci en entretien d’embauche, mais ça fait des années qu’on ne m’a pas demandé ce genre de choses…

+6 -0

Merci pour vos retours.

il serait peut-être utile de préciser que ces deux méthodes "sans variables temporaires" sont toutes les deux plus lentes et moins lisibles que la méthode de départ,[…] qu’on ne conseille pas de les utiliser en pratique.

gasche

Plus lente, je ne suis pas sur pour la méthodes avec les XOR (C++ implémente celle avec variable temporaires donc je suppose que tu as raison sur ce point). Mais elle gagne surement en consommation mémoire ce qui peut etre intérressant pour un micro-système. Pour la lisibilité, on est bien d’accord.

Ce billet n’a à mon sens qu’un interet théorique, et je ne saurais parler de ce qu’il se fait en pratique, que je ne connais pas suffisamment.

Moi ma question, c’est à quoi ça sert de permuter 2 variables ? Ça vous arrive souvent d’en avoir besoin ?

Phigger

C’est pas mal utilisé dans des algorithmes de tri par comparaison.

La première et la quatrième colonnes sont égales.

Dans le tableau du billet, non. Mais elles devraient, en effet. ^^

Gabbro

Oups! Oui c’est corrigé, merci.

(La correction sur un billet l’enlève de la première page apparement (C’est compréhensible))

+0 -0

C’est un besoin relativement courant par exemple pour trier un tableau.

En tout cas, suffisamment courant pour avoir une instruction processeur dédiée (XCHG) ou que C++ implémente une fonction standard pour le faire de manière performante. (std::swap)

+1 -0

Si on veut échanger des valeurs dans un tableau stocké en mémoire, on veut vraiment éviter les versions sans temporaire qui vont faire plus de lecture et écriture dans le tableau que nécessaire, là où un registre temporaire va coûter très peu cher. (Les segments du tableau autour de ces deux indices sont dans le cache, mais il faut consulter le cache, y propager les écritures, etc.)

Mais [la méthode avec des XOR] gagne sûrement en consommation mémoire ce qui peut être intéressant pour un micro-système.

Bof, d’une part la différence est minime, d’autre part la méthode avec des XOR a un code plus compliqué, donc tu risques de perdre en taille de code ce que tu gagnes en taille de pile.

La différence la plus sensible dans la vraie vie c’est que si on utilise une de ces méthodes, on écrit du code trop compliqué et nos collaborateurs ont envie de se venger.

Tout dépend ce que tu appelles « optimal ».

Si c’est la lecture/facilité de compréhension, le passage par une variable temporaire est optimal. Si c’est en espace mémoire, je dirais que l’instruction processeur est optimale. Si c’est en vitesse d’exécution, je ne saurais dire qui est optimal.

Plus d’info swap (en).

+3 -0

Dans les années 70, je programmais en assembleur pour IBM 360.
Cette machine disposait de 16 registres pour y faire les opérations.
Dans ce contexte, la permutation par XOR était rapide et économique en ressources mémoire.
C’est une méthode générique pour permuter les contenus de deux registres sans aucune restriction, ni utiliser un troisième registre.

Ce n’est pas le cas des méthodes basées sur l’addition ou la soustraction qui ne fonctionnent pas toujours du fait des limitations de l’arithmétique en nombres entiers.

+5 -0

Je ne comprend pas pourquoi ont dis que la méthode basées sur l’addition ou la soustraction ne marche pas toujours.

Selon moi, c’est faux.

int a = rand(), b = rand();

if( a > b) {
  a = a - b;
  b = a + b;
  a = b - a;
} else {
  a = b - a;
  b = b - a;
  a = a + b;
}

Cette méthode semble convenir dans tous les cas pour moi, tant que a et b sont du même type bien-sûr.

+1 -0

Si a est positif et que b est négatif, a - b peut dépasser. Mais y a moyen de s’arranger je pense. En faisant a = a + b (correct car a positif et b négatif), puis b = a - b on a a + b dans a et a dans b, donc a = a - b permet d’obtenir le résultat. De même on adapte si b est positif et a négatif. :)

+0 -0

Je ne comprend pas pourquoi ont dis que la méthode basées sur l’addition ou la soustraction ne marche pas toujours.

Selon moi, c’est faux.

int a = rand(), b = rand();

if( a > b) {
  a = a - b;
  b = a + b;
  a = b - a;
} else {
  a = b - a;
  b = b - a;
  a = a + b;
}

Cette méthode semble convenir dans tous les cas pour moi, tant que a et b sont du même type bien-sûr.

ache

Ou alors pour éviter le bloc conditionnel, si a est plus grand que b, tu permutes a et b avec la méthode XOR puis tu appliques la formule des additions et soustractions ! :D

+5 -0

Sinon (je sais que ça existe en python) On peut faire une manière ultra élégante : a, b = b, a Et tadam :magicien:

Mais sinon c’est un manière mathématique jolie de faire (même si à pas utiliser car, comme dit plus haut, il y a quelques contraintes)

+0 -0

Sinon (je sais que ça existe en python) On peut faire une manière ultra élégante : a, b = b, a Et tadam :magicien:

depfryer

C’est évoqué en intro du billet, mais on notera tout de même que cette solution n’échappe pas à la valeur temporaire.

Quand tu écris b, a, tu crées un tuple temporaire référençant ces deux valeurs, que tu déconstruis ensuite en l’assignant aux variables a et b.

@entwanne j’ai pas réussi à retrouver l’évocation ^^' Pour la tulpe je suis partagé, car pour moi pour créer un tulpe, il aurait fallu rajouter les parenthèse (puis de toutes manières en assembleur faire une inversion aussi simplement que a<=>b prend une variable temporaire donc on fera pas mieux 😂)

Pour la tulpe je suis partagé, car pour moi pour créer un tulpe, il aurait fallu rajouter les parenthèse

depfryer

C’est la virgule l’opérateur qui crée un tuple et non les parenthèses (à l’exception du tuple vide qui s’écrit ()).

>>> (1)
1
>>> (1,)
(1,)
>>> 1,
(1,)
>>> ()
()

Ça crée une variable temporaire sémantiquement en Python. Ça ne veut pas dire qu’effectivement, l’interpréteur crée une variable temporaire.

+0 -0

Ça crée une variable temporaire sémantiquement en Python. Ça ne veut pas dire qu’effectivement, l’interpréteur crée une variable temporaire.

ache

Un flip est un pattern tellement récurrent qu’il est effectivement optimisé via l’opcode ROT_TWO, implémenté de la façon suivante, autrement dit avec deux temporaires au lieu d’un seul. Quand on commence à regarder un langage qui fonctionne avec une VM, je ne suis pas sûr que regarder les temporaires effectivement créées ou non par l’implémentation est pertinent de toute façon. Ce n’est qu’un détail, ce qui est standardisé est que ça doit conceptuellement se comporter comme si on construisait et déconstruisait un tuple et donc qu’on utilise une construction temporaire (que ce soit un vrai objet Python ou un jeu de pointeurs pour l’émuler, on s’en fout).

Pour exemple :

>>> def flip():
...   a=1
...   b=2
...   a, b = b, a
... 
>>> import dis
>>> dis.dis(flip)
  2           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (a)

  3           4 LOAD_CONST               2 (2)
              6 STORE_FAST               1 (b)

  4           8 LOAD_FAST                1 (b)
             10 LOAD_FAST                0 (a)
             12 ROT_TWO
             14 STORE_FAST               0 (a)
             16 STORE_FAST               1 (b)
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE
+2 -0

ROT_TWO ne permute pas deux variables nommées, mais elle échange les deux valeurs en haut de la pile de valeurs de la machine virtuelle Python. C’est une instruction courante parce qu’on parle d’un jeu d’instruction bytecode à pile, plutôt qu’à registre, donc il y a plein de petits opcodes pour permuter, dupliquer et effacer les valeurs sur la pile — dont celle-ci (mais aussi ROT_THREE, ROT_FOUR, DUP_TOP_TWO etc., dont on conviendra qu’elles ne sont presque jamais utilisées directement par les utilisateurs dans le programme source). On peut remarquer au passage que le consensus actuel est plutôt que les machines à registre sont plus efficaces (au détriment de la taille du code), et qu’elles n’ont pas besoin de ce genre de primitives.

L’avantage de l’écriture Python, c’est qu’elle permet de swapper à peu près n’importe quoi.

Il n’empêche que, quand cela convient, il est plus simple d’écrie trois instructions XOR en assembleur que d’exécuter plusieurs milliers d’instructions depuis Python en utilisant des tuples.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte