Là đệ quy bao giờ nhanh hơn vòng lặp?


286

Tôi biết rằng đệ quy đôi khi gọn gàng hơn nhiều so với vòng lặp và tôi không hỏi bất cứ điều gì về việc khi nào tôi nên sử dụng đệ quy trên vòng lặp, tôi biết đã có rất nhiều câu hỏi về điều đó.

Điều tôi đang hỏi là, đệ quy nhanh hơn vòng lặp không? Đối với tôi có vẻ như, bạn sẽ luôn có thể tinh chỉnh một vòng lặp và khiến nó thực hiện nhanh hơn chức năng đệ quy vì vòng lặp không có liên tục thiết lập các khung ngăn xếp mới.

Tôi đặc biệt tìm kiếm xem đệ quy có nhanh hơn trong các ứng dụng hay không, trong đó đệ quy là cách phù hợp để xử lý dữ liệu, chẳng hạn như trong một số hàm sắp xếp, trong cây nhị phân, v.v.


3
Đôi khi thủ tục lặp lại hoặc các công thức dạng đóng cho một số lần tái phát phải mất hàng thế kỷ để bật lên. Tôi nghĩ rằng chỉ có những lúc đệ quy nhanh hơn :) lol
Pratik Deoghare

23
Nói cho bản thân tôi, tôi thích lặp đi lặp lại nhiều hơn. ;-)
lặp

có thể trùng lặp đệ quy hoặc lặp lại?
nawfal


@PratikDeoghare Không, câu hỏi không phải là về việc chọn một thuật toán hoàn toàn khác. Bạn luôn có thể chuyển đổi một hàm đệ quy thành một phương thức hoạt động giống hệt nhau sử dụng một vòng lặp. Ví dụ, câu trả lời này có cùng thuật toán ở cả định dạng đệ quy và lặp . Nói chung, bạn sẽ đặt một bộ các đối số cho hàm đệ quy vào một ngăn xếp, đẩy vào ngăn xếp để gọi, loại bỏ khỏi ngăn xếp để trả về từ hàm.
TamaMcGlinn

Câu trả lời:


356

Điều này phụ thuộc vào ngôn ngữ được sử dụng. Bạn đã viết 'ngôn ngữ bất khả tri', vì vậy tôi sẽ đưa ra một số ví dụ.

Trong Java, C và Python, đệ quy khá tốn kém so với phép lặp (nói chung) vì nó yêu cầu phân bổ khung ngăn xếp mới. Trong một số trình biên dịch C, người ta có thể sử dụng cờ trình biên dịch để loại bỏ chi phí này, biến đổi một số loại đệ quy nhất định (thực tế, một số loại lệnh gọi đuôi) thành các bước nhảy thay vì gọi hàm.

Trong các triển khai ngôn ngữ lập trình chức năng, đôi khi, phép lặp có thể rất tốn kém và đệ quy có thể rất rẻ. Trong nhiều trường hợp, đệ quy được chuyển thành một bước nhảy đơn giản, nhưng việc thay đổi biến vòng lặp (có thể thay đổi) đôi khi đòi hỏi một số thao tác tương đối nặng, đặc biệt là trên các triển khai hỗ trợ nhiều luồng thực thi. Đột biến rất tốn kém trong một số môi trường này do sự tương tác giữa trình biến đổi và trình thu gom rác, nếu cả hai có thể đang chạy cùng một lúc.

Tôi biết rằng trong một số triển khai Đề án, đệ quy thường sẽ nhanh hơn lặp.

Tóm lại, câu trả lời phụ thuộc vào mã và việc thực hiện. Sử dụng bất cứ phong cách nào bạn thích. Nếu bạn đang sử dụng ngôn ngữ chức năng, đệ quy thể nhanh hơn. Nếu bạn đang sử dụng một ngôn ngữ bắt buộc, việc lặp lại có thể nhanh hơn. Trong một số môi trường, cả hai phương pháp sẽ dẫn đến việc lắp ráp giống nhau được tạo ra (đặt nó vào đường ống của bạn và hút nó).

