Là một vòng lặp while về bản chất là một đệ quy?


37

Tôi tự hỏi liệu một vòng lặp while thực chất là một đệ quy?

Tôi nghĩ rằng đó là bởi vì một vòng lặp while có thể được xem như là một hàm tự gọi nó ở cuối. Nếu nó không được đệ quy thì sự khác biệt là gì?



13
Bạn có thể chuyển đổi đệ quy sang phép lặp và ngược lại, vâng. Điều đó không có nghĩa là chúng giống nhau, chúng chỉ có khả năng giống nhau. Có những lúc đệ quy tự nhiên hơn, và có những lúc lặp lại tự nhiên hơn.
Polygnome

18
@MooingDuck Bạn có thể chứng minh bằng cảm ứng rằng bất kỳ đệ quy nào cũng có thể được viết dưới dạng lặp và ngược lại. Vâng, nó sẽ trông rất khác nhau, nhưng dù sao bạn cũng có thể làm điều đó.
Polygnome

6
Thực chất không có nghĩa gì ở đây? Trong lập trình, sử dụng đệ quy có nghĩa là một điều cụ thể, khác với lặp (vòng lặp). Trong CS, khi bạn đến gần hơn với khía cạnh toán học lý thuyết của sự vật, những điều này bắt đầu có nghĩa là một chút khác nhau.
hyde

3
@MooingDuck Việc chuyển đổi từ đệ quy sang lặp lại thực sự khá tầm thường. Bạn chỉ cần giữ một chồng các tham số gọi hàm và một chồng kết quả cho các lệnh gọi hàm. Bạn thay thế các cuộc gọi đệ quy bằng cách thêm các tham số vào ngăn xếp cuộc gọi. chắc chắn rằng tất cả các xử lý của ngăn xếp phá vỡ một chút cấu trúc của thuật toán, nhưng một khi bạn hiểu điều này khá dễ dàng để thấy rằng mã làm điều tương tự. Về cơ bản, bạn đang viết rõ ràng ngăn xếp cuộc gọi ẩn trong các định nghĩa đệ quy.
Bakuriu

Câu trả lời:


116

Vòng lặp rất nhiều không đệ quy. Trong thực tế, chúng là ví dụ điển hình của cơ chế ngược lại : lặp lại .

Điểm đệ quy là một yếu tố xử lý gọi một thể hiện khác của chính nó. Máy móc điều khiển vòng lặp chỉ đơn giản là nhảy trở lại điểm bắt đầu.

Nhảy xung quanh trong mã và gọi một khối mã khác là các hoạt động khác nhau. Chẳng hạn, khi bạn nhảy đến điểm bắt đầu của vòng lặp, biến điều khiển vòng lặp vẫn có cùng giá trị như trước khi nhảy. Nhưng nếu bạn gọi một thể hiện khác của thói quen mà bạn tham gia, thì thể hiện mới có các bản sao mới, không liên quan của tất cả các biến của nó. Thực tế, một biến có thể có một giá trị ở cấp độ xử lý đầu tiên và một giá trị khác ở cấp độ thấp hơn.

Khả năng này rất quan trọng để nhiều thuật toán đệ quy hoạt động và đây là lý do tại sao bạn không thể mô phỏng đệ quy thông qua phép lặp mà không quản lý một chồng các khung được gọi là theo dõi tất cả các giá trị đó.


10
@Giorgio Điều đó có thể đúng, nhưng đó là nhận xét về khiếu nại mà câu trả lời đã không đưa ra. "Tùy tiện" không có trong câu trả lời này và sẽ thay đổi đáng kể ý nghĩa.
hvd

12
@hvd Về nguyên tắc, đệ quy đuôi là đệ quy đầy đủ như mọi thứ khác. Trình biên dịch thông minh có thể tối ưu hóa phần "tạo khung ngăn xếp mới" thực tế để mã được tạo rất giống với vòng lặp, nhưng các khái niệm chúng ta đang nói về áp dụng cho cấp mã nguồn. Tôi coi hình thức thuật toán có mã nguồn là điều quan trọng, vì vậy tôi vẫn gọi nó là đệ quy
Kilian Foth

15
@Giorgio "đây chính xác là những gì đệ quy: gọi chính nó với các đối số mới" - ngoại trừ cuộc gọi. Và những lý lẽ.
hobbs

12
@Giorgio Bạn đang sử dụng các định nghĩa khác nhau của các từ hơn hầu hết ở đây. Từ ngữ, bạn biết, là cơ sở của giao tiếp. Đây là lập trình viên, không phải CS Stack Exchange. Nếu chúng tôi sử dụng các từ như "đối số", "gọi", "hàm", v.v. theo cách bạn đề xuất, sẽ không thể thảo luận về mã thực tế.
hyde

