Özyineleme mi, Yineleme mi?


228

Her ikisinin de aynı amaca hizmet edebileceği algoritmalarda özyineleme yerine bir döngü kullanırsak veya tam tersi olursa bir performans artışı olur mu? Örn: Verilen dizenin palindrom olup olmadığını kontrol edin. Basit bir yineleme algoritmasının faturaya uyduğunu göstermek için özyineleme aracı olarak birçok programcı gördüm. Derleyici ne kullanacağına karar vermede hayati bir rol oynuyor mu?


4
@ Savaşçı Her zaman değil. Örneğin satranç programları ile özyinelemeyi okumak daha kolaydır. Satranç kodunun “yinelemeli” bir versiyonu gerçekten hıza yardımcı olmaz ve daha karmaşık hale getirebilir.
Mateen Ulhaq

12
Bir çekiç bir testere yerine neden tercih edilmelidir? Bir bız üzerinde tornavida? Bir burgu üzerinde bir keski?
Wayne Conrad

3
Sık kullanılan yok. Hepsi sadece araçlar, her birinin kendi amacı var. “Yineleme özyinelemeden ne tür problemler yinelemeden daha iyidir?
Wayne Conrad

9
“Özyineleme ile ilgili bu kadar iyi olan nedir?” ... Özyinelemeli olan budur. ; o)
Keng

9
Yanlış öncül. Özyineleme iyi değil; aslında çok kötü. Sağlam bir yazılım yazan herkes, tüm geri çağrıyı ortadan kaldırmaya çalışacaktır, çünkü kuyruk çağrısı optimize edilemezse veya logaritmik veya benzerleriyle sınırlanan seviye sayısı, özyineleme neredeyse her zaman kötü türden yığın taşmasına yol açar .
R .. GitHub DURDURMAK BUZA YARDIMCI

Yanıtlar:


181

Yinelemeli işlevin kuyruk yinelemeli olmasına bağlı olarak yinelemenin daha pahalı olması mümkündür (son satır yinelemeli çağrıdır). Kuyruk özyineleme derleyici tarafından tanınmalı ve yinelemeli karşılığına göre optimize edilmelidir (kodunuzda açık ve net uygulamayı sürdürürken).

Algoritmayı en mantıklı şekilde ve birkaç ay veya yıl içinde kodu korumak zorunda olan zavallı enayi (kendiniz veya başka biri) için en açık şekilde yazardım. Performans sorunlarıyla karşılaşırsanız, kodunuzu profil haline getirin ve yalnızca yinelemeli bir uygulamaya geçerek optimizasyona bakın. Sen içine bakmak isteyebilirsiniz memoization ve dinamik programlama .


12
Doğruluğu tümevarım ile kanıtlanabilen algoritmalar kendilerini doğal olarak özyinelemeli olarak yazma eğilimindedir. Kuyruk özyinelemesinin derleyiciler tarafından optimize edilmesiyle birleştiğinde, yinelenen olarak daha fazla algoritma görüyorsunuz.
Binil Thomas

15
re: tail recursion is optimized by compilersAma tüm derleyiciler kuyruk özyineleme desteklemez ..
Kevin Meredith

350

Döngüler programınız için bir performans kazancı sağlayabilir. Yineleme, programlayıcınız için bir performans kazancı sağlayabilir. Durumunuzda hangisinin daha önemli olduğunu seçin!


3
@LeighCaldwell: Bence bu düşüncelerimi özetler. Yazık Omnipotent modifiye etmedi. Kesinlikle var. :)
Ande Turner

36
Cevap ifadeniz nedeniyle bir kitaba atıfta bulunduğunuzu biliyor muydunuz? LOL amazon.com/Grokking-Algorithms-illustrated-programmers-curious/…
Aipi

4
Bu yanıtı seviyorum .. ve "Grokking Algorithms" kitabını beğendim)
Max

en azından ben ve 341 insan Grokking Algoritmaları kitabını okuduk!
zzfima

78

Yinelemenin yinelemeyle karşılaştırılması, yıldız uçlu bir tornavidayı düz uçlu bir tornavidayla karşılaştırmak gibidir. Çoğunlukla size verebilir düz kafa ile herhangi Phillips kafa vidayı çıkarmak, ama o vida hakkı için tasarlanmış tornavida kullanılırsa daha kolay olurdu?

Bazı algoritmalar, tasarlandıkları yoldan dolayı kendilerini özyinelemeye ödünç verir (Fibonacci dizileri, ağaç benzeri bir yapıda gezinme, vb.). Özyineleme, algoritmayı daha kısa ve anlaşılır kılar (bu nedenle paylaşılabilir ve yeniden kullanılabilir).

