Có bất cứ điều gì có thể được thực hiện với đệ quy không thể được thực hiện với các vòng lặp không?


126

Có những lúc sử dụng đệ quy tốt hơn sử dụng vòng lặp và có những lúc sử dụng vòng lặp tốt hơn sử dụng đệ quy. Chọn "đúng" có thể tiết kiệm tài nguyên và / hoặc dẫn đến ít dòng mã hơn.

Có trường hợp nào một nhiệm vụ chỉ có thể được thực hiện bằng cách sử dụng đệ quy, thay vì một vòng lặp không?


13
Tôi nghiêm túc nghi ngờ nó. Đệ quy là một vòng lặp vinh quang.
Các cuộc đua nhẹ nhàng trong quỹ đạo

6
Nhìn thấy các hướng chuyển hướng mà câu trả lời đi vào (và bản thân tôi đã thất bại trong việc cung cấp một câu trả lời tốt hơn), bạn có thể làm bất cứ ai cố gắng trả lời nếu bạn cung cấp thêm một chút nền tảng và loại câu trả lời bạn sẽ theo sau. Bạn có muốn một bằng chứng lý thuyết cho các máy giả định (với thời gian lưu trữ và thời gian chạy không giới hạn) không? Hay ví dụ thực tế? (Trường hợp có thể phức tạp một cách lố bịch, có thể đủ điều kiện vì tầm đó không thể thực hiện được.) Hoặc điều gì đó khác biệt?
5gon12eder

8
@LightnessRacesinOrbit Đối với người không nói tiếng Anh bản ngữ của tôi, "Recursion là một vòng lặp được tôn vinh" có nghĩa là "Bạn cũng có thể sử dụng cấu trúc lặp thay vì gọi đệ quy ở bất cứ đâu và khái niệm này không thực sự xứng đáng với tên riêng của nó" . Có lẽ tôi giải thích thành ngữ "tôn vinh một cái gì đó", sau đó.
hyde

13
Còn chức năng Ackermann thì sao? vi.wikipedia.org/wiki/Ackermann_feft , không đặc biệt hữu ích nhưng không thể thực hiện thông qua vòng lặp. (Bạn cũng có thể muốn xem video này youtube.com/watch?v=i7sm9dzFtEI bằng Computerphile)
WizardOfMenlo

8
@WizardOfMenlo mã befunge là một triển khai của giải pháp ERRE (cũng là một giải pháp tương tác ... với một ngăn xếp). Một cách tiếp cận lặp lại với một ngăn xếp có thể mô phỏng một cuộc gọi đệ quy. Trên bất kỳ chương trình nào có sức mạnh phù hợp, một cấu trúc lặp có thể được sử dụng để mô phỏng một chương trình khác. Máy đăng ký với các hướng dẫn INC (r), JZDEC (r, z)có thể thực hiện một máy Turing. Nó không có 'đệ quy' - đó là một Bước nhảy nếu không có quyết định khác. Nếu chức năng Ackermann có thể tính toán được (đó là), máy đăng ký đó có thể làm điều đó.

Câu trả lời:


164

Có và không. Cuối cùng, không có đệ quy nào có thể tính toán được rằng vòng lặp không thể, nhưng vòng lặp cần nhiều hệ thống ống nước hơn. Do đó, một điều đệ quy có thể làm mà các vòng lặp không thể làm cho một số nhiệm vụ trở nên siêu dễ dàng.

Đi dạo trên cây. Đi trên cây với đệ quy là ngu ngốc-dễ dàng. Đó là điều tự nhiên nhất trên thế giới. Đi bộ một cây với các vòng lặp là rất ít đơn giản. Bạn phải duy trì một ngăn xếp hoặc một số cấu trúc dữ liệu khác để theo dõi những gì bạn đã làm.

Thông thường, giải pháp đệ quy cho một vấn đề là đẹp hơn. Đó là một thuật ngữ kỹ thuật, và nó quan trọng.


120
Về cơ bản, thực hiện các vòng lặp thay vì đệ quy có nghĩa là tự xử lý ngăn xếp.
Silviu Burcea

15
... Các ngăn xếp . Tình huống sau đây có thể thích có nhiều hơn một ngăn xếp. Hãy xem xét một hàm đệ quy Atìm thấy một cái gì đó trong cây. Bất Acứ khi nào gặp điều đó, nó sẽ khởi chạy một hàm đệ quy khác Btìm thấy một thứ liên quan trong cây con ở vị trí mà nó được khởi chạy A. Sau khi Bkết thúc đệ quy, nó sẽ Atiếp tục và đệ quy tiếp tục. Người ta có thể khai báo một ngăn xếp cho Avà một cho B, hoặc đặt Bngăn xếp bên trong Avòng lặp. Nếu một người khăng khăng sử dụng một ngăn xếp, mọi thứ sẽ thực sự phức tạp.
rwong