6
@Giorgio Tôi đang xem xét khái niệm trừu tượng. Có khái niệm nơi bạn tái diễn và khái niệm nơi bạn lặp. Chúng là những khái niệm khác nhau . Hobbs đúng 100% rằng không có đối số và không có cuộc gọi. Chúng khác nhau về cơ bản và trừu tượng. Và đó là tốt bởi vì họ giải quyết các vấn đề khác nhau. Mặt khác, bạn đang xem xét cách bạn có thể thực hiện các vòng lặp khi công cụ duy nhất của bạn là đệ quy. Trớ trêu thay, bạn đang nói với Hobbs ngừng suy nghĩ về việc thực hiện và bắt đầu xem xét các khái niệm khi phương pháp của bạn là phương pháp thực sự cần đánh giá lại.
corsiKa

37

Nói rằng X về bản chất Y chỉ có ý nghĩa nếu bạn có một số hệ thống (chính thức) trong tâm trí rằng bạn đang thể hiện X. Nếu bạn xác định ngữ nghĩa whiletheo tính toán lambda, bạn có thể đề cập đến đệ quy *; nếu bạn xác định nó theo thuật ngữ của một máy đăng ký, bạn có thể sẽ không.

Trong cả hai trường hợp, mọi người có thể sẽ không hiểu bạn nếu bạn gọi hàm đệ quy chỉ vì nó chứa vòng lặp while.

* Mặc dù có lẽ chỉ gián tiếp, ví dụ nếu bạn định nghĩa nó theo fold.


4
Để công bằng, hàm không đệ quy theo bất kỳ định nghĩa nào. Nó chỉ chứa một phần tử đệ quy, vòng lặp.
Luaan

@Luaan: Chắc chắn là như vậy, nhưng vì trong các ngôn ngữ có whilecấu trúc đệ quy nói chung là một thuộc tính của các hàm, tôi không thể nghĩ ra bất cứ điều gì khác để mô tả là "đệ quy" trong ngữ cảnh này.
Anton Golov

36

Điều này phụ thuộc vào quan điểm của bạn.

Nếu bạn nhìn vào lý thuyết tính toán , thì phép lặp và đệ quy đều có tính biểu cảm như nhau . Điều này có nghĩa là bạn có thể viết một hàm tính toán một cái gì đó và không quan trọng bạn thực hiện đệ quy hay lặp đi lặp lại, bạn sẽ có thể chọn cả hai cách tiếp cận. Không có gì bạn có thể tính toán đệ quy mà bạn không thể tính toán lặp lại và ngược lại (mặc dù hoạt động nội bộ của chương trình có thể khác nhau).

Nhiều ngôn ngữ lập trình không xử lý đệ quy và lặp lại như nhau, và vì lý do chính đáng. Thông thường , đệ quy có nghĩa là ngôn ngữ / trình biên dịch xử lý ngăn xếp cuộc gọi và lặp lại có nghĩa là bạn có thể phải tự xử lý ngăn xếp.

Tuy nhiên, có những ngôn ngữ - đặc biệt là ngôn ngữ chức năng - trong đó những thứ như vòng lặp (trong, trong khi) thực sự chỉ là cú pháp cú pháp để đệ quy và thực hiện đằng sau hậu trường theo cách đó. Điều này thường được mong muốn trong các ngôn ngữ chức năng, bởi vì chúng thường không có khái niệm lặp, và thêm nó sẽ làm cho phép tính của chúng phức tạp hơn, vì ít lý do thực tế.

Vì vậy, không, về bản chất chúng không giống nhau . Chúng có tính biểu cảm như nhau , có nghĩa là bạn không thể tính toán một cái gì đó lặp đi lặp lại, bạn không thể tính toán đệ quy và ngược lại, nhưng đó là về nó, trong trường hợp chung (theo luận án Church-Turing).

Lưu ý rằng chúng ta đang nói về các chương trình đệ quy ở đây. Có các hình thức đệ quy khác, ví dụ như trong cấu trúc dữ liệu (ví dụ: cây).


Nếu bạn nhìn nó từ quan điểm thực hiện , thì đệ quy và lặp lại khá nhiều không giống nhau. Đệ quy tạo ra một khung ngăn xếp mới cho mỗi cuộc gọi. Mỗi bước của đệ quy là khép kín, nhận được các đối số cho tính toán từ callee (chính nó).

Mặt khác, các vòng lặp không tạo khung cuộc gọi. Đối với họ, bối cảnh không được bảo tồn trên mỗi bước. Đối với vòng lặp, chương trình chỉ đơn giản là nhảy trở lại điểm bắt đầu của vòng lặp cho đến khi điều kiện vòng lặp thất bại.

Điều này khá quan trọng để biết, vì nó có thể tạo ra sự khác biệt khá triệt để trong thế giới thực. Để đệ quy, toàn bộ bối cảnh phải được lưu trên mỗi cuộc gọi. Đối với phép lặp, bạn có quyền kiểm soát chính xác về các biến trong bộ nhớ và những gì được lưu ở đâu.

