Tiếp tục đệ quy python phân số

Nếu bạn đã quen thuộc với các hàm trong Python, thì bạn sẽ biết rằng việc một hàm này gọi một hàm khác là điều khá phổ biến. Trong Python, một hàm cũng có thể gọi chính nó. Một hàm gọi chính nó được gọi là đệ quy và kỹ thuật sử dụng hàm đệ quy được gọi là đệ quy

Nội dung chính Hiển thị

Nó có vẻ đặc biệt đối với một chức năng để gọi chính nó, nhưng nhiều loại vấn đề lập trình được thể hiện tốt nhất bằng cách đệ quy. Khi bạn gặp phải một vấn đề như vậy, đệ quy là một công cụ không thể thiếu để bạn có trong bộ công cụ của mình

Đến cuối hướng dẫn này, bạn sẽ hiểu

  • Ý nghĩa của việc một hàm gọi chính nó một cách đệ quy
  • Cách thiết kế các hàm Python hỗ trợ đệ quy
  • Những yếu tố cần xem xét khi lựa chọn có hay không giải quyết vấn đề theo cách đệ quy
  • Cách triển khai hàm đệ quy trong Python

Sau đó, bạn sẽ nghiên cứu một số vấn đề lập trình Python sử dụng đệ quy và so sánh giải pháp đệ quy với giải pháp không đệ quy có thể so sánh được

Đệ quy là gì?

Từ đệ quy xuất phát từ từ tiếng Latin recurrere, có nghĩa là chạy hoặc đẩy nhanh trở lại, quay trở lại, hoàn nguyên hoặc lặp lại. Dưới đây là một số định nghĩa trực tuyến về đệ quy

  • Từ điển. com. Hành động hoặc quá trình quay trở lại hoặc chạy lại
  • từ điển mở. Hành động xác định một đối tượng [thường là một chức năng] theo chính đối tượng đó
  • từ điển miễn phí. Một phương pháp xác định một chuỗi các đối tượng, chẳng hạn như một biểu thức, hàm hoặc tập hợp, trong đó một số đối tượng ban đầu được cho trước và mỗi đối tượng kế tiếp được xác định theo các đối tượng trước đó

Định nghĩa đệ quy là định nghĩa trong đó thuật ngữ được xác định xuất hiện trong chính định nghĩa đó. Các tình huống tự giới thiệu thường xuất hiện trong cuộc sống thực, ngay cả khi chúng không được nhận ra ngay lập tức như vậy. Ví dụ: giả sử bạn muốn mô tả nhóm người tạo nên tổ tiên của bạn. Bạn có thể mô tả chúng theo cách này

Lưu ý cách khái niệm đang được định nghĩa, tổ tiên, xuất hiện trong định nghĩa của chính nó. Đây là một định nghĩa đệ quy

Trong lập trình, đệ quy có một ý nghĩa rất chính xác. Nó đề cập đến một kỹ thuật mã hóa trong đó một chức năng gọi chính nó

Tại sao sử dụng đệ quy?

Hầu hết các vấn đề lập trình đều có thể giải được mà không cần đệ quy. Vì vậy, nói đúng ra, đệ quy thường không cần thiết

Tuy nhiên, một số tình huống đặc biệt phù hợp với định nghĩa tự quy chiếu—ví dụ: định nghĩa về tổ tiên được trình bày ở trên. Nếu bạn đang nghĩ ra một thuật toán để xử lý trường hợp như vậy theo chương trình, thì một giải pháp đệ quy có thể sẽ rõ ràng và ngắn gọn hơn

Truyền tải cấu trúc dữ liệu dạng cây là một ví dụ điển hình khác. Vì đây là các cấu trúc lồng nhau nên chúng dễ dàng phù hợp với định nghĩa đệ quy. Một thuật toán không đệ quy để đi qua một cấu trúc lồng nhau có thể hơi rắc rối, trong khi một giải pháp đệ quy sẽ tương đối thanh lịch. Một ví dụ về điều này xuất hiện sau trong hướng dẫn này

Mặt khác, đệ quy không dành cho mọi tình huống. Dưới đây là một số yếu tố khác để xem xét

  • Đối với một số vấn đề, một giải pháp đệ quy, mặc dù có thể, sẽ khó xử hơn là thanh lịch
  • Việc triển khai đệ quy thường tiêu tốn nhiều bộ nhớ hơn so với các triển khai không đệ quy
  • Trong một số trường hợp, sử dụng đệ quy có thể dẫn đến thời gian thực hiện chậm hơn