Phụ lục: Trong một số môi trường, sự thay thế tốt nhất không phải là đệ quy hay lặp mà là các hàm bậc cao hơn. Chúng bao gồm "bản đồ", "bộ lọc" và "thu nhỏ" (còn được gọi là "gấp"). Đây không chỉ là kiểu được ưa thích, không chỉ chúng thường sạch hơn, mà trong một số môi trường, các hàm này là đầu tiên (hoặc duy nhất) có được sự tăng cường từ song song tự động - vì vậy chúng có thể nhanh hơn đáng kể so với phép lặp hoặc đệ quy. Dữ liệu song song Haskell là một ví dụ về một môi trường như vậy.

Danh sách hiểu là một cách khác, nhưng đây thường chỉ là đường cú pháp cho phép lặp, đệ quy hoặc các hàm bậc cao hơn.


48
Tôi +1 điều đó và muốn nhận xét rằng "đệ quy" và "vòng lặp" chỉ là những gì con người đặt tên cho mã của họ. Điều quan trọng đối với hiệu suất không phải là cách bạn đặt tên cho mọi thứ, mà là cách chúng được biên dịch / diễn giải. Recursion, theo định nghĩa, là một khái niệm toán học, và ít liên quan đến khung stack và công cụ lắp ráp.
P Shved

1
Ngoài ra, nói chung, đệ quy là cách tiếp cận tự nhiên hơn trong các ngôn ngữ chức năng và phép lặp thường trực quan hơn trong các ngôn ngữ mệnh lệnh. Sự khác biệt hiệu suất dường như không đáng chú ý, vì vậy chỉ cần sử dụng bất cứ điều gì cảm thấy tự nhiên hơn cho ngôn ngữ cụ thể đó. Ví dụ, có lẽ bạn sẽ không muốn sử dụng phép lặp trong Haskell khi phép đệ quy đơn giản hơn nhiều.
Sasha Chedygov

4
Nói chung đệ quy được biên dịch thành các vòng lặp, với các vòng lặp là một cấu trúc cấp thấp hơn. Tại sao? Bởi vì đệ quy thường được thiết lập tốt trên một số cấu trúc dữ liệu, tạo ra một đại số F ban đầu và cho phép bạn chứng minh một số tính chất về chấm dứt cùng với các đối số quy nạp về cấu trúc của tính toán (đệ quy). Quá trình mà đệ quy được biên dịch thành các vòng lặp là tối ưu hóa cuộc gọi đuôi.
Kristopher Micinski

Điều quan trọng nhất là các hoạt động không được thực hiện. Bạn càng "IO", bạn càng phải xử lý. Dữ liệu hủy IO (hay còn gọi là lập chỉ mục) luôn là hiệu suất tăng mạnh nhất cho bất kỳ hệ thống nào vì bạn không phải xử lý dữ liệu đó ngay từ đầu.
Jeff Fischer

53

đệ quy bao giờ nhanh hơn một vòng lặp?

Không, Lặp lại sẽ luôn nhanh hơn Recursion. (trong Kiến trúc Von Neumann)

Giải trình:

Nếu bạn xây dựng các hoạt động tối thiểu của một máy tính chung từ đầu, "Lặp lại" trước tiên là một khối xây dựng và ít tốn tài nguyên hơn "đệ quy", ergo sẽ nhanh hơn.

Xây dựng một máy tính giả từ đầu:

Tự hỏi : Bạn cần gì để tính toán một giá trị, tức là tuân theo một thuật toán và đạt được kết quả?