Ayrıca, bazı özyinelemeli algoritmalar onları tekrarlayan kardeşlerinden daha verimli hale getiren "Tembel Değerlendirme" yi kullanır. Bu, döngü her çalıştığında değil, yalnızca gerekli zamanda pahalı hesaplamaları yaptıkları anlamına gelir.

Başlamanız için bu yeterli olmalı. Sizin için bazı makaleler ve örnekler de bulacağım.

Bağlantı 1: Haskel - PHP (Özyineleme ve Yineleme)

İşte programcının PHP kullanarak büyük bir veri kümesini işlemesi gereken bir örnek. Haskel'de özyineleme kullanarak baş etmenin ne kadar kolay olacağını gösteriyor, ancak PHP'nin aynı yöntemi başarmanın kolay bir yolu olmadığından, sonucu elde etmek için yinelemeyi kullanmak zorunda kaldı.

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

Bağlantı 2: Özyinelemede Mastering

Özyinelemenin kötü şöhretinin çoğu, zorunlu dillerde yüksek maliyetlerden ve verimsizlikten kaynaklanmaktadır. Bu makalenin yazarı, yinelemeli algoritmaları daha hızlı ve daha verimli hale getirmek için nasıl optimize edeceğinizi anlatıyor. Ayrıca geleneksel bir döngüyü özyinelemeli bir işleve nasıl dönüştüreceğini ve kuyruk sonu özyinelemeyi kullanmanın faydalarını ele alır. Kapanış sözleri bence bazı önemli noktalarımı özetledi:

"özyinelemeli programlama, programcıya kodu hem sürdürülebilir hem de mantıksal olarak tutarlı bir şekilde düzenlemenin daha iyi bir yolunu verir."

https://developer.ibm.com/articles/l-recurs/

Bağlantı 3: Özyineleme döngüden daha hızlı mı? (Cevap)

İşte size benzer bir yığın akışı sorusunun yanıtının bağlantısı. Yazar, tekrarlama veya döngü ile ilişkili birçok ölçütün çok dile özgü olduğuna dikkat çekiyor . Zorunlu diller genellikle bir döngü kullanarak daha hızlıdır ve özyinelemeyle daha yavaştır ve işlevsel diller için bunun tersi de geçerlidir. Sanırım bu bağlantıdan alınacak asıl nokta, soruyu bir dil agnostik / durum kör anlamında cevaplamanın çok zor olduğudur.

Özyineleme döngüden daha hızlı mı?


4
Tornavida benzetmesini gerçekten beğendim
jh314


16

Her özyinelemeli çağrı genellikle yığına itilmek için bir bellek adresi gerektirdiğinden özyineleme bellekte daha maliyetlidir - böylece daha sonra program bu noktaya dönebilir.

Yine de, özyinelemenin ağaçlarla çalışırken olduğu gibi döngülerden çok daha doğal ve okunabilir olduğu birçok durum vardır. Bu durumlarda özyineye bağlı kalmanızı tavsiye ederim.


5
Tabii ki derleyiciniz Scala gibi kuyruk çağrılarını optimize etmez.
Ben Hardy

11

Tipik olarak, performans cezasının diğer yönde yatması beklenir. Yinelemeli çağrılar, ek yığın çerçevelerinin oluşturulmasına yol açabilir; bunun cezası değişir. Ayrıca, Python gibi bazı dillerde (daha doğru, bazı dillerin bazı uygulamalarında ...), bir ağaç veri yapısındaki maksimum değeri bulma gibi yinelemeli olarak belirleyebileceğiniz görevler için yığın sınırlarına kolayca girebilirsiniz. Bu durumlarda, gerçekten döngülere bağlı kalmak istersiniz.

İyi özyinelemeli işlevler yazmak, kuyruk yinelemelerini optimize eden bir derleyiciye sahip olduğunuzu varsayarak, performans cezasını bir miktar azaltabilir. üzerinde.)

"Edge" vakalarının yanı sıra (yüksek performanslı hesaplama, çok büyük özyineleme derinliği, vb.), Niyetinizi en açık şekilde ifade eden, iyi tasarlanmış ve bakımı kolay olan yaklaşımı benimsemek tercih edilir. Yalnızca bir ihtiyaç belirledikten sonra optimize edin.


8

Birden fazla , daha küçük parçalara ayrılabilen sorunlar için özyineleme yinelemeden daha iyidir .

Örneğin, özyinelemeli bir Fibonnaci algoritması yapmak için fib (n) 'i fib (n-1) ve fib (n-2) olarak parçalayıp her iki parçayı da hesaplarsınız. Yineleme yalnızca tek bir işlevi tekrar tekrar yapmanızı sağlar.