Thông thường, khả năng đọc mã sẽ là yếu tố quyết định lớn nhất. Nhưng nó phụ thuộc vào hoàn cảnh. Các ví dụ được trình bày dưới đây sẽ giúp bạn cảm nhận được khi nào bạn nên chọn đệ quy

Đệ quy trong Python

Khi bạn gọi một hàm trong Python, trình thông dịch sẽ tạo một không gian tên cục bộ mới để các tên được xác định trong hàm đó không xung đột với các tên giống hệt nhau được xác định ở nơi khác. Một chức năng có thể gọi một chức năng khác và ngay cả khi cả hai đều xác định các đối tượng có cùng tên, tất cả đều hoạt động tốt vì các đối tượng đó tồn tại trong các không gian tên riêng biệt

Điều này cũng đúng nếu nhiều phiên bản của cùng một chức năng đang chạy đồng thời. Ví dụ, xét định nghĩa sau

def function[]:
    x = 10
    function[]

Khi

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1 thực thi lần đầu tiên, Python tạo một không gian tên và gán cho
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
2 giá trị
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3 trong không gian tên đó. Sau đó,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1 tự gọi đệ quy. Lần thứ hai
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1 chạy, trình thông dịch tạo một không gian tên thứ hai và gán
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3 cho
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
2 ở đó. Hai phiên bản của tên
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
2 này khác biệt với nhau và có thể cùng tồn tại mà không xung đột vì chúng nằm trong các không gian tên riêng biệt

Thật không may, việc chạy

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1 khi nó đứng tạo ra một kết quả kém truyền cảm hứng, như dấu vết sau đây cho thấy

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
0

Như đã viết, về lý thuyết,

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1 sẽ tiếp tục mãi mãi, tự gọi đi gọi lại mà không có bất kỳ cuộc gọi nào quay trở lại. Trong thực tế, tất nhiên, không có gì là thực sự mãi mãi. Máy tính của bạn chỉ có rất nhiều bộ nhớ và cuối cùng nó sẽ hết

Python không cho phép điều đó xảy ra. Trình thông dịch giới hạn số lần tối đa mà một hàm có thể gọi chính nó theo cách đệ quy và khi đạt đến giới hạn đó, nó sẽ đưa ra một ngoại lệ

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
21, như bạn thấy ở trên

Không có nhiều công dụng đối với một chức năng gọi chính nó một cách bừa bãi mà không có kết thúc. Nó gợi nhớ đến các hướng dẫn mà đôi khi bạn tìm thấy trên chai dầu gội đầu. "Lót, rửa sạch, lặp lại. ” Nếu bạn làm theo những hướng dẫn này theo đúng nghĩa đen, bạn sẽ gội đầu mãi mãi

Lỗ hổng hợp lý này rõ ràng đã xảy ra với một số nhà sản xuất dầu gội đầu, bởi vì một số chai dầu gội thay vì ghi “Tạo bọt, xả, lặp lại khi cần thiết. ” Điều đó cung cấp một điều kiện chấm dứt cho các hướng dẫn. Có lẽ, cuối cùng bạn sẽ cảm thấy tóc của mình đủ sạch để cân nhắc việc lặp lại thêm lần nữa là không cần thiết. Gội đầu sau đó có thể dừng lại

Tương tự, một chức năng gọi chính nó một cách đệ quy phải có một kế hoạch để cuối cùng dừng lại. Các hàm đệ quy thường tuân theo mẫu này

  • Có một hoặc nhiều trường hợp cơ sở có thể giải trực tiếp mà không cần đệ quy thêm
  • Mỗi cuộc gọi đệ quy di chuyển giải pháp dần dần đến gần trường hợp cơ sở

Bây giờ bạn đã sẵn sàng để xem nó hoạt động như thế nào với một số ví dụ

Bắt đầu. Đếm ngược đến không

Ví dụ đầu tiên là một hàm có tên là

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
22, lấy một số dương làm đối số và in các số từ đối số đã chỉ định xuống 0

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
4

