Lu76Fer
XLDnaute Occasionnel
Bonjour à toutes et tous !
Vous utilisez sans doute souvent des collections ou dictionnaires mais ce sont des structures qui comportent des fonctionnalités très limitées. Personnellement je n'ai pas trouver d'algorithme classique codé pour le VB ou VBA du type Arbre AVL ou table de hachage; si vous avez des liens, je serais heureux de les connaitre. En attendant, je vous propose mon adaptation d'un arbre AVL écrit à l'origine en langage C qui utilise d'astucieux  effets de bord et possède une grande adaptabilité grâce à une interface très ingénieuse pour un langage purement fonctionnel. 
J'ai ajouté à cet algorithme une petite optimisation sur la partie suppression ainsi que d'autres fonctionnalités de recherche, de suppression et d'interface.
J'ai ajouté à cet algorithme une petite optimisation sur la partie suppression ainsi que d'autres fonctionnalités de recherche, de suppression et d'interface.
Les caractéristiques d'un arbre AVL : Wikipédia
Cet algorithme permet de classer des données d'une façon ordonnée tout en permettant l'ajout et la suppression de données. Pour réaliser une opération de recherche, d'ajout ou de suppression le niveau de complexité de cet algo est O(log2 n). Pour rechercher la donnée dans un arbre on parcours des Nœuds, voici  un tableau qui montre la hauteur de l'arbre en fonction du nombre de données maximum qu'il peut contenir :
		
		
	
	Le nombre de nœud moyen parcouru pour toute action sur l'arbre est égale à sa hauteur pour des grands nombres.
Il s'agit d'un algorithme très efficace pour gérer des données triées.
Implémentation
Au préalable, il est nécessaire de définir la structure de base qui compose l'arbre : le Nœud qui sera un module de Classe :
		VB:
	
	
	'CLASSE : 'UcNode'
Public Left As UcNode, Right As UcNode
Public Element As Object
Public Bal As Byte '1 :Penche à gauche, 2 : Equilibré, 3 : Penche à droite
'1:  O  2: O  3: O
'   /     / \     \
'  O     O   O     O
'Les valeurs d'équibre sont décalées de +2 afin d'optimiser le code.Ci-dessous la structure principale, l'arbre qui est composé de nœud(s), un module de classe capable de stocker des Objets génériques, Object, s'interfaçant grâce à des liaisons tardives.
		VB:
	
	
	'CLASSE : 'UcTree'
'Arbre de Recherche BinaiRe Equilibré ou Arbre AVL
Private Tree As UcNode      'Nœud principal de l'arbre (il peut se déplacer à gauche ou à droite)
Private RefNode As UcNode   'Référence à un nœud (Pointe dessus)
Private RefElt As Object    'Référence à un objet (Pointe dessus)
Private Replace As Boolean  'Mode remplacement pour l'ajout d'un élément
Public Total As Long    'Compteur de Nœud (ou d'élément : si doublon à gérer)
'Tree
Private Sub Class_Initialize()
    Set Tree = Nothing
End Sub
'Supprimer l'Arbre
Sub DeleteTree()
    Set Tree = Nothing
End Sub
'Ajoute l'Elément elt et renvoie (la référence à) celui-ci
'   Renvoie Nothing si déjà présent sauf si pReplace=True
Function AddElement(elt As Object, pReplace As Boolean) As Object
    Replace = pReplace
    Insert elt, Tree
    If RefElt Is Nothing Then Exit Function
    Set AddElement = RefElt: Set RefElt = Nothing: Total = Total + 1
End Function
'Retire et renvoie l'Elément identifié par key ou Nothing si non présent
Function RemoveElement(key As Variant) As Object
    Delete key, Tree
    If RefElt Is Nothing Then Exit Function
    Set RemoveElement = RefElt: Set RefElt = Nothing: Total = Total - 1
End Function
'Retire et renvoie l'Elément Min(si isMin=True) ou MAX ou Nothing si non présent
'   filter : si True appel la fonction d'interface : Function FilterElt() As Boolean
Function RemoveTipElt(isMin As Boolean, Optional filter As Boolean = False) As Object
    If Tree Is Nothing Then Exit Function
    If Tree.Left Is Nothing And Tree.Right Is Nothing Then
        Set RemoveTipElt = Tree.Element: Set Tree = Nothing
        Exit Function
    End If
    If isMin Then
        If RmvMinLeaf(Tree, filter) Then BalAfterLeftDel Tree
    Else
        If RmvMaxLeaf(Tree, filter) Then BalAfterRightDel Tree
    End If
    If RefNode Is Nothing Then Exit Function
    Set RemoveTipElt = RefNode.Element: Set RefNode = Nothing: Total = Total - 1
