Bölüm 13: Kümeler ve Ayrık Kümeler
Giriş
Öğrenme Çıktıları
Küme ADT'sini ve Java Set arayüzünü açıklamak.
Tekrarlı öğe gerektiren problemlerde çoklu küme yaklaşımını kullanmak.
Yol kısaltma ve rütbeye göre birleştirme yöntemlerini uygulamak.
Union-Find yapısını bağlantılı bileşen sayma gibi bağlılık problemlerinde kullanmak.
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 ADT · Kodu adım adım çalıştır
Kod İzleme: add dönüşüyle ilk tekrar
Boş bir HashSet oluşturulur: 16 boş kova. Her öğe hash & 15 kovasına gider.
Döngü sıradaki öğeyi alır: x = 4.
add(4): kova 4 boş, öğe eklenir ve true döner → !true yanlış, döngü sürer. Tek kova bakıldı: O(1).
Döngü sıradaki öğeyi alır: x = 7.
add(7): kova 7 boş, öğe eklenir ve true döner → !true yanlış, döngü sürer. Tek kova bakıldı: O(1).
Döngü sıradaki öğeyi alır: x = 2.
add(2): kova 2 boş, öğe eklenir ve true döner → !true yanlış, döngü sürer. Tek kova bakıldı: O(1).
Döngü sıradaki öğeyi alır: x = 7.
add(7): kova 7'de 7 zaten var; küme değişmez ve false döner → koşul doğru.
İlk tekrar eden öğe 7 döndürülür. Toplam iş O(n): her öğe için bir add; iç içe döngü yok.
int ilkTekrar(int[] a) { Set<Integer> gorulen = new HashSet<>(); for (int x : a) { if (!gorulen.add(x)) // eklenemedi mi? return x; // zaten vardı: tekrar } return -1; // tekrar yok }
Küme Uygulamaları
java.util.Set Uygulamaları
| Sınıf | İç yapı | Gezinme sırası | add / contains / remove |
|---|---|---|---|
| HashSet | hash tablosu | 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ç | 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 → [elma, armut, kiraz] (kova sırası: hash'e bağlı) // 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; 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üme yapan 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ı · Kodu adım adım çalıştır
Kod İzleme: add / contains (m = 7)
// alanlar: List<Integer>[] kova; n; m = 7 boolean add(int x) { // x ≥ 0 int i = x % m; // kova no for (int y : kova[i]) if (y == x) return false; // zaten var kova[i].add(x); n++; return true; } boolean contains(int x) { int i = x % m; for (int y : kova[i]) if (y == x) return true; return false; }
add(12): kova no = 12 % 7 = 5. Kova boş; tarama hemen biter.
12 kovada yok → zincirin sonuna eklenir, n = 1, return true.
add(25): kova no = 25 % 7 = 4. Kova boş; tarama hemen biter.
25 kovada yok → zincirin sonuna eklenir, n = 2, return true.
add(7): kova no = 7 % 7 = 0. Kova boş; tarama hemen biter.
7 kovada yok → zincirin sonuna eklenir, n = 3, return true.
add(19): kova no = 19 % 7 = 5. Kovadaki zincir taranmalı: 12.
y = 12 ≠ 19 → zincirde ilerle.
19 kovada yok → zincirin sonuna eklenir, n = 4, return true. (Çakışma: kova 5'te artık 2 öğe var.)
add(12): kova no = 12 % 7 = 5. Kovadaki zincir taranmalı: 12, 19.
y = 12 == 12 → öğe zaten kümede: return false, hiçbir şey eklenmez. Kümeyi küme yapan budur.
contains(26): 26 % 7 = 5 → yalnızca kova 5 taranır; diğer kovalara hiç bakılmaz.
y = 12 ≠ 26 → devam.
y = 19 ≠ 26 → devam.
Zincir bitti, 26 yok → return false. Ortalama zincir boyu n/m sabit → O(1).
contains(19): 19 % 7 = 5 → yalnızca kova 5 taranır; diğer kovalara hiç bakılmaz.
y = 12 ≠ 19 → devam.
y = 19 == 19 → bulundu, return true.
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; kök ortada (inorder) gezinme sıralı çıktı verir.
- Arama, ekleme, silme O(log n).
- Öğeler karşılaştırılabilir olmalı (
ComparableveyaComparator). compare(a, b) == 0iseTreeSetiki öğeyi aynı kabul eder; bu nedenle sıralamaequalsile tutarlı olmalıdır.
Sıralı yapının getirdiği ek işlemler (S = {5, 10, 15, 20, 25, 30, 40}):
Küme Uygulamaları · Kodu adım adım çalıştır
Sıralı Dizilerle Birleşim ve Kesişim (iki imleç)
void birlesimKesisim(int[] a, int[] b, List<Integer> bir, List<Integer> kes) { int i = 0, j = 0; while (i < a.length && j < b.length) { if (a[i] < b[j]) bir.add(a[i++]); else if (a[i] > b[j]) bir.add(b[j++]); else { // ortak öğe bir.add(a[i]); kes.add(a[i]); i++; j++; } } while (i < a.length) bir.add(a[i++]); while (j < b.length) bir.add(b[j++]); }
Kümeler sıralı dizilerde. Birleştirme sıralamasındaki merge gibi iki imleç tek geçişte ilerler (Goodrich: generic merge).
a[i] = 1 < b[j] = 3 → 1 yalnızca a'da: birleşime eklenir, i++.
a[i] = b[j] = 3 → ortak öğe: birleşime bir kez ve kesişime eklenir; iki imleç de ilerler.
a[i] = 5 > b[j] = 4 → 4 yalnızca b'de: birleşime eklenir, j++.
a[i] = b[j] = 5 → ortak öğe: birleşime bir kez ve kesişime eklenir; iki imleç de ilerler.
a[i] = 7 < b[j] = 8 → 7 yalnızca a'da: birleşime eklenir, i++.
a[i] = 9 > b[j] = 8 → 8 yalnızca b'de: birleşime eklenir, j++.
j = 4 = b.length → b bitti, ana döngüden çıkılır. Kesişim tamam: artık ortak öğe olamaz.
a'da kalan 9 doğrudan birleşime eklenir, i++.
b'de kalan yok. Birleşim [1, 3, 4, 5, 7, 8, 9], kesişim [3, 5]. Her adımda bir imleç ilerler → en fazla n + m adım: O(n + m).
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.
Küme Uygulamaları
Bit Vektörü ile Küme (Bitset)
| & &~.| İşlem | int maskesi (U ≤ 32) | java.util.BitSet |
|---|---|---|
| ekle x / var mı x? | a |= 1 << x · (a >> x & 1) == 1 | set(x) · get(x) |
| birleşim / kesişim | a | b · a & b | or(b) · and(b) |
| fark / öğe sayısı | a & ~b · Integer.bitCount(a) | andNot(b) · cardinality() |
Bellek U bit: 10⁶ öğelik evren ≈ 122 KB. Evren büyük ama küme seyrekse (ör. 3 öğe, U = 10⁹) hash tabanlı küme seçilir.
Küme Uygulamaları · Kodu adım adım çalıştır
Kod İzleme: bit işlemleriyle küme cebri
İki boş küme: tüm bitler 0.
1 << 1 = 2 maskesi; a |= maske → 1. bit 1 olur: 1 kümeye eklendi.
1 << 3 = 8 maskesi; a |= maske → 3. bit 1 olur: 3 kümeye eklendi.
1 << 4 = 16 maskesi; a |= maske → 4. bit 1 olur: 4 kümeye eklendi.
b kümesine 3 eklenir: 3. bit yanar.
b kümesine 5 eklenir: 5. bit yanar.
VEYA: bitlerden biri 1 ise 1 → birleşim {1, 3, 4, 5}. Tek makine komutu.
VE: iki bit de 1 ise 1 → kesişim {3}.
~b b'nin bitlerini ters çevirir; a & ~b: a'da olup b'de olmayanlar → fark {1, 4}.
Üyelik testi: a >> 3 3. biti en sağa kaydırır, & 1 onu ayırır → 1, yani 3 ∈ a. O(1).
int a = 0, b = 0; for (int x : new int[]{1, 3, 4}) a |= 1 << x; // x. biti yak for (int x : new int[]{3, 5}) b |= 1 << x; int bir = a | b; // birleşim int kes = a & b; // kesişim int fark = a & ~b; // fark boolean var3 = (a >> 3 & 1) == 1;
Ç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.
- Ö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.Çoklu Küme · Kodu adım adım çalıştır
Kod İzleme: CokluKume sayaçları
void add(E e) { say.merge(e, 1, Integer::sum); n++; } int count(E e) { return say.getOrDefault(e, 0); } boolean remove(E e) { Integer c = say.get(e); if (c == null) return false; if (c == 1) say.remove(e); else say.put(e, c - 1); n--; return true; }
merge: "a" yoktu → sayaç 1 ile eklenir; n = 1.
merge: "b" yoktu → sayaç 1 ile eklenir; n = 2.
merge: "a" vardı → Integer::sum ile 1 + 1 = 2; n = 3. Set olsaydı değişmezdi.
merge: "c" yoktu → sayaç 1 ile eklenir; n = 4.
count("a"): getOrDefault → 2. Tek hash araması: O(1).
remove("a"): c = say.get(e) = 2.
c = 2 > 1 → sayaç 1'e iner (girdi kalır); n = 3.
remove("b"): c = say.get(e) = 1.
c == 1 → sayaç 0 olacaktı: girdi tamamen silinir (0 sayaç saklanmaz); n = 2.
remove("x"): c = say.get(e) = null.
c == null → öğe yok: return false, n değişmez.
count("b"): getOrDefault → 0. Tek hash araması: O(1).
Sonuç M = {a, c}, n = 2. Tüm işlemler tek hash erişimi → ortalama O(1).
Çoklu Küme
Çoklu Eşleme (Multimap)
- Çoklu eşleme (multimap): bir anahtar birden çok değere eşlenebilir (ders → öğrenciler).
- Goodrich ADT:
get(k)tüm değerleri,put(k, v),remove(k, v),removeAll(k),size()tüm (k, v) çiftleri. - Java'da hazır sınıf yok:
Map<K, List<V>>ile kurulur.
Map<String, List<String>> m = new HashMap<>(); void put(String k, String v) { // yoksa boş liste aç m.computeIfAbsent(k, x -> new ArrayList<>()).add(v); } List<String> get(String k) { return m.getOrDefault(k, List.of()); }
Çoklu kümeden farkı: yalnız kaç kez değil, değerlerin kendisi tutulur. Hash ile put / get O(1)*, remove(k, v) O(1 + liste boyu).
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 ebeveynini (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. Ebeveyn bağlantıları kök bulunana kadar izlenir.
- Bu, arama ağaçlarındaki 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.ebeveyn != x: x.ebeveyn = find(x.ebeveyn) // yol kısaltma return x.ebeveyn 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 ebeveyn kök1.ebeveyn = 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
ebeveyn[i] = iile her öğe kendi kümesinin kökü olarak başlar (makeSet).
Map<Character, Integer> indis = new HashMap<>(); // 'E'→0, 'F'→1, … int[] ebeveyn = new int[12]; for (int i = 0; i < 12; i++) ebeveyn[i] = i; // 12 tekil küme
Ayrık Kümeler (Union-Find)
Union İşlemi Adım Adım
Başlangıç: 12 tekil küme, ebeveyn[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. Yol kısaltma yok.
union(C, K): find(C) = C, find(K) = K; boyutlar eşit (1) → K, C'nin altına bağlanır (ebeveyn[9] = 4).
union(F, E): find(F) = F, find(E) = E; boyutlar eşit (1) → E, F'nin altına bağlanır (ebeveyn[0] = 1).
union(A, J): find(A) = A, find(J) = J; boyutlar eşit (1) → J, A'nın altına bağlanır (ebeveyn[6] = 5).
union(A, B): find(A) = A, find(B) = B; boyut 1 < 2 → B, A'nın altına bağlanır (ebeveyn[10] = 5).
union(C, D): find(C) = C, find(D) = D; boyut 1 < 2 → D, C'nin altına bağlanır (ebeveyn[3] = 4).
union(D, I): find(D) = C, find(I) = I; boyut 1 < 3 → I, C'nin altına bağlanır (ebeveyn[2] = 4).
union(L, F): find(L) = L, find(F) = F; boyut 1 < 2 → L, F'nin altına bağlanır (ebeveyn[7] = 1).
union(C, A): find(C) = C, find(A) = A; boyut 3 < 4 → A, C'nin altına bağlanır (ebeveyn[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 (ebeveyn[8] = 11).
union(H, F): find(H) = H, find(F) = F; boyut 2 < 3 → H, F'nin altına bağlanır (ebeveyn[11] = 1).
union(H, B): find(H) = F, find(B) = C; boyut 5 < 7 → F, C'nin altına bağlanır (ebeveyn[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; kökler aynı → evet (toplam 4 adım).
Ayrık Kümeler (Union-Find)
Başka Bir Fikir: Quick-Find (Hızlı Bul)
- Ağaç yerine düz dizi:
id[i]= i'nin küme adı; aynı kümedekilerin id'si aynı. find(p) =id[p]→ O(1). - union(p, q): id'si
id[p]olan herkesiid[q]yap → tüm dizi taranır, O(n).
union(0,1), union(2,3) sonrası: 4 küme (aynı renk = aynı küme)
| Yöntem (Sedgewick) | find | union | n − 1 union toplamı |
|---|---|---|---|
| Quick-Find (dizi, küme adı) | O(1) | O(n) | O(n²) |
| Quick-Union (ebeveyn ağacı, naif) | O(h), en kötü O(n) | O(h) | en kötü O(n²) |
| Rütbe/boyut + yol kısaltma | O(α(n)) amortize | ≈ O(n) | |
Ayrık Kümeler (Union-Find) · Kodu adım adım çalıştır
Kod İzleme: Quick-Find union
union(1, 3): p'nin küme adı pid = id[1] = 1, q'nunki qid = id[3] = 3.
pid ≠ qid: farklı kümeler, birleştirme gerekli. Şimdi tüm dizi taranacak.
id[0] = 1 = pid → id[0] = 3 yapılır.
id[1] = 1 = pid → id[1] = 3 yapılır.
id[2] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[3] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[4] = 4 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[5] = 5 ≠ pid: dokunulmaz (ama yine de bakıldı).
union(4, 3): p'nin küme adı pid = id[4] = 4, q'nunki qid = id[3] = 3.
pid ≠ qid: farklı kümeler, birleştirme gerekli. Şimdi tüm dizi taranacak.
id[0] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[1] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[2] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[3] = 3 ≠ pid: dokunulmaz (ama yine de bakıldı).
id[4] = 4 = pid → id[4] = 3 yapılır.
id[5] = 5 ≠ pid: dokunulmaz. Her union 6 hücrenin hepsine baktı: n öğede O(n); n − 1 union → O(n²).
int find(int p) { return id[p]; } // O(1) void union(int p, int q) { int pid = id[p], qid = id[q]; if (pid == qid) return; for (int i = 0; i < id.length; i++) if (id[i] == pid) id[i] = qid; // O(n) tarama }
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 → en kötü O(n).
Başlangıç: 5 tekil küme; her düğüm kendi kökü. Sırayla union(1,2), union(2,3), union(3,4), union(4,5) yapılacak.
union(1, 2): iki tekil küme, iki yöntem de 1'i 2'nin altına bağlar.
union(2, 3): naif yöntem 2 kökünü (2 öğeli ağaç) tekil 3'ün altına bağlar → zincir uzar. Boyuta göre: tekil 3, büyük ağacın kökü 2'ye bağlanır.
union(3, 4): naif zincir bir kat daha uzar; boyuta göre 4 yine köke eklenir.
union(4, 5): naif ağaç zincir oldu (yükseklik n − 1 → find O(n)); boyuta göre ağaç yıldız (yükseklik 1).
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): ebeveyn[x] = x sira[x] = 0 find(x): // yol kısaltmalı if x != ebeveyn[x]: ebeveyn[x] = find(ebeveyn[x]) return ebeveyn[x] link(x, y): // x, y kök if sira[x] > sira[y]: ebeveyn[y] = x else: ebeveyn[x] = y if sira[x] == sira[y]: sira[y] = sira[y] + 1 union(x, y): link(find(x), find(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 ebeveyni 7 yapılır: ebeveyn[3] = ebeveyn[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 | |
ebeveyn[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 ebeveyn; SetDugum(int veri) { this.veri = veri; this.ebeveyn = this; this.sira = 0; } } SetDugum find(SetDugum d) { if (d != d.ebeveyn) d.ebeveyn = find(d.ebeveyn); // kısaltma return d.ebeveyn; }
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.ebeveyn = kokY; } else if (kokX.sira > kokY.sira) { kokY.ebeveyn = kokX; } else { // eşit: birini seç kokX.ebeveyn = kokY; kokY.sira++; } }
Ayrık Kümeler (Union-Find)
Java: Dizi Tabanlı Union-Find
class UnionFind { private final int[] ebeveyn, sira; UnionFind(int n) { ebeveyn = new int[n]; sira = new int[n]; for (int i = 0; i < n; i++) ebeveyn[i] = i; } int find(int x) { if (ebeveyn[x] != x) ebeveyn[x] = find(ebeveyn[x]); return ebeveyn[x]; } boolean union(int x, int y) { int kx = find(x), ky = find(y); if (kx == ky) return false; if (sira[kx] < sira[ky]) ebeveyn[kx] = ky; else if (sira[kx] > sira[ky]) ebeveyn[ky] = kx; else { ebeveyn[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ı.Ayrık Kümeler (Union-Find) · Kodu adım adım çalıştır
Kod İzleme: find + union
int find(int x) { if (ebeveyn[x] != x) ebeveyn[x] = find(ebeveyn[x]); return ebeveyn[x]; } boolean union(int x, int y) { int kx = find(x), ky = find(y); if (kx == ky) return false; if (sira[kx] < sira[ky]) ebeveyn[kx] = ky; else if (sira[kx] > sira[ky]) ebeveyn[ky] = kx; else { ebeveyn[kx] = ky; sira[ky]++; } return true; }
6 öğe, ebeveyn[i] = i, sira[i] = 0: 6 tekil küme (her kök kendini gösterir).
find(0): ebeveyn[0] == 0 → 0 köktür, döndürülür.
find(1): ebeveyn[1] == 1 → 1 köktür, döndürülür.
Rütbeler eşit (0) → ebeveyn[0] = 1 ve yeni kökün rütbesi artar: sira[1] = 1.
find(2): ebeveyn[2] == 2 → 2 köktür, döndürülür.
find(3): ebeveyn[3] == 3 → 3 köktür, döndürülür.
Rütbeler eşit (0) → ebeveyn[2] = 3 ve yeni kökün rütbesi artar: sira[3] = 1.
find(0): ebeveyn[0] = 1 ≠ 0 → kök değil; find(1) ile yukarı çıkılır.
find(1): ebeveyn[1] == 1 → 1 köktür, döndürülür.
Dönüşte ebeveyn[0] = 1 (zaten kökün çocuğu, değişmez); find(0) = 1 döner.
find(2): ebeveyn[2] = 3 ≠ 2 → kök değil; find(3) ile yukarı çıkılır.
find(3): ebeveyn[3] == 3 → 3 köktür, döndürülür.
Dönüşte ebeveyn[2] = 3 (zaten kökün çocuğu, değişmez); find(2) = 3 döner.
Rütbeler eşit (1) → ebeveyn[1] = 3 ve yeni kökün rütbesi artar: sira[3] = 2.
Ayrık Kümeler (Union-Find) · Kodu adım adım çalıştır
Kod İzleme: find + union (devam)
int find(int x) { if (ebeveyn[x] != x) ebeveyn[x] = find(ebeveyn[x]); return ebeveyn[x]; } boolean union(int x, int y) { int kx = find(x), ky = find(y); if (kx == ky) return false; if (sira[kx] < sira[ky]) ebeveyn[kx] = ky; else if (sira[kx] > sira[ky]) ebeveyn[ky] = kx; else { ebeveyn[kx] = ky; sira[ky]++; } return true; }
find(4): ebeveyn[4] == 4 → 4 köktür, döndürülür.
find(0): ebeveyn[0] = 1 ≠ 0 → kök değil; find(1) ile yukarı çıkılır.
find(1): ebeveyn[1] = 3 ≠ 1 → kök değil; find(3) ile yukarı çıkılır.
find(3): ebeveyn[3] == 3 → 3 köktür, döndürülür.
Dönüşte ebeveyn[1] = 3 (zaten kökün çocuğu, değişmez); find(1) = 3 döner.
Yol kısaltma: dönüşte ebeveyn[0] = 1 → 3; 0 artık doğrudan köke bağlı. find(0) = 3.
sira[4] = 0 < sira[3] = 2 → alçak ağacın kökü 4, 3'e bağlanır: ebeveyn[4] = 3. Yükseklik artmaz.
find(1): ebeveyn[1] = 3 ≠ 1 → kök değil; find(3) ile yukarı çıkılır.
find(3): ebeveyn[3] == 3 → 3 köktür, döndürülür.
Dönüşte ebeveyn[1] = 3 (zaten kökün çocuğu, değişmez); find(1) = 3 döner.
find(4): ebeveyn[4] = 3 ≠ 4 → kök değil; find(3) ile yukarı çıkılır.
find(3): ebeveyn[3] == 3 → 3 köktür, döndürülür.
Dönüşte ebeveyn[4] = 3 (zaten kökün çocuğu, değişmez); find(4) = 3 döner.
kx = ky = 3 → 1 ve 4 zaten aynı kümede: return false (Kruskal: döngü).
Tek küme, kök 3. Yol kısaltma 0'ı köke taşıdı; sira[3] = 2 yalnızca üst sınır. Birlikte: işlem başına ≈ O(α(n)).
Ayrık Kümeler (Union-Find) · Kodu adım adım çalıştır
Kod İzleme: döngüyle find + yol kısaltma
Zincir 4 → 3 → 2 → 1 → 0 (kötü birleştirmeler sonucu). find(4) önce kökü arar: kok = 4.
1. tur: ebeveyn[4] = 3 ≠ 4 → yukarı çık, kok = 3.
1. tur: ebeveyn[3] = 2 ≠ 3 → yukarı çık, kok = 2.
1. tur: ebeveyn[2] = 1 ≠ 2 → yukarı çık, kok = 1.
ebeveyn[1] = 0 → kok = 0; ebeveyn[0] = 0 olduğundan döngü biter: kök bulundu (4 adım).
2. tur: ebeveyn[4] = 3, kök değil → bu düğüm kısaltılacak.
Önce eski ebeveyn saklanır (sonraki = 3), sonra ebeveyn[4] = 0: 4 artık doğrudan köke bağlı.
Yolda bir yukarı: x = 3.
2. tur: ebeveyn[3] = 2, kök değil → bu düğüm kısaltılacak.
Önce eski ebeveyn saklanır (sonraki = 2), sonra ebeveyn[3] = 0: 3 artık doğrudan köke bağlı.
Yolda bir yukarı: x = 2.
2. tur: ebeveyn[2] = 1, kök değil → bu düğüm kısaltılacak.
Önce eski ebeveyn saklanır (sonraki = 1), sonra ebeveyn[2] = 0: 2 artık doğrudan köke bağlı.
Yolda bir yukarı: x = 1.
ebeveyn[1] = 0 = kok: 1 zaten köke bağlı, döngü biter.
Kök 0 döner. Yol üzerindeki herkes köke bağlandı: sonraki find'lar 1 adım. Özyineleme yok → yığın taşması riski de yok.
int find(int x) { int kok = x; while (ebeveyn[kok] != kok) kok = ebeveyn[kok]; while (ebeveyn[x] != kok) { // yol kısaltma int sonraki = ebeveyn[x]; ebeveyn[x] = kok; x = sonraki; } return kok; }
Ayrık Kümeler (Union-Find) · Uygulama · Kodu adım adım çalıştır
Kod İzleme: Union-Find ile bağlantılı bileşen sayısı
7 düğüm, 5 kenar. UnionFind(7): her düğüm kendi kümesinin kökü (ebeveyn[i] = i).
Başta hiç kenar yok: 7 bileşen.
Kenar (0, 1): kökler farklı → union kümeleri birleştirir, true döner.
İki bileşen bire indi: say = 6.
Kenar (1, 2): kökler farklı → union kümeleri birleştirir, true döner.
İki bileşen bire indi: say = 5.
Kenar (3, 4): kökler farklı → union kümeleri birleştirir, true döner.
İki bileşen bire indi: say = 4.
Kenar (0, 2): find(0) = find(2) → zaten aynı bileşende; union false, say değişmez (bu kenar bir döngü kapatır).
Kenar (5, 6): kökler farklı → union kümeleri birleştirir, true döner.
İki bileşen bire indi: say = 3.
Sonuç 3 bileşen: {0, 1, 2}, {3, 4}, {5, 6}. Toplam O(n + m·α(n)); kenarlar sonradan gelse de sayaç anında güncel (dinamik bağlılık).
int bilesenSay(int n, int[][] kenar) { UnionFind uf = new UnionFind(n); int say = n; // n tekil küme for (int[] e : kenar) if (uf.union(e[0], e[1])) say--; // iki bileşen birleşti return say; }
Özet
Sık Yapılan Hatalar
contains false döner.p.x = 5; set.contains(p) → falseset.remove(x) → ConcurrentModificationException.set.removeIf(x -> x % 2 == 0);ebeveyn[x] = y yanlış: x'in ağacı bölünür. Doğrusu:ebeveyn[find(x)] = find(y);sira[ky]++ yoksa rütbe yüksekliği izlemez; O(log n) garantisi kaybolur.compare(a, b) == 0 olan farklı nesnelerden yalnız biri saklanır (ör. yalnız soyada göre sıralama).Ö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
Ö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 ebeveyn[x] = y). Kaç küme kalır? find(0) nedir?
- 0..4 için sırayla union(0,1), union(1,2), union(3,4), union(2,0) yapılıyor. Hangi union false döner? Kaç bağlantılı bileşen kalır?
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): ebeveyn[0]=1, sira[1]=1 · union(2,3): ebeveyn[2]=3, sira[3]=1 · union(1,3): eşit → ebeveyn[1]=3, sira[3]=2 · union(4,5): ebeveyn[4]=5. İki küme: {0,1,2,3} ve {4,5}; find(0) = 3.
- union(2,0) false döner: 0 ve 2 zaten aynı kümede (bu kenar bir döngü kapatır). Bileşenler {0, 1, 2} ve {3, 4} → 2.
Özet
Alıştırmalar
- Boş HashSet'e (16 kova, kova = x & 15) 12, 28, 44, 7 eklenir. Kovalar?
add(28)ne döner? - U = {0..7}: A = 1011 0010₂, B = 0111 0100₂. A | B, A & B, A & ~B kümeleri nedir?
- Quick-Find ile n öğede n − 1 union: toplam kaç hücreye bakılır?
ebeveyn = [1, 2, 3, 3, 3]iken yinelemelifind(0)sonrasıebeveyn?- Rütbeye göre birleştirmede rütbe en fazla kaç olabilir? Neden?
- Kenarlar tek tek geliyor; her kenardan sonra "çizge bağlı mı?" Hangi yapı, maliyet?
Cevaplar / ipuçları
- 12, 28, 44 → kova 12 (zincir), 7 → kova 7. 28 yeni öğe:
true. - A = {1, 4, 5, 7}, B = {2, 4, 5, 6} → ∪ {1, 2, 4, 5, 6, 7} · ∩ {4, 5} · A − B {1, 7}.
- Her union n hücre: (n − 1) · n → O(n²).
- Kök 3;
ebeveyn = [3, 3, 3, 3, 3]. - ⌊log₂ n⌋: rütbesi r olan ağaçta en az 2r öğe var.
- Union-Find + sayaç (başarılı union'da say−−; say == 1 ⇔ bağlı): O(m·α(n)).
Ö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.