Correction : Diviser pour régner

Recherche dans une liste

Recherche séquentielle

Pseudo-code


	fonction recherche(L: liste, n:valeur):
	
		pour chaque élément de la liste L :
			si élément == n :
				renvoyer VRAI ( et éventuellement indice de la valeur dans la liste )
			fin si
		fin pour
		
		renvoyer FAUX
			

Code Python


def recherche(L, n):
    for i in range(len(L)) :  # parcours de la liste par indice, ce qui permettra de renvoyer l'index de la valeur dans la liste dans le cas où on la trouve
        if L[i] == n:
            return True, i
    return False
    		

Et pourquoi pas envisager une version récursive :


def rech_rec(L, n, i = 0):
    if i < len(L):
        if L[i] == n:
            return True, i
        else:
            return rech_rec(L, n, i+1)
    return False
    		

La récursivité remplace simplement l'utilisation d'une boucle dans l'algorithme itératif, ce qui n'apporte donc aucun avantage par rapport à lui...

Complexité en temps

Dans le pire des cas, la valeur recherchée se trouve à la toute fin de la liste ( ou alors pas du tout ! ) : on est donc obligé de parcourir intégralement cette dernière, soit ses n éléments : la complexité est donc en O(n).

Recherche par dichotomie

Version itérative

Pseudo-code

fonction rech_dichoI(L : tableau trié, n : valeur à chercher ):
 
 	g ← indice du premier élément du tableau
 	d ← indice du dernier élément du tableau
 	
 	tant que g <= d:
 		m ← (g+d)//2
 		si L[m] = n :
 			renvoyer m
 		sinon :
 			si n < L[m]:
 				d ← m-1
 			sinon  ( donc si valeur > L[m] ):
 				g ← m+1
 			fin si
 		fin si
 	fin tant que
 	
 	renvoyer FAUX
			

Si on ne trouve pas la valeur à l'indice m ( ligne 9 ), il faut donc exclure l'élément d'indice m de la recherche à l'étape suivante :

Elément exclus dans la recherche dichotomique
Implémentation Python

def rech_dichoI(L, n):
 
 	g = 0
 	d = len(L) - 1
 	
 	while g <= d:
 		m = (g+d)//2
 		if L[m] == n :
 			return 'VRAI' + str(m)
 		else :
 			if n < L[m]:
 				d = m-1
 			else:
 				g = m+1
 	return 'FAUX'
			

Version récursive

Pseudo-code

La recherche dichotomique s'exprime bien plus simplement récursivement :


fonction rech_dichoR(L: liste triée, n:valeur recherchée):
    si L a encore des éléments:
        m ← indice du milieu du tableau
        si L[m] = n :
		      renvoyer VRAI , m
		  sinon:
		      si L[m] > n :
		          rechercher récursivement la valeur dans la partie "gauche" du tableau
		              sinon:
		          rechercher récursivement la valeur dans la partie "droite" du tableau
		  sinon:
		      renvoyer FAUX
			

On peut également considérer que la récursion "remplace" la boucle while de l'algorithme itératif.

Version avec recopie de liste par compréhension

   .........
   			if L[m] > n:
            	return rech_dichoR([L[i] for in range(m)], n)
            else:
                return rech_dichoR([L[i] for in range(m+1, len(L))], n)  
          

L'inconvénient, à part le fait que l'on ne peut pas renvoyer l'indice de la valeur dans la liste, est la complexité supplémentaire introduite par ces recopies de portions de liste ( en O(n) ).

L'idéal serait de s'en passer, et pour ça, le mieux est d'adopter la méthode utilisée dans le script itératif : gérer deux simples variables indiquant l'indice du "bord gauche" et du "bord droit" de la liste.

Version sans recopie

def rech_dichoR(L, n, g, d):
    
    if g <= d:
        m = (g+d)//2
        if L[m] == n:
            return 'VRAI :'+ str(m)
        else:
            if n < L[m]:
                return rech_dichoR(L, n, g, m-1)
            else:
                return rech_dichoR(L, n, m+1, d)
    return 'FAUX'
    
rech_dichoR(L, n, 0, len(L)-1) # appel de fonction
         