Nếu bạn nhìn theo cách đó, bạn sẽ nhanh chóng thấy rằng đối với hầu hết các ngôn ngữ, phép lặp và phép đệ quy khác nhau về cơ bản và có các thuộc tính khác nhau. Tùy thuộc vào tình huống, một số thuộc tính được mong muốn hơn những người khác.

Đệ quy có thể làm cho các chương trình đơn giản hơn và dễ dàng hơn để kiểm tra và chứng minh . Chuyển đổi một đệ quy thành lặp thường làm cho mã phức tạp hơn, làm tăng khả năng thất bại. Mặt khác, chuyển đổi sang lặp và giảm số lượng khung ngăn xếp cuộc gọi có thể tiết kiệm nhiều bộ nhớ cần thiết.


Một ngôn ngữ có các biến cục bộ và đệ quy nhưng không có mảng nào có thể thực hiện các tác vụ mà ngôn ngữ lặp không thể thực hiện được với các biến cục bộ và không có mảng. Ví dụ: báo cáo xem một đầu vào có chứa một chuỗi ký tự chữ và số có độ dài không xác định theo sau là một khoảng trống và sau đó là các ký tự của chuỗi gốc theo thứ tự ngược lại.
supercat

3
Miễn là ngôn ngữ được hoàn thành, nó có thể. Ví dụ, một mảng có thể dễ dàng được thay thế bằng một danh sách liên kết (gấp đôi). Nói về phép lặp hoặc đệ quy và thời tiết chúng bằng nhau chỉ có ý nghĩa nếu bạn so sánh hai ngôn ngữ hoàn chỉnh.
Polygnome

Tôi có nghĩa là không có gì ngoài các biến tĩnh hoặc tự động đơn giản, tức là không hoàn thành Turing. Một ngôn ngữ lặp thuần túy sẽ bị giới hạn trong những nhiệm vụ có thể được thực hiện thông qua máy tự động hữu hạn xác định đơn giản, trong khi ngôn ngữ đệ quy sẽ thêm khả năng thực hiện các nhiệm vụ đòi hỏi ít nhất một máy tự động hữu hạn xác định đẩy xuống.
supercat

1
Nếu ngôn ngữ không hoàn thành, việc bắt đầu với nó là vô nghĩa. Các DFA không thể lặp lại tùy ý cũng như đệ quy btw.
Polygnome

2
Không có triển khai nào thực sự là Turing-Complete và các ngôn ngữ không phải là Turing-Complete có thể hữu ích cho nhiều mục đích. Bất kỳ chương trình nào có thể được chạy với số lượng biến hữu hạn với phạm vi hữu hạn đều có thể được điều chỉnh bởi DFA trong đó mọi kết hợp giá trị có thể là một trạng thái riêng biệt.
supercat

12

Sự khác biệt là ngăn xếp ngầm và ngữ nghĩa.

Một vòng lặp while "tự gọi mình ở cuối" không có ngăn xếp để thu thập dữ liệu khi hoàn thành. Lần lặp lại cuối cùng sẽ thiết lập trạng thái nào khi nó kết thúc.

Tuy nhiên, việc đệ quy không thể được thực hiện nếu không có ngăn xếp ngầm này ghi nhớ trạng thái công việc đã thực hiện trước đó.

Đúng là bạn có thể giải quyết bất kỳ vấn đề đệ quy nào với phép lặp nếu bạn cấp cho nó quyền truy cập vào ngăn xếp một cách rõ ràng. Nhưng làm theo cách đó không giống nhau.

Sự khác biệt về ngữ nghĩa có liên quan đến thực tế là nhìn vào mã đệ quy truyền tải một ý tưởng theo một cách hoàn toàn khác so với mã lặp. Mã lặp làm mọi thứ một bước tại một thời điểm. Nó chấp nhận bất kỳ trạng thái nào xuất phát từ trước và chỉ hoạt động để tạo trạng thái tiếp theo.

Mã đệ quy phá vỡ một vấn đề thành fractals. Phần nhỏ này trông giống như phần lớn đó vì vậy chúng ta có thể làm phần này và phần đó theo cùng một cách. Đó là một cách khác để suy nghĩ về các vấn đề. Nó rất mạnh mẽ và làm quen với. Rất nhiều điều có thể được nói trong một vài dòng. Bạn không thể lấy nó ra khỏi vòng lặp while ngay cả khi nó có quyền truy cập vào ngăn xếp.


