Bölüm 12: Kümeler
Giriş
Öğrenme Çıktıları
Küme ADT'sini ve Java Set arayüzünü açıklamak.
Tekrarlı eleman gerektiren problemlerde çoklu küme yaklaşımını kullanmak.
Yol sıkıştırma ve rütbeye göre birleştirme yöntemlerini uygulamak.
Union-Find yapısını Kruskal algoritması ve bağlılık problemleriyle ilişkilendirmek.
Küme ADT
Küme (Set)
- Küme (set), benzersiz (tekil) öğelerden oluşan bir koleksiyondur: aynı öğe iki kez bulunmaz.
- Matematikteki küme kavramının veri yapısı karşılığıdır.
- Öğelerin sırası önemsizdir: {1, 2, 3} = {3, 1, 2}.
- Temel soru: "x kümede var mı?" (üyelik testi) hızlı yanıtlanmalıdır.
Küme ADT
Küme İşlemleri (İşleçler)
A'da veya B'de olanlar
Hem A'da hem B'de olanlar
A'da olup B'de olmayanlar
A'nın her öğesi B'de
| A = {1, 2, 3, 4, 5}, B = {3, 4, 5, 6, 7} | Sonuç |
|---|---|
| Birleşim A ∪ B | {1, 2, 3, 4, 5, 6, 7} |
| Kesişim A ∩ B | {3, 4, 5} |
| Fark A − B / B − A | {1, 2} / {6, 7} |
| Alt küme? | A ⊄ B (1 ∉ B) · {3, 4} ⊆ A ✓ |
Küme ADT
java.util.Set Arayüzü
| Metot | Açıklama |
|---|---|
add(e) | ekler; varsa false |
remove(o), contains(o) | çıkarır; üyeliği sorgular |
addAll(T) | birleşim: S ← S ∪ T |
retainAll(T) | kesişim: S ← S ∩ T |
removeAll(T) | fark: S ← S − T |
Set<Integer> a = new HashSet<>(List.of(1,2,3,4,5)); Set<Integer> b = new HashSet<>(List.of(3,4,5,6,7)); Set<Integer> bir = new HashSet<>(a); bir.addAll(b); // [1..7] Set<Integer> kes = new HashSet<>(a); kes.retainAll(b); // [3, 4, 5] Set<Integer> fark = new HashSet<>(a); fark.removeAll(b); // [1, 2] a.containsAll(b); // false
addAll, retainAll, removeAll kümeyi yerinde değiştirir; a'yı korumak için önce kopyalanır.
Küme Uygulamaları
java.util.Set Uygulamaları
| Sınıf | İç yapı | Gezinme sırası | add / contains / remove |
|---|---|---|---|
| HashSet | hash tablosu (Bölüm 9) | hash koduna bağlı, öngörülemez | O(1) ortalama |
| LinkedHashSet | hash tablosu + çift bağlı liste | ekleme sırası | O(1) ortalama |
| TreeSet | kırmızı-siyah ağaç (Bölüm 10) | sıralı (küçükten büyüğe, alfabetik) | O(log n) |
for (String s : List.of("elma", "kiraz", "armut", "elma")) { hash.add(s); linked.add(s); tree.add(s); } // HashSet → [armut, kiraz, elma] (sıra hash'e bağlı, farklı olabilir) // LinkedHashSet → [elma, kiraz, armut] (eklenme sırası) // TreeSet → [armut, elma, kiraz] (alfabetik)
Küme Uygulamaları
Hash Tabanlı Küme
- Küme bir hash tablosu olarak temsil edilir (Bölüm 9); yalnızca anahtar saklanır, değer yoktur.
- Her öğenin konumu hash koduna göre hızla hesaplanır.
- Ekleme, silme ve arama ortalama O(1); kötü hash ile en kötü O(n).
- Java'da
HashSetiçeride birHashMapkullanır.
Başlangıç: 7 kovalı boş tablo. Konum = hashCode(x) mod 7.
add(12): 12 mod 7 = 5 → kova 5 boş, eklenir.
add(25): 25 mod 7 = 4 → kova 4'e eklenir.
add(7): 7 mod 7 = 0 → kova 0'a eklenir.
add(19): 19 mod 7 = 5 → çakışma! Kova 5'te 12 var; 19 zincirin sonuna eklenir.
add(12) tekrar: kova 5 taranır, 12 bulunur → eklenmez, false döner. Kümeyi kümeyapan budur.
contains(26): 26 mod 7 = 5 → yalnızca kova 5 (12, 19) taranır, bulunamaz → false. Diğer kovalara hiç bakılmaz.
Küme Uygulamaları
Ağaç Tabanlı (Sıralı) Küme
- Küme bir dengeli ikili arama ağacı olarak temsil edilir (Java
TreeSet: kırmızı-siyah ağaç). - Öğeler sıralı saklanır; in-order gezinme sıralı çıktı verir.
- Arama, ekleme, silme O(log n).
- Öğeler karşılaştırılabilir olmalı (
ComparableveyaComparator).
Sıralı yapının getirdiği ek işlemler (S = {5, 10, 15, 20, 25, 30, 40}):
Küme Uygulamaları
Sıralı Yapıyla Birleşim ve Kesişim
Küme sıralı dizi/liste olarak tutulursa birleşim ve kesişim, birleştirme sıralamasındaki merge adımıyla tek geçişte bulunur (Goodrich: genel birleştirme, generic merge):
Başlangıç: iki sıralı dizi, i = j = 0. Her adımda A[i] ile B[j] karşılaştırılır.
A[i] = 1 < B[j] = 3 → 1 yalnızca A'da: birleşime eklenir, i++.
A[i] = B[j] = 3 → ortak öğe: hem birleşime hem kesişime (bir kez) eklenir, i++ ve j++.
B[j] = 4 < A[i] = 5 → 4 yalnızca B'de: birleşime eklenir, j++.
A[i] = B[j] = 5 → ortak öğe: hem birleşime hem kesişime (bir kez) eklenir, i++ ve j++.
A[i] = 7 < B[j] = 8 → 7 yalnızca A'da: birleşime eklenir, i++.
B[j] = 8 < A[i] = 9 → 8 yalnızca B'de: birleşime eklenir, j++.
A[i] = 9 (B bitti) → 9 yalnızca A'da: birleşime eklenir, i++. İki dizi de bitti: toplam O(n + m) karşılaştırma.
Küme Uygulamaları
Küme Uygulamalarının Karşılaştırması
| İşlem (|A| = n, |B| = m) | Sırasız liste | Sıralı dizi | Hash tablosu | Dengeli BST |
|---|---|---|---|---|
contains | O(n) | O(log n) | O(1)* | O(log n) |
add / remove | O(n)† | O(n) | O(1)* | O(log n) |
| A ∪ B, A ∩ B, A − B | O(n·m) | O(n + m) | O(n + m)* | O(m log(n+m)) |
| Sıralı gezinme | O(n log n) | O(n) | O(n log n) | O(n) |
| En küçük / floor / aralık | O(n) | O(log n) | O(n) | O(log n) |
* ortalama durum (en kötü O(n)). † tekrar kontrolü için önce arama gerekir. Hash ile birleşim: B'nin her öğesi için contains + add.
Çoklu Küme
Çoklu Küme (Multiset / Bag)
- Çoklu küme (multiset, bag): aynı öğenin birden çok kez bulunabildiği küme.
- Sıra yine önemsizdir; önemli olan her öğenin kaç kez geçtiğidir (çokluk, multiplicity).
- Doğal uygulama: öğe → sayaç eşlemesi (Bölüm 9).
- Örnek: bir metindeki kelime frekansları, alışveriş sepeti, oy sayımı.
| Öğe | Sayaç |
|---|---|
| a | 2 |
| b | 1 |
| c | 3 |
M = {a, a, b, c, c, c} → eşleme {a:2, b:1, c:3}
| Multiset ADT (Goodrich) | Anlamı | Hash ile |
|---|---|---|
add(e) / add(e, n) | e'yi 1 / n kez ekle (sayaç += n) | O(1)* |
count(e) | e kaç kez var? (yoksa 0) | O(1)* |
remove(e) / remove(e, n) | sayacı azalt; 0 olursa girdiyi sil | O(1)* |
size() | tekrarlar dahil öğe sayısı (örnekte 6) | O(1) |
Çoklu Küme
Java ile Çoklu Küme
class CokluKume<E> { private final Map<E, Integer> say = new HashMap<>(); private int n = 0; // tekrarlar dahil öğe sayısı void add(E e) { say.merge(e, 1, Integer::sum); n++; } // yoksa 1, varsa +1 int count(E e) { return say.getOrDefault(e, 0); } boolean remove(E e) { Integer c = say.get(e); if (c == null) return false; // e yok if (c == 1) say.remove(e); // sayaç 0 olacak: girdiyi sil else say.put(e, c - 1); n--; return true; } int size() { return n; } }
count("a") → 2
add("a") etkisiz; burada sayaç artar.TreeMap<E, Integer> ile sıralı gezinme.Ayrık Kümeler (Union-Find)
Ayrık Kümeler ve Union-Find
- Ayrık kümeler (disjoint sets): hiçbir öğesi ortak olmayan kümeler topluluğu. Her öğe tam olarak bir kümeye aittir.
- Union-Find (Disjoint Set Union, DSU) bu kümeleri tutan veri yapısıdır. İki ana işlemi vardır:
Her küme bir ağaç: her düğüm atasını (parent) gösterir; kök kendini gösterir ve kümenin temsilcisidir.
Ayrık Kümeler (Union-Find)
Mıknatıs Örneği
- Birbirini güçlü şekilde çeken mıknatıslar düşünelim: iki mıknatıs değince ayrılmaz.
- Birleşmeler sürerken "hangi mıknatıs hangi grupta?" sorusunu izlemek istiyoruz.
- Önce her mıknatısa bir numara verilir.
- Birleşme =
union, "aynı grupta mı?" = ikifindkarşılaştırması.
14 mıknatıs, her biri kendi grubunda: 14 ayrık küme. find(x) = x.
union(6, 8): en yakın olan 6 ile 8 birleşir; artık find(6) = find(8).
union(3, 4): 3 ile 4 birbirini çeker, yeni grup {3, 4}.
union(2, 3): 2, {3, 4} grubuna katılır → {2, 3, 4}.
union(9, 10), union(13, 14), union(9, 13) → {9, 10, 13, 14}.
union(5, 7), union(7, 11), union(11, 12) → {5, 7, 11, 12}. Beş grup kaldı (1 tek başına).
4 ile 10 aynı grupta mı? find(4) ≠ find(10) → hayır. union(4, 10) iki grubu birleştirir.
Birleşmeler sürer ve sonunda tüm mıknatıslar tek grupta toplanır: find(x) herkes için aynı kökü verir.
Ayrık Kümeler (Union-Find)
Find ve Union İşlemleri
- find(x): x'i içeren kümenin adını (kökünü) döndürür. Ata bağlantıları kök bulunana kadar izlenir.
- Bu, arama ağaçlarındaki (Bölüm 10) arama ile aynı şey değildir: değer aranmaz, yukarı doğru köke çıkılır.
- union(x, y): iki kümenin yerine, ikisinin birleşimi olan tek bir küme yaratır: bir kökü diğerinin altına bağlar.
find(x): if x.ata != x: x.ata = find(x.ata) // yol kısaltma return x.ata union(x, y): kök1 = find(x) kök2 = find(y) if kök1 != kök2: // bir kümenin kökünü diğer // kümenin köküne ata kök1.ata = kök2
Ayrık Kümeler (Union-Find)
Nesneleri İndislere Eşleme
- Union-Find en verimli hâliyle dizi üzerinde çalışır: öğeler 0..n−1 tamsayılarıdır.
- Öğeler harf, şehir, kişi… ise önce her nesne bir indise rastgele (ama kalıcı) eşlenir ve bu eşleme bir hash tablosunda saklanır.
- Sonra
ata[i] = iile her öğe kendi kümesinin kökü olarak başlar (makeSet).
Map<Character, Integer> indis = new HashMap<>(); // 'E'→0, 'F'→1, … int[] ata = new int[12]; for (int i = 0; i < 12; i++) ata[i] = i; // 12 tekil küme
Ayrık Kümeler (Union-Find)
Union İşlemi Adım Adım
Başlangıç: 12 tekil küme, ata[i] = i. Kural: küçük ağacın kökü büyüğün köküne bağlanır; eşitse ikinci kök birinciye bağlanır.
union(C, K): find(C) = C, find(K) = K; boyutlar eşit (1) → K, C'nin altına bağlanır (ata[9] = 4).
union(F, E): find(F) = F, find(E) = E; boyutlar eşit (1) → E, F'nin altına bağlanır (ata[0] = 1).
union(A, J): find(A) = A, find(J) = J; boyutlar eşit (1) → J, A'nin altına bağlanır (ata[6] = 5).
union(A, B): find(A) = A, find(B) = B; boyut 1 < 2 → B, A'nin altına bağlanır (ata[10] = 5).
union(C, D): find(C) = C, find(D) = D; boyut 1 < 2 → D, C'nin altına bağlanır (ata[3] = 4).
union(D, I): find(D) = C, find(I) = I; boyut 1 < 3 → I, C'nin altına bağlanır (ata[2] = 4).
union(L, F): find(L) = L, find(F) = F; boyut 1 < 2 → L, F'nin altına bağlanır (ata[7] = 1).
union(C, A): find(C) = C, find(A) = A; boyut 3 < 4 → A, C'nin altına bağlanır (ata[5] = 4).
union(A, B): find(A) = find(B) = C → zaten aynı kümede, hiçbir şey değişmez. union(H, G): find(H) = H, find(G) = G; boyutlar eşit (1) → G, H'nin altına bağlanır (ata[8] = 11).
union(H, F): find(H) = H, find(F) = F; boyut 2 < 3 → H, F'nin altına bağlanır (ata[11] = 1).
union(H, B): find(H) = F, find(B) = C; boyut 5 < 7 → F, C'nin altına bağlanır (ata[1] = 4).
Sonuç: tek küme, kök C. H ile B aynı kümede mi? find(H): H → F → C, find(B): B → A → C → 4 adım.
Ayrık Kümeler (Union-Find)
Neden İyileştirme Gerekir?
- find, kökten uzaklık kadar adım atar. Birleştirme sırası kötü seçilirse ağaç bir zincire dönüşür.
- Örnek: union(2,1), union(3,2), union(4,3), union(5,4) ve her seferinde eski kök yeninin altına bağlanırsa find(5) = 4 adım → en kötü O(n).
Ayrık Kümeler (Union-Find)
Boyuta Göre Birleştirme (Union by Size)
- Her kökte ağacın boyutunu (öğe sayısı) sakla.
- Birleştirirken küçük ağacın kökünü büyük ağacın köküne bağla; yeni boyut = iki boyutun toplamı.
İki küme: {1, 4, 7} (boyut 3) ve {2, 3, 6, 9} (boyut 4). Yükseklikler 1 ve 2.
Naif union: büyük ağacın kökü 2, küçüğün altına bağlanırsa yükseklik 3'e çıkar; 9'un find'ı uzar.
Boyuta göre: küçük ağacın kökü 1, büyüğün kökü 2'ye bağlanır. Yükseklik 2 kalır, boyut[2] = 3 + 4 = 7.
Ayrık Kümeler (Union-Find)
Rütbeye Göre Birleştirme (Union by Rank)
- Rütbe, yüksekliğin üst sınırıdır; yeni kümede 0'dır.
- Küçük rütbeli kök, büyük rütbeli köke bağlanır.
- Rütbeler eşitse yeni kökün rütbesi bir artar.
- Bu seçim, yol kısaltma olmadan bile yüksekliği O(log n) tutar.
MakeSet(x): p[x] = x rank[x] = 0 FindSet(x): if x != p[x]: p[x] = FindSet(p[x]) // yol kısaltma return p[x] Link(x, y): // x, y kök if rank[x] > rank[y]: p[y] = x else: p[x] = y if rank[x] == rank[y]: rank[y] = rank[y] + 1 Union(x, y): Link(FindSet(x), FindSet(y))
Ayrık Kümeler (Union-Find)
Yol Kısaltma (Path Compression)
Yol kısaltma (path compression): find sırasında rastlanan tüm düğümleri doğrudan köke bağla. Sonraki find'lar tek adımda biter.
Başlangıç: 3'ün kökü 7, ama aradaki yol 3 → 6 → 5 → 7 (3 adım).
find(3): özyineleme 3 → 6 → 5 → 7 diye köke çıkar; kök 7 bulunur.
Dönüşte her düğümün atası 7 yapılır: ata[3] = ata[6] = 7 (5 zaten 7'deydi). Artık find(3), find(6) tek adım; 10'un yolu da kısaldı.
Ayrık Kümeler (Union-Find)
Union-Find Karmaşıklığı
| Uygulama | find | union | Not |
|---|---|---|---|
| Naif (rastgele bağlama) | O(n) | O(n) | ağaç zincire dönüşebilir |
| Boyuta veya rütbeye göre | O(log n) | O(log n) | yükseklik ≤ log₂ n |
| Yalnızca yol kısaltma | O(log n) amortize | m işlem için | |
| Rütbe + yol kısaltma | O(α(n)) amortize | pratikte sabit | |
ata[n] ve sira[n] (veya boyut[n]) → O(n).Ayrık Kümeler (Union-Find)
Java: Düğüm (Nesne) Tabanlı Union-Find
class SetDugum { int veri; int sira; // rütbe SetDugum ata; SetDugum(int veri) { this.veri = veri; this.ata = this; this.sira = 0; } } SetDugum find(SetDugum d) { if (d != d.ata) d.ata = find(d.ata); // kısaltma return d.ata; }
void union(SetDugum x, SetDugum y) { SetDugum kokX = find(x); SetDugum kokY = find(y); if (kokX == kokY) return; // rütbeye göre birleştirme if (kokX.sira < kokY.sira) { kokX.ata = kokY; } else if (kokX.sira > kokY.sira) { kokY.ata = kokX; } else { // eşit: birini seç kokY.ata = kokX; kokX.sira++; } }
Ayrık Kümeler (Union-Find)
Java: Dizi Tabanlı Union-Find
class UnionFind { private final int[] ata, sira; UnionFind(int n) { ata = new int[n]; sira = new int[n]; for (int i = 0; i < n; i++) ata[i] = i; } int find(int x) { if (ata[x] != x) ata[x] = find(ata[x]); return ata[x]; } boolean union(int x, int y) { int kx = find(x), ky = find(y); if (kx == ky) return false; if (sira[kx] < sira[ky]) ata[kx] = ky; else if (sira[kx] > sira[ky]) ata[ky] = kx; else { ata[kx] = ky; sira[ky]++; } return true; } }
uf.union(0, 1); uf.union(3, 4);
uf.find(1) == uf.find(0) → true
uf.find(1) == uf.find(3) → false
union neden boolean?Birleşme olduysa true: Kruskal'da "kenar döngü oluşturuyor mu?" sorusunun cevabı.Kruskal Algoritması
Minimum Kapsayan Ağaç (Minimum Spanning Tree)
- Kapsayan ağaç (spanning tree): bağlantılı, ağırlıklı bir çizgenin tüm düğümlerini içeren ve döngü içermeyen alt çizgesi; n düğüm için n − 1 kenar.
- Minimum kapsayan ağaç (MKA, MST): toplam kenar ağırlığı en düşük olan kapsayan ağaç.
- Uygulama: şehirleri en az kabloyla / boruyla birbirine bağlamak.
- Kruskal algoritması, "bu kenar döngü oluşturur mu?" sorusunu Union-Find ile yanıtlar.
Kruskal Algoritması
Kruskal Algoritması
Kruskal(G): T = ∅ // seçilen kenarlar for her düğüm v: makeSet(v) kenarları ağırlığa göre sırala for her (u, v), küçükten büyüğe: if find(u) != find(v): // döngü yok T.ekle((u, v)) union(u, v) if |T| == n − 1: break return T
- Açgözlü: her zaman döngü oluşturmayan en hafif kenarı seç.
- u ve v zaten aynı kümedeyse, kenar bir döngü kapatır → atla.
- Başta n ayrık küme; her kabul edilen kenar iki kümeyi birleştirir.
| Sıralama | O(m log m) |
| 2m find + (n−1) union | O(m · α(n)) |
| Toplam | O(m log n) |
Kruskal Algoritması
Kruskal Algoritması Adım Adım
Kenarlar ağırlığa göre sıralandı (solda). Başlangıçta 10 düğüm = 10 ayrı küme, T boş.
I–J (0): find(I) ≠ find(J) → kabul, kümeler birleşir.
A–E (1): find(A) ≠ find(E) → kabul, kümeler birleşir.
C–I (1): find(C) ≠ find(I) → kabul, kümeler birleşir.
E–F (1): find(E) ≠ find(F) → kabul, kümeler birleşir.
G–H (1): find(G) ≠ find(H) → kabul, kümeler birleşir.
B–D (2): find(B) ≠ find(D) → kabul, kümeler birleşir.
C–J (2): C ve J zaten aynı kümede → döngü oluşturur, reddedilir.
D–E (2): find(D) ≠ find(E) → kabul, kümeler birleşir.
D–H (2): find(D) ≠ find(H) → kabul, kümeler birleşir.
A–D (4): A ve D zaten aynı kümede → döngü oluşturur, reddedilir.
B–C (4): find(B) ≠ find(C) → kabul, kümeler birleşir. 9 = n − 1 kenar seçildi: bitti, toplam ağırlık 14.
Minimum kapsayan ağaç: A–E, E–F, D–E, B–D, D–H, G–H, B–C, C–I, I–J. Kalan kenarlara bakmaya gerek yok.
Kruskal Algoritması
İkinci Örnek: Havalimanları
Goodrich'teki örnek: 9 havalimanı, 19 uçuş hattı (mil). Kruskal sırası:
| 144, 184, 187, 337, 621 | kabul |
| 740 ORD–JFK | ret (döngü) |
| 802 | kabul |
| 849, 867 | ret |
| 946 | kabul |
| 1090, 1121 | ret |
| 1235 LAX–DFW | kabul → 8 kenar |
Toplam: 4456 mil.
Özet
Özet
| Yapı | Temel işlemler | Maliyet | Ne zaman? |
|---|---|---|---|
| HashSet | add, remove, contains | O(1)* | sıra gerekmiyorsa |
| TreeSet | + first, floor, aralık | O(log n) | sıralı erişim gerekiyorsa |
| Çoklu küme | add, count, remove | O(1)* | tekrar sayısı önemliyse |
| Union-Find | find, union | O(α(n)) | ayrık kümeler, bağlantı sorguları |
* ortalama durum. Union-Find maliyeti rütbe + yol kısaltma ile amortize.
HashSet, sıralı erişim için TreeSet, dinamik bağlılık için Union-Find.Özet
Etkileşimli Simülatörler
Bu bölümün konularını tarayıcıda deneyerek pekiştirin:
Önceki bölümlerle bağlantı: Hash Tablosu · Kırmızı-Siyah Ağaç · Çizge
Özet
Örnek Problemler
- A = {2, 4, 6, 8}, B = {1, 2, 3, 4} için A ∪ B, A ∩ B, B − A nedir?
- Bir sözlükteki kelimeleri alfabetik sırayla ve tekrarsız yazdırmak için hangi Set uygulamasını seçersiniz?
- "kavak" kelimesinin harflerini çoklu kümede tutun: count('a') ve size() ne döner?
- 0..5 için sırayla union(0,1), union(2,3), union(1,3), union(4,5) yapılıyor (rütbeye göre, eşitlikte ata[x] = y). Kaç küme kalır? find(0) nedir?
- Kruskal örneğinde C–J kenarı neden reddedildi?
Cevapları göster
- A ∪ B = {1, 2, 3, 4, 6, 8}, A ∩ B = {2, 4}, B − A = {1, 3}.
- TreeSet (sıralı, tekrarsız, O(log n)).
- {k:2, a:2, v:1} → count('a') = 2, size() = 5.
- union(0,1): ata[0]=1, sira[1]=1 · union(2,3): ata[2]=3, sira[3]=1 · union(1,3): eşit → ata[1]=3, sira[3]=2 · union(4,5): ata[4]=5. İki küme: {0,1,2,3} ve {4,5}; find(0) = 3.
- C–I ve I–J daha önce seçilmişti; C ile J aynı kümede, kenar C–I–J–C döngüsünü kapatır.
Özet
Kaynaklar
- Goodrich, Tamassia ve Goldwasser, Data Structures and Algorithms in Java, 6. baskı, 2014.
- Cormen, Leiserson, Rivest ve Stein, Introduction to Algorithms, 4. baskı, 2022.
- Sedgewick ve Wayne, Algorithms, 4. baskı, 2011.
- Oracle, Java SE 17
Set,HashSetveTreeSetAPI belgeleri.
Son
← → veya boşluk ile gezinin · F: tam ekran · Home/End: ilk/son slayt