Du point de vue de l'interface avec l'utilisateur, il est un peu pénible de devoir passer tous ces paramètres lors de l'appel initial de la fonction...
Pour améliorer cette interface, on peut, comme on l'a déjà vu, coder un ensemble de deux fonctions, l'une "publique" avec le minimum de paramètres, et l'autre "privée" qui fera la recherche effective :


# fonction "publique" ( = interface )
def rech_dichoR(L, n):
	return _rech_dichoR(L, n, 0, len(L) - 1)

# fonction "privée"    		
def _rech_dichoR(L, n, g, d):
    
    if g <= d:
        m = (g+d)//2
        if L[m] == n:
            return 'VRAI :'+ str(m)
        else:
            if n < L[m]:
                return _rech_dichoR(L, n, g, m-1)
            else:
                return _rech_dichoR(L, n, m+1, d)
    return 'FAUX'
    
rech_dichoR(L, n) # appel de fonction    
         

Tri-fusion

Trace de l'algorithme sur un exemple

Trace tri-fusion
"AR" = Appel Récursif - "F" = fusion

On constate donc que les appels récursifs à tri_fusion() ne servent "qu'à" découper la liste jusqu'à n'arriver qu'à des listes à un seul élément.

Le renvoi de ces appels récursifs correspondent à l'appel de la fonction de fusion, qui est l'opération de tri proprement dite de deux listes, elles-mêmes triées :

Fonction de fusion

Algorithme

Itératif

fonction fusionI(L1, L2:listes triées):
	L3 ← liste vide
	
	tant qu'on n'est pas arrivé à la fin d'une des listes :
		si l'élément courant de L1 est plus petit que ( ou égal à ) l'élément courant de L2:
			L3 ← L3 + élément courant de L1
			on passe à l'élément suivant de L1
		sinon:
			L3 ← L3 + élément courant de L2
			on passe à l'élément suivant de L2
		fin si
	fin tant que
	
	L3 ← L3 + liste restante non-vide
	
		renvoyer L3
			
Récursif

Encore plus simple à concevoir :


	fonction fusionR(L1, L2:listes triées):
		si on est arrivé à la fin de L1:
			renvoyer L2
		sinon si on est arrivé à la fin de L2:
			renvoyer L1
		sinon:
			si l'élément courant de L1 est plus petit que ( ou égal à ) l'élément courant de L2:
				renvoyer élément courant de L1 + fusion(reste de L1, L2)
			sinon:
				renvoyer élément courant de L2 + fusion(L1, reste de L2)
			fin si
		fin si
			

Voila une situation où la récursivité, bien que ne changeant rien à la complexité par rapport à l'algorithme itératif, apporte beaucoup plus de clarté et "d'élégance"à cet algorithme...

Implémentation

L'implémentation va dépendre du fait que les données sont structurées en "vraies" listes ou alors si il s'agit seulement de tableaux...

Version "full-liste"

On peut partir du principe que, comme on manipule des listes, on dispose des primitives pop(0) et append() qui permettent respectivement de supprimer le premier élément et d'ajouter un élément à la fin d'une liste. Si les listes sont implémentées sous forme de liste chaînée, la suppression de leur premier élément est de plus très efficace en temps ( voir le chapitre correspondant ) !

Dans la fonction de fusion, "l'élément courant" sera alors le premier élément de chaque liste, que l'on supprimera de la "bonne" liste selon le résultat de la comparaison; une des listes diminue donc d'un élément à chaque itération : l'algorithme s'arrête quand une des listes est vide, on insère alors les éléments restants de l'autre à la fin de la nouvelle liste :


def fusionI(L1, L2):
    L3 = []

    while len(L1) != 0 and len(L2) != 0:
        if L1[0] <= L2[0]:
            L3.append(L1.pop(0))
        else:
            L3.append(L2.pop(0))

    if len(L1) == 0:
        L3 = L3 + L2
    else:
        L3 = L3 + L1

    return L3
		

Et pour la version récursive :


def fusionR(L1, L2):

	if len(L1) == 0 :
		return L2
	if len(L2) == 0 :
		return L1
	else:
		if L1[0] > L2[0]:
			return [L2.pop(0)] + fusion(L1, L2)
		else:
			return [L1.pop(0)] + fusion(L1, L2)			
			