5
Tôi nghĩ rằng "ngăn xếp ngầm" là sai lệch. Đệ quy là một phần của ngữ nghĩa của ngôn ngữ, không phải là một chi tiết thực hiện. (Được cấp, hầu hết các ngôn ngữ hỗ trợ đệ quy đều sử dụng ngăn xếp cuộc gọi; nhưng trước hết, một số ngôn ngữ như vậy không, và thứ hai, ngay cả trong các ngôn ngữ, không phải mọi cuộc gọi đệ quy đều nhất thiết phải nối với ngăn xếp cuộc gọi, vì nhiều ngôn ngữ hỗ trợ tối ưu hóa như vậy như loại bỏ cuộc gọi đuôi .) Hiểu cách thực hiện thông thường / đơn giản có thể hữu ích trong việc xử lý sự trừu tượng, nhưng bạn không nên tự lừa mình nghĩ rằng đó là toàn bộ câu chuyện.
ruakh

2
@ruakh Tôi sẽ tranh luận một chức năng thực thi trong không gian liên tục bằng cách sử dụng loại bỏ cuộc gọi đuôi thực sự là một vòng lặp. Ở đây, ngăn xếp không phải là chi tiết triển khai, đó là sự trừu tượng cho phép bạn tích lũy các trạng thái khác nhau cho các mức đệ quy khác nhau.
Cimbali

@ruakh: bất kỳ trạng thái nào trong một lệnh gọi đệ quy sẽ được lưu trữ trên một ngăn xếp ngầm, trừ khi đệ quy có thể được trình biên dịch chuyển đổi thành một vòng lặp. Loại bỏ cuộc gọi đuôi một chi tiết thực hiện, một chi tiết mà bạn cần phải biết nếu bạn muốn tổ chức lại chức năng của mình để được đệ quy đuôi. Ngoài ra, "một số ngôn ngữ như vậy không" - bạn có thể cung cấp một ví dụ về các ngôn ngữ không cần ngăn xếp cho các cuộc gọi đệ quy không?
Groo


@ruakh: CPS tự tạo ra cùng một ngăn xếp ngầm định, do đó, nó phải dựa vào việc loại bỏ cuộc gọi đuôi để có ý nghĩa (điều này làm cho tầm thường do cách nó được xây dựng). Ngay cả bài viết trên wikipedia mà bạn liên kết cũng nói như vậy: Sử dụng CPS mà không tối ưu hóa cuộc gọi đuôi (TCO) sẽ khiến không chỉ sự tiếp tục được xây dựng có khả năng phát triển trong quá trình đệ quy, mà cả ngăn xếp cuộc gọi .
Groo

7

Tất cả bản lề về việc bạn sử dụng thuật ngữ này về bản chất . Ở cấp độ ngôn ngữ lập trình, chúng khác nhau về mặt cú pháp và ngữ nghĩa, và chúng có hiệu suất và sử dụng bộ nhớ khá khác nhau. Nhưng nếu bạn đào sâu vào lý thuyết, chúng có thể được định nghĩa theo nghĩa của nhau, và do đó "giống nhau" trong một số ý nghĩa lý thuyết.

Câu hỏi thực sự là: Khi nào thì có ý nghĩa để phân biệt giữa phép lặp (vòng lặp) và đệ quy, và khi nào thì hữu ích khi nghĩ về nó như những điều tương tự? Câu trả lời là khi thực sự lập trình (trái ngược với việc viết bằng chứng toán học), điều quan trọng là phải phân biệt giữa phép lặp và phép đệ quy.

Đệ quy tạo ra một khung ngăn xếp mới, tức là một tập hợp các biến cục bộ mới cho mỗi cuộc gọi. Điều này có chi phí hoạt động và chiếm không gian trên ngăn xếp, điều đó có nghĩa là một đệ quy đủ sâu có thể tràn vào ngăn xếp khiến chương trình bị sập. Mặt khác, việc lặp lại chỉ sửa đổi các biến hiện có nên thường nhanh hơn và chỉ chiếm một lượng bộ nhớ không đổi. Vì vậy, đây là một sự khác biệt rất quan trọng đối với một nhà phát triển!

Trong các ngôn ngữ có đệ quy cuộc gọi đuôi (ngôn ngữ chức năng điển hình), trình biên dịch có thể tối ưu hóa các cuộc gọi đệ quy theo cách chúng chỉ chiếm một lượng bộ nhớ không đổi. Trong các ngôn ngữ đó, sự khác biệt quan trọng không phải là lặp lại so với đệ quy, mà là phiên bản đệ quy không gọi đuôi-đệ quy và gọi lại đệ quy và lặp lại.

Điểm mấu chốt: Bạn cần có khả năng cho biết sự khác biệt, nếu không chương trình của bạn sẽ bị sập.


3

whilevòng lặp là một hình thức đệ quy, xem ví dụ câu trả lời được chấp nhận cho câu hỏi này . Chúng tương ứng với toán tử in trong lý thuyết tính toán (xem ví dụ ở đây ).