35
Therefore, the one thing recursion can do that loops can't is make some tasks super easy. Và một điều mà các vòng lặp có thể thực hiện đệ quy đó là không thể thực hiện một số nhiệm vụ siêu dễ dàng. Bạn đã thấy những điều xấu xí, không trực quan mà bạn phải làm để chuyển đổi hầu hết các vấn đề lặp đi lặp lại tự nhiên từ đệ quy ngây thơ sang đệ quy đuôi để chúng không thổi tung ngăn xếp?
Mason Wheeler

10
@MasonWheeler 99% thời gian những "thứ" đó có thể được gói gọn hơn bên trong toán tử đệ quy như maphoặc fold(trên thực tế nếu bạn chọn xem xét chúng nguyên thủy, tôi nghĩ bạn có thể sử dụng fold/ unfoldnhư một cách thay thế thứ ba cho các vòng lặp hoặc đệ quy). Trừ khi bạn viết mã thư viện, không có nhiều trường hợp bạn nên lo lắng về việc thực hiện Lặp lại, thay vì nhiệm vụ mà nó phải hoàn thành - trong thực tế, điều đó có nghĩa là các vòng lặp rõ ràng và đệ quy rõ ràng đều kém như nhau trừu tượng nên tránh ở cấp cao nhất.
Leushenko

7
Bạn có thể so sánh hai chuỗi bằng cách so sánh đệ quy các chuỗi con, nhưng chỉ cần so sánh từng ký tự, từng ký tự một, cho đến khi bạn nhận được sự không phù hợp là có thể thực hiện tốt hơn và rõ ràng hơn với người đọc.
Steven Burnap

78

Không.

Bắt xuống rất căn bản của tối thiểu cần thiết để tính toán, bạn chỉ cần để có thể lặp (điều này không thôi thì chưa đủ, mà đúng hơn là một thành phần cần thiết). Nó không quan trọng như thế nào .

Bất kỳ ngôn ngữ lập trình nào có thể thực hiện Turing Machine, được gọi là Turing hoàn chỉnh . Và có rất nhiều ngôn ngữ đang hoàn thiện.

Ngôn ngữ yêu thích của tôi theo cách "có thực sự hoạt động không?" Tính đầy đủ của Turing là của FRACTRAN , là Turing hoàn chỉnh . Nó có một cấu trúc vòng lặp và bạn có thể triển khai máy Turing trong đó. Do đó, bất cứ điều gì có thể tính toán được, đều có thể được thực hiện bằng ngôn ngữ không có đệ quy. Do đó, không có mà đệ quy có thể cung cấp cho bạn về khả năng tính toán mà việc lặp đơn giản không thể thực hiện được.

Điều này thực sự sôi xuống đến một vài điểm:

  • Bất cứ thứ gì có thể tính toán được đều có thể tính toán được trên máy Turing
  • Bất kỳ ngôn ngữ nào có thể triển khai máy Turing (được gọi là Turing hoàn chỉnh), có thể tính toán mọi thứ mà bất kỳ ngôn ngữ nào khác có thể
  • Vì có các máy Turing trong các ngôn ngữ thiếu đệ quy (và có những máy khác chỉ có đệ quy khi bạn vào một số esolang khác), điều chắc chắn là bạn không thể làm gì với đệ quy mà bạn không thể làm với vòng lặp (và không có gì bạn có thể làm với vòng lặp mà bạn không thể làm với đệ quy).

Điều này không có nghĩa là có một số lớp vấn đề dễ dàng được nghĩ đến với đệ quy hơn là với vòng lặp, hoặc với vòng lặp hơn là với đệ quy. Tuy nhiên, những công cụ này cũng mạnh mẽ như nhau.

Và trong khi tôi đưa điều này đến mức cực đoan 'esolang' (chủ yếu là vì bạn có thể tìm thấy những thứ Turing hoàn chỉnh và được thực hiện theo những cách khá kỳ lạ), điều đó không có nghĩa là esolang hoàn toàn không phải là tùy chọn. Có một danh sách toàn bộ những thứ vô tình Turing hoàn thành bao gồm Magic the Gathering, Sendmail, các mẫu MediaWiki và hệ thống kiểu Scala. Nhiều trong số này không được tối ưu khi thực sự làm bất cứ điều gì thiết thực, chỉ là bạn có thể tính toán bất cứ thứ gì có thể tính toán được bằng các công cụ này.


