Özyineleme nedir ve ne zaman kullanmalıyım?


121

Posta listelerinde ve çevrimiçi tartışmalarda düzenli olarak ortaya çıkan konulardan biri, Bilgisayar Bilimleri Derecesi yapmanın yararları (veya yokluğu). Negatif parti için defalarca gündeme gelen bir argüman, birkaç yıldır kod yazıyor olmaları ve asla özyineleme kullanmamış olmalarıdır.

Yani soru şu:

  1. Özyineleme nedir?
  2. Özyinelemeyi ne zaman kullanırım?
  3. İnsanlar neden özyineleme kullanmıyor?

9
Ve belki bu yardımcı olur: stackoverflow.com/questions/126756/…
kennytm

3
Bu, kavramı anlamaya yardımcı olabilir: bu sayfadaki sorunun ikinci yorumunda verilen bağlantıya gidin ve yorumların ne dediğini yapın: stackoverflow.com/questions/3021/…
dtmland

Yanıtlar:


86

Bu iş parçacığında özyinelemenin birkaç iyi açıklaması vardır , bu cevap onu neden çoğu dilde kullanmamanız gerektiğiyle ilgilidir. Ruby, Java ve C #) yineleme , özyinelemeye büyük ölçüde tercih edilir.

Nedenini görmek için, yukarıdaki dillerin bir işlevi çağırmak için kullandığı adımları izleyin:

  1. işlevin argümanları ve yerel değişkenleri için yığın üzerine boşluk oyulmuştur
  2. fonksiyonun argümanları bu yeni alana kopyalanır
  3. kontrol, işleve atlar
  4. işlevin kodu çalışır
  5. işlevin sonucu bir dönüş değerine kopyalanır
  6. yığın önceki konumuna geri sarılır
  7. denetim işlevin çağrıldığı yere geri döner

Tüm bu adımları uygulamak, genellikle bir döngü boyunca yinelemekten biraz daha fazla zaman alır. Ancak asıl sorun 1. adımda. Birçok program başladığında, yığınları için tek bir bellek parçası ayırırlar ve bu bellek bittiğinde (genellikle, ancak her zaman değil özyineleme nedeniyle) program, yığın taşması nedeniyle çöker .

Yani bu dillerde özyineleme daha yavaştır ve sizi çökmeye karşı savunmasız hale getirir. Yine de kullanmak için bazı argümanlar var. Genel olarak, yinelemeli olarak yazılan kod, nasıl okunacağını öğrendikten sonra daha kısa ve biraz daha zariftir.

Dil uygulayıcılarının, bazı yığın taşması sınıflarını ortadan kaldırabilen kuyruk çağrısı optimizasyonu adı verilen bir teknik vardır . Kısaca söylemek gerekirse: Eğer bir fonksiyonun dönüş ifadesi sadece bir fonksiyon çağrısının sonucuysa, yığına yeni bir seviye eklemenize gerek yoktur, çağrılan fonksiyon için mevcut olanı yeniden kullanabilirsiniz. Ne yazık ki, birkaç zorunlu dil uygulamasında yerleşik kuyruk arama optimizasyonu vardır.

* Özyinelemeyi seviyorum. En sevdiğim statik dil döngüleri hiç kullanmıyor, bir şeyi tekrar tekrar yapmanın tek yolu özyineleme. Ben sadece, özyinelemenin genellikle ona göre ayarlanmayan dillerde iyi bir fikir olduğunu düşünmüyorum.

** Bu arada Mario, ArrangeString işlevinizin tipik adı "join" ve seçtiğiniz dilin zaten bir uygulaması yoksa şaşırırım.


1
Özyinelemenin doğasında var olan ek yükün bir açıklamasını görmek güzel. Buna cevabımda da değindim. Ama bana göre, özyinelemenin en büyük gücü, çağrı yığınıyla yapabilecekleridir. Tekrar tekrar dallanan özyinelemeli kısa bir algoritma yazabilir, böylece hiyerarşileri (üst / alt ilişkileri) tarama gibi şeyleri kolaylıkla yapmanızı sağlar. Bir örnek için cevabıma bakın.
Steve Wortham

7
"Özyineleme nedir ve ne zaman kullanmalıyım?" Başlıklı sorunun en iyi cevabını bulmak beni çok hayal kırıklığına uğrattı. değil aslında ya olanların cevap Bahsettiğiniz dillerin çoğunda yaygın kullanımına rağmen, özyineleme karşı son derece önyargı uyarı aldırma (orada Söylediklerini özellikle yanlış bir şey değildir, ama sorunu ve underexaggerating abarttığını gibi görünüyor kullanışlılık).
Bernhard Barker

2
Muhtemelen haklısın @Dukeling. Bağlam için, bu cevabı yazdığımda, zaten yazılmış olan pek çok büyük özyineleme açıklaması vardı ve bunu, en üstteki cevap değil, o bilgiye ek olarak yazdım. Pratikte, bir ağaçta yürümem veya başka herhangi bir iç içe geçmiş veri yapısını işlemem gerektiğinde, genellikle özyinelemeye dönüyorum ve henüz vahşi doğada kendi oluşturduğum yığın taşmasına ulaşmadım.
Peter Burns