Lưu ý cách

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
22 phù hợp với mô hình cho thuật toán đệ quy được mô tả ở trên

  • Trường hợp cơ sở xảy ra khi
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    24 bằng 0, tại điểm đó đệ quy dừng lại
  • Trong lời gọi đệ quy, đối số nhỏ hơn một giá trị hiện tại của
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    24, vì vậy mỗi lần đệ quy sẽ tiến gần hơn đến trường hợp cơ sở

Phiên bản của

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
22 hiển thị ở trên làm nổi bật rõ ràng trường hợp cơ bản và lệnh gọi đệ quy, nhưng có một cách ngắn gọn hơn để diễn đạt nó

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]

Đây là một triển khai không đệ quy có thể có để so sánh

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
2

Đây là trường hợp mà giải pháp không đệ quy ít nhất cũng rõ ràng và trực quan như giải pháp đệ quy, và có lẽ còn hơn thế nữa

Tính giai thừa

Ví dụ tiếp theo liên quan đến khái niệm toán học về giai thừa. Giai thừa của số nguyên dương n, ký hiệu là n. , được định nghĩa như sau

Nói cách khác, n. là tích của tất cả các số nguyên từ 1 đến n, kể cả

Giai thừa tự nó phù hợp với định nghĩa đệ quy đến nỗi các văn bản lập trình hầu như luôn bao gồm nó như một trong những ví dụ đầu tiên. Bạn có thể diễn đạt định nghĩa của n. đệ quy như thế này

Như với ví dụ hiển thị ở trên, có những trường hợp cơ bản có thể giải quyết được mà không cần đệ quy. Các trường hợp phức tạp hơn là rút gọn, nghĩa là chúng rút gọn thành một trong các trường hợp cơ bản

  • Các trường hợp cơ bản [n = 0 hoặc n = 1] có thể giải được mà không cần đệ quy
  • Đối với các giá trị của n lớn hơn 1, n. được xác định theo [n - 1]. , vì vậy giải pháp đệ quy dần dần tiếp cận trường hợp cơ sở

Ví dụ, tính toán đệ quy của 4. trông như thế này

Phép tính đệ quy của 4

Các tính toán của 4. , 3. , và 2. tạm dừng cho đến khi thuật toán đạt đến trường hợp cơ sở trong đó n = 1. Tại thời điểm đó, 1. có thể tính toán được mà không cần đệ quy thêm và các phép tính trì hoãn chạy đến khi hoàn thành

Xác định hàm giai thừa Python

Đây là một hàm Python đệ quy để tính giai thừa. Lưu ý mức độ ngắn gọn của nó và mức độ phản ánh định nghĩa được hiển thị ở trên

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
1

Một chút tô điểm cho chức năng này với một số câu lệnh

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
27 giúp hiểu rõ hơn về trình tự gọi và trả về

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3

Lưu ý cách tất cả các cuộc gọi đệ quy xếp chồng lên nhau. Hàm được gọi với

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
24 =
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
29,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
10,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
11 và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
12 liên tiếp trước khi bất kỳ lệnh gọi nào trả về. Cuối cùng, khi
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
24 là
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
12, vấn đề có thể được giải quyết mà không cần đệ quy nữa. Sau đó, mỗi lệnh gọi đệ quy được xếp chồng lên nhau sẽ thoát ra ngoài, trả về
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
12,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
11,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
17 và cuối cùng là
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
18 từ lệnh gọi ngoài cùng

Đệ quy không cần thiết ở đây. Bạn có thể triển khai lặp đi lặp lại

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
19 bằng cách sử dụng vòng lặp
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
30

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
7

Bạn cũng có thể triển khai giai thừa bằng cách sử dụng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
31 của Python, mà bạn có thể nhập từ mô-đun
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
32

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
0

Một lần nữa, điều này cho thấy rằng nếu một vấn đề có thể giải quyết được bằng đệ quy, thì cũng có khả năng sẽ có một số giải pháp không đệ quy khả thi. Thông thường, bạn sẽ chọn dựa trên cái nào dẫn đến mã trực quan và dễ đọc nhất

Một yếu tố khác cần xem xét là tốc độ thực thi. Có thể có sự khác biệt đáng kể về hiệu suất giữa các giải pháp đệ quy và không đệ quy. Trong phần tiếp theo, bạn sẽ khám phá thêm một chút về những khác biệt này