Sự tương đương này có thể trở nên đặc biệt thú vị khi bạn tham gia vào một loại đệ quy cụ thể được gọi là đuôi gọi .

Nếu bạn có, giả sử, một phương pháp giai thừa được viết là:

int fact(int n) {
    return fact(n, 1);
}

int fact(int n, int accum) {
    if(n == 0) { return 1; }
    if(n == 1) { return accum; }
    return fact(n-1, n * accum);
}

Kiểu đệ quy này sẽ được viết lại dưới dạng một vòng lặp - không sử dụng ngăn xếp. Các cách tiếp cận như vậy thực sự thường thanh lịch và dễ hiểu hơn so với vòng lặp tương đương được viết, nhưng một lần nữa, đối với mỗi cuộc gọi đệ quy có thể có một vòng lặp tương đương được viết và đối với mỗi vòng lặp có thể có một cuộc gọi đệ quy được viết.

Ngoài ra còn có lần nơi chuyển đổi vòng lặp đơn giản thành một cuộc gọi đuôi gọi đệ quy có thể phức tạp và nhiều khó hiểu.


Nếu bạn muốn đi vào khía cạnh lý thuyết của nó, hãy xem luận án Church Turing . Bạn cũng có thể thấy luận điểm của nhà thờ về CS.SE là hữu ích.


29
Turing hoàn chỉnh được ném xung quanh quá nhiều như nó quan trọng. Rất nhiều thứ là Turing Complete ( như Magic the Gathering ), nhưng điều đó không có nghĩa là nó giống như một thứ khác là Turing Complete. Ít nhất là không ở mức độ quan trọng. Tôi không muốn đi trên cây với Magic the Gathering.
Scant Roger

7
Một khi bạn có thể giảm một vấn đề thành "điều này có sức mạnh tương đương với máy Turing" thì đủ để đưa nó đến đó. Máy Turing là một trở ngại khá thấp, nhưng đó là tất cả những gì cần thiết. Không có gì một vòng lặp có thể làm mà đệ quy không thể làm, hoặc ngược lại.

4
Tuyên bố được đưa ra trong câu trả lời này tất nhiên là đúng, nhưng tôi dám nói rằng lập luận này không thực sự thuyết phục. Máy Turing không có khái niệm đệ quy trực tiếp, vì vậy, nói rằng bạn có thể mô phỏng máy Turing mà không cần đệ quy không thực sự chứng minh điều gì. Những gì bạn phải trình bày để chứng minh tuyên bố là máy Turing có thể mô phỏng đệ quy. Nếu bạn không thể hiện điều này, bạn phải trung thành cho rằng giả thuyết Church-Turing cũng áp dụng cho đệ quy (điều này đúng) nhưng OP đã đặt câu hỏi này.
5gon12eder

10
Câu hỏi của OP là "có thể", không phải "tốt nhất" hay "hiệu quả nhất" hoặc một số vòng loại khác. "Turing Complete" có nghĩa là bất cứ điều gì có thể được thực hiện với đệ quy cũng có thể được thực hiện bằng một vòng lặp. Cho dù đó là cách tốt nhất để thực hiện nó trong bất kỳ triển khai ngôn ngữ cụ thể nào là một câu hỏi hoàn toàn khác nhau.
Steven Burnap

7
"Có thể" rất nhiều KHÔNG giống như "tốt nhất". Khi bạn nhầm "không tốt nhất" với "không thể", bạn sẽ bị tê liệt bởi vì dù bạn có làm gì đi chăng nữa, gần như luôn luôn là một cách tốt hơn.
Steven Burnap

31

Có trường hợp nào một nhiệm vụ chỉ có thể được thực hiện bằng cách sử dụng đệ quy, thay vì một vòng lặp không?

Bạn luôn có thể biến thuật toán đệ quy thành một vòng lặp, sử dụng cấu trúc dữ liệu Lần đầu vào trước (ngăn xếp AKA) để lưu trữ trạng thái tạm thời, bởi vì cuộc gọi đệ quy chính xác là, lưu trữ trạng thái hiện tại trong ngăn xếp, tiến hành thuật toán, sau đó khôi phục lại nhà nước. Vì vậy, câu trả lời ngắn gọn là: Không, không có trường hợp như vậy .