63

Özyinelemenin basit İngilizce örneği.

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
kalp dokunmak için yukarı + :)
Suhail Mumtaz Awan

Çin halk masallarında uyuya kalmayan küçük çocuklar için de buna benzer bir hikaye var, bunu yeni hatırladım ve bana gerçek dünyadaki özyinelemenin nasıl çalıştığını hatırlatıyor.
Harvey Lin

49

En temel bilgisayar bilimi anlamında özyineleme, kendisini çağıran bir işlevdir. Bağlantılı bir liste yapınız olduğunu varsayalım:

struct Node {
    Node* next;
};

Ve bağlantılı bir listenin ne kadar uzun olduğunu öğrenmek istiyorsanız, bunu özyineleme ile yapabilirsiniz:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Bu elbette bir for döngüsü ile de yapılabilir, ancak konseptin bir örneği olarak kullanışlıdır)


@Christopher: Bu güzel, basit bir özyineleme örneği. Özellikle bu, bir kuyruk özyineleme örneğidir. Bununla birlikte, Andreas'ın da belirttiği gibi, bir for döngüsü ile kolayca yeniden yazılabilir (daha verimli bir şekilde). Cevabımda açıkladığım gibi, özyineleme için daha iyi kullanımlar var.
Steve Wortham

2
Burada gerçekten başka bir ifadeye ihtiyacınız var mı?
Adrien Be

1
Hayır, sadece açıklık için orada.
Andreas Brinck

@SteveWortham: Bu, yazıldığı gibi kuyruk özyinelemeli değildir; ikincisinin sonuca 1 ekleyebilmesi length(list->next)için yine de dönmesi gerekiyor length(list). Şimdiye kadarki uzunluğu geçmek için yazılmış olsaydı, ancak o zaman arayanın var olduğunu unutabilirdik. Beğen int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

Ne zaman bir işlev kendisini çağırsa, bir döngü yaratır, o zaman bu özyinelemedir. Her şeyde olduğu gibi, özyineleme için iyi ve kötü kullanımlar vardır.

En basit örnek, işlevin en son satırının kendisine yapılan bir çağrı olduğu kuyruk özyinelemesidir:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

Bununla birlikte, bu topal, neredeyse anlamsız bir örnektir çünkü daha verimli bir yineleme ile kolayca değiştirilebilir. Sonuçta, özyineleme, yukarıdaki örnekte işlevin kendi içindeki işlemle karşılaştırıldığında önemli olabilecek işlev çağrısı ek yükünden muzdariptir.

Dolayısıyla, yinelemeden ziyade özyineleme yapmanın tüm nedeni, bazı zekice şeyler yapmak için çağrı yığınından yararlanmak olmalıdır . Örneğin, aynı döngü içinde farklı parametrelere sahip bir işlevi birden çok kez çağırırsanız, bu, dallanma gerçekleştirmenin bir yoludur . Klasik bir örnek, Sierpinski üçgenidir .

görüntü açıklamasını buraya girin

Çağrı yığınının 3 yönde dallandığı yineleme ile bunlardan birini çok basit bir şekilde çizebilirsiniz:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

Yinelemeyle aynı şeyi yapmaya çalışırsanız, başarmak için çok daha fazla kod gerektiğini göreceksiniz.

Diğer yaygın kullanım örnekleri, web sitesi tarayıcıları, dizin karşılaştırmaları vb. Gibi geçiş hiyerarşilerini içerebilir.

Sonuç

Pratik anlamda, yinelemeli dallanmaya ihtiyaç duyduğunuzda özyineleme en mantıklıdır.


27

Özyineleme, böl ve fethet mantığına dayalı sorunları çözme yöntemidir. Temel fikir, orijinal problemi alıp kendi kendisinin daha küçük (daha kolay çözülen) örneklerine bölmeniz, bu küçük örnekleri (genellikle aynı algoritmayı tekrar kullanarak) çözmeniz ve ardından bunları nihai çözüme dönüştürmenizdir.

Kanonik örnek, n'nin Faktöriyelini oluşturmak için bir rutindir. N faktöriyeli, 1 ile n arasındaki tüm sayıların çarpılmasıyla hesaplanır. C #'daki yinelemeli çözüm şuna benzer:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

Yinelemeli çözümle ilgili şaşırtıcı hiçbir şey yoktur ve C # ile aşina olan herkes için mantıklı olmalıdır.

Özyinelemeli çözüm, n'inci Faktörün n * Gerçek (n-1) olduğu kabul edilerek bulunur. Ya da başka bir deyişle, belirli bir Faktör sayısının ne olduğunu biliyorsanız, bir sonrakini hesaplayabilirsiniz. İşte C #'daki özyinelemeli çözüm:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

