Birinin sadece bir döngü yerine özyinelemeyi kullanacağı bazı (göreceli) temel (birinci sınıf üniversite düzeyinde CS öğrencisi) örnekleri ne zamandır?
Birinin sadece bir döngü yerine özyinelemeyi kullanacağı bazı (göreceli) temel (birinci sınıf üniversite düzeyinde CS öğrencisi) örnekleri ne zamandır?
Yanıtlar:
Yaklaşık iki senedir mezun olanlara C ++ 'ı öğrettim ve özyineleme yaptım Tecrübelerime göre, sorunuz ve hisleriniz çok yaygın. Aşırı, bazı öğrenciler özyinelemeyi anlamakta zorlanırken, diğerleri bunu hemen hemen her şey için kullanmak istemektedir.
Bence Dave bunu iyi özetliyor: uygun olan yerde kullanın. Yani, doğal hissettiğinde kullanın. Güzel bir şekilde uyduğu bir problemle karşılaştığınızda, muhtemelen onu tanıyacaksınız: yinelemeli bir çözüm bulamayacaksınız gibi görünecek. Ayrıca, netlik programlama önemli bir yönüdür. Diğer insanlar (ve siz de!) Ürettiğiniz kodu okuyup anlayabilmelidir. Yinelemeli döngüler ilk bakışta anlamak özyinelemeden daha kolay olduğunu söylemek güvenli olduğunu düşünüyorum.
Genel olarak programlama veya bilgisayar bilimlerini ne kadar iyi bildiğinizi bilmiyorum, ancak sanal işlevler, kalıtım ya da buradaki ileri düzey kavramlar hakkında konuşmanın bir anlamı olmadığını kesinlikle biliyorum. Fibonacci sayıları hesaplamaya klasik örneklerle sık sık başladım. Fibonacci sayıları özyinelemeli olarak tanımlandığı için buraya güzel bir şekilde uyar . Bu anlaşılması kolaydır ve dilin herhangi bir fantezi özelliğini gerektirmez . Öğrenciler bazı temel özyinelemeler anlayışı kazandıktan sonra, daha önce kurduğumuz bazı basit fonksiyonlara bir kez daha göz attık. İşte bir örnek:
Bir dize karakterini içeriyor mu?
Daha önce de yaptığımız gibi: string iterate ve herhangi bir indeks içerip içermediğine bakın .
bool find(const std::string& s, char x)
{
for(int i = 0; i < s.size(); ++i)
{
if(s[i] == x)
return true;
}
return false;
}
Sorun o zaman, özyinelemeli yapabilir miyiz? Elbette yapabiliriz, işte bir yol:
bool find(const std::string& s, int idx, char x)
{
if(idx == s.size())
return false;
return s[idx] == x || find(s, ++idx);
}
Bir sonraki doğal soru, böyle yapmalı mıyız? Muhtemelen değil. Niye ya? Anlaması zor ve gelmesi daha zor. Bu yüzden de, hataya daha yatkındır.
Bazı sorunların çözümleri özyineleme kullanılarak daha doğal olarak ifade edilir.
Örneğin, iki tür düğüme sahip bir ağaç veri yapınız olduğunu varsayalım: bir tamsayı değeri depolayan yapraklar; ve alanlarında sol ve sağ alt ağaç bulunan dallar. Yaprakların sıralandığını ve böylelikle en düşük değerin en soldaki yaprakta olduğunu varsayınız.
Görevin, ağacın değerlerini sırayla yazdırmak olduğunu varsayalım. Bunu yapmak için özyinelemeli bir algoritma oldukça doğaldır:
class Node { abstract void traverse(); }
class Leaf extends Node {
int val;
void traverse() { print(val); }
}
class Branch extends Node {
Node left, right;
void traverse() { left.traverse(); right.traverse(); }
}
Özyineleme olmadan eşdeğer kod yazmak çok daha zor olurdu. Dene!
Daha genel olarak, özyineleme, ağaçlar gibi özyinelemeli veri yapıları algoritmaları veya doğal olarak alt problemlere ayrılabilecek problemler için iyi çalışır. Örneğin, algoritmalar bölmek ve fethetmek , göz atın .
Özyinelemeyi en doğal ortamında görmek istiyorsan Haskell gibi işlevsel bir programlama diline bakmalısın. Böyle bir dilde, döngüsel bir yapı yoktur, bu yüzden her şey özyinelemeyi kullanarak (veya daha üst düzey işlevler kullanılarak ifade edilir, ancak bu, başka bir hikaye de bilmeye değer).
Ayrıca, işlevsel programlama dillerinin optimize edilmiş kuyruk özyineleme gerçekleştirdiğine dikkat edin. Bu, temelde özyinelemenin bir döngüye dönüştürülebilmesi gerekmediği sürece bir yığın çerçevesini bırakmadıkları anlamına gelir. Pratik açıdan, doğal bir şekilde kod yazabilir, ancak yinelemeli kodun performansını elde edebilirsiniz. Kayıt için, C ++ derleyicileri kuyruk çağrılarını da optimize ediyor gibi görünüyor , bu nedenle C ++ 'da özyineleme kullanmanın ek yükü yok
Pratikte özyinelemede yaşayan birinden , konuyla ilgili biraz ışık tutacağım.
Özyinelemeye ilk girildiğinde, bunun kendisini çağıran bir fonksiyon olduğunu ve temelde ağaç geçişi gibi algoritmalarla gösterildiğini öğrenirsiniz. Daha sonra , LISP ve F # gibi diller için işlevsel programlamada çok kullanıldığını görürsünüz . Yazdığım F # ile yazdıklarımın çoğu özyinelemeli ve kalıp eşleştirme.
F # gibi işlevsel programlama hakkında daha fazla şey öğrenirseniz, F # Listelerinin tek başına bağlı listeler olarak uygulandığını öğreneceksiniz ; bu, yalnızca listenin başına erişen işlemlerin O (1) ve öğe erişiminin O (n) olduğu anlamına gelir. Bunu öğrendikten sonra, verileri liste halinde dolaşma, tersine yeni liste oluşturma ve daha sonra çok etkili olan fonksiyondan geri dönmeden önce listeyi tersine çevirme eğilimindedir.
Şimdi, bunu düşünmeye başlarsanız, kısa bir süre sonra özyinelemeli işlevlerin bir işlev çağrısı yapıldığında bir yığın çerçevesine iteceğini ve bir yığın taşmasına neden olabileceğini anlarsınız. Bununla birlikte, özyinelemeli işlevinizi bir kuyruk çağrısı gerçekleştirecek şekilde yapılandırırsanız ve derleyici, kuyruk çağrısı için kodu en iyi duruma getirme özelliğini destekler. örn. .NET OpCodes.Tailcall Alan bir yığın taşmasına neden olmayacak. Bu noktada herhangi bir döngüyü özyinelemeli bir işlev olarak yazmaya ve herhangi bir kararı bir eşleşme olarak yazmaya başlarsınız; günleri if
ve while
şimdi tarih.
PROLOG gibi dillerde geri izlemeyi kullanarak AI'ye geçtiğinizde, her şey özyineliydi. Bu, zorunluluk kodundan oldukça farklı bir şekilde düşünmeyi gerektirse de, PROLOG sorun için doğru araç ise, sizi çok sayıda kod satırı yazmak zorunda kalmanın yükünden kurtarır ve hataları önemli ölçüde azaltabilir. Bakınız: Amzi müşterisi eoTek
Özyinelemeyi ne zaman kullanacağınıza dair sorunuza geri dönmek için; Programlamaya bakmamın bir yolu bir ucunda donanım, diğer ucunda soyut kavramlar. Sorun donanıma daha yakın daha Beraber şart dillerde düşünmek if
ve while
daha soyut sorun, daha ben özyineleme ile yüksek seviyeli dillerdeki düşünüyorum. Bununla birlikte, düşük seviye sistem kodunu yazmaya başlıyorsanız ve bunun geçerli olduğunu doğrulamak istiyorsanız, o zaman teorem provers gibi çözümlerin kullanışlı olduğunu göreceksiniz , bu da büyük ölçüde özyinelemeye dayanıyor.
Jane Caddesi'ne bakarsanız , işlevsel OCaml dilini kullandıklarını göreceksiniz . Kodlarının hiçbirini görmedim, kodları hakkında söylediklerini okumaktan, özyinelemeyle düşünürler.
DÜZENLE
Bir kullanım listesi aradığınız için, size kodda neye bakmanız gerektiği konusunda temel bir fikir ve çoğunlukla temellerin ötesinde olan Katamorfizm kavramına dayanan temel kullanımların bir listesini vereceğim .
C ++ için: Aynı yapı veya sınıfa imleci olan bir yapı veya bir sınıf tanımlarsanız, işaretçileri kullanan geçiş yöntemleri için özyineleme dikkate alınmalıdır.
Basit vaka, bağlantılı bir liste. Baştan veya kuyruktan başlayarak listeyi işler ve daha sonra işaretçileri kullanarak listeyi tekrar eder.
Ağaç, özyinelemenin sıklıkla kullanıldığı başka bir durumdur; Öyle ki, özyinelemesiz ağaç geçişi görürseniz nedenini sormaya başlamalısınız? Bu yanlış değil, yorumlarda belirtilmesi gereken bir şey.
Özyinelemenin ortak kullanımları:
Size diğer cevaplarda verilenlerden daha az okçalı olan bir kullanım durumu vermek için: özyineleme ortak bir kaynaktan türetilen ağaç benzeri (Nesneye Yönelik) sınıf yapıları ile çok iyi karışır. C ++ örneği:
class Expression {
public:
// The "= 0" means 'I don't implement this, I let my subclasses do that'
virtual int ComputeValue() = 0;
}
class Plus : public Expression {
private:
Expression* left
Expression* right;
public:
virtual int ComputeValue() { return left->ComputeValue() + right->ComputeValue(); }
}
class Times : public Expression {
private:
Expression* left
Expression* right;
public:
virtual int ComputeValue() { return left->ComputeValue() * right->ComputeValue(); }
}
class Negate : public Expression {
private:
Expression* expr;
public:
virtual int ComputeValue() { return -(expr->ComputeValue()); }
}
class Constant : public Expression {
private:
int value;
public:
virtual int ComputeValue() { return value; }
}
Yukarıdaki örnek özyinelemeyi kullanır: ComputeValue özyinelemeli olarak uygulanır. Örneği çalışması için sanal işlevler ve miras kullanırsınız. Plus sınıfının sol ve sağ bölümlerinin tam olarak ne olduğunu bilmiyorsunuz, ama umursamıyorsunuz: bu, kendi değerini hesaplayabilen, bilmeniz gereken tek şey.
Yukarıdaki yaklaşımın kritik avantajı, her sınıfın kendi hesaplamaları ile ilgilenmesidir . Her olası alt ifadelerin farklı uygulamalarını tamamen ayırıyorsunuz: birbirlerinin işleyişi hakkında hiçbir bilgisi yok. Bu program hakkında mantıklı olmayı kolaylaştırır ve bu nedenle programın anlaşılmasını, sürdürülmesini ve genişletilmesini kolaylaştırır.
Başlangıç programlama sınıfımdaki özyinelemeyi öğretmek için kullanılan ilk örnek, tüm basamakları bir sayının tersi sırayla ayrı ayrı listeleme işlevidir.
void listDigits(int x){
if (x <= 0)
return;
print x % 10;
listDigits(x/10);
}
Ya da öyle bir şey (Burada bellekten gidiyorum ve test etmiyorum). Ayrıca, daha yüksek seviyeli sınıflara girdiğinizde, özellikle arama algoritmaları, sıralama algoritmaları, vb.
Dolayısıyla şimdi dilde işe yaramaz bir fonksiyon gibi görünebilir, ancak uzun vadede çok faydalıdır.