Bölüm 13: Kümeler ve Ayrık Kümeler

Bölüm 13: Kümeler ve Ayrık Kümeler · Çalışma Soruları
Küme ADT ve java.util.SetHash ve ağaç tabanlı kümeBit vektörüÇoklu küme ve çoklu eşlemeUnion-FindBağlantılı bileşenlerSık hatalar
Sercan KÜLCÜ · Giresun Üniversitesi · Bilgisayar Mühendisliği

Klasik Soru · Küme ADT

Soru 1: Set Metotlarıyla Küme Cebri

Programın çıktısı nedir? Kullanılan her metodun hangi küme işlemine karşılık geldiğini belirtiniz.

Set<Integer> a =
    new TreeSet<>(List.of(5, 1, 9, 3, 7));
Set<Integer> b =
    new HashSet<>(List.of(3, 4, 5, 6));
Set<Integer> k = new TreeSet<>(a);
k.retainAll(b);
Set<Integer> f = new TreeSet<>(a);
f.removeAll(b);
Set<Integer> u = new TreeSet<>(a);
u.addAll(b);
System.out.println(k + " " + f + " "
    + u.size() + " " + a.add(3));
Cevabı göster
  • retainAll = kesişim A ∩ B = [3, 5]
  • removeAll = fark A − B = [1, 7, 9]
  • addAll = birleşim = [1, 3, 4, 5, 6, 7, 9] → boyut 7
  • 3 zaten a'da: add false döner, küme değişmez.

Çıktı: [3, 5] [1, 7, 9] 7 false

Üç metot da kümeyi yerinde değiştirir; a'yı korumak için önce kopya (new TreeSet<>(a)) alınmıştır. TreeSet olduğu için sonuçlar sıralı yazdırılır.

Çoktan Seçmeli · Küme ADT

Soru 2: Kopya mı, Referans mı?

Programın çıktısı nedir?

Set<Integer> a =
    new TreeSet<>(List.of(1, 2, 3, 4));
Set<Integer> b =
    new TreeSet<>(List.of(3, 4, 5));
Set<Integer> kes = a;
kes.retainAll(b);
System.out.println(a + " " + kes.size());

Doğru: C. kes = a kopya oluşturmaz; iki değişken aynı nesneyi gösterir. retainAll yerinde çalıştığı için a da [3, 4] olur. A şıkkı a'nın kopyalandığını varsayar; kesişimi a'yı bozmadan almak için new TreeSet<>(a) gerekir.

Çoktan Seçmeli · Küme Uygulamaları

Soru 3: Gezinme Sırası

Programın çıktısı nedir?

Set<String> l = new LinkedHashSet<>();
Set<String> t = new TreeSet<>();
for (String s : List.of("muz", "elma",
                         "kiraz", "elma")) {
  l.add(s);  t.add(s);
}
System.out.println(l + " " + t);

Doğru: A. LinkedHashSet ekleme sırasını korur (hash tablosu + çift bağlı liste); TreeSet kırmızı-siyah ağaçtır ve alfabetik sırayla gezer. B yanlıştır: ikinci "elma" iki kümeye de eklenmez, çünkü küme tekrar tutmaz.

Klasik Soru · Küme Uygulamaları

Soru 4: Zincirlemeli Hash Küme

m = 7 kovalı, zincirlemeli bir hash kümesine (kova = x % 7) sırasıyla 9, 16, 5, 30, 12, 16 ekleniyor. Kovaların son durumunu yazınız ve ardından yapılan contains(23) çağrısının hangi öğelere baktığını gösteriniz.

Cevabı göster
add9165301216
x % 7225252
dönüştruetruetruetruetruefalse

Son durum: kova 2: 9 → 16 → 30, kova 5: 5 → 12; diğer kovalar boş; n = 5. İkinci 16 için kova 2 taranır, 16 bulunur ve eklenmez: kümeyi küme yapan budur.