Bu işlevin ilk bölümü Temel Durum (veya bazen Koruma İfadesi) olarak bilinir ve algoritmanın sonsuza kadar çalışmasını engelleyen şey budur. İşlev 1 veya daha küçük bir değerle çağrıldığında yalnızca 1 değerini döndürür. İkinci bölüm daha ilginçtir ve Yinelemeli Adım olarak bilinir . Burada aynı yöntemi biraz değiştirilmiş bir parametre ile çağırıyoruz (1 ile azaltıyoruz) ve sonra sonucu n kopyamızla çarpıyoruz.

İlk karşılaşıldığında bu biraz kafa karıştırıcı olabilir, bu nedenle çalıştırıldığında nasıl çalıştığını incelemek öğreticidir. FactRec (5) dediğimizi hayal edin. Rutine giriyoruz, temel durum tarafından yakalanmıyoruz ve bu yüzden şu şekilde sonlandırıyoruz:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

Yönteme parametre 4 ile tekrar girersek, yine koruma cümlesiyle durdurulmayız ve bu nedenle sonuçta:

// In FactRec(4)
return 4 * FactRec(3);

Bu dönüş değerini yukarıdaki dönüş değerine koyarsak,

// In FactRec(5)
return 5 * (4 * FactRec(3));

Bu size nihai çözüme nasıl ulaşıldığına dair bir ipucu vermeli, böylece hızlı bir şekilde her adımı takip edip göstereceğiz:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

Bu son değişiklik, temel durum tetiklendiğinde gerçekleşir. Bu noktada, çözmemiz gereken basit bir cebirsel formül var ve bu da doğrudan Faktör tanımına eşittir.

Yönteme yapılan her çağrının ya bir temel durumun tetiklenmesi ya da parametrelerin temel duruma daha yakın olduğu aynı yönteme (genellikle özyinelemeli çağrı olarak adlandırılır) bir çağrı ile sonuçlandığını not etmek öğreticidir. Durum böyle değilse, yöntem sonsuza kadar çalışacaktır.


2
İyi bir açıklama, ancak bunun sadece kuyruk özyinelemesi olduğunu ve yinelemeli çözüme göre hiçbir avantajı olmadığını belirtmek önemli. Kabaca aynı miktarda koddur ve fonksiyon çağrısı ek yükü nedeniyle daha yavaş çalışacaktır.
Steve Wortham

1
@SteveWortham: Bu kuyruk özyineleme değil. Özyinelemeli adımda, sonucu dönmeden önce FactRec()ile çarpılmalıdır n.
rvighne

12

Özyineleme, kendisini çağıran bir işlevle bir sorunu çözmektir. Bunun güzel bir örneği, faktöryel bir fonksiyondur. Faktöriyel, örneğin 5'in faktöriyelinin 5 * 4 * 3 * 2 * 1 olduğu bir matematik problemidir. Bu fonksiyon, bunu pozitif tamsayılar için C # 'da çözer (test edilmemiştir - bir hata olabilir).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

Özyineleme, problemin daha küçük bir versiyonunu çözerek ve ardından bu sonucu ve orijinal probleme cevabı formüle etmek için başka bir hesaplamayı kullanarak problemi çözen bir metodu ifade eder. Çoğu zaman, daha küçük versiyonu çözme sürecinde, yöntem problemin daha küçük bir versiyonunu çözecektir ve çözülmesi önemsiz olan bir "temel duruma" ulaşana kadar bu şekilde devam eder.

Örneğin, sayı için bir faktöriyel hesaplamak için X, kişi onu olarak temsil edebilir X times the factorial of X-1. Böylece, yöntem faktöriyelini bulmak için "yinelenir" X-1ve daha sonra Xson bir cevabı vermek için ne olursa olsun çarparak çoğalır . Elbette, faktöriyelini bulmak için X-1önce faktöriyelini hesaplayacak X-2ve bu böyle devam edecek. Temel durum X0 veya 1 olduğunda olur, bu durumda o 1zamandan beri döneceğini bilir 0! = 1! = 1.


1
Hakaret ettiğiniz şeyin özyineleme değil, <a href=" en.wikipedia.org/wiki/… ve Conquer</a> algoritma tasarım ilkesi olduğunu düşünüyorum. Örneğin <a href = " en.wikipedia'ya bakın. org / wiki / Ackermann_function "> Ackermans işlevi </a>.
Gabriel Ščerbák

2
Hayır, D & C'den bahsetmiyorum. D&C, 2 veya daha fazla alt problemin var olduğunu ima eder, özyineleme kendi başına yoktur (örneğin, burada verilen faktöriyel örnek D&C değildir - tamamen doğrusaldır). D&C, esasen özyinelemenin bir alt kümesidir.
Amber

3
Tam olarak bağladığınız makaleden alıntı yapıldı: "Böl ve ele geçir algoritması, bir problemi aynı (veya ilgili) türden iki veya daha fazla alt probleme tekrar tekrar ayırarak çalışır "
Amber