Ancak, Fibonacci aslında kırık bir örnek ve bence yineleme aslında daha verimli. Fib (n) = fib (n-1) + fib (n-2) ve fib (n-1) = fib (n-2) + fib (n-3) olduğuna dikkat edin. fib (n-1) iki kez hesaplanır!

Daha iyi bir örnek, bir ağaç için özyinelemeli algoritmadır. Ana düğümü analiz etme sorunu, her bir alt düğümü analiz etme konusunda çok daha küçük problemlere ayrılabilir . Fibonacci örneğinden farklı olarak, daha küçük problemler birbirinden bağımsızdır.

Yani evet - özyineleme, birden çok, daha küçük, bağımsız, benzer sorunlara ayrılabilen sorunlar için yinelemeden daha iyidir.


1
İki kez hesaplama aslında hatırlatma ile önlenebilir.
Siddhartha

7

Özyinelemeyi kullanırken performansınız kötüleşir, çünkü herhangi bir dilde bir yöntemi çağırmak çok fazla hazırlık gerektirir: çağrı kodu bir dönüş adresi gönderir, çağrı parametreleri, işlemci kayıtları gibi diğer bazı bağlam bilgileri bir yere kaydedilebilir ve dönüş zamanında çağrılan yöntem, daha sonra arayan tarafından alınan bir dönüş değeri gönderir ve önceden kaydedilmiş olan bağlam bilgileri geri yüklenir. performans yinelemeli ve yinelemeli bir yaklaşım arasında farklılık gösterir bu işlemlerin gerçekleştiği zaman.

Uygulama açısından bakıldığında, çağıran bağlamı işlemek için geçen süre, yönteminizin yürütülmesi için gereken zamanla karşılaştırılabilir olduğunda farkın farkına varırsınız. Özyinelemeli yönteminizin yürütülmesi daha sonra çağıran bağlam yönetimi bölümünden daha uzun sürerse, kod genellikle daha okunabilir ve anlaşılması kolay olduğundan ve performans kaybını fark etmeyeceğiniz için özyinelemeli yoldan devam edin. Aksi takdirde verimlilik nedenlerinden dolayı tekrarlanır.


Bu her zaman doğru değil. Özyineleme, kuyruk çağrısı optimizasyonunun yapılabileceği bazı durumlarda yineleme kadar verimli olabilir. stackoverflow.com/questions/310974/…
Sid Kshatriya

6

Java'da kuyruk özyinelemesinin şu anda optimize edilmediğine inanıyorum. Detaylar bu tartışma boyunca LtU ve ilgili bağlantılar üzerine serpilir. Bu olabilir yaklaşan sürümü 7'de bir özellik olabilir, ama görünüşe göre belirli çerçeveler nedeniyle ayrılarak Stack Muayene ile kombine bazı zorluklar vardır. Yığın Denetimi, Java 2'den beri hassas güvenlik modellerini uygulamak için kullanılmıştır.

http://lambda-the-ultimate.org/node/1333


Java için kuyruk özyinelemesini optimize eden JVM'ler vardır. ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

5

Yinelemeli yöntem üzerinde çok daha zarif bir çözüm verdiği birçok durum vardır, ortak örnek bir ikili ağacın geçişidir, bu yüzden bakımı daha zor değildir. Genel olarak, yinelemeli sürümler genellikle biraz daha hızlıdır (ve optimizasyon sırasında özyinelemeli bir sürümün yerini alabilir), ancak özyinelemeli sürümlerin doğru anlaşılması ve uygulanması daha kolaydır.


5

Özyineleme bazı durumlarda çok yararlıdır. Örneğin, faktöriyeli bulma kodunu düşünün

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

Şimdi yinelemeli işlevi kullanarak düşünün

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

Bu ikisini gözlemleyerek, özyinelemenin anlaşılmasının kolay olduğunu görebiliriz. Ancak dikkatle kullanılmazsa, çok fazla hata eğilimli olabilir. Kaçırırsak if (input == 0), kodun bir süre çalıştırılacağını ve genellikle bir yığın taşmasıyla biteceğini varsayalım .


6
Aslında yinelemeli versiyonu daha kolay anlıyorum. Sanırım her biri için.
Maksimum

@Maxpm, yüksek dereceli özyinelemeli bir çözüm çok daha iyidir: foldl (*) 1 [1..n]işte bu kadar.
SK-logic

5

Birçok durumda, önbelleğe alma nedeniyle özyineleme daha hızlıdır, bu da performansı artırır. Örneğin, geleneksel birleştirme rutini kullanarak birleştirme sıralamasının yinelemeli bir sürümü. Geliştirilmiş performansların önbelleğe alınması nedeniyle yinelemeli uygulamadan daha yavaş çalışacaktır.

Yinelemeli uygulama

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