Chúng tôi sẽ thiết lập một hệ thống phân cấp các khái niệm, bắt đầu từ đầu và xác định trước tiên các khái niệm cơ bản, cốt lõi, sau đó xây dựng các khái niệm cấp hai với các khái niệm đó, v.v.

  1. Khái niệm đầu tiên: Các ô nhớ, lưu trữ, Nhà nước . Để làm một cái gì đó bạn cần nơi để lưu trữ giá trị kết quả cuối cùng và trung gian. Giả sử chúng ta có một mảng vô hạn các ô "số nguyên", được gọi là Bộ nhớ , M [0..Infinite].

  2. Hướng dẫn: làm một cái gì đó - biến đổi một ô, thay đổi giá trị của nó. thay đổi trạng thái . Mỗi hướng dẫn thú vị thực hiện một sự chuyển đổi. Hướng dẫn cơ bản là:

    a) Đặt và di chuyển các ô nhớ

    • lưu trữ một giá trị vào bộ nhớ, ví dụ: lưu trữ 5 m [4]
    • sao chép một giá trị sang vị trí khác: ví dụ: store m [4] m [8]

    b) Logic và số học

    • và, hoặc, xor, không
    • thêm, phụ, mul, div. ví dụ: thêm m [7] m [8]
  3. Một tác nhân thực thi : một lõi trong CPU hiện đại. Một "tác nhân" là một cái gì đó có thể thực hiện các hướng dẫn. Một tác nhân cũng có thể là một người theo thuật toán trên giấy.

  4. Thứ tự các bước: một chuỗi các hướng dẫn : tức là: làm điều này trước, làm điều này sau, v.v ... Một chuỗi hướng dẫn bắt buộc. Ngay cả một biểu thức dòng là "một chuỗi các hướng dẫn bắt buộc". Nếu bạn có một biểu thức với một "thứ tự đánh giá" cụ thể thì bạn có các bước . Điều đó có nghĩa là thậm chí còn có một biểu thức tổng hợp duy nhất có ẩn bước Bước và cũng có một biến cục bộ ẩn (hãy gọi nó là Kết quả trực tiếp). ví dụ:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    Biểu thức trên bao hàm 3 bước với biến "kết quả" ẩn.

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    Vì vậy, ngay cả các biểu thức infix, vì bạn có một thứ tự đánh giá cụ thể, là một chuỗi các hướng dẫn bắt buộc . Biểu thức ngụ ý một chuỗi các hoạt động được thực hiện theo một thứ tự cụ thể và bởi vì có các bước , nên cũng có một biến trung gian "kết quả" ẩn.

  5. Con trỏ lệnh : Nếu bạn có một chuỗi các bước, bạn cũng có một "con trỏ lệnh" ẩn. Con trỏ lệnh đánh dấu hướng dẫn tiếp theo và tiến lên sau khi đọc lệnh nhưng trước khi lệnh được thực thi.

    Trong máy tính giả này, Con trỏ lệnh là một phần của Bộ nhớ . (Lưu ý: Thông thường, Con trỏ lệnh sẽ là một thanh ghi đặc biệt, trong một lõi CPU, nhưng ở đây chúng tôi sẽ đơn giản hóa các khái niệm và giả sử tất cả dữ liệu (bao gồm các thanh ghi) là một phần của Bộ nhớ Bộ nhớ

  6. Nhảy - Khi bạn đã có số bước được đặt hàng và Con trỏ lệnh , bạn có thể áp dụng hướng dẫn " lưu trữ " để thay đổi giá trị của chính Con trỏ lệnh. Chúng tôi sẽ gọi việc sử dụng cụ thể này của hướng dẫn cửa hàng với một tên mới: Jump . Chúng tôi sử dụng một tên mới vì dễ nghĩ về nó như một khái niệm mới. Bằng cách thay đổi con trỏ lệnh, chúng tôi sẽ hướng dẫn nhân viên chuyển sang Bước x.

  7. Lặp lại vô hạn : Bằng cách nhảy trở lại, bây giờ bạn có thể làm cho tác nhân "lặp lại" một số bước nhất định. Tại thời điểm này chúng ta có Lặp lại vô hạn.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. Có điều kiện - Thực hiện có điều kiện hướng dẫn. Với mệnh đề "có điều kiện", bạn có thể thực hiện một cách có điều kiện một trong một số hướng dẫn dựa trên trạng thái hiện tại (có thể được đặt bằng một lệnh trước đó).

  9. Lặp lại đúng : Bây giờ với mệnh đề điều kiện , chúng ta có thể thoát khỏi vòng lặp vô hạn của lệnh nhảy trở lại . Bây giờ chúng ta có một vòng lặp có điều kiện và sau đó lặp đúng

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. Đặt tên : đặt tên cho một vị trí bộ nhớ cụ thể giữ dữ liệu hoặc giữ một bước . Đây chỉ là một "tiện lợi" để có. Chúng tôi không thêm bất kỳ hướng dẫn mới nào bằng cách có khả năng xác định tên của tên Cameron cho các vị trí bộ nhớ. Naming Naming không phải là một hướng dẫn cho các đại lý, nó chỉ là một tiện lợi cho chúng tôi. Đặt tên làm cho mã (tại thời điểm này) dễ đọc hơn và dễ thay đổi hơn.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. Chương trình con một cấp : Giả sử có một loạt các bước bạn cần thực hiện thường xuyên. Bạn có thể lưu trữ các bước trong một vị trí được đặt tên trong bộ nhớ và sau đó nhảy đến vị trí đó khi bạn cần thực hiện chúng (gọi). Khi kết thúc chuỗi, bạn cần quay lại điểm gọi để tiếp tục thực hiện. Với cơ chế này, bạn đang tạo hướng dẫn mới (chương trình con) bằng cách soạn các hướng dẫn cốt lõi.

    Thực hiện: (không yêu cầu khái niệm mới)

    • Lưu con trỏ lệnh hiện tại vào vị trí bộ nhớ được xác định trước
    • nhảy đến chương trình con
    • ở cuối chương trình con, bạn truy xuất Con trỏ lệnh từ vị trí bộ nhớ được xác định trước, quay trở lại hướng dẫn sau của cuộc gọi ban đầu

    Vấn đề với việc thực hiện một cấp : Bạn không thể gọi một chương trình con khác từ chương trình con. Nếu bạn làm như vậy, bạn sẽ ghi đè địa chỉ trả về (biến toàn cục), do đó bạn không thể lồng các cuộc gọi.

    Để có một triển khai tốt hơn cho các chương trình con: Bạn cần một STACK

  12. Ngăn xếp : Bạn xác định một không gian bộ nhớ để hoạt động như một "ngăn xếp", bạn có thể đẩy các giá trị trên các ngăn xếp trên ngăn xếp, và cũng có thể bật nhạc pop vào cuối cùng. Để triển khai một ngăn xếp, bạn sẽ cần một Con trỏ ngăn xếp (tương tự như Con trỏ lệnh) trỏ đến đầu con trỏ thực tế của ngăn xếp. Khi bạn đẩy đẩy một giá trị, con trỏ ngăn xếp giảm xuống và bạn lưu trữ giá trị. Khi bạn xuất hiện, bạn nhận được giá trị tại Con trỏ ngăn xếp thực tế và sau đó Con trỏ ngăn xếp được tăng lên.

  13. Chương trình con Bây giờ chúng ta có một ngăn xếp, chúng ta có thể thực hiện các chương trình con phù hợp cho phép các cuộc gọi lồng nhau . Việc triển khai là tương tự, nhưng thay vì lưu trữ Con trỏ lệnh ở vị trí bộ nhớ được xác định trước, chúng tôi "đẩy" giá trị của IP trong ngăn xếp . Khi kết thúc chương trình con, chúng ta chỉ cần giá trị pop pop giá trị từ ngăn xếp, thực sự quay trở lại hướng dẫn sau cuộc gọi ban đầu . Việc thực hiện này, có một ngăn xếp trên mạng, cho phép gọi một chương trình con từ một chương trình con khác. Với việc triển khai này, chúng ta có thể tạo ra một số mức độ trừu tượng khi xác định các hướng dẫn mới là chương trình con, bằng cách sử dụng các hướng dẫn cốt lõi hoặc các chương trình con khác làm các khối xây dựng.

  14. Đệ quy : Điều gì xảy ra khi một chương trình con gọi chính nó?. Điều này được gọi là "đệ quy".

    Vấn đề: Ghi đè kết quả trung gian cục bộ mà chương trình con có thể được lưu trữ trong bộ nhớ. Vì bạn đang gọi / sử dụng lại các bước tương tự, nếu kết quả trung gian được lưu trữ trong các vị trí bộ nhớ được xác định trước (biến toàn cục), chúng sẽ bị ghi đè lên các cuộc gọi lồng nhau.

    Giải pháp: Để cho phép đệ quy, chương trình con nên lưu trữ kết quả trung gian cục bộ trong ngăn xếp , do đó, trên mỗi cuộc gọi đệ quy (trực tiếp hoặc gián tiếp), kết quả trung gian được lưu trữ ở các vị trí bộ nhớ khác nhau.

...

đã đạt đến đệ quy chúng tôi dừng lại ở đây.

Phần kết luận:

Trong Kiến trúc Von Neumann, rõ ràng "Lặp lại" là một khái niệm đơn giản / cơ bản hơn so với Recursion " . Chúng ta có một dạng " Lặp lại " ở cấp 7, trong khi " Recursion " ở cấp 14 của hệ thống phân cấp khái niệm.

Lặp lại sẽ luôn nhanh hơn trong mã máy vì nó bao hàm ít hướng dẫn hơn do đó chu kỳ CPU ít hơn.

Cái nào tốt hơn"?

  • Bạn nên sử dụng "phép lặp" khi bạn đang xử lý các cấu trúc dữ liệu tuần tự, đơn giản và ở mọi nơi mà một vòng lặp đơn giản, sẽ làm.

  • Bạn nên sử dụng "đệ quy" khi bạn cần xử lý cấu trúc dữ liệu đệ quy (tôi muốn gọi chúng là Cấu trúc dữ liệu Fractal dữ liệu), hoặc khi giải pháp đệ quy rõ ràng hơn thanh lịch.

Lời khuyên : sử dụng công cụ tốt nhất cho công việc, nhưng hiểu rõ hoạt động bên trong của từng công cụ để lựa chọn sáng suốt.

Cuối cùng, lưu ý rằng bạn có nhiều cơ hội để sử dụng đệ quy. Bạn có Cấu trúc dữ liệu đệ quy ở mọi nơi, hiện bạn đang xem: các phần của DOM hỗ trợ những gì bạn đang đọc là RDS, biểu thức JSON là RDS, hệ thống tệp phân cấp trong máy tính của bạn là RDS, tức là: bạn có một thư mục gốc, chứa các tệp và thư mục, mọi thư mục chứa tệp và thư mục, mỗi thư mục chứa tệp và thư mục ...


2
Bạn đang giả định rằng sự tiến bộ của bạn là 1) cần thiết và 2) rằng nó dừng lại ở đó bạn đã làm. Nhưng 1) không cần thiết (ví dụ, đệ quy có thể được chuyển thành bước nhảy, vì câu trả lời được chấp nhận đã giải thích, do đó không cần ngăn xếp) và 2) nó không phải dừng ở đó (ví dụ, cuối cùng bạn Sẽ đạt được xử lý đồng thời, có thể cần khóa nếu bạn có trạng thái có thể thay đổi như bạn đã giới thiệu ở bước 2, vì vậy mọi thứ chậm lại, trong khi một giải pháp bất biến như chức năng / đệ quy sẽ tránh bị khóa, do đó có thể nhanh hơn / song song hơn) .
hmijail thương tiếc người từ chức