contains(23): 23 % 7 = 2 → yalnızca kova 2'deki 9, 16, 30 ile karşılaştırılır (3 karşılaştırma), bulunamaz → false. Diğer kovalara hiç bakılmaz; ortalama zincir boyu n/m sabit kaldıkça işlem O(1).

Klasik Soru · Küme Uygulamaları

Soru 5: TreeSet ile Sıralı Sorgular

Programın çıktısı nedir? Bu sorgular HashSet ile yapılsaydı maliyet nasıl değişirdi?

TreeSet<Integer> s = new TreeSet<>(
    List.of(8, 3, 14, 21, 5, 17, 30));
System.out.println(s.first() + " "
    + s.last() + " " + s.floor(10) + " "
    + s.ceiling(17) + " " + s.subSet(5, 21));
Cevabı göster

Çıktı: 3 30 8 17 [5, 8, 14, 17]

  • first / last: en küçük 3, en büyük 30.
  • floor(10): ≤ 10 olan en büyük öğe → 8.
  • ceiling(17): ≥ 17 olan en küçük öğe → 17'nin kendisi (eşitlik dahil).
  • subSet(5, 21): alt sınır dahil, üst sınır hariç → [5, 8, 14, 17].

Dengeli ağaçta her biri O(log n) (aralık sorgusu + k çıktı). HashSet'te sıra bilgisi olmadığından aynı sorgular tüm öğeleri tarayarak O(n) sürer.

Çoktan Seçmeli · Küme Uygulamaları

Soru 6: TreeSet ve compare

Kisi sınıfında ad ve soyad alanları var. Program ne yazdırır?

TreeSet<Kisi> s = new TreeSet<>(
    Comparator.comparing((Kisi k) -> k.soyad));
s.add(new Kisi("Ali", "Kaya"));
s.add(new Kisi("Ayşe", "Kaya"));
s.add(new Kisi("Can", "Demir"));
System.out.println(s.size());

Doğru: D. TreeSet eşitliği compare(a, b) == 0 ile belirler; "Ayşe Kaya", "Ali Kaya" ile aynı sayılır ve eklenmez (add false). A şıkkı equals'a bakıldığını varsayar. Düzeltme: .thenComparing(k -> k.ad), yani sıralama equals ile tutarlı olmalı. Comparator verildiği için Comparable gerekmez (C yanlış).

Adım Adım Çalıştırma · Küme Uygulamaları

Soru 7: İki İmleçle Birleşim ve Kesişim

Sıralı a = {2, 4, 6, 8} ve b = {1, 4, 5, 8, 9} dizileri için metot bittiğinde bir ve kes listeleri ne olur?

İki imleç dizilerin başında. Birleştirme sıralamasındaki merge gibi tek geçiş yapılacak.

a[0] = 2 > b[0] = 1 → 1 yalnızca b'de: birleşime eklenir, j++.

a[0] = 2 < b[1] = 4 → 2 yalnızca a'da: birleşime eklenir, i++.

a[1] = b[1] = 4 → ortak öğe: birleşime bir kez, kesişime de eklenir; iki imleç ilerler.

a[2] = 6 > b[2] = 5 → 5 birleşime eklenir, j++.

a[2] = 6 < b[3] = 8 → 6 birleşime eklenir, i++.

a[3] = b[3] = 8 → ortak öğe; i = 4, j = 4.

i = 4 = a.length → ana döngü biter; a'da kalan yok. Kesişim tamam: artık ortak öğe olamaz.

b'de kalan 9 doğrudan birleşime eklenir.

Sonuç: bir = [1, 2, 4, 5, 6, 8, 9], kes = [4, 8]. Her adımda en az bir imleç ilerler → O(n + m).

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++]);
}

Çoktan Seçmeli · Küme Uygulamaları

Soru 8: Hash mi, Ağaç mı?

Aşağıdaki işlemlerden hangisinde hash tabanlı küme, dengeli BST tabanlı kümeden asimptotik olarak daha yavaştır?