On rappelle que la méthode pop() retire un élément d'une liste et renvoie cet élément : LX.pop(0) modifie donc la liste, que l'on peut ensuite passer en argument lors de l'appel récursif suivant.

Attention à la syntaxe de la concaténation d'un élément à une liste ( lignes 10 et 12 ) : on ne peut pas "ajouter" une valeur à une liste ( types incompatibles ), c'est pourquoi on concatène en réalité une liste à un seul élément ( [LX.pop(0)] ) au reste de la liste construite par la fonction fusion().

Autre possibilité :


def fusionR(L1, L2, L3):

	if len(L1) == 0 :
		for elt in L2:
			L3.append(elt)
		return L3
	if len(L2) == 0 :
		for elt in L1:
			L3.append(elt)
		return L3
	else:
		if L1[0] > L2[0]:
			L3.append(L2.pop(0))
			return fusion(L1, L2, L3)
		else:
			L3.append(L1.pop(0))
			return fusion(L1, L2, L3)			
			

L'intérêt ici est d'utiliser la méthode de liste append, plus efficace que la concaténation de listes; la version récursive nécessite cependant de passer un troisième paramètre à la fonction, la liste fusionnée, initialement vide.
Or, on ne peut pas donner à un paramètre mutable une valeur par défaut : c'est donc dans l'appel de la fonction fusionR qu'il faut directement passer un argument "liste vide" [].


listes_fusionnees = fusionR(L1, L2, [])
			
Version tableau

Oui mais alors, si on ne dispose pas d'une structure de listes mais de tableaux, il faut revenir à une méthode manipulant des indices pour désigner "l'élément courant" de chaque liste :


def fusionI(L1, L2):
    L3 = [] # tableau vide qui contiendra la liste fusionnée
    
    i = 0 # indice indiquant l'élément courant dans L1
    j = 0 # idem pour L2
    
    while i != len(L1) and j != len(L2) :
        if L1[i] <= L2[j]:
            L3 = L3 + [L1[i]]
            i = i + 1 # avance d'un élément dans L1
        else:
           L3 = L3 + [L2[j]]
           j = j + 1 # avance d'un élément dans L2
      
    if i == len(L1): # si on est arrivé à la fin de L1
        while j != len(L2):
            L3 = L3 + [L2[j]] # ajout des éléments restants de L2
            j = j + 1
    else:
         while i != len(L1): # sinon, ajout des éléments restants de L1
            L3 = L3 + [L2[j]]
            i = i + 1
    
    return L3
 			

Oui, plus délicat à coder...et encore, on ne respecte pas complètement le type tableau de taille fixe, puisque L3 est en réalité un tableau initialement vide, auquel on ajoute à la fin les éléments un par un ( ce qui crée en fait un nouveau tableau à chaque ajout ).
L'approche la plus efficace aurait été de créer initialement un tableau L3 de taille définie, égale à la taille cumulée de L1 et L2, et à ensuite affecter à chaque élément de ce tableau une valeur.

Mais cela oblige à rajouter un index supplémentaire pour parcourir le tableau L3, et le code devient franchement compliqué ( vous pouvez le consulter ici ); on se contentera donc de la présente version qui est un "compromis" entre respect des types et clarté du code !

Et la version récursive ( plus simple aussi ! ) :


def fusionR(L1, L2, i = 0, j = 0):

    if i == len(L1):
        return [L2[k] for k in range(j, len(L2))]
    elif j == len(L2):
        return [L1[k] for k in range(i, len(L1))]       

    else:
        if L1[i] <= L2[j]:
            return [L1[i]] + fusionR(L1, L2, i+1, j)
        else:
            return [L2[j]] + fusionR(L1, L2, i, j+1)
 			

Le tri-fusion

Le plus dur est fait ! Il est alors très facile de coder l'algorithme de tri-fusion proprement dit :


def tri_fusion(L):

    if len(L) == 1 :
        return L

    mid = len(L)//2

    gauche = [L[i] for i in range(0,mid)] 
    droite = [L[i] for i in range(mid, len(L))]
 

    gauche = tri_fusion(gauche)
    droite = tri_fusion(droite)
    
    return fusion(gauche, droite)   # version itérative ou récursive, peu importe !