Özyinelemeli uygulama

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS - Profesör Kevin Wayne (Princeton Üniversitesi) tarafından Coursera'da sunulan algoritmalar hakkında söylenen budur.


4

Özyineleme kullanarak, her bir "yineleme" ile bir işlev çağrısının maliyetine katlanırken, bir döngüde, genellikle ödediğiniz tek şey bir artış / azalıştır. Bu nedenle, döngü kodu özyinelemeli çözüm kodundan çok daha karmaşık değilse, döngü genellikle özyinelemeden daha üstün olacaktır.


1
Aslında, derlenmiş Scala kuyruk özyinelemeli işlev, onlara bakmak isterseniz, bayt kodundaki bir döngüye kadar kaynar (önerilir). İşlev çağrısı yükü yok. İkincisi, kuyruk yinelemeli fonksiyonlar, değişebilir değişkenler / yan etkiler veya açık döngüler gerektirmeme avantajına sahiptir ve doğruluğun kanıtlanması çok daha kolaydır.
Ben Hardy

4

Özyineleme ve yineleme uygulamak istediğiniz iş mantığına bağlıdır, ancak çoğu durumda birbirinin yerine kullanılabilir. Çoğu geliştirici özyinelemeye gider, çünkü anlaşılması daha kolaydır.


4

Dile bağlıdır. Java'da döngüler kullanmalısınız. İşlevsel diller özyinelemeyi optimize eder.


3

Sadece bir liste üzerinde yineleme yapıyorsanız, yineleyin.

Birkaç cevap daha (önce derinlik) ağaç dolaşımından bahsetmiştir. Gerçekten harika bir örnek, çünkü çok yaygın bir veri yapısında yapılması çok yaygın bir şey. Özyineleme bu sorun için son derece sezgiseldir.

"Bul" yöntemlerine buradan göz atın: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html


3

Özyineleme, yinelemenin olası herhangi bir tanımından daha basittir (ve dolayısıyla - daha temeldir). Turing-complete sistemini sadece bir çift ​​birleştiriciyle tanımlayabilirsiniz (evet, özyineleme bile böyle bir sistemde türevsel bir kavramdır). Lambda hesabı, özyinelemeli işlevlere sahip, eşit derecede güçlü bir temel sistemdir. Ancak bir yinelemeyi doğru bir şekilde tanımlamak istiyorsanız, başlamak için çok daha fazla ilkeliğe ihtiyacınız olacaktır.

Kod - gelince, özyinelemeli kodu anlamak ve sürdürmek aslında tamamen yinelemeli bir koddan çok daha kolaydır, çünkü çoğu veri yapısı özyinelemelidir. Tabii ki, doğru bir şekilde elde etmek için, en azından tüm standart birleştiricileri ve yineleyicileri düzgün bir şekilde elde etmek için yüksek dereceli işlevler ve kapanışları destekleyen bir dile ihtiyaç duyacaktır. C ++ 'da, elbette, FC ++ ve benzeri bir hardcore kullanıcı değilseniz, karmaşık özyinelemeli çözümler biraz çirkin görünebilir .


Özyinelemeli kodun izlenmesi son derece zor olabilir, özellikle parametrelerin sırası veya her özyinelemedeki türler değişirse. Yinelemeli kod çok basit ve açıklayıcı olabilir. Önemli olan, tekrarlanabilir veya yinelemeli olsun önce okunabilirliği (ve dolayısıyla güvenilirliği) kodlamak, ardından gerekirse optimize etmektir.
Marcus Clements

2

(Kuyruk olmayan) özyinelemede, işlev çağrıldığında (elbette dile bağlı olarak) yeni bir yığın vb. Ayırmak için bir performans isabet olacağını düşünürdüm.


2

"özyineleme derinliğine" bağlıdır. fonksiyon çağrısı ek yükünün toplam yürütme süresini ne kadar etkileyeceğine bağlıdır.

Örneğin, klasik faktöriyeli özyinelemeli bir şekilde hesaplamak aşağıdakilerden dolayı çok verimsizdir: - veri taşması riski - yığın taşması riski - fonksiyon çağrısı yükü yürütme süresinin% 80'ini işgal eder

satranç oyununda sonraki N hareketlerini analiz edecek pozisyon analizi için bir min-max algoritması geliştirilirken, "analiz derinliği" üzerinde özyineleme şeklinde uygulanabilir (^ _ ^ yaptığım gibi)


burada ugasoft ile tamamen katılıyorum ... özyineleme derinliğine ve yinelemeli uygulamanın karmaşıklığına bağlıdır ... ikisini karşılaştırmanız ve hangisinin daha verimli olduğunu görmeniz gerekir ... Böyle bir başparmak kuralı yoktur. ..
rajya vardhan

