Bài giảng Ngôn ngữ lập trình - Bài 9: Đệ quy - Lê Nguyễn Tuấn Thành
Tóm tắt Bài giảng Ngôn ngữ lập trình - Bài 9: Đệ quy - Lê Nguyễn Tuấn Thành: ...– BỨC TRANH LỚN Tổng quan về hàm đệ quy thành công: Một hoặc nhiều trường hợp khi hàm hoàn thiện những nhiệm vụ của nó bằng cách: Tạo một hoặc nhiều lời gọi đệ quy để giải quyết những phiên bản nhỏ hơn của tác vụ gốc Được gọi là “các trường hợp đệ quy” Một hoặc nhiều trường hợp ...ụng nhiều bộ nhớ Ít code hơn, giải pháp ngắn gọn hơn (elegent solution) 16 HÀM ĐỆ QUY TRẢ VỀ MỘT GIÁ TRỊ Đệ quy không chỉ giới hạn với các hàm void Có thể trả về giá trị của bất kỳ kiểu nào Cùng một kỹ thuật: 1. Một hoặc nhiều trường hợp có giá trị trả về được tính toán bởi l...bảo tính đến trường hợp cơ sở 24 KỸ THUẬT THIẾT KẾ ĐỆ QUY Không lưu vết toàn bộ chuỗi đệ quy Chỉ kiểm tra 3 thuộc tính: 1. Không đệ quy vô hạn 2. Các trường hợp dừng trả về giá trị đúng 3. Các trường hợp đệ quy trả về giá trị đúng 25 KIỂM TRA THIẾT KẾ ĐỆ QUY: HÀM POWER() Kiể...
NGÔN NGỮ LẬP TRÌNH Bài 9: Đệ Quy Giảng viên: Lê Nguyễn Tuấn Thành Email: thanhlnt@tlu.edu.vn Bộ Môn Công Nghệ Phần Mềm – Khoa CNTT Trường Đại Học Thủy Lợi NỘI DUNG Đệ quy với hàm void Truy vết lời gọi đệ quy Đệ quy vô hạn (infinite recursion), tràn (overflows) Đệ quy với hàm trả về giá trị Hàm Power() Suy nghĩ theo kiểu đệ quy Kỹ thuật thiết kế đệ quy Tìm kiếm nhị phân 2 Bài giảng có sử dụng hình vẽ trong cuốn sách “Practical Debugging in C++, A. Ford and T. Teorey, Prentice Hall, 2002” GIỚI THIỆU VỀ ĐỆ QUY (RECURSION) Một hàm gọi chính nó Trong định nghĩa của hàm đó, có lời gọi đến chính hàm đó C++ cho phép đệ quy Giống như phần lớn ngôn ngữ lập trình bậc cao Có thể là một kỹ thuật lập trình hữu ích Có những giới hạn 3 ĐỆ QUY VỚI HÀM VOID Chia để trị (Devide and Conquer) Kỹ thuật thiết kế cơ bản Chia các tác vụ lớn thành các tác vụ con Tác vụ con có thể là phiên bản nhỏ hơn của tác vụ gốc! Khi đó gọi là đệ quy 4 VÍ DỤ ĐỆ QUY VỚI HÀM VOID Xem xét tác vụ sau: Tìm kiếm một giá trị trong danh sách Tác vụ con 1: tìm kiếm nửa đầu của danh sách Tác vụ con 2: tìm kiếm nửa sau của danh sách Các tác vụ con là phiên bản nhỏ hơn của tác vụ gốc! Khi điều này xảy ra, hàm đệ quy có thể được sử dụng 5 ĐỆ QUY VỚI HÀM VOID: SỐ THEO CHIỀU DỌC Tác vụ: hiển thị các chữ số của một số nguyên theo chiều dọc, mỗi số một dòng Ví dụ lời gọi hàm writeVertical(1234); sẽ có kết quả: 1 2 3 4 6 SỐ THEO CHIỀU DỌC ĐỊNH NGHĨA HÀM ĐỆ QUY Chia vấn đề thành 2 trường hợp Trường hợp đơn giản/cơ sở: if n<10 Đơn giản in số n ra màn hình Trường hợp đệ quy: if n>=10, có 2 tác vụ con: 1. Hiển thị theo chiều dọc tất cả chữ số trừ chữ số cuối cùng 2. Hiển thị chữ số cuối cùng Ví dụ: với tham số 1234 Tác vụ con 1: hiển thị 1,2,3 theo chiều dọc Tác vụ con 2: hiển thị chữ số 4 7 ĐỊNH NGHĨA HÀM WRITEVERTICAL() Xét các trường hợp ở slide trước void writeVertical(int n) { if (n < 10) // Trường hợp cơ sở cout << n << endl; else { // Bước đệ quy writeVertical(n/10); cout << (n%10) << endl; } } 8 TRUY VẾT HÀM WRITEVERTICAL() Ví dụ lời gọi: writeVertical(123); writeVertical(12); (123/10) writeVertical(1); (12/10) cout << 1 << endl; cout << 2 << endl; cout << 3 << endl; Mũi tên chỉ định tác vụ hàm thực hiện Chú ý hai lời gọi đầu tiên: gọi lại cùng hàm (đệ quy) Lời gọi cuối cùng hiển thị (1) và “kết thúc” 9 ĐỆ QUY – MỘT CÁI NHÌN GẦN HƠN Máy tính lưu vết các lời gọi đệ quy Dừng hàm hiện tại Phải biết kết quả của lời gọi đệ quy mới trước khi tiến hành xử lý (proceeding) Lưu trữ mọi thông tin cần thiết cho lời gọi hiện tại Để sử dụng sau Tiến hành với đánh giá lời gọi đệ quy mới Khi lời gọi đó hoàn thiện, trả lại cho tính toán bên ngoài ("outer" computation) 10 ĐỆ QUY – BỨC TRANH LỚN Tổng quan về hàm đệ quy thành công: Một hoặc nhiều trường hợp khi hàm hoàn thiện những nhiệm vụ của nó bằng cách: Tạo một hoặc nhiều lời gọi đệ quy để giải quyết những phiên bản nhỏ hơn của tác vụ gốc Được gọi là “các trường hợp đệ quy” Một hoặc nhiều trường hợp khi hàm hoàn thiện tác vụ của nó mà không dùng lời gọi đệ quy Được gọi là “trường hợp cơ sở” hoặc trường hợp dừng 11 ĐỆ QUY VÔ HẠN Trường hợp cơ sở cuối cùng phải được gọi Nếu không đệ quy vô hạn Lời gọi đệ quy không bao giờ dừng! Nhớ lại ví dụ: writeVertical() Trường hợp cơ sở xảy ra khi xét đến số chỉ có 1 chữ số Khi đó sự đệ quy sẽ dừng 12 VÍ DỤ ĐỆ QUY VÔ HẠN Xét một định nghĩa hàm thay thế void newWriteVertical(int n) { newWriteVertical(n/10); cout << (n%10) << endl; } Dường như hàm này là đầy đủ Thiếu “trường hợp cơ sở”! Đệ quy không bao giờ dừng 13 NGĂN XẾP CHO ĐỆ QUY Một ngăn xếp (stack) Một cấu trúc bộ nhớ chuyên biệt Giống ngăn xếp giấy Đặt tờ mới trên đầu ngăn xếp Lấy ra khi cần thiết từ đầu ngăn xếp Được gọi là cấu trúc bộ nhớ “vào sau/ra trước” (last- in/first-out) Đệ quy sử dụng ngăn xếp Mỗi lời gọi đệ quy được đặt trên ngăn xếp Khi một lời gọi hoàn thành, nó sẽ được loại bỏ khỏi ngăn xếp 14 TRÀN NGĂN XẾP (STACK OVERFLOW) Kích thước của ngăn xếp là giới hạn Bộ nhớ có hạn Chuỗi dài của lời gọi đệ quy tiếp tục được thêm vào ngăn xếp Tất cả được thêm vào trước khi gặp trường hợp cơ sở (sẽ khiến lời gọi bị loại bỏ khỏi ngăn xếp) Nếu ngăn xếp cố gắng phát triển vượt quá giới hạn Lỗi tràn ngăn xếp Đệ quy vô hạn luôn tạo ra lỗi này 15 ĐỆ QUY SO VỚI CẤU TRÚC LẶP Đệ quy không phải luôn luôn cần thiết Thậm chí không cho phép trong một số ngôn ngữ Mọi tác vụ được hoàn thành với đệ quy có thể được làm mà không cần sử dụng đệ quy Khử đệ quy (nonrecursive): gọi là cấu trúc lặp, sử dụng vòng lặp Đệ quy: Chạy chậm, sử dụng nhiều bộ nhớ Ít code hơn, giải pháp ngắn gọn hơn (elegent solution) 16 HÀM ĐỆ QUY TRẢ VỀ MỘT GIÁ TRỊ Đệ quy không chỉ giới hạn với các hàm void Có thể trả về giá trị của bất kỳ kiểu nào Cùng một kỹ thuật: 1. Một hoặc nhiều trường hợp có giá trị trả về được tính toán bởi lời gọi đệ quy: những vấn đề nhỏ hơn 2. Một hoặc nhiều trường hợp có giá trị trả về được tính toán mà không có lời gọi đệ quy: trường hợp cơ sở 17 TRẢ VỀ MỘT GIÁ TRỊ VÍ DỤ ĐỆ QUY: HÀM LŨY THỪA (POWER) Nhớ lại hàm được định nghĩa trước pow(): result = pow(2.0,3.0); Trả về kết quả 2.0 lũy thừa 3 (8.0) Nhận 2 tham số kiểu double Trả về giá trị kiểu double Hãy viết theo cách đệ quy Cho ví dụ đơn giản 18 ĐỊNH NGHĨA HÀM POWER() int power(int x, int n) { if (n<0) { cout << "Illegal argument"; exit(1); } if (n>0) return (power(x, n-1)*x); else return (1); } 19 GỌI HÀM POWER() (1/2) Ví dụ lời gọi hàm: power(2, 0); trả về giá trị 1 power(2, 1); trả về (power(2, 0) * 2); trả về 1 Giá trị 1 được nhân với 2 và trả về cho lời gọi gốc 20 GỌI HÀM POWER() (2/2) Ví dụ lớn hơn power(2,3); power(2,2)*2 power(2,1)*2 power(2,0)*2 1 Đi đến trường hợp cơ sở Dừng đệ quy Giá trị được trả lại ngược trên ngăn xếp 21 LƯU VẾT HÀM POWER() ĐÁNH GIÁ LỜI GỌI HÀM ĐỆ QUY POWER(2,3) 22 SUY NGHĨ THEO CÁCH ĐỆ QUY Bỏ qua chi tiết Quên đi cách ngăn xếp hoạt động Quên đi những tính toán bị treo (suspended) Đây là một nguyên tắc “trừu tượng” ("abstraction" principle)! Và cũng là nguyên tắc đóng gói (encapsulation principle)! Hãy để máy tính thực hiện chi tiết (“bookkeeping”) Lập trình viên chỉ nghĩ về “bức tranh lớn” (big picture) 23 SUY NGHĨ THEO CÁCH ĐỆ QUY: HÀM POWER Xét lại hàm power() Định nghĩa đệ quy của hàm power(): power(x, n) Trả về giá trị: power(x, n – 1) * x Chỉ cần đảm bảo “công thức” đúng Và đảm bảo tính đến trường hợp cơ sở 24 KỸ THUẬT THIẾT KẾ ĐỆ QUY Không lưu vết toàn bộ chuỗi đệ quy Chỉ kiểm tra 3 thuộc tính: 1. Không đệ quy vô hạn 2. Các trường hợp dừng trả về giá trị đúng 3. Các trường hợp đệ quy trả về giá trị đúng 25 KIỂM TRA THIẾT KẾ ĐỆ QUY: HÀM POWER() Kiểm tra hàm power() với 3 thuộc tính 1. Không đệ quy vô hạn Tham số thứ 2 giảm đi 1 mỗi lần gọi hàm Cuối cùng phải gặp trường hợp cơ sở là 1 2. Các trường hợp dừng trả về giá trị đúng power(x,0) là trường hợp cơ sở Trả về giá trị 1, đúng với x0 3. Các trường hợp đệ quy trả về giá trị đúng Với n > 1, power(x,n) trả về power(x,n-1)*x Giá trị này đúng 26 TÌM KIẾM NHỊ PHÂN Hàm đệ quy để tìm kiếm trong mảng Quyết định xem liệu một phần tử có ở trong mảng không và nếu có: Phần tử đó ở vị trí nào trong mảng Giả sử mảng đã được sắp xếp Chia danh sách thành 2 phần Quyết định xem liệu phần tử đó ở nửa thứ nhất hay nửa thứ hai Sau đó lại bắt đầu tìm kiếm trên nửa đó -> một cách làm theo kiểu đệ quy! 27 MÃ GIẢ CHO TÌM KIẾM NHỊ PHÂN 28 KIỂM TRA SỰ ĐỆ QUY Kiểm tra tìm kiếm nhị phân theo các tiêu chí: 1. Không đệ quy vô hạn Mỗi lời gọi tăng first hoặc giảm last Cuối cùng first sẽ lớn hơn last 2. Các trường hợp dừng thực hiện hành động đúng Nếu first > last không có phần tử nào ở giữa chúng, do đó key không thể ở đó! Nếu key == a[mid] tìm được đúng! 3. Các trường hợp đệ quy thực hiện hành động đúng Nếu key < a[mid] key ở trong nửa đầu tiên – lời gọi đúng Nếu key > a[mid] key ở trong nửa thứ hai – lời gọi đúng 29 THỰC THI TÌM KIẾM NHỊ PHÂN 30 HIỆU QUẢ CỦA TÌM KIẾM NHỊ PHÂN Cực kỳ nhanh, khi so sánh với tìm kiếm tuần tự Một nửa của mảng bị loại bỏ tại thời điểm đầu tiên Sau đó là 1/4, tiếp theo là 1/8, etc. Về bản chất loại bỏ một nửa với mỗi lời gọi Ví dụ: một mảng có 100 phần tử Tìm kiếm nhị phân không bao giờ cần đến nhiều hơn 7 so sánh! Mức độ hiệu quả theo logarit (log n) 31 NHỮNG GIẢI PHÁP ĐỆ QUY Chú ý rằng thuật toán tìm kiếm nhị phân thực sự giải quyết vấn đề “tổng quan hơn” Mục tiêu ban đầu: thiết kế hàm để tìm kiếm trong toàn bộ mảng Hàm của chúng ta: cho phép tìm kiếm bất kỳ đoạn con nào của mảng Bằng cách chỉ định hai ranh giới first và last Rất phổ biến khi thiết kế hàm đệ quy 32 TÓM TẮT Giảm một vấn đề thành các trường hợp nhỏ hơn của cùng một vấn đề -> giải pháp đệ quy Thuật toán đệ quy có 2 trường hợp Trường hợp cơ sở/dừng Trường hợp đệ quy Đảm bảo không có đệ quy vô hạn Sử dụng các tiêu chí để quyết định xem đệ quy đúng Ba thuộc tính quan trọng Thường giải quyết vấn đề “tổng quát hơn” 33 GIÁO TRÌNH THAM KHẢO Giáo trình chính: W. Savitch, Absolute C++, Addison Wesley, 2002 Tham khảo: A. Ford and T. Teorey, Practical Debugging in C++, Prentice Hall, 2002 Nguyễn Thanh Thủy, Kĩ thuật lập trình C++, NXB Khoa học và Kĩ Thuật, 2006 34
File đính kèm:
- bai_giang_ngon_ngu_lap_trinh_bai_9_de_quy_le_nguyen_tuan_tha.pdf