Tất cả các biến thể của forcác vòng lặp lặp trên một dãy số, tập hợp hữu hạn, một mảng, v.v., tương ứng với đệ quy nguyên thủy, xem ví dụ ở đâyđây . Lưu ý rằng các forvòng lặp của C, C ++, Java, v.v., thực sự là đường cú pháp cho một whilevòng lặp, và do đó nó không tương ứng với đệ quy nguyên thủy. forVòng lặp Pascal là một ví dụ về đệ quy nguyên thủy.

Một sự khác biệt quan trọng là đệ quy nguyên thủy luôn chấm dứt, trong khi đệ quy tổng quát ( whilecác vòng lặp) có thể không chấm dứt.

CHỈNH SỬA

Một số làm rõ liên quan đến ý kiến ​​và câu trả lời khác. "Đệ quy xảy ra khi một vật được định nghĩa theo chính nó hoặc thuộc loại của nó." (xem wikipedia ). Vì thế,

Là một vòng lặp while về bản chất là một đệ quy?

Vì bạn có thể định nghĩa một whilevòng lặp theo chính nó

while p do c := if p then (c; while p do c))

sau đó, vâng , một whilevòng lặp là một hình thức đệ quy. Hàm đệ quy là một dạng đệ quy khác (một ví dụ khác về định nghĩa đệ quy). Danh sách và cây là các hình thức đệ quy khác.

Một câu hỏi khác được ngầm định bởi nhiều câu trả lời và bình luận là

Là trong khi các vòng lặp và các hàm đệ quy tương đương?

Câu trả lời cho câu hỏi này là không : Một whilevòng lặp tương ứng với hàm đệ quy đuôi, trong đó các biến được truy cập bởi vòng lặp tương ứng với các đối số của hàm đệ quy ẩn, nhưng, như các hàm khác đã chỉ ra, các hàm đệ quy không đuôi không thể được mô hình hóa bằng một whilevòng lặp mà không sử dụng một ngăn xếp bổ sung.

Vì vậy, thực tế rằng "một whilevòng lặp là một dạng đệ quy" không mâu thuẫn với thực tế là "một số hàm đệ quy không thể được biểu thị bằng một whilevòng lặp".


2
@morbidCode: đệ quy nguyên thủy và đệ quy are là các hình thức đệ quy với các hạn chế cụ thể (hoặc thiếu chúng), được nghiên cứu, ví dụ như trong lý thuyết tính toán. Hóa ra, một ngôn ngữ chỉ có một FORvòng lặp có thể tính toán chính xác tất cả các hàm đệ quy nguyên thủy và một ngôn ngữ chỉ với một WHILEvòng lặp có thể tính toán chính xác tất cả các hàm đệ quy (và hóa ra các hàm đệ quy chính xác là các hàm đó chính xác là các hàm đó một máy Turing có thể tính toán). Hay nói ngắn gọn: đệ quy nguyên thủy và đệ quy là các thuật ngữ kỹ thuật từ lý thuyết toán học / tính toán.
Jörg W Mittag

2
Tôi nghĩ rằng "đệ quy" ngụ ý một hàm gọi chính nó, dẫn đến trạng thái thực thi hiện tại bị đẩy lên ngăn xếp và cứ thế; do đó, hầu hết các máy đều có giới hạn thực tế về số lượng bạn có thể lặp lại. Mặc dù các vòng lặp không có bất kỳ giới hạn nào như vậy bởi vì bên trong chúng sẽ sử dụng thứ gì đó như "JMP" và không sử dụng ngăn xếp. Chỉ cần hiểu biết của tôi, có thể sai.
Jay

13
Câu trả lời này đang sử dụng một định nghĩa hoàn toàn khác cho từ "đệ quy" so với OP đang sử dụng, và do đó rất dễ gây hiểu lầm.
Vịt mướp

2
@DavidGrinberg: Trích dẫn: "C, C ++, Java cho các vòng lặp không phải là một ví dụ về đệ quy nguyên thủy. Đệ quy nguyên thủy có nghĩa là số lần lặp / độ sâu đệ quy tối đa được cố định trước khi bắt đầu vòng lặp." Giorgio đang nói về nguyên thủy lý thuyết tính toán . Không liên quan đến ngôn ngữ lập trình.
Vịt mướp

3
Tôi phải đồng ý với Vịt Mooing. Mặc dù lý thuyết tính toán có thể thú vị trong CS lý thuyết, tôi nghĩ mọi người đều đồng ý rằng OP đang nói về khái niệm ngôn ngữ lập trình.
Voo

2

Cuộc gọi đuôi (hoặc cuộc gọi đệ quy đuôi) được triển khai chính xác dưới dạng "goto với đối số" (không đẩy bất kỳ khung cuộc gọi bổ sung nào trên ngăn xếp cuộc gọi ) và trong một số ngôn ngữ chức năng (đáng chú ý là Ocaml) là cách lặp thông thường.