2

Özyineleme? Nereden başlayacağım, wiki size “bu, öğeleri benzer bir şekilde tekrarlama süreci” diyecektir.

Ben C yaptığım gün, C ++ özyineleme bir tanrı gönderme, "Kuyruk özyineleme" gibi şeyler oldu. Ayrıca özyineleme kullanan birçok sıralama algoritması bulacaksınız. Hızlı sıralama örneği: http://alienryderflex.com/quicksort/

Özyineleme, belirli bir sorun için yararlı olan diğer tüm algoritmalar gibidir. Belki hemen veya sık bir kullanım bulamayabilirsiniz, ancak mevcut olduğundan memnun olacağınız bir sorun olacaktır.


Derleyici optimizasyonunu geri aldığını düşünüyorum. Derleyiciler, yığın büyümesini önlemek için özyinelemeli işlevleri yinelemeli bir döngüye dönüştürecektir.
CoderDennis

Adil nokta, geriye dönüktü. Ancak bunun kuyruk özyineleme için hala geçerli olduğundan emin değilim.
Nickz

2

C ++ 'da, özyinelemeli işlev şablonlanmış bir işlevse, derleyici, tüm tür kesinti ve işlev örneklerinin derleme zamanında gerçekleşeceğinden, en iyi duruma getirme şansına sahiptir. Modern derleyiciler de mümkünse işlevi satır içi yapabilir. Dolayısıyla, biri gibi -O3veya -O2içinde optimizasyon bayrakları kullanırsa g++, özyinelemeler yinelemelerden daha hızlı olma şansına sahip olabilir. Yinelemeli kodlarda, derleyici zaten az ya da çok optimal durumda olduğu için (yeterince iyi yazılmışsa) onu optimize etme şansı daha az olur.

Benim durumumda, Armadillo matris nesnelerini kullanarak özyinelemeli ve yinelemeli bir şekilde kareler oluşturarak matriks üstellemeyi uygulamaya çalışıyordum. Algoritmayı burada bulabilirsiniz ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . İşlevlerim 1,000,000 12x12ayarlandı ve güce yükseltilen matrisleri hesapladım 10. Aşağıdaki sonucu aldım:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

Bu sonuçlar c ++ 11 bayrağıyla gcc-4.8 ( -std=c++11) ve Intel mkl'li Armadillo 6.1 kullanılarak elde edilmiştir. Intel derleyici de benzer sonuçlar gösteriyor.


1

Mike haklı. Kuyruk özyineleme Java derleyicisi veya JVM tarafından optimize edilmez . Her zaman böyle bir şeyle yığın taşması elde edersiniz:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}

3
Scala'da yazmadıkça ;-)
Ben Hardy

1

Çok derin özyineleme kullanarak, izin verilen yığın boyutuna bağlı olarak Yığın Taşması ile karşılaşacağınızı unutmayın. Bunu önlemek için, özyinelemeyi sona erdiren bazı temel durumlar sağladığınızdan emin olun.


1

Özyineleme, özyineleme kullanarak yazdığınız algoritmanın O (n) boşluk karmaşıklığına sahip olması dezavantajına sahiptir. Yinelemeli yaklaşımın uzay karmaşıklığı O (1) iken, yinelemede yinelemenin kullanılmasının avantajı budur. Öyleyse neden özyineleme kullanıyoruz?

Aşağıya bakınız.

Bazen yineleme kullanarak aynı algoritmayı yazmak biraz daha zorken özyineleme kullanarak bir algoritma yazmak daha kolaydır.Bu durumda yineleme yaklaşımını izlemeyi tercih ederseniz, yığını kendiniz işlemek zorunda kalırsınız.


1

Yinelemeler atomikse ve büyüklük sıraları yeni bir yığın çerçevesini itmek ve yeni bir iş parçacığı oluşturmaktan daha pahalıysa ve birden fazla çekirdeğe sahip ve sizin çalıştırma ortamı hepsini kullanabilirsiniz birleştirildiğinde daha sonra özyinelemeli yaklaşım çok büyük bir performans artışı elde verebilir çoklu kullanım. Ortalama yineleme sayısı öngörülebilir değilse, iş parçacığı tahsisini kontrol edecek ve işleminizin çok fazla iş parçacığı oluşturmasını ve sistemi dolaşmasını engelleyecek bir iş parçacığı havuzu kullanmak iyi bir fikir olabilir.

Örneğin, bazı dillerde, özyinelemeli çok iş parçacıklı birleştirme sıralama uygulamaları vardır.

