Bir Parmak Ağacı Yapısını Önyükleme


16

2-3 parmak ağacı ile biraz çalıştıktan sonra çoğu operasyonda hızlarından etkilendim. Ancak, karşılaştığım bir sorun, büyük bir parmak ağacının ilk oluşturulmasıyla ilişkili büyük ek yük. Bina, birleştirme işlemleri dizisi olarak tanımlandığından, gereksiz çok sayıda parmak ağacı yapısı oluşturursunuz.

2-3 parmak ağacının karmaşık doğası nedeniyle, onları önyüklemek için sezgisel bir yöntem görmüyorum ve tüm aramalarım boş çıktı. Yani soru şu ki, 2-3 parmak ağacını minimum yük ile nasıl öngörebilirsin?

Bir dizi verilen: açık olmak için, bilinen uzunlukta N parmak ağaç temsilini oluşturmak S az işlemleri ile.SnS

naif yolu, eksilerini operasyona ardı ardına çağırmaktır (literatürde ' ' operatörü). Ancak, bu [ 1 .. i ] için tüm S dilimlerini temsil eden n farklı parmak ağacı yapısı oluşturacaktır .nS[1..i]


1
Does basit genel amaçlı veri yapısı: Parmak ağaçları Hinze ve Paterson tarafından cevaplar?
Dave Clarke

@Kağıtlarını aslında uyguladım ve verimli yaratıma değinmiyorlar.
jbondeson

Ben de düşündüm.
Dave Clarke

Bu durumda "derleme" ile ne demek istediğinize biraz daha açık olabilir misiniz? Bu bir unfoldr mı?
jbapple

@jbapple - Karışıklık için daha açık, özür dilerim.
jbondeson

Yanıtlar:


16

GHC en Data.Sequence'in replicatefonksiyonu bir fingertree kurar zaman ve mekan, ancak bu olsun halindeyken gelen parmak ağacının sağ omurga devam unsurları bilerek etkindir. Bu kütüphane orijinal gazetenin yazarları tarafından 2-3 parmak ağaç üzerine yazılmıştır.Ö(lgn)

Tekrarlanan birleştirme ile bir parmak ağacı oluşturmak istiyorsanız, dikenlerin temsilini değiştirerek bina sırasında geçici alan kullanımını azaltabilirsiniz. 2-3 parmak ağacı üzerindeki dikenler akıllıca senkronize tekil listeler halinde saklanır. Bunun yerine, dikenleri deque olarak saklarsanız, ağaçları birleştirirken yerden tasarruf etmek mümkün olabilir. Fikir, aynı yükseklikte iki ağacı birleştirmenin , ağaçların dikenlerini yeniden kullanarak alanı kaplamasıdır. Başlangıçta tarif edildiği gibi 2-3 parmak ağacını birleştirirken, yeni ağacın içindeki dikenler artık olduğu gibi kullanılamaz.Ö(1)

Kaplan ve Tarjan'ın "Katlanabilir Sıralama Listelerinin Tamamen Fonksiyonel Gösterimleri" daha karmaşık bir parmak ağacı yapısını anlatıyor. Bu makalede (bölüm 4'te) yukarıda yaptığım deque önerisine benzer bir yapı tartışılmaktadır. Açıkladıkları yapının zaman ve mekanda eşit yükseklikte iki ağacı birleştirebileceğine inanıyorum . Parmak ağaçları inşa etmek için, bu sizin için yeterli yer tasarrufu sağlıyor mu?Ö(1)

Dikkat: “Bootstrapping” kelimesini kullanmaları, yukarıdaki kullanımınızdan biraz farklı bir şey ifade eder. Bir veri yapısının bir kısmını aynı yapının daha basit bir versiyonunu kullanarak saklamak anlamına gelir.


Çok ilginç bir fikir. Bunu incelemeli ve genel veri yapısında değiş tokuşların ne olacağını görmeliyim.
jbondeson

Bu cevapta iki fikir olması gerekiyordu: (1) Çoğaltılmış fikir (2) Neredeyse eşit büyüklükteki ağaçlar için daha hızlı birleşir. Ben giriş bir dizi ise çoğaltma fikir çok az ekstra alanda parmak ağaçlar inşa düşünüyorum.
jbapple

Evet, ikisini de gördüm. Üzgünüm ikisine de yorum yapmadım. Ben ilk önce çoğaltma koduna bakıyorum - kesinlikle Haskell bilgimi gidebildiği kadar uzatıyorum. İlk bakışta, hızlı rastgele erişiminiz varsa, yaşadığım sorunların çoğunu çözebilir gibi görünüyor. Hızlı bağlantı, rasgele erişim olmaması durumunda biraz daha genel bir çözüm olabilir.
jbondeson

10

Jbapple'ın mükemmel cevabına dayanarak replicate, bunun yerine replicateA( replicateüzerine kurulu) kullanarak , aşağıdakileri buldum:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(biraz daha verimli bir versiyonda) zaten tanımlanmış veData.Sequence türlerin sonuçları olan parmak ağaçlarının yapımında dahili olarak kullanılmaktadır .

