C ile Veri Yapıları ve Algoritmalar · Ders Notu

Bölüm 15: Çizgeler

Çizge, nesneleri ve aralarındaki bağlantıları bir arada tutan en genel veri yapısıdır. Bağlı liste ve ağaç onun özel durumlarıdır. Bu not Euler'in Königsberg köprüleri problemiyle başlar. Ardından düğüm, kenar, yol, derece ve bağlantılılık gibi temel kavramları tanımlar ve derece toplamı teoremini kanıtlar. Notun ağırlık merkezi çizgenin bellekte nasıl tutulacağıdır. Komşuluk matrisi, komşuluk listesi ve tersi, tek diziyle sıralı gösterim, ortogonal liste ve çok bağlı liste C koduyla kurulur; bellek ve işlem maliyetleri karşılaştırılır. Sonra çizge üzerindeki temel işlem olan gezinme gelir. Önce derinliğine arama özyinelemeyle, önce genişliğine arama dinamik bir kuyrukla yazılır ve adım adım izlenir. Son kesimde bu iki gezinme bağlantılı bileşenleri bulmak, döngü aramak ve çizgenin iki parçalı olup olmadığını sınamak için kullanılır. Her yordamın satır satır anlatımı, izleme tablosu ve karmaşıklık analizi verilir.

Öğrenme çıktıları

Tanımlamak
G = (V, E), yönlü ve yönsüz çizge, komşuluk, alt çizge, yol, derece ve bağlantılılık kavramlarını doğru kullanmak.
Kanıtlamak
Derecelerin toplamının 2e olduğunu ve bağlantılı bir çizgede en az n − 1 kenar bulunduğunu gerekçelendirmek.
Gerçeklemek
Komşuluk matrisini, komşuluk listesini, sıralı diziyi, ortogonal listeyi ve çok bağlı listeyi C ile kurmak; bellekteki görünümlerini çizmek.
Gezinmek
Önce derinliğine (DFS) ve önce genişliğine (BFS) aramayı kodlamak, elle izlemek ve O(n + e) analizini yapmak.
Uygulamak
Bağlantılı bileşenleri, döngüleri ve iki parçalılığı DFS ya da BFS ile bulmak.
Seçmek
Problemin işlemlerine bakarak uygun gösterimi seçmek: önce veri yapısı, sonra kod.

15.1Giriş: köprüler, haritalar ve ağlar

Şimdiye kadarki veri yapılarında öğeler arasındaki ilişkinin bir biçimi vardı. Dizide ve bağlı listede her öğenin en çok bir öncesi ve bir sonrası bulunur. Ağaçta her düğümün tek bir ebeveyni vardır; çocuklar yukarıya dönmez. Gerçek dünyadaki pek çok ilişki bu kalıplara uymaz. Bir şehirden birçok yol çıkar ve bu yollar birbirine karışarak başka şehirlerde birleşir. Bir ağdaki bilgisayar birden çok bilgisayara bağlıdır; bağlantılar halkalar oluşturabilir. Böyle ilişkileri hiçbir kısıt koymadan tutan yapıya çizge (graph) denir.

15.1.1Königsberg'in yedi köprüsü

Çizge kuramının başlangıcı olarak 1736 yılı kabul edilir. Doğu Prusya'nın Königsberg şehri Pregel nehrinin iki yakasına kurulmuştu. Nehrin ortasında iki ada vardı. Şekil 15.1'in solundaki krokide adalar A ve D, kıyılar B (güney) ve C (kuzey) harfleriyle gösterilir. Bu dört kara parçası yedi köprüyle birbirine bağlanıyordu: A'dan C'ye iki, A'dan B'ye iki, A'dan D'ye bir, D'den C'ye bir ve D'den B'ye bir köprü.

Şehir halkının sorusu şuydu: Herhangi bir kara parçasından yola çıkıp her köprüden tam olarak bir kez geçerek başlanan yere dönülebilir mi? Deneme yanılmayla bir cevap bulunamıyordu. Leonhard Euler (“Oyler” diye okunur) soruyu çözmek için haritanın ayrıntılarını bir kenara bıraktı. Her kara parçasını bir nokta, her köprüyü iki noktayı birleştiren bir çizgi olarak çizdi. Noktalara bugün düğüm (vertex, node), çizgilere kenar (edge) denir. Ortaya çıkan çizim, şeklin sağındaki çizgedir. A ile C arasında iki ayrı kenar vardır, çünkü iki ayrı köprü vardır.

C (kuzey kıyısı)B (güney kıyısı)A (ada)D7 köprüCADBd=3d=5d=3d=3
Şekil 15.1: Königsberg köprüleri (solda) ve onlardan elde edilen çizge (sağda). Kırmızı şeritler köprülerdir. Her düğümün yanında derecesi yazar: A'nın derecesi 5, öteki üç düğümünki 3'tür.

Bu soyutlamanın önemi şudur: Soruyu belirleyen, kara parçalarının büyüklüğü, köprülerin uzunluğu ya da nehrin kıvrımları değildir. Yalnız hangi parçanın hangisine kaç köprüyle bağlı olduğu önemlidir. Çizgede düğümlerin kâğıt üzerindeki yeri de önemsizdir; A düğümü sola ya da sağa çizilebilir. Aynı çizge birçok farklı biçimde çizilebilir. Çizim bir gösterimdir, çizgenin kendisi bağlantılardan ibarettir.

Euler soruyu bir sayıya bağladı. Bir düğüme değen kenarların sayısına o düğümün derecesi (degree) denir. Yürüyüş sırasında bir ara düğüme her girişte o düğümden bir de çıkılır. Giriş ve çıkış farklı köprülerden yapılmak zorundadır. Öyleyse her uğrayışta düğümün iki kenarı harcanır. Başlangıç düğümü de aynı durumdadır: yürüyüş ondan çıkar ve sonunda ona girer. Bütün köprüleri birer kez kullanan ve başa dönen bir yürüyüşte her düğümün kenarları ikişer ikişer tükenmelidir. Bu nedenle böyle bir yürüyüş ancak her düğümün derecesi çiftse bulunabilir. Königsberg'de dereceler 5, 3, 3 ve 3'tür; dördü de tek sayıdır. Aranan yürüyüş yoktur.

Euler'in vardığı sonuç bugün onun adıyla anılır. Bağlantılı bir çizgede her kenardan bir kez geçip başa dönen yürüyüşe Euler devresi denir. Böyle bir devre, ancak ve ancak bütün dereceler çiftse vardır. Koşulun gerekli olduğu yukarıdaki sayma kanıtından çıkar. Yeterli olduğunun kanıtı daha uzundur ve çizge kuramı derslerinin konusudur. Bu notta önemli olan, bir şehir haritası sorusunun yalnızca sayılar ve bağlantılar üzerinden cevaplanabilmesidir.

15.1.2Çizgeler nerelerde kullanılır

Königsberg'den bu yana çizgeler hemen her mühendislik alanına girdi. Kullanımları o kadar geniştir ki çizgenin kullanılmadığı bir alan bulmak, kullanıldığı alanları saymaktan zordur. Tablo 15.1 başlıca örnekleri toplar.

Tablo 15.1: Çizgelerin kullanıldığı başlıca alanlar.
AlanDüğümKenarTipik soru
Elektrik devreleriBağlantı noktasıDevre elemanıAkım ve gerilimler nedir?
Proje planlama (aktivite ağları)Olay ya da işÖncelik ilişkisiProje en erken ne zaman biter?
KimyaAtomBağİki molekül aynı yapıda mı?
Yol ve bilgisayar ağlarıŞehir, yönlendiriciYol, hatEn kısa yol hangisi?
Taşıma ağlarıDepo, pompaBoru (kapasiteli)Kaynaktan hedefe en çok ne kadar akış geçer?
Altyapı kurmaKöyOlası kablo hattıBütün köyler en az kabloyla nasıl bağlanır?
Biçimsel dillerDurumGeçişSonlu özdevinir bu sözcüğü kabul eder mi?
Ağ güvenilirliğiBilgisayarBağlantıAğda kopukluk var mı, kaç parçaya ayrılmış?

Tablodaki soruların bir kısmı adlarıyla bilinen klasik problemlerdir. En kısa yol problemi bir haritada iki şehir arasındaki en ucuz güzergâhı arar. En büyük akış (max flow) problemi bir boru ağından birim zamanda en çok ne kadar petrol ya da su taşınabileceğini sorar. Minimum kapsayan ağaç (minimum spanning tree) problemi bütün köyleri birbirine en az toplam kablo uzunluğuyla bağlamak ister. Ağaçların kendileri de birer çizgedir; topolojik sıralama ve sonlu özdevinirler (finite automata) da çizge üzerinde tanımlanır. Bu problemlerin hepsi aynı temel üzerine kurulur: çizgenin bellekte nasıl tutulacağı ve üzerinde nasıl gezinileceği. Bu not tam olarak bu temeli kurar.

15.2Tanımlar ve terimler

Çizgeyle ilgili her yordam birkaç temel kavrama dayanır. Bu kesim onları kesin biçimde tanımlar ve notun geri kalanında kullanılacak örnek çizgeleri tanıtır.

15.2.1Çizgenin tanımı

Tanım 15.1

Bir çizge G = (V, E) iki sonlu kümeden oluşur. V(G), boş olmayan düğüm kümesidir. E(G), düğüm çiftlerinden oluşan ve boş olabilen kenar kümesidir. Düğüm sayısı n = |V|, kenar sayısı e = |E| ile gösterilir.

Düğüm kümesinin boş olmaması bir gerekliliktir: düğümsüz bir çizge üzerinde hiçbir işlem anlamlı değildir. Kenar kümesi ise boş olabilir. Tek bir düğümden oluşan ya da birbirine hiç bağlı olmayan düğümlerden oluşan çizgeler de geçerlidir. Aşağıdaki seyrek çizge örneğinde 11 düğümün 7'si hiçbir kenara değmez.

Kenarın iki ucunun sırası önemliyse çizge yönlüdür (directed graph, digraph); önemsizse yönsüzdür (undirected graph). Gösterimde bu fark parantez biçimiyle verilir:

  • Yönsüz çizgede kenar sıradan bağımsız bir çifttir ve normal parantezle yazılır: (v0, v1) ile (v1, v0) aynı kenardır.
  • Yönlü çizgede kenar sıralı bir çifttir ve açılı parantezle yazılır: <v0, v1> ≠ <v1, v0>. Okun çıktığı ilk düğüme kuyruk (tail), okun saplandığı ikinci düğüme baş (head) denir: <kuyruk, baş>.

Kenar kümesi, düğüm kümesinin kendisiyle kartezyen çarpımı V × V'nin bir alt kümesidir. Bu çarpım bütün sıralı ikilileri içerir; çizge bunlardan yalnız bazılarını seçer. Seçilen ikililer, hangi düğümün hangisine bağlı olduğunu söyler.

Not boyunca dört küçük örnek çizge kullanılır. Şekil 15.2 onları çizer, Tablo 15.2 de düğüm ve kenar kümelerini yazar.

0123G1: bağlantılı (K4)0123456G2: ağaç, döngü yok012G3: yönlü02134567G4: iki bileşen
Şekil 15.2: Örnek çizgeler. G1 dört düğümlü tam çizgedir. G2 yedi düğümlü bir ağaçtır. G3 üç düğümlü yönlü bir çizgedir; 0 ile 1 arasında iki yönde birer kenar vardır. G4 iki parçadan oluşur: {0, 1, 2, 3} ve {4, 5, 6, 7}.
Tablo 15.2: Örnek çizgelerin düğüm ve kenar kümeleri.
ÇizgeV(G)E(G)ne
G1{0, 1, 2, 3}{(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)}46
G2{0, 1, …, 6}{(0,1), (0,2), (1,3), (1,4), (2,5), (2,6)}76
G3{0, 1, 2}{<0,1>, <1,0>, <1,2>}33
G4{0, 1, …, 7}{(0,1), (0,2), (1,3), (2,3), (4,5), (5,6), (6,7)}87

G3'ün kenar kümesinde <0,1> ile <1,0> ayrı ayrı yazılır. Yönlü çizgede bunlar iki farklı kenardır; birinin varlığı ötekini gerektirmez. Yönsüz bir çizgede ise (0,1) yazmak yeter; (1,0) aynı kenarın öteki yazılışıdır.

Bu notta aksi söylenmedikçe çizgeler basittir: iki düğüm arasında en çok bir kenar bulunur ve hiçbir kenar bir düğümü kendisine bağlamaz. Düğümü kendisine bağlayan kenara öz döngü (self-loop) denir. Königsberg çizgesi basit değildir; A ile C arasında iki kenar vardır. Böyle çizgelere çoklu çizge (multigraph) denir.

Sık yapılan hata

Yönlü bir çizgede <u, v> verilince <v, u>'yu da var saymak. G3'te <1,2> vardır ama <2,1> yoktur: 2'den hiçbir yere gidilemez. Yönsüz çizgede her kenar iki yönde de kullanılabilir; yönlüde yalnız oku izleyen yönde.

15.2.2Komşu düğüm ve alt çizge

Tanım 15.2

(v0, v1) yönsüz bir çizgenin kenarıysa v0 ile v1 komşudur (adjacent); kenar bu iki düğüme bitişiktir (incident). Yönlü çizgede <v0, v1> varsa v0, v1'e komşudur; v1 de v0'dan komşudur.

G1'de her düğüm öteki üçüyle komşudur. G2'de 0'ın komşuları 1 ile 2'dir; 3 ile 4 birbirine komşu değildir, çünkü aralarında kenar yoktur. İkisi de 1'e komşudur, ama bu onları birbirine komşu yapmaz.

Tanım 15.3

G′ çizgesi V(G′) ⊆ V(G) ve E(G′) ⊆ E(G) koşullarını sağlıyorsa G′, G'nin bir alt çizgesidir (subgraph).

Tanımın gizli bir koşulu vardır: G′ kendi başına bir çizge olmalıdır. Yani E(G′)'deki her kenarın iki ucu da V(G′)'de bulunmalıdır. Uçlarından biri seçilmemiş bir kenar alt çizgeye giremez. Kenar kümesi boş olabildiği için tek bir düğüm de geçerli bir alt çizgedir.

0123G100121230123G1’in olası alt çizgelerinden bazıları012G301012G3’ün alt çizgeleri
Şekil 15.3: Solda G1 ve onun dört alt çizgesi: tek düğüm 0; 0, 1, 2 düğümleri ve iki kenar; 1, 2, 3 üçgeni; 0'a bağlı üç kenar. Sağda G3 ve iki alt çizgesi: yalnız <0,1>; <0,1> ve <1,2>.

Bir çizgenin kaç alt çizgesi olduğu hızla büyür. G1'in yalnız kenar seçimleri için 26 = 64 olasılık vardır, üstelik düğüm seçimleri de eklenir. Şekil bunlardan yalnız birkaçını gösterir.

15.2.3Yol, bağlantılılık ve derece

Tanım 15.4

vp düğümünden vq düğümüne bir yol (path), (vp, vi1), (vi1, vi2), …, (vin, vq) çiftlerinin her biri E(G)'de bulunacak biçimde dizilmiş vp, vi1, …, vin, vq düğüm dizisidir. Yolun uzunluğu, içerdiği kenar sayısıdır. Yönlü çizgede çiftler <·,·> biçiminde, yani okların yönünde olmalıdır.

G2'de 3, 1, 0, 2, 5 dizisi 3'ten 5'e uzunluğu 4 olan bir yoldur. Bütün düğümleri birbirinden farklı olan yola basit yol denir. İlk ve son düğümü aynı olan, başka bir düğümü yinelenmeyen ve en az bir kenar içeren yola döngü (cycle) denir. G1'de 0, 1, 2, 0 bir döngüdür. G2 bir ağaç olduğu için hiç döngü içermez. Yönsüz basit çizgede bir döngü en az üç kenar içerir; 0, 1, 0 dizisi aynı kenardan gidip geri döner ve döngü sayılmaz.