Ancak yine, çoklu iş parçacığı özyineleme yerine döngü ile kullanılabilir, bu nedenle bu kombinasyonun ne kadar iyi çalışacağı, işletim sistemi ve iş parçacığı ayırma mekanizması dahil olmak üzere daha fazla faktöre bağlıdır.


0

Bildiğim kadarıyla, Perl kuyruk yinelemeli çağrıları optimize etmez, ancak sahte yapabilirsiniz.

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

İlk çağrıldığında yığın üzerinde yer ayıracaktır. Daha sonra, argümanlarını değiştirecek ve yığına başka bir şey eklemeden alt rutini yeniden başlatacaktır. Bu nedenle, asla kendini tekrarlamamış gibi davranarak onu yinelemeli bir sürece dönüştürecektir.

" my @_;" Veya " local @_;" olmadığını unutmayın, bunu yaparsanız artık çalışmaz.


0

Sadece Chrome 45.0.2454.85 m kullanıldığında, özyineleme daha hızlı bir şekilde iyi görünüyor.

İşte kod:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

SONUÇLAR

// 100 döngü için standart kullanarak çalışır

Döngü çalışması için 100x. Tamamlanma süresi: 7.683ms

// Kuyruk özyinelemeli fonksiyonel özyinelemeli yaklaşım kullanılarak 100 çalıştırma

100x özyineleme çalıştırması. Tamamlanma süresi: 4.841ms

Aşağıdaki ekran görüntüsünde, test başına 300 döngüde özyineleme tekrar daha büyük bir farkla kazanır

Tekrarlama tekrar kazanıyor!


Döngü işlevi içindeki işlevi çağırdığınız için test geçersizdir - bu, döngünün en önemli performans avantajlarından birini geçersiz kılar, bu da yönerge atlamalarının olmamasıdır (işlev çağrıları, yığın ataması, yığın patlaması vb. Dahil). Döngü içinde bir görev gerçekleştiriyor olsaydınız (sadece işlev olarak adlandırılmaz), yinelemeli işlev içinde görev gerçekleştirmeye karşı farklı sonuçlar elde edersiniz. (PS performansı, gerçek komut algoritmasının bir sorudur, burada bazen talimat atlamaları, onlardan kaçınmak için gerekli hesaplamalardan daha ucuzdur).
Myst

0

Bu yaklaşımlar arasında başka farklar buldum. Basit ve önemsiz görünüyor, ancak röportajlara hazırlanırken çok önemli bir rolü var ve bu konu ortaya çıkıyor, bu yüzden yakından bakın.

Kısacası: 1) yinelemeli post-order traversal kolay değildir - bu DFT'yi daha karmaşık hale getirir 2) döngüler özyineleme ile daha kolay kontrol edilir

Detaylar:

Özyinelemeli durumda, ön ve son geçişler oluşturmak kolaydır:

Oldukça standart bir soru düşünün: "görevler diğer görevlere bağlı olduğunda, görevi 5 yürütmek için gerçekleştirilmesi gereken tüm görevleri yazdırın"

Misal:

    //key-task, value-list of tasks the key task depends on
    //"adjacency map":
    Map<Integer, List<Integer>> tasksMap = new HashMap<>();
    tasksMap.put(0, new ArrayList<>());
    tasksMap.put(1, new ArrayList<>());

    List<Integer> t2 = new ArrayList<>();
    t2.add(0);
    t2.add(1);
    tasksMap.put(2, t2);

    List<Integer> t3 = new ArrayList<>();
    t3.add(2);
    t3.add(10);
    tasksMap.put(3, t3);

    List<Integer> t4 = new ArrayList<>();
    t4.add(3);
    tasksMap.put(4, t4);

    List<Integer> t5 = new ArrayList<>();
    t5.add(3);
    tasksMap.put(5, t5);

    tasksMap.put(6, new ArrayList<>());
    tasksMap.put(7, new ArrayList<>());

    List<Integer> t8 = new ArrayList<>();
    t8.add(5);
    tasksMap.put(8, t8);

    List<Integer> t9 = new ArrayList<>();
    t9.add(4);
    tasksMap.put(9, t9);

    tasksMap.put(10, new ArrayList<>());

    //task to analyze:
    int task = 5;


    List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
    System.out.println(res11);**//note, no reverse required**

    List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
    Collections.reverse(res12);//note reverse!
    System.out.println(res12);

    private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
         List<Integer> result = new ArrayList<>();
         Set<Integer> visited = new HashSet<>();
         reqPreOrder(tasksMap,task,result, visited);
         return result;
    }

private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {

    if(!visited.contains(task)) {
        visited.add(task);
        result.add(task);//pre order!
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPreOrder(tasksMap,child,result, visited);
            }
        }
    }
}

private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    reqPostOrder(tasksMap,task,result, visited);
    return result;
}