Bunun harika bir açıklama olduğunu sanmıyorum, çünkü özyinelemenin kesinlikle sorunu çözmesi gerekmiyor. Kendinize sadece (Ve taşma) diyebilirsiniz.
UK-AL

Açıklamanızı PHP Master için yazdığım bir makalede kullanıyorum, ancak size atfedemiyorum. Umarım aldırmazsın
donma harikası

9

Eski, iyi bilinen bir sorunu düşünün :

Matematikte, iki veya daha fazla sıfır olmayan tam sayının en büyük ortak bölencisi (gcd)… sayıları kalansız bölen en büyük pozitif tam sayıdır.

Gcd'nin tanımı şaşırtıcı derecede basittir:

gcd tanımı

burada mod, modulo operatörüdür (yani, tamsayı bölmesinden sonra kalan).

İngilizcede bu tanım herhangi bir sayının en büyük ortak böleni diyor ve sıfır bu sayı ve iki sayının en büyük ortak böleni olan m ve n en büyük ortak böleni olan n ve bölünmesi sonucu kalan m tarafından n .

Bunun neden işe yaradığını öğrenmek istiyorsanız, Öklid algoritması hakkındaki Wikipedia makalesine bakın .

Örnek olarak gcd (10, 8) 'i hesaplayalım. Her adım, kendisinden hemen önceki adıma eşittir:

  1. gcd (10; 8)
  2. gcd (10; 10 mod 8)
  3. gcd (8; 2)
  4. gcd (8, 8 mod 2)
  5. gcd (2, 0)
  6. 2

İlk adımda 8 sıfıra eşit olmadığından tanımın ikinci kısmı uygulanır. 10 mod 8 = 2, çünkü 8, 2'nin kalanıyla bir kez 10'a giriyor. 3. adımda, ikinci kısım tekrar uygulanır, ancak bu sefer 8 mod 2 = 0 çünkü 2, 8'i hiç kalmadan böler. 5. adımda, ikinci bağımsız değişken 0'dır, dolayısıyla cevap 2'dir.

Gcd'nin eşittir işaretinin hem sol hem de sağ tarafında göründüğünü fark ettiniz mi? Bir matematikçi bu tanımın yinelemeli olduğunu söyler çünkü tanımladığınız ifade , tanımının içinde yinelenir .

Özyinelemeli tanımlar zarif olma eğilimindedir. Örneğin, bir listenin toplamı için özyinelemeli bir tanım,

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

headbir listedeki ilk öğe nerede ve listenin tailgeri kalanı. Not sumsonunda tanımı içinde tekrarlanır.

Belki bunun yerine bir listedeki maksimum değeri tercih edersiniz:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

Negatif olmayan tamsayıların çarpımını bir dizi eklemeye dönüştürmek için yinelemeli olarak tanımlayabilirsiniz:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

Çarpmayı bir dizi eklemeye dönüştürmekle ilgili bu kısım bir anlam ifade etmiyorsa, nasıl çalıştığını görmek için birkaç basit örneği genişletmeyi deneyin.

Birleştirme sıralaması hoş bir yinelemeli tanıma sahiptir:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

Ne arayacağınızı biliyorsanız, yinelemeli tanımlar her yerde. Tüm bu tanımların nasıl çok basit temel durumlara sahip olduğuna dikkat edin, örneğin gcd (m, 0) = m. Özyinelemeli vakalar, kolay cevaplara inmek için problemi ortadan kaldırır.

Bu anlayışla, Wikipedia'nın özyineleme hakkındaki makalesindeki diğer algoritmaları artık takdir edebilirsiniz !


8
  1. Kendini çağıran bir işlev
  2. Bir işlev (kolayca) basit bir işlem artı sorunun daha küçük bir bölümünde aynı işlev olarak ayrıştırılabildiğinde. Daha ziyade, bunun onu yineleme için iyi bir aday yaptığını söylemeliyim.
  3. Onlar yapar!

Kanonik örnek, aşağıdaki gibi görünen faktöryel örnektir:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

Genel olarak, özyineleme mutlaka hızlı değildir (işlev çağrısı ek yükü yüksek olma eğilimindedir çünkü özyinelemeli işlevler küçük olma eğilimindedir, yukarıya bakın) ve bazı sorunlardan muzdarip olabilir (yığın taşması var mı?). Bazıları önemsiz olmayan durumlarda 'doğru' olmanın zor olduğunu söylüyor ama ben buna gerçekten inanmıyorum. Bazı durumlarda, özyineleme en mantıklı olanıdır ve belirli bir işlevi yazmanın en zarif ve açık yoludur. Bazı dillerin yinelemeli çözümleri tercih ettiği ve bunları çok daha fazla optimize ettiği unutulmamalıdır (LISP akla gelir).


6

Özyinelemeli işlev, kendisini çağıran işlevdir. Kullanmamın en yaygın nedeni, bir ağaç yapısını geçmektir. Örneğin, onay kutuları olan bir TreeView varsa (yeni bir programın kurulumunu düşünün, "yüklenecek özellikleri seçin" sayfasını düşünün), şöyle bir "tümünü kontrol et" düğmesi isteyebilirim (sözde kod):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

