Đệ quy hoặc vòng lặp trong khi


123

Tôi đã đọc về một số thực tiễn phỏng vấn phát triển, cụ thể là về các câu hỏi và kiểm tra kỹ thuật được hỏi trong các cuộc phỏng vấn và tôi đã vấp ngã khá nhiều lần về câu nói của thể loại "Ok bạn đã giải quyết vấn đề bằng một vòng lặp, bây giờ bạn có thể làm điều đó với đệ quy "hoặc" mọi người có thể giải quyết điều này bằng một vòng lặp 100 dòng trong khi họ có thể thực hiện nó trong hàm đệ quy 5 dòng không? " Vân vân.

Câu hỏi của tôi là, đệ quy thường tốt hơn so với if / while / cho các cấu trúc?

Thành thật tôi luôn nghĩ rằng đệ quy không được ưu tiên, bởi vì nó bị giới hạn trong bộ nhớ ngăn xếp nhỏ hơn rất nhiều so với heap, cũng thực hiện một số lượng lớn các lệnh gọi phương thức / phương thức là tối ưu từ quan điểm hiệu năng, nhưng tôi có thể sai...



73
Về chủ đề đệ quy, điều này có vẻ khá thú vị.
dan_waterworth

4
@dan_waterworth cũng vậy, điều này sẽ giúp: google.fr/, nhưng tôi dường như luôn đánh vần sai: P
Shivan Dragon

@ShivanDragon Tôi nghĩ vậy ^ _ ^ Rất thích hợp mà tôi đã đăng nó ngày hôm qua :-)
Neal

2
Trong các môi trường nhúng, tôi đã làm việc trong đệ quy được tán thành tốt nhất và khiến bạn bị công khai ở mức tồi tệ nhất. Không gian ngăn xếp hạn chế có hiệu lực làm cho nó bất hợp pháp.
Fred Thomsen

Câu trả lời:


192

Sự đệ quy về bản chất không tốt hơn hoặc tệ hơn các vòng lặp - mỗi cái đều có ưu điểm và nhược điểm, và những thứ thậm chí còn phụ thuộc vào ngôn ngữ lập trình (và cách thực hiện).

Về mặt kỹ thuật, các vòng lặp phù hợp với các hệ thống máy tính điển hình tốt hơn ở cấp độ phần cứng: ở cấp mã máy, một vòng lặp chỉ là một thử nghiệm và một bước nhảy có điều kiện, trong khi đệ quy (được thực hiện một cách ngây thơ) liên quan đến việc đẩy khung stack, nhảy, quay lại và bật lại từ ngăn xếp. OTOH, nhiều trường hợp đệ quy (đặc biệt là các trường hợp tương đương tầm thường với các vòng lặp) có thể được viết để có thể tránh được việc đẩy / bật ngăn xếp; điều này có thể xảy ra khi lệnh gọi hàm đệ quy là điều cuối cùng xảy ra trong thân hàm trước khi quay trở lại và nó thường được gọi là tối ưu hóa cuộc gọi đuôi (hoặc tối ưu hóa đệ quy đuôi ). Hàm đệ quy được tối ưu hóa cho cuộc gọi đúng đuôi hầu hết tương đương với một vòng lặp ở cấp mã máy.

Một xem xét khác là các vòng lặp yêu cầu cập nhật trạng thái phá hủy, khiến chúng không tương thích với ngữ nghĩa ngôn ngữ thuần túy (không có tác dụng phụ). Đây là lý do tại sao các ngôn ngữ thuần túy như Haskell hoàn toàn không có cấu trúc vòng lặp và nhiều ngôn ngữ lập trình chức năng khác thiếu chúng hoàn toàn hoặc tránh chúng càng nhiều càng tốt.

