Tablica haszująca (hash table)
Ostatnia aktualizacja
Tablica haszująca przechowuje elementy w tablicy kubełków. Aby umieścić lub znaleźć klucz, przepuszcza go przez funkcję haszującą i bierze wynik modulo liczba kubełków, co daje indeks kubełka w O(1). Kliknij odtwarzanie powyżej i zobacz, jak każdy klucz jest haszowany do kubełka i zapisywany, a wyszukiwanie przeskakuje prosto do właściwego kubełka.
Gdy dwa klucze trafiają do tego samego kubełka, czyli dochodzi do kolizji, ta wizualizacja używa metody łańcuchowej: każdy kubełek przechowuje małą listę, a kolidujące klucze są do niej dopisywane. Dopóki tablica nie jest zbyt pełna (niski współczynnik zapełnienia), a hasz dobrze rozkłada klucze, łańcuchy pozostają krótkie, a wstawianie, wyszukiwanie i usuwanie działają średnio w O(1).
Złożoność czasowa
| Operacja | Średnio | Najgorszy przypadek |
|---|---|---|
| Wstawianie | O(1) | O(n) (wszystkie klucze kolidują) |
| Wyszukiwanie | O(1) | O(n) |
| Usuwanie | O(1) | O(n) |
| Pamięć | O(n) | O(n) |
Kluczowe pojęcia
| Pojęcie | Znaczenie |
|---|---|
| Funkcja haszująca | Odwzorowuje klucz na indeks kubełka |
| Kolizja | Dwa klucze trafiają do tego samego kubełka |
| Metoda łańcuchowa | Każdy kubełek przechowuje listę kolidujących wpisów |
| Adresowanie otwarte | Alternatywa: szukanie kolejnego wolnego miejsca |
| Współczynnik zapełnienia | wpisy / kubełki: decyduje o zmianie rozmiaru |
Przykład krok po kroku
Wstawianie kluczy 20, 34, 9, 13 do tablicy z 7 kubełkami przy użyciu hash(k) = k % 7:
| Krok | Struktura | Działanie |
|---|---|---|
Wstaw 20 | kubełek 6: [20] | 20 % 7 = 6, kubełek 6 pusty, zapisz 20 |
Wstaw 34 | kubełek 6: [20, 34] | 34 % 7 = 6, kolizja z 20, dopisz 34 do łańcucha |
Wstaw 9 | kubełek 2: [9] | 9 % 7 = 2, kubełek 2 pusty, zapisz 9 |
Wstaw 13 | kubełek 6: [20, 34, 13] | 13 % 7 = 6, kolizja, dopisz 13 do łańcucha kubełka 6 |
Wyszukaj 34 | kubełek 6: [20, 34, 13] | 34 % 7 = 6, przejrzyj łańcuch: 20 nie, 34 pasuje: znaleziono |
Kiedy używać tablicy haszującej
| Używaj, gdy | Unikaj, gdy |
|---|---|
Potrzebujesz średnio O(1) na wstawianie, wyszukiwanie i usuwanie po kluczu | Potrzebujesz kluczy w posortowanej kolejności: użyj zrównoważonego drzewa BST |
| Klucze nie mają użytecznego porządku i sprawdzasz tylko dokładną przynależność | Potrzebujesz zapytań o zakres lub szukania najbliższego klucza |
| Stać cię na trochę dodatkowej pamięci na kubełki i niski współczynnik zapełnienia | Pamięci jest skrajnie mało i liczy się każdy bajt |
| Istnieje dobra funkcja haszująca dla twojego typu klucza | Czas odpowiedzi w najgorszym przypadku musi być ograniczony: kolizje dają O(n) |
Hash Table: kod
Przejrzysta, gotowa do uruchomienia implementacja algorytmu Hash Table w językach: Python, JavaScript, Java, C++, C. Wybierz język, skopiuj kod albo otwórz go od razu w edytorze online Coddy.
Hash Table: kod (Python)
1class HashTable:2 def __init__(self, size=8):3 self.size = size4 self.buckets = [[] for _ in range(size)]5
6 def _index(self, key):7 # Hash the key to a bucket; different keys can collide8 return sum(ord(ch) for ch in key) % self.size9
10 def set(self, key, value):11 bucket = self.buckets[self._index(key)]12 for i, (k, _) in enumerate(bucket):13 if k == key:14 bucket[i] = (key, value) # update existing key15 return16 bucket.append((key, value)) # chain on collision17
18 def get(self, key):19 for k, v in self.buckets[self._index(key)]:20 if k == key:21 return v22 raise KeyError(key)23
24 def delete(self, key):25 bucket = self.buckets[self._index(key)]26 for i, (k, _) in enumerate(bucket):27 if k == key:28 del bucket[i]29 return30 raise KeyError(key)31
32
33table = HashTable()34table.set("apple", 3)35table.set("banana", 7)36table.set("cherry", 5)37
38print("apple ->", table.get("apple"))39print("banana ->", table.get("banana"))40table.set("apple", 10)41print("apple ->", table.get("apple"))42table.delete("banana")43print("bucket sizes:", [len(b) for b in table.buckets])Hash Table: kod (JavaScript)
1class HashTable {2 constructor(size = 8) {3 this.size = size;4 this.buckets = Array.from({ length: size }, () => []);5 }6
7 // Fold character codes into a bucket index8 hash(key) {9 let h = 0;10 for (const ch of key) h = (h * 31 + ch.charCodeAt(0)) % this.size;11 return h;12 }13
14 set(key, value) {15 const bucket = this.buckets[this.hash(key)];16 const pair = bucket.find(([k]) => k === key);17 if (pair) pair[1] = value;18 else bucket.push([key, value]); // Separate chaining on collision19 }20
21 get(key) {22 const bucket = this.buckets[this.hash(key)];23 const pair = bucket.find(([k]) => k === key);24 return pair ? pair[1] : undefined;25 }26
27 delete(key) {28 const bucket = this.buckets[this.hash(key)];29 const index = bucket.findIndex(([k]) => k === key);30 if (index === -1) return false;31 bucket.splice(index, 1);32 return true;33 }34}35
36const table = new HashTable();37table.set("apple", 3);38table.set("banana", 7);39table.set("cherry", 5);40table.set("apple", 4);41console.log("apple ->", table.get("apple"));42console.log("banana in bucket", table.hash("banana"));43table.delete("cherry");44console.log("cherry after delete:", table.get("cherry"));Hash Table: kod (Java)
1public class Main {2 static class Entry {3 String key; int value;4 Entry next; // chain for keys sharing a bucket5 Entry(String key, int value) { this.key = key; this.value = value; }6 }7
8 static final int CAPACITY = 8;9 static Entry[] buckets = new Entry[CAPACITY];10
11 static int index(String key) { return Math.abs(key.hashCode()) % CAPACITY; }12
13 static void put(String key, int value) {14 int i = index(key);15 for (Entry e = buckets[i]; e != null; e = e.next) {16 if (e.key.equals(key)) { e.value = value; return; }17 }18 Entry entry = new Entry(key, value);19 entry.next = buckets[i]; // prepend to the chain20 buckets[i] = entry;21 }22
23 static Integer get(String key) {24 for (Entry e = buckets[index(key)]; e != null; e = e.next) {25 if (e.key.equals(key)) return e.value;26 }27 return null;28 }29
30 static void remove(String key) {31 int i = index(key);32 Entry prev = null;33 for (Entry e = buckets[i]; e != null; prev = e, e = e.next) {34 if (e.key.equals(key)) {35 if (prev == null) buckets[i] = e.next;36 else prev.next = e.next;37 return;38 }39 }40 }41
42 public static void main(String[] args) {43 put("apple", 3); put("banana", 7); put("cherry", 5);44 System.out.println("apple -> " + get("apple"));45 put("apple", 10); // update in place46 System.out.println("apple -> " + get("apple"));47 remove("banana");48 System.out.println("banana -> " + get("banana"));49 }50}Hash Table: kod (C++)
1#include <iostream>2#include <string>3#include <vector>4
5struct HashTable {6 static constexpr int CAPACITY = 8;7 // Separate chaining: each bucket holds (key, value) pairs8 std::vector<std::vector<std::pair<std::string, int>>> buckets{CAPACITY};9
10 size_t hash(const std::string& key) const {11 size_t h = 0;12 for (char c : key) h = h * 31 + static_cast<unsigned char>(c);13 return h % CAPACITY;14 }15
16 void set(const std::string& key, int value) {17 auto& bucket = buckets[hash(key)];18 for (auto& entry : bucket) {19 if (entry.first == key) {20 entry.second = value; // update existing key21 return;22 }23 }24 bucket.push_back({key, value});25 }26
27 const int* get(const std::string& key) const {28 for (const auto& entry : buckets[hash(key)]) {29 if (entry.first == key) return &entry.second;30 }31 return nullptr;32 }33
34 void erase(const std::string& key) {35 auto& bucket = buckets[hash(key)];36 for (size_t i = 0; i < bucket.size(); ++i) {37 if (bucket[i].first == key) {38 bucket.erase(bucket.begin() + i);39 return;40 }41 }42 }43};44
45int main() {46 HashTable table;47 table.set("apple", 3);48 table.set("banana", 7);49 table.set("apple", 4); // overwrites the old value50 for (const std::string& key : {"apple", "banana", "grape"}) {51 const int* value = table.get(key);52 if (value != nullptr) std::cout << key << " = " << *value << "\n";53 else std::cout << key << " not found\n";54 }55 table.erase("banana");56 std::cout << "banana after erase: "57 << (table.get("banana") ? "found" : "not found") << "\n";58 return 0;59}Hash Table: kod (C)
1#include <stdio.h>2#include <stdlib.h>3#include <string.h>4
5#define CAPACITY 86
7// Separate chaining: each bucket is a linked list of entries8typedef struct Entry {9 char key[16];10 int value;11 struct Entry* next;12} Entry;13
14Entry* buckets[CAPACITY];15
16unsigned int hash(const char* key) {17 unsigned int h = 5381; // djb2, then modulo the bucket count18 for (; *key; key++) h = h * 33 + (unsigned char)*key;19 return h % CAPACITY;20}21
22void set(const char* key, int value) {23 unsigned int i = hash(key);24 for (Entry* e = buckets[i]; e != NULL; e = e->next) {25 if (strcmp(e->key, key) == 0) {26 e->value = value; // update existing key27 return;28 }29 }30 Entry* e = malloc(sizeof(Entry));31 strcpy(e->key, key);32 e->value = value;33 e->next = buckets[i];34 buckets[i] = e;35}36
37int* get(const char* key) {38 for (Entry* e = buckets[hash(key)]; e != NULL; e = e->next) {39 if (strcmp(e->key, key) == 0) return &e->value;40 }41 return NULL;42}43
44void removeKey(const char* key) {45 Entry** link = &buckets[hash(key)];46 while (*link != NULL) {47 if (strcmp((*link)->key, key) == 0) {48 Entry* old = *link;49 *link = old->next;50 free(old);51 return;52 }53 link = &(*link)->next;54 }55}56
57void printBuckets(void) {58 for (int i = 0; i < CAPACITY; i++) {59 printf("bucket %d:", i);60 for (Entry* e = buckets[i]; e != NULL; e = e->next) {61 printf(" (%s=%d)", e->key, e->value);62 }63 printf("\n");64 }65}66
67int main(void) {68 set("apple", 3);69 set("banana", 7);70 set("apple", 4); // overwrites the old value71 set("grape", 9);72 printBuckets();73 int* value = get("apple");74 printf("get(apple): %d\n", value ? *value : -1);75 removeKey("banana");76 printf("banana after remove: %s\n", get("banana") ? "found" : "not found");77 return 0;78}Tablica haszująca: najczęstsze pytania
Czym jest kolizja haszy i jak się ją rozwiązuje?
Jaka jest złożoność czasowa tablicy haszującej?
O(1), gdy funkcja haszująca równomiernie rozkłada klucze, a współczynnik zapełnienia jest niski. W najgorszym przypadku, gdy każdy klucz koliduje w jednym kubełku, degradują się do O(n), dlatego dobra funkcja haszująca i zmiana rozmiaru mają znaczenie.Czym jest współczynnik zapełnienia?
Kiedy użyć tablicy haszującej zamiast binarnego drzewa poszukiwań?
O(1) bez wymogu porządku. Użyj zrównoważonego binarnego drzewa poszukiwań, gdy potrzebujesz kluczy w posortowanej kolejności, zapytań o zakres lub szukania poprzednika i następnika, czego tablica haszująca nie zrobi wydajnie. Drzewo BST daje gwarantowane operacje w O(log n), a tablica haszująca wymienia tę gwarancję na szybszą średnią wydajność.Czym różni się metoda łańcuchowa od adresowania otwartego?
Dlaczego nie mogę polegać na tym, że tablica haszująca zachowa kolejność wstawiania lub sortowania kluczy?
dict w Pythonie zachowuje kolejność wstawiania, ale to gwarancja języka, a nie wrodzona cecha tablicy haszującej). Nigdy nie zakładaj, że kolejność iteracji odpowiada kolejności wstawiania kluczy, chyba że język wyraźnie to obiecuje.