Bir yazılım projesini derlemek istiyorsun ama modüller birbirine bağımlı: A modülü B'ye, B de C'ye ihtiyaç duyuyor. Hangi sırayla derleyeceksin? Ya da bir üniversitede ders seçimi yapıyorsun; bazı dersler ön koşul gerektiriyor. Hangi sırayla alacaksın?
Bu soruların ortak yapısı şudur: Bazı işler diğerlerinden önce yapılmak zorunda ve bu kısıtları bozmayan bir sıralama arıyorsun. Bu sıralamaya topolojik sıralama denir. Bağımlılıkları yönlü kenarlar olarak modellersen, problem bir graf problemine dönüşür.
Kritik bir ön koşul vardır: Graf döngü içermemelidir. A, B'ye bağımlıysa ve B de A'ya bağımlıysa hiçbir geçerli sıralama yoktur. Bu yüzden topolojik sıralama yalnızca yönlü döngüsüz graflar (DAG — Directed Acyclic Graph) için tanımlıdır. Bu derste iki farklı topolojik sıralama algoritmasını, yönlü graflarda döngü tespitini ve gerçek uygulamalarını öğreneceksin.
Yönlü döngüsüz graflar
Ders bağımlılıkları (ok: ön koşul → ders)
Matematik ──► Algoritmalar ──► Yapay Zekâ
│ │
▼ ▼
İstatistik ──► Makine Öğrenmesi
Geçerli bir sıra:
Matematik, Algoritmalar, İstatistik, Makine Öğrenmesi, Yapay Zekâ
Matematik, İstatistik, Algoritmalar, Yapay Zekâ, Makine Öğrenmesi
...
Birden çok geçerli sıra olabilir! Topolojik sıralama TEK değildir.DAG'ların iki temel özelliği vardır. Birincisi, en az bir kaynak düğümü vardır: hiç gelen kenarı olmayan bir düğüm. (Olmasaydı, her düğümün bir öncesi olurdu ve geriye doğru sonsuza kadar gidebilirdik — bu ancak döngü varsa mümkündür.) İkincisi, en az bir bitiş düğümü vardır: hiç giden kenarı olmayan.
Bu birinci özellik, birinci algoritmanın temelini oluşturur.
Aşağıdaki görselleştirmede bir bağımlılık grafını topolojik olarak sıralayabilir, hangi düğümün ne zaman hazır hâle geldiğini izleyebilirsin:
Kahn algoritması
Fikir çok sezgiseldir: Bağımlılığı kalmamış bir işi al, yap, sil. Tekrarla.
Her düğümün GİRİŞ DERECESİ (kaç ön koşulu var) hesaplanır.
A(0) ──► B(1) ──► D(2)
│ │ ▲
▼ ▼ │
C(1) ────────────────┘
adım 1: derecesi 0 olanlar: [A] → A'yı al, komşularının derecesini azalt
B(0), C(0)
adım 2: [B, C] → B'yi al → D(1)
adım 3: [C] → C'yi al → D(0)
adım 4: [D] → D'yi al
sıra: A, B, C, Dpackage main
import (
"fmt"
"maps"
"slices"
)
type DAG struct {
adj map[string][]string
nodes map[string]bool
}
func NewDAG() *DAG {
return &DAG{adj: map[string][]string{}, nodes: map[string]bool{}}
}
func (g *DAG) AddNode(n string) { g.nodes[n] = true }
// AddEdge: from → to (from, to'nun ön koşuludur)
func (g *DAG) AddEdge(from, to string) {
g.AddNode(from)
g.AddNode(to)
g.adj[from] = append(g.adj[from], to)
slices.Sort(g.adj[from])
}
func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
// inDegrees: her düğümün kaç gelen kenarı var
func (g *DAG) inDegrees() map[string]int {
deg := map[string]int{}
for _, n := range g.Nodes() {
deg[n] = 0
}
for _, targets := range g.adj {
for _, t := range targets {
deg[t]++
}
}
return deg
}
// kahn: giriş derecesi tabanlı topolojik sıralama — O(V + E)
func (g *DAG) kahn() ([]string, bool) {
deg := g.inDegrees()
// Derecesi sıfır olanlarla başla (alfabetik: deterministik çıktı)
var ready []string
for _, n := range g.Nodes() {
if deg[n] == 0 {
ready = append(ready, n)
}
}
slices.Sort(ready)
var order []string
for len(ready) > 0 {
cur := ready[0]
ready = ready[1:]
order = append(order, cur)
for _, next := range g.adj[cur] {
deg[next]--
if deg[next] == 0 {
ready = append(ready, next)
slices.Sort(ready) // deterministik sıra
}
}
}
// Tüm düğümler sıralandıysa döngü yok
return order, len(order) == len(g.nodes)
}
// levels: aynı anda yapılabilecek işleri gruplar
func (g *DAG) levels() ([][]string, bool) {
deg := g.inDegrees()
var current []string
for _, n := range g.Nodes() {
if deg[n] == 0 {
current = append(current, n)
}
}
var result [][]string
processed := 0
for len(current) > 0 {
slices.Sort(current)
result = append(result, current)
processed += len(current)
var next []string
for _, node := range current {
for _, t := range g.adj[node] {
deg[t]--
if deg[t] == 0 {
next = append(next, t)
}
}
}
current = next
}
return result, processed == len(g.nodes)
}
func main() {
g := NewDAG()
deps := [][2]string{
{"Matematik", "Algoritmalar"},
{"Matematik", "İstatistik"},
{"Algoritmalar", "Yapay Zekâ"},
{"Algoritmalar", "Makine Öğrenmesi"},
{"İstatistik", "Makine Öğrenmesi"},
}
for _, d := range deps {
g.AddEdge(d[0], d[1])
}
fmt.Println("bağımlılıklar (ön koşul → ders):")
for _, d := range deps {
fmt.Printf(" %s → %s\n", d[0], d[1])
}
fmt.Println()
fmt.Println("giriş dereceleri:")
deg := g.inDegrees()
for _, n := range g.Nodes() {
fmt.Printf(" %-20s %d\n", n, deg[n])
}
order, ok := g.kahn()
fmt.Println()
fmt.Println("topolojik sıra (döngü yok:", ok, "):")
for i, n := range order {
fmt.Printf(" %d. %s\n", i+1, n)
}
fmt.Println()
fmt.Println("paralel yapılabilecek gruplar (dönem planı):")
lvls, _ := g.levels()
for i, level := range lvls {
fmt.Printf(" dönem %d: %v\n", i+1, level)
}
fmt.Println("en az dönem sayısı:", len(lvls))
}bağımlılıklar (ön koşul → ders): Matematik → Algoritmalar Matematik → İstatistik Algoritmalar → Yapay Zekâ Algoritmalar → Makine Öğrenmesi İstatistik → Makine Öğrenmesi giriş dereceleri: Algoritmalar 1 Makine Öğrenmesi 2 Matematik 0 Yapay Zekâ 1 İstatistik 1 topolojik sıra (döngü yok: true ): 1. Matematik 2. Algoritmalar 3. Yapay Zekâ 4. İstatistik 5. Makine Öğrenmesi paralel yapılabilecek gruplar (dönem planı): dönem 1: [Matematik] dönem 2: [Algoritmalar İstatistik] dönem 3: [Makine Öğrenmesi Yapay Zekâ] en az dönem sayısı: 3
Kahn algoritmasının iki güzel yan ürünü vardır. Birincisi, döngü tespiti bedavadır: Algoritma bittiğinde sıralanan düğüm sayısı toplam düğüm sayısından azsa, kalan düğümler bir döngü içindedir. İkincisi, levels fonksiyonunda gördüğün gibi, aynı anda yapılabilecek işleri gruplayabilirsin — bu, paralel derleme ve proje planlamanın temelidir.
DFS tabanlı topolojik sıralama
İkinci yaklaşım, DFS'in çıkış sırasını kullanır: Bir düğümün tüm ardıllarını işledikten sonra onu listenin başına ekle.
DFS'in çıkış sırası, topolojik sıranın TERSİDİR.
Neden? Bir düğüm, tüm ardılları işlendikten sonra çıkar.
Yani çıkış sırasında sonra çıkan, sıralamada önce gelir.
A → B → D
A → C → D
DFS(A): B'ye in → D'ye in → D çık → B çık → C'ye in → C çık → A çık
çıkış sırası: D, B, C, A
ters çevir: A, C, B, D ← topolojik sırapackage main
import (
"fmt"
"maps"
"slices"
)
type DAG struct {
adj map[string][]string
nodes map[string]bool
}
func NewDAG() *DAG {
return &DAG{adj: map[string][]string{}, nodes: map[string]bool{}}
}
func (g *DAG) AddNode(n string) { g.nodes[n] = true }
func (g *DAG) AddEdge(from, to string) {
g.AddNode(from)
g.AddNode(to)
g.adj[from] = append(g.adj[from], to)
slices.Sort(g.adj[from])
}
func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
// topoDFS: DFS çıkış sırasının tersi — O(V + E)
func (g *DAG) topoDFS() ([]string, bool) {
const (
white = 0 // hiç ziyaret edilmedi
gray = 1 // ziyaret ediliyor (yığında)
black = 2 // tamamlandı
)
color := map[string]int{}
var order []string
hasCycle := false
var dfs func(string)
dfs = func(node string) {
if hasCycle {
return
}
color[node] = gray
for _, next := range g.adj[node] {
switch color[next] {
case gray:
hasCycle = true // GRİ bir düğüme dönmek: döngü!
return
case white:
dfs(next)
}
}
color[node] = black
order = append(order, node) // çıkışta ekle
}
for _, n := range g.Nodes() {
if color[n] == white {
dfs(n)
}
}
if hasCycle {
return nil, false
}
slices.Reverse(order) // çıkış sırasının tersi
return order, true
}
// findCycle: döngü varsa yolunu döndürür
func (g *DAG) findCycle() []string {
const (
white = 0
gray = 1
black = 2
)
color := map[string]int{}
parent := map[string]string{}
var cycle []string
var dfs func(string) bool
dfs = func(node string) bool {
color[node] = gray
for _, next := range g.adj[node] {
switch color[next] {
case gray:
// Döngüyü geri kur
cycle = []string{next}
for n := node; n != next && n != ""; n = parent[n] {
cycle = append(cycle, n)
}
slices.Reverse(cycle)
cycle = append(cycle, next)
return true
case white:
parent[next] = node
if dfs(next) {
return true
}
}
}
color[node] = black
return false
}
for _, n := range g.Nodes() {
if color[n] == white && dfs(n) {
return cycle
}
}
return nil
}
func main() {
// Geçerli DAG
g := NewDAG()
for _, d := range [][2]string{
{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}, {"D", "E"},
} {
g.AddEdge(d[0], d[1])
}
order, ok := g.topoDFS()
fmt.Println("DAG için DFS tabanlı topolojik sıra:")
fmt.Println(" ", order, "| geçerli:", ok)
fmt.Println(" döngü:", g.findCycle())
// Döngülü graf
cyclic := NewDAG()
for _, d := range [][2]string{
{"A", "B"}, {"B", "C"}, {"C", "A"}, {"C", "D"},
} {
cyclic.AddEdge(d[0], d[1])
}
order2, ok2 := cyclic.topoDFS()
fmt.Println()
fmt.Println("döngülü graf:")
fmt.Println(" topolojik sıra:", order2, "| geçerli:", ok2)
fmt.Println(" bulunan döngü:", cyclic.findCycle())
// Kendine döngü
self := NewDAG()
self.AddEdge("X", "X")
fmt.Println()
fmt.Println("kendine döngü:")
_, ok3 := self.topoDFS()
fmt.Println(" geçerli:", ok3, "| döngü:", self.findCycle())
// Bağlantısız DAG
forest := NewDAG()
forest.AddEdge("A", "B")
forest.AddEdge("C", "D")
forest.AddNode("E")
order4, ok4 := forest.topoDFS()
fmt.Println()
fmt.Println("bağlantısız DAG:")
fmt.Println(" topolojik sıra:", order4, "| geçerli:", ok4)
}DAG için DFS tabanlı topolojik sıra: [A C B D E] | geçerli: true döngü: [] döngülü graf: topolojik sıra: [] | geçerli: false bulunan döngü: [B C A A] kendine döngü: geçerli: false | döngü: [X X] bağlantısız DAG: topolojik sıra: [E C D A B] | geçerli: true
Üç renkli döngü tespiti
Yönlü graflarda döngü tespiti, yönsüz graflardan farklıdır. Yönsüz grafta "ziyaret edilmiş bir komşuya rastlamak" döngü demekti; yönlü grafta bu yetmez.
A ──► B
│ │
▼ ▼
C ──► D
C'den D'ye giderken D zaten ziyaret edilmiş (B üzerinden).
Ama bu bir DÖNGÜ DEĞİL! Yalnızca iki farklı yol D'ye varıyor.
Döngü, ancak HÂLÂ İŞLENMEKTE OLAN (yığında olan) bir düğüme
dönüldüğünde vardır.
ÜÇ RENK:
BEYAZ: hiç ziyaret edilmedi
GRİ: ziyaret ediliyor, hâlâ çağrı yığınında
SİYAH: tamamlandı, yığından çıktı
GRİ bir düğüme rastlamak → DÖNGÜ
SİYAH bir düğüme rastlamak → sorun yok, zaten işlendipackage main
import (
"fmt"
"maps"
"slices"
)
type Digraph struct {
adj map[string][]string
nodes map[string]bool
}
func NewDigraph() *Digraph {
return &Digraph{adj: map[string][]string{}, nodes: map[string]bool{}}
}
func (g *Digraph) AddEdge(from, to string) {
g.nodes[from], g.nodes[to] = true, true
g.adj[from] = append(g.adj[from], to)
slices.Sort(g.adj[from])
}
func (g *Digraph) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
type color int
const (
white color = iota
gray
black
)
// analyze: üç renkli DFS ile döngü ve renk geçmişini raporlar
func (g *Digraph) analyze() (bool, []string) {
colors := map[string]color{}
var log []string
var dfs func(string) bool
dfs = func(node string) bool {
colors[node] = gray
log = append(log, fmt.Sprintf("%s → GRİ", node))
for _, next := range g.adj[node] {
switch colors[next] {
case gray:
log = append(log, fmt.Sprintf("%s → %s: GRİ düğüme dönüş → DÖNGÜ", node, next))
return true
case black:
log = append(log, fmt.Sprintf("%s → %s: SİYAH, zaten işlenmiş (sorun yok)", node, next))
case white:
if dfs(next) {
return true
}
}
}
colors[node] = black
log = append(log, fmt.Sprintf("%s → SİYAH", node))
return false
}
for _, n := range g.Nodes() {
if colors[n] == white {
if dfs(n) {
return true, log
}
}
}
return false, log
}
func main() {
// Elmas şekli: ziyaret edilmiş düğüme rastlanır ama döngü YOK
diamond := NewDigraph()
for _, e := range [][2]string{{"A", "B"}, {"A", "C"}, {"B", "D"}, {"C", "D"}} {
diamond.AddEdge(e[0], e[1])
}
fmt.Println("ELMAS GRAF (A→B→D, A→C→D):")
cyc, log := diamond.analyze()
for _, l := range log {
fmt.Println(" ", l)
}
fmt.Println(" döngü var mı:", cyc)
// Gerçek döngü
fmt.Println()
cyclic := NewDigraph()
for _, e := range [][2]string{{"A", "B"}, {"B", "C"}, {"C", "A"}} {
cyclic.AddEdge(e[0], e[1])
}
fmt.Println("DÖNGÜLÜ GRAF (A→B→C→A):")
cyc2, log2 := cyclic.analyze()
for _, l := range log2 {
fmt.Println(" ", l)
}
fmt.Println(" döngü var mı:", cyc2)
fmt.Println()
fmt.Println("ÖNEMLİ: Yönsüz grafta 'ziyaret edilmiş komşu' döngü demekti.")
fmt.Println("Yönlü grafta yetmez; komşunun HÂLÂ YIĞINDA olması gerekir.")
}ELMAS GRAF (A→B→D, A→C→D): A → GRİ B → GRİ D → GRİ D → SİYAH B → SİYAH C → GRİ C → D: SİYAH, zaten işlenmiş (sorun yok) C → SİYAH A → SİYAH döngü var mı: false DÖNGÜLÜ GRAF (A→B→C→A): A → GRİ B → GRİ C → GRİ C → A: GRİ düğüme dönüş → DÖNGÜ döngü var mı: true ÖNEMLİ: Yönsüz grafta 'ziyaret edilmiş komşu' döngü demekti. Yönlü grafta yetmez; komşunun HÂLÂ YIĞINDA olması gerekir.
Üç renkli yaklaşım, yönlü graflarla ilgili pek çok algoritmanın temelini oluşturur. Kenarları sınıflandırmayı da sağlar: gri bir düğüme giden kenar geri kenar (döngü), siyaha giden kenar ileri veya çapraz kenardır. Bu sınıflandırma, güçlü bağlı bileşen bulma ve köprü tespiti algoritmalarında kullanılır.
Kahn mı DFS mi?
İki algoritma da O(V + E) çalışır ve aynı işi yapar. Aradaki farklar pratikte seçim yapmana yardımcı olur.
Pratik öneri: Kahn algoritmasını varsayılan olarak kullan. Döngü tespiti daha nettir, katman bilgisi bedavadır, özyineleme derinliği sorunu yoktur ve paralel planlama gibi ek ihtiyaçları doğal karşılar. DFS tabanlı sürüm, zaten DFS yapıyorsan (örneğin aynı geçişte başka bilgi de topluyorsan) daha uygundur.
Uygulama: derleme ve bağımlılık çözümleme
Paket yöneticileri, derleme araçları ve görev çalıştırıcıları bu algoritmayı her gün kullanır.
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
type BuildSystem struct {
deps map[string][]string // hedef → ön koşulları
targets map[string]bool
}
func NewBuildSystem() *BuildSystem {
return &BuildSystem{deps: map[string][]string{}, targets: map[string]bool{}}
}
// Require: target, prerequisites'e bağımlıdır
func (b *BuildSystem) Require(target string, prerequisites ...string) {
b.targets[target] = true
for _, p := range prerequisites {
b.targets[p] = true
}
b.deps[target] = append(b.deps[target], prerequisites...)
slices.Sort(b.deps[target])
}
func (b *BuildSystem) Targets() []string { return slices.Sorted(maps.Keys(b.targets)) }
// BuildOrder: Kahn ile derleme sırası
func (b *BuildSystem) BuildOrder() ([]string, []string, error) {
// Ters graf: ön koşul → ona bağımlı olan
forward := map[string][]string{}
inDeg := map[string]int{}
for _, t := range b.Targets() {
inDeg[t] = 0
}
for target, prereqs := range b.deps {
for _, p := range prereqs {
forward[p] = append(forward[p], target)
inDeg[target]++
}
}
for k := range forward {
slices.Sort(forward[k])
}
var ready []string
for _, t := range b.Targets() {
if inDeg[t] == 0 {
ready = append(ready, t)
}
}
slices.Sort(ready)
var order []string
for len(ready) > 0 {
cur := ready[0]
ready = ready[1:]
order = append(order, cur)
for _, next := range forward[cur] {
inDeg[next]--
if inDeg[next] == 0 {
ready = append(ready, next)
slices.Sort(ready)
}
}
}
if len(order) != len(b.targets) {
// Döngüde kalan hedefleri bul
var stuck []string
inOrder := map[string]bool{}
for _, o := range order {
inOrder[o] = true
}
for _, t := range b.Targets() {
if !inOrder[t] {
stuck = append(stuck, t)
}
}
return nil, stuck, fmt.Errorf("dairesel bağımlılık: %s", strings.Join(stuck, ", "))
}
// Paralel gruplar
return order, b.parallelGroups(forward), nil
}
func (b *BuildSystem) parallelGroups(forward map[string][]string) []string {
inDeg := map[string]int{}
for _, t := range b.Targets() {
inDeg[t] = 0
}
for _, prereqs := range b.deps {
_ = prereqs
}
for target, prereqs := range b.deps {
inDeg[target] = len(prereqs)
}
var current []string
for _, t := range b.Targets() {
if inDeg[t] == 0 {
current = append(current, t)
}
}
var groups []string
round := 1
for len(current) > 0 {
slices.Sort(current)
groups = append(groups, fmt.Sprintf("aşama %d: %s", round, strings.Join(current, ", ")))
var next []string
for _, node := range current {
for _, t := range forward[node] {
inDeg[t]--
if inDeg[t] == 0 {
next = append(next, t)
}
}
}
current = next
round++
}
return groups
}
func main() {
// Gerçekçi bir Go projesi bağımlılık grafı
b := NewBuildSystem()
b.Require("app", "handlers", "config")
b.Require("handlers", "models", "validators")
b.Require("models", "database")
b.Require("validators", "models")
b.Require("database", "config")
b.Require("config")
fmt.Println("bağımlılıklar:")
for _, t := range b.Targets() {
if len(b.deps[t]) == 0 {
fmt.Printf(" %-12s (bağımlılık yok)\n", t)
continue
}
fmt.Printf(" %-12s ← %s\n", t, strings.Join(b.deps[t], ", "))
}
order, groups, err := b.BuildOrder()
if err != nil {
fmt.Println("hata:", err)
return
}
fmt.Println()
fmt.Println("derleme sırası:")
for i, t := range order {
fmt.Printf(" %d. %s\n", i+1, t)
}
fmt.Println()
fmt.Println("paralel derleme aşamaları:")
for _, g := range groups {
fmt.Println(" " + g)
}
// Dairesel bağımlılık
fmt.Println()
broken := NewBuildSystem()
broken.Require("a", "b")
broken.Require("b", "c")
broken.Require("c", "a")
broken.Require("d")
_, stuck, err2 := broken.BuildOrder()
fmt.Println("bozuk proje:")
if err2 != nil {
fmt.Println(" hata:", err2)
fmt.Println(" döngüde kalan hedefler:", stuck)
}
}bağımlılıklar: app ← config, handlers config (bağımlılık yok) database ← config handlers ← models, validators models ← database validators ← models derleme sırası: 1. config 2. database 3. models 4. validators 5. handlers 6. app paralel derleme aşamaları: aşama 1: config aşama 2: database aşama 3: models aşama 4: validators aşama 5: handlers aşama 6: app bozuk proje: hata: dairesel bağımlılık: a, b, c döngüde kalan hedefler: [a b c]
Bu örnekteki parallelGroups fonksiyonu, gerçek derleme araçlarının nasıl paralelleştirme yaptığını gösteriyor: Aynı aşamadaki hedefler birbirine bağımlı olmadığı için eşzamanlı derlenebilir. Aşama sayısı, paralel derlemenin alabileceği en kısa süreyi (kritik yol uzunluğunu) belirler.
Dairesel bağımlılık hatası da gerçek araçlarda gördüğün mesajın ta kendisidir. Go derleyicisi de içeri alma döngülerini tam olarak böyle tespit eder ve hangi paketlerin döngüde olduğunu bildirir.
Topolojik sıralamayı tanımak
Bir problemin topolojik sıralama gerektirdiğini fark etmek, çözümün tamamını verir. Tanıman gereken işaretler şunlardır.
"Şundan önce bunu yap" kısıtları. Problem metninde ön koşul, bağımlılık, öncelik ya da sıra kısıtı geçiyorsa, bunlar yönlü kenarlardır ve topolojik sıralama aranıyordur.
"Geçerli bir sıra var mı" soruları. Cevap, grafın döngü içerip içermediğine bağlıdır. Topolojik sıralama hem cevabı hem sıranın kendisini verir.
Katmanlı planlama. "En az kaç adımda tamamlanır", "aynı anda kaç iş yapılabilir" soruları Kahn algoritmasının katman çıktısıyla yanıtlanır.
DAG üzerinde dinamik programlama. Topolojik sıra, DAG üzerinde DP yapmak için doğru işleme sırasını verir: Bir düğümü işlemeden önce tüm öncüllerinin işlenmiş olduğu garanti edilir. En uzun yol, yol sayma ve DAG'da en kısa yol problemleri bu şekilde çözülür.
Döngü tespiti tek başına. Bazen sıralamaya ihtiyacın yoktur, yalnızca "dairesel bağımlılık var mı" sorusunu yanıtlaman gerekir. Her iki algoritma da bunu bedavaya verir.
Pratikte dikkat edilecek bir nokta: Topolojik sıra tek değildir. Birden çok geçerli sıra varsa ve sen belirli bir tanesini istiyorsan (örneğin sözlük sırasına göre en küçüğü), Kahn algoritmasındaki hazır düğüm kuyruğunu bir öncelik kuyruğuna çevirmen yeterlidir. Bu ders boyunca deterministik çıktı için hazır listeyi sıralı tuttuk; gerçek uygulamalarda bunun maliyeti genelde ihmal edilebilir.
Kısmi sıralar ve genel resim
Topolojik sıralamanın arkasında, matematikte kısmi sıra adı verilen bir kavram durur ve bu kavramı anlamak algoritmayı daha derin kavramanı sağlar.
Tam sıra, herhangi iki eleman arasında kesin bir karşılaştırma tanımlar: Sayılar arasında hangisinin büyük olduğu her zaman bellidir. Kısmi sıra ise bazı çiftler için sessiz kalır: Bir projede "tasarım, geliştirmeden önce gelmeli" kuralı vardır ama "belgeleme, test etmeden önce mi sonra mı" sorusunun cevabı yoktur — ikisi birbirinden bağımsızdır.
Topolojik sıralamanın yaptığı iş tam olarak şudur: Kısmi bir sırayı, onu bozmayan bir tam sıraya genişletmek. Bu yüzden sonuç tek değildir; kısıtlanmamış her çift için birden çok seçenek vardır. Matematikte bu işleme "doğrusal genişletme" denir ve her kısmi sıranın en az bir doğrusal genişletmesi olduğu kanıtlanmıştır — bu kanıt, DAG'ların her zaman topolojik sıralanabilmesinin gerekçesidir.
Bu bakış açısı pratik sorular da yanıtlar. "Kaç farklı geçerli sıra var?" sorusu, doğrusal genişletme sayısını sormaktır ve genel durumda bu sayıyı hesaplamak zor bir problemdir. "İki eleman arasında kesin bir sıra var mı?" sorusu, birinden diğerine yol olup olmadığını kontrol etmektir. "Sıralamayı tam olarak belirlemek için kaç kısıt eklemek gerekir?" sorusu ise belirsizlikleri ölçmektir — bu dersteki alfabe çözümleme alıştırmasında tam olarak bunu yaptık.
Aynı kavram sürüm numaralandırma, izin hiyerarşileri, tip sistemlerinde alt tip ilişkileri ve dağıtık sistemlerde olay sıralaması gibi çok farklı alanlarda karşına çıkar. Hepsinde ortak olan şey, elimizde kısmi bir bilgi bulunması ve onunla tutarlı bir düzen kurmamız gerektiğidir.
Sık yapılan hatalar
- Döngü kontrolünü atlamak. Döngülü bir grafta algoritma sessizce eksik bir sıra döndürür; sıralanan düğüm sayısını kontrol et.
- Yönlü grafta yönsüz döngü tespiti kullanmak. Ziyaret edilmiş düğüme rastlamak yönlü grafta döngü anlamına gelmez; üç renk gerekir.
- Kenar yönünü ters kurmak. "A, B'ye bağımlı" ifadesini modellerken oku hangi yöne çizdiğine dikkat et; sonuç tersine döner.
- Topolojik sıranın tek olduğunu sanmak. Birden çok geçerli sıra olabilir; testlerinde tek bir sıra beklemek kırılganlık üretir.
- Yalıtılmış düğümleri kaydetmemek. Hiç kenarı olmayan hedefler, yalnızca kenarlardan kurulan grafta görünmez.
- Derin graflarda özyinelemeli DFS kullanmak. Girdi kontrolsüzse Kahn algoritmasını tercih et.
- Kendine döngüyü unutmak. Bir düğümün kendisine kenarı varsa geçerli sıra yoktur; üç renkli kontrol bunu yakalar.
Alıştırmalar
Ders ön koşulları verilmiş. Tüm dersleri alabilmek için geçerli bir sıra bulunup bulunmadığını belirle; bulunuyorsa sırayı ve en az kaç dönem gerektiğini raporla.
İpucu
Kahn algoritmasının katman çıktısı doğrudan dönem sayısını verir: Aynı katmandaki dersler birbirine bağımlı değildir ve aynı dönemde alınabilir.
Çözümü göster
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
type Curriculum struct {
prereqs map[string][]string
courses map[string]bool
}
func NewCurriculum() *Curriculum {
return &Curriculum{prereqs: map[string][]string{}, courses: map[string]bool{}}
}
func (c *Curriculum) Add(course string, prereqs ...string) {
c.courses[course] = true
for _, p := range prereqs {
c.courses[p] = true
}
c.prereqs[course] = append(c.prereqs[course], prereqs...)
}
func (c *Curriculum) Courses() []string { return slices.Sorted(maps.Keys(c.courses)) }
// Plan: dönem dönem ders planı
func (c *Curriculum) Plan() ([][]string, bool) {
inDeg := map[string]int{}
unlocks := map[string][]string{}
for _, course := range c.Courses() {
inDeg[course] = len(c.prereqs[course])
}
for course, ps := range c.prereqs {
for _, p := range ps {
unlocks[p] = append(unlocks[p], course)
}
}
var current []string
for _, course := range c.Courses() {
if inDeg[course] == 0 {
current = append(current, course)
}
}
var semesters [][]string
placed := 0
for len(current) > 0 {
slices.Sort(current)
semesters = append(semesters, current)
placed += len(current)
var next []string
for _, course := range current {
for _, unlocked := range unlocks[course] {
inDeg[unlocked]--
if inDeg[unlocked] == 0 {
next = append(next, unlocked)
}
}
}
current = next
}
return semesters, placed == len(c.courses)
}
func main() {
c := NewCurriculum()
c.Add("Programlamaya Giriş")
c.Add("Veri Yapıları", "Programlamaya Giriş")
c.Add("Algoritmalar", "Veri Yapıları")
c.Add("Ayrık Matematik")
c.Add("Veritabanı", "Veri Yapıları")
c.Add("İşletim Sistemleri", "Programlamaya Giriş")
c.Add("Yapay Zekâ", "Algoritmalar", "Ayrık Matematik")
c.Add("Dağıtık Sistemler", "İşletim Sistemleri", "Veritabanı")
fmt.Println("ön koşullar:")
for _, course := range c.Courses() {
if len(c.prereqs[course]) == 0 {
fmt.Printf(" %-24s (ön koşul yok)\n", course)
continue
}
fmt.Printf(" %-24s ← %s\n", course, strings.Join(c.prereqs[course], ", "))
}
semesters, ok := c.Plan()
fmt.Println()
fmt.Println("plan geçerli mi:", ok)
if ok {
for i, s := range semesters {
fmt.Printf(" dönem %d: %s\n", i+1, strings.Join(s, ", "))
}
fmt.Println("en az dönem sayısı:", len(semesters))
}
// Dairesel ön koşul
fmt.Println()
broken := NewCurriculum()
broken.Add("A", "B")
broken.Add("B", "C")
broken.Add("C", "A")
_, ok2 := broken.Plan()
fmt.Println("dairesel ön koşullu müfredat geçerli mi:", ok2)
}ön koşullar: Algoritmalar ← Veri Yapıları Ayrık Matematik (ön koşul yok) Dağıtık Sistemler ← İşletim Sistemleri, Veritabanı Programlamaya Giriş (ön koşul yok) Veri Yapıları ← Programlamaya Giriş Veritabanı ← Veri Yapıları Yapay Zekâ ← Algoritmalar, Ayrık Matematik İşletim Sistemleri ← Programlamaya Giriş plan geçerli mi: true dönem 1: Ayrık Matematik, Programlamaya Giriş dönem 2: Veri Yapıları, İşletim Sistemleri dönem 3: Algoritmalar, Veritabanı dönem 4: Dağıtık Sistemler, Yapay Zekâ en az dönem sayısı: 4 dairesel ön koşullu müfredat geçerli mi: false
Dönem sayısı, grafın en uzun yol uzunluğuna eşittir ve bu değere kritik yol denir. Ne kadar paralel çalışırsan çalış, bu sayının altına inemezsin: Zincirin her halkası bir dönem alır. Proje yönetiminde aynı hesap, projenin en erken bitiş tarihini belirler.
Bir DAG'da en uzun yolu bul. Genel graflarda bu problem NP-zordur, ama DAG'da topolojik sıra sayesinde doğrusal sürede çözülür. Yolun kendisini de döndür.
İpucu
Topolojik sırada ilerlerken her düğüm için "buraya kadarki en uzun yol" değerini güncelle. Topolojik sıra, bir düğüme geldiğinde tüm öncüllerinin hesaplanmış olmasını garanti eder.
Çözümü göster
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
type Edge struct {
To string
Weight int
}
type DAG struct {
adj map[string][]Edge
nodes map[string]bool
}
func NewDAG() *DAG {
return &DAG{adj: map[string][]Edge{}, nodes: map[string]bool{}}
}
func (g *DAG) AddEdge(from, to string, weight int) {
g.nodes[from], g.nodes[to] = true, true
g.adj[from] = append(g.adj[from], Edge{to, weight})
slices.SortFunc(g.adj[from], func(a, b Edge) int { return strings.Compare(a.To, b.To) })
}
func (g *DAG) Nodes() []string { return slices.Sorted(maps.Keys(g.nodes)) }
func (g *DAG) topoOrder() ([]string, bool) {
inDeg := map[string]int{}
for _, n := range g.Nodes() {
inDeg[n] = 0
}
for _, edges := range g.adj {
for _, e := range edges {
inDeg[e.To]++
}
}
var ready []string
for _, n := range g.Nodes() {
if inDeg[n] == 0 {
ready = append(ready, n)
}
}
slices.Sort(ready)
var order []string
for len(ready) > 0 {
cur := ready[0]
ready = ready[1:]
order = append(order, cur)
for _, e := range g.adj[cur] {
inDeg[e.To]--
if inDeg[e.To] == 0 {
ready = append(ready, e.To)
slices.Sort(ready)
}
}
}
return order, len(order) == len(g.nodes)
}
// longestPath: DAG'da en uzun yol — O(V + E)
func (g *DAG) longestPath() (int, []string, bool) {
order, ok := g.topoOrder()
if !ok {
return 0, nil, false
}
dist := map[string]int{}
parent := map[string]string{}
for _, n := range g.Nodes() {
dist[n] = 0
}
// Topolojik sırada ilerle: öncüller kesin hesaplanmış
for _, node := range order {
for _, e := range g.adj[node] {
if dist[node]+e.Weight > dist[e.To] {
dist[e.To] = dist[node] + e.Weight
parent[e.To] = node
}
}
}
// En uzun yolun bitiş düğümünü bul
best, end := 0, ""
for _, n := range g.Nodes() {
if dist[n] >= best {
best, end = dist[n], n
}
}
var path []string
for n := end; n != ""; n = parent[n] {
path = append(path, n)
}
slices.Reverse(path)
return best, path, true
}
// countPaths: iki düğüm arasındaki yol sayısı
func (g *DAG) countPaths(from, to string) int {
order, ok := g.topoOrder()
if !ok {
return 0
}
count := map[string]int{from: 1}
for _, node := range order {
for _, e := range g.adj[node] {
count[e.To] += count[node]
}
}
return count[to]
}
func main() {
// Proje görevleri ve süreleri
g := NewDAG()
tasks := []struct {
from, to string
days int
}{
{"başla", "tasarım", 3},
{"tasarım", "arayüz", 5},
{"tasarım", "veritabanı", 4},
{"arayüz", "entegrasyon", 2},
{"veritabanı", "entegrasyon", 6},
{"entegrasyon", "test", 4},
{"test", "yayın", 1},
{"başla", "belgeleme", 2},
{"belgeleme", "yayın", 1},
}
for _, t := range tasks {
g.AddEdge(t.from, t.to, t.days)
}
fmt.Println("görev süreleri (gün):")
for _, t := range tasks {
fmt.Printf(" %-14s → %-14s %d gün\n", t.from, t.to, t.days)
}
order, _ := g.topoOrder()
fmt.Println()
fmt.Println("topolojik sıra:", order)
length, path, ok := g.longestPath()
fmt.Println()
fmt.Println("kritik yol (en uzun süre):", length, "gün | geçerli:", ok)
fmt.Println(" ", strings.Join(path, " → "))
fmt.Println()
fmt.Println("başla → yayın arasındaki yol sayısı:", g.countPaths("başla", "yayın"))
// Döngülü graf
cyclic := NewDAG()
cyclic.AddEdge("A", "B", 1)
cyclic.AddEdge("B", "A", 1)
_, _, ok2 := cyclic.longestPath()
fmt.Println()
fmt.Println("döngülü grafta en uzun yol hesaplanabilir mi:", ok2)
fmt.Println("(döngüde ağırlıklar sonsuza kadar birikeceği için tanımsızdır)")
}görev süreleri (gün): başla → tasarım 3 gün tasarım → arayüz 5 gün tasarım → veritabanı 4 gün arayüz → entegrasyon 2 gün veritabanı → entegrasyon 6 gün entegrasyon → test 4 gün test → yayın 1 gün başla → belgeleme 2 gün belgeleme → yayın 1 gün topolojik sıra: [başla belgeleme tasarım arayüz veritabanı entegrasyon test yayın] kritik yol (en uzun süre): 18 gün | geçerli: true başla → tasarım → veritabanı → entegrasyon → test → yayın başla → yayın arasındaki yol sayısı: 3 döngülü grafta en uzun yol hesaplanabilir mi: false (döngüde ağırlıklar sonsuza kadar birikeceği için tanımsızdır)
Genel graflarda en uzun yol bulmak NP-zordur — bilinen polinom zamanlı bir algoritma yoktur. DAG'da ise doğrusal sürede çözülür. Farkı yaratan şey topolojik sıradır: Bir düğüme geldiğinde tüm öncüllerinin cevabı zaten hesaplanmıştır, bu yüzden tek geçiş yeterlidir.
Bu, DAG üzerinde dinamik programlama kalıbının tipik örneğidir ve proje yönetiminde "kritik yol yöntemi" adıyla kullanılır: Projenin en kısa tamamlanma süresi, görev grafındaki en uzun yola eşittir. Kritik yol üzerindeki her gecikme projeyi doğrudan geciktirir; diğer yollardaki görevler ise bir miktar esnekliğe sahiptir.
Bilinmeyen bir alfabeye göre sıralanmış kelimeler verilmiş. Bu kelimelerden harflerin sırasını çıkar. Sıra belirsizse ya da çelişkiliyse bunu bildir.
İpucu
Ardışık iki kelimeyi karşılaştır; ilk farklı harf çifti bir sıra kısıtı verir. Bu kısıtlardan bir graf kur ve topolojik sıralama yap. Çelişki döngü demektir; belirsizlik ise birden çok geçerli sıra olması.
Çözümü göster
package main
import (
"fmt"
"slices"
)
// deriveAlphabet: sıralı kelimelerden harf sırasını çıkarır
func deriveAlphabet(words []string) (string, error) {
// Tüm harfleri topla
letters := map[rune]bool{}
for _, w := range words {
for _, r := range w {
letters[r] = true
}
}
adj := map[rune][]rune{}
inDeg := map[rune]int{}
for r := range letters {
inDeg[r] = 0
}
// Ardışık kelime çiftlerinden kısıt çıkar
for i := 0; i+1 < len(words); i++ {
a, b := []rune(words[i]), []rune(words[i+1])
minLen := min(len(a), len(b))
found := false
for j := range minLen {
if a[j] != b[j] {
// a[j] < b[j] kısıtı
if !slices.Contains(adj[a[j]], b[j]) {
adj[a[j]] = append(adj[a[j]], b[j])
inDeg[b[j]]++
}
found = true
break
}
}
// Geçersiz durum: "abc" sonra "ab" gelemez
if !found && len(a) > len(b) {
return "", fmt.Errorf("geçersiz sıra: %q, %q'dan sonra gelemez", words[i+1], words[i])
}
}
// Kahn algoritması
var ready []rune
for r := range letters {
if inDeg[r] == 0 {
ready = append(ready, r)
}
}
slices.Sort(ready)
var order []rune
multiplePossible := false
for len(ready) > 0 {
if len(ready) > 1 {
multiplePossible = true // birden çok seçenek: sıra belirsiz
}
cur := ready[0]
ready = ready[1:]
order = append(order, cur)
next := slices.Clone(adj[cur])
slices.Sort(next)
for _, n := range next {
inDeg[n]--
if inDeg[n] == 0 {
ready = append(ready, n)
slices.Sort(ready)
}
}
}
if len(order) != len(letters) {
return "", fmt.Errorf("çelişkili kısıtlar: harf sırasında döngü var")
}
result := string(order)
if multiplePossible {
return result, fmt.Errorf("sıra belirsiz (birden çok geçerli sıra var); biri: %q", result)
}
return result, nil
}
// verify: bulunan alfabeye göre kelimeler sıralı mı
func verify(words []string, alphabet string) bool {
rank := map[rune]int{}
for i, r := range alphabet {
rank[r] = i
}
less := func(a, b string) bool {
ra, rb := []rune(a), []rune(b)
for i := 0; i < len(ra) && i < len(rb); i++ {
if ra[i] != rb[i] {
return rank[ra[i]] < rank[rb[i]]
}
}
return len(ra) <= len(rb)
}
for i := 0; i+1 < len(words); i++ {
if !less(words[i], words[i+1]) {
return false
}
}
return true
}
func main() {
cases := [][]string{
{"wrt", "wrf", "er", "ett", "rftt"},
{"z", "x"},
{"z", "x", "z"}, // çelişki
{"abc", "ab"}, // geçersiz sıra
{"ab", "ac", "bc"},
{"kedi", "kera", "köpek"},
}
for _, words := range cases {
fmt.Printf("kelimeler: %v\n", words)
alphabet, err := deriveAlphabet(words)
switch {
case alphabet == "" && err != nil:
fmt.Println(" hata:", err)
case err != nil:
fmt.Println(" uyarı:", err)
fmt.Println(" doğrulama:", verify(words, alphabet))
default:
fmt.Printf(" alfabe: %q (tek geçerli sıra)\n", alphabet)
fmt.Println(" doğrulama:", verify(words, alphabet))
}
fmt.Println()
}
fmt.Println("Kısıt çıkarma kuralı:")
fmt.Println("Ardışık iki kelimenin İLK FARKLI harfi bir sıra kısıtı verir.")
fmt.Println("Sonraki harfler hakkında hiçbir bilgi vermez!")
fmt.Println()
fmt.Println("Örnek: \"wrt\" < \"wrf\" → yalnızca t < f")
fmt.Println("(w ve r eşit olduğu için onlar hakkında bilgi yok)")
}kelimeler: [wrt wrf er ett rftt] alfabe: "wertf" (tek geçerli sıra) doğrulama: true kelimeler: [z x] alfabe: "zx" (tek geçerli sıra) doğrulama: true kelimeler: [z x z] hata: çelişkili kısıtlar: harf sırasında döngü var kelimeler: [abc ab] hata: geçersiz sıra: "ab", "abc"'dan sonra gelemez kelimeler: [ab ac bc] alfabe: "abc" (tek geçerli sıra) doğrulama: true kelimeler: [kedi kera köpek] uyarı: sıra belirsiz (birden çok geçerli sıra var); biri: "adeikprö" doğrulama: true Kısıt çıkarma kuralı: Ardışık iki kelimenin İLK FARKLI harfi bir sıra kısıtı verir. Sonraki harfler hakkında hiçbir bilgi vermez! Örnek: "wrt" < "wrf" → yalnızca t < f (w ve r eşit olduğu için onlar hakkında bilgi yok)
Bu problem, topolojik sıralamanın gerçek bir keşif aracı olarak kullanılabileceğini gösteriyor. Verilen sıralı kelime listesi, harfler arasındaki kısmi sıra bilgisini içerir; topolojik sıralama bu kısmi bilgiyi tam bir sıraya çevirir.
Üç farklı sonuç durumu olduğuna dikkat et. Tek geçerli sıra varsa, algoritmanın her adımında hazır listede tek eleman bulunur. Birden çok geçerli sıra varsa, bazı adımlarda hazır listede birden fazla eleman olur — bu, o harfler arasında hiçbir kısıt bulunmadığı anlamına gelir. Çelişki varsa graf döngü içerir ve hiçbir geçerli sıra yoktur.
En sık yapılan hata, ardışık kelimelerden yalnızca ilk farklı harfin bilgi verdiğini gözden kaçırmaktır. Sonraki harfler hakkında hiçbir şey söylenemez; onlar için kısıt eklemek yanlış sonuçlar üretir.
Kısa sınav
Topolojik sıralama hangi graf türü için tanımlıdır?
Kahn algoritması hangi bilgiyi kullanır?
Kahn algoritmasında döngü nasıl tespit edilir?
DFS tabanlı topolojik sıralama nasıl elde edilir?
Yönlü grafta döngü tespitinde üç renk neden gereklidir?
Bir DAG'ın topolojik sıralaması kaç farklı olabilir?
Özet
- Topolojik sıralama, yönlü kenarların belirttiği "önce-sonra" kısıtlarını bozmayan bir düğüm sıralamasıdır.
- Yalnızca yönlü döngüsüz graflar (DAG) için tanımlıdır; döngü varsa geçerli sıra yoktur.
- Kahn algoritması giriş derecelerini kullanır: derecesi sıfır olanı al, komşularının derecesini azalt.
- Kahn algoritması döngü tespitini ve katman bilgisini bedavaya verir; katman sayısı kritik yol uzunluğudur.
- DFS tabanlı sürüm, çıkış sırasının tersini alır ve üç renkli işaretlemeyle döngüyü yakalar.
- Yönlü grafta döngü, yalnızca gri (hâlâ yığında olan) bir düğüme dönüldüğünde vardır.
- Topolojik sıra tek değildir; kısıtlanmamış düğümlerin sırası serbesttir.
- Topolojik sıra, DAG üzerinde dinamik programlama için doğru işleme sırasını verir: en uzun yol, yol sayma, kritik yol.
- Derleme sistemleri, paket yöneticileri, ders planlama ve görev çizelgeleme bu algoritmanın günlük uygulamalarıdır.