BIT: İkili indeksli bir ağacın arkasındaki sezgi nedir ve nasıl düşünülmüş?


99

İkili indekslenmiş bir ağacın diğer veri yapılarına kıyasla daha az veya nispeten hiç literatürü yoktur. Öğretildiği tek yer üst kod öğreticisidir . Öğretici tüm açıklamalarda eksiksiz olmasına rağmen, böyle bir ağacın arkasındaki sezgiyi anlayamıyorum? Nasıl icat edildi? Doğruluğunun gerçek kanıtı nedir?


4
Vikipedi'deki bir makalede, buna Fenwick ağaçları denir .
David Harkness

2
@ DavidHarkness- Peter Fenwick veri yapısını icat etti, bu yüzden bazen Fenwick ağaçları olarak adlandırılıyor. Orijinal makalesinde ( citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.14.8917 adresinde bulunur ), bunlara ikili indekslenmiş ağaçlar adını verdi. İki terim sıklıkla birbirinin yerine kullanılır.
templatetypedef


1
Nasıl hissettiğini biliyorum, ilk kodlayıcı makalesini ilk okuduğumda sihir gibiydi.
Rockstar5645

Yanıtlar:


168

Sezgisel olarak, ikili dizine alınmış bir ağacı, standart bir dizi gösteriminin optimizasyonu olan bir ikili ağacın sıkıştırılmış bir gösterimi olarak düşünebilirsiniz. Bu cevap bir olası türev içine giriyor.

Örneğin, toplam 7 farklı element için kümülatif frekansları saklamak istediğinizi varsayalım. Sayıların dağıtılacağı yedi kova yazarak başlayabilirsiniz:

[   ] [   ] [   ] [   ] [   ] [   ] [   ]
  1     2     3     4     5     6     7

Şimdi, kümülatif frekansların şunun gibi göründüğünü varsayalım:

[ 5 ] [ 6 ] [14 ] [25 ] [77 ] [105] [105]
  1     2     3     4     5     6     7

Dizinin bu sürümünü kullanarak, o noktada depolanan sayının değerini artırarak, daha sonra gelen her şeyin frekansını artırarak, herhangi bir öğenin kümülatif frekansını artırabilirsiniz. Örneğin, kümülatif sıklığı 3'e 7 arttırmak için, dizideki her bir öğeye, burada gösterildiği gibi, 3 konumundaki veya sonrasındaki 7 değerini ekleyebiliriz:

[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

Bununla ilgili sorun şu ki, n'nin büyük olması oldukça yavaş olan bu işlemi yapmak için O (n) zaman alıyor.

Bu işlemi iyileştirmeyi düşünebilmemizin bir yolu da kovalarda depoladıklarımızı değiştirmek. Kümülatif frekansı belirtilen noktaya kadar saklamak yerine, sadece mevcut frekansın önceki kovaya göre artmış olduğu miktarı depolamayı düşünebilirsiniz. Örneğin, bizim durumumuzda yukarıdaki kovaları şu şekilde yeniden yazardık:

Before:
[ 5 ] [ 6 ] [21 ] [32 ] [84 ] [112] [112]
  1     2     3     4     5     6     7

After:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

Şimdi, sadece bir kovaya uygun miktarda ekleyerek O (1) zamanında bir kova içindeki frekansı artırabiliriz. Ancak, bir arama yapmanın toplam maliyeti şimdi O (n) olur, çünkü tüm küçük kovalardaki değerleri toplayarak kovadaki toplamı yeniden hesaplamamız gerekir.

Buradan ikilik dizinlenmiş bir ağaca girmemiz gereken ilk ana fikir şudur: belirli bir öğeden önce gelen dizi öğelerinin toplamını sürekli olarak yeniden hesaplamak yerine, belirli bir öğeden önce tüm öğelerin toplam toplamını önceden hesaplamak için ne yapmalıyız? dizideki noktalar? Bunu yapabilirsek, o zaman birikmiş toplamı, bu önceden hesaplanmış toplamların doğru kombinasyonunu toplayarak bir noktada çözebiliriz.

Bunu yapmanın bir yolu, gösterimi bir kova dizisi olmaktan oluşan ikili bir düğüm ağacı olmaktan değiştirmektir. Her bir düğüm, verilen düğümün solundaki tüm düğümlerin kümülatif toplamını temsil eden bir değerle açıklanacaktır. Örneğin, bu düğümlerden aşağıdaki ikili ağacı kurduğumuzu varsayalım:

             4
          /     \
         2       6
        / \     / \
       1   3   5   7

Şimdi, her bir düğümü, o düğüm ve sol alt ağacı dahil tüm değerlerin kümülatif toplamını depolayarak artırabiliriz. Örneğin, değerlerimize bakarsak, aşağıdakileri saklardık:

Before:
[ +5] [ +1] [+15] [+11] [+52] [+28] [ +0]
  1     2     3     4     5     6     7

After:
                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [ +5] [+15] [+52] [ +0]

Bu ağaç yapısı göz önüne alındığında, kümülatif toplamı bir noktaya kadar belirlemek kolaydır. Fikir şudur: İlk önce 0 olan bir sayacı koruruz, sonra söz konusu düğümü bulana kadar normal bir ikili arama yaparız. Bunu yaptığımız gibi, aşağıdakileri de yapıyoruz: doğru hareket ettiğimiz her zaman, geçerli değeri sayaca da ekleriz.

Örneğin, 3 için toplamı aramak istediğimizi varsayalım. Bunu yapmak için aşağıdakileri yaparız:

  • Kökünden başlayın (4). Sayaç 0.
  • Düğüme (2) sola gidin. Sayaç 0.
  • Düğümden (3) sağa gidin. Sayaç 0 + 6 = 6'dır.
  • Düğümü (3) bulun. Sayaç 6 + 15 = 21'dir.

Bu işlemi tersten yürütmeyi de hayal edebilirsiniz: verilen bir düğümden başlayarak, o düğümün değerine karşı sayacı ilklendirin, ardından ağacı köke kadar yürütün. Yukarı doğru bir alt bağlantıyı takip ettiğinizde, vardığınız düğümdeki değeri ekleyin. Örneğin, 3 için frekansı bulmak için aşağıdakileri yapabiliriz:

  • Düğümden (3) başlayın. Sayaç 15.
  • Düğüme (2) yukarı doğru gidin. Sayaç 15 + 6 = 21'dir.
  • Düğüme (4) yukarı doğru gidin. Sayaç 21.

Bir düğümün sıklığını artırmak (ve dolaylı olarak, ondan sonra gelen tüm düğümlerin frekanslarını) artırmak için, ağacın sol alt ağacına dahil olan düğüm kümesini güncellememiz gerekir. Bunu yapmak için aşağıdakileri yaparız: o düğüm için frekansı artırın, ardından ağacın köküne doğru yürümeye başlayın. Sizi sol bir çocuk olarak alan bir bağlantıyı takip ettiğinizde, karşılaştığınız düğümün sıklığını geçerli değere ekleyerek artırın.

Örneğin, düğüm 1'in frekansını beşe kadar artırmak için aşağıdakileri yaparız:

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [ +5] [+15] [+52] [ +0]

1. düğümden başlayarak, frekansı 5’e çıkarmak için

                 4
               [+32]
              /     \
           2           6
         [ +6]       [+80]
         /   \       /   \
      > 1     3     5     7
      [+10] [+15] [+52] [ +0]

Şimdi ebeveyne git:

                 4
               [+32]
              /     \
         > 2           6
         [ +6]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

Sol alt bağlantıyı yukarı doğru takip ettik, bu yüzden bu düğümün sıklığını da arttırdık:

                 4
               [+32]
              /     \
         > 2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

Şimdi ebeveyne gidiyoruz:

               > 4
               [+32]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

Bu bir sol alt bağlantıydı, bu yüzden bu düğümü de arttırıyoruz:

                 4
               [+37]
              /     \
           2           6
         [+11]       [+80]
         /   \       /   \
        1     3     5     7
      [+10] [+15] [+52] [ +0]

Ve şimdi bitti!

Son adım, bundan ikilik indeksli bir ağaca dönüştürmek, ve işte ikili sayılarla eğlenceli şeyler yapacağımız yer burası. Bu ağaçtaki her bir kova dizinini ikili olarak yeniden yazalım:

                100
               [+37]
              /     \
          010         110
         [+11]       [+80]
         /   \       /   \
       001   011   101   111
      [+10] [+15] [+52] [ +0]

Burada çok, çok güzel bir gözlem yapabiliriz. Bu ikili sayılardan herhangi birini alın ve sayıda ayarlanan son 1 sayıyı bulun, ardından o parçayı bırakın, ardından gelen tüm bitlerle birlikte bırakın. Şimdi aşağıdakilerden ayrıldı:

              (empty)
               [+37]
              /     \
           0           1
         [+11]       [+80]
         /   \       /   \
        00   01     10   11
      [+10] [+15] [+52] [ +0]

İşte gerçekten, gerçekten harika bir gözlem: 0'a "sola" ve "1" anlamına "doğru" anlamına gelirseniz, her bir sayıdaki kalan bitler tam olarak kökten nasıl başlayıp o sayıya ineceğinizi gösterir. Örneğin, düğüm 5, ikili düzen 101'e sahiptir. Son 1 son bittir, bu yüzden onu 10'a düşürürüz. Gerçekten de, kökünden başlarsanız, sağa (1), sonra sola (0) gidin 5. düğümde!

Bunun önemli olmasının nedeni, arama ve güncelleme işlemlerimizin, düğümden geriye doğru kök yoluna ve sol veya sağ alt bağlantıları takip edip etmediğimize bağlı olmasıdır. Örneğin, bir arama sırasında, takip ettiğimiz doğru bağlantıları önemsiyoruz. Güncelleme sırasında, takip ettiğimiz sol linkleri önemsiyoruz. Bu ikili dizine alınmış ağaç, bu dizinin tamamını, yalnızca dizindeki bitleri kullanarak verimli bir şekilde yapar.

Anahtar numara, bu mükemmel ikili ağacın aşağıdaki özelliğidir:

Verilen düğüm n'de, erişim yolundaki bir sonraki düğüm, doğru gittiğimiz köke geri döner, n'nin ikili gösterimini alarak ve son 1'i kaldırarak verilir.

Örneğin, 111 olan düğüm 7'nin erişim yoluna bir göz atın. Erişim yolundaki köklerin, sağ göstericiyi yukarı doğru takip etmeyi gerektiren erişim yolundaki düğümleri

  • Düğüm 7: 111
  • Düğüm 6: 110
  • Düğüm 4: 100

Bunların hepsi doğru bağlantılar. 011 olan 3. düğümün erişim yolunu izlersek ve doğru gittiğimiz düğümlere bakarsak,

  • Düğüm 3: 011
  • Düğüm 2: 010
  • (Düğüm 4: 100, bu sol linki takip eder)

Bu, bir düğüme kadar kümülatif toplamı çok, çok verimli bir şekilde şu şekilde hesaplayabileceğimiz anlamına gelir:

  • N düğümünü ikili olarak yazın.
  • Sayacı 0 olarak ayarlayın.
  • N = 0 iken aşağıdakileri tekrarlayın:
    • N düğümündeki değere ekleyin.
    • En sağdaki 1 biti n'den silin.

Benzer şekilde, bir güncelleme adımını nasıl yapacağımızı düşünelim. Bunu yapmak için, sol bir bağlantıyı yukarı doğru takip ettiğimiz tüm düğümleri güncelleyerek kök dizinine kadar olan erişim yolunu takip etmek isteriz. Bunu esasen yukarıdaki algoritmayı yaparak yapabiliriz, ancak tüm 1'leri 0 ve 0'ları 1'e değiştiririz.

İkili indekslenmiş ağacın son adımı, bu bit hilesi nedeniyle, ağacın artık açık bir şekilde depolanmasına gerek kalmamasına dikkat etmek. Tüm düğümleri n uzunluğundaki bir dizi dizide depolayabiliriz, daha sonra ağacın üzerinde gezinmek için bitsel döndürme tekniklerini kullanırız. Aslında, tam olarak bitsel dizinli ağacın yaptığı şey budur - düğümleri bir dizide saklar, daha sonra bu bitsel hileleri bu ağaçta yukarı doğru yürümeyi verimli bir şekilde simüle etmek için kullanır.

Bu yardımcı olur umarım!



Beni ikinci paragrafta kaybettin. Ne demek 7 farklı elementin kümülatif frekansları?
Jason Goemaat

20
Bu, bugüne kadar internette bulduğum tüm kaynaklar arasında, konuyla ilgili şu ana kadar okuduğum en iyi açıklama. Aferin !
Anmol Singh Jaggi,

2
Fenwick bu zekayı nasıl elde etti?
Rockstar5645

1
Bu çok güzel bir açıklama ancak Fenwick'in kendi makalesinin yanı sıra diğer tüm açıklamalarla aynı problemden muzdarip!
DarthPaghius

3

Bence Fenwick'in orijinal makalesi çok daha açık. @Templatetypedef'in yukarıdaki cevabı, kafa karıştırıcı ve büyülü olan mükemmel bir ikili ağacın indekslenmesi hakkında bazı "çok güzel gözlemler" gerektiriyor.

Fenwick basitçe sorgu ağacındaki her düğümün sorumluluk aralığının son belirlenmiş bitine göre olacağını söyledi:

Fenwick ağacı düğümleri sorumlulukları

Örneğin, 6== kümesinin son biti 00110"2 bit" dir ve 2 düğümden sorumlu olacaktır. İçin 12== 01100o 4 düğümler bir dizi sorumlu olacak, böylece, bu, bir "4-bitlik" dir.

Yani F(12)== sorgulama yaparken F(01100), bitleri birer birer çıkartarak elde ediyoruz F(9:12) + F(1:8). Bu neredeyse kesin bir kanıt değil, ama bence basitçe sayılar eksenine ve mükemmel bir ikili ağaç üzerine değil, her düğümün sorumlulukları nelerdir ve sorgu maliyetinin neden sayıya eşittir? bit ayarlayın.

Bu hala net değilse, kağıt çok tavsiye edilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.