private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
    if(!visited.contains(task)) {
        visited.add(task);
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPostOrder(tasksMap,child,result, visited);
            }
        }
        result.add(task);//post order!
    }
}

Yinelenen post-order-traversal sonucun daha sonra tersine çevrilmesini gerektirmediğini unutmayın. Önce çocuklar, sorudaki göreviniz de en son yazdırılır. Herşey yolunda. Yinelenen bir ön sipariş geçişi (yukarıda da gösterilmektedir) yapabilirsiniz ve sonuç listesinin tersine çevrilmesi gerekir.

Yinelemeli yaklaşımla o kadar da basit değil! Yinelemeli (bir yığın) yaklaşımda yalnızca bir ön sipariş-geçiş yapabilirsiniz, böylece sonuç dizisini sonunda tersine çevirmek zorunda kalırsınız:

    List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
    Collections.reverse(res1);//note reverse!
    System.out.println(res1);

    private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    Stack<Integer> st = new Stack<>();


    st.add(task);
    visited.add(task);

    while(!st.isEmpty()){
        Integer node = st.pop();
        List<Integer> children = tasksMap.get(node);
        result.add(node);
        if(children!=null && children.size() > 0){
            for(Integer child:children){
                if(!visited.contains(child)){
                    st.add(child);
                    visited.add(child);
                }
            }
        }
        //If you put it here - it does not matter - it is anyway a pre-order
        //result.add(node);
    }
    return result;
}

Basit görünüyor, değil mi?

Ama bazı görüşmelerde bir tuzak var.

Bu şu anlama gelir: özyinelemeli yaklaşımla, önce Derinlik İlk Geçişini uygulayabilir ve daha sonra hangi siparişe veya ön siparişe ihtiyacınız olduğunu seçebilirsiniz (sadece "baskı listesine" konumunu değiştirerek, "sonuç listesine ekleme" ). Yinelemeli (bir yığın) yaklaşımı ile kolayca sadece ön siparişi çaprazlama yapabilirsiniz ve böylece çocukların ilk önce basılması gerektiği durumda (hemen hemen alt düğümlerden yazdırmaya başlamanız gerektiğinde, hemen hemen tüm durumlar) - siz bela. Bu sorunla karşılaşırsanız, daha sonra tersine çevirebilirsiniz, ancak algoritmanıza bir ek olacaktır. Ve bir görüşmeci saatine bakıyorsa, bu sizin için bir sorun olabilir. Yinelenen bir post-order traversal yapmanın karmaşık yolları vardır, bunlar vardır, ancak basit değildir . Misal:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/

Sonuç olarak, görüşmeler sırasında özyineleme kullanırdım, yönetmek ve açıklamak daha kolaydır. Herhangi bir acil durumda ön siparişten sonrasına geçiş için kolay bir yolunuz var. Yinelemeyle o kadar esnek değilsiniz.

Özyineleme kullanır ve daha sonra şunu söylerdim: "Tamam, ama yinelemeli kullanılan bellek üzerinde daha doğrudan kontrol sağlayabilir, yığın boyutunu kolayca ölçebilir ve bazı tehlikeli taşmalara izin vermezim."

Bir başka özyineleme artı - bir grafikte döngüleri önlemek / fark etmek daha kolaydır.

Örnek (ön kod):

dft(n){
    mark(n)
    for(child: n.children){
        if(marked(child)) 
            explode - cycle found!!!
        dft(child)
    }
    unmark(n)
}

0

Özyineleme veya bir uygulama olarak yazmak eğlenceli olabilir.

Ancak, kod üretimde kullanılacaksa, yığın taşması olasılığını dikkate almanız gerekir.

Kuyruk özyineleme optimizasyonu yığın taşmasını ortadan kaldırabilir, ancak bunu yapma sorununu yaşamak ister misiniz ve ortamınızda optimizasyona sahip olduğuna güvenebileceğinizden emin olmanız gerekir.

Algoritma her tekrarlandığında, veri boyutu ne kadar nazalır veya azaltılır?

Verilerin boyutunu veya nher yeniden alımınızda yarı yarıya azaltıyorsanız, genel olarak yığın taşması konusunda endişelenmenize gerek yoktur. Diyelim ki, programın taşmayı istiflemesi için 4.000 seviye derinlik veya 10.000 seviye derinlik olması gerekiyorsa, programınızın taşması istiflemesi için veri boyutunuzun kabaca 2 4000 olması gerekir. Bunu perspektife koymak için, son zamanlarda en büyük depolama cihazı 2 61 bayt tutabilir ve bu tür cihazların 2 61'i varsa, yalnızca 2 122 veri boyutu ile ilgilenirsiniz . Evrendeki tüm atomlara bakıyorsanız, bunun 2 84'ten az olabileceği tahmin edilmektedir.. Evrenin doğumunun 14 milyar yıl önce olduğu tahmin edilen her milisaniye boyunca evrendeki ve durumlarıyla ilgili tüm verileri ele almanız gerekiyorsa, bu sadece 2 153 olabilir . Dolayısıyla, programınız 2400 birim veri nişleyebiliyorsa veya evrendeki tüm verileri işleyebiliyorsanız, program taşmayı yığınlamaz. 2 4000 (4000 bit tam sayı) kadar büyük sayılarla uğraşmanız gerekmiyorsa, genel olarak yığın taşması konusunda endişelenmenize gerek yoktur.