Doğru: B. Hash tablosu sıra bilgisi tutmaz; ≤ x olan en büyük öğeyi bulmak için tüm öğeler taranır: O(n). Dengeli BST'de O(log n). contains/add hash'te O(1)*, BST'de O(log n). D çeldiricidir ama hash ile birleşim O(n + m)*, BST ile O(m log(n + m)): hash yine daha hızlıdır.

Klasik Soru · Küme Uygulamaları

Soru 9: Bit Vektörüyle Küme Cebri

Program ne yazdırır? Yazdırılan her sayının temsil ettiği kümeyi de belirtiniz.

int a = 0, b = 0;
for (int x : new int[]{0, 2, 5, 6})
  a |= 1 << x;
for (int x : new int[]{2, 3, 6})
  b |= 1 << x;
System.out.println((a | b) + " " + (a & b)
    + " " + (a & ~b) + " "
    + Integer.bitCount(a));
Cevabı göster

a = 0110 0101₂ = 101 ({0, 2, 5, 6}), b = 0100 1100₂ = 76 ({2, 3, 6}).

İfadeİşlemBitlerDeğerKüme
a | bbirleşim0110 1101109{0, 2, 3, 5, 6}
a & bkesişim0100 010068{2, 6}
a & ~bfark a − b0010 000133{0, 5}
bitCount(a)|a|4 tane 14—

Çıktı: 109 68 33 4. Evren 0..31 içinde kaldığı için her işlem tek makine komutu, O(1); daha büyük evrende java.util.BitSet ile O(U / 64).

Klasik Soru · Çoklu Küme

Soru 10: Çoklu Küme Sayaçları

Sunumdaki CokluKume sınıfı (öğe → sayaç eşlemesi; sayaç 1 iken remove girdiyi siler) kullanılıyor. Program ne yazdırır, eşlemenin son hâli nedir?

CokluKume<Character> m = new CokluKume<>();
for (char c : "ananas".toCharArray())
  m.add(c);
m.remove('a');
m.remove('s');
System.out.println(m.count('a') + " "
    + m.count('s') + " " + m.size()
    + " " + m.remove('x'));
Cevabı göster
  • Eklemelerden sonra {a:3, n:2, s:1}, size = 6 (tekrarlar dahil).
  • remove('a'): sayaç 3 → 2, size = 5.
  • remove('s'): sayaç 1 olduğundan girdi tamamen silinir (0 sayaç saklanmaz), size = 4.
  • remove('x'): say.get null → false, size değişmez.

Çıktı: 2 0 4 false; eşleme {a:2, n:2}. count('s'), getOrDefault sayesinde 0 döner.

Aynı harfler bir HashSet'e eklenseydi küme {a, n, s} olurdu (boyut 3): küme yalnızca "var mı?" bilgisini, çoklu küme ise çokluğu tutar. Tüm işlemler tek hash erişimi, ortalama O(1).

Çoktan Seçmeli · Çoklu Küme

Soru 11: Sıfır Sayaçlı Girdiler

remove metodunda if (c == 1) say.remove(e); else say.put(e, c - 1); yerine her zaman say.put(e, c - 1); yazılırsa ne olur?

boolean remove(E e) {
  Integer c = say.get(e);
  if (c == null) return false;
  say.put(e, c - 1);   // değişiklik
  n--;
  return true;
}

Doğru: C. Sayaç 0 olunca girdi silinmezse c == null kontrolü artık "öğe yok" durumunu yakalayamaz: ikinci remove sayaç −1 yapar ve size'ı bozar; ayrıca farklı öğe sayısı (say.size()) şişer. A çeldiricidir: ilk remove'dan sonra count gerçekten 0 döner, hata ancak sonraki işlemlerde ortaya çıkar.

Çoktan Seçmeli · Ayrık Kümeler (Union-Find)

Soru 12: Naif Birleştirmede Zincir