Böylece checkRecursively ilk önce geçirildiği düğümü kontrol ettiğini, ardından bu düğümün her bir çocuğu için kendisini çağırdığını görebilirsiniz.

Özyineleme konusunda biraz dikkatli olmanız gerekir. Sonsuz özyinelemeli bir döngüye girerseniz, bir Yığın Taşması istisnası alırsınız :)

Uygun olduğunda insanların onu kullanmaması için bir neden düşünemiyorum. Bazı durumlarda yararlıdır, bazılarında değildir.

Bence bu ilginç bir teknik olduğu için, bazı kodlayıcılar bunu gerçekten gerekçe göstermeden kullanmaları gerekenden daha sık kullanıyorlar. Bu, bazı çevrelerde özyinelemeye kötü bir isim verdi.


5

Özyineleme, doğrudan veya dolaylı olarak kendisine atıfta bulunan bir ifadedir.

Özyinelemeli kısaltmaları basit bir örnek olarak düşünün:

  • GNU , GNU'nun Unix Değildir anlamına gelir
  • PHP , PHP'nin kısaltmasıdır : Hypertext Preprocessor
  • YAML , YAML Ain't Markup Language anlamına gelir
  • ŞARAP , Şarap Emülatör Değildir anlamına gelir
  • VISA , Visa International Service Association'ın kısaltmasıdır

Wikipedia'da daha fazla örnek


4

Özyineleme, benim "fraktal problemler" olarak adlandırdığım şeyle en iyi şekilde işe yarar, burada büyük şeyin daha küçük versiyonlarından oluşan büyük bir şeyle uğraşıyorsunuz, her biri büyük şeyin daha da küçük bir versiyonu, vb. Bir ağaç veya iç içe geçmiş benzer yapılar gibi bir şeyi çaprazlamanız veya aramanız gerekirse, özyineleme için iyi bir aday olabilecek bir probleminiz var demektir.

İnsanlar birkaç nedenden dolayı özyinelemeden kaçınırlar:

  1. Çoğu insan (ben de dahil), işlevsel programlamanın aksine, prosedürel veya nesne yönelimli programlamayla programlama dişlerini keser. Bu tür insanlar için yinelemeli yaklaşım (tipik olarak döngüler kullanarak) daha doğal geliyor.

  2. Prosedürel veya nesne yönelimli programlamada programlama dişlerimizi kesenlere, hataya açık olduğu için sık sık yinelemeden kaçınmamız söylenir.

  3. Sık sık yinelemenin yavaş olduğu söylenir. Bir rutinden tekrar tekrar arama ve geri dönüş, döngüden daha yavaş olan çok sayıda yığın itme ve fırlatma gerektirir. Bence bazı diller bunu diğerlerinden daha iyi ele alıyor ve bu diller büyük olasılıkla baskın paradigmanın yöntemsel veya nesne yönelimli olduğu diller değil.

  4. Kullandığım en azından birkaç programlama dili için, belirli bir derinliğin ötesine geçerse özyinelemeyi kullanmama tavsiyelerini duyduğumu hatırlıyorum çünkü yığını o kadar derin değil.


4

Özyinelemeli bir ifade, bir sonraki adımda ne yapılacağını, girdilerin ve zaten yaptıklarınızın bir kombinasyonu olarak tanımladığınız bir ifadedir.

Örneğin, faktöriyel alın:

factorial(6) = 6*5*4*3*2*1

Ancak faktöryel (6) 'nın da görülmesi kolaydır:

6 * factorial(5) = 6*(5*4*3*2*1).

Yani genel olarak:

factorial(n) = n*factorial(n-1)

Elbette, özyinelemeyle ilgili zor olan şey, bir şeyleri zaten yaptığınız şeylere göre tanımlamak istiyorsanız, başlamak için bir yer olması gerektiğidir.

Bu örnekte, faktöriyel (1) = 1'i tanımlayarak özel bir durum oluşturuyoruz.

Şimdi aşağıdan yukarıya görüyoruz:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

Faktöriyel (1) = 1 olarak tanımladığımız için "en alta" ulaşıyoruz.

Genel olarak konuşursak, yinelemeli prosedürler iki bölümden oluşur:

1) Bazı prosedürü, aynı prosedür aracılığıyla "zaten yaptığınız" şeyle birleştirilen yeni girdiler açısından tanımlayan yinelemeli kısım. (yani factorial(n) = n*factorial(n-1))

2) Başlamak için bir yer vererek sürecin sonsuza kadar tekrarlanmamasını sağlayan bir temel parça (yani factorial(1) = 1)