Genel olarak, sezgi replicateA basittir. applicativeTree işlevinin replicateAüzerine kurulmuştur . büyüklüğünde bir ağaç parçası alır ve bunun kopyalarını içeren dengeli bir ağaç üretir . 8'e kadar olan durumlar (tek bir parmak) sabit kodlanmıştır. Bunun üzerindeki her şey ve kendini tekrar tekrar çağırır. "Uygulanabilir" eleman, basitçe, yukarıdaki kod durumunda, durumun iş parçacığı oluşturma efektleri ile ağacın yapısına serpiştirmesidir.applicativeTreemnnDeep

goÇoğaltılır fonksiyonu, sadece, mevcut durumunu alır üst bir eleman açılır ve geri kalan yerini alan bir işlemdir. Böylece her çağrıda girdi olarak verilen listeden daha aşağı iner.

Biraz daha somut notlar

main = print (length (show (Seq.fromList [1..10000000::Int])))

Bazı basit testlerde, bu ilginç bir performans dengesi sağladı. Yukarıdaki ana işlev myFromList ile olduğundan yaklaşık 1/3 daha düşüktü fromList. Öte yandan, myFromList2 MB'lık sabit bir yığın fromListkullanılırken , standart 926MB'a kadar kullanıldı. Bu 926MB, tüm listeyi bir kerede bellekte tutmaya ihtiyaç duymasından kaynaklanıyor. Bu arada, çözüm myFromListtembel bir akış tarzında yapıyı tüketebilir. Hız sorunu, myFromListkabaca iki kat daha fazla tahsis edilmesi gerektiği gerçeğinden kaynaklanmaktadır (çift devlet inşasının / çiftinin inşası / imhası sonucu)fromList. Bu tahsisleri CPS ile dönüştürülmüş bir devlet monadına geçerek ortadan kaldırabiliriz, ancak bu, herhangi bir zamanda çok daha fazla belleğe tutunmasına neden olur, çünkü tembellik kaybı listenin akışsız bir şekilde geçmesini gerektirir.

Öte yandan, tüm diziyi bir gösteri ile zorlamak yerine, sadece kafa veya son elemanı çıkarmak için hareket ediyorum, myFromListhemen daha büyük bir kazanç sunuyorum - kafa elemanını çıkarmak neredeyse anında ve son elemanı çıkarmak 0.8s. . Bu arada, standart ile fromList, kafa veya son elemanın çıkarılması ~ 2.3 saniye sürer.

Tüm bunlar detay ve saflık ve tembellikten kaynaklanıyor. Mutasyon ve rastgele erişime sahip bir durumda, replicateçözümün kesinlikle daha iyi olduğunu hayal ediyorum .

Ancak, yeniden yazmak için bir yol olup olmadığını sorusunu zam yapar applicativeTreeböyle myFromListkesinlikle daha verimlidir. Sorun, bence, uygulanabilir eylemlerin ağaçtan doğal olarak geçildiğinden farklı bir sırayla yürütüldüğü, ancak bunun nasıl çalıştığı veya bunu çözmenin bir yolu olup olmadığı üzerinde tam olarak çalışmadım.


4
(1) İlginç. Bu , bu görevi gerçekleştirmenin doğru yolu gibi görünüyor . Bunun fromListtüm dizinin zorlandığı zamandan daha yavaş olduğunu duyduğuma şaşırdım . (2) Belki de bu cevap cstheory.stackexchange.com için çok fazla kod ağırlığına ve dile bağlıdır. Dilden replicateAbağımsız bir şekilde nasıl çalıştığına dair bir açıklama ekleyebilirseniz harika olur .
Tsuyoshi Ito

9

Çok sayıda ara parmak yapısı ile sarılırken, yapılarının büyük çoğunluğunu birbirleriyle paylaşırlar. Sonunda, idealize edilmiş durumdan en fazla iki kat daha fazla bellek ayırırsınız ve geri kalanı ilk koleksiyonla serbest bırakılır. Bunun asimtotikleri elde edebildikleri kadar iyi, çünkü sonunda n değerleriyle dolu bir parmak ucuna ihtiyacınız var.

Parmak ucunu kullanarak Data.FingerTree.replicateve FingerTree.fmapWithPosdeğerlerini sonlu sekansınızın rolünü oynayan bir dizide aramak için veya traverseWithPosbunları bir listeden veya bilinen bir başka boyuttaki kaptan çıkarmak için kullanarak oluşturabilirsiniz.

Ö(günlükn)Ö(n)Ö(günlükn) çöp toplama işleri temizleyene kadar belleği harcar, Yani en uygun ~ 1000 düğüm yerine ~ 1010, cons'd inşaat ~ 2000 yerine ödemek zorunda kalacak.

Ö(günlükn)replicateAmapAccumL veya geçiş yapmak için gereken işlem, aslında orantılı olarak benzer olacaktır. tüm ekstra ürün hücreleri için ödeme yapmak için ek yük.

TL; DR Bunu yapmak zorunda olsaydım, muhtemelen şunu kullanırdım:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

ve sadece yukarıda (arr !)için temin ediyorum sabit boyutlu bir dizi dizin f.

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.