Tut của bạn programmer2010 viết bên forum cũ, bạn này hình như bỏ vn-zoom khá lâu rồi, thấy còn giá trị nên tui đem qua đây
Ngôn ngữ: C/C++. Có sử dụng cấu trúc của C++, nhưng có thể cài đặt dễ dàng trong C.
Mục tiêu: Nắm được cách khử đệ quy tuyến tính, nhị phân để giải quyết trường hợp đệ quy phi tuyến.
Mỗi chương trình có một vùng nhớ riêng đặc biệt gọi là stack, một cấu trúc "Last In First Out", hay "vào trước, ra sau". Cấu trúc Stack có hai thao tác chính: Đẩy vào đỉnh stack (Push) và Lấy ra từ đỉnh stack (Pop). Khi gọi hàm, các tham số được hàm gọi (caller) đẩy hết lên stack ngược với thứ tự xuất hiện của chúng, cuối cùng là địa chỉ của câu lệnh tiếp theo sau lời gọi để quay trở về.
Ngoài ra, các biến tạm (không static) của hàm cũng được lưu trữ trong stack. Ngay sau khi hàm được gọi (callee) khởi chạy, chính nó sẽ dành chỗ cho những biến này (bằng lệnh máy) và trả lại phần stack đó trước khi trả quyền điều khiển cho caller.
Nếu callee không phải void thì callee sẽ đẩy giá trị trả về vào stack cho caller. Dù gì thì dọn stack vẫn là nhiệm vụ của caller, sau đó nó sẽ tiếp tục chạy.
Stack frame: (từ đỉnh xuống)
- Biến local của callee
- Địa chỉ trở về caller
- t,u,v (hay x,y,z)
- Biến local của caller
- Địa chỉ trở về caller trên nó
................................
# Một hàm đệ quy hay được đề cập là hàm tính giai thừa.
Đệ quy đuôi (tail recursion) nghĩa là hàm có lời gọi đệ quy là câu lệnh cuối cùng. Đây là trường hợp khử đệ quy dễ nhất vì máy không cần lưu trạng thái vào stack mà chỉ việc sửa lại tham số rồi "nhảy" trở lại phần đầu của hàm.
# Phân tích đoạn code hồi nãy thì thực sự phần đệ quy là:
vậy đây không phải là dạng đệ quy đuôi. Để trở thành đệ quy đuôi thì thì bên lập trình hàm sẽ thêm một tham số lưu kết quả của bài toán lớn.
Tất nhiên nhìn vào rất là chuối, nhưng từ đây dễ dàng viết ra 1 đoạn code tương đương như sau:
Vậy là ta khử được dạng đệ quy đuôi. Bây giờ ta sẽ qua dạng còn lại của đệ quy tuyến tính.
Vấn đề: In ngược danh sách liên kết (DSLK) đơn.
DSLK đơn là cấu trúc móc xích với mỗi mắt xích gồm dữ liệu (data) và một con trỏ đến mắt xích tiếp theo (next). Do DSLK đơn chỉ duyệt theo 1 chiều nên dữ liệu các node được đảo ngược ngầm qua tác động của stack.
Để khử đệ quy dạng này thì có thể nói cách duy nhất là tạo 1 stack riêng thay cho call stack của chương trình. Trong STL của C++ đã có cấu trúc std::stack với ba hàm quan trọng:
- push(value): đẩy vào
- pop(): lấy ra
- empty(): rỗng hay không rỗng.
Ta sẽ khai báo: std::stack<Node*> s;
Ở phần đầu ta thấy caller vừa đẩy tham số lên stack vừa dọn tham số ra khỏi stack. Hãy ghi nhớ kĩ điều này. Và khi stack này rỗng thì quá trình đệ quy của ta chấm dứt, nên ta sẽ cần vòng:
while(s.empty() != false) {
}
Dòng đầu tiên của "code 4" cho thấy rằng p==NULL là điều kiện dừng đệ quy. Và lời gọi đệ quy xảy ra ngay sau đó nên ta viết:
if(p!=NULL) { s.push(p->next); }
Đến đây ta phải nhìn thấy trước 1 bước. Sau khi gọi như vậy thì p của callee tức là p->next của caller. Vậy p phải trở thành p->next sau khi push.
if(p!=NULL) { s.push(p = p->next); }
Ta thấy rằng khi p==NULL thì không cần push nữa nên ta thay if(p!=NULL) bằng while(p!=NULL).
Trở lại hàm chính: callee nhận được tham số NULL và trả quyền cho caller. Lúc này ta phải dọn stack và xử lí nó. Vậy đoạn code làm việc này sẽ là:
Phần hoàn chỉnh code xin dành cho bạn đọc.
Tóm lại, điều ta cần nhớ là:
- Nếu không phải đệ quy đuôi thì ta biến đổi thành đệ quy đuôi HOẶC thay lời gọi đệ quy bằng lệnh push các tham số vào stack.
- Nếu sử dụng stack riêng thì ta xác định khi nào dừng đệ quy, sau đó xử lí dữ liệu và pop khỏi stack.
- Để biến đổi thành đệ quy đuôi thì hàm phải có dạng aggregate (tạm dịch là tổng hợp), hay bên lập trình hàm gọi là fold: r := fold(r, S, *) với S là dữ liệu, r là biến kết quả và *: S, r -> r. Đồng thời hàm nó biểu diễn phải có tính giao hoán thì mới đổi trực tiếp được.
Ví dụ như hàm tính giai thừa thì r là tích, S là số tự nhiên trong dãy và * là phép nhân. Nếu * là phép nhân ma trận thì do AB tính được chưa chắc BA tính được nên không thể đổi ra đệ quy đuôi. Phép tính trung bình không thể dùng đệ quy đuôi do không có chỗ cho N. avg: S -> fold(r, S, +)/|S|
Thực ra có thể sửa lại thành while(p!=NULL) { s.push(p); p=p->next; }. Khi đó DSLK rỗng <=> stack rỗng và lúc đó không có gì xảy ra.
Xong phần khử đệ quy tuyến tính.
Khử đệ quy cho 1 số dạng hàm
Yêu cầu tiên quyết: (Requisite) Khai báo và sử dụng được hàm trong C/C++. Hiểu được khái niệm đệ quy và viết được chương trình đệ quy.
Ngôn ngữ: C/C++. Có sử dụng cấu trúc của C++, nhưng có thể cài đặt dễ dàng trong C.
Mục tiêu: Nắm được cách khử đệ quy tuyến tính, nhị phân để giải quyết trường hợp đệ quy phi tuyến.
Mỗi chương trình có một vùng nhớ riêng đặc biệt gọi là stack, một cấu trúc "Last In First Out", hay "vào trước, ra sau". Cấu trúc Stack có hai thao tác chính: Đẩy vào đỉnh stack (Push) và Lấy ra từ đỉnh stack (Pop). Khi gọi hàm, các tham số được hàm gọi (caller) đẩy hết lên stack ngược với thứ tự xuất hiện của chúng, cuối cùng là địa chỉ của câu lệnh tiếp theo sau lời gọi để quay trở về.
Ngoài ra, các biến tạm (không static) của hàm cũng được lưu trữ trong stack. Ngay sau khi hàm được gọi (callee) khởi chạy, chính nó sẽ dành chỗ cho những biến này (bằng lệnh máy) và trả lại phần stack đó trước khi trả quyền điều khiển cho caller.
Nếu callee không phải void thì callee sẽ đẩy giá trị trả về vào stack cho caller. Dù gì thì dọn stack vẫn là nhiệm vụ của caller, sau đó nó sẽ tiếp tục chạy.
PHP:
void caller() {
//đẩy x,y,z lên stack
//đẩy địa chỉ trở về lên stack
callee(x,y,z)
//dọn stack (x,y,z)
}
void callee(int t, int u, int v) {
//dành chỗ cho biến nội
// *sử dụng* t,u,v từ stack
.......
//thu lại vùng nhớ
//lấy địa chỉ nhảy về từ stack và trả quyền điều khiển cho caller
}
Stack frame: (từ đỉnh xuống)
- Biến local của callee
- Địa chỉ trở về caller
- t,u,v (hay x,y,z)
- Biến local của caller
- Địa chỉ trở về caller trên nó
................................
Đệ quy tuyến tính
# Một hàm đệ quy hay được đề cập là hàm tính giai thừa.
PHP:
// code 1
uint64_t giaiThua(int32_t n) { return n<=0? 1 : giaiThua(n-1)*n; }
# Phân tích đoạn code hồi nãy thì thực sự phần đệ quy là:
PHP:
int t = giaiThua(n-1)*n;
return t;
PHP:
//code 2
uint64_t giaiThua(int32_t n, int64_t kq) {
return n<=0 ? kq : giaiThua(n-1,kq*n);
}
// lời gọi: t = giaiThua(n,1);
PHP:
// code 3
uint64_t giaiThua(int32_t n) {
uint64_t kq=1;
while(n>0) { kq=kq*n; n=n-1; }
return kq;
}
Vấn đề: In ngược danh sách liên kết (DSLK) đơn.
DSLK đơn là cấu trúc móc xích với mỗi mắt xích gồm dữ liệu (data) và một con trỏ đến mắt xích tiếp theo (next). Do DSLK đơn chỉ duyệt theo 1 chiều nên dữ liệu các node được đảo ngược ngầm qua tác động của stack.
PHP:
// code 4
void inNguoc(Node* p) {
if(p==NULL) return;
inNguoc(p->next);
print(p->data); //truy cập phần tử data trong node
}
- push(value): đẩy vào
- pop(): lấy ra
- empty(): rỗng hay không rỗng.
Ta sẽ khai báo: std::stack<Node*> s;
Ở phần đầu ta thấy caller vừa đẩy tham số lên stack vừa dọn tham số ra khỏi stack. Hãy ghi nhớ kĩ điều này. Và khi stack này rỗng thì quá trình đệ quy của ta chấm dứt, nên ta sẽ cần vòng:
while(s.empty() != false) {
}
Dòng đầu tiên của "code 4" cho thấy rằng p==NULL là điều kiện dừng đệ quy. Và lời gọi đệ quy xảy ra ngay sau đó nên ta viết:
if(p!=NULL) { s.push(p->next); }
Đến đây ta phải nhìn thấy trước 1 bước. Sau khi gọi như vậy thì p của callee tức là p->next của caller. Vậy p phải trở thành p->next sau khi push.
if(p!=NULL) { s.push(p = p->next); }
Ta thấy rằng khi p==NULL thì không cần push nữa nên ta thay if(p!=NULL) bằng while(p!=NULL).
Trở lại hàm chính: callee nhận được tham số NULL và trả quyền cho caller. Lúc này ta phải dọn stack và xử lí nó. Vậy đoạn code làm việc này sẽ là:
PHP:
//code 5a
if(s.empty()!=false) s.pop(); //lấy NULL ra trước
while(s.empty()!=false) {
Node* t = s.pop();
print(t->data);
}
Tóm lại, điều ta cần nhớ là:
- Nếu không phải đệ quy đuôi thì ta biến đổi thành đệ quy đuôi HOẶC thay lời gọi đệ quy bằng lệnh push các tham số vào stack.
- Nếu sử dụng stack riêng thì ta xác định khi nào dừng đệ quy, sau đó xử lí dữ liệu và pop khỏi stack.
- Để biến đổi thành đệ quy đuôi thì hàm phải có dạng aggregate (tạm dịch là tổng hợp), hay bên lập trình hàm gọi là fold: r := fold(r, S, *) với S là dữ liệu, r là biến kết quả và *: S, r -> r. Đồng thời hàm nó biểu diễn phải có tính giao hoán thì mới đổi trực tiếp được.
Ví dụ như hàm tính giai thừa thì r là tích, S là số tự nhiên trong dãy và * là phép nhân. Nếu * là phép nhân ma trận thì do AB tính được chưa chắc BA tính được nên không thể đổi ra đệ quy đuôi. Phép tính trung bình không thể dùng đệ quy đuôi do không có chỗ cho N. avg: S -> fold(r, S, +)/|S|
Thực ra có thể sửa lại thành while(p!=NULL) { s.push(p); p=p->next; }. Khi đó DSLK rỗng <=> stack rỗng và lúc đó không có gì xảy ra.
Xong phần khử đệ quy tuyến tính.