Tuy nhiên, lý do tại sao những câu hỏi này xuất hiện rất nhiều trong các cuộc phỏng vấn là vì để trả lời chúng, bạn cần có sự hiểu biết thấu đáo về nhiều khái niệm lập trình quan trọng - biến, gọi hàm, phạm vi, và tất nhiên là vòng lặp và đệ quy - và bạn có để mang lại sự linh hoạt về mặt tinh thần cho phép bạn tiếp cận một vấn đề từ hai góc độ hoàn toàn khác nhau và di chuyển giữa các biểu hiện khác nhau của cùng một khái niệm.

Kinh nghiệm và nghiên cứu cho thấy rằng có một ranh giới giữa những người có khả năng hiểu các biến, con trỏ và đệ quy và những người không. Hầu hết mọi thứ khác trong lập trình, bao gồm khung, API, ngôn ngữ lập trình và các trường hợp cạnh của chúng, có thể có được thông qua nghiên cứu và trải nghiệm, nhưng nếu bạn không thể phát triển trực giác cho ba khái niệm cốt lõi này, bạn không thể trở thành lập trình viên. Chuyển một vòng lặp đơn giản thành phiên bản đệ quy là cách nhanh nhất để lọc ra những người không lập trình - ngay cả một lập trình viên khá thiếu kinh nghiệm thường có thể làm điều đó trong 15 phút, và đó là một vấn đề rất khó hiểu về ngôn ngữ, vì vậy ứng viên có thể chọn một ngôn ngữ của sự lựa chọn của họ thay vì vấp phải sự bình dị.

Nếu bạn nhận được một câu hỏi như thế này trong một cuộc phỏng vấn, đó là một dấu hiệu tốt: điều đó có nghĩa là nhà tuyển dụng tiềm năng đang tìm kiếm những người có thể lập trình chứ không phải những người đã ghi nhớ hướng dẫn sử dụng công cụ lập trình.


3
Tôi thích câu trả lời của bạn nhất, bởi vì nó chạm vào hầu hết các khía cạnh quan trọng mà câu trả lời cho câu hỏi này có thể có, nó giải thích các phần kỹ thuật chính, và nó cũng đưa ra một cái nhìn phóng to tốt về cách vấn đề này phù hợp trong lĩnh vực lập trình.
Rồng Shivan

1
Tôi cũng đã nhận thấy một mẫu trong lập trình đồng bộ hóa, nơi các vòng lặp được tránh để ủng hộ các cuộc gọi đệ quy trên một trình vòng lặp có chứa phương thức .next (). Tôi giả sử nó giữ cho mã chạy dài không trở nên quá tham lam CPU.
Evan Plaice

1
Phiên bản lặp cũng liên quan đến các giá trị đẩy và bật. Bạn chỉ cần tự viết mã để làm điều này. Phiên bản đệ quy đang đẩy trạng thái lên ngăn xếp trong thuật toán lặp, bạn thường phải mô phỏng thủ công bằng cách đẩy trạng thái vào một cấu trúc nào đó. Chỉ các thuật toán tầm thường nhất không cần trạng thái này và trong những trường hợp này, trình biên dịch thường có thể phát hiện ra đệ quy đuôi và đưa ra một giải pháp lặp.
Martin York

1
@tdammers Bạn có thể cho tôi biết nơi tôi có thể đọc nghiên cứu mà bạn đề cập "Kinh nghiệm và nghiên cứu cho thấy rằng có một ranh giới giữa mọi người ..." Điều này dường như rất thú vị đối với tôi.
Yoo Matsuo

2
Một điều bạn quên đề cập, mã lặp có xu hướng hoạt động tốt hơn khi bạn xử lý một luồng thực thi duy nhất, nhưng các thuật toán đệ quy có xu hướng cho vay để được thực thi trong nhiều luồng.
GordonM

37

Nó phụ thuộc.

  • Một số vấn đề rất phù hợp với các giải pháp đệ quy, ví dụ quicksort
  • Một số ngôn ngữ không thực sự hỗ trợ đệ quy, ví dụ FORTRAN sớm
  • Một số ngôn ngữ cho rằng đệ quy là phương tiện chính để lặp, ví dụ: Haskell