2
"đệ quy có thể biến thành một bước nhảy" là sai. Đệ quy thực sự hữu ích không thể được biến thành một bước nhảy. Đuôi gọi "đệ quy" là một trường hợp đặc biệt, trong đó bạn mã "đệ quy" một cái gì đó có thể được đơn giản hóa thành một vòng lặp bởi trình biên dịch. Ngoài ra, bạn đang kết hợp "bất biến" với "đệ quy", đó là những khái niệm trực giao.
Lucio M. Tato

"Đệ quy thực sự hữu ích không thể biến thành một bước nhảy" -> vì vậy tối ưu hóa cuộc gọi đuôi là bằng cách nào đó vô dụng? Ngoài ra, bất biến và đệ quy có thể là trực giao, nhưng bạn thực hiện liên kết vòng lặp với các bộ đếm có thể thay đổi - hãy nhìn vào bước 9. Dường như với tôi rằng bạn đang nghĩ rằng lặp và đệ quy là những khái niệm hoàn toàn khác nhau; họ không. stackoverflow.com/questions/2651112/
hy

@hmijail Tôi nghĩ rằng một từ tốt hơn "hữu ích" là "đúng". Đệ quy đuôi không phải là đệ quy đúng vì nó chỉ sử dụng cú pháp gọi hàm để ngụy trang phân nhánh vô điều kiện, tức là lặp. Đệ quy thực sự cung cấp cho chúng ta một ngăn xếp quay lại. Tuy nhiên, đệ quy đuôi vẫn có tính biểu cảm, điều này làm cho nó hữu ích. Các thuộc tính của đệ quy giúp phân tích mã dễ dàng hoặc dễ dàng hơn cho tính chính xác được trao cho mã lặp khi nó được biểu thị bằng các lệnh gọi đuôi. Mặc dù điều đó đôi khi được bù đắp một chút bởi sự phức tạp thêm trong phiên bản đuôi như các tham số bổ sung.
Kaz