Tuy nhiên, một đối số có thể được đưa ra cho "có". Hãy lấy một ví dụ cụ thể, dễ dàng: hợp nhất sắp xếp. Bạn cần chia dữ liệu thành hai phần, hợp nhất sắp xếp các phần và sau đó kết hợp chúng. Ngay cả khi bạn không thực hiện một cuộc gọi chức năng ngôn ngữ lập trình thực tế để hợp nhất sắp xếp để thực hiện sắp xếp hợp nhất trên các bộ phận, bạn cần thực hiện chức năng giống hệt như thực hiện một cuộc gọi chức năng (đẩy trạng thái vào ngăn xếp của riêng bạn, nhảy đến bắt đầu vòng lặp với các tham số bắt đầu khác nhau, sau đó bật trạng thái từ ngăn xếp của bạn).

Có phải là đệ quy, nếu bạn thực hiện phép gọi đệ quy, như các bước "trạng thái đẩy" riêng biệt và "nhảy đến bắt đầu" và "trạng thái pop"? Và câu trả lời cho điều đó là: Không, nó vẫn không được gọi là đệ quy, nó được gọi là phép lặp với ngăn xếp rõ ràng (nếu bạn muốn sử dụng thuật ngữ đã thiết lập).


Lưu ý, điều này cũng phụ thuộc vào định nghĩa của "nhiệm vụ". Nếu nhiệm vụ là sắp xếp, thì bạn có thể thực hiện nó với nhiều thuật toán, nhiều thuật toán không cần bất kỳ loại đệ quy nào. Nếu nhiệm vụ là thực hiện thuật toán cụ thể, như sắp xếp hợp nhất, thì áp dụng sự mơ hồ trên.

Vì vậy, hãy xem xét câu hỏi, có những nhiệm vụ chung, trong đó chỉ có các thuật toán giống như đệ quy. Từ nhận xét của @WizardOfMenlo trong câu hỏi, hàm Ackermann là một ví dụ đơn giản về điều đó. Vì vậy, khái niệm đệ quy đứng riêng, ngay cả khi nó có thể được thực hiện với một cấu trúc chương trình máy tính khác (lặp với ngăn xếp rõ ràng).


2
Khi làm việc với một bộ lắp ráp cho bộ xử lý stackless, hai kỹ thuật này đột nhiên trở thành một.
Joshua

@Joshua Thật vậy! Đây là một vấn đề về mức độ trừu tượng. Nếu bạn xuống một hoặc hai cấp, đó chỉ là cổng logic ,.
hyde

2
Điều đó không hoàn toàn chính xác. Để mô phỏng đệ quy với phép lặp, bạn cần một ngăn xếp có thể truy cập ngẫu nhiên. Một ngăn xếp đơn lẻ không có quyền truy cập ngẫu nhiên cộng với một lượng bộ nhớ có thể truy cập trực tiếp hữu hạn sẽ là một thiết bị PDA, không hoàn thành Turing.
Gilles

@Gilles Bài cũ, nhưng tại sao ngăn xếp truy cập ngẫu nhiên lại cần thiết? Ngoài ra, không phải tất cả các máy tính thực sau đó thậm chí còn ít hơn các máy PDA, vì chúng chỉ có lượng bộ nhớ có thể truy cập trực tiếp hữu hạn và không có ngăn xếp nào cả (ngoại trừ bằng cách sử dụng bộ nhớ đó)? Điều này dường như không thực tế trừu tượng, nếu nó nói "chúng ta không thể thực hiện đệ quy trong thực tế".
hyde

20

Nó phụ thuộc vào cách bạn xác định "đệ quy".

Nếu chúng tôi thực sự yêu cầu nó liên quan đến ngăn xếp cuộc gọi (hoặc bất kỳ cơ chế nào để duy trì trạng thái chương trình được sử dụng), thì chúng tôi luôn có thể thay thế nó bằng một cái gì đó không. Thật vậy, các ngôn ngữ dẫn đến việc sử dụng đệ quy một cách tự nhiên có xu hướng có các trình biên dịch sử dụng tối ưu hóa cuộc gọi đuôi, vì vậy những gì bạn viết là đệ quy nhưng những gì bạn chạy là lặp đi lặp lại.