So sánh tốc độ triển khai giai thừa

Để đánh giá thời gian thực hiện, bạn có thể sử dụng hàm có tên là

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33 từ một mô-đun cũng có tên là
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
34. Chức năng này hỗ trợ một số định dạng khác nhau, nhưng bạn sẽ sử dụng định dạng sau trong hướng dẫn này

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33 đầu tiên thực hiện các lệnh có trong
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
36 được chỉ định. Sau đó, nó thực thi
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
37 số lượng đã cho của
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
38 và báo cáo thời gian thực hiện tích lũy tính bằng giây

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
00

Ở đây, tham số

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
39 gán cho
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
70 giá trị
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
71. Sau đó, ________ 233 bản in ________ 370 một trăm lần. Tổng thời gian thực hiện chỉ hơn 3/100 giây

Các ví dụ hiển thị bên dưới sử dụng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33 để so sánh việc triển khai đệ quy, lặp lại và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
31 của giai thừa từ bên trên. Trong mỗi trường hợp,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
76 chứa một chuỗi thiết lập xác định hàm
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
19 có liên quan. Sau đó,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33 thực thi
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
79 tổng cộng mười triệu lần và báo cáo việc thực thi tổng hợp

Đầu tiên, đây là phiên bản đệ quy

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
01

Tiếp theo là thực hiện lặp đi lặp lại

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
02

Cuối cùng, đây là phiên bản sử dụng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
31

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
03

Trong trường hợp này, việc triển khai lặp lại là nhanh nhất, mặc dù giải pháp đệ quy không thua xa. Phương pháp sử dụng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
31 là chậm nhất. Số dặm của bạn có thể sẽ thay đổi nếu bạn thử các ví dụ này trên máy của chính mình. Bạn chắc chắn sẽ không nhận được cùng thời gian và thậm chí bạn có thể không nhận được cùng thứ hạng

Nó có quan trọng không?

Nếu bạn sẽ gọi một chức năng nhiều lần, bạn có thể cần tính đến tốc độ thực thi khi chọn cách triển khai. Mặt khác, nếu chức năng sẽ chạy tương đối không thường xuyên, thì sự khác biệt về thời gian thực hiện có thể sẽ không đáng kể. Trong trường hợp đó, tốt hơn hết bạn nên chọn cách triển khai có vẻ thể hiện giải pháp cho vấn đề một cách rõ ràng nhất

Đối với giai thừa, thời gian được ghi ở trên cho thấy việc triển khai đệ quy là một lựa chọn hợp lý

Thành thật mà nói, nếu bạn đang viết mã bằng Python, bạn hoàn toàn không cần triển khai hàm giai thừa. Nó đã có sẵn trong mô-đun

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
03 tiêu chuẩn

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
04

Có lẽ bạn sẽ quan tâm để biết điều này hoạt động như thế nào trong bài kiểm tra thời gian

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05

Ồ.

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
04 hoạt động tốt hơn so với cách triển khai tốt nhất trong ba cách triển khai khác được hiển thị ở trên với hệ số xấp xỉ 10

Một chức năng được triển khai trong C hầu như sẽ luôn nhanh hơn một chức năng tương ứng được triển khai trong Python thuần túy

Duyệt một danh sách lồng nhau

Ví dụ tiếp theo liên quan đến việc truy cập từng mục trong cấu trúc danh sách lồng nhau. Hãy xem xét danh sách Python sau đây

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
06

Như sơ đồ sau đây cho thấy,

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05 chứa hai danh sách con. Bản thân danh sách con đầu tiên chứa một danh sách con khác

Giả sử bạn muốn đếm số phần tử lá trong danh sách này—các đối tượng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
06 cấp thấp nhất—như thể bạn đã làm phẳng danh sách. Các phần tử lá là ________ 407, ________ 408, _______ 409,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
30,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
31,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
32,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
34,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
35 và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
36, vì vậy câu trả lời phải là
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3

Chỉ gọi

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
38 trong danh sách không đưa ra câu trả lời chính xác

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
38 đếm các đối tượng ở cấp cao nhất của
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05, đó là ba phần tử lá ________ 407,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
33 và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
36 và hai danh sách con ________ 1004 và ________ 1005

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
07

