Bağlı Listeler

Doğrusal listenin işaretçilerle gerçekleştirilmesi: düğüm ekleme, silme, arama ve gezinme, sonra dairesel, çift bağlı ve çok bağlı çeşitleri.

Yayımlandı

bağlı liste · c · veri yapıları

Bu yazı BLM212 Veri Yapıları dersinin bağlı listeler konusu için hazırlanmış bir çalışma dosyası. Ders notlarından yola çıkılarak yapay zekâ ile derlendi ve düzenlendi. Ekleme ve silme bölümündeki altı senaryo adım adım ilerletilebilir — o bölümün asıl dersi bir sıra kuralı ve sıra, bitmiş bir şemada görünmüyor.

Doğrusal liste

Doğrusal liste, her elemanın tek bir halefi olduğu bir listedir. Üzerinde tanımlı dört temel işlem var: araya sığdıran insertion, çıkaran deletion, veriyi listeden çıkarmadan okuyan retrieval ve baştan sona her düğüme tam bir kez uğrayan traversal.

Bu dört işlem her doğrusal liste ADT'sinin çekirdeğidir. Yığın ve kuyruk da doğrusal listedir — onlarda kısıtlanan şey işlemlerin nerede yapılabileceğidir, ve kısıtlı listelerde gezinme yoktur.

Bütün konu tek bir gerilimin üstüne kurulu: sıralı verilerde ekleme ve silme gerektiğinde diziler verimsizdir, çünkü her şeyi kaydırmak gerekir. Bağlı liste bunu çözer, ama karşılığında arama ve erişim verimsizleşir, çünkü artık rastgele erişim yoktur.

Dizi mi, bağlı liste mi?

N elemanlı bir liste için dört işlemin maliyeti
işlemdizibağlı listeneden
başa ekle / silO(N)pahalıO(1)ucuzdizide her eleman bir kayar; listede iki bağ değişir
sona ekle / silO(1)ucuzO(1)ucuziki tarafta da kaydırma yok
tümünü yazdırO(N)ortaO(N)ortaher elemana bir kez uğranır
i. elemana erişO(1)ucuzO(i)pahalıdizide indeksle; listede baştan yürüyerek
Tabloyu ezberlemek yerine nedenini aklında tut: dizide sıra konumdan, bağlı listede işaretçiden gelir.

Uygulaman yalnızca listenin sonuna ekleme ve i. elemana erişim yapıyorsa dizi uygundur; ikisi de sabit zamanlıdır. Ekleme ve silmeler listenin her noktasında, özellikle de başında oluyorsa dizi iyi bir seçim değildir — boyut önceden bilinmiyorsa da öyle.

Bedeli şu: bağlı listede düğümler fiziksel olarak ardışık değildir, bu yüzden ikili arama yapılamaz, yalnızca sıralı arama mümkündür. findKth artık listeyi gezmeyi gerektirir.

İki yapı

Listeyi tanımlamak için teknik olarak tek bir işaretçi yeterlidir. Ama pratikte baş yapısı (head structure) kurmak çok daha kullanışlıdır: baş işaretçinin yanına listeyle ilgili başka bilgiler de konur.

(a) head structure — liste hakkındaki bilgiler3countposheadcount ve pos → metadata:listedeki veriler hakkındaki veriler(b) data node structuredataSözde kod bildirimilistcount <integer>pos <pointer>head <pointer>nodedata <dataType>link <pointer>
Düğümler self-referential yapılardır: yapının her örneği, aynı yapısal türden başka bir örneğe işaretçi içerir. pos alanı gezinme sırasında nerede kalındığını tutar.
Algorithm createList
   Bağlı liste baş düğümü için dinamik bellek ayırır ve adresini döndürür.
1 if (memory available)
   1  allocate (pNew)
   2  pNew->head  = null pointer
   3  pNew->count = 0
2 else
   1  pNew = null pointer
3 return pNew
end createList

Boş liste demek, baş yapısı var ama head null demektir. Liste "yok" değildir — sadece içi boştur.

Ekleme ve silme