Nhưng hãy xem xét trường hợp chúng tôi thực hiện cuộc gọi đệ quy và sử dụng kết quả của cuộc gọi đệ quy cho cuộc gọi đệ quy đó.

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  if (m == 0)
    return  n+1;
  if (n == 0)
    return Ackermann(m - 1, 1);
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Thực hiện cuộc gọi đệ quy đầu tiên rất dễ dàng:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
  if (m == 0)
    return  n+1;
  if (n == 0)
  {
    m--;
    n = 1;
    goto restart;
  }
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Sau đó, chúng tôi có thể dọn sạch gotođể tránh xa Velociraptors và bóng râm của Dijkstra:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  while(m != 0)
  {
    if (n == 0)
    {
      m--;
      n = 1;
    }
    else
      return Ackermann(m - 1, Ackermann(m, n - 1));
  }
  return  n+1;
}

Nhưng để loại bỏ các cuộc gọi đệ quy khác, chúng ta sẽ phải lưu các giá trị của một số cuộc gọi vào một ngăn xếp:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  Stack<BigInteger> stack = new Stack<BigInteger>();
  stack.Push(m);
  while(stack.Count != 0)
  {
    m = stack.Pop();
    if(m == 0)
      n = n + 1;
    else if(n == 0)
    {
      stack.Push(m - 1);
      n = 1;
    }
    else
    {
      stack.Push(m - 1);
      stack.Push(m);
      --n;
    }
  }
  return n;
}

Bây giờ, khi chúng tôi xem xét mã nguồn, chúng tôi chắc chắn đã biến phương thức đệ quy của mình thành phương pháp lặp.

Xem xét những gì đã được biên dịch, chúng tôi đã biến mã sử dụng ngăn xếp cuộc gọi để thực hiện đệ quy thành mã không (và khi làm như vậy đã biến mã sẽ ném ngoại lệ ngăn xếp cho các giá trị khá nhỏ thành mã sẽ chỉ đơn thuần là mã mất nhiều thời gian để quay trở lại [xem Làm cách nào tôi có thể ngăn chức năng Ackerman của mình tràn ra khỏi ngăn xếp? để biết thêm một số tối ưu hóa khiến nó thực sự quay trở lại cho nhiều đầu vào khả dĩ hơn]).

Xem xét cách thức đệ quy được triển khai nói chung, chúng tôi đã biến mã sử dụng ngăn xếp cuộc gọi thành mã sử dụng một ngăn xếp khác để giữ các hoạt động đang chờ xử lý. Do đó, chúng tôi có thể lập luận rằng nó vẫn còn đệ quy, khi được xem xét ở mức thấp đó.

Và ở cấp độ đó, thực sự không có cách nào khác xung quanh nó. Vì vậy, nếu bạn coi phương pháp đó là đệ quy, thì thực sự có những điều chúng ta không thể làm nếu không có nó. Nói chung mặc dù chúng tôi không dán nhãn đệ quy như vậy. Thuật ngữ đệ quy rất hữu ích vì nó bao gồm một tập hợp các cách tiếp cận nhất định và cho chúng ta một cách để nói về chúng, và chúng ta không còn sử dụng một trong số chúng.

Tất nhiên, tất cả những điều này giả định rằng bạn có một sự lựa chọn. Có cả hai ngôn ngữ cấm các cuộc gọi đệ quy và các ngôn ngữ thiếu các cấu trúc vòng lặp cần thiết để lặp lại.


Chỉ có thể thay thế ngăn xếp cuộc gọi bằng thứ gì đó tương đương nếu ngăn xếp cuộc gọi bị chặn hoặc người ta có quyền truy cập vào bộ nhớ không giới hạn bên ngoài ngăn xếp cuộc gọi. Có một loại vấn đề đáng kể có thể giải quyết được bằng cách tự động đẩy xuống có ngăn xếp cuộc gọi không giới hạn nhưng chỉ có thể có số lượng trạng thái hữu hạn theo cách khác.
supercat

Đây là câu trả lời tốt nhất, có lẽ là câu trả lời đúng duy nhất. Ngay cả ví dụ thứ hai vẫn là đệ quy, và ở cấp độ này, câu trả lời cho câu hỏi ban đầu là không . Với định nghĩa rộng hơn về đệ quy, đệ quy cho hàm Ackermann là không thể tránh được.
gerrit

@gerrit và với một hẹp hơn, nó sẽ tránh nó. Cuối cùng, nó đi xuống các cạnh của những gì chúng tôi làm hoặc không áp dụng nhãn hữu ích này mà chúng tôi sử dụng cho một số mã nhất định.
Jon Hanna

1
Tham gia trang web để bỏ phiếu này. Hàm Ackermann / là / đệ quy trong tự nhiên. Việc thực hiện một cấu trúc đệ quy với một vòng lặp và một ngăn xếp không làm cho nó trở thành một giải pháp lặp, bạn vừa chuyển đệ quy vào không gian người dùng.
Aaron McMillin