Vì vậy, một vòng lặp while (trong các ngôn ngữ có chúng) có thể được xem là kết thúc bằng một cuộc gọi đuôi đến cơ thể của nó (hoặc thử nghiệm đầu của nó).

Tương tự, các cuộc gọi đệ quy thông thường (không gọi đuôi) có thể được mô phỏng bằng các vòng lặp (sử dụng một số ngăn xếp).

Đọc thêm về tiếp tụcphong cách tiếp tục đi qua .

Vì vậy, "đệ quy" và "lặp lại" là tương đương sâu sắc.


1

Đúng là cả đệ quy và vòng lặp không giới hạn đều tương đương nhau về tính biểu cảm tính toán. Nghĩa là, bất kỳ chương trình nào được viết đệ quy đều có thể được viết lại thành một chương trình tương đương bằng cách sử dụng các vòng lặp và ngược lại. Cả hai cách tiếp cận đều hoàn chỉnh , có thể được sử dụng để tính bất kỳ hàm tính toán nào.

Sự khác biệt cơ bản về mặt lập trình là đệ quy cho phép bạn sử dụng dữ liệu được lưu trữ trên ngăn xếp cuộc gọi. Để minh họa điều này, giả sử bạn muốn in một phần tử của danh sách liên kết đơn bằng cách sử dụng vòng lặp hoặc đệ quy. Tôi sẽ sử dụng C cho mã ví dụ:

 typedef struct List List;
 struct List
 {
     List* next;
     int element;
 };

 void print_list_loop(List* l)
 {
     List* it = l;
     while(it != NULL)
     {
          printf("Element: %d\n", it->element);
          it = it->next;
     }
 }

 void print_list_rec(List* l)
 {
      if(l == NULL) return;
      printf("Element: %d\n", l->element);
      print_list_rec(l->next);
 }

Đơn giản phải không? Bây giờ chúng ta hãy thực hiện một sửa đổi nhỏ: In danh sách theo thứ tự ngược lại.

Đối với biến thể đệ quy, đây là một sửa đổi gần như tầm thường đối với chức năng ban đầu:

void print_list_reverse_rec(List* l)
{
    if (l == NULL) return;
    print_list_reverse_rec(l->next);
    printf("Element: %d\n", l->element);
}

Đối với chức năng lặp mặc dù, chúng tôi có một vấn đề. Danh sách của chúng tôi được liên kết đơn và do đó chỉ có thể được chuyển tiếp. Nhưng vì chúng tôi đang in ngược lại, chúng tôi phải bắt đầu in phần tử cuối cùng. Khi chúng ta đạt đến phần tử cuối cùng, chúng ta không thể quay lại phần tử thứ hai đến cuối cùng nữa.

Vì vậy, chúng tôi hoặc phải thực hiện lại rất nhiều lần hoặc chúng tôi phải xây dựng một cấu trúc dữ liệu phụ trợ để theo dõi các yếu tố được truy cập và từ đó chúng tôi có thể in hiệu quả.

Tại sao chúng ta không có vấn đề này với đệ quy? Bởi vì trong đệ quy chúng ta đã có sẵn một cấu trúc dữ liệu phụ trợ: Ngăn xếp cuộc gọi hàm.

Vì đệ quy cho phép chúng ta quay trở lại lệnh gọi đệ quy trước đó, với tất cả các biến cục bộ và trạng thái của cuộc gọi đó vẫn còn nguyên, chúng ta có được sự linh hoạt để mô hình hóa trong trường hợp lặp.


1
Tất nhiên, chức năng đệ quy thứ hai không phải là đệ quy đuôi - việc tối ưu hóa không gian sẽ khó hơn rất nhiều vì bạn không thể sử dụng TCO để sử dụng lại ngăn xếp. Việc thực hiện một danh sách liên kết đôi sẽ làm cho cả hai thuật toán trở nên tầm thường, với chi phí không gian của một con trỏ / tham chiếu cho mỗi phần tử.
Baldrickk

@Baldrickk Điều thú vị về đệ quy đuôi là bạn kết thúc với một phiên bản gần giống với phiên bản vòng lặp hơn, vì nó lại loại bỏ khả năng lưu trữ trạng thái của bạn trên ngăn xếp cuộc gọi. Một danh sách liên kết đôi sẽ giải quyết nó, nhưng thiết kế lại cấu trúc dữ liệu thường không phải là một lựa chọn khi gặp vấn đề này. Trong khi ví dụ ở đây hơi bị ràng buộc một cách giả tạo, nó minh họa một mô hình xuất hiện thường xuyên trong các ngôn ngữ chức năng trong bối cảnh các loại đại số đệ quy.
ComicSansMS

Quan điểm của tôi là nếu bạn gặp phải vấn đề này, thì sẽ thiếu thiết kế chức năng hơn là ngôn ngữ mà bạn sử dụng để thực hiện nó và mỗi lựa chọn đều có những mặt tích cực và tiêu cực riêng của nó :)
Baldrickk