Ekleme üç adımdır: ① yeni düğüme yer aç ve veriyi koy, ② yeni düğümden önce gelen düğümü (pPre) bul, ③ o düğümün yeni düğümü göstermesini sağla. Kritik nokta sıra: önce yeni düğümün bağı, sonra öncekinin bağı. Tersi olursa listenin kuyruğu kaybolur.

adım 0 / 7
  1. 1 allocate (pNew)
  2. 2 if (memory overflow) return false
  3. 3 pNew->data = dataIn
  4. 4 if (pPre null)
  5. 1 pNew->link = pList->head
  6. 2 pList->head = pNew
  7. 5 else
  8. 1 pNew->link = pPre->link
  9. 2 pPre->link = pNew
  10. 6 pList->count = pList->count + 1
  11. 7 return true
0counthead

ADIM

Boş liste: head null, count 0. Eklenecek verinin önünde düğüm yok, yani pPre = null.

Ekleme tek bir if ile iki dala ayrılır. pPre null ise (boş listeye ya da başa ekleme) yeni düğümün bağı head'e, head ise yeni düğüme çevrilir. pPre null değilse (ortaya ya da sona ekleme) yeni düğümün bağı pPre->link'e, pPre->link ise yeni düğüme çevrilir. Dört durumun tamamı bu iki dalla karşılanır; sona ekleme için ayrı bir kod yoktur.

Silme aynı iki dalı kullanır. pPre null ise (ilk düğümü silme) head = pLoc->link; değilse pPre->link = pLoc->link. Her iki dalda da sonunda sayaç azaltılır ve pLoc'un belleği geri verilir.

En sık yapılan hata, ekleme sırasında önce pPre->link = pNew yazmaktır. O anda pPre->link'in eski değeri — yani listenin geri kalanı — kaybolur ve geri getirilemez. Sıra daima önce yeni düğümün bağı. Yukarıdaki senaryolarda "ortaya ekle" adımlarını izlersen, kuyruğun bir adım boyunca iki yerden birden tutulduğu anı görürsün; sıranın sebebi tam olarak o an.

Arama

Ekleme için önceki düğümü, silme için silinecek düğümü ve öncekini, erişim için düğümün kendisini bilmek gerekir. Üçünü de tek bir arama fonksiyonu sağlar — bu yüzden geriye iki işaretçi döndürür.

Düğümler arasında fiziksel bir ilişki olmadığı için sıralı arama zorunludur. Klasik sıralı aramadan farkı şu: liste sıralı olduğu için, aranan bulunamadığında da olması gereken yer döndürülür.

(a) Başarılı arama — target = 20 ⇒ true5152095100pPrepLoc(b) Başarısız arama — target = 17 ⇒ false5152095100pPrepLocpLoc iki durumda da aynı yerde:hedeften küçük olmayan ilk düğüm
İşin püf noktası: arama başarısız olsa bile pPre ve pLoc doğru yeri gösterir — yani 17'yi eklemek isteseydin, ekleme noktası tam olarak burasıdır. Bu yüzden aynı fonksiyon hem aramaya hem eklemeye hizmet eder.
Algorithm searchList (pList, pPre, pLoc, target)
1 pPre = null
2 pLoc = pList->head
3 loop (pLoc not null AND target > pLoc->data.key)
   1  pPre = pLoc
   2  pLoc = pLoc->link
4 if (pLoc is null)  found = false
5 else
   1  if (target equal pLoc->data.key)  found = true
   2  else                              found = false
6 return found

Döngüdeki iki koşul iki ayrı işi yapar: birincisi listenin sonundan taşmayı engeller, ikincisi hedef bulunduğunda ya da hedeften büyük bir düğüme gelindiğinde — yani hedef listede yoksa — döngüyü durdurur.

searchList fonksiyonunun altı durumda döndürdükleri
koşulpPrepLocdönüş
target < ilk düğümnullilk düğümfalse
target = ilk düğümnullilk düğümtrue
ilk < target < sontarget'tan küçük en büyük düğümtarget'tan büyük ilk düğümfalse
target = ortadaki düğümdüğümün selefieşit düğümtrue
target = son düğümsonun selefison düğümtrue
target > son düğümson düğümnullfalse
Arama başarısız olduğunda bile pPre ve pLoc doğru yeri gösterir — yani ekleme noktası zaten elinizdedir.