End Function
'Renvoie l'Elément identifié par key dans l'arbre Tree ou Nothing si non présent
Function FindElement(key As Variant) As Object
    Set FindElement = FindNodElt(key, Tree)
End Function
'Renvoie l'Elément Min(si isMin=True) ou MAX, ou encore Nothing si non présent
'   filter : si True appel la fonction d'interface : Function FilterElt() As Boolean
Function FindTipElt(isMin As Boolean, Optional filter As Boolean = False) As Object
Dim nod As UcNode
    If Tree Is Nothing Then Exit Function
    Set nod = Tree
    If isMin Then
        Do: Set RefNode = nod: Set nod = nod.Left: Loop Until nod Is Nothing
    Else
        Do: Set RefNode = nod: Set nod = nod.Right: Loop Until nod Is Nothing
    End If
    If flt Then If nod.Element.FilterElt() Then Exit Function
    Set FindTipElt = RefNode.Element: Set RefNode = Nothing
End Function
'Enumère les éléments de l'arbre dans l'ordre croissant (ou décroissant si rev=True)
'jusqu'au bout ou jusqu'à ce que votre fonction d'interface renvoie False
'Interface : Function EnumEltCallBack(param As Variant) As Boolean
Function EnumElement(param As Variant, Optional rev As Boolean = False) As Boolean
    If rev Then EnumElement = EnumNodEltRev(Tree, param) Else EnumElement = EnumNodElt(Tree, param)
End Function
'Partie récursive de la fonction FindElement
Private Function FindNodElt(key As Variant, nod As UcNode) As Object
Dim cmpVal As Long
    If nod Is Nothing Then Exit Function
    cmpVal = nod.Element.Compare(key)
    If cmpVal < 0 Then Set FindNodElt = FindNodElt(key, nod.Left): Exit Function
    If cmpVal > 0 Then Set FindNodElt = FindNodElt(key, nod.Right): Exit Function
    Set FindNodElt = nod.Element
End Function
'Partie récursive de la fonction EnumNodElt : ordre croissant
Private Function EnumNodElt(nod As UcNode, param As Variant)
    EnumNodElt = True
    If Not (nod Is Nothing) Then
        If Not (nod.Left Is Nothing) Then EnumNodElt = EnumNodElt(nod.Left, param)
        If EnumNodElt Then EnumNodElt = nod.Element.EnumEltCallBack(param)
        If (EnumNodElt And Not (nod.Right Is Nothing)) Then EnumNodElt = EnumNodElt(nod.Right, param)
    End If
End Function
'Ordre décroissant
Private Function EnumNodEltRev(nod As UcNode, param As Variant)
    EnumNodEltRev = True
    If Not (nod Is Nothing) Then
        If Not (nod.Right Is Nothing) Then EnumNodEltRev = EnumNodEltRev(nod.Right, param)
        If EnumNodEltRev Then EnumNodEltRev = nod.Element.EnumEltCallBack(param)
        If (EnumNodEltRev And Not (nod.Left Is Nothing)) Then EnumNodEltRev = EnumNodEltRev(nod.Left, param)
    End If
End Function
'Ajoute un nouveau noeud dans l'arbre nod qui référencera l'Elément elt
Private Function Insert(elt As Object, ByRef nod As UcNode) As Boolean
Dim cmpVal As Long, nodT As UcNode
    If nod Is Nothing Then    'Affecte elt au noeud créé
        Set nod = New UcNode: Set nod.Element = elt: Set RefElt = elt
        Insert = True: Exit Function
    End If
    cmpVal = elt.AddCompare(nod.Element)
    If cmpVal < 0 Then  'Insère elt à gauche
        Set nodT = nod.Left: Insert = Insert(elt, nodT): Set nod.Left = nodT
        If Insert Then Insert = BalAfterLeftIns(nod)
        Exit Function
    End If
    If cmpVal > 0 Then  'Insère elt à droite
        Set nodT = nod.Right: Insert = Insert(elt, nodT): Set nod.Right = nodT
        If Insert Then Insert = BalAfterRightIns(nod)
        Exit Function
    End If
    If Replace Then Set RefElt = nod.Element: Set nod.Element = elt
End Function
'Equilibre le noeud nod après une insertion à gauche
Private Function BalAfterLeftIns(nod As UcNode) As Boolean
Dim nod1 As UcNode, nod2 As UcNode
    On nod.Bal GoTo toLeft, equi, toRight
    Exit Function