Tanım 15.5

Yönsüz bir çizgede v0'dan v1'e bir yol varsa bu iki düğüm bağlantılıdır (connected). Birbirinden farklı her (vi, vj) düğüm çifti bağlantılıysa çizgenin kendisi bağlantılıdır.

G1 ve G2 bağlantılıdır. G4 bağlantılı değildir: 0'dan 5'e hiçbir yol yoktur. G4 iki ayrı parçadan oluşur. Bu parçalara bağlantılı bileşen denir; bileşenler bu notun son kesiminde ayrıntılı olarak ele alınır.

Tanım 15.6

Bir düğümün derecesi d(v), o düğümü uç kabul eden kenarların sayısıdır. Yönlü çizgede düğüme gelen kenarların sayısı giriş derecesi (in-degree), düğümden çıkan kenarların sayısı çıkış derecesi (out-degree) adını alır. Düğümün derecesi bu ikisinin toplamıdır.

Yönsüz örneklerde G2'de d(0) = 2, G1'de d(1) = 3, G2'de d(4) = 1'dir. Yönlü G3'te her düğümün iki sayısı ayrı ayrı hesaplanır (Tablo 15.3).

Tablo 15.3: G3'te giriş ve çıkış dereceleri.
DüğümGelen kenarlarGirişÇıkan kenarlarÇıkışDerece
0<1,0>1<0,1>11 + 1 = 2
1<0,1>1<1,0>, <1,2>21 + 2 = 3
2<1,2>1—01 + 0 = 1
012giriş: 1çıkış: 1
Şekil 15.4: G3'te 0 düğümü. Açık renkli ok 0'a gelen kenardır (giriş 1), koyu kırmızı ok 0'dan çıkan kenardır (çıkış 1). Düğüm 0'ın derecesi 1 + 1 = 2'dir.

Derece her zaman belirli bir düğüm için söylenir. “G3'ün derecesi 2'dir” gibi bir cümlenin anlamı yoktur; doğru ifade “G3'te 0 düğümünün derecesi 2'dir” biçimindedir. Bir çizge için söylenebilecek olanlar en büyük derece ya da derecelerin toplamıdır.

15.2.4Derecelerin toplamı

Derece ile kenar sayısı arasında basit ama çok kullanılan bir ilişki vardır.

Teorem 15.1 (Derece toplamı)

n düğümlü ve e kenarlı bir G çizgesinde, di ile i. düğümün derecesi gösterilirse

d0 + d1 + … + dn−1 = 2e

olur.

Derecelerin toplamı, her düğümün kendisine değen kenarları sayıp bu sayıların toplanmasıyla bulunur. Bu toplamda her kenar kaç kez sayılır? Bir (u, v) kenarı u'nun derecesinde bir kez, v'nin derecesinde bir kez sayılır. Başka hiçbir düğümün derecesine katılmaz. Öyleyse her kenar toplama tam olarak 2 katkı yapar ve toplam 2e'dir. Yönlü çizgede de aynı akıl yürütme geçerlidir: <u, v>, u'nun çıkış derecesine ve v'nin giriş derecesine birer katkı yapar. ∎

Kanıt kenarların ayrı ayrı sayılmasına dayanır; iki düğüm arasında birden çok kenar olması bir şey değiştirmez. Teorem bu yüzden çoklu çizgelerde de geçerlidir. Königsberg çizgesinde 5 + 3 + 3 + 3 = 14 = 2 · 7'dir. Öz döngüye izin verilirse böyle bir kenar düğümün derecesine 2 katkı yapacak biçimde sayılır ve teorem yine bozulmaz.

Şekil 15.5 teoremi G1 üzerinde adım adım gösterir. Kenarlar boş bir çizgeye birer birer eklenir. Her eklemede iki ucun derecesi birer artar ve toplam 2 büyür. Altı kenar eklendiğinde toplam 12'ye varır.

0123d=1d=1d=0d=0işlenen kenar: e = 1Σ d = 1 + 1 + 0 + 0 = 22e = 2
0123d=3d=3d=3d=3işlenen kenar: e = 6Σ d = 3 + 3 + 3 + 3 = 122e = 12
Şekil 15.5: Derece toplamı teoremi. Üstte ilk kenar (0,1) eklenmiştir: Σd = 2 = 2e. Altta G1'in altı kenarının hepsi eklenmiştir: her düğümün derecesi 3, toplam 12 = 2 · 6.

Teoremin hemen çıkan bir sonucu vardır: Bir çizgede derecesi tek olan düğümlerin sayısı çifttir. Toplam 2e çift olduğuna göre tek terimlerin sayısı tek olamaz. Bu sonuç “şu derecelere sahip bir çizge var mı?” sorusuna hızlı bir ön cevap verir.

Örnek 15.1

Derece dizisi (3, 3, 2, 2, 1) olan basit, yönsüz bir çizge var mıdır? Dereceler toplanır: 3 + 3 + 2 + 2 + 1 = 11. Toplam tektir, ama teoreme göre 2e'ye, yani bir çift sayıya eşit olmalıdır. Böyle bir çizge yoktur; kenar sayısı sorusu da anlamını yitirir. Dizi (3, 3, 2, 2, 2) olsaydı toplam 12, kenar sayısı 6 olurdu. Bu durumda teorem yalnız bir gerekli koşul verir; çizgenin gerçekten var olup olmadığını ayrıca kurarak göstermek gerekir. Örneğin 0–1–2–3–4–0 döngüsüne bir de (0,2) kenarı eklenirse dereceler (3, 2, 3, 2, 2) olur; düğümlerin adları değiştirilince dizi elde edilir.

Teoremin veri yapısı açısından önemi de büyüktür. Yönsüz bir çizgenin komşuluk listesinde her kenar iki kez, bir ucunun listesinde ve öteki ucunun listesinde, görünür. Listelerdeki toplam öğe sayısı derecelerin toplamıdır, yani 2e'dir. Bütün listeleri tarayan bir yordamın maliyeti bu nedenle O(n + e) olur. Bu cümle aşağıdaki analizlerin neredeyse hepsinde yeniden karşılaşılan bir akıl yürütmedir.

15.3Çizgeyi bellekte gerçeklemek

Kavramlar kâğıt üzerinde çizilerek anlaşılır, ama program çizimi göremez. Bir program için çizge, bellekteki sayılardan ve işaretçilerden ibarettir. Bu kesimin konusu ayrık matematik değildir; konusu, “çizgeyi bellekte nasıl tutarız?” sorusudur. Aynı çizge için birçok gösterim vardır ve aralarındaki seçim, çizge üzerinde yapılacak işlemlerin hızını belirler. Burada beş gösterim ele alınır:

  1. komşuluk matrisi,
  2. komşuluk listesi ve onun tersi,
  3. tek dizi ile sıralı gösterim,
  4. yönlü çizgeler için ortogonal liste,
  5. çok bağlı liste.

Bunlara kenarlara maliyet ekleyen ağırlıklı gösterim eklenir. Her gösterim için üç soru sorulur: Ne kadar bellek ister? “u ile v arasında kenar var mı?” sorusu ne kadar sürede cevaplanır? Bir düğümün bütün komşuları ne kadar sürede bulunur? Gezinme gibi algoritmaların hızı en çok bu son soruya bağlıdır.

15.3.1Komşuluk matrisi

Tanım 15.7

n düğümlü bir G çizgesinin komşuluk matrisi (adjacency matrix), n × n boyutlu iki boyutlu bir a dizisidir. Yönsüz çizgede (i, j) ∈ E(G), yönlü çizgede <i, j> ∈ E(G) ise a[i][j] = 1, değilse a[i][j] = 0'dır.

C'de matris, en çok düğüm sayısına göre boyutlanmış bir tamsayı dizisiyle tutulur: int a[MAX_DUGUM][MAX_DUGUM];. Tablo 15.4 G1 ile G3'ün matrislerini yan yana verir.

Tablo 15.4: Solda G1'in, sağda G3'ün komşuluk matrisi. Satır, kenarın kuyruğunu; sütun, başını gösterir.
0123G3012
001110010
110111101
211012000
31110

G1'de (0,2) bir kenar olduğu için hem a[0][2] hem a[2][0] 1'dir. Yönsüz çizgenin her kenarı matrise iki kez, köşegene göre simetrik iki hücreye yazılır. Bu yüzden yönsüz çizgenin matrisi simetriktir: her i, j için a[i][j] = a[j][i]. Basit çizgede öz döngü olmadığından köşegen tamamen sıfırdır. G3'ün matrisi ise simetrik değildir: a[1][2] = 1 ama a[2][1] = 0.

Simetri yer kazanmak için kullanılabilir. Matrisin yalnız alt ya da üst üçgeni saklanırsa hücre sayısı yaklaşık yarıya iner. Ama bellek yine de n(n − 1)/2 hücreyle orantılıdır; asimptotik olarak O(n²) değişmez. Matrisin belleği kenar sayısına hiç bağlı değildir: hiç kenarı olmayan bir çizge de, tam çizge de aynı yeri kaplar. 50 düğümlük bir int matrisi, 4 baytlık int ile 10 000 bayt tutar.

Matrisle iki işlem çok kolaydır. (u, v) kenarının varlığı tek bir hücreye bakılarak O(1)'de öğrenilir. Kenar eklemek ya da silmek de bir hücreye 1 ya da 0 yazmaktır, yine O(1). Öteki işlemler satırları taramayı gerektirir:

  • Derece. Yönsüz çizgede i'nin derecesi i. satırdaki 1'lerin sayısıdır (ya da aynı anlama gelen i. sütundakilerin). Satırda n hücre olduğu için maliyet O(n)'dir. Yönlü çizgede satır toplamı çıkış derecesini, sütun toplamı giriş derecesini verir; ikisi birlikte 2n hücre okur ve maliyet yine O(n) olur. G3'te 2'nin çıkış derecesi 0 (satır 2 boş), giriş derecesi 1'dir (sütun 2'de bir tane 1).
  • Kenar sayısı. Bütün 1'ler sayılıp ikiye bölünür (Teorem 15.1). G1'de 12 tane 1 vardır; kenar sayısı 6'dır. Simetri yüzünden yalnız üst üçgene bakmak yeter, ama bu n(n − 1)/2 hücre demektir. Maliyet O(n²)'den aşağı inmez.
  • Bir düğümün komşuları. Satırın n hücresi taranır: O(n). Düğümün tek komşusu olsa bile bütün satır okunur.

Son madde gezinme algoritmalarının maliyetini belirler ve matrisin asıl zayıflığıdır. Seyrek bir çizgede satırların çoğu sıfırdır, ama sıfırları atlamanın bir yolu yoktur.

G4'ün matrisi ve simetri denetimi

Matris elle yazılırken en sık hata, bir kenarı yalnız tek yönde işlemektir. Şekil 15.6 G4'ü, Tablo 15.5 doğru matrisini verir.

02134567
Şekil 15.6: G4 çizgesi. İki bileşenden oluşur: 0, 1, 2, 3 düğümlerinin oluşturduğu dörtgen ve 4–5–6–7 zinciri.
Tablo 15.5: G4'ün komşuluk matrisi. Matris simetriktir; iki bileşen köşegen üzerinde iki ayrı blok oluşturur.
01234567
001100000
110010000
210010000
301100000
400000100
500001010
600000101
700000010
Sık yapılan hata

G4'ün 1. satırını 1 0 1 0 0 0 0 0, 7. satırını … 0 0 1 biçiminde yazmak. Birinci satırdaki fazladan 1, var olmayan (1,2) kenarını ekler; yedinci satırdaki 1 ise 7'yi kendine bağlar. İkisi de simetriyi bozar: a[1][2] = 1 iken a[2][1] = 0 kalır. Doğrusu 1. satır için 1 0 0 1 0 0 0 0 (komşular 0 ve 3), 7. satır için 0 0 0 0 0 0 1 0'dır (tek komşu 6). Yazılan matrisin simetrisini a[i][j] == a[j][i] karşılaştırmasıyla sınamak bu hatayı hemen yakalar.

Matris bağlantılı bileşenleri bulmak için de kullanılabilir. Bunun için bir düğümden başlayıp ulaşılabilen bütün düğümler işaretlenir; işaretsiz kalan ilk düğümden yeniden başlanır. Ama her düğümün komşularını bulmak bir satır taraması, yani O(n) olduğundan toplam maliyet O(n²)'dir. Aynı iş komşuluk listesiyle çok daha ucuzdur.

Matriste derece: kod ve izleme

Yönlü bir çizgede bir düğümün derecesini hesaplayan yordam, satırı ve sütunu ayrı ayrı toplar:

int derece(int a[][MAX_DUGUM], int n, int v)
{
    int j, cikis = 0, giris = 0;
    for (j = 0; j < n; j++)
        cikis += a[v][j];      /* satir  */
    for (j = 0; j < n; j++)
        giris += a[j][v];      /* sutun */
    return cikis + giris;
}
  • Satır 1. İki boyutlu dizi parametre olarak geçirilirken ikinci boyut yazılmak zorundadır: int a[][MAX_DUGUM]. Derleyici a[v][j]'nin adresini v · MAX_DUGUM + j ile hesaplar ve bunun için satır uzunluğunu bilmelidir. Birinci boyut boş bırakılabilir. n gerçek düğüm sayısıdır; dizinin yalnız sol üst n × n bölümü kullanılır.
  • Satır 3. İki sayaç sıfırlanır. Başlangıç değeri unutulursa yerel değişkenler belirsiz değerle başlar ve sonuç rastgele olur.
  • Satır 4–5. v. satır taranır: a[v][j], <v, j> kenarı varsa 1'dir. Toplam çıkış derecesidir.
  • Satır 6–7. v. sütun taranır: a[j][v], <j, v> kenarı varsa 1'dir. Toplam giriş derecesidir.
  • Satır 8. İkisinin toplamı döndürülür.

Yordam yönsüz bir çizgeye uygulanırsa satır ve sütun aynı olduğundan her kenar iki kez sayılır ve derecenin iki katı döner. Yönsüz çizgede yalnız ilk döngü yeter. Tablo 15.6 derece(a, 3, 1) çağrısını G3 matrisi üzerinde izler.

Tablo 15.6: derece(a, 3, 1) izlemesi (G3). Satır numaraları yukarıdaki koda aittir.
AdımSatırjOkunan hücrecikisgiris
13——00
24–50a[1][0] = 110
34–51a[1][1] = 010
44–52a[1][2] = 120
56–70a[0][1] = 121
66–71a[1][1] = 021
76–72a[2][1] = 021
88—return 2 + 1sonuç 3
012001011012000a[][] (G3)012
Şekil 15.7: İzlemenin sonunda G3 ve matrisi. Düğüm 1 vurgulanmıştır; 1. satırdaki iki 1 çıkış derecesini, 1. sütundaki tek 1 giriş derecesini verir.

Her döngü tam n kez döner; toplam 2n hücre okunur. Maliyet O(2n) = O(n)'dir ve düğümün gerçek derecesinden bağımsızdır. Aynı yordam G3'ün öteki düğümleri için 2 (düğüm 0) ve 1 (düğüm 2) döndürür; bu değerler Tablo 15.3 ile örtüşür.

15.3.2Komşuluk listesi

Matrisin her satırı çoğunlukla sıfırlarla doludur. Komşuluk listesi bu sıfırları hiç saklamaz. Matrisin her satırı yerine bir bağlı liste tutulur ve listede yalnız 1 olan sütunların numaraları bulunur.

Tanım 15.8

Komşuluk listesi (adjacency list) gösteriminde her i düğümü için bir bağlı liste vardır; listenin öğeleri i'nin komşularının numaralarıdır. Listelerin baş işaretçileri n öğeli bir baslik dizisinde tutulur: baslik[i], i'nin komşu listesinin ilk öğesini gösterir.

C bildirimi kendine başvuran bir yapıyla yapılır:

#define MAX_DUGUM 50   /* en fazla dugum sayisi */
typedef struct oge *oge_gost;
typedef struct oge {
    int dugum;         /* komsu dugumun numarasi */
    oge_gost bag;      /* listedeki sonraki komsu */
} oge;                 /* ';' ve typedef adi unutulmamali */
oge_gost baslik[MAX_DUGUM];   /* her dugume bir liste */
  • Satır 1. MAX_DUGUM, programın tutabileceği en çok düğüm sayısıdır. Dizi boyutları bu sabitle belirlenir.
  • Satır 2. oge_gost, henüz tanımı yazılmamış struct oge'yi gösteren işaretçi tipidir. C'de bir yapının işaretçisi, yapının tanımından önce bildirilebilir; yapının boyutunun bilinmesi gerekmez. Bu sıra, yapının içinde kendi tipini gösteren bir alan kullanmayı kolaylaştırır.
  • Satır 3–6. Her liste öğesi iki alan taşır: komşunun numarası dugum ve listedeki sonraki öğeyi gösteren bag. Tanım } ve tip adı oge'den sonra ; ile biter. Noktalı virgül unutulursa derleyici hatayı çoğunlukla sonraki satırda bildirir ve hatanın yeri şaşırtıcı görünür.
  • Satır 7. baslik dizisi her düğüme bir liste başı ayırır. Küresel bir dizi olduğu için bütün öğeleri program başlarken NULL'dır; yani başta her liste boştur.