9

Câu trả lời cổ điển là "không", nhưng cho phép tôi giải thích lý do tại sao tôi nghĩ "có" là câu trả lời tốt hơn.


Trước khi tiếp tục, hãy hiểu điều gì đó: từ quan điểm tính toán và độ phức tạp:

  • Câu trả lời là "không" nếu bạn được phép có một ngăn xếp phụ trợ khi lặp.
  • Câu trả lời là "có" nếu bạn không được phép thêm bất kỳ dữ liệu nào khi lặp.

Được rồi, bây giờ, hãy đặt một chân vào đất thực hành, giữ chân kia trong đất lý thuyết.


Ngăn xếp cuộc gọi là một cấu trúc điều khiển , trong khi ngăn xếp thủ công là cấu trúc dữ liệu . Kiểm soát và dữ liệu không phải là các khái niệm bằng nhau, nhưng chúng tương đương nhau theo nghĩa chúng có thể được giảm bớt với nhau (hoặc "mô phỏng" thông qua nhau) từ quan điểm tính toán hoặc độ phức tạp.

Khi nào sự phân biệt này có thể quan trọng? Khi bạn làm việc với các công cụ trong thế giới thực. Đây là một ví dụ:

Giả sử bạn đang thực hiện N-way mergesort. Bạn có thể có một forvòng lặp mà đi qua từng Nphân đoạn, gọi mergesortvào chúng một cách riêng biệt, sau đó kết hợp các kết quả.

Làm thế nào bạn có thể song song hóa điều này với OpenMP?

Trong vương quốc đệ quy, nó cực kỳ đơn giản: chỉ cần đặt #pragma omp parallel forvòng lặp của bạn đi từ 1 đến N, và bạn đã hoàn thành. Trong vương quốc lặp lại, bạn không thể làm điều này. Bạn phải sinh ra các luồng thủ công và truyền cho chúng dữ liệu phù hợp bằng tay để chúng biết phải làm gì.