toLeft: '*Penche à gauche*
    Set nod1 = nod.Left
    If nod1.Bal = 1 Then  'Single L rotation
        Set nod.Left = nod1.Right: Set nod1.Right = nod
        nod.Bal = 2: Set nod = nod1
    Else   'Double LR rotation
        Set nod2 = nod1.Right: Set nod1.Right = nod2.Left: Set nod2.Left = nod1
        Set nod.Left = nod2.Right: Set nod2.Right = nod
        If nod2.Bal = 1 Then nod.Bal = 3 Else nod.Bal = 2
        If nod2.Bal = 3 Then nod1.Bal = 1 Else nod1.Bal = 2
        Set nod = nod2
    End If
    nod.Bal = 2 'BalAfterLeftIns = False
    Exit Function
equi:   '*Equilibré*
    nod.Bal = 1: BalAfterLeftIns = True
    Exit Function
toRight:    '*Penche à droite*
    nod.Bal = 2 'BalAfterLeftIns = False
End Function
'Equilibre le noeud nod après une insertion à droite
Private Function BalAfterRightIns(nod As UcNode) As Boolean
Dim nod1 As UcNode, nod2 As UcNode
    On nod.Bal GoTo toLeft, equi, toRight
    Exit Function
toLeft: '*Penche à gauche*
    nod.Bal = 2   'BalAfterRightIns = False
    Exit Function
equi:   '*Equilibré*
    nod.Bal = 3: BalAfterRightIns = True
    Exit Function
toRight:    '*Penche à droite*
        Set nod1 = nod.Right
        If nod1.Bal = 3 Then  'Single R rotation
            Set nod.Right = nod1.Left: Set nod1.Left = nod
            nod.Bal = 2: Set nod = nod1
        Else   'Double RL rotation
            Set nod2 = nod1.Left: Set nod1.Left = nod2.Right: Set nod2.Right = nod1
            Set nod.Right = nod2.Left: Set nod2.Left = nod
            If nod2.Bal = 3 Then nod.Bal = 1 Else nod.Bal = 2
            If nod2.Bal = 1 Then nod1.Bal = 3 Else nod1.Bal = 2
            Set nod = nod2
        End If
        nod.Bal = 2   'BalAfterRightIns = False
End Function
'Supprime le noeud dans l'arbre nod contenant l'Elément identifié par key
'   et référence l'élément supprimé
Private Function Delete(key As Variant, nod As UcNode) As Boolean
Dim cmpVal As Long, nodT As UcNode
    If nod Is Nothing Then Delete = False: Exit Function
    cmpVal = nod.Element.Compare(key)
    If cmpVal < 0 Then
        Set nodT = nod.Left: Delete = Delete(key, nodT): Set nod.Left = nodT
        If Delete Then Delete = BalAfterLeftDel(nod)
        Exit Function
    End If
    If cmpVal > 0 Then
        Set nodT = nod.Right: Delete = Delete(key, nodT): Set nod.Right = nodT
        If Delete Then Delete = BalAfterRightDel(nod)
        Exit Function
    End If
    Set RefNode = nod: Set RefElt = nod.Element   'Référence l'élément supprimer
    If nod.Right Is Nothing Then Set nod = nod.Left: Delete = True: Exit Function
    If nod.Left Is Nothing Then Set nod = nod.Right: Delete = True: Exit Function
    'Remplacer le noeud supprimer par une 'feuille' (ou noeud à une feuille)
    If nod.Bal = 3 Then   'si nod penche à droite (rajouter à l'algo d'origine)
        Set nodT = nod.Right: Delete = RmvMinLeaf(nodT): Set nod.Right = nodT
        If Delete Then Delete = BalAfterRightDel(nod)
    Else    'Equilibré ou penche à gauche
        Set nodT = nod.Left: Delete = RmvMaxLeaf(nodT): Set nod.Left = nodT
        If Delete Then Delete = BalAfterLeftDel(nod)
    End If
    Set RefNode = Nothing
End Function
'Retire le noeud contenant l'élément MIN d'un sous-arbre nod, NON VIDE
'   Si un noeud est référencé on stocke l'élément de nod dans ce noeud sinon on référence nod (si non filtré)
Private Function RmvMinLeaf(nod As UcNode, Optional flt As Boolean = False) As Boolean
Dim nod1 As UcNode
    If nod.Left Is Nothing Then
        If RefNode Is Nothing Then  'Non référencé
            If flt Then If nod.Element.FilterElt() Then Exit Function   'Filtrage
            Set RefNode = nod
        Else    'Stocke Elt dans RefNode
            Set RefNode.Element = nod.Element
        End If
        Set nod = nod.Right 'L'élément supprimer peut aussi être un NOEUD n'ayant qu'une feuille
        RmvMinLeaf = True
    Else
        Set nod1 = nod.Left: RmvMinLeaf = RmvMinLeaf(nod1): Set nod.Left = nod1
        If RmvMinLeaf Then RmvMinLeaf = BalAfterLeftDel(nod)
    End If
