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ì?
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ì?
Câu trả lời:
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ị đó.
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 while
theo 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
.
while
cấ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.
Đ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.
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.
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.
while
vò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 for
cá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 và đây . Lưu ý rằng các for
vòng lặp của C, C ++, Java, v.v., thực sự là đường cú pháp cho một while
vòng lặp, và do đó nó không tương ứng với đệ quy nguyên thủy. for
Vò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 ( while
cá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 while
vòng lặp theo chính nó
while p do c := if p then (c; while p do c))
sau đó, vâng , một while
vò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 while
vò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 while
vò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 while
vò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 while
vòng lặp".
FOR
vò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 WHILE
vò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.
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ục và phong 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.
Đú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.
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
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:
Một khung ngăn xếp được thêm vào ngăn xếp.
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:
Một điều kiện hỏi nếu một cái gì đó là đúng sự thật.
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%.
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".