Cũng đáng lưu ý rằng hỗ trợ cho đệ quy đuôi làm cho các vòng lặp đệ quy và vòng lặp tương đương, nghĩa là đệ quy không phải luôn lãng phí ngăn xếp.

Ngoài ra, một thuật toán đệ quy luôn có thể được thực hiện lặp đi lặp lại bằng cách sử dụng một ngăn xếp rõ ràng .

Cuối cùng, tôi lưu ý rằng một giải pháp năm dòng có thể luôn luôn tốt hơn một dòng 100 (giả sử chúng thực sự tương đương).


5
Câu trả lời hay (+1). "một giải pháp 5 dòng có lẽ luôn luôn tốt hơn một dòng 100": Tôi nghĩ rằng sự đồng nhất không phải là lợi thế duy nhất của đệ quy. Sử dụng một cuộc gọi đệ quy buộc bạn phải làm cho các phụ thuộc chức năng giữa các giá trị trong các lần lặp khác nhau rõ ràng.
Giorgio

4
Các giải pháp ngắn hơn có xu hướng tốt hơn, nhưng có một thứ như là quá ngắn gọn.
dan_waterworth

5
@dan_waterworth so với "100 dòng", thật khó để quá ngắn gọn
gnat

4
@Giorgio, Bạn có thể làm cho các chương trình nhỏ hơn bằng cách loại bỏ mã không cần thiết hoặc bằng cách làm cho những điều rõ ràng ẩn. Vì vậy, miễn là bạn gắn bó với trước đây, bạn cải thiện chất lượng.
dan_waterworth

1
@jk, tôi nghĩ đó là một hình thức khác để ẩn thông tin rõ ràng. Thông tin về những gì một biến được sử dụng cho loại bỏ khỏi tên của nó trong đó nó là rõ ràng và được đẩy vào sử dụng của nó là ẩn.
dan_waterworth

17

Không có sự đồng ý chung về định nghĩa "tốt hơn" khi nói đến lập trình, nhưng tôi sẽ coi nó là "dễ duy trì / đọc" hơn.

Đệ quy có sức mạnh biểu cảm hơn các cấu trúc lặp lặp: Tôi nói điều này bởi vì một vòng lặp while tương đương với một hàm đệ quy đuôi và các hàm đệ quy không cần phải đệ quy đuôi. Cấu trúc mạnh mẽ thường là một điều xấu bởi vì chúng cho phép bạn làm những điều khó đọc. Tuy nhiên, đệ quy cung cấp cho bạn khả năng viết các vòng lặp mà không cần sử dụng tính đột biến và đối với tôi, khả năng biến đổi mạnh hơn nhiều so với đệ quy.

Vì vậy, từ công suất biểu cảm thấp đến công suất biểu cảm cao, các cấu trúc vòng lặp xếp chồng lên nhau như thế này:

  • Đuôi các hàm đệ quy sử dụng dữ liệu bất biến,
  • Hàm đệ quy sử dụng dữ liệu bất biến,
  • Trong khi các vòng lặp sử dụng dữ liệu có thể thay đổi,
  • Đuôi các hàm đệ quy sử dụng dữ liệu có thể thay đổi,
  • Các hàm đệ quy sử dụng dữ liệu có thể thay đổi,

Lý tưởng nhất là bạn sử dụng các cấu trúc ít biểu cảm nhất mà bạn có thể. Tất nhiên, nếu ngôn ngữ của bạn không hỗ trợ tối ưu hóa cuộc gọi đuôi, thì điều đó cũng có thể ảnh hưởng đến sự lựa chọn cấu trúc vòng lặp của bạn.


1
"Vòng lặp while tương đương với hàm đệ quy đuôi và các hàm đệ quy không cần phải đệ quy đuôi": +1. Bạn có thể mô phỏng đệ quy bằng một vòng lặp while + một ngăn xếp.
Giorgio

1
Tôi không chắc chắn tôi đồng ý 100% nhưng chắc chắn đó là một viễn cảnh thú vị nên +1 cho điều đó.
Konrad Rudolph