End Function
'Retire le noeud contenant l'élément MAX d'un sous-arbre nod, NON VIDE
'   Si un noeud est référencé on stocke l'élément de nod dans ce noeud sinon on référence nod (si non filtré)
Private Function RmvMaxLeaf(nod As UcNode, Optional flt As Boolean = False) As Boolean
Dim nod1 As UcNode
    If nod.Right Is Nothing Then
        If RefNode Is Nothing Then  'Non référencé
            If flt Then If nod.Element.FilterElt() Then Exit Function   'Filtrage
            Set RefNode = nod
        Else    'Stocke Elt dans RefNode
            Set RefNode.Element = nod.Element
        End If
        Set nod = nod.Left  'L'élément supprimer peut aussi être un NOEUD n'ayant qu'une feuille
        RmvMaxLeaf = True
    Else
        Set nod1 = nod.Right: RmvMaxLeaf = RmvMaxLeaf(nod1): Set nod.Right = nod1
        If RmvMaxLeaf Then RmvMaxLeaf = BalAfterRightDel(nod)
    End If
End Function
'Equilibre le noeud nod après une suppression à gauche
Private Function BalAfterLeftDel(nod As UcNode) As Boolean
Dim nod1 As UcNode, nod2 As UcNode
    On nod.Bal GoTo toLeft, equi, toRight
    Exit Function
toLeft: '*Penche à gauche*
    nod.Bal = 2: BalAfterLeftDel = True
    Exit Function
equi:   '*Equilibré*
    nod.Bal = 3
    Exit Function
toRight:  '*Penche à droite*
    Set nod1 = nod.Right
    If nod1.Bal = 1 Then 'Double RL rotation
        Set nod2 = nod1.Left: Set nod1.Left = nod2.Right: Set nod2.Right = nod1
        Set nod.Right = nod2.Left: Set nod2.Left = nod
        If nod2.Bal = 3 Then nod.Bal = 1 Else nod.Bal = 2
        If nod2.Bal = 1 Then nod1.Bal = 3 Else nod1.Bal = 2
        Set nod = nod2: nod.Bal = 2: BalAfterLeftDel = True
    Else 'Single R rotation
        Set nod.Right = nod1.Left: Set nod1.Left = nod
        If nod1.Bal = 2 Then
            nod.Bal = 3: nod1.Bal = 1
        Else
            nod.Bal = 2: nod1.Bal = 2: BalAfterLeftDel = True
        End If
        Set nod = nod1
    End If
End Function
'Equilibre le noeud nod après une suppression à droite
Private Function BalAfterRightDel(nod As UcNode) As Boolean
Dim nod1 As UcNode, nod2 As UcNode
    On nod.Bal GoTo toLeft, equi, toRight
    Exit Function
toLeft: '*Penche à gauche*
    Set nod1 = nod.Left
    If nod1.Bal = 3 Then 'Double LR rotation
        Set nod2 = nod1.Right: Set nod1.Right = nod2.Left: Set nod2.Left = nod1
        Set nod.Left = nod2.Right: Set nod2.Right = nod
        If nod2.Bal = 1 Then nod.Bal = 3 Else nod.Bal = 2
        If nod2.Bal = 3 Then nod1.Bal = 1 Else nod1.Bal = 2
        Set nod = nod2: nod2.Bal = 2: BalAfterRightDel = True
    Else 'Single L rotation
        Set nod.Left = nod1.Right: Set nod1.Right = nod
        If nod1.Bal = 2 Then
            nod.Bal = 1: nod1.Bal = 3
        Else
            nod.Bal = 2: nod1.Bal = 2: BalAfterRightDel = True
        End If
        Set nod = nod1
    End If
    Exit Function
equi:   '*Equilibré*
    nod.Bal = 1
    Exit Function
toRight:    '*Penche à droite*
    nod.Bal = 2: BalAfterRightDel = True
End FunctionVoici un exemple du cas d'adaptation de code auquel j'ai eu à faire :
		VB:
	
	
	Dim nodT As UcNode