0123102320133012
Şekil 15.8: G1'in komşuluk listesi. Soldaki kutular baslik dizisinin hücreleridir. Her öğe iki bölmelidir: komşu numarası ve sonraki öğenin bağı. Eğik çizgi NULL bağı gösterir.

Şekil 15.8'de her düğümün üç komşusu vardır ve her liste üç öğelidir. Toplam 12 öğe bulunur; G1'in 6 kenarı vardır. Yönsüz çizgede her kenar iki ucunun listesinde birer kez görünür. Bu, gösterimin bir dezavantajıdır: aynı bilgi iki yerde saklanır ve kenar silinirken iki listenin de güncellenmesi gerekir.

Listelerin bellek ve zaman maliyetleri şöyledir:

  • Bellek. n başlık ve yönsüz çizgede 2e öğe: O(n + e). x86-64 üzerinde bir oge 16 bayttır (4 baytlık int, 4 bayt dolgu, 8 baytlık işaretçi); bir başlık 8 bayttır.
  • Derece. Yönsüz çizgede i'nin listesindeki öğeler sayılır: O(di). Matristeki O(n)'e göre derece küçükse büyük kazançtır.
  • Kenar sayısı. Bütün listelerdeki öğeler sayılır ve ikiye bölünür: n başlığa bakmak ve 2e öğeyi saymak gerekir, toplam O(n + e). Matristeki O(n²)'den çok daha azdır, çünkü e çoğu çizgede n²'den çok küçüktür.
  • (u, v) var mı? u'nun listesinde v aranır: O(du). Matriste bu soru O(1)'di; listenin kaybettiği tek yer burasıdır.

Çizge algoritmalarında karmaşıklık bu yüzden çoğunlukla hem n'ye hem e'ye bağlı yazılır. “O(n + e)” ifadesi, her düğüme ve her kenara sabit sayıda kez dokunan bir algoritmayı anlatır.

Yönsüz ve yönlü örnekler

0121032033124554665776G4’ün komşuluk listesi011022G3 (çıkan kenarlar)011021G3’ün tersi (gelen kenarlar)012
Şekil 15.9: Solda G4'ün komşuluk listesi; sağda G3'ün listesi (çıkan kenarlar) ve onun tersi (gelen kenarlar). Listeler küçükten büyüğe sıralıdır.

Yönsüz G4'ün listeleri sıralı tutulursa kenarları saymak biraz kısalır. Her (i, j) kenarı hem i'nin hem j'nin listesinde bulunur. Yalnız j > i olan öğeler sayılırsa her kenar bir kez sayılır ve ikiye bölmeye gerek kalmaz. Sıralı listede j < i olan öğeler başta toplanır, örneğin 3'ün listesindeki 1 ve 2. Bu öğeler hızla geçilir, kalanlar sayılır. Taranan öğe sayısı azalır, ama asimptotik maliyet yine O(n + e)'dir.

Yönlü G3'te durum farklıdır. <0,1> ile <1,0> ayrı kenarlar olduğundan her öğe bir kenardır ve hepsi sayılmalıdır. Liste yalnız çıkan kenarları tutar: baslik[i]'deki öğe sayısı i'nin çıkış derecesidir ve O(d)'de bulunur. Giriş derecesi ise listede doğrudan yazmaz. i'ye gelen kenarları bulmak için bütün listeleri gezip i'yi içeren öğeleri saymak gerekir: O(n + e). Giriş derecesi sık soruluyorsa bu maliyet kabul edilemez. Çözüm, listenin tersini, yani gelen kenarların listesini de tutmaktır. Bu ters liste (inverse adjacency list) bir sonraki alt kesimde kurulur.

Seyrek bir çizge: G5

Matris ile liste arasındaki farkı en iyi seyrek bir çizge gösterir. G5'in 11 düğümü ve yalnız 2 kenarı vardır: (1, 4) ve (7, 8).

056914710238G5
Şekil 15.10: G5: 11 düğüm, 2 kenar. Kenara değmeyen yedi düğüm tek başına birer bileşendir; çizgenin toplam 9 bileşeni vardır.

Komşuluk listesinde dört öğe bulunur: baslik[1] → 4, baslik[4] → 1, baslik[7] → 8 ve baslik[8] → 7. Kalan yedi başlık NULL'dır. Toplam 11 başlık ve 2e = 4 öğe tutulur. Matris ise 11 × 11 = 121 hücre ister; bunların yalnız 4'ü 1'dir ve 117 hücre boşa gider.

Buradan genel bir kural çıkar: seyrek çizgede (e, n²'ye göre küçükse) liste, yoğun çizgede (e, n²'ye yakınsa) matris düşünülür. Yoğun bir çizgede listenin işaretçileri matrisin bitlerinden çok daha fazla yer tutar. Ama karar yalnız yoğunluğa göre verilmez. Asıl ölçüt, çizge üzerinde yapılacak işlemlerdir. Program sürekli “bu iki düğüm komşu mu?” diye soruyorsa seyrek çizgede bile matris daha hızlı olabilir. Program her düğümün komşularında geziniyorsa yoğun bir çizgede bile liste daha uygun olabilir.

15.3.3Listeyi kurmak: kenar ekleme

Örneklerdeki listeleri bir kişi çizgeye bakarak çizdi. Program ise çizgeye bakamaz. Kenarlar programa bir dosyadan ya da kullanıcıdan birer birer gelir ve her biri geldiği anda yapıya eklenebilmelidir. Bu işi yapan yordam şudur:

void kenar_ekle(int u, int v)   /* u -> v */
{
    oge_gost p = malloc(sizeof(oge));
    if (p == NULL) exit(1);       /* bellek yok */
    p->dugum = v;
    p->bag = baslik[u];   /* basa ekle: O(1) */
    baslik[u] = p;
}
  • Satır 1. Yordam u → v yönünde tek bir kenar ekler: u'nun listesine v'yi koyar. Yönsüz bir (u, v) kenarı için yordam iki kez, kenar_ekle(u, v) ve kenar_ekle(v, u) biçiminde çağrılır.
  • Satır 3. Yeni öğe için heap bellekten yer alınır. malloc'un döndürdüğü void *, C'de dönüşüm yazmadan oge_gost'a atanabilir. sizeof(oge) yerine sizeof(oge_gost) yazmak sık bir hatadır: işaretçinin boyutu kadar (8 bayt) yer alınır, oysa öğe 16 bayttır ve sonraki atamalar ayrılmamış belleğe yazar.
  • Satır 4. malloc bellek bulamazsa NULL döndürür. Denetim yapılmazsa bir sonraki satır NULL işaretçi üzerinden yazar ve program çöker. Burada en basit tepki seçilmiştir: program bir hata koduyla sonlanır.
  • Satır 5. Komşu numarası öğeye yazılır.
  • Satır 6. Yeni öğenin bağı, listenin şimdiki ilk öğesini gösterir. Liste boşsa bu değer NULL'dır ve yeni öğe listenin son öğesi de olur.
  • Satır 7. Liste başı yeni öğeyi gösterir. Satır 6 ile 7'nin sırası önemlidir: önce 7 yazılsaydı eski liste başı kaybolur ve listenin geri kalanına ulaşılamazdı.

Yordam listeyi taramaz; her kenar sabit sayıda adımda, O(1)'de yerleşir. e kenarlı yönsüz bir çizge 2e çağrıyla kurulur; başlıkların sıfırlanması da eklenince kurulum O(n + e) sürer.

Tablo 15.7, kenarlar (0,1), (0,2), (1,2) sırasıyla geldiğinde listelerin nasıl oluştuğunu izler. Tabloda &oge(k), komşu numarası k olan öğenin adresini gösterir.

Tablo 15.7: Üç kenarlı bir üçgenin komşuluk listesinin kurulması. Her yönsüz kenar iki çağrıyla eklenir.
Çağrıp->dugump->bagbaslik[0]baslik[1]baslik[2]
kenar_ekle(0, 1)1NULL1——
kenar_ekle(1, 0)0NULL10—
kenar_ekle(0, 2)2&oge(1)2 → 10—
kenar_ekle(2, 0)0NULL2 → 100
kenar_ekle(1, 2)2&oge(0)2 → 12 → 00
kenar_ekle(2, 1)1&oge(0)2 → 12 → 01 → 0
021102012kurulan kenarlar koyu
021120210012kurulan kenarlar koyu
Şekil 15.11: Listenin kurulması. Üstte ilk üç çağrıdan sonra: baslik[0]'da 2 ve 1, baslik[1]'de 0. Altta altı çağrının sonunda. Her yeni öğe listenin başına girdiği için listeler gelen sıranın tersidir.

Sonuçta 0'ın listesi 2 → 1'dir; kenarlar 1, 2 sırasıyla gelmiştir. Başa ekleme, listeyi gelen sıranın tersine çevirir. Bu durum bir hata değildir, ama gezinme algoritmalarının çıktısını etkiler; çünkü gezinme komşuları listedeki sırayla ziyaret eder. Liste sırası değişince gezinme sırasının da değiştiği, gezinme kesimindeki örneklerde açıkça görülür.

Başa, sona ya da sıralı ekleme

Bir öğe listeye üç yerden girebilir. Seçim, istenen sıraya ve maliyete bağlıdır (Tablo 15.8).

Tablo 15.8: Komşuluk listesine ekleme seçenekleri. d, listenin o anki uzunluğudur.
YöntemBir eklemeListe sırasıNot
Başa eklemeO(1)Gelen sıranın tersiKurulum O(n + e)
Sona ekleme (yürüyerek)O(d)Gelen sıraHer seferinde NULL'a kadar yürünür
Sona ekleme (son işaretçisiyle)O(1)Gelen sıraHer liste için ayrıca bir son[i] tutulur
Sıralı eklemeO(d)Küçükten büyüğeGezinme sırası öngörülebilir olur

Sıralı listeler çizgenin hangi sırayla kurulduğundan bağımsızdır; gezinme çıktısı yalnız çizgeye bağlı olur. Notun izleme örneklerinde listeler bu yüzden sıralıdır. Sıralı ekleme, bağlı listelerdeki işaretçinin işaretçisi tekniğiyle kısa yazılır:

void sirali_ekle(int u, int v)   /* u listesine v, artan sirada */
{
    oge_gost *pp = &baslik[u];   /* isaretcinin isaretcisi */
    oge_gost p;
    while (*pp && (*pp)->dugum < v)
        pp = &(*pp)->bag;
    p = malloc(sizeof(oge));
    if (p == NULL) exit(1);
    p->dugum = v;
    p->bag = *pp;
    *pp = p;
}

pp, değiştirilecek işaretçi alanının adresini tutar. Başta bu alan baslik[u]'dur. Döngü, v'den küçük öğeleri geçerken pp'yi her öğenin bag alanına ilerletir. Döngü bittiğinde *pp, yeni öğenin önüne gireceği öğeyi (ya da NULL'u) gösterir. Yeni öğe *pp'nin yerine yazılır. Listenin başına, ortasına ve sonuna ekleme aynı iki satırla yapılır; “liste boş mu, başa mı ekleniyor” gibi özel durumlar gerekmez.

Belleği geri vermek

Her malloc çağrısının karşılığında bir free çağrısı bulunmalıdır. Program sonunda ya da çizge artık gerekmediğinde her baslik[i] listesi gezilerek öğeler serbest bırakılır:

void listeleri_birak(oge_gost bas[], int n)
{
    int i;
    oge_gost p;
    for (i = 0; i < n; i++) {
        while (bas[i]) {
            p = bas[i];
            bas[i] = p->bag;   /* once bag saklanir */
            free(p);
        }
    }
}

Döngünün içindeki sıra bağlı listelerden bilinen kuraldır: öğe serbest bırakılmadan önce bağı saklanır. free(p) sonrasında p->bag'i okumak serbest bırakılmış belleğe erişmektir ve tanımsız davranıştır. Yordam her öğeye bir kez dokunur: O(n + e). Notun bütün C kodları sayaçlı bir malloc/free sarmalayıcısıyla çalıştırıldı; listeler bu yordamla bırakıldığında ayrılan ve geri verilen blok sayıları eşitti.

Sık yapılan hata

Listeyi kurarken malloc sonucunu denetlememek ve program sonunda listeleri serbest bırakmamak. Küçük bir deneme programında bu hatalar görünmez. Çizgeyi defalarca kurup yıkan uzun süreli bir programda ise bellek sızıntısı yavaş yavaş birikir ve sonunda malloc gerçekten NULL döndürür.

15.3.4Ters komşuluk listesi

Yönlü çizgede komşuluk listesi çıkan kenarları tutar. Ters liste ise gelen kenarları tutar: ters[j], j'ye kenar gönderen düğümlerin listesidir. Bu, çizgenin bütün oklarının ters çevrildiği transpoz çizgenin komşuluk listesidir. Ters liste, mevcut listeleri bir kez gezerek kurulur:

void devrik_kur(oge_gost bas[], oge_gost ters[],
               int n)
{
    int i;
    oge_gost p;
    for (i = 0; i < n; i++) ters[i] = NULL;
    for (i = 0; i < n; i++)
        for (p = bas[i]; p; p = p->bag)
            ekle(ters, p->dugum, i);  /* <j,i> */
}

Yordamın kullandığı ekle, kenar_ekle'nin herhangi bir başlık dizisi üzerinde çalışan biçimidir:

void ekle(oge_gost bas[], int u, int v)   /* bas[u] listesine v */
{
    oge_gost p = malloc(sizeof(oge));
    if (p == NULL) exit(1);
    p->dugum = v;
    p->bag = bas[u];
    bas[u] = p;
}
  • Satır 6. Ters listenin bütün başlıkları NULL yapılır.
  • Satır 7–8. Her i düğümünün listesi baştan sona gezilir. Listede j numaralı bir öğe <i, j> kenarı demektir.
  • Satır 9. Bu kenar ters çizgede <j, i> olur: ters[j] listesine i eklenir. Açıklama satırındaki <j,i> bunu söyler.
Tablo 15.9: devrik_kur(bas, ters, 3) izlemesi (G3). bas[0] = 1, bas[1] = 0 → 2, bas[2] = NULL.
Satırip->dugumKenarEklenen
6———ters[0..2] = NULL
8–901<0,1>ters[1]'e 0
8–910<1,0>ters[0]'a 1
8–912<1,2>ters[2]'ye 1
7–82——bas[2] boş, ekleme yok
73——döngü biter
bas[] (G3)011022ters[] (transpoz)011021
Şekil 15.12: G3'ün komşuluk listesi (solda) ve devrik_kur ile kurulan tersi (sağda). ters[2]'deki 1, 2'ye 1'den gelindiğini söyler.