+1 cho một câu trả lời tuyệt vời và cũng để đề cập rằng một số ngôn ngữ (hoặc trình biên dịch) không thực hiện tối ưu hóa cuộc gọi đuôi.
Shivan Dragon

@Giorgio, "Bạn có thể mô phỏng đệ quy bằng vòng lặp while + stack", đây là lý do tại sao tôi nói sức mạnh biểu cảm. Tính toán, chúng mạnh mẽ như nhau.
dan_waterworth

@dan_waterworth: Chính xác, và như bạn đã nói trong câu trả lời của mình, đệ quy một mình có ý nghĩa hơn một vòng lặp while vì bạn cần thêm một ngăn xếp vào một vòng lặp while để mô phỏng đệ quy.
Giorgio

7

Đệ quy thường ít rõ ràng hơn. Ít rõ ràng là khó để duy trì.

Nếu bạn viết for(i=0;i<ITER_LIMIT;i++){somefunction(i);}trong luồng chính, bạn làm cho nó hoàn toàn rõ ràng bạn đang viết một vòng lặp. Nếu bạn viết somefunction(ITER_LIMIT);bạn không thực sự làm rõ điều gì sẽ xảy ra. Chỉ nhìn thấy nội dung: somefunction(int x)các cuộc gọi đó somefunction(x-1)cho bạn biết thực tế đó là một vòng lặp sử dụng các lần lặp. Ngoài ra, bạn không thể dễ dàng đặt một điều kiện thoát với break;một số lần lặp giữa chừng, bạn phải thêm một điều kiện sẽ được thông qua lại hoặc ném ngoại lệ. (và ngoại lệ một lần nữa thêm phức tạp ...)

Về bản chất, nếu đó là một sự lựa chọn rõ ràng giữa phép lặp và phép đệ quy, hãy làm điều trực quan. Nếu việc lặp lại thực hiện công việc một cách dễ dàng, việc lưu 2 dòng hiếm khi gây ra đau đầu mà nó có thể tạo ra trong thời gian dài.

Tất nhiên, nếu nó tiết kiệm cho bạn 98 dòng, đó là một vấn đề hoàn toàn khác.

Có những tình huống đệ quy đơn giản phù hợp hoàn hảo, và chúng không thực sự hiếm. Truyền qua các cấu trúc cây, các mạng liên kết nhiều lần, các cấu trúc có thể chứa kiểu riêng của nó, các mảng lởm chởm đa chiều, về cơ bản là bất cứ thứ gì không phải là một vectơ đơn giản hoặc một mảng có kích thước cố định. Nếu bạn đi qua một con đường thẳng, đã biết, lặp đi lặp lại. Nếu bạn lặn vào không rõ, tái diễn.

Về cơ bản, nếu somefunction(x-1)được gọi từ bên trong chính nó nhiều hơn một lần cho mỗi cấp độ, hãy quên các lần lặp.

... Viết các hàm lặp cho các nhiệm vụ được thực hiện tốt nhất bằng cách đệ quy là có thể nhưng không dễ chịu. Bất cứ nơi nào bạn sử dụng int, bạn cần một cái gì đó như stack<int>. Tôi đã làm nó một lần, nhiều hơn là một bài tập hơn là cho các mục đích thực tế. Tôi có thể đảm bảo với bạn một khi bạn đối mặt với một nhiệm vụ như vậy bạn sẽ không nghi ngờ như những gì bạn đã bày tỏ.


10
Điều gì là hiển nhiên và điều gì ít rõ ràng phụ thuộc một phần vào những gì bạn đã quen. Lặp lại đã được sử dụng thường xuyên hơn trong các ngôn ngữ lập trình vì nó gần với cách làm việc của CPU hơn (tức là nó sử dụng ít bộ nhớ hơn và thực thi nhanh hơn). Nhưng nếu bạn quen suy nghĩ theo quy nạp, đệ quy có thể trực quan như vậy.
Giorgio