Những gì bạn cần ở đây là một chức năng duyệt qua toàn bộ cấu trúc danh sách, bao gồm cả danh sách con. Thuật toán diễn ra như thế này

  1. Đi qua danh sách, kiểm tra lần lượt từng mục
  2. Nếu bạn tìm thấy một phần tử lá, sau đó thêm nó vào số lượng tích lũy
  3. Nếu gặp sublist thì làm như sau
    • Thả xuống danh sách phụ đó và tương tự đi qua nó
    • Sau khi bạn đã sử dụng hết danh sách phụ, hãy quay lại, thêm các phần tử từ danh sách phụ vào tổng số tích lũy và tiếp tục duyệt qua danh sách gốc mà bạn đã dừng lại

Lưu ý bản chất tự giới thiệu của mô tả này. Đi qua danh sách. Nếu bạn gặp một danh sách phụ, thì hãy đi qua danh sách đó một cách tương tự. Tình huống này yêu cầu đệ quy

Duyệt qua danh sách lồng nhau theo cách đệ quy

Đệ quy phù hợp với vấn đề này rất độc đáo. Để giải quyết nó, bạn cần có khả năng xác định xem một mục danh sách nhất định có phải là mục lá hay không. Để làm được điều đó, bạn có thể sử dụng hàm Python tích hợp sẵn

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
006

Trong trường hợp của danh sách

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05, nếu một mục là một thể hiện của loại
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
008, thì đó là một danh sách con. Mặt khác, nó là một mục lá

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
08

Bây giờ bạn đã có sẵn các công cụ để triển khai một hàm đếm các phần tử lá trong danh sách, chiếm các danh sách con theo cách đệ quy

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
09

Nếu bạn chạy

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
009 trên một số danh sách, bao gồm danh sách
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05 được xác định ở trên, bạn sẽ nhận được điều này

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
40

Như với ví dụ giai thừa, việc thêm một số câu lệnh

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
27 giúp chứng minh chuỗi các cuộc gọi đệ quy và giá trị trả về

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
41

Đây là một bản tóm tắt về những gì đang xảy ra trong ví dụ trên

  • Dòng 9.
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    012 là
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    013, vì vậy
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    009 đã tìm thấy một danh sách phụ
  • Dòng 11. Hàm tự gọi đệ quy để đếm các mục trong danh sách con, sau đó thêm kết quả vào tổng tích lũy
  • Dòng 12.
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    012 là
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    016 nên
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    009 gặp phải lá mục
  • Dòng 14. Hàm tăng tổng tích lũy lên một để tính cho mục lá

Đầu ra từ

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
009 khi nó được thực thi trên danh sách
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
05 bây giờ trông như thế này

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
42

Mỗi khi cuộc gọi đến

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
009 kết thúc, nó sẽ trả về số lượng phần tử lá mà nó đã kiểm tra trong danh sách được truyền cho nó. Cuộc gọi cấp cao nhất trả về
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
3, vì nó phải

Duyệt qua danh sách lồng nhau không đệ quy

Giống như các ví dụ khác được hiển thị cho đến nay, việc duyệt qua danh sách này không yêu cầu đệ quy. Bạn cũng có thể hoàn thành nó lặp đi lặp lại. Đây là một khả năng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
43

Nếu bạn chạy phiên bản không đệ quy này của

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
009 trên cùng một danh sách như được hiển thị trước đó, bạn sẽ nhận được kết quả tương tự

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
40

Chiến lược được sử dụng ở đây sử dụng một ngăn xếp để xử lý các danh sách con lồng nhau. Khi phiên bản này của

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
009 gặp một danh sách phụ, nó sẽ đẩy danh sách đang được xử lý và chỉ mục hiện tại trong danh sách đó lên một ngăn xếp. Khi nó đã đếm danh sách con, hàm sẽ bật danh sách mẹ và chỉ mục khỏi ngăn xếp để nó có thể tiếp tục đếm ở nơi nó dừng lại

Trên thực tế, về cơ bản, điều tương tự cũng xảy ra trong quá trình triển khai đệ quy. Khi bạn gọi một hàm theo cách đệ quy, Python sẽ lưu trạng thái của phiên bản đang thực thi trên một ngăn xếp để cuộc gọi đệ quy có thể chạy. Khi cuộc gọi đệ quy kết thúc, trạng thái được bật ra khỏi ngăn xếp để phiên bản bị gián đoạn có thể tiếp tục. Đó là cùng một khái niệm, nhưng với giải pháp đệ quy, Python đang thực hiện công việc tiết kiệm trạng thái cho bạn

