Chcesz wzbogacić swój zestaw narzędzi programistycznych o zaawansowane struktury danych? Zacznij już dziś, eksplorując różnorodne struktury danych dostępne w Pythonie.
Podczas nauki nowego języka programowania kluczowe jest zrozumienie podstawowych typów danych oraz wbudowanych struktur, które ten język oferuje. W tym przewodniku po strukturach danych Pythona przeanalizujemy:
- korzyści płynące ze stosowania struktur danych,
- wbudowane struktury danych w Pythonie, takie jak listy, krotki, słowniki i zbiory,
- sposoby implementacji abstrakcyjnych typów danych, takich jak stosy i kolejki.
Zaczynamy naszą podróż!
Dlaczego znajomość struktur danych jest tak ważna?
Zanim przejdziemy do konkretnych struktur danych, warto przyjrzeć się, jak wykorzystanie struktur danych może być korzystne:
- Efektywna obróbka danych: Odpowiedni dobór struktury danych pozwala na bardziej efektywne przetwarzanie informacji. Na przykład, jeśli potrzebujesz przechowywać zbiór elementów tego samego typu, z szybkim dostępem i silnym powiązaniem, tablica może być idealnym wyborem.
- Optymalne zarządzanie pamięcią: W rozbudowanych projektach, dla tych samych danych, jedna struktura może okazać się bardziej oszczędna pamięciowo niż inna. W Pythonie, zarówno listy, jak i krotki mogą przechowywać kolekcje danych tego samego lub różnych typów. Jeżeli jednak wiesz, że kolekcja nie będzie modyfikowana, krotka zużyje mniej pamięci niż lista.
- Przejrzysty kod: Użycie właściwej struktury danych dla określonej funkcji sprawia, że kod jest bardziej zorganizowany. Programiści, którzy będą czytać Twój kod, będą oczekiwać, że użyjesz konkretnych struktur w zależności od wymaganego działania. Na przykład, jeśli potrzebujesz mapowania klucz-wartość z szybkim dostępem i wstawianiem, słownik będzie najlepszym rozwiązaniem.
Listy
Listy są podstawowymi strukturami danych w Pythonie, wykorzystywanymi do tworzenia dynamicznych tablic, od zadań rekrutacyjnych po standardowe zastosowania.
Listy w Pythonie są typami danych kontenerowych, które są zmienne i dynamiczne. Możesz łatwo dodawać i usuwać elementy z listy, bez potrzeby tworzenia jej kopii.
Kiedy pracujesz z listami w Pythonie:
- Dostęp do elementu o określonym indeksie jest realizowany w czasie stałym.
- Dodanie elementu na koniec listy odbywa się w czasie stałym.
- Wstawienie elementu w określonym miejscu w liście wymaga czasu liniowego.
Python oferuje zestaw metod listowych, które ułatwiają wykonywanie typowych operacji. Poniższy fragment kodu demonstruje, jak wykonywać operacje na przykładowej liście:
>>> nums = [5,4,3,2] >>> nums.append(7) >>> nums [5, 4, 3, 2, 7] >>> nums.pop() 7 >>> nums [5, 4, 3, 2] >>> nums.insert(0,9) >>> nums [9, 5, 4, 3, 2]
Listy w Pythonie obsługują także wycinanie i sprawdzanie przynależności za pomocą operatora `in`:
>>> nums[1:4] [5, 4, 3] >>> 3 in nums True
Struktura danych listy jest nie tylko elastyczna i prosta, ale również umożliwia przechowywanie elementów różnego typu. Python posiada także dedykowaną strukturę tablicową, która pozwala na wydajniejsze przechowywanie elementów tego samego typu. O tym porozmawiamy w dalszej części przewodnika.
Krotki
Krotki są kolejną popularną wbudowaną strukturą danych w Pythonie. Działają podobnie do list, umożliwiając szybki dostęp do elementów za pomocą indeksowania i wycinania. Kluczową różnicą jest jednak to, że są one niezmienne, więc nie można ich modyfikować po utworzeniu. Poniższy przykład pokazuje te różnice:
>>> nums = (5,4,3,2) >>> nums[0] 5 >>> nums[0:2] (5, 4) >>> 5 in nums True >>> nums[0] = 7 # To nie zadziała! Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment
Jeśli więc chcesz utworzyć niezmienną kolekcję, która będzie wydajnie przetwarzana, warto rozważyć użycie krotki. W sytuacji, gdy potrzebujesz modyfikowalnej kolekcji, lepiej sprawdzi się lista.
📋 Dowiedz się więcej na temat podobieństw i różnic pomiędzy listami i krotkami w Pythonie.
Tablice
Tablice są mniej znaną strukturą danych w Pythonie. Pod względem operacji, takich jak dostęp w czasie stałym i wstawianie elementu w określonym miejscu, przypominają listy.
Główną różnicą między listami a tablicami jest to, że tablice przechowują elementy tylko jednego typu. Z tego powodu są one ściślej powiązane i bardziej wydajne pod względem wykorzystania pamięci.
Aby utworzyć tablicę, używamy konstruktora `array()` z wbudowanego modułu `array`. Konstruktor `array()` pobiera jako argument ciąg znaków określający typ danych oraz elementy. W poniższym przykładzie tworzymy tablicę `nums_f` przechowującą liczby zmiennoprzecinkowe:
>>> from array import array >>> nums_f = array('f',[1.5,4.5,7.5,2.5]) >>> nums_f array('f', [1.5, 4.5, 7.5, 2.5])
Możemy uzyskać dostęp do elementów tablicy przy pomocy indeksowania (podobnie jak w listach):
>>> nums_f[0] 1.5
Tablice są zmienne, więc można je modyfikować:
>>> nums_f[0]=3.5 >>> nums_f array('f', [3.5, 4.5, 7.5, 2.5])
Nie możemy jednak zmienić typu danych elementu:
>>> nums_f[0]='zero' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: must be real number, not str
Łańcuchy znaków
W Pythonie łańcuchy znaków są niezmiennymi kolekcjami znaków Unicode. W przeciwieństwie do języków takich jak C, Python nie posiada dedykowanego typu danych znakowych. W Pythonie pojedynczy znak to łańcuch o długości jeden.
Jak wspomniano, łańcuchy znaków są niezmienne:
>>> str_1 = 'python' >>> str_1[0] = 'c' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
Łańcuchy znaków w Pythonie wspierają operacje wycinania oraz posiadają szereg metod formatowania. Oto kilka przykładów:
>>> str_1[1:4] 'yth' >>> str_1.title() 'Python' >>> str_1.upper() 'PYTHON' >>> str_1.swapcase() 'PYTHON'
⚠ Należy pamiętać, że wszystkie powyższe operacje zwracają kopię łańcucha i nie modyfikują oryginalnego łańcucha. Jeśli jesteś zainteresowany, zachęcamy do zapoznania się z przewodnikiem dotyczącym operacji na łańcuchach w Pythonie.
Zbiory
W Pythonie zbiory są kolekcjami unikalnych i haszowalnych elementów. Możemy na nich wykonywać typowe operacje zbiorowe, takie jak suma, przecięcie i różnica:
>>> set_1 = {3,4,5,7} >>> set_2 = {4,6,7} >>> set_1.union(set_2) {3, 4, 5, 6, 7} >>> set_1.intersection(set_2) {4, 7} >>> set_1.difference(set_2) {3, 5}
Zbiory domyślnie są zmienne, więc możemy dodawać i modyfikować ich elementy:
>>> set_1.add(10) >>> set_1 {3, 4, 5, 7, 10}
📚 Przeczytaj więcej o zbiorach w Pythonie: kompletny przewodnik z przykładami kodu.
Frozen Zbiory
Jeśli potrzebujesz niezmiennego zbioru, możesz użyć `frozenset`. Możemy go stworzyć z istniejącego zbioru lub dowolnego iterowalnego obiektu.
>>> frozenset_1 = frozenset(set_1) >>> frozenset_1 frozenset({3, 4, 5, 7, 10})
Ponieważ `frozenset_1` jest niezmiennym zbiorem, próba dodania elementów (lub innej modyfikacji) zakończy się błędem:
>>> frozenset_1.add(15) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'frozenset' object has no attribute 'add'
Słowniki
Słowniki w Pythonie działają podobnie do map skrótów. Przechowują one pary klucz-wartość. Klucze słownika muszą być haszowalne, czyli ich wartość skrótu nie zmienia się w czasie życia obiektu.
Dostęp do wartości, wstawianie nowych elementów i usuwanie istniejących odbywa się w czasie stałym. Python udostępnia metody słownikowe do wykonywania tych operacji.
>>> favorites = {'book':'Orlando'} >>> favorites {'book': 'Orlando'} >>> favorites['author']='Virginia Woolf' >>> favorites {'book': 'Orlando', 'author': 'Virginia Woolf'} >>> favorites.pop('author') 'Virginia Woolf' >>> favorites {'book': 'Orlando'}
OrderedDict
Słowniki Pythona zapewniają mapowanie klucz-wartość, ale są z natury nieuporządkowane. Od wersji Pythona 3.7 kolejność wstawiania elementów jest zachowywana, ale aby podkreślić to zachowanie, można użyć `OrderedDict` z modułu `collections`.
Jak widać, `OrderedDict` zachowuje kolejność kluczy:
>>> from collections import OrderedDict >>> od = OrderedDict() >>> od['first']='one' >>> od['second']='two' >>> od['third']='three' >>> od OrderedDict([('first', 'one'), ('second', 'two'), ('third', 'three')]) >>> od.keys() odict_keys(['first', 'second', 'third'])
Defaultdict
Podczas pracy ze słownikami w Pythonie, błędy związane z brakiem klucza są dość powszechne. Za każdym razem, gdy próbujesz uzyskać dostęp do klucza, który nie został jeszcze dodany do słownika, otrzymasz wyjątek `KeyError`.
Używając `defaultdict` z modułu `collections`, możemy obsłużyć takie sytuacje. Kiedy próbujemy uzyskać dostęp do klucza, którego nie ma w słowniku, klucz jest dodawany i inicjalizowany wartością domyślną, określoną przez domyślną fabrykę.
>>> from collections import defaultdict >>> prices = defaultdict(int) >>> prices['carrots'] 0
Stosy
Stos to struktura danych, w której elementy są dodawane i usuwane w kolejności „ostatni na wejściu, pierwszy na wyjściu” (LIFO – Last-In-First-Out). Operacje na stosie to:
- Dodawanie elementów na wierzch stosu (push).
- Usuwanie elementów z wierzchu stosu (pop).
Poniższy schemat ilustruje operacje push i pop na stosie:
Implementacja stosu za pomocą listy
W Pythonie stos można zaimplementować za pomocą listy.
Operacje na stosie i ich odpowiedniki na liście:
Operacja stosu | Operacja na liście
Push na szczyt stosu | Dodawanie na koniec listy za pomocą `append()`
Pop ze szczytu stosu | Usuwanie i zwracanie ostatniego elementu za pomocą `pop()`
Poniższy przykład pokazuje, jak naśladować działanie stosu za pomocą listy Pythona:
>>> l_stk = [] >>> l_stk.append(4) >>> l_stk.append(3) >>> l_stk.append(7) >>> l_stk.append(2) >>> l_stk.append(9) >>> l_stk [4, 3, 7, 2, 9] >>> l_stk.pop() 9
Implementacja stosu za pomocą Deque
Stos można również zaimplementować za pomocą `deque` z modułu `collections`. `Deque` (dwustronna kolejka) obsługuje dodawanie i usuwanie elementów z obu końców.
Aby stworzyć stos za pomocą `deque`, możemy:
- Dodawać elementy na końcu deque używając `append()`
- Pobierać ostatnio dodany element używając `pop()`
>>> from collections import deque >>> stk = deque() >>> stk.append(4) >>> stk.append(3) >>> stk.append(7) >>> stk.append(2) >>> stk.append(9) >>> stk deque([4, 3, 7, 2, 9]) >>> stk.pop() 9
Kolejki
Kolejka to struktura danych, w której elementy są dodawane na końcu i usuwane z początku, zgodnie z zasadą „pierwszy na wejściu, pierwszy na wyjściu” (FIFO – First-In-First-Out). Poniższy schemat ilustruje zasadę działania kolejki:
Kolejkę możemy zaimplementować za pomocą `deque`:
- Dodajemy elementy na koniec kolejki za pomocą `append()`.
- Usuwamy element z początku kolejki za pomocą `popleft()`.
>>> from collections import deque >>> q = deque() >>> q.append(4) >>> q.append(3) >>> q.append(7) >>> q.append(2) >>> q.append(9) >>> q.popleft() 4
Stosy binarne (Heap)
W tej części omówimy sterty binarne, skupiając się na stertach typu min-heap.
Sterta typu min-heap to kompletne drzewo binarne, gdzie każdy węzeł jest mniejszy od swoich potomków. Wyjaśnijmy, co znaczy „kompletne drzewo binarne”:
- Drzewo binarne to struktura danych, gdzie każdy węzeł ma co najwyżej dwa węzły potomne.
- „Kompletne” oznacza, że drzewo jest całkowicie wypełnione, być może z wyjątkiem ostatniego poziomu. Jeśli ostatni poziom jest częściowo wypełniony, wypełniany jest od lewej do prawej.
Z powodu tej własności, korzeń drzewa min-heap jest zawsze najmniejszym elementem.
Poniżej przykład drzewa min-heap:
W Pythonie, moduł `heapq` pomaga konstruować sterty i operować na nich. Zaimportujmy wymagane funkcje z `heapq`:
>>> from heapq import heapify, heappush, heappop
Jeśli mamy listę lub inny iterowalny obiekt, możemy zbudować z niego stertę używając `heapify()`:
>>> nums = [11,8,12,3,7,9,10] >>> heapify(nums)
Możemy sprawdzić, czy pierwszy element jest rzeczywiście najmniejszy, uzyskując dostęp do niego za pomocą indeksowania:
>>> nums[0] 3
Po dodaniu nowego elementu do sterty, węzły są automatycznie przestawiane, aby zachować własność min-heap.
>>> heappush(nums,1)
Ponieważ 1 jest mniejsze niż 3, `nums[0]` teraz zwraca 1, który jest najmniejszym elementem (i węzłem głównym).
>>> nums[0] 1
Elementy ze sterty min-heap usuwamy za pomocą funkcji `heappop()`:
>>> while nums: ... print(heappop(nums)) ...
# Output 1 3 7 8 9 10 11 12
Sterty maksymalne (Max Heap) w Pythonie
Skoro wiemy już, czym są sterty minimalne, możemy z łatwością zaimplementować stertę maksymalną. Aby to zrobić, musimy jedynie pomnożyć każdą liczbę przez -1. W ten sposób, liczby ułożone w kopiec min, stają się równoważne liczbom ułożonym w stercie max.
W Pythonie możemy pomnożyć elementy przez -1 podczas dodawania elementu do sterty za pomocą `heappush()`:
>>> maxHeap = [] >>> heappush(maxHeap,-2) >>> heappush(maxHeap,-5) >>> heappush(maxHeap,-7)
Węzeł główny – pomnożony przez -1 – będzie maksymalnym elementem.
>>> -1*maxHeap[0] 7
Usuwając elementy ze sterty, korzystamy z `heappop()`, a następnie mnożymy wartość przez -1, aby odzyskać pierwotną wartość:
>>> while maxHeap: ... print(-1*heappop(maxHeap)) ...
# Output 7 5 2
Kolejki priorytetowe
Na koniec omówimy strukturę danych kolejki priorytetowej w Pythonie.
W standardowej kolejce elementy są usuwane w kolejności, w jakiej zostały dodane. Kolejka priorytetowa, obsługuje elementy według przypisanego im priorytetu. Jest to bardzo przydatne w zastosowaniach takich jak planowanie. W każdej chwili zwracany jest element o najwyższym priorytecie.
Do określenia priorytetu możemy wykorzystać klucze. W naszym przykładzie użyjemy wartości liczbowych dla kluczy.
Implementacja kolejki priorytetowej za pomocą heapq
Poniżej znajduje się implementacja kolejki priorytetowej z wykorzystaniem biblioteki heapq:
>>> from heapq import heappush,heappop >>> pq = [] >>> heappush(pq,(2,'write')) >>> heappush(pq,(1,'read')) >>> heappush(pq,(3,'code')) >>> while pq: ... print(heappop(pq)) ...
Podczas usuwania elementów, kolejka wybiera najpierw element o najwyższym priorytecie (1, 'read’), następnie (2, 'write’), i na końcu (3, 'code’).
# Output (1, 'read') (2, 'write') (3, 'code')
Implementacja kolejki priorytetowej za pomocą PriorityQueue
Do implementacji kolejki priorytetowej można także użyć klasy `PriorityQueue` z modułu `queue`. Wewnętrznie wykorzystuje ona strukturę sterty.
Poniżej znajduje się implementacja kolejki priorytetowej przy pomocy `PriorityQueue`:
>>> from queue import PriorityQueue >>> pq = PriorityQueue() >>> pq.put((2,'write')) >>> pq.put((1,'read')) >>> pq.put((3,'code')) >>> pq <queue.PriorityQueue object at 0x00BDE730> >>> while not pq.empty(): ... print(pq.get()) ...
# Output (1, 'read') (2, 'write') (3, 'code')
Podsumowanie
W tym artykule omówiliśmy różne wbudowane struktury danych w Pythonie, przedstawiając operacje, które można na nich wykonywać oraz metody do tego służące.
Przyjrzeliśmy się także innym strukturom, takim jak stosy, kolejki i kolejki priorytetowe, oraz sposobom ich implementacji w Pythonie przy użyciu funkcjonalności modułu `collections`.
Na zakończenie, zachęcamy do zapoznania się z listą projektów Pythona przyjaznych dla początkujących programistów.