İlk başta kafanızı karıştırmak biraz kafa karıştırıcı olabilir, ancak sadece birkaç örneğe bakın ve hepsi bir araya gelmelidir. Kavramı daha derinlemesine anlamak istiyorsanız, matematiksel tümevarımı çalışın. Ayrıca, bazı dillerin yinelemeli aramalar için optimize ederken bazılarının optimize etmediğini unutmayın. Dikkatli değilseniz, çılgınca yavaş özyinelemeli işlevler yapmak oldukça kolaydır, ancak çoğu durumda bunları performansa dönüştüren teknikler de vardır.

Bu yardımcı olur umarım...


4

Bu tanımı beğendim:
Özyinelemede, bir rutin, bir problemin küçük bir bölümünü kendi başına çözer, problemi daha küçük parçalara böler ve daha sonra küçük parçaların her birini çözmeye çağırır.

Steve McConnell'ın, Bilgisayar Bilimi'nin Özyineleme üzerine kitaplarında kullanılan örnekleri eleştirdiği Code Complete'deki özyineleme tartışmasını da seviyorum.

Faktöriyeller veya Fibonacci sayıları için özyineleme kullanmayın

Bilgisayar bilimi ders kitaplarıyla ilgili bir sorun, saçma yineleme örnekleri sunmalarıdır. Tipik örnekler, bir faktöryel veya bir Fibonacci dizisini hesaplamaktır. Özyineleme güçlü bir araçtır ve bu iki durumda da onu kullanmak gerçekten aptalca. Benim için çalışan bir programcı bir faktöryel hesaplamak için özyinelemeyi kullanırsa, başka birini işe alırdım.

Bunun dile getirilmesi gereken çok ilginç bir nokta olduğunu ve yinelemenin genellikle yanlış anlaşılmasının bir nedeni olabileceğini düşündüm.

DÜZENLEME: Bu Dav'un ​​cevabında bir kazı değildi - bunu gönderirken bu cevabı görmemiştim


6
Faktöryellerin veya fibonacci dizilerinin örnek olarak kullanılmasının nedenlerinin çoğu, özyinelemeli bir şekilde tanımlanan ortak öğeler olmaları ve bu nedenle kendilerini hesaplamak için özyineleme örneklerine doğal olarak borç vermeleridir - aslında en iyi yöntem bu olmasa bile CS açısından.
Amber

Katılıyorum - kitabı okurken, özyineleme ile ilgili bir bölümün ortasında
gündeme getirmenin

4

1.) Bir yöntem kendini çağırabiliyorsa özyinelemelidir; ya doğrudan:

void f() {
   ... f() ... 
}

veya dolaylı olarak:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.) Özyineleme ne zaman kullanılır?

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.) İnsanlar özyinelemeyi yalnızca yinelemeli kod yazmak çok karmaşık olduğunda kullanır. Örneğin, ön sipariş, postorder gibi ağaç çaprazlama teknikleri hem yinelemeli hem de yinelemeli yapılabilir. Ama genellikle basitliği nedeniyle özyinelemeli kullanırız.


Performanslarla ilgili bölme ve fethetme sırasında karmaşıklığı azaltmaya ne dersiniz?
mfrachet

4

İşte basit bir örnek: Bir kümede kaç öğe var. (bir şeyleri saymanın daha iyi yolları vardır, ancak bu güzel, basit, özyinelemeli bir örnek.)

İlk önce iki kurala ihtiyacımız var:

  1. küme boşsa, kümedeki öğe sayısı sıfırdır (ha!).
  2. set boş değilse, sayı bir artı bir öğe kaldırıldıktan sonra setteki öğe sayısıdır.

Bunun gibi bir kümeniz olduğunu varsayalım: [xxx]. kaç tane öğe olduğunu sayalım.

  1. küme boş olmayan [xxx], bu nedenle 2. kuralı uyguluyoruz. öğe sayısı bir artı [xx] içindeki öğelerin sayısıdır (yani bir öğeyi kaldırdık).
  2. küme [xx], dolayısıyla 2. kuralı tekrar uyguluyoruz: [x] 'deki bir + öğe sayısı.
  3. küme [x], kural 2: bir + [] içindeki öğe sayısı ile hala eşleşiyor.
  4. Şimdi küme, kural 1 ile eşleşen [] olur: sayı sıfırdır!
  5. Artık 4. adımdaki (0) cevabı bildiğimize göre, 3. adımı (1 + 0) çözebiliriz
  6. Aynı şekilde, 3. (1). Adımdaki cevabı bildiğimize göre, 2. adımı (1 + 1) çözebiliriz.
  7. Son olarak, adım 2 (2) 'deki cevabı bildiğimize göre, 1. adımı (1 + 2) çözebilir ve 3 olan [xxx]' deki öğelerin sayısını alabiliriz. Yaşasın!

Bunu şu şekilde ifade edebiliriz:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

Özyinelemeli bir çözüm uygularken, genellikle en az 2 kuralınız olur:

  • temel, tüm verilerinizi "kullandığınızda" ne olacağını belirten basit durum. Bu genellikle "işlenecek veriniz yoksa cevabınız X" ifadesinin bir çeşitlemesidir.
  • hala verileriniz varsa ne olacağını belirten yinelemeli kural. Bu genellikle "veri kümenizi küçültmek için bir şeyler yapın ve kurallarınızı daha küçük veri kümesine yeniden uygulayın" diyen bir tür kuraldır.