5
"Nếu họ thấy một từ khóa vòng lặp, họ biết đó là một vòng lặp. Nhưng không có từ khóa lặp lại, họ chỉ có thể nhận ra nó bằng cách xem f (x-1) bên trong f (x).": Khi bạn gọi một hàm đệ quy bạn làm không muốn biết rằng nó là đệ quy. Tương tự, khi bạn gọi một hàm chứa một vòng lặp, bạn không muốn biết rằng nó chứa một vòng lặp.
Giorgio

3
@SF: Có nhưng bạn chỉ có thể thấy điều này nếu bạn nhìn vào phần thân của hàm. Trong trường hợp của một vòng lặp, bạn thấy vòng lặp, trong trường hợp đệ quy, bạn thấy rằng hàm gọi chính nó.
Giorgio

5
@SF: Có vẻ hơi giống lý luận vòng tròn với tôi: "Nếu theo trực giác của tôi thì đó là một vòng lặp, thì đó là một vòng lặp." mapcó thể được định nghĩa là một hàm đệ quy (xem ví dụ haskell.org/tutorial/fifts.html ), ngay cả khi nó rõ ràng bằng trực giác rằng nó đi qua một danh sách và áp dụng một chức năng cho từng thành viên của danh sách.
Giorgio

5
@SF, mapkhông phải là một từ khóa, nó là một chức năng thông thường, nhưng điều đó không liên quan. Khi các lập trình viên chức năng sử dụng đệ quy, thường không phải vì họ muốn thực hiện một chuỗi các hành động, bởi vì vấn đề đang được giải quyết có thể được thể hiện dưới dạng một hàm và một danh sách các đối số. Vấn đề sau đó có thể giảm xuống một chức năng khác và một danh sách các đối số khác. Cuối cùng, bạn có một vấn đề có thể được giải quyết một cách tầm thường.
dan_waterworth

6

Như thường lệ, điều này nói chung là không thể trả lời được vì có các yếu tố bổ sung, trong thực tế là không đồng đều giữa các trường hợp và không bằng nhau trong trường hợp sử dụng. Dưới đây là một số áp lực.

  • Mã ngắn, thanh lịch nói chung là vượt trội so với mã dài, phức tạp.
  • Tuy nhiên, điểm cuối cùng có phần bị vô hiệu nếu cơ sở nhà phát triển của bạn không quen với đệ quy và không muốn / không thể học. Nó thậm chí có thể trở thành một tiêu cực nhẹ hơn là tích cực.
  • Đệ quy có thể không tốt cho hiệu quả nếu bạn sẽ cần các cuộc gọi lồng nhau sâu sắc trong thực tế bạn không thể sử dụng đệ quy đuôi (hoặc môi trường của bạn không thể tối ưu hóa đệ quy đuôi).
  • Đệ quy cũng rất tệ trong nhiều trường hợp nếu bạn không thể lưu trữ kết quả trung gian đúng cách. Ví dụ, ví dụ phổ biến của việc sử dụng cây đệ quy để tính số Fibonacci thực hiện khủng khiếp xấu nếu bạn không nhớ cache. Nếu bạn làm bộ đệm, nó đơn giản, nhanh chóng, thanh lịch và hoàn toàn tuyệt vời.
  • Đệ quy không được áp dụng cho một số trường hợp, cũng tốt như lặp lại ở những trường hợp khác và hoàn toàn cần thiết trong những trường hợp khác. Lập kế hoạch thông qua chuỗi dài các quy tắc kinh doanh thường không được giúp đỡ bằng cách đệ quy. Lặp lại thông qua các luồng dữ liệu có thể được thực hiện hữu ích với đệ quy. Lặp lại các cấu trúc dữ liệu động đa chiều (ví dụ: mê cung, cây đối tượng ...) là khá nhiều không thể nếu không có đệ quy, rõ ràng hoặc ẩn. Lưu ý rằng trong những trường hợp này, đệ quy rõ ràng là nhiều hơn so với tiềm ẩn - không có gì là đau đớn hơn đọc mã nơi ai đó thực hiện ad-hoc, không đầy đủ, lỗi stack riêng của họ trong ngôn ngữ chỉ để tránh R-word đáng sợ.