0

Vòng lặp là một hình thức đệ quy đặc biệt để đạt được một nhiệm vụ cụ thể (chủ yếu là lặp). Người ta có thể thực hiện một vòng lặp theo kiểu đệ quy với cùng hiệu suất [1] trong một số ngôn ngữ. và trong SICP [2], bạn có thể thấy các vòng lặp được mô tả là "đường tổng hợp". Trong hầu hết các ngôn ngữ lập trình bắt buộc, các khối for và while đang sử dụng cùng phạm vi với hàm cha của chúng. Tuy nhiên, trong hầu hết các ngôn ngữ lập trình chức năng, không tồn tại các vòng lặp trong khi không có nhu cầu về chúng.

Lý do các ngôn ngữ bắt buộc có các vòng lặp for / while là vì chúng đang xử lý các trạng thái bằng cách biến đổi chúng. Nhưng thực ra, nếu bạn nhìn từ góc độ khác nhau, nếu bạn nghĩ về một khối trong khi là một hàm, lấy tham số, xử lý nó và trả về một trạng thái mới - cũng có thể là lệnh gọi của cùng hàm với các tham số khác nhau - bạn có thể nghĩ về vòng lặp như một đệ quy.

Thế giới cũng có thể được định nghĩa là đột biến hoặc bất biến. nếu chúng ta định nghĩa thế giới là một tập hợp các quy tắc và gọi một hàm cuối cùng lấy tất cả các quy tắc và trạng thái hiện tại làm tham số và trả về trạng thái mới theo các tham số này có cùng chức năng (tạo trạng thái tiếp theo trong cùng một trạng thái cách), chúng ta cũng có thể nói đó là một đệ quy và một vòng lặp.

trong ví dụ sau, cuộc sống là hàm có hai tham số "quy tắc" và "trạng thái" và trạng thái mới sẽ được xây dựng trong lần đánh dấu tiếp theo.

life rules state = life rules new_state
    where new_state = construct_state_in_time rules state

[1]: tối ưu hóa cuộc gọi đuôi là tối ưu hóa phổ biến trong các ngôn ngữ lập trình chức năng để sử dụng ngăn xếp chức năng hiện có trong các cuộc gọi đệ quy thay vì tạo một cuộc gọi mới.

[2]: Cấu trúc và giải thích các chương trình máy tính, MIT. https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs


4
@Giorgio Không phải là downvote của tôi, mà chỉ là một phỏng đoán: Tôi nghĩ rằng hầu hết các lập trình viên đều cảm thấy, đệ quy đó ngụ ý có một lệnh gọi hàm đệ quy, bởi vì, đó là những gì đệ quy là, một hàm gọi chính nó. Trong một vòng lặp, không có lệnh gọi hàm đệ quy. Vì vậy, nói rằng một vòng lặp không có hàm gọi đệ quy là một hình thức đệ quy đặc biệt sẽ bị sai một cách rõ ràng, nếu đi theo định nghĩa này.
hyde

1
Chà, có lẽ nhìn nó từ một quan điểm trừu tượng hơn, những thứ dường như là những thứ khác nhau, thực ra là giống nhau về mặt khái niệm. Tôi thấy khá nản lòng và buồn khi nghĩ rằng mọi người hạ thấp câu trả lời chỉ vì chúng không tương ứng với mong đợi của họ thay vì lấy chúng làm cơ hội để học hỏi điều gì đó. Tất cả các câu trả lời cố gắng nói: "Này, nhìn này, những thứ này trông khác nhau trên bề mặt, nhưng thực sự giống nhau ở mức độ trừu tượng hơn" đã bị đánh giá thấp.
Giorgio

3
@Georgio: Mục đích của trang web này là để có được câu trả lời cho các câu hỏi. Câu trả lời hữu ích và chính xác xứng đáng với upvote, câu trả lời gây nhầm lẫn và không hữu ích xứng đáng downvote. Một câu trả lời khéo léo sử dụng một định nghĩa khác nhau của một thuật ngữ phổ biến mà không làm rõ định nghĩa khác được sử dụng là gì gây nhầm lẫn và không có ích. Câu trả lời chỉ có ý nghĩa nếu bạn đã biết câu trả lời, có thể nói, không hữu ích, và chỉ phục vụ để cho các nhà văn nắm bắt vượt trội về thuật ngữ.
JacquesB

2
@JacquesB: "Câu trả lời chỉ có ý nghĩa nếu bạn đã biết câu trả lời, có thể nói, không hữu ích ...": Điều này cũng có thể được nói về câu trả lời chỉ xác nhận những gì người đọc đã biết hoặc nghĩ là biết. Nếu một câu trả lời giới thiệu thuật ngữ không rõ ràng, có thể viết bình luận để hỏi thêm chi tiết trước khi bỏ qua.
Giorgio

