Dosya Yapıları (Başlangıçtan itibaren izlenen yaklaşımlar)
Kayıtları disk üzerinde farklı şekillerde organize edebiliriz. En iyi organizasyon şekli dosyayı nasıl kullanmak istediğimiz ile ilgilidir. Dosyanın tümüne mi erişilmek isteniyor? Dosya içerisindeki yalnızca bir kayıda mı erişilmek isteniyor? Dosya içerisinde seçili birden fazla kayda mı erişilmek isteniyor?
(i) Başlangıç Başlangıçta dosya yapıları fiziksel olarak tape ler üzerinde saklanmaktaydı. Erişim şeklide sıradüzenseldi. Erişimin maliyeti dosyanın boyutu ile orantılıydı.
Bu yapı üzerinde arama performansını arrtırmak için indeksler kullanılır. İndeksli yapıda indeks dosyası ve verinin bulunduğu dosya olmak üzere iki dosya bulunur. İndeksli dosya üzerinde sıradüzensel olarak yapılan arama sonucu ulaşılan kayıt konumu kullanılarak gerçek dosya üzerinde rastgele erişim sağlamak mümkündür. Veri dosyası için birden fazla sayıda indeks oluşturmak mümkündür.
(ii) Manyetik Diskler Manyetik disklerin kullanımı artmaktadır. Erişim şekli olarak rastgele erişim (random access) tercih edilmektedir. Indeks yapıları geliştirilmiştir. Böylelikle büyük veri kümelerinde istenilen kayda anında ulaşılma imkanına ulaşılmışıtır. Indeksli yapıda eğer dosya ana belleğe sığacak kadar büyükse herhangi bir sıkıntı yaşanmaz. Fakat dosya yapısı büyüdükçe indeks dosyası da buna bağlı olarak büyüyecektir.
(iii) Ağaç Yapıları (1960 lar) Dosya üzerindeki verilere daha hızlı erişim sağlayabilmek için ağaç yapıları geliştirilmiştir. İkili arama ağacı (Binary Searh Tree-BST) ve dengesini kendisi ayarlayabilen BST (AVL ağaçları ) 1960 yıllar boyunca ön plana çıkmıştır.
(v) Farklı Ağaç Türleri (1979) B ağaçları ve B+ ağaçları ile milyonlarca kayda sahip olan dosyalara çok düşük sayıda hamle ile erişilebilir hale gelinmiştir.
(vi) Hashleme (1960,70 ve günümüz) Herhangi bir kayda tek hamle ile ulaşma fikri hashleme ile karşımıza çıkmaktadır. Bu fikir eğer tablodaki kayıtlar zaman içerisinde çok fazla değişmiyorsa oldukça güzel bir yaklaşımdır. Eğer dosya boyutu hızlı bir şekilde artıyorsa extensible veya dynamic hashleme gibi teknikler geliştirilmiştir.
Hashing Gerçekleştirimi
Hashleme tekniği ikili arama ağacının tarafından izin verilen işlemlerin altkümesini destekler. Hash table ın uygulaması hashing olarak isimlendirilir. Hashing ekleme, silme ve arama işlemlerini O(1) gibi bir karmaşıklıkla gerçekleştirir. Fakat, sıralama, min. ve max. gibi işlemlerde etkili değildir.
Temel İlke İdeal hash tablo yapısı elemanlar içeren sabit uzunluklu diziden oluşur. Bu yapıda saklanan elemanın data member isimlendirilen yapı anahtar (key) olup elemanın indeks değerini hesaplamada kullanılır. Key tamsayı veya string olabilir. (TC kimlik no, ISBN, telefon no vb.) Dizinin boyutu TableSize olarak belirtilir. Indeks değerleri 0 ile TableSize-1 arasında değişim gösterir. Her bir anahtar 0 ile TableSize-1 ile belirtilen aralığa haritalanır. Haritalama hashleme fonksiyonu (hashing function) olarak isimlendirilir.
Hash Table 0 Items john 25000 phil 31250 dave 27500 key Hash Function 1 2 3 4 5 john 25000 phil 31250 mary 28200 6 7 dave 27500 mary 28200 key 8 9
Hash Fonksiyonu Hash fonksiyonu Basit ve hesaplanabilir olmalıdır. Anahtarları hücrelere eşsiz bir şekilde dağıtabiliyor olması gerekir. Yer ve kayıt sayısı gibi kısıtlar altında bu dağıtımı en iyi şekilde gerçekleştiren hash fonksiyonuna mükemmel hashleme (perfect hashing) olarak isimlendirilir.
Kısıtlar: Anahtarlar numeric olmayabilir. Anahtarların sayısı var olan kapasitenin çok üzerinde olabilir. Aynı anahtarlar aynı konuma yerleşmek isteyebilir. Böyle bir durumda çakışma (collision) gerçekleşir. Çakışmanın olduğu durumda hash fonksiyonu 1-1 çalışmaz. Eğer çok fazla çakışma varsa hash fonksiyonun performansında ciddi düşüşler gözlemlenir. Eğer anahtarlar nümerik değere sahip değilse öncelikli olarak nümerik hale getirilmelidir.
Bazı Hash Fonksiyonları h(x)=x mod N (0<=x<=N-1) (i) h(x)=x mod P (P asal sayı) Folding...
Örnek1: int hash(const string &key, int tablesize) { int hasval = 0; } for (int i = 0; i < key.length(); i++) hashval += key[i]; return hashval % tablesize; Basit ve hızlıdır.
Örnek 2: int hash (const string &key, int tablesize) { return (key[0]+27 * key[1] + 729*key[2]) % tablesize; } Anahtarın sadece ilk üç karakterine bakmaktadır.
Örnek 3: int hash (const string &key, int tablesize) { int hashval = 0; for (int i = 0; i < key.length(); i++) hashval = 37 * hashval + key[i]; hashval %=tablesize; if (hashval < 0) /* in case overflows occurs */ hashval += tablesize; }; return hashval;
Stringlerin Hashlenmesi 98 108 105 key a l i 0 1 2 i KeySize = 3; hash( ali ) = (105 * 1 + 108*37 + 98*37 2 ) % 10,007 = 8172 ali hash function ali 0 1 2 8172 10,006 (TableSize)
Çakışmaların Çözümlenmesi Meydana gelen çakışmaların çözümlenmesi için statik çakışma çözme algoritmalarını incelemiştik. LISCH LICH EICH Prograssive Overflow Use of Buckets Linear Quotient
Ayrık Zinciler (Separate Chaining) Aynı hash değerine sahip olan elemanların listesini tutma ilkesine dayanır. Dizi elemanları listenin ilk düğümüne işaret etmektedirler. Yeni eleman listenin başlangıcına eklenir. Avantajları: Geniş elemanlar için daha iyi kapasite yönetimi Çakışmaların yönetimi diğer çakışma çözme yöntemlerine göre görece daha kolaydır. Hash tablosunun boyutundan daha fazla sayıda eleman saklanabilmesine olanak verir. Silme işlemi bağlı listelerde olduğu gibi gerçekleştirilir.
Keys: 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 hash(key) = key % 10. 0 1 2 0 81 1 3 4 5 6 64 4 25 36 16 7 8 9 49 9
Temel İşlemler İlk değer atma (Initilization) : İlk değer olarak NULL atanır. Arama (Find): Hash fonksiyonu kullanılarak ilgili hücreye konumlanılır. Sonrasında bağlı listed olduğu gibi arama işlemi gerçekleştirilir. Ekleme(Insertion): Hash fonksiyonu kullanılarak ilgili hücreye konumlanılır. Eğer eleman mevcut değilse ilk eleman olacak (head first) listeye eklenir. Silme (Deletion): Hash fonksiyonu kullanılarak ilgili hücreye konumlanılır. Eleman bağlı listeden silinir.
Hash Table Sınıfı template <class HashedObj> class HashTable { public: HashTable(const HashedObj & notfound, int size=101 ); HashTable( const HashTable & rhs ) :ITEM_NOT_FOUND( rhs.item_not_found ), thelists( rhs.thelists ) { } const HashedObj & find( const HashedObj & x ) const; void makeempty( ); void insert( const HashedObj & x ); void remove( const HashedObj & x ); const HashTable & operator=( const HashTable & rhs ); private: vector<list<hashedobj> > thelists; // The array of Lists const HashedObj ITEM_NOT_FOUND; }; int hash( const string & key, int tablesize ); int hash( int key, int tablesize );
Ekleme (Insertion) /** * Insert item x into the hash table. If the item is * already present, then do nothing. */ template <class HashedObj> void HashTable<HashedObj>::insert(const HashedObj & x ) { List<HashedObj> & whichlist = thelists[ hash( x, thelists.size( ) ) ]; ListItr<HashedObj> itr = whichlist.find( x ); } if(!itr.isvalid() ) whichlist.insert( x, whichlist.zeroth( ) );
Silme (Remove) /** * Remove item x from the hash table. */ template <class HashedObj> void HashTable<HashedObj>::remove( const HashedObj & x ) { thelists[hash(x, thelists.size())].remove( x ); }
/** Arama (Find) * Find item x in the hash table. * Return the matching item or ITEM_NOT_FOUND if not found */ template <class HashedObj> const HashedObj & HashTable<HashedObj>::find( const HashedObj & x ) const { } ListItr<HashedObj> itr; itr = thelists[ hash( x, thelists.size( ) ) ].find( x ); if(!itr.isvalid()) return ITEM_NOT_FOUND; else return itr.retrieve( );
Ayrık Zinciler (Separate Chaining) Çakışmalar oldukça algoritmanın performansı değişim göstermektedir. Herhangi bir kayda erişimin maliyeti Sabit zamanda hash fonksiyonun belirttiği hücreye erişim sağlanmakta fakat bağlı liste üzerinde dolaşım gerektirmektedir. Başarız aramalarda bağlı liste üzerindeki bütün kayıtların üzerinde geçilmesi gerekmektedir.
Daha önce incelemiş olduğumuz statik çakışma çözme algoritmaları (bkz 12) aynı zamanda açık adreslemeli (open addressing) çakışma çözme algoritmalarıdır. Bu yöntemlerin çeşitli versiyonları veya hibritleri literatürde bulunmaktadır. Bu algoritmalarda, bütün veriler tablo içerinde tutulurlar. Bu yüzden daha büyük bir tabloya gereksinim vardır. Hatırlanacağı üzere bir çakışma meydana geldiğinde kaydın yerleştirileceği uygun konumu bulabilmek için tablo üzerinde dolaşım yapılmaktadır.
Hücre konumu = (hash(x) + f(i) ) mod TableSize Çakışma çözme yaklaşımı Yine literatürde farklı bir kategorilendirmede ise çakışma çözme yakşımlarını üç kategori altında toplamak mümkündür: Linear Probing (bkz LISCH, LICH vd.) Quadratic Probing Double Hashing
Linear Probing Hatırlanacağı üzere bu teknikte çakışmalar dizi üzerinde boş bir konum bulununcaya kadar gerçekleştirilmektedir. Örn: 89, 18, 49, 58, 9 anahtarlarını boş bir hash tablosuna yerleştirelim. Tablo boyutu 10 olsun. hash(x)=x mod 10
Bu yöntemlerden bazıları link kullıyorken bazıları ise kullanmamaktadır.
Arama & Silme Arama yaparken ekleme işleminde izlenen stratejinin aynısı izlenir. 58 kaydına ulaşmak için 4 probe 9 kaydına ulaşmak için 5 probe a gereksinim vardır. (bkz Progressive Overflow) Silme işleminde ise silinmek istenen kaydın işaretlenmesi gerekir. (bkz. tombstone).
Farklı Zincirlerin Birleşmesi (Coalesce) Yandaki şekilde A,B,C elemanlarından meydana gelen bir zincir olsun ve bu zincire sırasıyla X,D ve Y eklensin. X ve Y in home adresi s den D nin ki ise r den başlasın LISCH
Farklı home adreslere sahip halkaların iç içe geçme durumu söz konusudur. Böyle bir durum ise aramalardaki gerekli olan probe miktarını gereksiz yere arttıracaktır.
Performans Hash Function: h(x) = x mod 11 Başarılı Arama Durumu: 20: 9 -- 30: 8 -- 2 : 2 -- 13: 2, 3 -- 25: 3,4 24: 2,3,4,5 -- 10: 10 -- 9: 9,10, 0 Avg. Probe = (1+1+1+2+2+4+1+3)/8=15/8 Başarısız Arama Durumu: 0: 0,1 -- 1: 1 -- 2: 2,3,4,5,6 -- 3: 3,4,5,6 4: 4,5,6 -- 5: 5,6 -- 6: 6 -- 7: 7 -- 8: 8,9,10,0,1 9: 9,10,0,1 -- 10: 10,0,1 (Aranılan kayıtların bakılacağı muhtemel pozisyonlar) Avg. Probe = (2+1+5+4+3+2+1+1+5+4+3)/11 =31/11 0 9 1 2 2 3 13 4 25 5 24 6 7 8 30 9 20 10 10
Quadratic Probing Çalışma şekli bakımında Linear Quotient a benzemektedir. Bu yöntem linear probing de oluşan problemi önler. Çakışma çözme fonksiyonu quadratictir. Çoğu zaman f(i)=i 2 seçilir. Eğer hash fonksiyonu h adresini göstermekte ve bu konumda bir kayıt var ise yeni konum h+1 2, h+2 2, h+3 2,...,h+i 2 şeklinde hesaplanır.
Bu yöntemde tablonun boyutu asal sayı seçilmediği durumda tablonun yarıdan fazlasının dolu olması halinde boş konum bulunamayabilir. Fakat, diğer durumlarda( tablo boyutunun asal sayı seçilmesi ve doluluk oranı yarıdan az ise ) yeni elemanlar sorunsuz bir şekilde tabloya eklenbilir.
Double Hashing İkinci bir fonksiyon çakışma durumunda uygun olan konumun seçiminde kullanılabilir (bkz Linear Quotient). f(i)=i * hash 2 (x) Bu ikinci fonksiyon çakışmaları giderecek şekilde seçilmelidir.