Lưu ý mức độ ngắn gọn và dễ đọc của mã đệ quy khi so sánh với phiên bản không đệ quy

Truyền tải danh sách lồng nhau đệ quy và không đệ quy

Đây là trường hợp sử dụng đệ quy chắc chắn là một lợi thế

Phát hiện Palindromes

Việc lựa chọn có sử dụng đệ quy để giải quyết vấn đề hay không phụ thuộc phần lớn vào bản chất của vấn đề. Ví dụ, thừa số tự nhiên chuyển thành cách thực hiện đệ quy, nhưng giải pháp lặp lại cũng khá đơn giản. Trong trường hợp đó, nó được cho là một sự tung lên

Vấn đề duyệt danh sách là một câu chuyện khác. Trong trường hợp đó, giải pháp đệ quy rất tao nhã, trong khi giải pháp không đệ quy là cồng kềnh nhất

Đối với vấn đề tiếp theo, sử dụng đệ quy được cho là ngớ ngẩn

Palindrome là một từ đọc ngược giống như đọc xuôi. Ví dụ bao gồm các từ sau

  • Xe đua
  • Cấp độ
  • Chèo xuồng
  • người hồi sinh
  • công dân

Nếu được yêu cầu nghĩ ra một thuật toán để xác định xem một chuỗi có phải là đối xứng hay không, có lẽ bạn sẽ nghĩ ra thứ gì đó như “Đảo ngược chuỗi và xem nó có giống với chuỗi ban đầu không. ” Bạn không thể nhận được nhiều đơn giản hơn thế

Thậm chí hữu ích hơn, cú pháp cắt lát

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
024 của Python để đảo ngược chuỗi cung cấp một cách thuận tiện để mã hóa nó

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
45

Điều này là rõ ràng và ngắn gọn. Hầu như không cần tìm kiếm giải pháp thay thế. Nhưng để giải trí, hãy xem xét định nghĩa đệ quy này của một bảng màu

  • trường hợp cơ sở. Một chuỗi rỗng và một chuỗi bao gồm một ký tự đơn lẻ vốn dĩ là palindromic
  • đệ quy rút gọn. Một chuỗi có độ dài bằng hai hoặc lớn hơn là một đối xứng nếu nó thỏa mãn cả hai tiêu chí này
    1. Ký tự đầu và cuối giống nhau
    2. Chuỗi con giữa ký tự đầu tiên và ký tự cuối cùng là một bảng màu

Cắt lát cũng là bạn của bạn ở đây. Đối với chuỗi

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
025, lập chỉ mục và cắt cho các chuỗi con sau

  • Ký tự đầu tiên là
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    026
  • Ký tự cuối cùng là
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    027
  • Chuỗi con giữa ký tự đầu tiên và ký tự cuối cùng là
    def countdown[n]:
        print[n]
        if n > 0:
            countdown[n - 1]
    
    028

Vì vậy, bạn có thể định nghĩa đệ quy

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
029 như thế này

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
46

Đó là một bài tập thú vị để suy nghĩ theo cách đệ quy, ngay cả khi nó không đặc biệt cần thiết

Sắp xếp với Quicksort

Ví dụ cuối cùng được trình bày, giống như duyệt danh sách lồng nhau, là một ví dụ điển hình về một vấn đề gợi ý một cách tiếp cận đệ quy một cách rất tự nhiên. Thuật toán Quicksort là một thuật toán sắp xếp hiệu quả được phát triển bởi nhà khoa học máy tính người Anh Tony Hoare vào năm 1959

Quicksort là thuật toán chia để trị. Giả sử bạn có một danh sách các đối tượng để sắp xếp. Bạn bắt đầu bằng cách chọn một mục trong danh sách, được gọi là mục trục. Đây có thể là bất kỳ mục nào trong danh sách. Sau đó, bạn phân vùng danh sách thành hai danh sách con dựa trên mục trục và sắp xếp đệ quy các danh sách con