34

Đệ quy có thể nhanh hơn trong đó phương án thay thế là quản lý rõ ràng một ngăn xếp, như trong thuật toán cây nhị phân hoặc cây nhị phân mà bạn đề cập.

Tôi đã có một trường hợp viết lại một thuật toán đệ quy trong Java làm cho nó chậm hơn.

Vì vậy, cách tiếp cận đúng là trước tiên hãy viết nó theo cách tự nhiên nhất, chỉ tối ưu hóa nếu hồ sơ cho thấy nó là quan trọng, và sau đó đo lường sự cải thiện được cho là.


2
+1 cho " lần đầu tiên viết nó theo cách tự nhiên nhất " và đặc biệt là " chỉ tối ưu hóa nếu hồ sơ cho thấy nó rất quan trọng "
TripeHound

2
+1 để thừa nhận rằng ngăn xếp phần cứng có thể nhanh hơn một phần mềm, được triển khai thủ công, ngăn xếp heap. Hiệu quả cho thấy rằng tất cả các câu trả lời "không" là không chính xác.
sh1


12

Xem xét những gì hoàn toàn phải được thực hiện cho mỗi, lặp và đệ quy.

  • lặp: nhảy đến đầu vòng lặp
  • đệ quy: một bước nhảy để bắt đầu hàm được gọi