Yukarıdakileri sözde koda çevirirsek, şunu elde ederiz:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

Diğer insanların da ele alacağından emin olduğum çok daha yararlı örnekler (örneğin bir ağacın üzerinden geçmek) var.


3

Bu oldukça iyi bir tanımınız var. Wikipedia'nın da iyi bir tanımı var. Bu yüzden sizin için başka (muhtemelen daha kötü) bir tanım ekleyeceğim.

İnsanlar "özyinelemeden" bahsettiklerinde, genellikle yazdıkları ve işini bitirene kadar kendisini tekrar tekrar çağıran bir işlevden bahsederler. Veri yapılarındaki hiyerarşileri geçerken özyineleme yardımcı olabilir.


3

Bir örnek: Bir merdivenin özyinelemeli tanımı şöyledir: Bir merdiven şunlardan oluşur: - tek bir basamak ve bir merdiven (özyineleme) - veya sadece tek bir basamak (sonlandırma)


2

Çözülmüş bir sorunu tekrarlamak için: hiçbir şey yapmayın, işiniz bitti.
Açık bir sorunu tekrarlamak için: bir sonraki adımı yapın, sonra geri kalanını tekrarlayın.


2

Düz İngilizce: 3 şeyi yapabileceğinizi varsayın:

  1. Bir elma al
  2. Çetele işaretlerini yazın
  3. Çetele işaretlerini say

Önünüzde bir masada çok fazla elma var ve kaç tane elma olduğunu bilmek istiyorsunuz.

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

Tamamlayana kadar aynı şeyi tekrar etme sürecine özyineleme denir.

Umarım aradığınız "sade İngilizce" cevabı budur!


1
Bekle, bir masada önümde çok sayıda çetele işareti var ve şimdi kaç puan işareti olduğunu bilmek istiyorum. Elmaları bunun için bir şekilde kullanabilir miyim?
Christoffer Hammarström

Yerden bir elma alırsanız (işlem sırasında oraya koyduğunuzda) ve listenin bir çetel işaretini her kaşıdığınızda, hiç puan kalmayana kadar masaya yerleştirirseniz, eminim ki sizden masada sahip olduğunuz taksitli notların sayısına eşit miktarda elma ile sonuçlanır. Şimdi anında başarı için bu elmaları sayın! (not: bu işlem artık yineleme değil, sonsuz bir döngüdür)
Bastiaan Linders

2

Özyinelemeli işlev, kendisine bir çağrı içeren bir işlevdir. Özyinelemeli yapı, kendisinin bir örneğini içeren bir yapıdır. İkisini özyinelemeli bir sınıf olarak birleştirebilirsiniz. Özyinelemeli bir öğenin anahtar kısmı, kendisinin bir örneğini / çağrısını içermesidir.

Birbirine bakan iki aynayı düşünün. Yaptıkları temiz sonsuzluk etkisini gördük. Her bir yansıma, bir aynanın başka bir örneğinde bulunan bir aynanın bir örneğidir. Kendisinin bir yansımasını içeren ayna özyinelemedir.

Bir ikili arama ağacı özyineleme iyi bir programlama örneğidir. Yapı, bir Düğümün 2 örneğini içeren her Düğümle özyinelemelidir. İkili arama ağacında çalışmak için işlevler de özyinelemelidir.


2

Bu eski bir sorudur, ancak lojistik açıdan bir yanıt eklemek istiyorum (yani algoritma doğruluğu açısından veya performans açısından değil).

Java'yı iş için kullanıyorum ve Java iç içe geçmiş işlevi desteklemiyor. Bu nedenle, yineleme yapmak istersem, harici bir işlev tanımlamam gerekebilir (bu, yalnızca kodum Java'nın bürokratik kuralına aykırı olduğu için var olur) veya kodu tamamen yeniden düzenlemem gerekebilir (ki bunu yapmaktan gerçekten nefret ederim).

Bu nedenle, genellikle özyinelemeden kaçınırım ve bunun yerine yığın işlemini kullanırım, çünkü özyinelemenin kendisi aslında bir yığın işlemi.


1

Bir ağaç yapınız olduğunda onu kullanmak istersiniz. XML okumada çok kullanışlıdır.


1

Programlamaya uygulandığı şekliyle özyineleme, temelde, bir görevi gerçekleştirmek için farklı parametrelerle kendi tanımının içinden (kendi içinden) bir işlevi çağırmaktır.


1

"Çekiçim varsa, her şeyi çivi gibi göster."

Özyineleme, her adımda sadece "2 küçük şeyi daha büyük bir şeye dönüştüren", her seferinde aynı çekiçle yapılan büyük sorunlar için bir problem çözme stratejisidir .

Misal