Các bước của thuật toán như sau

  • Chọn mục trục
  • Phân vùng danh sách thành hai danh sách con
    1. Những mặt hàng nhỏ hơn mặt hàng trục
    2. Những mục lớn hơn mục trục
  • Sắp xếp nhanh các danh sách con theo cách đệ quy

Mỗi phân vùng tạo ra các danh sách con nhỏ hơn, vì vậy thuật toán được rút gọn. Các trường hợp cơ sở xảy ra khi các danh sách con trống hoặc có một phần tử, vì chúng vốn đã được sắp xếp

Chọn mục Pivot

Thuật toán Quicksort sẽ hoạt động bất kể mục nào trong danh sách là mục trục. Nhưng một số lựa chọn tốt hơn những lựa chọn khác. Hãy nhớ rằng khi phân vùng, hai danh sách con được tạo. một với các mục nhỏ hơn mục trục và một với các mục lớn hơn mục trục. Lý tưởng nhất là hai danh sách con có độ dài gần bằng nhau

Hãy tưởng tượng rằng danh sách ban đầu của bạn để sắp xếp có tám mục. Nếu mỗi phân vùng dẫn đến các danh sách con có độ dài gần bằng nhau, thì bạn có thể tiếp cận các trường hợp cơ bản trong ba bước

Phân vùng tối ưu, danh sách tám mục

Ở đầu kia của quang phổ, nếu sự lựa chọn mục trục của bạn đặc biệt không may mắn, thì mỗi phân vùng sẽ dẫn đến một danh sách con chứa tất cả các mục ban đầu ngoại trừ mục trục và một danh sách con khác trống. Trong trường hợp đó, phải mất bảy bước để rút gọn danh sách thành các trường hợp cơ bản

Phân vùng dưới mức tối ưu, Danh sách tám mục

Thuật toán Quicksort sẽ hiệu quả hơn trong trường hợp đầu tiên. Nhưng bạn cần biết trước một số điều về bản chất của dữ liệu bạn đang sắp xếp để chọn các mục trục tối ưu một cách có hệ thống. Trong mọi trường hợp, không có sự lựa chọn nào là tốt nhất cho mọi trường hợp. Vì vậy, nếu bạn đang viết một hàm Quicksort để xử lý trường hợp chung, thì việc lựa chọn mục trục có phần tùy ý

Mục đầu tiên trong danh sách là một lựa chọn phổ biến, cũng như mục cuối cùng. Chúng sẽ hoạt động tốt nếu dữ liệu trong danh sách được phân phối khá ngẫu nhiên. Tuy nhiên, nếu dữ liệu đã được sắp xếp hoặc thậm chí gần như vậy, thì những dữ liệu này sẽ dẫn đến phân vùng dưới mức tối ưu như được hiển thị ở trên. Để tránh điều này, một số thuật toán Quicksort chọn mục ở giữa trong danh sách làm mục trục

Một tùy chọn khác là tìm trung vị của các mục đầu tiên, cuối cùng và ở giữa trong danh sách và sử dụng mục đó làm mục xoay vòng. Đây là chiến lược được sử dụng trong mã mẫu bên dưới

Thực hiện phân vùng

Khi bạn đã chọn mục trục, bước tiếp theo là phân vùng danh sách. Một lần nữa, mục tiêu là tạo hai danh sách phụ, một danh sách chứa các mục nhỏ hơn mục trục và danh sách còn lại chứa các mục lớn hơn

Bạn có thể thực hiện điều này trực tiếp tại chỗ. Nói cách khác, bằng cách hoán đổi các mục, bạn có thể xáo trộn các mục trong danh sách xung quanh cho đến khi mục trục nằm ở giữa, tất cả các mục nhỏ hơn ở bên trái và tất cả các mục lớn hơn ở bên phải. Sau đó, khi bạn sắp xếp nhanh các danh sách con theo cách đệ quy, bạn sẽ chuyển các phần của danh sách sang bên trái và bên phải của mục trục

Ngoài ra, bạn có thể sử dụng khả năng thao tác danh sách của Python để tạo danh sách mới thay vì thao tác trên danh sách ban đầu tại chỗ. Đây là cách tiếp cận được thực hiện trong đoạn mã dưới đây. Thuật toán như sau

  • Chọn mục trục bằng phương pháp trung bình của ba được mô tả ở trên
  • Sử dụng mục trục, tạo ba danh sách phụ
    1. Các mục trong danh sách ban đầu nhỏ hơn mục trục
    2. Bản thân mục trục
    3. Các mục trong danh sách ban đầu lớn hơn mục trục
  • Danh sách Quicksort đệ quy 1 và 3
  • Nối cả ba danh sách lại với nhau