Dış döngü n kez döner; iç döngüler toplamda her öğeye bir kez bakar. Her ekle çağrısı O(1)'dir. Ters liste bu yüzden O(n + e)'de kurulur. Kurulduktan sonra giriş derecesi de ters[i]'deki öğeleri saymakla, O(d)'de bulunur. Bedeli bellektir: her kenar iki kez, bir kez çıkış listesinde, bir kez giriş listesinde saklanır.

15.3.5Dizi ile sıralı gösterim

Komşuluk listesinin her öğesi bir işaretçi taşır. 64 bitlik bir makinede bu işaretçi, 4 baytlık komşu numarasının iki katı yer tutar. Çizge hiç değişmeyecekse işaretçilerden tümüyle kurtulmak mümkündür: bütün listeler tek bir diziye art arda yazılır.

Tanım 15.9

n düğümlü ve e kenarlı yönsüz bir çizgenin sıralı gösterimi (sequential representation), n + 2e + 1 öğeli tek bir int dugum[] dizisidir. İlk n + 1 hücre başlangıç indislerini tutar. i'nin komşuları dugum[dugum[i]], …, dugum[dugum[i+1] − 1] hücrelerindedir.

Dizinin iki bölümü vardır. Baştaki n + 1 hücre bir içindekiler tablosu gibidir: dugum[i], i'nin komşularının dizide nereden başladığını söyler. Arkadaki 2e hücre komşu numaralarının kendisidir; düğümlerin listeleri sırayla, aralarında boşluk bırakılmadan yazılır. i'nin komşuları bir sonraki düğümün listesinin başladığı yerde biter. Son düğümün de bir bitiş sınırı olması için fazladan bir hücre, dugum[n], eklenir. Bu yüzden hücre sayısı n + 1 + 2e'dir.

0911121331541751862072282391102110123130143151162175184196205217226n + 1 = 9 başlık (düğüm[n] = n + 2e + 1 = 23)2e = 14 komşu kaydı
Şekil 15.13: G4'ün sıralı gösterimi. 0–8 indisli hücreler başlangıç indisleridir; dugum[8] = 23 son düğümün bitiş sınırıdır. 9–22 indisli 14 hücre komşu numaralarını tutar.

G4 için n = 8 ve e = 7'dir; dizi 8 + 14 + 1 = 23 hücrelidir. Komşu kayıtları 9. indisten başlar. 0'ın iki komşusu 9 ve 10. hücrelere, 1'in iki komşusu 11 ve 12. hücrelere yazılır; bu böyle sürer. Tablo 15.10 diziyi okumayı örneklerle gösterir.

Tablo 15.10: G4'ün sıralı gösteriminden komşu okuma. Başlangıç dahil, bitiş hariçtir.
Düğüm idugum[i]dugum[i+1]Okunan indislerKomşularDerece
09119, 101, 22
1111311, 120, 32
417181751
5182018, 194, 62
722232261

Bir düğümün komşularını yazan yordam iki sınır arasında yürür:

void komsulari_yaz(const int dugum[], int i)
{
    int k;
    for (k = dugum[i]; k < dugum[i + 1]; k++)
        printf("%d ", dugum[k]);
    printf("\n");
}

Dizi const int dugum[] olarak alınır; yordam diziyi yalnız okur ve derleyici yanlışlıkla yazılmasını engeller. Döngü değişkeni k, dugum[i]'den başlar ve dugum[i + 1]'e varmadan durur. Tablo 15.11 komsulari_yaz(dugum, 5) çağrısını izler.

Tablo 15.11: komsulari_yaz(dugum, 5) izlemesi (G4).
SatırkKoşul k < dugum[6]dugum[k]Çıktı
4–51818 < 20 doğru44
4–51919 < 20 doğru64 6
4, 62020 < 20 yanlış—4 6 ve satır sonu

Döngü düğümün derecesi kadar döner: O(d). Derecenin kendisi döngüye bile gerek kalmadan dugum[i+1] − dugum[i] farkıyla, O(1)'de bulunur. Bellek n + 2e + 1 tamsayıdır ve hiç işaretçi yoktur. G4 için 23 tamsayı, yani 92 bayt yeter. Aynı çizgenin komşuluk listesi 8 başlık (64 bayt) ve 14 öğe (224 bayt) ister.

Dizi var olan listelerden tek geçişte kurulabilir:

int sirali_kur(int dugum[], int n)   /* listelerden tek dizi */
{
    int i, k = n + 1;                /* komsular n+1'den baslar */
    oge_gost p;
    for (i = 0; i < n; i++) {
        dugum[i] = k;                /* i'nin ilk komsusunun yeri */
        for (p = baslik[i]; p; p = p->bag)
            dugum[k++] = p->dugum;
    }
    dugum[n] = k;                    /* son dugumun bitis siniri */
    return k;                        /* kullanilan hucre: n + 2e + 1 */
}

Yordam her düğüm için önce başlangıç indisini yazar, sonra listesindeki komşuları sıradaki boş hücrelere kopyalar. Döngü bitince k, kullanılan hücre sayısına, yani n + 2e + 1'e eşittir. Bu değer dugum[n]'e yazılarak son düğümün sınırı konur. G4'ün sıralı listelerine uygulandığında Şekil 15.13'teki dizinin aynısını üretir.

Gösterimin bedeli değişikliktir. Yeni bir kenar eklemek, iki listeye birer hücre açmak demektir. Bu da arkadaki bütün hücreleri kaydırmayı ve kaydırılan düğümlerin başlangıç indislerini güncellemeyi gerektirir: O(n + e). Çok sayıda ekleme ve silme yapılan bir çizge için bu yapı verimli değildir. Depolamak ayrı bir konudur, saklanan veri üzerinde çalışacak algoritmaların etkinliği ayrı bir konudur. Örneğin bir binadaki bilgisayar ağı neredeyse hiç değişmez. Problem yalnız bu ağı tutup üzerinde sorgu yapmaksa sıralı gösterim hem az yer kaplar hem de bellekte bitişik durduğu için hızlı okunur. Seyrek matrislerdeki sıkıştırılmış satır gösterimi (CSR) aynı fikrin matrise uygulanmış biçimidir.

15.3.6Yönlü çizgelerin ortogonal gösterimi

Yönlü çizgede hem çıkış hem giriş derecesi sık soruluyorsa komşuluk listesi ile tersini birlikte tutmak gerekir. Bu durumda her kenar iki öğeyle, toplam 2e öğeyle saklanır. Ortogonal gösterim her kenarı tek bir öğede tutar ve bu öğeyi aynı anda iki listeye bağlar.

Tanım 15.10

Ortogonal liste (orthogonal list) gösteriminde yönlü çizgenin her <kuyruk, baş> kenarı için tek bir öğe vardır. Öğe dört alan taşır: kuyruk, baş, satır bağı (aynı kuyruktan çıkan sonraki kenar) ve kolon bağı (aynı başa gelen sonraki kenar). Her düğümün bir satır başlığı (çıkış listesi) ve bir sütun başlığı (giriş listesi) vardır.

001122001122satır başlıklarısütun başlıkları (aynı öğeler, iki kez çizildi)011012kuyruk | baş | kolon bağı | satır bağıkoyu hücre = NULL
Şekil 15.14: G3'ün ortogonal gösterimi. Solda satır başlıkları, üstte sütun başlıkları vardır. Her kenar öğesi bir satır (çıkış) ve bir sütun (giriş) listesindedir. Koyu hücreler NULL bağlardır.

Gösterimin adı yapının görünüşünden gelir: öğeler bir matrisin dolu hücreleri gibi satır ve sütunlara dizilir. Bir düğümün satırında gezinmek o düğümden nereye çıkıldığını, sütununda gezinmek o düğüme nereden gelindiğini verir. Aynı kenar için tek öğe yeter. G3'te <1,0> öğesi hem 1'in satırında hem 0'ın sütunundadır.

Bu yapı seyrek matrislerin bağlı gösterimine çok benzer. Seyrek matriste de her sıfır olmayan değer bir öğedir ve kendi satır ve sütun listelerine bağlanır. Yapı ihtiyaca göre döngülü, çift yönlü bağlı ya da başlık düğümlü kurulabilir. Çıkış ve giriş listeleri O(d)'de gezilir; başa ekleme O(1)'dir.

Bu gösterimin ortaya çıkışı öğretici bir düşünme biçimini gösterir. Liste ile tersini birlikte tutan çözüm doğrudur ama 2e öğe kullanır. Bu israf rahatsız edicidir ve “aynı işi daha az yerle yapabilir miyim?” sorusunu doğurur. Akla gelen ilk çözüme ve onun ilk koduna hiçbir zaman tam güvenilmemelidir. Arka plandaki veri yapısı başka bir şey olsaydı problem daha verimli çözülebilir miydi? Bu soruyu sormak, iyi bir veri yapısı tasarımının ilk adımıdır.

15.3.7Çok bağlı liste

Yönsüz çizgenin komşuluk listesinde de her kenar iki öğeyle saklanır. Kenarlara bir bilgi eklenecekse (örneğin “bu kenar işlendi” işareti) bu bilginin iki kopyasını tutarlı tutmak gerekir. Çok bağlı liste bu tekrarı kaldırır.

Tanım 15.11

Çok bağlı liste (adjacency multilist) gösteriminde yönsüz çizgenin her (vi, vj) kenarı için tek bir öğe vardır. Öğe hem vi'nin hem vj'nin kenar listesinde yer alır. Temel öğe düğüm değil, kenardır.

typedef struct kenar *kenar_gost;
typedef struct kenar {
    short isaret;        /* islendi mi */
    int dugum1, dugum2;  /* uclar */
    kenar_gost list1;    /* dugum1 sonraki */
    kenar_gost list2;    /* dugum2 sonraki */
} kenar;
kenar_gost baslik[MAX_DUGUM];
  • Satır 3. isaret, kenarın işlenip işlenmediğini tutar. Kenar iki listede göründüğü için bir gezinme aynı kenara iki kez rastlar; işaret, ikinci rastlayışta kenarın yeniden ele alınmasını önler.
  • Satır 4. dugum1 ve dugum2 kenarın iki ucudur.
  • Satır 5–6. list1, dugum1'in listesindeki sonraki kenarı; list2, dugum2'nin listesindeki sonraki kenarı gösterir. Bir öğe iki ayrı listenin zincirinde aynı anda bulunur.
  • Satır 8. baslik[v], v'nin kenar listesindeki ilk öğeyi gösterir. Hiç kenarı olmayan düğümün başlığı NULL'dır.

Düğüme veri taşımak gerekirse (örneğin şehir adı) yapıya ayrı bir düğüm dizisi ya da öğeye char veri gibi bir alan eklenir. x86-64 üzerinde bu kenar yapısı dolgu baytlarıyla 32 bayttır. Komşuluk listesinde aynı kenar iki öğeyle 2 · 16 = 32 bayt tutar. Kazanç bellekte değil, bilginin tek yerde durmasındadır.

Tablo 15.12 G1'in çok bağlı listesini verir. Altı kenar N0–N5 öğeleridir.

Tablo 15.12: G1'in çok bağlı listesi. Başlıklar: baslik[0] = N0, baslik[1] = N0, baslik[2] = N1, baslik[3] = N2.
Öğedugum1dugum2list1list2Hangi listelerde
N001N1N30 ve 1
N102N2N30 ve 2
N203NULLN40 ve 3
N312N4N51 ve 2
N413NULLN51 ve 3
N523NULLNULL2 ve 3

Tabloyu okumak için bir düğümün listesi izlenir. 1'in listesi baslik[1] = N0'dan başlar. N0'da 1, dugum2'dir; sonraki kenar list2 = N3'tür. N3'te 1, dugum1'dir; sonraki kenar list1 = N4'tür. N4'te 1 yine dugum1'dir ve list1 = NULL ile liste biter. 1'in kenarları N0, N3, N4; komşuları 0, 2 ve 3'tür.

Çok bağlı listede gezinme

Gezinmenin kuralı kısadır: v, öğenin dugum1 alanındaysa list1, dugum2 alanındaysa list2 izlenir. NULL görülünce liste biter.

void komsulari_yaz(int v)
{
    kenar_gost p = baslik[v];
    while (p) {
        if (p->dugum1 == v) {       /* v: dugum1 */
            printf("%d ", p->dugum2);  p = p->list1;
        } else {                    /* v: dugum2 */
            printf("%d ", p->dugum1);  p = p->list2;
        }
    }
}

Her öğede iki iş yapılır: öteki uç yazılır ve doğru bağ seçilir. v hangi uçta ise öteki uç komşudur ve izlenecek bağ v'nin tarafındaki bağdır. Tablo 15.13 komsulari_yaz(2) çağrısını izler.

Tablo 15.13: Çok bağlı listede komsulari_yaz(2) izlemesi (G1).
SatırpÖğe2 hangi uçtaYazılanSonraki p
3N1(0,2)———
5, 8N1(0,2)dugum20list2 = N3
5, 8N3(1,2)dugum21list2 = N5
5–6N5(2,3)dugum13list1 = NULL
4NULL——döngü biter; çıktı 0 1 3
01··N0N1N302··N1N2N303··N2∅N412··N3N4N513··N4∅N523··N5∅∅alanlar: dugum1 | dugum2 | list1 | list2 (∅ = NULL)p
Şekil 15.15: G1'in çok bağlı listesi bellekte. Her kutu dört alanlıdır: dugum1, dugum2, list1, list2; altlarındaki adlar bağların gösterdiği öğelerdir. p, izlemede 2'nin ikinci kenarı N3'e gelmiştir.

Döngü v'nin her kenarı için bir kez döner: O(dv). Her kenar bellekte yalnız bir kez saklanmıştır, ama iki ucunun listesinden de ulaşılabilir.

Çok bağlı listeye kenar eklemek

Yeni bir kenar iki listeye birden girer. Listelerin sonuna eklemek mümkündür, ama her iki listede NULL'a kadar yürümeyi gerektirir. Başa eklemek bu maliyeti ortadan kaldırır:

void ml_ekle(int u, int v)
{
    kenar_gost p = malloc(sizeof(kenar));
    if (p == NULL) exit(1);
    p->isaret = 0;
    p->dugum1 = u;  p->dugum2 = v;
    p->list1 = baslik[u];  /* u listesine */
    p->list2 = baslik[v];  /* v listesine */
    baslik[u] = baslik[v] = p;  /* ikisine de basa */
}
  • Satır 3–4. Öğe için bellek alınır ve denetlenir.
  • Satır 5–6. Kenar henüz işlenmemiştir; uçlar yazılır.
  • Satır 7. list1, u'nun eski ilk kenarını gösterir. Bu eski kenarda u'nun hangi uçta olduğu önemli değildir; gezinme yordamı her öğede bunu kendisi belirler.
  • Satır 8. list2, v'nin eski ilk kenarını gösterir.
  • Satır 9. İki başlık da yeni öğeyi gösterir. Satır 7 ile 8, bu atamadan önce yapılmalıdır; yoksa eski listeler kaybolur.

Ekleme O(1)'dir. Yönsüz çizgede tek bir başlık dizisi yeter. Yönlü bir çizgede ise ortogonal gösterimdeki gibi bir baş dizisiyle birlikte bir kuyruk dizisi tutulur. G1'in kenarları (0,1), (0,2), …, (2,3) sırasıyla ml_ekle ile eklenirse listeler gelen sıranın tersi olur; komsulari_yaz(2) bu kez 3 1 0 yazar. Tablo 15.12 ise kenarların listelerin sonuna eklendiği düzeni gösterir; o düzende çıktı 0 1 3'tür. İki yapı da doğrudur, ama hangi kodun hangi çizimi ürettiği tutarlı olmalıdır. Kenarlar bu sırayla gelir ve “her biri O(1)'de yerleşsin” denirse liste başına eklenir. Bu durumda listeyi sona ekleniyormuş gibi çizmek, kodun yaptığıyla çelişen bir çizimdir.

15.3.8Uygulama: aynı çizgenin beş gösterimi