Mặt khác, có những công cụ khác (như vectơ tự động, ví dụ #pragma vector) hoạt động với các vòng lặp nhưng hoàn toàn vô dụng với đệ quy.

Quan điểm, chỉ vì bạn có thể chứng minh hai mô hình tương đương nhau về mặt toán học, điều đó không có nghĩa là chúng bằng nhau trong thực tế. Một vấn đề có thể không quan trọng để tự động hóa trong một mô hình (giả sử, song song hóa vòng lặp) có thể khó giải quyết hơn nhiều trong mô hình khác.

tức là: Các công cụ cho một mô hình không tự động dịch sang các mô hình khác.

Do đó, nếu bạn yêu cầu một công cụ để giải quyết vấn đề, rất có thể công cụ đó sẽ chỉ hoạt động với một cách tiếp cận cụ thể và do đó bạn sẽ không giải quyết được vấn đề bằng một cách tiếp cận khác, ngay cả khi bạn có thể chứng minh một cách toán học vấn đề có thể được giải quyết một trong hai cách.


Thậm chí, hãy xem xét rằng tập hợp các vấn đề có thể được giải quyết bằng máy tự động đẩy xuống lớn hơn bộ có thể được giải quyết bằng máy tự động hữu hạn (dù có xác định hay không) nhưng nhỏ hơn bộ có thể được giải quyết bằng Máy turing.
supercat

8

Đặt lý luận sang một bên, chúng ta hãy xem xét đệ quy và các vòng lặp trông như thế nào từ quan điểm của máy (phần cứng hoặc ảo). Đệ quy là sự kết hợp của luồng điều khiển cho phép bắt đầu thực thi một số mã và hoàn trả khi hoàn thành (trong chế độ xem đơn giản khi tín hiệu và ngoại lệ bị bỏ qua) và của dữ liệu được truyền đến mã khác (đối số) và được trả về từ nó (kết quả). Thông thường không có quản lý bộ nhớ rõ ràng có liên quan, tuy nhiên có sự phân bổ ngầm định của bộ nhớ ngăn xếp để lưu địa chỉ trả về, đối số, kết quả và dữ liệu cục bộ trung gian.

Một vòng lặp là sự kết hợp của luồng điều khiển và dữ liệu cục bộ. So sánh điều này với đệ quy chúng ta có thể thấy rằng lượng dữ liệu trong trường hợp này là cố định. Cách duy nhất để phá vỡ giới hạn này là sử dụng bộ nhớ động (còn được gọi là heap ) có thể được phân bổ (và giải phóng) bất cứ khi nào cần.

Để tóm tắt:

  • Trường hợp đệ quy = Điều khiển luồng + Ngăn xếp (+ Heap)
  • Trường hợp vòng lặp = Điều khiển luồng + Heap

Giả sử rằng phần lưu lượng điều khiển là mạnh mẽ hợp lý, sự khác biệt duy nhất là trong các loại bộ nhớ khả dụng. Vì vậy, chúng tôi còn lại 4 trường hợp (sức mạnh biểu cảm được liệt kê trong ngoặc đơn):

  1. Không ngăn xếp, không heap: đệ quy và cấu trúc động là không thể. (đệ quy = vòng lặp)
  2. Ngăn xếp, không có đống: đệ quy là OK, cấu trúc động là không thể. (đệ quy> vòng lặp)
  3. Không có stack, heap: đệ quy là không thể, cấu trúc động là OK. (đệ quy = vòng lặp)
  4. Stack, heap: đệ quy và cấu trúc động là OK. (đệ quy = vòng lặp)

Nếu quy tắc của trò chơi nghiêm ngặt hơn một chút và việc triển khai đệ quy không được phép sử dụng các vòng lặp, chúng tôi sẽ nhận được điều này thay thế:

  1. Không ngăn xếp, không heap: đệ quy và cấu trúc động là không thể. (đệ quy <vòng lặp)
  2. Ngăn xếp, không có đống: đệ quy là OK, cấu trúc động là không thể. (đệ quy> vòng lặp)
  3. Không có stack, heap: đệ quy là không thể, cấu trúc động là OK. (đệ quy <vòng lặp)
  4. Stack, heap: đệ quy và cấu trúc động là OK. (đệ quy = vòng lặp)

Sự khác biệt chính với kịch bản trước đó là việc thiếu bộ nhớ ngăn xếp không cho phép đệ quy mà không có các vòng lặp để thực hiện nhiều bước hơn trong quá trình thực thi so với các dòng mã.


2

Đúng. Có một số nhiệm vụ phổ biến dễ thực hiện bằng cách sử dụng đệ quy nhưng không thể chỉ với các vòng lặp:

  • Gây chồng tràn.
  • Hoàn toàn khó hiểu lập trình viên mới bắt đầu.
  • Tạo các hàm tìm nhanh mà thực sự là O (n ^ n).

3
Xin vui lòng, những điều này thực sự dễ dàng với các vòng lặp, tôi thấy chúng mọi lúc. Heck, với một chút nỗ lực, bạn thậm chí không cần các vòng lặp. Ngay cả khi đệ quy dễ dàng hơn.
AviD

1
thực tế, A (0, n) = n + 1; A (m, 0) = A (m-1,1) nếu m> 0; A (m, n) = A (m-1, A (m, n-1)) nếu m> 0, n> 0 tăng nhanh hơn một chút so với O (n ^ n) (với m = n) :)
John Donn

1
@JohnDonn Hơn một chút, nó siêu cấp số nhân. cho n = 3 n ^ n ^ n cho n = 4 n ^ n ^ n ^ n ^ n và cứ thế. n đến n sức mạnh n lần.
Aaron McMillin

1

Có một sự khác biệt giữa các hàm đệ quy và các hàm đệ quy nguyên thủy. Các hàm đệ quy nguyên thủy là các hàm được tính toán bằng các vòng lặp, trong đó số lần lặp tối đa của mỗi vòng lặp được tính trước khi bắt đầu thực hiện vòng lặp. (Và "đệ quy" ở đây không liên quan gì đến việc sử dụng đệ quy).

Các hàm đệ quy nguyên thủy hoàn toàn ít mạnh hơn các hàm đệ quy. Bạn sẽ nhận được kết quả tương tự nếu bạn thực hiện các hàm sử dụng đệ quy, trong đó độ sâu tối đa của đệ quy phải được tính toán trước.


3
Tôi không chắc làm thế nào điều này áp dụng cho câu hỏi trên? Bạn có thể vui lòng làm cho kết nối đó rõ ràng hơn?
Yakk

1
Thay thế "vòng lặp" không chính xác bằng sự phân biệt quan trọng giữa "vòng lặp với số lần lặp hạn chế" và "vòng lặp với số lần lặp không giới hạn", mà tôi nghĩ mọi người sẽ biết từ CS 101.
gnasher729

