illustrer
Si vous avez besoin d'utiliser ces connaissances mais que vous ne les possédez pas, cela sera frustrant et pourrait conduire à un refus de l'entretien. Que vous passiez quelques jours à « faire du blitz » ou que vous utilisiez un temps fragmenté pour continuer à apprendre, cela vaut la peine de travailler sur la structure des données. Alors, quelles sont les structures de données en Python ? Des listes, des dictionnaires, des ensembles et... des piles ? Python a-t-il une pile ? Cette série d'articles donnera des pièces de puzzle détaillées.
9章:Listes liées avancées
Nous avons déjà introduit les listes chaînées simples. Un nœud de liste chaînée ne contient que des données et des champs suivants. Ce chapitre présente les listes chaînées avancées.
Liste doublement chaînée, liste doublement chaînée, chaque nœud a un précédent supplémentaire pointant vers le nœud précédent. Les listes à double chaînage peuvent être utilisées pour écrire des tampons d’éditeur de texte.
class DListNode : def __init__(self, data) : self.data = data self.prev = None self.next = None def revTraversa(tail) : curNode = tail alors que cruNode n'est pas None : print(curNode.data) curNode = curNode .prev def search_sorted_doubly_linked_list(head, tail,probe, target) : """ technique de sondage, qui améliore le parcours direct, mais la pire complexité temporelle est toujours O(n) recherchant une liste doublement chaînée triée en utilisant la technique de sondage Args : head ( DListNode obj) queue (DListNode obj) sonde (DListNode ou None) cible (DListNode.data) : données à rechercher """ si head est None : # assurez-vous que la liste n'est pas vide, retournez False si la sonde est Aucune : # si la sonde est nulle, initialisez-la au premier nœud sonde = head # si la cible vient avant le nœud de la sonde, nous traversons en arrière, sinon # traversons forward si cible <probe.data : alors que la sonde n'est pas Aucune et cible <= sonde.data : si cible == sonde.dta : renvoie True else : sonde = sonde.prev sinon : tandis que la sonde n'est pas Aucune et cible >= sonde .data : si cible == sonde.data : renvoie True sinon : sonde = sonde.next return False def insert_node_into_ordered_doubly_linekd_list(value): """ Il est préférable de faire un dessin pour le voir. Les opérations de liste chaînée sont faciles à confondre, alors faites attention à l'ordre d'affectation. """ newnode = DListNode (valeur) si head est Aucun : # liste vide head = newnode tail = head valeur elif < head.data : # insérer avant head newnode.next = head head.prev = newnode head = newnode elif value > tail.data : # insérer après la queue newnode.prev = tail tail.next = newnode tail = newnode else : # insérer au milieu le nœud du milieu = head tandis que le nœud n'est pas Aucun et node.data < valeur : node = node.next newnode.next = node newnode.prev = node.prev node.prev.next = newnode node.prev = newnode
liste chaînée circulaire
def travrseCircularList(listRef) : curNode = listRef done = listRef est Aucun alors qu'il n'est pas Aucun : curNode = curNode.next print(curNode.data) done = curNode est listRef # 回到遍历起始点 def searchCircularList(listRef, target) : curNode = listRef done = listRef est None tant que ce n'est pas fait : curNode = curNode.next si curNode.data == target : return True sinon : done = curNode est listRef ou curNode.data > target return False def add_newnode_into_ordered_circular_linked_list(listRef, value): "" " 插入并维持顺序 1. Insérez une liste chaînée vide ; 2. Insérez la tête ; 3. Insérez la queue ; 4. Insérez le milieu dans l'ordre """ newnode = ListNode(value) si listRef est None : # liste vide listRef = newnode newnode.next = newnode elif value < listRef.next.data : # insérer devant newnode.next = listRef.next listRef.next = newnode valeur elif > listRef.data : # insérer à l'arrière newnode.next = listRef.next listRef.next = newnode listRef = newnode else : # insérer au milieu preNode = Aucun curNode = listRef done = listRef est Aucun tant que ce n'est pas fait : preNode = curNode preNode = curNode.suivant done = curNode est listRef ou curNode.data > valeur newnode.next = curNode preNode.next = newnode
En utilisant une liste chaînée circulaire à double extrémité, nous pouvons implémenter un algorithme classique d'invalidation du cache, lru :
# -*- codage : utf-8 -*- class Node(object) : def __init__(self, prev=None, next=None, key=None, value=None) : self.prev, self.next, self. clé, self.value = prev, next, key, value class CircularDoubleLinkedList(object): def __init__(self): node = Node() node.prev, node.next = node, node self.rootnode = node def headnode(self ) : return self.rootnode.next def tailnode(self) : return self.rootnode.prev def remove(self, node) : si le nœud est self.rootnode : return else : node.prev.next = node.next node.next.prev = node.prev def append(self, node) : tailnode = self.tailnode() tailnode.next = nœud node.next = self.rootnode self.rootnode.prev = classe de nœud LRUCache(object) : def __init__(self, maxsize=16) : self.maxsize = maxsize self.cache = {} self.access = CircularDoubleLinkedList() self.isfull = len(self.cache) >= self.maxsize def __call__(self, func) : def wrapper(n) : cachenode = self .cache.get(n) si cachenode n'est pas Aucun : # hit self.access.remove(cachenode) self.access.append(cachenode) return cachenode.value else : # miss value = func(n) if not self.isfull : tailnode = self.access.tailnode() newnode = Node(tailnode, self.access.rootnode, n, value) self.access.append(newnode) self .cache[n] = newnode self.isfull = len(self.cache) >= self.maxsize valeur de retour else : # full lru_node = self.access.headnode() del self.cache[lru_node.key] self.access.remove(lru_node) tailnode = self. accès.tailnode() newnode = Node(tailnode, self.access.rootnode, n, value) self.access.append(newnode) self.cache[n] = newnode valeur de retour return wrapper @LRUCache() def fib(n) : si n <= 2 : renvoie 1 sinon : renvoie fib(n - 1) + fib(n - 2) pour i dans la plage (1, 35) : print(fib(i))
Chapitre 10 : Récursion
La récursivité est un processus permettant de résoudre des problèmes en subdivisant un problème plus vaste en cas plus petits du problème lui-même, puis en résolvant les parties les plus petites et les plus triviales.
Fonction récursive : appelez votre propre fonction
# Fonction récursive : appelez votre propre fonction, regardez la fonction récursive la plus simple, imprimez un nombre dans l'ordre inverse def printRev(n) : si n > 0 : print(n) printRev(n-1) printRev(3) # Sortie de 10 à 1 # Faites un léger changement et mettez print à la fin pour obtenir la fonction d'impression d'ordre positif def printInOrder(n): if n > 0: printInOrder(n-1) print(n) # La raison pour laquelle le plus petit est imprimé en premier parce que la fonction Recurse jusqu'à la pile la plus profonde lorsque n == 1. A ce moment, il n'y a plus de récursion et l'instruction print commence à être exécutée. A ce moment, n == 1, après chaque couche de la pile est sautée, une valeur plus grande est imprimée. printInOrder(3) # Sortie de séquence positive
Propriétés de la récursion : tous les problèmes résolus à l'aide de la pile peuvent être résolus à l'aide de la récursivité
- Une solution récursive doit contenir un cas de base ; une sortie récursive, représentant le plus petit sous-problème (n == 0 sortie d'impression)
- Une solution récursive doit contenir un cas récursif ; des sous-problèmes pouvant être décomposés
- Une solution récursive doit progresser vers le cas de base. Décrémenter n pour que n soit plus proche de la sortie récursive
Récursion de queue : se produit lorsqu'une fonction inclut un seul appel récursif comme dernière instruction de la fonction. Dans ce cas, une pile n'est pas nécessaire pour stocker les valeurs à utiliser au retour de l'appel récursif et une solution peut donc être implémentée en utilisant une boucle itérative à la place.
# Recherche binaire récursive def recBinarySearch(target, theSeq, first, last) : # Vous pouvez écrire des tests unitaires pour vérifier l'exactitude de cette fonction si premier > dernier : # Sortie récursive 1 return False else : mid = (premier + dernier) / / 2 if theSeq[mid] == cible : return True # Sortie récursive 2 elif theSeq[mid] > target : return recBinarySearch(target, theSeq, first, mid - 1) sinon : return recBinarySearch(target, theSeq, mid + 1 , dernier)
Chapitre 11 : Tables de hachage
La meilleure complexité temporelle de la recherche basée sur la comparaison (recherche linéaire, recherche binaire de tableaux ordonnés) ne peut atteindre que O(logn). La recherche O(1) peut être réalisée en utilisant le hachage. L'implémentation du dict intégré de Python est le hachage. Vous constaterez que la clé de dict doit implémenter la méthode __hash__
and __eq__
.
Hachage : le hachage est le processus de mappage d'une recherche d'une clé à une gamme limitée d'index de tableau dans le but de fournir un accès direct aux clés.
La méthode de hachage a une fonction de hachage qui est utilisée pour calculer une valeur de hachage pour la clé, qui est utilisée comme indice de tableau et placée dans l'emplacement correspondant à l'indice. Lorsque différentes clés ont le même indice calculé en fonction de la fonction de hachage, un conflit se produit. Il existe de nombreuses façons de résoudre les conflits, comme faire de chaque emplacement une liste chaînée et le placer à la fin de la liste chaînée des emplacements après chaque conflit, mais le temps de requête se dégradera et n'est plus O(1). Il existe également une méthode de sondage. Lorsque les emplacements de la clé sont en conflit, une méthode de calcul sera utilisée pour trouver le prochain emplacement vide pour le stockage. Les méthodes de sondage incluent le sondage linéaire, le sondage quadratique, etc. L'interpréteur Python utilise la méthode d'exploration du carré quadratique. . Un autre problème est que lorsque le nombre d'emplacements utilisés par Python est supérieur aux 2/3 du nombre pré-alloué, la mémoire sera réaffectée et les données précédentes seront copiées. Par conséquent, parfois l'opération d'ajout de dict est relativement coûteuse, sacrifiant de l'espace mais il peut toujours garantir l'efficacité des requêtes O (1). S'il y a une grande quantité de données, il est recommandé d'utiliser bloomfilter ou HyperLogLog fourni par redis.
Si vous êtes intéressé, vous pouvez consulter cet article, qui présente comment l'interpréteur c implémente l'objet python dict : Implémentation du dictionnaire Python . Nous utilisons Python pour implémenter une structure de hachage similaire.
import ctypes class Array: # L'ADT défini au chapitre 2 est utilisé ici comme tableau d'emplacements de HashMap def __init__(self, size): assert size > 0, 'array size must be > 0' self._size = size PyArrayType = ctypes .py_object * size self._elements = PyArrayType() self.clear(None) def __len__(self) : return self._size def __getitem__(self, index) : assert index >= 0 et index < len(self), ' out of range' return self._elements[index] def __setitem__(self, index, value) : assert index >= 0 et index < len(self), 'hors plage' self._elements[index] = valeur def clear( self ,valeur): """ 设置每个元素为value """ for i in range(len(self)): self._elements[i] = value def __iter__(self): return _ArrayIterator(self._elements) class _ArrayIterator: def __init__( self, items) : self._items = items self._idx = 0 def __iter__(self) : return self def __next__(self) : si self._idx < len(self._items) : val = self._items[self._idx ] self._idx += 1 return val else: raise StopIteration class HashMap: """ HashMap ADT实现,类似于python内置的dict Un slot a trois états : 1. HashMap.UNUSED n'a jamais été utilisé. Cet emplacement n'a pas été utilisé ou n'est pas en conflit. Tant que UNUSEd est trouvé lors de la recherche, il n'est pas nécessaire de continuer la recherche. 2. Il a été utilisé mais supprimé. Pour le moment, il s'agit de HashMap.EMPTY. L'élément derrière le point de sonde peut avoir une clé. 3. Le slot utilise le nœud _MapEntry """ classe _MapEntry : # Les données stockées dans le slot def __init__(self , key, value) : self.key = key self.value = value UNUSED = None # Un slot inutilisé, en tant qu'instance unique de ce type de variable, est jugé ci-dessous par EMPTY = _MapEntry(None, Aucun) # Slots utilisés mais supprimés def __init__(self): self._table = Array(7) # Initialiser 7 slots self._count = 0 # Réallouer lorsque plus des 2/3 de l'espace est utilisé, facteur de charge = 2/3 self ._maxCount = len(self._table) - len(self._table) // 3 def __contains__(self, key) : slot = self._findSlot(key, False) def __len__( soi): return self._count return slot is not None def add(self, key, value): if key in self: # 覆盖原有value slot = self._findSlot(key, False) self._table[slot].value = value return False sinon : slot = self._findSlot(key, True) self._table[slot] = HashMap._MapEntry(key, value) self._count += 1 if self._count == self._maxCount: # 超过2/3使用就rehash self._rehash() return True def valueOf(self, key): slot = self._findSlot(key, False) assert slot n'est pas Aucun, 'Clé de carte invalide' return self._table[slot].value def remove(self, key) : """ L'opération de suppression définit l'emplacement sur VIDE """ assert key in self, 'Key error %s' % key slot = self._findSlot(key, forInsert=False) value = self ._table[slot].value self._count -= 1 self._table[slot] = HashMap.EMPTY valeur de retour def __iter__(self) : return _HashMapIteraotr(self._table) def _slot_can_insert(self, slot) : return (self . _table[slot] est HashMap.EMPTY ou self._table[slot] est HashMap.UNUSED) def _findSlot(self, key, forInsert=False): """ Notez qu'il y a des erreurs dans le livre d'origine et que le code ne peut pas s'exécuter du tout. Je l'ai réécrit moi-même ici Args : forInsert (bool) : si la recherche porte sur un si la recherche porte sur une insertion , renvoie : slot d'insertion ou None """ slot = self._hash1(key) step = self._hash2(key) _len = len(self._table) if not forInsert : # 查找是否存在key while self._table[slot] n'est pas HashMap.UNUSED: # 如果一个槽是UNUSED, 直接跳出 if self._table[slot] is HashMap.EMPTY: slot = (slot + step) % _len continue elif self._table[ slot].key == clé : retour slot slot = (slot + pas) % _len return Aucun else : # Afin d'insérer la clé sans self._slot_can_insert(slot): # Bouclez jusqu'à ce que vous trouviez un emplacement pouvant être inséré slot = (slot + step) % _len return slot def _rehash(self): # Le nombre de les emplacements actuellement utilisés sont supérieurs aux 2/3 Lors de la recréation d'une nouvelle table origTable = self._table newSize = len(self._table) * 2 + 1 # L'original 2*n+1 fois self._table = Array(newSize) self._count = 0 self._maxCount = newSize - newSize // 3 # Ajoutez la valeur de clé d'origine à la nouvelle table pour l'entrée dans origTable : si l'entrée n'est pas HashMap.UNUSED et que l'entrée n'est pas HashMap.EMPTY : slot = self._findSlot (entry.key, True) self._table[slot] = entrée self._count += 1 def _hash1(self, key): """ clé de hachage et de hachage""" return abs(hash(clé)) % len(self._table) self._idx += 1 def _hash2(self, clé) : """ key""" return 1 + abs(hash(key)) % (len(self._table)-2) class _HashMapIteraotr: def __init__(self, array): self. _array = array self._idx = 0 def __iter__(self) : return self def __next__(self) : si self._idx < len(self._array) : si self._array[self._idx] n'est pas None et self._array [self._idx].key n'est pas Aucun : key = self._array[self._idx].key return key else : self._idx += 1 else : raise StopIteration def print_h(h) : pour idx, i in enumerate(h): print(idx, i) print('\n') def test_HashMap(): """ Quelques tests unitaires simples, mais la couverture des cas de test n'est pas très complète""" h = HashMap () assert len(h) == 0 h.add('a', 'a') assert h.valueOf('a') == 'a' assert len(h) == 1 a_v = h.remove ( 'a') assert a_v == 'a' assert len(h) == 0 h.add('a', 'a') h.add('b', 'b') assert len(h) = = 2 affirmer h.valueOf('b') == 'b' b_v = h.Remove('b') assert b_v == 'b' assert len(h) == 1 h.remove('a') assert len(h) == 0 n = 10 pour i dans la plage (n) : h.add (str (i), i) assert len (h) == n print_h (h) pour i dans la plage (n): assert str (i) dans h pour i dans la plage (n): h.remove(str(i)) assert len(h) == 0
12章 : Tri avancé
Le chapitre 5 présente les algorithmes de tri de base et ce chapitre présente les algorithmes de tri avancés.
Tri par fusion : diviser pour mieux régner
def merge_sorted_list(listA, listB): """ s'il vous plaît, O(max(m, n)) ,mnn是数组长度""" print(' fusionner la liste gauche droite', listA, listB, end='') new_list = list() a = b = 0 tandis que a < len(listA) et b < len(listB) : si listA[a] < listB[b] : new_list.append(listA[a]) a += 1 sinon : new_list.append(listB[b]) b += 1 while a < len(listA) : new_list.append(listA[a]) a += 1 while b < len(listB) : new_list. append(listB[b]) b += 1 print(' ->',new_list) renvoie new_list def mergesort(theList) : """ O(nlogn), appel de couche de journal, n opérations par couche mergesort : diviser et conquérir Diviser et conquérir 1. Décomposer le tableau d'origine en sous-tableaux de plus en plus petits 2. Fusionner les sous-tableaux pour créer un tableau ordonné """ print(theList) # J'ai tapé les étapes clés, vous pouvez l'exécuter pour voir l'ensemble du processus si len(theList) <= 1 : # Sortie récursive return theList else: mid = len(theList ) // 2 # Décomposer récursivement les tableaux gauche et droit left_half = mergesort(theList[:mid]) right_half = mergesort(theList[mid:]) # Fusionner les sous-tableaux ordonnés des deux côtés newList = merge_sorted_list(left_half, right_half) return newList "" "Il s'agit du processus de tri [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] [10, 9, 8, 7,6] [10, 9] [10] [9] fusionner la liste gauche droite [10] [9] -> [9, 10] [8, 7, 6] [8] [7, 6] [7] [6] fusionner la liste gauche droite [7] [6 ] -> [6, 7] fusionner la liste gauche droite [8] [6, 7] -> [6, 7, 8] fusionner la liste gauche droite [9, 10] [6, 7, 8] -> [6, 7, 8, 9, 10] [5, 4, 3, 2, 1] [5, 4] [5] [4] fusionner la liste gauche droite [5] [4] -> [4, 5] [3, 2, 1] [3] [2, 1] [2] [1] fusionner la liste gauche droite [2] [1] -> [1, 2] fusionner la liste gauche droite [3] [1, 2] -> [ 1, 2, 3] fusionner la liste gauche droite [4, 5] [1, 2, 3] -> [1, 2, 3, 4, 5] "" "
Tri rapide
def quicksort(theSeq, first, last): # moyenne: O(nlog(n)) """ quicksort : il s'agit également de diviser pour régner, mais contrairement au tri par fusion, il utilise le pivot sélectionné (pivot) au lieu du tableau de Division du milieu 1. Dans la première étape, sélectionnez le pivot pour diviser le tableau. Les éléments du côté gauche du pivot sont plus petits que lui et les éléments du côté droit sont supérieurs ou égaux. 2. Récursion les tableaux divisés sur les côtés gauche et droit jusqu'à la sortie récursive (le nombre d'éléments du tableau est inférieur à 2) 3. Fusionner le pivot et les tableaux divisés gauche et droit en un tableau ordonné """ si premier < dernier : pos = partitionSeq (theSeq, first, last) # Effectuer un tri rapide de manière récursive (theSeq, first, pos - 1) sur le tri rapide des sous-tableaux divisés (theSeq, pos + 1, last) def partitionSeq(theSeq, first, last): """ L'opération de partition dans un tri rapide, déplacez les éléments plus petits que le pivot vers la gauche et déplacez les éléments plus grands que le pivot vers la droite.""" pivot = theSeq[first] print('before partitionSeq',theSeq) gauche = premier + 1 right = last while True: # Trouvez le premier plus grand que pivot while left <= right et theSeq[left] < pivot: left += 1 # Commencez par la droite et trouvez le premier plus petit que pivot while right >= left et theSeq[right ] >= pivot: right -= 1 if right < left: break else: theSeq[left], theSeq[right] = theSeq[right], theSeq[left] # Mettre le pivot dans la position appropriée theSeq[first ], theSeq [right] = theSeq[right], theSeq[first] print('after partitionSeq {}: {}\t'.format(theSeq, pivot)) return right # Renvoie la position du pivot def test_partitionSeq(): je = [0,1,2,3,4] assert partitionSeq(l, 0, len(l)-1) == 0 l = [4,3,2,1,0] quicksort(to_sort, 0, len(to_sort)-1) # Notez que le tri sur place est utilisé ici et que le tableau est directement modifié. assert partitionSeq(l, 0, len(l)-1) == 4 l = [2,3,0,1,4] assert partitionSeq(l, 0, len(l)-1) == 2 test_partitionSeq() def test_quicksort() : def _is_sorted(seq) : pour i dans la plage (len(seq)-1) : si seq[i] > seq[i+1] : renvoie False renvoie True à partir d'un randint d'importation aléatoire pour i dans la plage ( 100) : _len = randint(1, 100) to_sort = [] pour i dans range(_len) : to_sort.append(randint(0, 100)) print(to_sort) assert _is_sorted(to_sort) test_quicksort()
En utilisant l'opération partitionSeq de tri rapide, nous pouvons également implémenter un autre algorithme, nth_element, pour trouver rapidement le kième plus grand élément dans un tableau non ordonné.
def nth_element(seq, beg, end, k) : si beg == end : return seq[beg] pivot_index = partitionSeq(seq, beg, end) if pivot_index == k : return seq[k] elif pivot_index > k : return nth_element(seq, beg, pivot_index-1, k) else : return nth_element(seq, pivot_index+1, end, k) def test_nth_element() : à partir d'une importation aléatoire shuffle n = 10 l = list(range(n)) shuffle( l) print(l) pour i in range(len(l)) : assert nth_element(l, 0, len(l)-1, i) == i test_nth_element()