Bạn thấy rằng không có nhiều chỗ cho sự khác biệt ở đây.

(Tôi giả sử đệ quy là một cuộc gọi đuôi và trình biên dịch nhận thức được sự tối ưu hóa đó).


9

Hầu hết các câu trả lời ở đây quên thủ phạm rõ ràng tại sao đệ quy thường chậm hơn các giải pháp lặp. Nó được liên kết với việc xây dựng và phá bỏ các khung stack nhưng không chính xác như vậy. Nói chung, đó là một sự khác biệt lớn trong việc lưu trữ biến tự động cho mỗi lần đệ quy. Trong thuật toán lặp có vòng lặp, các biến thường được giữ trong các thanh ghi và ngay cả khi chúng bị đổ, chúng sẽ nằm trong bộ đệm cấp 1. Trong một thuật toán đệ quy, tất cả các trạng thái trung gian của biến được lưu trữ trên ngăn xếp, có nghĩa là chúng sẽ gây ra nhiều sự cố tràn vào bộ nhớ. Điều này có nghĩa là ngay cả khi nó thực hiện cùng một số lượng hoạt động, nó sẽ có rất nhiều bộ nhớ truy cập trong vòng lặp nóng và điều gì làm cho nó tồi tệ hơn, các hoạt động bộ nhớ này có tốc độ tái sử dụng tệ hại làm cho bộ nhớ cache kém hiệu quả.

Các thuật toán đệ quy TL; DR thường có hành vi bộ đệm kém hơn so với các thuật toán lặp.


6