Ancak, veri boyutunu veya nher yinelediğinizde sabit bir miktarda azaltırsanız, programınız ne zaman iyi çalışıyorsa n, 1000ancak bazı durumlarda nyalnızca ne zaman olursa yığın taşması ile karşılaşabilirsiniz 20000.

Bu nedenle, yığın taşması olasılığınız varsa, bunu yinelemeli bir çözüm haline getirmeye çalışın.


-1

Sorunuza, özyineleme için bir çeşit "ikili" olan "indüksiyon" ile bir Haskell veri yapısı tasarlayarak cevaplayacağım. Ve sonra bu dualitenin nasıl güzel şeylere yol açtığını göstereceğim.

Basit bir ağaç türü sunuyoruz:

data Tree a = Branch (Tree a) (Tree a)
            | Leaf a
            deriving (Eq)

Bu tanımı "Bir ağaç bir Daldır (iki ağaç içerir) veya bir yaprak (bir veri değeri içerir)" diyerek okuyabiliriz. Yani yaprak bir çeşit asgari durum. Bir ağaç yaprak değilse, iki ağaç içeren bileşik bir ağaç olmalıdır. Bunlar sadece vakalar.

Bir ağaç yapalım:

example :: Tree Int
example = Branch (Leaf 1) 
                 (Branch (Leaf 2) 
                         (Leaf 3))

Şimdi, ağaçtaki her bir değere 1 eklemek istediğimizi varsayalım. Bunu arayarak yapabiliriz:

addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a)     = Leaf (a + 1)

İlk olarak, bunun aslında özyinelemeli bir tanım olduğuna dikkat edin. Dal ve Yaprak veri yapıcılarını vaka olarak alır (ve Yaprak minimal olduğundan ve bunlar sadece olası durumlar olduğundan), fonksiyonun sona ereceğinden eminiz.

AddOne'u yinelemeli bir tarzda yazmak için ne gerekir? Rastgele sayıda şubeye dönüşmek neye benzeyecek?

Ayrıca, bu tür bir özyineleme "işlev" açısından çoğu zaman çarpanlarına ayrılabilir. Aşağıdakileri tanımlayarak Ağaçları Functors'a yapabiliriz:

instance Functor Tree where fmap f (Leaf a)     = Leaf (f a)
                            fmap f (Branch a b) = Branch (fmap f a) (fmap f b)

ve tanımlayan:

addOne' = fmap (+1)

Bir cebirsel veri türü için katamorfizma (veya katlama) gibi diğer özyineleme şemalarını hesaba katabiliriz. Bir katamorfizma kullanarak şunları yazabiliriz:

addOne'' = cata go where
           go (Leaf a) = Leaf (a + 1)
           go (Branch a b) = Branch a b

-2

Yığın taşması yalnızca yerleşik bellek yönetiminde olmayan bir dilde programlama yapıyorsanız gerçekleşir .... Aksi takdirde, işlevinizde (veya işlev çağrısında, STDLbs, vb.) Bir şey olduğundan emin olun. Özyineleme olmadan, Google veya SQL gibi bir şeye sahip olmak mümkün olmazdı veya büyük veri yapıları (sınıflar) veya veritabanları arasında verimli bir şekilde sıralanması gereken herhangi bir yer.

Özyineleme, dosyalar arasında yineleme yapmak istiyorsanız, bu şekilde 'bul * | ? grep * 'çalışır. Biraz ikili özyineleme, özellikle boru ile (ama başkalarının kullanması için dışarı koyacağınız bir şey varsa, birçokları gibi bir grup sistem çağrısı yapmayın).

Daha yüksek seviyeli diller ve hatta clang / cpp bunu arka planda da uygulayabilir.


1
"Yığın taşması yalnızca dahili bellek yönetiminde olmayan bir dilde programlama yapıyorsanız gerçekleşir" - bir anlam ifade etmiyor. Çoğu dil sınırlı boyutta yığın kullanır, bu nedenle özyineleme yakında başarısızlığa neden olur.
StaceyGirl
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.