4
Vòng lặp không phải là một hình thức đệ quy đặc biệt. Nhìn vào lý thuyết tính toán và ví dụ ngôn ngữ WHILE lý thuyết và tính toán. Đúng, một số ngôn ngữ sử dụng các vòng lặp như đường cú pháp để thực sự sử dụng đệ quy đằng sau hậu trường, nhưng chúng có thể làm điều đó bởi vì đệ quy và lặp lại có tính biểu cảm như nhau , không phải vì chúng giống nhau.
Polygnome

-1

Một vòng lặp while khác với đệ quy.

Khi một chức năng được gọi, sau đây diễn ra:

  1. Một khung ngăn xếp được thêm vào ngăn xếp.

  2. Con trỏ mã di chuyển đến đầu hàm.

Khi một vòng lặp while ở cuối, điều sau đây xảy ra:

  1. Một điều kiện hỏi nếu một cái gì đó là đúng sự thật.

  2. Nếu vậy, mã nhảy đến một điểm.

Nói chung, vòng lặp while gần giống với mã giả sau:

 if (x)

 {

      Jump_to(y);

 }

Quan trọng nhất trong tất cả, đệ quy và vòng lặp có các cách biểu diễn mã lắp ráp khác nhau và biểu diễn mã máy. Điều này có nghĩa là chúng không giống nhau. Chúng có thể có cùng kết quả, nhưng mã máy khác nhau chứng tỏ chúng không giống nhau 100%.


2
Bạn đang nói về việc thực hiện một cuộc gọi thủ tục và một vòng lặp while và, vì chúng được thực hiện khác nhau, bạn kết luận rằng chúng khác nhau. Tuy nhiên, về mặt khái niệm thì rất giống nhau.
Giorgio

1
Tùy thuộc vào trình biên dịch, một lệnh gọi đệ quy được tối ưu hóa, được tối ưu hóa cũng có thể tạo ra cùng một cụm, như vòng lặp đơn giản.
hyde

@hyde ... đó chỉ là một ví dụ cho một thực tế nổi tiếng rằng người ta có thể được thể hiện thông qua người khác; không có nghĩa là chúng giống hệt nhau. Một chút như khối lượng và năng lượng. Tất nhiên người ta có thể lập luận rằng tất cả các cách để tạo ra đầu ra giống hệt nhau là "giống nhau". Nếu thế giới là hữu hạn, cuối cùng tất cả các chương trình sẽ là constexpr.
Peter - Tái lập Monica

@Giorgio Nah, đó là một mô tả hợp lý, không phải là một triển khai. Chúng tôi biết rằng hai điều tương đương nhau ; nhưng sự tương đương không phải là danh tính , bởi vì câu hỏi (và câu trả lời) chính xác là cách chúng ta đi đến kết quả, nghĩa là chúng nhất thiết phải chứa các mô tả thuật toán (có thể được biểu thị dưới dạng ngăn xếp và biến, v.v.).
Peter - Tái lập Monica

1
@ PeterA.Schneider Vâng, nhưng câu trả lời này cho biết "Quan trọng nhất trong tất cả ... mã lắp ráp khác nhau", điều này không hoàn toàn đúng.
hyde

-1

Chỉ lặp đi lặp lại là không đủ để nói chung tương đương với đệ quy, nhưng lặp lại với một ngăn xếp thường tương đương. Bất kỳ hàm đệ quy nào cũng có thể được lập trình lại dưới dạng một vòng lặp với một ngăn xếp và ngược lại. Tuy nhiên, điều này không có nghĩa là nó thực tế và trong bất kỳ tình huống cụ thể nào, một hoặc hình thức khác có thể có lợi ích rõ ràng so với phiên bản khác.

Tôi không chắc tại sao điều này lại gây tranh cãi. Đệ quy và lặp với một ngăn xếp là cùng một quá trình tính toán. Họ là cùng một "hiện tượng", có thể nói như vậy.

Điều duy nhất tôi có thể nghĩ là khi xem chúng là "công cụ lập trình", tôi đồng ý rằng bạn không nên nghĩ về chúng như những điều tương tự. Chúng tương đương "về mặt toán học" hoặc "tính toán" ( lặp lại với một ngăn xếp , không phải là lặp chung), nhưng điều đó không có nghĩa là bạn nên tiếp cận các vấn đề với suy nghĩ mà một trong hai sẽ làm. Từ quan điểm thực hiện / giải quyết vấn đề, một số vấn đề có thể hoạt động tốt hơn theo cách này hay cách khác, và công việc của bạn là một lập trình viên là quyết định chính xác cái nào phù hợp hơn.

Để làm rõ, câu trả lời cho câu hỏi Thực chất là một vòng lặp đệ quy? là một không xác định , hoặc ít nhất là "không trừ khi bạn cũng có stack".

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.