Örnek 15.2

Kenarları <0,1>, <0,3>, <1,2>, <2,0>, <3,2> olan dört düğümlü yönlü çizge için komşuluk matrisi, komşuluk listesi, ters listesi ve çok bağlı (ortogonal) listesi kurulsun.

0132
Şekil 15.16: Uygulamanın yönlü çizgesi. 0 → 1 → 2 → 0 bir yönlü döngüdür.

Matris. <i, j> varsa a[i][j] = 1 yazılır; satır kuyruğu, sütun başı gösterir (Tablo 15.14). Satır toplamları çıkış derecelerini (2, 1, 1, 1), sütun toplamları giriş derecelerini (1, 1, 2, 1) verir. İki toplamın her biri 5'tir; yönlü çizgede her kenar bir çıkış ve bir giriş derecesine katkı yapar.

Tablo 15.14: Uygulama çizgesinin komşuluk matrisi.
0123çıkış
001012
100101
210001
300101
giriş11215

Liste ve tersi. Çıkış listeleri: 0 → 1, 3; 1 → 2; 2 → 0; 3 → 2. Giriş listeleri (transpoz): 0 ← 2; 1 ← 0; 2 ← 1, 3; 3 ← 0. İkisinde toplam 2e = 10 öğe vardır.

013122032021021330
Şekil 15.17: Uygulama çizgesinin komşuluk listesi (çıkan kenarlar, solda) ve tersi (gelen kenarlar, sağda).

Tek öğeli gösterim. Her kenar tek bir öğedir: N1 = <0,1>, N2 = <0,3>, N3 = <1,2>, N4 = <2,0>, N5 = <3,2>. Her öğe kuyruğunun çıkış listesinde ve başının giriş listesinde yer alır. Kuyruk bağı, aynı kuyruktan çıkan sonraki kenarı; baş bağı, aynı başa giren sonraki kenarı gösterir. Yalnız e = 5 öğe gerekir.

Tablo 15.15: Uygulama çizgesinin tek öğeli gösterimi. Solda kenar öğeleri, sağda düğüm başlıkları.
Öğekuyrukbaşkuyruk bağıbaş bağıDüğümçıkışgiriş
N101N2NULL0N1N4
N203NULLNULL1N3N1
N312NULLN52N4N3
N420NULLNULL3N5N2
N532NULLNULL

Sağlama için 2'nin giriş listesi izlenir: başlık N3'tür (<1,2>), N3'ün baş bağı N5'tir (<3,2>), N5'in baş bağı NULL'dır. 2'ye 1'den ve 3'ten gelinir; giriş derecesi 2'dir ve matrisin 2. sütunuyla örtüşür. 0'ın çıkış listesi N1'den başlar, kuyruk bağıyla N2'ye geçer ve biter: 0'dan 1'e ve 3'e gidilir.

15.3.9Gösterimlerin karşılaştırılması

Tablo 15.16 beş gösterimi aynı ölçütlerle yan yana koyar. du, u'nun derecesidir.

Tablo 15.16: Çizge gösterimlerinin bellek ve işlem maliyetleri.
GösterimBellek(u, v) var mı?u'nun komşularıEkleme / silme
Komşuluk matrisiO(n²)O(1)O(n)O(1)
Komşuluk listesiO(n + 2e)O(du)O(du)başa O(1); silme için arama O(d)
Sıralı dizin + 2e + 1 tamsayıO(du)O(du)pahalı: kaydırma O(n + e)
Ortogonal liste (yönlü)O(n + e)O(d)giriş ve çıkış O(d)başa O(1)
Çok bağlı listeO(n + e)O(du)O(du)başa O(1)

Tablonun hiçbir satırı her sütunda en iyi değildir. Matris kenar sorgusunda rakipsizdir ama komşu taramasında ve bellekte yavaştır. Listeler gezinmede hızlıdır ama kenar sorgusunda yavaştır. Sıralı dizi en az yeri tutar ama değişikliğe gelmez. Seçim, problemde hangi işlemin kaç kez yapılacağına bağlıdır.

Bu kesimin çıkarımı bir yöntemdir: önce verinin nasıl depolanacağı düşünülür, sonra kod yazılır. Akla gelen ilk çözüm iyi bir yöntem olmayabilir. Veri yapısı iyi tasarlanırsa n³ adımlı bir algoritmanın n adıma inmesi bile mümkün olabilir. Bunun ilk örneği hemen aşağıda görülür: aynı gezinme algoritması matrisle O(n²), listeyle O(n + e) sürer. Veriyi depolamak göründüğü kadar kolay bir iş değildir.

15.3.10Ağırlıklı çizgeler

Pek çok problemde kenarın yalnız var olması yetmez; bir de değeri vardır. Yol ağında bu değer mesafe, bilgisayar ağında gecikme, proje planında süre, kablo döşemede maliyettir. Kenarlarına sayı atanmış çizgeye ağırlıklı çizge (weighted graph), bu sayılara da ağırlık denir.

42581ABCD
Şekil 15.18: Dört düğümlü ağırlıklı bir çizge. Kenarların üzerindeki sayılar ağırlıklardır.

Her iki temel gösterim kolayca genişler. Matriste 1 yerine kenarın ağırlığı yazılır; böyle bir matrise maliyet matrisi denir. Kenar yoksa hücreye “sonsuz” anlamına gelen bir değer yazılır. Köşegen 0'dır: bir düğümden kendisine gitmenin maliyeti yoktur. Listede ise her öğeye bir agirlik alanı eklenir.

Tablo 15.17: Şekildeki çizgenin maliyet matrisi. Kenar olmayan çift ∞ ile gösterilir.
ABCD
A042∞
B4015
C2108
D∞580
#define SONSUZ 1000000   /* INT_MAX degil: toplamda tasar */
int maliyet[MAX_DUGUM][MAX_DUGUM];

typedef struct aoge *aoge_gost;
typedef struct aoge {
    int dugum;
    int agirlik;        /* kenarin maliyeti */
    aoge_gost bag;
} aoge;
aoge_gost abaslik[MAX_DUGUM];

C'de sonsuz diye bir tamsayı yoktur; büyük bir sabit seçilir. Akla ilk gelen INT_MAX'tır, ama bu tehlikeli bir seçimdir. En kısa yol algoritmaları sürekli d + w biçiminde toplamlar hesaplar. d = INT_MAX iken herhangi bir pozitif w eklenirse işaretli tamsayı taşar. C'de işaretli taşma tanımsız davranıştır; pratikte sonuç çoğunlukla büyük bir negatif sayıdır ve algoritma “sonsuz uzaklıktaki” düğümü en yakın düğüm sanır. SONSUZ bu yüzden özenle seçilmiştir. Gerçek ağırlıkların hepsinden büyüktür, ama iki katı bile int'e sığar. 2 · 1 000 000, 32 bitlik int'in sınırının (yaklaşık 2,1 milyar) çok altındadır.

Liste öğesi aoge, oge'ye bir int agirlik eklenmiş hâlidir; x86-64 üzerinde iki int 8 bayt, işaretçi 8 bayt eder ve dolgu gerekmez: 16 bayt. Minimum kapsayan ağaç ve en kısa yol algoritmalarının hepsi bu iki gösterimden birini kullanır.

15.4Gezinmeler

Çizge üzerindeki en temel işlem gezinmedir. Yönsüz bir G = (V, E) çizgesi ve bir v düğümü verilsin. v'den ulaşılabilen bütün düğümlere birer kez uğramak istenir. “Uğramak” ya da ziyaret etmek, düğümle ilgili işi yapmak demektir; bu notta düğümün numarasını yazdırmaktır. Bileşen bulma, döngü arama, en kısa yol ve kapsayan ağaç gibi pek çok algoritma bir gezinmenin üzerine kurulur.

Ağaçlarda gezinme kolaydır, çünkü ağaçta döngü yoktur ve her düğüme kökten tek bir yolla varılır. Çizgede ise bir düğüme birçok yoldan varılabilir. Bu yüzden çizge gezinmesinin ilk kuralı şudur: ziyaret edilen düğüm işaretlenir ve işaretli bir düğüme yeniden girilmez. İşaretler olmazsa gezinme bir döngü üzerinde sonsuza dek döner.

İki temel gezinme yöntemi vardır:

  • Önce derinliğine arama (depth first search, DFS; sözlükte “derinlik öncelikli arama” adıyla da geçer). Bir komşuya gidilir ve oradan olabildiğince derine inilir. Ağaçtaki kök önde gezinmeye benzer.
  • Önce genişliğine arama (breadth first search, BFS; sözlükte “genişlik öncelikli arama”). Önce başlangıcın bütün komşularına, sonra onların komşularına uğranır. Ağaçtaki düzey düzey gezinmeye benzer.

İki yöntemde de çizge komşuluk listesiyle gerçeklenir ve listeler sıralıdır. Örnek olarak sekiz düğümlü bir çizge kullanılır (Şekil 15.19).

012345670121034205631741752762773456
Şekil 15.19: Gezinmelerin örnek çizgesi (solda) ve sıralı komşuluk listesi (sağda). Çizgenin 8 düğümü ve 10 kenarı vardır; listelerde 2e = 20 öğe bulunur.

15.4.1Önce derinliğine arama

DFS'nin fikri bir labirentte ip çekerek ilerlemeye benzer. v ziyaret edilir. Sonra v'nin listesinden ziyaret edilmemiş bir w seçilir ve arama w üzerinden sürer. w'nin de ziyaret edilmemiş bir komşusu varsa ona geçilir. Ziyaret edilmemiş komşusu kalmayan bir u düğümüne gelindiğinde bir adım geri dönülür ve u'ya hangi düğümden gelindiyse onun listesinde kalınan yerden devam edilir.

Bu geri dönüşler için her düğümün listesinde nerede kalındığının hatırlanması gerekir. Son giren ilk çıktığı için bu bilgi bir yığında (stack) tutulur: derine inerken düğümler yığına konur, gidecek yer kalmayınca yığından bir düğüm alınıp devam edilir. Açık bir yığınla yazılmış DFS karmaşık görünür. Özyinelemeli yordam ise aynı işi çok kısa yapar, çünkü yığını derleyicinin çağrı yığını tutar. Her çağrının yerel w değişkeni, o düğümün listesinde nerede kalındığını saklar.

Önce ortak bildirimler:

#include <stdio.h>
#include <stdbool.h>
#define TRUE  true
#define FALSE false
bool ugranildi[MAX_DUGUM];   /* ';' unutulmamali */

stdbool.h, C99'dan beri bool tipini ve true/false değerlerini verir. TRUE ve FALSE makroları ders kitabındaki yazımı korumak içindir. ugranildi küresel bir dizidir; program başlarken bütün öğeleri false'tur. Gezinme yeniden başlatılacaksa dizinin elle sıfırlanması gerekir.

void dfs(int v)   /* once derinligine arama */
{
    oge_gost w;
    ugranildi[v] = TRUE;    /* ugradim */
    printf("%d ", v);
    for (w = baslik[v]; w; w = w->bag)
        if (!ugranildi[w->dugum])
            dfs(w->dugum);
}
  • Satır 3. w, v'nin listesinde ilerleyen işaretçidir. Her özyinelemeli çağrının kendi w'si olur.
  • Satır 4. v işaretlenir. İşaret, düğümün listesi taranmadan önce konur. Böylece v'nin komşularından birinin listesinde v görüldüğünde ona geri girilmez.
  • Satır 5. Ziyaret: düğüm numarası yazılır.
  • Satır 6. v'nin listesi baştan sona gezilir. for başlığındaki w koşulu w != NULL anlamına gelir.
  • Satır 7–8. Komşu işaretli değilse arama onun üzerinden sürer. Özyinelemeli çağrı döndüğünde döngü, bu çağrının w'sinin gösterdiği yerden, yani kalınan yerden devam eder.

15.4.2DFS izlemesi

dfs(0) çağrısı örnek çizgede izlenir. Tablo 15.18'de çağrı yığını en alttaki dfs(0)'dan başlayıp en üstteki etkin çağrıya kadar yazılır. “Atlanır” satırlarında komşu zaten işaretlidir.

Tablo 15.18: Örnek çizgede dfs(0) izlemesi.
AdımOlayÇağrı yığınıÇıktı
1dfs(0): 0 işaretlenir, yazılır00
20 → 1 işaretsiz: dfs(1)0 10 1
31'in listesinde w → 0: işaretli, atlanır0 10 1
41 → 3 işaretsiz: dfs(3)0 1 30 1 3
53'ün listesinde w → 1: atlanır0 1 30 1 3
63 → 7 işaretsiz: dfs(7)0 1 3 70 1 3 7
77'nin listesinde w → 3: atlanır0 1 3 70 1 3 7
87 → 4 işaretsiz: dfs(4)0 1 3 7 40 1 3 7 4
9–104'ün listesi: 1 ve 7 işaretli; dfs(4) biter0 1 3 70 1 3 7 4
117 → 5 işaretsiz: dfs(5)0 1 3 7 50 1 3 7 4 5
125 → 2 işaretsiz: dfs(2)0 1 3 7 5 20 1 3 7 4 5 2
13–142'nin listesinde 0 ve 5: atlanır0 1 3 7 5 20 1 3 7 4 5 2
152 → 6 işaretsiz: dfs(6)0 1 3 7 5 2 60 1 3 7 4 5 2 6
16–176'nın listesi: 2 ve 7 işaretli; dfs(6), dfs(2) biter0 1 3 7 5aynı
185'te kalınan yer: w → 7 atlanır; dfs(5) biter0 1 3 7aynı
197'de kalınan yer: w → 6 atlanır; dfs(7), dfs(3) biter0 1aynı
201'de kalınan yer: w → 4 atlanır; dfs(1) biter0aynı
210'da kalınan yer: w → 2 atlanır; dfs(0) biter—0 1 3 7 4 5 2 6

İzlemenin en önemli anları dönüşlerdir. 12. adımda arama 5'ten 2'ye geçer. 0 ile 2 arasında doğrudan bir kenar vardır. Yine de 2'ye 0 → 1 → 3 → 7 → 5 → 2 gibi uzun bir yoldan varılmıştır. DFS yakını değil, derini önce dener. 18. adımda dfs(5), 2'ye inmeden önceki yerinden, listesindeki ikinci öğe 7'den devam eder. Bunu sağlayan, dfs(5) çerçevesinde saklı duran w'dir. Yığının en derin anı 15. adımdır: yedi çağrı aynı anda açıktır.

01234567ugranildi[]0T1T2T3T4T5T6F7Tbaslik[2]056
01234567ugranildi[]0T1T2T3T4T5T6T7T
Şekil 15.20: DFS'nin iki anı. Üstte dfs(2) yeni çağrılmıştır: kırmızı kalın kenarlar şimdiye dek izlenen ağaç kenarlarıdır, sağda ugranildi dizisi ve 2'nin listesi görünür. Altta gezinme bitmiştir; ağaç kenarları önce derinliğine kapsayan ağacı oluşturur.

Gezinme sırasında bir düğüme ilk kez girilen kenarlar bir ağaç oluşturur: 0–1, 1–3, 3–7, 7–4, 7–5, 5–2, 2–6. Yedi kenar sekiz düğümün hepsini döngüsüz bağlar. Bu ağaca önce derinliğine kapsayan ağaç (depth first spanning tree) denir. Ağaca kullanılmayan kenarlardan biri, örneğin 0–2, eklenirse bir döngü oluşur ve yapı ağaç olmaktan çıkar.

15.4.3DFS'nin sonucu neye bağlıdır

DFS'nin ürettiği sıra çizgenin kendisine ek olarak iki şeye bağlıdır.

Başlangıç düğümü. Başlangıcın 0 olması gerekmez. Aynı çizgede ve aynı listelerle dfs(6) 6 2 0 1 3 7 4 5, dfs(4) ise 4 1 0 2 5 7 3 6 yazar. Ziyaret edilen düğümler aynıdır, dizilişleri farklıdır. Bağlantılı bir çizgede her başlangıç bütün düğümlere ulaşır.