Hầu hết các câu trả lời ở đây là sai . Câu trả lời đúng là nó phụ thuộc . Ví dụ, đây là hai hàm C đi qua một cái cây. Đầu tiên là đệ quy:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

Và đây là chức năng tương tự được thực hiện bằng cách sử dụng phép lặp:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

Nó không quan trọng để hiểu các chi tiết của mã. Chỉ cần đó plà các nút và đó P_FOR_EACH_CHILDlà đi bộ. Trong phiên bản lặp, chúng ta cần một ngăn xếp rõ ràng sttrên đó các nút được đẩy và sau đó bật lên và thao tác.

Hàm đệ quy chạy nhanh hơn nhiều so với hàm lặp. Lý do là bởi vì sau này, đối với mỗi mục, cần có một CALLchức năng st_pushvà sau đó là một chức năng khác st_pop.

Trước đây, bạn chỉ có đệ quy CALLcho mỗi nút.

Thêm vào đó, việc truy cập các biến trên callstack cực kỳ nhanh. Điều đó có nghĩa là bạn đang đọc từ bộ nhớ có khả năng luôn ở trong bộ đệm trong cùng. Mặt khác, một ngăn xếp rõ ràng phải được hỗ trợ bởi malloc: bộ nhớ ed từ heap, tốc độ truy cập chậm hơn nhiều.

Với tối ưu hóa cẩn thận, chẳng hạn như nội tuyến st_pushst_pop, tôi có thể đạt được mức ngang bằng với phương pháp đệ quy. Nhưng ít nhất trên máy tính của tôi, chi phí truy cập bộ nhớ heap lớn hơn chi phí của cuộc gọi đệ quy.

Nhưng cuộc thảo luận này chủ yếu là tranh luận vì đi bộ cây đệ quy là không chính xác . Nếu bạn có một cây đủ lớn, bạn sẽ hết dung lượng cuộc gọi, đó là lý do tại sao phải sử dụng thuật toán lặp.


Tôi có thể xác nhận rằng tôi đã gặp phải một tình huống tương tự, và có những tình huống đệ quy có thể nhanh hơn một ngăn xếp thủ công trên đống. Đặc biệt là khi tối ưu hóa được bật trong trình biên dịch để tránh một số chi phí gọi hàm.
while1fork

1
Đã thực hiện một giao dịch đặt hàng trước của cây nhị phân 7 nút 10 ^ 8 lần. Đệ quy 25ns. Ngăn xếp rõ ràng (được kiểm tra ràng buộc hoặc không - không tạo ra nhiều sự khác biệt) ~ 15ns. Đệ quy cần phải làm nhiều hơn (đăng ký lưu và phục hồi + (thường) sắp xếp khung chặt chẽ hơn) ngoài việc chỉ đẩy và nhảy. (Và nó trở nên tồi tệ hơn với PLT trong các lib được liên kết động.) Bạn không cần phải phân bổ đống ngăn xếp rõ ràng. Bạn có thể thực hiện một chướng ngại vật có khung đầu tiên nằm trong ngăn xếp cuộc gọi thông thường để bạn không hy sinh cục bộ bộ đệm cho trường hợp phổ biến nhất khi bạn không vượt quá khối đầu tiên.
PSkocik

3

Nói chung, không, đệ quy sẽ không nhanh hơn một vòng lặp trong bất kỳ cách sử dụng thực tế nào có triển khai khả thi trong cả hai hình thức. Ý tôi là, chắc chắn, bạn có thể mã hóa các vòng lặp mất mãi mãi, nhưng sẽ có nhiều cách tốt hơn để thực hiện cùng một vòng lặp có thể vượt trội hơn bất kỳ việc thực hiện nào của cùng một vấn đề thông qua đệ quy.

Bạn đánh vào đầu đinh về lý do; tạo và phá hủy các khung stack đắt hơn một bước nhảy đơn giản.