Gezinme ve yok etme

Gezinmenin çekirdeği dört satırdır. Zor kısım, ADT'de her çağrının bir sonraki elemanı döndürmesi gerektiğidir; bu yüzden baş yapısında pos alanı tutulur.

pWalker = pList->head
loop (pWalker not null)
    process (pWalker->data)
    pWalker = pWalker->link

ADT içinde aynı iş, kaldığı yeri hatırlamak zorunda olduğu için biraz daha uzar:

if (fromWhere is 0)   // baştan başla
   pList->pos = pList->head
else                  // kaldığın yerden
   if (pList->pos->link is null)
        return false   // liste bitti
   pList->pos = pList->pos->link

Uygulama programcısının liste yapısına erişimi olmadığı için, nerede kalındığını ADT'nin kendisi hatırlamalıdır.

Listeyi yok etmek üç iştir: ① listedeki düğümleri silip belleklerini geri ver, ② baş yapısının belleğini geri ver, ③ listenin artık var olmadığını gösteren null işaretçiyi döndür.

algorithm destroyList (ref pList)
1 loop (pList->count not zero)
   1  dltPtr       = pList->head
   2  pList->head  = dltPtr->link      // önce zinciri kurtar
   3  pList->count = pList->count - 1
   4  release (dltPtr)                 // sonra sil
2 release (pList)
3 return null pointer

Yine aynı sıra kuralı: düğümü serbest bırakmadan önce bağını kopyala. Önce release yaparsan, listenin geri kalanının adresini okuyacağın yer artık senin değildir.

List ADT

Bağlı liste gerçekleştirmesini bir ADT'ye çevirmek iki problem doğurur: ADT verinin türünü bilmez, ama listeyi anahtar sırasında tutmak zorundadır. Sıralamak için karşılaştırmak, karşılaştırmak için türü bilmek gerekir.

Veriyi uygulama programı tutar: belleği o ayırır ve düğümün adresini ADT'ye geçirir. C strongly typed olduğu için veri işaretçisi void* olarak geçer. Sıralama sorununun çözümü ise uygulamanın bir compare fonksiyonu yazması ve ADT'nin onun adresini baş yapısında metadata olarak saklamasıdır. Sözleşme basit: arg < key → −1, arg = key → 0, arg > key → +1. Programcı yapıya erişemediği için emptyList, fullList ve listCount gibi durum fonksiyonları da gerekir.

Uygulamaveri için belleğio ayırırcompare()türü bilentek yerList ADTpublic — kullanıcı arayüzücreateList · addNode · removeNode · searchList · traverse · destroyListinternal — yalnızca ADT içinden çağrılır_insert · _delete · _searchhead structure (count · pos · head · compare) + düğümleradresi
Gerçek iş dahili fonksiyonlarda döner; public fonksiyonlar yalnızca arayüzdür. compare'ın adresi baş yapısında metadata olarak durur — ADT karşılaştırmayı yapmaz, yaptırır.

Buradaki iki araç, ADT dersinde görülen ikisinin ta kendisi: void* veriyi tür bilmeden taşır, fonksiyon işaretçisi işlemi tür bilmeden yaptırır. Liste ADT'si bunların ilk gerçek uygulaması.

Liste çeşitleri

Tek yönlü listenin iki eksiği var: geriye gidilemez ve sondan başa dönülemez. Üç kullanışlı çeşit bu eksikleri kapatır.

Dairesel bağlı liste

Son düğümdeki bağ ilk düğümü (ya da başlık düğümünü) gösterir. Ekleme ve silme tek bağlı listeyle aynıdır — tek fark son düğümün bağı. Asıl mesele arama: döngü ne zaman duracak?

startAddressABCDson düğüm ilk düğüme döner ⇒ her düğümden her düğüme ulaşılırloop (target <> pLoc->data.key AND pLoc->link <> startAddress)başlangıç adresi saklanır; ona geri dönülünce durulur
Dairesel listede null yoktur, dolayısıyla 'liste bitti' testi de yoktur. Çözüm: başlangıç düğümünün adresini sakla, ona geri dönünce dur.

Çift bağlı liste