Naif union(x, y) her zaman kök(x)'i kök(y)'nin altına bağlıyor (boyut/rütbe yok, yol kısaltma yok). 5 tekil kümede sırasıyla union(1,2), union(2,3), union(3,4), union(4,5) yapılırsa ağacın yüksekliği ve find(1)'in izlediği bağlantı sayısı ne olur?

Doğru: D. Her adımda büyüyen ağacın kökü tekil düğümün altına girer: 1 → 2 → 3 → 4 → 5 zinciri, yükseklik n − 1 = 4; find(1) dört bağlantı izler, en kötü O(n). A, boyuta göre birleştirmenin sonucudur: orada tekil düğümler büyük ağacın köküne eklenir ve ağaç yıldız (yükseklik 1) olur.

Klasik Soru · Ayrık Kümeler (Union-Find)

Soru 13: Boyuta Göre Birleştirme

A–H harfleri bir hash tablosuyla 0–7 indislerine eşlenmiş; boyuta göre birleştirme (küçük ağacın kökü büyüğün köküne; boyutlar eşitse ikinci kök birincinin altına) yapılıyor, yol kısaltma yok. Aşağıdaki işlemlerden sonra her harfin ebeveyni ve ağacın yüksekliği nedir?

union(A,B), union(C,D), union(E,F), union(C,G), union(A,C), union(E,H), union(E,B)

Cevabı göster
  • (A,B), (C,D), (E,F): eşit boyut → B → A, D → C, F → E.
  • (C,G): 2 > 1 → G → C (boyut C = 3).
  • (A,C): boyut 2 < 3 → A → C (boyut C = 5).
  • (E,H): 2 > 1 → H → E (boyut E = 3).
  • (E,B): find(B) = C (B → A → C); 3 < 5 → E → C (boyut C = 8).
ABCDEFGH
ebeveynCACCCECE

Tek küme, kök C; yükseklik 2 (B, F, H derinlik 2). Boyuta göre birleştirme yüksekliği ≤ log₂ 8 = 3 tutar.

Adım Adım Çalıştırma · Ayrık Kümeler (Union-Find)

Soru 14: Rütbe ve Yol Kısaltma

6 öğeli yapıda (ebeveyn[i] = i, sira[i] = 0) koddaki beş union çağrısından sonra ebeveyn ve sira dizileri ne olur?

union(0,1): kökler 0 ve 1, rütbeler eşit (0) → ebeveyn[0] = 1, sira[1] = 1.

union(2,3): eşit rütbe → ebeveyn[2] = 3, sira[3] = 1.

union(4,5): eşit rütbe → ebeveyn[4] = 5, sira[5] = 1.

union(1,3): 1 ve 3 zaten kök, rütbeler eşit (1) → ebeveyn[1] = 3, sira[3] = 2.

union(4,0): find(4): ebeveyn[4] = 5 ≠ 4 → find(5); 5 kök. kx = 5 (ebeveyn[4] zaten 5).

find(0): 0 → 1 → 3 yolu boyunca özyineleme; 3 köktür.

Dönüşte yol kısaltma: ebeveyn[0] = 1 → 3. 0 artık doğrudan köke bağlı; ky = 3.

sira[5] = 1 < sira[3] = 2 → alçak ağacın kökü 5, 3'e bağlanır: ebeveyn[5] = 3. Rütbe değişmez.

Sonuç: ebeveyn = [3, 3, 3, 3, 5, 3], sira = [0, 1, 0, 2, 0, 1]; tek küme, kök 3. 4 hâlâ 5 üzerinden bağlı; yolu ilk find(4)'te kısalır.

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;
}
// union(0,1); union(2,3); union(4,5);
// union(1,3); union(4,0);

Çoktan Seçmeli · Ayrık Kümeler (Union-Find)

Soru 15: Union-Find Karmaşıklığı

n öğe üzerinde m tane find/union işlemi, rütbeye göre birleştirme ve yol kısaltma birlikte kullanılarak yapılıyor. Toplam maliyet nedir?