Tuy nhiên, xin lưu ý rằng tôi đã nói "có triển khai khả thi trong cả hai hình thức". Đối với những thứ như nhiều thuật toán sắp xếp, có xu hướng không phải là một cách rất khả thi để thực hiện chúng mà không thiết lập hiệu quả phiên bản ngăn xếp của nó, do sinh ra các "nhiệm vụ" vốn là một phần của quá trình. Do đó, đệ quy có thể nhanh như việc cố gắng thực hiện thuật toán thông qua việc lặp.

Chỉnh sửa: Câu trả lời này giả sử các ngôn ngữ phi chức năng, trong đó hầu hết các loại dữ liệu cơ bản đều có thể thay đổi. Nó không áp dụng cho các ngôn ngữ chức năng.


Đó cũng là lý do tại sao một số trường hợp đệ quy thường được tối ưu hóa bởi trình biên dịch trong các ngôn ngữ thường sử dụng đệ quy. Ví dụ, trong F #, ngoài việc hỗ trợ đầy đủ để theo dõi các hàm đệ quy với opcode .tail, bạn thường thấy một hàm đệ quy được biên dịch dưới dạng một vòng lặp.
em70

Vâng. Đệ quy đuôi đôi khi có thể là tốt nhất của cả hai thế giới - cách "phù hợp" về mặt chức năng để thực hiện một nhiệm vụ đệ quy và hiệu suất của việc sử dụng một vòng lặp.
Amber

1
Điều này nói chung là không chính xác. Trong một số môi trường, đột biến (tương tác với GC) đắt hơn đệ quy đuôi, được chuyển thành một vòng lặp đơn giản hơn trong đầu ra không sử dụng khung ngăn xếp phụ.
Dietrich Epp

2

Trong bất kỳ hệ thống thực tế nào, không, việc tạo khung stack sẽ luôn đắt hơn so với INC và JMP. Đó là lý do tại sao các trình biên dịch thực sự tốt tự động chuyển đổi đệ quy đuôi thành một cuộc gọi vào cùng một khung, tức là không có chi phí chung, do đó bạn có được phiên bản nguồn dễ đọc hơn và phiên bản được biên dịch hiệu quả hơn. Một trình biên dịch thực sự, thực sự tốt thậm chí có thể chuyển đổi đệ quy bình thường thành đệ quy đuôi khi có thể.


1

Lập trình chức năng là về " cái gì " hơn là " làm thế nào ".

Những người triển khai ngôn ngữ sẽ tìm cách tối ưu hóa cách thức hoạt động của mã bên dưới, nếu chúng ta không cố gắng làm cho nó được tối ưu hóa hơn mức cần thiết. Đệ quy cũng có thể được tối ưu hóa trong các ngôn ngữ hỗ trợ tối ưu hóa cuộc gọi đuôi.

Điều quan trọng hơn từ quan điểm lập trình viên là khả năng đọc và bảo trì hơn là tối ưu hóa ở nơi đầu tiên. Một lần nữa, "tối ưu hóa sớm là gốc rễ của mọi tội lỗi".


0

Đây là một phỏng đoán. Nói chung đệ quy có thể không đánh bại vòng lặp thường xuyên hoặc bao giờ gặp vấn đề về kích thước khá nếu cả hai đều sử dụng thuật toán thực sự tốt (không tính độ khó thực hiện), có thể khác nếu được sử dụng với đệ quy cuộc gọi w / tail (và thuật toán đệ quy đuôi và với các vòng lặp cũng là một phần của ngôn ngữ) -có lẽ có thể rất giống nhau và thậm chí có thể thích đệ quy một số thời gian.


0

Theo lý thuyết của nó những điều tương tự. Đệ quy và vòng lặp có cùng độ phức tạp O () sẽ hoạt động với cùng tốc độ lý thuyết, nhưng tất nhiên tốc độ thực phụ thuộc vào ngôn ngữ, trình biên dịch và bộ xử lý. Ví dụ với sức mạnh của số có thể được mã hóa theo cách lặp với O (ln (n)):

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }

1
Big O là tỷ lệ thuận với tỷ lệ cược Vì vậy, cả hai là O(n), nhưng người ta có thể mất nhiều xthời gian hơn người kia, cho tất cả n.
ctrl-alt-delor
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.