Bạn có ý nghĩa gì khi lưu vào bộ nhớ cache liên quan đến đệ quy?
Giorgio

@Giorgio có lẽ là ghi nhớ
jk.

Feh. Nếu môi trường của bạn không tối ưu hóa các cuộc gọi đuôi, bạn nên tìm một môi trường tốt hơn và nếu nhà phát triển của bạn không quen với đệ quy, bạn nên tìm nhà phát triển tốt hơn. Có một số tiêu chuẩn, mọi người!
CA McCann

1

Đệ quy là về việc lặp lại cuộc gọi đến chức năng, vòng lặp là về việc lặp lại bước nhảy đến vị trí trong bộ nhớ.

Cũng nên được đề cập về tràn ngăn xếp - http://en.wikipedia.org/wiki/Stack_overflow


1
Đệ quy có nghĩa là một hàm có định nghĩa đòi hỏi phải gọi chính nó.
hardmath

1
Mặc dù định nghĩa đơn giản của bạn không chính xác 100%, nhưng bạn là người duy nhất đề cập đến ngăn xếp tràn.
Qix

1

Nó thực sự phụ thuộc vào sự thuận tiện hoặc yêu cầu:

Nếu bạn sử dụng ngôn ngữ lập trình Python , nó hỗ trợ đệ quy, nhưng theo mặc định, có giới hạn cho độ sâu đệ quy (1000). Nếu vượt quá giới hạn, chúng tôi sẽ nhận được một lỗi hoặc ngoại lệ. Giới hạn đó có thể được thay đổi, nhưng nếu chúng ta làm điều đó, chúng ta có thể gặp tình huống bất thường trong ngôn ngữ.

Tại thời điểm này (số lượng cuộc gọi nhiều hơn độ sâu đệ quy), chúng ta cần ưu tiên các cấu trúc vòng lặp. Ý tôi là, nếu kích thước ngăn xếp là không đủ, chúng ta phải thích các cấu trúc vòng lặp.


3
Đây là một blog của Guido van Rossum về lý do tại sao anh ta không muốn tối ưu hóa đệ quy đuôi trong Python (ủng hộ quan điểm rằng các ngôn ngữ khác nhau có cách tiếp cận chiến thuật khác biệt).
hardmath

-1

Sử dụng mẫu thiết kế chiến lược.

  • Đệ quy sạch
  • Vòng lặp là (có thể) hiệu quả

Tùy thuộc vào tải của bạn (và / hoặc các điều kiện khác), chọn một.


5
Đợi đã, cái gì? Làm thế nào để mô hình chiến lược phù hợp ở đây? Và câu thứ hai nghe như một cụm từ trống rỗng.
Konrad Rudolph

@KonradRudolph Tôi sẽ đi đệ quy. Đối với các tập dữ liệu rất lớn, tôi sẽ chuyển sang các vòng lặp. Ý tôi là thế Tôi xin lỗi nếu nó không rõ ràng.
Pravin Sonawane

3
Ah. Chà, tôi vẫn không chắc rằng cái này có thể được gọi là mô hình thiết kế chiến lược, có một ý nghĩa rất cố định và bạn sử dụng nó theo nghĩa bóng. Nhưng bây giờ ít nhất tôi thấy bạn đang đi đâu.
Konrad Rudolph

@KonradRudolph đã học được một bài học quan trọng. Giải thích sâu sắc những gì bạn muốn nói .. Cảm ơn .. điều đó đã giúp .. :)
Pravin Sonawane

2
@Pravin Sonawane: Nếu bạn có thể sử dụng tối ưu hóa đệ quy đuôi, bạn cũng có thể sử dụng đệ quy trên các tập dữ liệu khổng lồ.
Giorgio
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.