Doğru: C (Tarjan, 1975). α(n) son derece yavaş büyür; evrendeki atom sayısı kadar öğe için bile ≤ 4, yani pratikte sabittir. A çeldiricidir ama bu sınır yalnızca rütbe/boyuta göre birleştirme (ya da yalnızca yol kısaltma) içindir. D yanlıştır: sınır amortizedir, tek bir find yine O(log n) sürebilir. Bellek: ebeveyn ve sira dizileri, O(n).

Klasik Soru · Ayrık Kümeler (Union-Find)

Soru 16: Bağlantılı Bileşen Sayısı

bilesenSay(8, new int[][]{{0,1}, {2,3}, {1,2}, {0,3}, {4,5}, {5,4}}) ne döndürür? Hangi kenarlarda union false döner ve bu ne anlama gelir?

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--;
  return say;
}
Cevabı göster
Kenar(0,1)(2,3)(1,2)(0,3)(4,5)(5,4)
uniontruetruetruefalsetruefalse
say765544

Dönüş: 4; bileşenler {0, 1, 2, 3}, {4, 5}, {6}, {7}.

(0,3): 0 ve 3 zaten (1,2) üzerinden bağlı, kenar bir döngü kapatır. (5,4): aynı kenarın tekrarı. İkisinde de say değişmez.

Toplam O(n + m·α(n)); kenarlar sonradan geldikçe sayaç anında güncellenir (dinamik bağlılık).

Çoktan Seçmeli · Özet

Soru 17: Kümedeki Öğeyi Değiştirmek

Nokta sınıfının equals ve hashCode metotları x ve y alanlarını kullanıyor. Program ne yazdırır?

Set<Nokta> s = new HashSet<>();
Nokta p = new Nokta(1, 2);
s.add(p);
p.x = 5;
System.out.println(s.contains(p) + " "
    + s.size());

Doğru: A. p, (1, 2) hash'inin kovasına yerleşti. x değişince hash kodu da değişir; contains(p) yeni hash'in kovasına bakar ve p'yi bulamaz. Öğe ise eski kovada durur, size 1 kalır. B çeldiricidir ("aynı nesne"), ama HashSet nesneyi kova üzerinden arar. Kümedeki nesnelerin hash'e giren alanları değiştirilmemelidir.

Klasik Soru · Çoklu Küme

Soru 18: Çoklu Eşleme İşlemleri

Yorumdaki çağrılar sırayla yapılıyor. Üç remove ne döndürür, sonunda m nedir ve çoklu eşlemenin size()'ı (tüm (k, v) çiftleri) kaçtır?

Map<String, List<String>> m = new HashMap<>();
void put(String k, String v) {
  m.computeIfAbsent(k, x -> new ArrayList<>())
     .add(v);
}
boolean remove(String k, String v) {
  List<String> l = m.get(k);
  if (l == null || !l.remove(v)) return false;
  if (l.isEmpty()) m.remove(k);
  return true;
}
// put("BIL203","Ali"); put("BIL203","Can");
// put("MAT101","Ali"); put("BIL203","Ali");
// remove("BIL203","Ali");
// remove("MAT101","Ali");
// remove("MAT101","Ece");
Cevabı göster
  • put'lardan sonra: BIL203 → [Ali, Can, Ali], MAT101 → [Ali]. Anahtarın ilk put'unda computeIfAbsent boş liste açıp onu döndürür; sonrakilerde var olan listeyi döndürür.
  • remove("BIL203","Ali") → true: List.remove(Object) yalnızca ilk Ali'yi siler → [Can, Ali].
  • remove("MAT101","Ali") → true; liste boşaldığı için anahtar da silinir.
  • remove("MAT101","Ece") → false: m.get null döner.

Son: m = {BIL203=[Can, Ali]}, size = 2. Boş liste silinmeseydi containsKey("MAT101") true kalırdı. put ortalama O(1), remove(k, v) O(1 + liste boyu).

Çoktan Seçmeli · Çoklu Küme

Soru 19: computeIfAbsent Yerine put

