Khái niệm đệ quy, theo định nghĩa đơn giản nhất là một hàm gọi chính nó, được áp dụng rộng rãi trong không gian lập trình vì nó giúp chia nhỏ các bài toán phức tạp lớn hơn thành các bài toán con nhỏ hơn, có thể giải quyết dễ dàng hơn và các câu trả lời của chúng có thể được kết hợp với nhau . Một trong những ví dụ thường thấy nhất cho hàm đệ quy là tính giai thừa của một số
Bạn sẽ viết hàm như vậy bằng Python như thế nào?
Đây là một lời gọi đệ quy và nếu chúng ta xem kỹ cách máy tính xử lý đệ quy chi tiết hơn, chúng ta biết rằng với mỗi lời gọi đệ quy, một ngăn xếp sẽ được tạo và đẩy vào bộ nhớ. Ngăn xếp này chứa tất cả thông tin thích hợp bao gồm các giá trị tham số cho hàm. Khi chức năng kết thúc [i. e. trường hợp cơ sở được gọi], thông tin được bật ra khỏi ngăn xếp và mỗi giá trị trong lệnh gọi được trả về và cuối cùng là giá trị cuối cùng được trả về, như được minh họa trong sơ đồ này
nguồn. https. //www. đám mâyavvyit. com/11720/đệ-quy-trong-lập-trình-và-bạn-dùng-nó-như-thế-nào/
Có một chút phức tạp liên quan ở đây vì bộ nhớ của chúng tôi bị hạn chế - chúng tôi không có tài nguyên vô hạn, vì vậy nếu chúng tôi kích hoạt một số lượng lớn các lệnh gọi đệ quy, độ sâu ngăn xếp sẽ trở nên rất lớn và chúng tôi có thể đạt đến mức tối đa của không gian ngăn xếp. Ví dụ: chúng ta sẽ gặp
12067 trong Python khi cố gắng tìm giai thừa của 10000 bằng hàm chúng ta vừa tạo
Có cách nào để tối ưu hóa nó nếu chúng ta vẫn muốn sử dụng đệ quy? . Một hàm là đệ quy đuôi nếu nó kết thúc bằng cách trả về giá trị của lệnh gọi đệ quy — trong khi nếu bất kỳ quá trình xử lý nào khác được thực hiện trên giá trị trả về của lệnh gọi đệ quy thì hàm đó không phải là đệ quy đuôi. Ví dụ, trong trường hợp của chúng tôi, giá trị của
12068 được sử dụng để nhân với
12069. Máy tính rất thông minh và nếu chúng biết chúng ta sẽ không làm gì khác với giá trị trả về, chúng sẽ không thêm ngăn xếp khác và sử dụng lại ngăn xếp hiện có. Như vậy về lý thuyết ta sẽ không gặp lỗi của
12067 khi tối ưu đệ quy đuôi
Ok, vậy làm cách nào để chúng tôi sửa đổi chức năng của mình để tối ưu hóa đệ quy đuôi?
Chức năng này có thể chính xác như những gì chúng ta muốn — ngoại trừ việc nó không hoạt động. [
Mặc dù một số ngôn ngữ lập trình tự động nhận dạng và tối ưu hóa chức năng đệ quy đuôi, nhưng Python là ngôn ngữ thích có truy nguyên thích hợp. Người tạo ra ngôn ngữ Python, Guido van Rossum cũng đã đề cập trong hai blog của mình [liên kết & liên kết] về việc ông không thích tối ưu hóa đệ quy đuôi như thế nào. Anh ấy tin rằng việc bật tối ưu hóa đệ quy đuôi có thể gây nhầm lẫn cho những người dùng vô tình viết nội dung đệ quy nào đó và cần gỡ lỗi. Đó cũng là niềm tin cơ bản khác nhau của các nhà khoa học máy tính khác nhau. Tôi sẽ không bình luận về các ý kiến ở đây, nhưng tôi tin rằng chúng ta nên luôn được khuyến khích nghĩ đến việc tối ưu hóa các chức năng của mình, thay vì chỉ bắt chúng hoạt động, vì vậy tôi đã thực hiện một số nghiên cứu về một số cách chúng ta có thể thực hiện 'một cách' chiến thuật đuôi
Đầu tiên, chúng ta có thể tạo một decorator [về khái niệm decorator trong Python, vui lòng đọc tại đây. ] để chức năng của chúng tôi làm cho cuộc gọi đuôi được tối ưu hóa
Một cách khác để tránh lỗi
12067là sử dụng hàm lặp thay vì hàm đệ quy
Đệ quy đuôi được định nghĩa là một hàm đệ quy trong đó lời gọi đệ quy là câu lệnh cuối cùng được thực hiện bởi hàm. Vì vậy, về cơ bản không còn gì để thực thi sau lệnh gọi đệ quy
Ví dụ hàm C++ print[] sau đây là đệ quy đuôi
C
12072
12073
12074
12075
12076
12077
12078
12079
1200
_______01____02____03
12078
1205
1206
1207
12078
1209
12078
120721
120722
C++
12072
120724
12073
12074
12075
12076
12077
12078
12079
1200
_______01____02____03
12078
1205
1206
1207
120740
12078
1209
12078
120721
120722
120746
Java
12072
120724
12073
12074
12075
12076
12077
12078
12079
120756______1757
120758
_______01____02____03
12078
120763____06
120765
12078
120767
12078
120769
12078
120771____1772
120773
120722
120775
Python3
120776
________ 1777 ________ 1778
12078
12079
120756______1757
120783
1201
1202
12078
120787
120788
120789
120790
120791
120792
120758
12078
120795
12078
120797____1798
120772
120758
12078
12002
12078
12004
C#
12072
120724
12073
12074
12075
12076
12077
12078
12079
1200
_______01____02____03
12078
12019
1206
120765
12078
120767
12078
120769
12078
120721
120722
12029
Javascript
12030
12072
12032
12033
12077
12078
12079
1200
_______038____02____03
12041
12078
12043____06
120765
12041
12078
120767
12038
120769
12078
120721
120722
12054
12055
Cần đệ quy đuôi
Các hàm đệ quy đuôi được coi là tốt hơn các hàm đệ quy không đuôi vì trình biên dịch có thể tối ưu hóa đệ quy đuôi.
Trình biên dịch thường thực hiện các thủ tục đệ quy bằng cách sử dụng ngăn xếp. Ngăn xếp này bao gồm tất cả các thông tin thích hợp, bao gồm các giá trị tham số, cho mỗi lệnh gọi đệ quy. Khi một thủ tục được gọi, thông tin của nó được đẩy vào ngăn xếp và khi chức năng kết thúc, thông tin sẽ được đưa ra khỏi ngăn xếp. Do đó, đối với các hàm không đệ quy đuôi, độ sâu ngăn xếp [lượng không gian ngăn xếp tối đa được sử dụng bất kỳ lúc nào trong quá trình biên dịch] là nhiều hơn.
Ý tưởng được các trình biên dịch sử dụng để tối ưu hóa các hàm đệ quy đuôi rất đơn giản, vì lời gọi đệ quy là câu lệnh cuối cùng, không còn gì để làm trong hàm hiện tại, vì vậy việc lưu khung ngăn xếp của hàm hiện tại là vô ích [Xem phần này để biết thêm
Hàm không đệ quy đuôi có thể được viết dưới dạng đệ quy đuôi để tối ưu hóa nó không?
Xét hàm sau để tính giai thừa của n.
Nó là một hàm không đệ quy đuôi. Mặc dù thoạt nhìn nó giống như một đệ quy đuôi. Nếu chúng ta xem xét kỹ hơn, chúng ta có thể thấy rằng giá trị được trả về bởi fact[n-1] được sử dụng trong fact[n]. Vì vậy, lời gọi đến fact[n-1] không phải là điều cuối cùng được thực hiện bởi fact[n]
C++
12056
12057
12058
12059
12060
12061
12062
12063
12064
12075
12066
12075
12076
12077
12078
12079
12072
1201
1202
12075
12078
1202
12078
120722
12080
12075
12082
12077
12078
12085
12078
1202
12088
120722
Java
12090
12091
12078
12093
12078
12095
12078
12097
12078
12099
12078
1207201
12078____17203
12078____17205
12078
120724
12075
1207209
12075
12076
12078
12077
1201
12079
1207216
120757
120758
1207219
1202
120772
1203
1201
1202
1207225____1772
120773
12078
120722
12078____17231
12078
1207233
120724
12073
1207236
12078
12077
1201____17240
1207241
1207242
12078
120722
120722
1207246
Python3
1207247
1207248
1207249
1207250
1207251
1207252
1207253
120777
1207255
12078
12079
1207258
120791
120791
120757
120783
1201
1202
120772
12078
1202
1207268
1207269
1207270
120798
120772
120758
1207274
1207275
12079
1207277
120791
120791
1207280
1207281
12078
120787
1207284
1207241
1207286
1207287
C#
12057
1207289
12090
12091
12078
12093
12078
12095
12078
12097
12078
12099
12078
1207201
12078____17203
12078____17205
12078
120724
12075
1207209
12075
12076
12078
12077
1201
12079
1207316
1207219
1202
12075
1201
1202
12078
12078
120722
12078____17326
12078____17328
12078
1207233
120724
12073
1207333
120722
1207335
PHP
1207336
12093
12095
12097
1207340
1207341
1207342
12032
1207209
1207345
120758
12077
12078
12079
120788
1207345
1207352
1202
12075
12078
1202
1207345
1207358
1207345
1207360
120722
12078
1207363
12078
1207365
1207366
1207367
1207368
Javascript
12030
12093
12095
12097
12099
1207201
1207203
1207205
12032
1207378
12077
12078
12079
1207316
1201
1202
12075
120740
12078
1202
12078
120722
1207391
1207392
12029
12055
Đầu ra
120
Hàm trên có thể được viết dưới dạng hàm đệ quy đuôi. Ý tưởng là sử dụng thêm một đối số và tích lũy giá trị giai thừa trong đối số thứ hai. Khi n về 0, trả về giá trị tích lũy
Dưới đây là cách triển khai sử dụng hàm đệ quy đuôi
C++
12056
12057
12058
12059
1207399
1207400
12075
1207402
12075
1207404
12077
12078
12079
1207408
1201
1202
1207411
12078
1202
1207414
120722
1207416
12064______175
12066
12075
1207421
1202
1207423
12080
12075
12082
12077
12078
12085
12078
1202
12088
120722
Java
1207434
12090
12091
12078____17438
12078
1207440
12078
120724
12075
1207444
12075
1207446
12075
1207404
12078
12077
1201
12079
1207453____1757
120758
1207219
1202
1207411
1201
1202
1207461
120772
1207463
12078
120722
12078
1207416
12078
120724
12075
1207209
12075
1207421
1202
1207475
120772
1207477
12078
1207391
12078
120724
1207233
12073
1207236
12078
12077
1201____17240
1207241
1207242
12078
120722
120722
1207246
Python3
1207495
1207496
120777
1207498
120791
120772
120783
12078
12079
120756______1791
120772
120783
1201
1202
1207510
12078
1202
1207270
120798
120772
1207516
1207269
1207404
1207274
1207275
120787
1207284
1207241
1207286
1207525
1207526
1207527
C#
1207528
12057
1207289
12090
12091
12078____17438
12078
1207440
12078
120724
12075
1207444
12075
1207446
12075
1207404
12078
12077
1201
12079
12072
1207219
1202
1207411
1201
1202
1207414
12078
120722
12078
1207416
12078
120724
12075
1207209
12075
1207421
1202
1207423
12078
1207391
12078
120724
1207233
12073
1207574
12078
12077
_______01____17578
12078
120722
120722
1207582
PHP
1207336
1207438
1207440
12032
1207444
1207345
1207589
1207590
120758
12077
12078
12079
120788
1207345
1207597
1202
1207590
1203
12078
1202
1207444
1207345
1207605
1207345
1207269
1207590
120773
120722
1207416
12032
1207209
1207345
120758
12077
12078
1202
1207444
1207345
1207621
120722
1207326
1207328
1207365
1207366
1207627
1207628
1207368
Javascript
12030
1207631
1207438
1207440
12032
1207635
12077
12078
12079
12072
1201
1202
1207411
120740
12078
1202
1207414
120722
120740
1207416
12032
1207378
12077
12078
1202
1207655
120722
1207391
1207392
1207659
12078
12055
Đầu ra
120
Các bài viết tiếp theo về chủ đề này.
- Loại bỏ cuộc gọi đuôi
- QuickSort Tail Call Optimization [Giảm khoảng trống trong trường hợp xấu nhất thành Log n ]
Vui lòng viết bình luận nếu bạn thấy bất cứ điều gì không chính xác hoặc bạn muốn chia sẻ thêm thông tin về chủ đề thảo luận ở trên