Listelerin sırası. 0'ın listesi {1, 2} yerine {2, 1} olsaydı dfs(0) önce 2'ye inerdi ve sıra 0 2 5 7 3 1 4 6 olurdu. Listenin başa mı sona mı eklenerek kurulduğu ya da sıralı tutulup tutulmadığı gezinmeyi doğrudan değiştirir. Bu nedenle bir DFS çıktısı ancak listelerin tam sırası bilindiğinde kesin olarak söylenebilir.

Teorem 15.2 (DFS'nin maliyeti)

Komşuluk listesiyle gerçeklenmiş n düğümlü, e kenarlı bir çizgede dfs'nin toplam çalışma süresi O(n + e)'dir. Komşuluk matrisiyle gerçeklenirse O(n²)'dir.

dfs(v) yalnız ugranildi[v] yanlışken çağrılır ve ilk iş olarak bu değeri doğru yapar. Bu yüzden her düğüm için en çok bir çağrı yapılır; çağrıların toplam sayısı en çok n'dir. Her çağrı kendi düğümünün listesini bir kez tarar; v'nin listesinde dv öğe vardır. Bütün listelerin toplam uzunluğu Teorem 15.1'e göre 2e'dir. Toplam iş O(n + 2e) = O(n + e) olur. Matriste bir düğümün komşularını bulmak bütün satırı, yani n hücreyi okumayı gerektirir; n çağrının her biri O(n) sürer ve toplam O(n²) olur. ∎

Örnek çizgede sayaçlı bir dfs 8 çağrı yaptı ve listelerde tam 20 öğeye baktı; bu değerler n = 8 ve 2e = 20 ile örtüşür. Kenar sayısı en çok n(n − 1)/2 olabilir. Tam çizgede e, n² mertebesine çıkar ve iki gösterimin maliyeti eşitlenir. Seyrek çizgelerde ise liste, gezinmeyi doğrusal zamanda yapar.

Ek bellek de hesaba katılmalıdır. ugranildi dizisi n öğelidir. Çağrı yığınının derinliği en kötü durumda n'dir: düğümleri bir zincir gibi dizilmiş bir çizgede her çağrı bir sonrakini açar. Çok büyük çizgelerde bu derinlik çağrı yığınını taşırabilir; o durumda DFS açık bir yığınla yazılır.

15.4.4Önce genişliğine arama

BFS, suya atılan taşın oluşturduğu halkalar gibi yayılır. v'den başlanır ve v işaretlenir. Sonra v'nin listesindeki düğümlerin hepsi ziyaret edilir. Ardından v'nin listesindeki ilk düğümün ziyaret edilmemiş komşuları, sonra ikinci düğümünkiler ziyaret edilir. Bu böyle sürer. Ziyaret edilen her düğüm bir kuyruğa (queue) konur. Bir düğümün listesi bitince kuyruğun önündeki düğüm alınır ve onun listesine geçilir. Kuyruk boşalınca arama biter.

Yöntemin doğası gereği kuyruk zorunludur, tıpkı DFS'de yığının zorunlu olması gibi. Önce ziyaret edilen düğümün komşuları önce ziyaret edilmelidir; “ilk giren ilk çıkar” kuralı tam olarak budur.

void bfs(int v)   /* once genisligine arama */
{
    oge_gost w;
    kuyruk_gost on = NULL, arka = NULL;
    printf("%d ", v);
    ugranildi[v] = TRUE;
    kuyruk_ekle(&on, &arka, v);
    while (on) {
        v = kuyruk_al(&on, &arka);
        for (w = baslik[v]; w; w = w->bag)
            if (!ugranildi[w->dugum]) {
                printf("%d ", w->dugum);
                kuyruk_ekle(&on, &arka, w->dugum);
                ugranildi[w->dugum] = TRUE;
            }
    }
}
  • Satır 4. Kuyruk bağlı listeyle gerçeklenir; on ve arka başta NULL'dır, yani kuyruk boştur.
  • Satır 5–7. Başlangıç düğümü yazılır, işaretlenir ve kuyruğa konur.
  • Satır 8. Kuyruk boş değilken döner. Boş kuyrukta on NULL'dır.
  • Satır 9. Kuyruğun önündeki düğüm alınır. Parametre v burada yeniden kullanılır; yordamın başındaki değeri artık gerekmez.
  • Satır 10–15. v'nin listesi taranır. İşaretsiz her komşu yazılır, kuyruğun arkasına eklenir ve işaretlenir.

İşaretin zamanı önemlidir: düğüm kuyruğa eklenirken işaretlenir, kuyruktan çıkarken değil. Böylece bir düğüm kuyrukta beklerken başka bir düğümün listesinde yeniden görülse bile ikinci kez kuyruğa girmez. Bu ayrıntının değeri, hatalı biçimin sonucuna bakılınca anlaşılır.

Dinamik kuyruk

BFS'nin kullandığı kuyruk, bağlı listeyle gerçeklenmiş dinamik bir kuyruktur. Önden alınır, arkaya eklenir; iki uç için iki işaretçi tutulur.

typedef struct kuyruk *kuyruk_gost;
typedef struct kuyruk {
    int dugum;
    kuyruk_gost bag;
} kuyruk;
void kuyruk_ekle(kuyruk_gost *on,
                 kuyruk_gost *arka, int d)
{
    kuyruk_gost p = malloc(sizeof(kuyruk));
    if (p == NULL) exit(1);
    p->dugum = d;  p->bag = NULL;
    if (*on) (*arka)->bag = p;  else *on = p;
    *arka = p;
}

kuyruk_ekle, iki işaretçiyi de değiştirebilmek için onların adreslerini alır (kuyruk_gost *on). Bu, C'de bir işaretçiyi fonksiyon içinden değiştirmenin tek yoludur. Yeni öğenin bağı NULL'dır, çünkü kuyruğun sonuna girer. Kuyruk boş değilse eski son öğenin bağı yeni öğeyi gösterir; boşsa yeni öğe aynı zamanda ön öğedir. İki durumda da arka yeni öğeyi gösterir. İşlem O(1)'dir.

int kuyruk_al(kuyruk_gost *on,
              kuyruk_gost *arka)
{
    kuyruk_gost p = *on;   /* on != NULL */
    int d = p->dugum;
    *on = p->bag;
    if (*on == NULL) *arka = NULL;
    free(p);
    return d;
}

kuyruk_al boş olmayan bir kuyruk bekler; açıklama satırı bu ön koşulu söyler ve BFS yordamı onu while (on) ile sağlar. Önündeki öğenin değeri saklanır, ön işaretçi bir sonrakine geçer ve öğe serbest bırakılır. Kuyruk boşalınca arka da NULL yapılmalıdır. Yapılmazsa arka, az önce serbest bırakılmış öğeyi göstermeye devam eder. Sonraki kuyruk_ekle *on'u boş görür ve doğru davranır, ama aradaki sürede arka sarkan bir işaretçidir (dangling pointer). Bu yüzden int kuyruk_al(kuyruk_gost *) gibi yalnız ön işaretçiyi alan bir prototip yetmez; arka'nın da adresi geçirilir. İşlem O(1)'dir.

15.4.5BFS izlemesi

Tablo 15.19 bfs(0) çağrısını örnek çizgede izler. Kuyruk sütununda sol uç önü, sağ uç arkayı gösterir.

Tablo 15.19: Örnek çizgede bfs(0) izlemesi.
AdımvOlayKuyruk (ön … arka)Çıktı
100 yazılır, işaretlenir, kuyruğa girer00
200 alınır; listesi {1, 2}—0
3–401 ve 2 işaretsiz: yazılır, eklenir, işaretlenir1 20 1 2
511 alınır; listesi {0, 3, 4}; 0 işaretli20 1 2
6–713 ve 4 eklenir2 3 40 1 2 3 4
822 alınır; listesi {0, 5, 6}; 0 işaretli3 40 1 2 3 4
9–1025 ve 6 eklenir3 4 5 60 1 2 3 4 5 6
1133 alınır; listesi {1, 7}; 1 işaretli4 5 60 1 2 3 4 5 6
1237 eklenir4 5 6 70 1 2 3 4 5 6 7
1344 alınır; listesi {1, 7}: ikisi de işaretli5 6 7aynı
1455 alınır; listesi {2, 7}: işaretli6 7aynı
1566 alınır; listesi {2, 7}: işaretli7aynı
1677 alınır; listesi {3, 4, 5, 6}: işaretli—aynı
17—kuyruk boş: arama biter—0 1 2 3 4 5 6 7

12. adımda 7 kuyruğa girer ve işaretlenir. 13. adımda 4'ün listesinde 7 yeniden görülür, ama işaretli olduğu için ikinci kez eklenmez. 5 ve 6'nın listelerinde de aynı şey olur. 7'ye dört farklı düğümden varılabilir, ama kuyruğa bir kez girer.

01234567ugranildi[]0T1T2T3T4T5F6F7Fkuyruk234▲ on▲ arkabaslik[1]034w
01234567ugranildi[]0T1T2T3T4T5T6T7Tkuyrukboş (on = arka = NULL)
Şekil 15.21: BFS'nin iki anı. Üstte 1'in listesi taranıyor; 3 ve 4 kuyruğa eklenmiştir. Altta arama bitmiştir: kırmızı kenarlar önce genişliğine kapsayan ağaçtır.

Çıktı 0 1 2 3 4 5 6 7'dir: düğümler başlangıca olan kenar sayısına göre düzey düzey ziyaret edilmiştir. 0 düzey 0'da; 1 ve 2 düzey 1'de; 3, 4, 5, 6 düzey 2'de; 7 düzey 3'tedir. Ağaç kenarları 0–1, 0–2, 1–3, 1–4, 2–5, 2–6 ve 3–7'dir. Bu önce genişliğine kapsayan ağaçta kökten her düğüme giden yol, çizgedeki en az kenarlı yoldur.

Sık yapılan hata

BFS'de düğümü kuyruğa eklerken değil, kuyruktan çıkarırken işaretlemek. Örnek çizgede ugranildi[w->dugum] = TRUE satırı silinsin. Yerine v = kuyruk_al(…)'dan sonra ugranildi[v] = TRUE yazılsın. Bu durumda çıktı 0 1 2 3 4 5 6 7 7 7 7 olur. 7, kuyruktan çıkmadan önce 3, 4, 5 ve 6'nın listelerinde birer kez görülür ve dört kez kuyruğa girer. Kuyruğa 8 yerine 11 ekleme yapılır; listelerde 20 yerine 32 öğe taranır. Yinelenen her kopya kuyruktan çıktığında kendi listesini yeniden tarar. Daha yoğun çizgelerde iş O(n + e) sınırını açıkça aşar.

15.4.6BFS'nin analizi ve DFS ile karşılaştırılması

Teorem 15.3 (BFS'nin maliyeti)

Komşuluk listesiyle gerçeklenmiş bir çizgede bfs O(n + e) sürede çalışır; komşuluk matrisiyle O(n²).

Bir düğüm kuyruğa yalnız işaretsizken eklenir ve eklendiği anda işaretlenir. Bu yüzden her düğüm kuyruğa en çok bir kez girer ve while döngüsü en çok n kez döner. Her turda kuyruktan alınan düğümün listesi bir kez taranır. Bütün turlarda taranan öğelerin toplamı d0 + … + dn−1 = 2e'dir. kuyruk_ekle ve kuyruk_al O(1)'dir. Toplam O(n + e) olur. Matriste her tur bir satırın n hücresini okur; n turda O(n²) eder. ∎

BFS'nin ek belleği ugranildi dizisi ve kuyruktur; kuyrukta aynı anda en çok n düğüm bulunur. BFS'nin verdiği sıra, başlangıca olan kenar sayısının sırasıdır. Ağırlıksız bir çizgede bu, en kısa yolların sırası demektir; BFS bu yüzden en az kenarlı yolu bulan algoritmadır.

Önce derinliğine ve önce genişliğine arama kavramları veri yapılarının dışında da karşılaşılan iki genel arama stratejisidir. Yapay zekâda bir problemin bütün durumları bir arama uzayı oluşturur ve bu uzay bir ağaç gibi düşünülerek gezilir. Min-max araması, B* araması ve dal-sınır (branch and bound) yöntemleri bu gezinmelerin üzerine kurulur. Bir oyun probleminde “olası hamleler şunlardır” denip her hamle ayrı ayrı açılıyorsa arama genişliğine ilerliyor demektir.

Tablo 15.20: DFS ile BFS'nin karşılaştırılması.
ÖzellikDFSBFS
Yardımcı yapıYığın (özyinelemede çağrı yığını)Kuyruk
İlerleyişBir yoldan olabildiğince derineKatman katman, yakından uzağa
Ağaçtaki karşılığıKök önde gezinmeDüzey düzey gezinme
Örnek çizgede (0)0 1 3 7 4 5 2 60 1 2 3 4 5 6 7
Örnek çizgede (7)7 3 1 0 2 5 6 47 3 4 5 6 1 2 0
Maliyet (liste / matris)O(n + e) / O(n²)O(n + e) / O(n²)
Tipik kullanımBileşenler, döngü tespiti, topolojik sıralamaEn az kenarlı yol, iki parçalılık testi

15.5Bileşenler, döngüler ve iki parçalılık

Gezinme yordamları tek başına yalnız düğüm numaralarını yazar. Asıl değerleri, üzerlerine kurulan algoritmalardadır. Bu kesim DFS ve BFS'yi üç soruyu cevaplamak için kullanır: Çizge kaç parçadan oluşur? Çizgede döngü var mı? Çizgenin düğümleri, her kenar iki gruba arasında kalacak biçimde ikiye ayrılabilir mi?

15.5.1Bağlantılı bileşenler

Tanım 15.12

Yönsüz bir çizgenin bağlantılı bileşeni (connected component), çizgenin en büyük bağlantılı alt çizgelerinden biridir. “En büyük” sözü, bileşene çizgeden başka bir düğüm eklenirse alt çizgenin artık bağlantılı olmayacağı anlamına gelir.

Her düğüm tam olarak bir bileşene aittir; bileşenler düğüm kümesini ayrık parçalara böler. G4'ün iki, G5'in dokuz bileşeni vardır. Bağlantılı bir çizgenin tek bileşeni kendisidir.

Bileşenleri bulmak için gezinmenin bir özelliği kullanılır: dfs(v), v'den ulaşılabilen bütün düğümleri, yani v'nin bileşenini tümüyle işaretler. Gezinme bittiğinde işaretsiz kalan bir düğüm varsa o başka bir bileşendedir ve oradan yeni bir gezinme başlatılır.

void bitisik_bilesenler(int n)
{
    int i;
    for (i = 0; i < n; i++)
        if (!ugranildi[i]) {   /* yeni */
            dfs(i);
            printf("\n");
        }
}
  • Satır 4. Bütün düğümler sırayla denenir.
  • Satır 5. İşaretsiz bir düğüm, daha önceki hiçbir gezinmenin ulaşamadığı yeni bir bileşenin ilk düğümüdür.
  • Satır 6. dfs(i) bu bileşenin bütün düğümlerini yazar ve işaretler.
  • Satır 7. Her bileşen ayrı satıra yazılır. Bu satıra bir sayaç artırımı (sayi++) eklenirse yordam bileşen sayısını da verir; sayı 1 ise çizge bağlantılıdır.

Yordam DFS yerine BFS ile de aynı biçimde yazılabilir. Yordamın çalışması için ugranildi dizisinin başta tamamen FALSE olması gerekir.

0123456789
012345678910
Şekil 15.22: İki bileşen örneği. Üstte 10 düğümlü, üç bileşenli çizge; çıktı 0 1 3 2 / 4 5 6 / 7 8 9. Altta 11 düğümlü, dört bileşenli çizge; çıktı 0 / 1 2 4 3 / 5 6 7 8 / 9 10.

Alttaki örnekte ikinci satırın çıktısı 1 2 4 3'tür. 1'in listesi {2, 3} olduğu için DFS önce 2'ye, 2'nin listesindeki 4'e, oradan 3'e gider. 5 bu satırda görünmez; 5, 6, 7, 8 başka bir bileşendir. Yalnız 0'dan oluşan bileşen tek düğümlüdür; dfs(0) listesi boş olduğu için yalnız 0'ı yazar. Tablo 15.21 üstteki örneğin izlemesidir.

Tablo 15.21: bitisik_bilesenler(10) izlemesi (üstteki çizge).
iugranildi[i]OlayYazılan satırBileşen
0Fyeni bileşen: dfs(0)0 1 3 21
1, 2, 3Tif'e girilmez, her biri O(1)—1
4Fyeni bileşen: dfs(4)4 5 62
5, 6Tif'e girilmez—2
7Fyeni bileşen: dfs(7)7 8 93
8, 9Tif'e girilmez—3
10—döngü biter3 bileşen
0123456789ugranildi[]0T1T2T3T4T5T6T7F8F9Fbileşen sayısı: 2
Şekil 15.23: İzlemenin i = 4 anı. İlk bileşenin dört düğümü işaretlenmiştir (T); dfs(4) ikinci bileşeni gezmiştir.

15.5.2Analiz: çarpım değil, toplam

Yordamın maliyeti ilk bakışta yanıltıcıdır. Dıştaki for n kez döner. İçteki dfs çağrısının maliyeti O(n + e)'dir. Bu iki sayı çarpılıp karmaşıklık O(n(n + e)) denebilir mi? Hayır. “Döngüleri bulur, maliyetlerini çarparım” kuralı burada yanlış sonuç verir, çünkü iç işin maliyeti her turda aynı değildir.

Teorem 15.4 (Bileşen bulmanın maliyeti)

bitisik_bilesenler komşuluk listesiyle O(n + e), komşuluk matrisiyle O(n²) sürede çalışır.

Bütün dfs çağrıları aynı ugranildi dizisini paylaşır. Bir düğüm bir kez işaretlenince hiçbir çağrı ona yeniden girmez. Öyleyse for boyunca yapılan bütün dfs çağrıları birlikte her düğümü bir kez ziyaret eder ve her listeyi bir kez tarar: toplam O(n + e). Bu toplamın bir kısmı birinci bileşene, bir kısmı ikinciye düşer; bileşenler ayrık olduğu için hiçbir iş iki kez sayılmaz. Bunlara for'un kendi n turu eklenir; işaretli düğümlerde tur O(1)'dir. Toplam O(n) + O(n + e) = O(n + e)'dir. Matriste her ziyaret bir satır taraması gerektirdiğinden ziyaretlerin toplamı O(n²) olur. ∎

Uç durum bunu açıkça gösterir. Çizge bağlantılıysa ilk turda dfs(0) bütün çizgeyi O(n + e)'de gezer ve ugranildi dizisinin tamamı TRUE olur. i'nin geri kalan n − 1 değerinde if'e hiç girilmez. Çarpım ancak her turda dfs'nin bütün çizgeyi yeniden gezdiği durumda doğru olurdu; işaretler bunu engeller. Aynı türden akıl yürütme, döngülerin iç içe olduğu ama iç döngünün toplam işinin sınırlı olduğu pek çok algoritmada karşılaşılır.

15.5.3Bağlantılılık ve kenar sayısı

Bir çizgenin bağlantılı olabilmesi için kaç kenara ihtiyaç vardır?

Teorem 15.5 (Bağlantılılık için kenar sayısı)

n düğümlü bağlantılı bir çizgede en az n − 1 kenar bulunur. Basit bir çizgede kenar sayısı en çok n(n − 1)/2'dir. Bağlantılı basit bir çizgede bu yüzden

n − 1 ≤ e ≤ n(n − 1)/2

olur.

Alt sınır için kenarsız bir çizgeden başlanıp kenarlar birer birer eklendiği düşünülür. Başta her düğüm kendi başına bir bileşendir; n bileşen vardır. Bir kenar eklendiğinde iki ucu ya aynı bileşendedir ve bileşen sayısı değişmez, ya da farklı bileşenlerdedir ve iki bileşen birleşir. Bir kenar bileşen sayısını en çok 1 azaltır. n bileşenden tek bileşene inmek için en az n − 1 kenar gerekir. Üst sınır için basit çizgede her düğüm çiftinin arasında en çok bir kenar olduğu kullanılır. 0 düğümü öteki n − 1 düğüme bağlanabilir. 1 düğümü kendinden sonraki n − 2 düğüme bağlanabilir. Böyle devam edilirse (n − 1) + (n − 2) + … + 1 = n(n − 1)/2 bulunur. ∎

0123401234K5: 10 kenarn − 1 = 4 kenar, bağlantılı
Şekil 15.24: İki uç durum, 5 düğümle. Solda tam çizge K5: 5 · 4 / 2 = 10 kenar. Sağda 4 kenarlı bağlantılı bir çizge: n − 1 kenar yeterli olabilir.

Teorem tek yönlüdür. n − 1 kenar bağlantılılık için gereklidir ama yeterli değildir. 5 düğümlü bir çizgede 4 kenar bir zincir kurabilir; o zaman çizge bağlantılıdır. Aynı 4 kenar dört düğüm arasında bir döngü kurabilir; o zaman beşinci düğüm yalnız kalır ve çizge bağlantılı değildir. Kenar sayısı n − 1'den azsa ise çizge kesinlikle bağlantılı değildir; G5'te e = 2 < 10'dur.

Üst sınır, karmaşıklık ifadeleri açısından önemlidir. e, n² mertebesinde bir sayı olabilir. O(n + e) ifadesi bu yüzden tam çizgede O(n²)'ye eşittir; liste ancak seyrek çizgelerde matristen asimptotik olarak üstün olur. Bağlantılı ve tam n − 1 kenarlı bir çizge döngü içermez; böyle bir çizge bir ağaçtır. Gezinmelerin ürettiği kapsayan ağaçlarda da n − 1 kenar bulunur.

Oyun problemleri bu sınırların pratikteki önemini gösterir. Tic tac toe gibi bir oyunda her durum bir düğüm, her hamle bir kenardır. “Rakibin hamlesini nasıl bozarım?” sorusu bu çizgede bir arama yapmaktır. Durum sayısı hızla büyür ve aramanın maliyeti doğrudan düğüm ve kenar sayısına bağlıdır.

15.5.4Yönsüz çizgede döngü tespiti

DFS'nin bir yan ürünü, çizgede döngü olup olmadığıdır. DFS bir düğümün listesini tararken işaretli bir komşuya rastlayabilir. Yönsüz çizgede bunun iki nedeni olabilir. Birincisi, komşu düğüme hemen önceki adımda ondan gelinmiştir; bu, gelinen kenarın öteki ucudur ve döngü belirtisi değildir. İkincisi, komşu daha önce başka bir yoldan işaretlenmiştir. Bu durumda düğümden o komşuya hem ağaç kenarlarıyla hem de bu kenarla gidilebilir: bir döngü vardır. Böyle kenarlara geri kenar (back edge) denir.

bool dongu_var(int v, int ata)
{
    oge_gost w;
    ugranildi[v] = TRUE;
    for (w = baslik[v]; w; w = w->bag)
        if (!ugranildi[w->dugum]) {
            if (dongu_var(w->dugum, v)) return TRUE;
        } else if (w->dugum != ata)
            return TRUE;          /* geri kenar */
    return FALSE;
}
  • Satır 1. Yordam düğümün yanında ata'yı, yani bu düğüme hangi düğümden gelindiğini alır. İlk çağrıda ebeveyn yoktur; -1 hiçbir düğümün numarası olmadığı için bu anlamda kullanılır.
  • Satır 4. Düğüm işaretlenir.
  • Satır 6–7. İşaretsiz komşuya inilir; ebeveyni şimdiki düğümdür. Alt çağrı döngü bulursa TRUE, çağrı zinciri boyunca hemen yukarı taşınır.
  • Satır 8–9. Komşu işaretliyse ve ebeveyn değilse geri kenar bulunmuştur. else if, iç if'in değil dış if'in else'idir; süslü parantezler bunu açıkça belirler. Parantezler kaldırılırsa else en yakın if'e bağlanır ve yordamın anlamı değişir.
  • Satır 10. Bütün komşular geçildi ve döngü bulunmadı.

Kenarları 0–1, 1–2, 2–3 ve 3–1 olan dört düğümlü çizgede sıralı listeler 0: {1}, 1: {0, 2, 3}, 2: {1, 3}, 3: {1, 2}'dir. Tablo 15.22 dongu_var(0, -1) çağrısını izler.

Tablo 15.22: dongu_var(0, -1) izlemesi.
ÇağrıwDurumSonuç
dongu_var(0, -1)1işaretsiz: in—
dongu_var(1, 0)0işaretli ama ebeveyndöngü sayılmaz
dongu_var(1, 0)2işaretsiz: in—
dongu_var(2, 1)1işaretli ama ebeveyndöngü sayılmaz
dongu_var(2, 1)3işaretsiz: in—
dongu_var(3, 2)1işaretli ve ebeveyn (2) değilgeri kenar (3,1): TRUE
0123ugranildi[]0T1T2T3T
Şekil 15.25: Döngü tespitinin son anı. Kırmızı kenarlar DFS ağacıdır; kesikli kenar (3,1) geri kenardır ve 1–2–3–1 döngüsünü kapatır.

TRUE değeri 3'ten 2'ye, 2'den 1'e, 1'den 0'a taşınır ve yordam erken biter. Döngü yoksa yordam DFS'nin kendisi kadar, O(n + e) sürer. Çizgede birden çok bileşen varsa yordam, bileşen bulmadaki gibi her işaretsiz düğümden yeniden başlatılır. Ağaç olduğu bilinen G2 üzerinde yordam FALSE döndürür.

15.5.5Yönlü çizgede döngü

Yönlü çizgede ebeveyn denetimi yetmez. Daha önce işaretlenmiş bir düğüme giden bir kenar döngü kapatmayabilir: o düğüm başka bir dalda çoktan bitirilmiş olabilir. Örneğin 0 → 1, 0 → 2, 1 → 3, 2 → 3 kenarlarında DFS 0, 1, 3 sırasıyla iner, sonra 2'den 3'e giden kenarı görür. 3 işaretlidir, ama döngü yoktur. Ayırt edici soru şudur: işaretli düğüm şu anda yığında mı? Yığındaki bir düğüme giden kenar, o düğümden şimdiki düğüme inen yolu kapatır ve bir döngüdür.

Bu ayrım üç renkle yapılır: beyaz henüz girilmemiş, gri girilmiş ama bitmemiş (yığında), siyah bitmiş düğümdür.

enum { BEYAZ, GRI, SIYAH };
int durum[MAX_DUGUM];   /* hepsi basta BEYAZ */

bool yonlu_dongu(int v)
{
    oge_gost w;
    durum[v] = GRI;                  /* yigina girdi */
    for (w = baslik[v]; w; w = w->bag) {
        if (durum[w->dugum] == GRI)
            return TRUE;             /* yigindaki dugume kenar */
        if (durum[w->dugum] == BEYAZ && yonlu_dongu(w->dugum))
            return TRUE;
    }
    durum[v] = SIYAH;                /* bitti */
    return FALSE;
}

Düğüme girilince gri yapılır. Gri bir komşu görülürse döngü vardır. Beyaz komşuya inilir. Bütün komşular bitince düğüm siyah olur ve artık hiçbir döngünün parçası olarak görülmez. Yordam G3'te (0 → 1 → 0) ve uygulama çizgesinde (0 → 1 → 2 → 0) TRUE, yukarıdaki dört düğümlü örnekte FALSE döndürdü. Maliyet yine O(n + e)'dir; matrisle aynı DFS O(n²) olur. Topolojik sıralama yordamı da döngüyü kendiliğinden yakalar.

Döngü tespitinin pratik önemi büyüktür. İşler arasında öncelik ilişkisi tanımlanan bir projede yönlü döngü, “A, B'den önce; B, A'dan önce bitmeli” gibi bir çelişki demektir ve proje hiç bitmez. Minimum kapsayan ağaç kurulurken döngü kapatan bir kenar ise gereksiz bir kenardır ve ağaca alınmaz.

15.5.6İki parçalı çizge testi

Tanım 15.13

Düğüm kümesi V, ayrık iki A ve B kümesine ayrılabiliyorsa ve her kenarın bir ucu A'da, öteki ucu B'de bulunuyorsa çizge iki parçalıdır (bipartite).

Öğrenciler ve dersler iki parçalı bir çizge oluşturur: kenarlar yalnız bir öğrenciyi bir derse bağlar. K3,3, üçer düğümlü iki grubun her düğümünün öteki gruptaki her düğüme bağlı olduğu iki parçalı çizgedir.

Test bir boyama problemidir. Başlangıç düğümü 0 rengine boyanır. BFS her işaretsiz komşuyu, kendisinin ters rengine boyar. Bir kenarın iki ucu aynı renge boyanmış çıkarsa çizge iki parçalı değildir. Hiç çelişki çıkmazsa renkler iki grubu verir.

int renk[MAX_DUGUM];   /* -1: boyanmadi, 0 ya da 1 */

bool iki_parcali_mi(int n)
{
    int i, v;
    oge_gost w;
    kuyruk_gost on = NULL, arka = NULL;
    for (i = 0; i < n; i++) renk[i] = -1;
    for (i = 0; i < n; i++) {
        if (renk[i] != -1) continue;      /* yeni bilesen */
        renk[i] = 0;
        kuyruk_ekle(&on, &arka, i);
        while (on) {
            v = kuyruk_al(&on, &arka);
            for (w = baslik[v]; w; w = w->bag)
                if (renk[w->dugum] == -1) {
                    renk[w->dugum] = 1 - renk[v];
                    kuyruk_ekle(&on, &arka, w->dugum);
                } else if (renk[w->dugum] == renk[v]) {
                    while (on) kuyruk_al(&on, &arka);   /* kuyrugu bosalt */
                    return FALSE;
                }
        }
    }
    return TRUE;
}
  • Satır 1. renk dizisi hem işaret hem renk tutar: -1 henüz boyanmamış düğümdür. Ayrı bir ugranildi dizisi gerekmez.
  • Satır 8–12. Bütün renkler silinir; her bileşen için BFS ayrı başlatılır; bir bileşenin rengi ötekini etkilemez.
  • Satır 16–18. Boyanmamış komşu ters renge boyanır ve kuyruğa girer.
  • Satır 19–21. Komşu boyanmış ve rengi aynıysa test başarısızdır. Yordamdan çıkmadan önce kuyruk boşaltılır; yoksa kuyrukta kalan öğeler hiç serbest bırakılmaz.
0123456-döngü012üçgen
0123456-döngü: iki parçalı ✓012üçgen
Şekil 15.26: İki renklendirme. Üstte 6 düğümlü döngü BFS ile boyanmıştır: 0, 2, 4 bir renkte, 1, 3, 5 öteki renktedir; çizge iki parçalıdır. Altta üçgen: 0 ve 1 farklı renge boyandıktan sonra 2, ikisine birden komşudur ve iki renk yetmez.

6 düğümlü döngüde BFS düğümleri 0, 1, 5, 2, 4, 3 sırasıyla boyar; renkler 0, 1, 1, 0, 0, 1'dir. Hiçbir kenarın iki ucu aynı renkte değildir. Üçgende ise 0'a renk 0, onun komşuları 1 ve 2'ye renk 1 verilir. 1'in listesi tarandığında 2 görülür; ikisi de 1 rengindedir ve test başarısız olur.

Teorem 15.6 (İki parçalılık)

Bir çizge, ancak ve ancak tek uzunlukta bir döngü içermiyorsa iki parçalıdır.

Bir yönü kolaydır. Çizge iki parçalıysa her kenar renk değiştirir. Bir döngü başladığı düğüme, yani başladığı renge döner; bu yüzden döngüdeki renk değişimi sayısı, yani kenar sayısı çifttir. Tek uzunlukta döngü olamaz. Öteki yön, BFS boyamasının çelişki üretmesinden çıkar. Aynı renkli iki komşu u ve v bulunursa, BFS ağacında ikisinin ortak bir atası vardır. Aynı renkte oldukları için başlangıca olan uzaklıklarının farkı çifttir; BFS'de komşu düğümlerin düzeyleri en çok 1 farklı olabildiği için düzeyleri eşittir. Ortak atadan u'ya ve v'ye inen iki yol eşit uzunluktadır; bunlara (u, v) kenarı eklenince tek uzunlukta bir döngü oluşur. Öyleyse tek döngü yoksa boyama çelişkisiz biter. ∎

Testin maliyeti BFS'nin maliyetidir: O(n + e). Yordam 6 düğümlü döngü ve K3,3 için TRUE, üçgen için FALSE döndürdü; örnek gezinme çizgesi de iki parçalıdır, çünkü bütün döngüleri 4 ya da 6 kenarlıdır.

15.6Karmaşıklık özeti

Tablo 15.23 bu notta incelenen işlemlerin maliyetlerini iki temel gösterim için bir arada verir. n düğüm sayısı, e kenar sayısı, d ilgili düğümün derecesidir. Bağlantılı basit bir çizgede n − 1 ≤ e ≤ n(n − 1)/2'dir.

Tablo 15.23: Temel çizge işlemlerinin komşuluk matrisi ve komşuluk listesiyle maliyetleri.
İşlemMatrisListeGerekçe
BellekO(n²)O(n + e)Yönsüz listede 2e öğe
(u, v) var mı?O(1)O(du)Tek hücre / liste taraması
Derece (yönsüz)O(n)O(d)Satır / liste uzunluğu; yönlüde giriş için ters liste
Kenar sayısıO(n²)O(n + e)Derece toplamı / 2
DFS, BFSO(n²)O(n + e)Her düğüm bir kez, her liste bir kez
Bağlantılı bileşenlerO(n²)O(n + e)Gezinmelerin toplamı; çarpım değil
Döngü, iki parçalılıkO(n²)O(n + e)DFS ya da BFS üzerine sabit iş
Transpoz listeyi kurma—O(n + e)Listeler bir kez gezilir

Tablonun özü tek bir cümledir: komşuları bulmak matriste O(n), listede O(d)'dir. Bütün gezinme tabanlı algoritmalar bu farkı n düğüm üzerinden toplar; matriste n · n, listede Σd = 2e çıkar.

15.7Sık yapılan hatalar

  1. Yönlü çizgeyi yönsüz sanmak. <u, v> varken <v, u>'yu da var saymak ya da yönlü çizgenin matrisini simetrik sanmak. G3'te a[1][2] = 1, a[2][1] = 0'dır.
  2. BFS'de işareti geç koymak. Düğümü kuyruğa eklerken değil, çıkarırken işaretlemek. Aynı düğüm kuyruğa birden çok kez girer; örnek çizgede 7 dört kez yazılır.
  3. İşaret dizisini sıfırlamamak. ugranildi küreseldir. İkinci bir gezinmeden önce FALSE yapılmazsa ikinci gezinme hiçbir düğüme giremez ve yalnız başlangıcı yazar.
  4. Bileşen analizinde maliyetleri çarpmak. Dış for ile DFS'yi çarpıp O(n(n + e)) demek. İşaretler sayesinde bütün DFS çağrılarının toplamı O(n + e)'dir.
  5. Belleği özensiz yönetmek. malloc sonucunu denetlememek; sizeof(oge_gost) ile işaretçi boyutunda yer almak; program sonunda listeleri free etmemek; free ettikten sonra öğenin bağını okumak.
  6. Gereksiz yere sona eklemek. Kenarı listenin sonuna ekleyip her seferinde listeyi baştan yürümek. Sıra önemli değilse O(1)'lik başa ekleme yeter; çizim de bu durumda kodun ürettiği ters sırayı göstermelidir.
  7. Matris parametresinde boyutu unutmak. int a[][] yazmak derlenmez; ikinci boyut verilmelidir: int a[][MAX_DUGUM].
  8. Sonsuz için INT_MAX seçmek. Maliyet toplamlarında işaretli taşma olur; güvenli büyük bir sabit kullanılır.

15.8Alıştırmalar ve çözümleri

Bu kesimdeki alıştırmalar sunumdaki sırayla verilir; her birinin ardından çözüm gelir.

15.8.1Derece dizisi

Soru. Derece dizisi (3, 3, 2, 2, 1) olan basit, yönsüz bir çizge var mıdır? Kaç kenarı vardır?

Çözüm. Derecelerin toplamı 11'dir. Teorem 15.1'e göre toplam 2e, yani çift olmalıdır. Böyle bir çizge yoktur. Aynı sonuç, tek dereceli düğümlerin sayısından da çıkar: 3, 3 ve 1 olmak üzere üç tek dereceli düğüm vardır, oysa bu sayı çift olmalıdır.

15.8.2Sıralı gösterimde komşular

Soru. G4'ün dizi ile sıralı gösteriminde 5 numaralı düğümün komşularını hangi indisler tutar?

Çözüm. dugum[5] = 18 ve dugum[6] = 20'dir. Komşular 18 ve 19 indisli hücrelerdedir: dugum[18] = 4, dugum[19] = 6. 5'in komşuları 4 ve 6, derecesi 20 − 18 = 2'dir.

15.8.37'den başlayan gezinmeler

Soru. Sekiz düğümlü örnek çizgede DFS ve BFS 7'den başlatılırsa (listeler sıralı) çıktılar ne olur?

Çözüm. DFS: 7'nin listesi {3, 4, 5, 6}'dır; ilk komşu 3'e inilir. 3'ün işaretsiz ilk komşusu 1, 1'inki 0, 0'ınki 2, 2'ninki 5'tir. 5'in komşuları 2 ve 7 işaretlidir; 2'ye dönülür ve 6'ya inilir. Geri dönüşler 1'e kadar sürer ve 1'in listesindeki 4 bulunur. Çıktı 7 3 1 0 2 5 6 4'tür. BFS: 7'nin komşuları 3, 4, 5, 6 sırayla yazılır ve kuyruğa girer. 3 alınınca 1, 4 alınınca hiçbir şey, 5 alınınca 2 eklenir. 6'nın komşuları işaretlidir. 1 alınınca 0 eklenir. Çıktı 7 3 4 5 6 1 2 0'dır.

15.8.4İşaret satırını taşımak

Soru. BFS kodunda ugranildi[w->dugum] = TRUE satırı kuyruk_al'dan sonraya taşınırsa ne olur?

Çözüm. Bir düğüm kuyrukta beklerken işaretsiz kalır. Başka bir düğümün listesinde görülünce yeniden yazılır ve yeniden kuyruğa eklenir. Örnek çizgede 7, kuyruktan çıkmadan önce 3, 4, 5 ve 6 üzerinden dört kez eklenir; bfs(0) 0 1 2 3 4 5 6 7 7 7 7 yazar. 7'den başlanırsa 1, 2 ve 0 da yinelenir: 7 3 4 5 6 1 1 2 2 0 0 0 0. Çıktı yanlıştır ve iş O(n + e)'yi aşar.

15.8.5Matrisle bileşen sayısı

Soru. Komşuluk matrisiyle verilen bir çizgenin bileşen sayısını bulan yordamı yazınız; karmaşıklığı nedir?

Çözüm. DFS'nin matris biçimi, bir düğümün komşularını satırını tarayarak bulur:

void mdfs(int a[][MAX_DUGUM], int n, int v)
{
    int j;
    ugranildi[v] = TRUE;
    printf("%d ", v);
    for (j = 0; j < n; j++)            /* satirin tamami: O(n) */
        if (a[v][j] && !ugranildi[j])
            mdfs(a, n, j);
}

int matris_bilesen_sayisi(int a[][MAX_DUGUM], int n)
{
    int i, sayi = 0;
    for (i = 0; i < n; i++) ugranildi[i] = FALSE;
    for (i = 0; i < n; i++)
        if (!ugranildi[i]) {
            sayi++;
            mdfs(a, n, i);
            printf("\n");
        }
    return sayi;
}

mdfs, v'nin satırındaki her 1 için işaretsiz komşuya iner. matris_bilesen_sayisi işaretleri sıfırlar ve her işaretsiz düğümden yeni bir gezinme başlatıp sayacı artırır. G4'ün matrisine uygulandığında 0 1 3 2 ve 4 5 6 7 satırlarını yazar ve 2 döndürür. Her düğüm bir kez ziyaret edilir ve her ziyarette n hücrelik bir satır taranır: O(n²).

15.8.6Giriş derecesini O(1)'de vermek

Soru. Yönlü bir çizgede giriş derecesini O(1)'de verebilmek için hangi yapı tutulur ve nasıl güncellenir?

Çözüm. Her düğüm için bir giris[] sayacı tutulur. <u, v> kenarı eklenirken giris[v]++, silinirken giris[v]-- yapılır. Sorgu tek bir dizi okumasıdır. Gelen kenarların kendileri de gerekiyorsa ters liste ya da ortogonal liste tutulur; ters listenin uzunluğu giriş derecesidir, ama onu saymak O(d)'dir.

15.9Özet

  • Çizge G = (V, E), boş olmayan bir düğüm kümesi ile boş olabilen bir kenar kümesidir. Yönsüzde (u, v) = (v, u); yönlüde <u, v> ≠ <v, u>.
  • Komşu, alt çizge, yol, döngü, bağlantılılık ve derece temel kavramlardır. Derece her zaman belirli bir düğüm için söylenir; yönlüde giriş ve çıkış derecesinin toplamıdır.
  • Derecelerin toplamı 2e'dir. Bağlantılı bir çizgede n − 1 ≤ e ≤ n(n − 1)/2; alt sınır gerekli ama yeterli değildir.
  • Komşuluk matrisi O(n²) bellek ister; kenar sorgusu O(1), komşu taraması O(n)'dir. Komşuluk listesi O(n + e) bellek ister; komşu taraması O(d)'dir.
  • Başa ekleme her kenarı O(1)'de yerleştirir ve listeyi gelen sıranın tersine çevirir. Yönlü çizgede giriş derecesi için ters liste O(n + e)'de kurulur.
  • Sıralı dizi işaretçisiz, n + 2e + 1 tamsayılık bir gösterimdir ama değişikliğe uygun değildir. Ortogonal liste ve çok bağlı liste her kenarı tek öğede tutar.
  • Ağırlıklı çizgede matrise ağırlık, listeye agirlik alanı eklenir; sonsuz için taşmayan büyük bir sabit seçilir.
  • DFS özyineleme ya da yığınla derine iner; BFS kuyrukla katman katman ilerler. İkisi de listeyle O(n + e), matrisle O(n²)'dir. Sonuç sırası başlangıca ve liste sırasına bağlıdır.
  • Bağlantılı bileşenler, her işaretsiz düğümden bir gezinme başlatılarak O(n + e)'de bulunur; dış döngü ile gezinmenin maliyetleri çarpılmaz.
  • Yönsüz çizgede ebeveyn olmayan işaretli komşu, yönlü çizgede gri düğüme giden kenar döngü demektir. BFS ile iki renge boyama iki parçalılığı sınar; iki parçalı çizgede tek uzunlukta döngü yoktur.
  • Yöntem: önce problem ve işlemler, sonra verinin nasıl depolanacağı, en son kod. İlk akla gelen gösterime güvenilmez.

15.10Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.

15.11Kendini Dene

  1. 9 düğümlü, bağlantılı ve basit bir çizgede en az ve en çok kaç kenar bulunabilir?

    Cevap

    En az n − 1 = 8, en çok 9 · 8 / 2 = 36. 8 kenarlı bağlantılı çizge bir ağaçtır; 36 kenarlı çizge tam çizge K9'dur.

  2. Yönsüz bir çizgenin komşuluk listelerinde toplam 30 öğe vardır. Çizgenin kaç kenarı vardır? Aynı çizge komşuluk matrisiyle tutulsaydı matriste kaç tane 1 bulunurdu?

    Cevap

    Her kenar iki listede göründüğü için e = 30 / 2 = 15. Matriste de her kenar simetrik iki hücreye yazılır: 30 tane 1.

  3. G2'nin listeleri sıralı tutuluyor. dfs(0) ve bfs(0) ne yazar?

    Cevap

    DFS: 0 1 3 4 2 5 6; ağaçta kök önde gezinmenin aynısıdır. BFS: 0 1 2 3 4 5 6; düzey düzey gezinmenin aynısıdır.

  4. Sekiz düğümlü örnek çizgede bfs(6) ne yazar?

    Cevap

    6 2 7 0 5 3 4 1. 6'nın komşuları 2 ve 7'dir. 2 alınınca 0 ve 5, 7 alınınca 3 ve 4 eklenir. 0 alınınca 1 eklenir; kalanların komşuları işaretlidir.

  5. Sekiz düğümlü örnek çizge (e = 10) dizi ile sıralı gösterimle tutuluyor. Dizi kaç hücrelidir ve 7'nin komşuları hangi indislerdedir?

    Cevap

    n + 2e + 1 = 8 + 20 + 1 = 29 hücre. Başlangıç indisleri 9, 11, 14, 17, 19, 21, 23, 25, 29'dur. dugum[7] = 25, dugum[8] = 29: 7'nin komşuları 25–28 indislerinde, 3, 4, 5 ve 6'dır.

  6. G1'in kenarları (0,1), (0,2), (0,3), (1,2), (1,3), (2,3) sırasıyla ml_ekle ile çok bağlı listeye ekleniyor. komsulari_yaz(0) ne yazar?

    Cevap

    3 2 1. Her kenar listelerin başına girer; 0'ın listesinde en son eklenen (0,3) öndedir.

  7. G5'e (11 düğüm; kenarlar (1,4) ve (7,8)) bitisik_bilesenler(11) uygulanırsa kaç satır yazılır? if'e kaç kez girilir?

    Cevap

    9 satır: 0, 1 4, 2, 3, 5, 6, 7 8, 9, 10. if'e 9 kez girilir; i = 4 ve i = 8'de düğümler önceki gezinmelerde işaretlenmiştir.

  8. dongu_var'da else if (w->dugum != ata) yerine yalnız else yazılırsa ağaç olan G2 için yordam ne döndürür? Neden?

    Cevap

    TRUE döndürür, oysa G2'de döngü yoktur. dongu_var(1, 0), 1'in listesindeki ilk öğe olan 0'ı işaretli görür ve onu geri kenar sayar. Yönsüz çizgede gelinen kenar, öteki ucun listesinde de bulunur; ebeveyn denetimi bu kenarı ayırmak içindir.

15.12İleri okuma

  • E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, 2. baskı, Bölüm 6. Bu notun gösterimleri, dfs, bfs ve bağlantılı bileşen yordamları oradaki biçimiyle izlenmiştir; çok bağlı liste ve sıralı gösterimin ayrıntıları da oradadır.
  • T. H. Cormen ve ark., Introduction to Algorithms, 4. baskı, Bölüm 20. DFS'nin beyaz/gri/siyah renklendirmesi, kenar sınıflandırması ve BFS'nin en kısa yol özelliğinin kanıtları verilir.
  • R. Sedgewick, Algorithms in C, Part 5: Graph Algorithms, 3. baskı. Çizge gösterimleri ve gezinmeler C koduyla, büyük çizgeler üzerindeki deneylerle birlikte ele alınır.

15.13Kaynaklar

  1. E. Horowitz, S. Sahni, S. Anderson-Freed, Fundamentals of Data Structures in C, 2. bs., Silicon Press, 2008. Bölüm 6, Graphs.
  2. T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 4. bs., MIT Press, 2022. Bölüm 20, Elementary Graph Algorithms.
  3. R. Sedgewick, Algorithms in C, Part 5: Graph Algorithms, 3. bs., Addison-Wesley, 2002.

İlgili sayfalar: Bölüm 15 sunumu · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.