Çoklu eşlemenin put metodu m.put(k, new ArrayList<>()); m.get(k).add(v); olarak yazılırsa put("A","x"), put("A","y"), put("B","z") sonrası m.get("A") ne döner?

Doğru: B. Her put, anahtarın listesini yeni ve boş bir listeyle değiştirir; x'i tutan liste kaybolur. A, computeIfAbsent'in sonucudur: liste yalnızca anahtar yoksa açılır, varsa mevcut liste döner. C, ilk listenin korunduğunu sanmaktır. D yanlış: put'tan hemen sonra m.get(k) null olamaz; NPE ancak hiç liste açmadan m.get(k).add(v) denenirse çıkar.

Klasik Soru · Ayrık Kümeler (Union-Find)

Soru 20: Quick-Find İzleme

7 öğeli Quick-Find yapısında başta id[i] = i. Sunumdaki union(p, q) (id'si id[p] olan herkesi id[q] yapar; eşitse hemen döner) ile sırasıyla union(0,1), union(2,3), union(1,3), union(4,5), union(5,3), union(6,2), union(0,6) çağrılıyor.

Her çağrıdan sonra id dizisi nedir? Döngüde toplam kaç id[i] == pid karşılaştırması ve kaç yazma yapılır?

Cevabı göster
unionpid → qidYazmaid
(0,1)0 → 11[1, 1, 2, 3, 4, 5, 6]
(2,3)2 → 31[1, 1, 3, 3, 4, 5, 6]
(1,3)1 → 32[3, 3, 3, 3, 4, 5, 6]
(4,5)4 → 51[3, 3, 3, 3, 5, 5, 6]
(5,3)5 → 32[3, 3, 3, 3, 3, 3, 6]
(6,2)6 → 31[3, 3, 3, 3, 3, 3, 3]
(0,6)3 = 3–değişmez (return)

İlk altı union'un her biri diziyi baştan sona tarar: 6 · 7 = 42 karşılaştırma; yazma 1 + 1 + 2 + 1 + 2 + 1 = 8. (6,2)'de qid, 2 değil id[2] = 3'tür. find O(1), ama her union O(n): n − 1 union O(n²).

Adım Adım Çalıştırma · Ayrık Kümeler (Union-Find)

Soru 21: Yinelemeli find İzleme

ebeveyn = [3, 0, 1, 6, 6, 4, 6] iken find(2) çağrılıyor. Ne döner ve çağrıdan sonra ebeveyn dizisi nedir?

kok = x = 2 ile başlanır. İlk döngü kökü bulur, ikincisi yolu kısaltır.

ebeveyn[2] = 1 ≠ 2 → kok = 1.

ebeveyn[1] = 0 ≠ 1 → kok = 0.

ebeveyn[0] = 3 ≠ 0 → kok = 3.

ebeveyn[3] = 6 → kok = 6; ebeveyn[6] = 6 olduğundan kök bulundu: yol 2 → 1 → 0 → 3 → 6.

ebeveyn[2] = 1 ≠ 6 → sonraki = 1 saklanır, ebeveyn[2] = 6.

x = 1: ebeveyn[1] = 0 ≠ 6 → sonraki = 0, ebeveyn[1] = 6.

x = 0: ebeveyn[0] = 3 ≠ 6 → sonraki = 3, ebeveyn[0] = 6.

x = 3: ebeveyn[3] = 6 = kok → döngü biter; 3 zaten köke bağlıydı.

return 6. Yol üzerindeki 2, 1, 0 köke bağlandı. Yol dışındaki 5 (5 → 4 → 6) değişmedi; onun yolu kendi find'ında kısalır.

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;
}
// ebeveyn = [3, 0, 1, 6, 6, 4, 6]; find(2)

Son

Bölüm 13: Kümeler ve Ayrık Kümeler: Çalışma Soruları
Konu anlatımı: Kümeler sunumu
1 / 1 Sercan KÜLCÜ, Tüm hakları saklıdır.