Lưu ý rằng điều này liên quan đến việc tạo danh sách con thứ ba chứa chính mục trục. Một lợi thế của phương pháp này là nó xử lý trơn tru trường hợp mục trục xuất hiện trong danh sách nhiều lần. Trong trường hợp đó, danh sách 2 sẽ có nhiều hơn một phần tử

Sử dụng Triển khai Quicksort

Bây giờ nền tảng đã sẵn sàng, bạn đã sẵn sàng chuyển sang thuật toán Quicksort. Đây là mã Python

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
47

Đây là những gì mỗi phần của

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
030 đang làm

  • dòng 4. Các trường hợp cơ bản khi danh sách trống hoặc chỉ có một phần tử
  • Dòng 7 đến 13. Tính toán mục trục theo phương pháp trung bình của ba
  • Dòng 14 đến 18. Tạo ba danh sách phân vùng
  • Dòng 20 đến 24. Sắp xếp đệ quy và sắp xếp lại danh sách phân vùng

Dưới đây là một số ví dụ về

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
030 đang hoạt động

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
48

Đối với mục đích thử nghiệm, bạn có thể xác định một hàm ngắn để tạo danh sách các số ngẫu nhiên giữa

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
12 và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
033

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
49

Bây giờ bạn có thể sử dụng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
034 để kiểm tra
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
030

>>>

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
0

Để hiểu rõ hơn về cách thức hoạt động của

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
030, hãy xem sơ đồ bên dưới. Điều này cho thấy trình tự đệ quy khi sắp xếp danh sách mười hai phần tử

Thuật toán Quicksort, Danh sách 12 phần tử

Trong bước đầu tiên, các giá trị danh sách đầu tiên, giữa và cuối cùng lần lượt là

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
037,
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
038 và
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
039. Trung bình là
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
037, do đó trở thành mục trục. Phân vùng đầu tiên sau đó bao gồm các danh sách con sau

Các mục trong danh sách con
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
041Các mục nhỏ hơn mục xoay vòng
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
042Chính mục xoay vòng
def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
043Các mục lớn hơn mục xoay vòng

Mỗi danh sách con sau đó được phân vùng đệ quy theo cùng một cách cho đến khi tất cả các danh sách con chứa một phần tử hoặc trống. Khi các cuộc gọi đệ quy trở lại, các danh sách được sắp xếp lại theo thứ tự được sắp xếp. Lưu ý rằng trong bước thứ hai đến bước cuối cùng ở bên trái, mục xoay vòng

def countdown[n]:
    print[n]
    if n > 0:
        countdown[n - 1]
044 xuất hiện trong danh sách hai lần, do đó, danh sách mục xoay vòng có hai phần tử

Phần kết luận

Điều đó kết thúc hành trình của bạn thông qua đệ quy, một kỹ thuật lập trình trong đó một hàm gọi chính nó. Đệ quy không phải lúc nào cũng phù hợp với mọi nhiệm vụ. Nhưng một số vấn đề về lập trình hầu như kêu gào vì nó. Trong những tình huống đó, đó là một kỹ thuật tuyệt vời để bạn tùy ý sử dụng

Trong hướng dẫn này, bạn đã học

  • Ý nghĩa của việc một hàm gọi chính nó một cách đệ quy
  • Cách thiết kế các hàm Python hỗ trợ đệ quy
  • Những yếu tố cần xem xét khi lựa chọn có hay không giải quyết vấn đề theo cách đệ quy
  • Cách triển khai hàm đệ quy trong Python

Bạn cũng đã xem một số ví dụ về thuật toán đệ quy và so sánh chúng với các giải pháp không đệ quy tương ứng

Bây giờ bạn sẽ ở một vị trí thuận lợi để nhận ra khi nào đệ quy được yêu cầu và sẵn sàng sử dụng nó một cách tự tin khi cần thiết. Nếu bạn muốn khám phá thêm về đệ quy trong Python, hãy xem Tư duy đệ quy trong Python

Chủ Đề