Her düğümde iki işaretçi: selefini gösteren back ve halefini gösteren fore. Bedeli, her ekleme ve silmede dört bağın güncellenmesidir.

12backforepPre45backforepSucc27backforepNeweski bağlar kopar① fore② back③ pSucc->back④ pPre->fore
Sıra yine önemli: önce yeni düğümün iki bağı (① ②), sonra komşuların bağları (③ ④). Sondaki düğüme eklenirken pPre->fore null olur; o zaman listenin rear işaretçisi de güncellenir.

Silmede iki bağımsız kontrol gerekir, çünkü silinen düğüm başta ya da sonda olabilir:

if (pDlt->back <> null)   // ilk düğüm değil
     pDlt->back->fore = pDlt->fore
else pList->head       = pDlt->fore

if (pDlt->fore <> null)   // son düğüm değil
     pDlt->fore->back = pDlt->back
else pList->rear       = pDlt->back

Diğer iki çeşit

Çok bağlı listede aynı düğüm kümesi iki veya daha fazla mantıksal sıraya göre ayrı bağ zincirleriyle bağlanır. Veriler çoğaltılmaz, yalnızca ek bağlar eklenir. Ders örneği ABD başkanları listesi: kronolojik sıranın yanına başkanın adına ve eşinin adına göre iki zincir daha.

Bağlı liste dizisinde ise dizinin her elemanı bir bağlı listenin başıdır: her bağlı liste bir satırı, listedeki düğümler sütunları temsil eder. Satır sayısı sabit, satır uzunlukları değişkendir.

İkisinin birleşimi de mümkün: çift bağlı dairesel liste hem iki yönde gezilir hem de uçları birbirine bağlıdır, baş ile son düğüm arasındaki geçiş tek adımdır.

Kendini yokla

1Bir bağlı listenin başına ekleme neden O(1), diziye başa ekleme neden O(N)?

Dizide başa ekleme için mevcut her eleman bir sağa kaymalıdır; N eleman varsa N kaydırma yapılır.

Bağlı listede fiziksel konum hiç önemli değildir. Yalnızca iki bağ değişir: yeni düğümün bağı eski ilk düğüme, head ise yeni düğüme çevrilir. Eleman sayısından bağımsız, sabit sayıda iş.

2Eklemede pPre->link = pNew satırını pNew->link = pPre->link satırından önce yazarsanız ne olur?

Listenin geri kalanı kaybolur ve geri getirilemez.

pPre->link o an listenin kuyruğunun adresini tutan tek yerdir. Üzerine pNew yazdığın anda o adres hiçbir yerde kayıtlı değildir; pNew->link için okuyacağın değer de artık yoktur. Kural bu yüzden değişmez: önce yeni düğümün bağı.

3searchList aradığını bulamadığında pPre ve pLoc nereyi gösterir, bu neden işe yarar?

pLoc hedeften küçük olmayan ilk düğümü, pPre de onun selefini gösterir.

Bu tam olarak hedefin listede olması gereken yerdir. Yani başarısız bir arama boşa gitmez: dönen iki işaretçi, aynı değeri eklemek isteseydin kullanacağın ekleme noktasıdır. Aynı fonksiyonun hem aramaya hem eklemeye hizmet etmesinin sebebi bu.

4Dairesel bağlı listede arama döngüsü neden pLoc null testiyle bitirilemez?

Çünkü dairesel listede null yoktur. Son düğümün bağı ilk düğümü gösterir, o yüzden "liste bitti" diyecek bir işaret bulunmaz; döngü sonsuza kadar döner.

Çözüm başlangıç adresini saklamaktır: loop (target <> pLoc->data.key AND pLoc->link <> startAddress).

5Çift bağlı listede bir düğüm silinirken neden iki ayrı if gerekir?

Silinen düğüm aynı anda hem ilk hem son düğüm olabilir, ya da ikisinden biri olabilir — dört ihtimal var ve ikisi birbirinden bağımsız.

pDlt->back null ise silinen düğüm ilk düğümdür ve head güncellenmelidir; değilse selefinin fore'u. pDlt->fore null ise son düğümdür ve rear güncellenmelidir; değilse halefinin back'i. İki bağımsız soru, iki bağımsız if.