Masanızın düzensiz 1024 kağıtla kaplı olduğunu varsayalım. Özyineleme kullanarak, dağınıklıktan nasıl düzgün, temiz bir kağıt yığını yapabilirsiniz?

  1. Böl: Tüm sayfaları dağıtın, böylece her "yığın" da yalnızca bir sayfa olur.
  2. Fethetmek:
    1. Her sayfayı bir başka sayfanın üstüne koyarak etrafta dolaşın. Artık 2 yığınınız var.
    2. Her 2'li yığını başka bir 2'li yığının üzerine koyarak etrafta dolaşın. Artık 4 yığınınız var.
    3. Her 4'lü yığını başka bir 4'lü yığının üzerine koyun. Artık 8 yığınınız var.
    4. ... durmadan ...
    5. Artık 1024 sayfalık devasa bir yığına sahipsiniz!

Her şeyi saymanın yanı sıra bunun oldukça sezgisel olduğuna dikkat edin (kesinlikle gerekli değildir). Gerçekte 1 sayfalık yığınlara kadar inemeyebilirsiniz, ancak yapabilirdiniz ve yine de çalışırdı. Önemli olan çekiç: Kollarınızla, daha büyük bir yığın oluşturmak için her zaman bir yığını diğerinin üzerine koyabilirsiniz ve her iki yığının da ne kadar büyük olduğu (sebep dahilinde) önemli değildir.


6
Böl ve fethetmeyi anlatıyorsun. Bu bir özyineleme örneği olsa da , hiçbir şekilde tek değildir.
Konrad Rudolph

Bu iyi. Burada [özyineleme dünyasını] [1] bir cümle içinde yakalamaya çalışmıyorum. Sezgisel bir açıklama istiyorum. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

Özyineleme, bir yöntem çağrısının belirli bir görevi yerine getirebilmesi için kendi başına olduğu süreçtir. Kod fazlalığını azaltır. Özyinelemeli işlevlerin veya yöntemlerin çoğu, özyinelemeli çağrıyı kırmak için bir koşula sahip olmalıdır, yani bir koşul karşılandığında kendisini çağırmasını durdurur - bu, sonsuz bir döngünün yaratılmasını engeller. Tüm işlevler yinelemeli olarak kullanılmaya uygun değildir.


1

Hey, eğer fikrim birisiyle uyuşuyorsa özür dilerim, ben sadece özyinelemeyi düz İngilizce ile açıklamaya çalışıyorum.

Jack, John ve Morgan olmak üzere üç yöneticiniz olduğunu varsayalım. Jack, John - 3 ve Morgan - 5 olmak üzere 2 programcıyı yönetiyor. Her yöneticiye 300 $ vereceksiniz ve bunun ne kadara mal olacağını bilmek istiyorsunuz. Cevap açıktır - peki ya Morgan'ın 2 çalışanı da yönetici ise?

BURADA özyineleme geliyor. hiyerarşinin tepesinden başlıyorsunuz. yazlık maliyet 0 $ 'dır. Jack ile başlıyorsun, Sonra çalışan olarak herhangi bir yöneticisi olup olmadığını kontrol et. Eğer bunlardan herhangi birini bulursanız, çalışan olarak yöneticileri olup olmadığını kontrol edin. Her yönetici bulduğunuzda yazlık maliyete 300 $ ekleyin. Jack ile işiniz bittiğinde John'a, çalışanlarına ve sonra Morgan'a gidin.

Kaç yöneticiniz olduğunu ve kaç Bütçe harcayabileceğinizi bilmenize rağmen, bir cevap almadan önce ne kadar döngü yapacağınızı asla bilemezsiniz.

Özyineleme, sırasıyla ebeveyn ve çocuk olarak adlandırılan, dalları ve yaprakları olan bir ağaçtır. Bir özyineleme algoritması kullandığınızda, az çok bilinçli olarak verilerden bir ağaç oluşturursunuz.


1

Düz İngilizce'de özyineleme, bazı şeyleri tekrar tekrar tekrarlamak anlamına gelir.

Programlamada bir örnek, kendi içindeki işlevi çağırmaktır.

Bir sayının faktöriyelini hesaplamak için aşağıdaki örneğe bakın:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
Sade İngilizcede, bir şeyi tekrar tekrar tekrar etmeye iterasyon denir.
toon81

1

Temelde veri türünün her durumu için bir durum içeren bir anahtar ifadesinden oluşuyorsa, herhangi bir algoritma bir veri türü üzerinde yapısal özyineleme sergiler .

örneğin, bir tür üzerinde çalışırken

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

yapısal özyinelemeli bir algoritma,

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

bu gerçekten bir veri yapısı üzerinde çalışan herhangi bir algoritmayı yazmanın en açık yoludur.

şimdi, Peano aksiyomları kullanılarak tanımlanan tam sayılara (doğal sayılara) baktığınızda

 integer = 0 | succ(integer)

tamsayılar üzerindeki yapısal özyinelemeli algoritmanın şuna benzediğini görüyorsunuz

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

çok iyi bilinen faktöryel fonksiyon, bu formun en önemsiz örneğidir.


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.