'(...)
Delete = Delete(key, nod.Right)
'DEVIENT :
Set nodT = nod.Right: Delete = Delete(key, nodT): Set nod.Right = nodTLe passage de paramètre de nod.Right doit se faire par référence pour rendre effectif l'effet de bord du programme. Il s'avère que nod.Right est vue comme une expression et non une référence à un objet et VBA force le passage de paramètre à se faire par Valeur plutôt que par référence. Il suffit donc de passer par l'intermédiaire de nodT pour que l'interpréteur accepte le passage par référence.
Exemple sur la classe UcNumber, d'utilisation des fonctionnalités de UcTree
Voici un objet simple qui stocke une valeur entière et pour lequel il sera nécessaire de coder les fonctions d'interface AddCompare, Compare et EnumEltCallBack pour tester l'ajout, la suppression et l'affichage des éléments UcNumber stockés dans l'arbre.
	
	
	
	
Exemple sur la classe UcNumber, d'utilisation des fonctionnalités de UcTree
Voici un objet simple qui stocke une valeur entière et pour lequel il sera nécessaire de coder les fonctions d'interface AddCompare, Compare et EnumEltCallBack pour tester l'ajout, la suppression et l'affichage des éléments UcNumber stockés dans l'arbre.
		VB:
	
	
	'CLASSE : UcNumber
Public Val As Integer
'Comparaison de 2 éléments me et elt sur une opération d'ajout
Function AddCompare(elt As UcNumber) As Long
Dim oTmp As Object
    AddCompare = Val - elt.Val
End Function
'Comparaison de 2 éléments me et elt sur les autres opérations
Function Compare(key As Variant) As Long
    Compare = key - Val
End Function
'Fonction appelée par EnumElement pour chaque élément tant que retour=True
'   param : vos arguments (par ex une collection)
Function EnumEltCallBack(param As Variant) As Boolean
Dim s As String
    If Not (EqualsCl Is Nothing) Then s = " avec " & EqualsCl.Count + 1 & " exemplaires"
    Debug.Print "Valeur=" & Val & s
    EnumEltCallBack = True
End Function
__________________________________________________________________________________________________________
'MODULE : Number
Private CrtTreeNumbers As UcTree
Sub AjoutSuppressionDeNumbers()
Dim treeNbrs As New UcTree, numb As UcNumber, ar As Variant, v As Variant, cnt As Integer
    Set CrtTreeNumbers = treeNbrs
    ar = Array(8, 9, 5, 14, 5, 12, 78, 5, 85, 5, 112, 5, 3) '9 valeurs différentes (les doublons sont ignorés)
    For Each v In ar
        Set numb = New UcNumber
        numb.Val = v
        treeNbrs.AddElement numb, False
    Next v
    treeNbrs.EnumElement Null
    Debug.Print "*** Compteur=" & treeNbrs.Total
    For cnt = 0 To UBound(ar)
        Set numb = treeNbrs.RemoveElement(ar(cnt))
    Next cnt
    Debug.Print "*** Après effacement, compteur=" & treeNbrs.Total
End Sub
		VB:
	
	
	'CLASSE : UcNumber
'Renvoyez vrai si l'élément est filtré(ignoré) sur les opérations avec l'argument filtre
Function FilterElt() As Boolean
    If Val > 12 Then FilterElt = True
End Function
__________________________________________________________________________________________________________
'MODULE : Number
Sub SuppressionSelectiveDeNumbers()
Dim treeNbrs As New UcTree, numb As UcNumber, ar As Variant, v As Variant
    Set CrtTreeNumbers = treeNbrs
    ar = Array(8, 9, 5, 14, 5, 12, 78, 5, 85, 5, 112, 5, 3) '9 valeurs différentes (les doublons sont ignorés)
    For Each v In ar
        Set numb = New UcNumber
        numb.Val = v
        treeNbrs.AddElement numb, False
    Next v
    treeNbrs.EnumElement Null
    Debug.Print "*** Compteur=" & treeNbrs.Total
    Do
        Set numb = treeNbrs.RemoveTipElt(True, True)
    Loop Until numb Is Nothing
    treeNbrs.EnumElement Null
    Debug.Print "*** Après effacement sélectif, compteur=" & treeNbrs.Total
End SubSur la procédure ci-dessus, on supprime les éléments par ordre croissant jusqu'à ce que la fonction FltElt renvoie True. Ceci permet de supprimer tous les éléments inférieurs ou égale à 12 mais on pourrait modifier le code pour supprimer toutes les valeurs supérieures ou égales à 85... En passant par la fonction EnumElement, il est aussi possible de sélectionner les nombres paires par exemple, pour les supprimer.
			
				Dernière édition: 
			
		
	
								
								
									
	
		
			
		
		
	
	
	
		
			
		
		
	
								
							
							 
	 
 
		