chắc chắn, nhưng nó vẫn không áp dụng cho câu hỏi. Câu hỏi là về vòng lặp và đệ quy, không phải đệ quy và đệ quy nguyên thủy. Hãy tưởng tượng nếu ai đó hỏi về sự khác biệt của C / C ++ và bạn đã trả lời về sự khác biệt giữa K & R C và Ansi C. Chắc chắn điều đó làm cho mọi thứ chính xác hơn, nhưng nó không trả lời câu hỏi.
Yakk

1

Nếu bạn đang lập trình trong c ++ và sử dụng c ++ 11, thì có một điều phải được thực hiện bằng cách sử dụng rec recurs: các hàm constexpr. Nhưng tiêu chuẩn giới hạn ở mức 512, như được giải thích trong câu trả lời này . Không thể sử dụng các vòng lặp trong trường hợp này, vì trong trường hợp đó, hàm không thể là constexpr, nhưng điều này đã được thay đổi trong c ++ 14.


0
  • Nếu cuộc gọi đệ quy là câu lệnh đầu tiên hoặc cuối cùng (không bao gồm kiểm tra điều kiện) của một hàm đệ quy, thì việc chuyển thành cấu trúc vòng lặp là khá dễ dàng.
  • Nhưng nếu hàm thực hiện một số thứ khác trước và sau lệnh gọi đệ quy , thì việc chuyển đổi nó thành các vòng lặp sẽ rất khó khăn.
  • Nếu hàm có nhiều cuộc gọi đệ quy, việc chuyển đổi nó thành mã đang sử dụng chỉ các vòng lặp sẽ là khá nhiều không thể. Một số ngăn xếp sẽ là cần thiết để theo kịp dữ liệu. Trong đệ quy, chính ngăn xếp cuộc gọi sẽ hoạt động như ngăn xếp dữ liệu.

Đi bộ trên cây có nhiều cuộc gọi đệ quy (một cuộc gọi cho mỗi đứa trẻ), nhưng nó biến đổi một cách tầm thường thành một vòng lặp bằng cách sử dụng một ngăn xếp rõ ràng. Mặt khác, các trình phân tích cú pháp thường gây khó chịu khi chuyển đổi.
CodeInChaos

@CodesInChaos Đã chỉnh sửa.
Gul Sơn

-6

Tôi đồng ý với các câu hỏi khác. Không có gì bạn có thể làm với đệ quy bạn không thể làm với một vòng lặp.

NHƯNG , theo tôi đệ quy có thể rất nguy hiểm. Đầu tiên, đối với một số khó khăn hơn để hiểu những gì đang thực sự xảy ra trong mã. Thứ hai, ít nhất là đối với C ++ (Java tôi không chắc chắn) mỗi bước đệ quy có tác động đến bộ nhớ vì mỗi lệnh gọi phương thức gây ra sự tích lũy bộ nhớ và khởi tạo tiêu đề phương thức. Bằng cách này bạn có thể làm nổ tung ngăn xếp của bạn. Đơn giản chỉ cần thử đệ quy các số Fibonacci với giá trị đầu vào cao.


2
Một triển khai đệ quy ngây thơ của các số Fibonacci với đệ quy sẽ hết "hết thời gian" trước khi hết dung lượng ngăn xếp. Tôi đoán có những vấn đề khác tốt hơn cho ví dụ này. Ngoài ra, đối với nhiều vấn đề, một phiên bản vòng lặp có tác động bộ nhớ tương tự như đệ quy, chỉ trên heap thay vì ngăn xếp (nếu ngôn ngữ lập trình của bạn phân biệt các vấn đề đó).
Paŭlo Ebermann

6
Vòng lặp cũng có thể "rất nguy hiểm" nếu bạn quên tăng biến vòng lặp ...
h22

2
Vì vậy, thực sự, cố tình tạo ra một tràn ngăn xếp là một nhiệm vụ trở nên rất khó khăn mà không sử dụng đệ quy.
5gon12eder

@ 5gon12eder đưa chúng ta đến Phương thức nào để tránh tràn ngăn xếp trong thuật toán đệ quy? - viết để tham gia TCO, hoặc Memoisation có thể hữu ích. Phương pháp lặp lại so với đệ quy cũng rất thú vị vì nó liên quan đến hai cách tiếp cận đệ quy khác nhau đối với Fibonacci.

1
Hầu hết thời gian nếu bạn nhận được một ngăn xếp tràn về đệ quy, bạn sẽ có một phiên bản treo trên phiên bản lặp. Ít nhất là các cú ném trước đây với một dấu vết ngăn xếp.
Jon Hanna
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.