Phần 2: dùng stack để khử thuật tóan đệ quy nhánhNhư chúng ta đã biết, sự thực hiện của đệ quy chính là sự thực hiện lại chính bản thân nó với dữ liệu nhỏ hơn, nên khi gọi hàm đệ quy thì
Trang 1Phần 2: dùng stack để khử thuật tóan đệ quy nhánh
Như chúng ta đã biết, sự thực hiện của đệ quy chính là sự thực hiện lại chính bản thân
nó với dữ liệu nhỏ hơn, nên khi gọi hàm đệ quy thì máy sẽ cấp phát một vùng nhớ có cơ chế hoạt động như Stack (ngăn xếp) cho hàm đệ quy đó Cơ chế này hoạt động như kiểu LIFO( last in first out – vào sau ra trước), tức là việc thêm vào hay lấy ra đều thực hiện thông qua một đầu ra duy nhất
Nếu một chương trình con đệ qui P(x) được gọi từ chương trình chính ta nói chương trình con được thực hiện ở mức 1 Chương trình con này gọi chính nó, ta nói nó đi sâu vào mức 2 và cho đến một mức k Rõ ràng mức k phải thực hiện xong thì mức k-1 mới được thực hiện tiếp tục, hay ta còn nói là chương trình con quay về mức k-1
Trong khi một chương trình con từ mức i đi vào mức i+1 thì các biến cục bộ của mức i
và địa chỉ của mã lệnh còn dang dở phải được lưu trữ, địa chỉ này gọi là địa chỉ trở về Khi từ mức i+1 quay về mức i các giá trị đó được sử dụng Như vậy những biến cục bộ
và địa chỉ lưu sau được dùng trước Tính chất này gợi ý cho ta dùng một stack để lưu giữ các giá trị cần thiết của mỗi lần gọi tới chương trình con như biến cục bộ, các tham số Mỗi khi lùi về một mức thì các giá trị này được lấy ra để tiếp tục thực hiện mức này Ta tóm tắt quá trình:
• Bưóc 1: Tạo 1 stack rỗng
• Bưóc 2: Lưu các tham số hình thức ở mức 1 vào stack
• Bước 3: Lặp cho đến khi stack rỗng
1 Lấy ra 1 phần tử X khỏi stack
Vào sau
Vào trước
Đỉnh
Stack(Ngăn xếp)
Trang 22 Nếu X thoả điều kiện dừng của đệ qui (NEO) thì xử lý dữ liệu Ngược lại (phần đệ qui)
Xử lý theo thuật tóan Xác định lời gọi đệ quy với các tham số tương ứng và nạp các tham số đó vào stack
Bước 4: Kết thúc thuật toán
Một số ví dụ về dùng Stack để khử đệ qui
Ví dụ 1: Bài toán Tháp Hà Nội
Có ba cọc A,B,C Khởi đầu cọc A có một số đĩa xếp theo thứ tự nhỏ dần lên trên đỉnh Bài toán đặt ra là phải chuyển toàn bộ chồng đĩa từ A sang C Mỗi lần thực hiện chuyển một đĩa từ một cọc sang một cọc khác và không được đặt đĩa lớn nằm trên đĩa nhỏ
Phân tích bài toán:
Trường hợp 1 đĩa: Chuyển thẳng từ A sang C Đây là trường hợp suy biến
Trường hợp 2 đĩa: Chuyển 1 đĩa từ A sang B
Chuyển 1 đĩa từ A sang C Chuyển 1 đĩa từ B sang C Trường hợp chung n>1 đĩa; Ta coi n-1 đĩa trên như là 1 đĩa và ta áp dụng trong trường hợp 2 đĩa
Trang 3 Chuyển n-1 đĩa từ A sang B ( dùng cọc C làm trung gian)
Chuyển 1 đĩa từ A sang C
Chuyển n-1 đĩa từ B sang C (dùng cọc A làm trung gian)
thuật toán đệ qui mô phỏng như sau:
Proc Move(n,A,B,C) // Chuyển n đĩa từ cọc A sang cọc C
If n=1 Then
chuyển (A,’’,C)
Else { Call Move(n-1, A, C, B)
Call Move(1, A, B, C)
Call Move(n-1, B, A, C)
}
Return
Quá trình thực hiện chương trình con được minh hoạ với 3 đĩa (n=3) như sau:
Move(1,A,B,C) A->B Move(2,A,C,B) Move(1,A,C,B) A->C
Move(1,B,C,A) B->C
Move(1,C,A,B) C->A Move(2,C,B,A) Move(1,C,B,A) C->B
Move(1,A,B,C) A->B
Cây đệ quy trên được mô phỏng qua việc dùng stack để giải quyết như sau:
Trang 4(3,A,B,C) (2,A,C,B) Quá trình chuyển đĩa
(2,B,A,C)
Từ đây, ta xây dựng cấu trúc của mỗi phần tử được lưu trong stack để giải quyết bài toán tháp Hà nội như sau:
struct Item_Tower{
char sour, midl, dest;
void assign(int n, char a, char b, char c){ // hàm gán từng giá trị cho các thuộc tính num = n; sour = a; midl = b, dest = c;
}
};
A->C A->B C->B A->C
B->A B->C A->C
2ACB1ABC2BAC 1ABC1ACB1CAB1A
BC2BAC
2BAC 1BCA1BAC1ABC
Trang 5Thuật toán khử đệ qui
void haNoi_Tower(int n, char a, char b, char c){
Stack<Item_Tower> sTower; // Khai báo stack kiểu Item_Tower
Item_Tower t;
int d = 0;
t.assign(n, a, b, c);
sTower.push(t);
while (sTower.isEmpty() == false){
Item_Tower temp = sTower.topOfStack();
sTower.pop();
if(temp.num == 1) cout<<" \n "<<++d<<" "<<temp.sour <<" ->"<<temp.dest; else{
t.assign(temp.num-1, temp.midl, temp.sour, temp.dest);
sTower.push(t);
t.assign(1, temp.sour, temp.midl, temp.dest);
sTower.push(t);
t.assign(temp.num - 1, temp.sour, temp.dest, temp.midl);
sTower.push(t);
}
}
}
Ví dụ 2: Phủ bàn cờ kích thước 2n * 2n bằng các con tromino
Yêu cầu: có bàn cờ kích thước 2n * 2n (n nguyên >0) Đánh dấu 1 ô bất kỳ trên bàn cờ, sau đó dùng các con tromino phủ đầy bàn cờ
Trang 6Với bàn cờ cỡ n= 8
Thuật tóan:
Từ bàn cờ kích thước cỡ 2n * 2n với n>0 và 1 ô có tọa độ (x,y) bị đánh dấu, ta chia bàn cờ thành 4 bàn cờ con, với tâm bàn cờ là 1 con tromino có phần khuyết hướng về phía ô bị đánh dấu
Như vậy ta được 4 bàn cờ con, mỗi bàn cờ con có kích thước 2n-1 * 2n-1 với 1 ô bị đánh dấu, vì mỗi góc của con tromino vừa đặt vào tâm trở thành ô bị đánh dấu trong mỗi bàn
cờ con mới
X
X
X X
Trang 7Với mỗi bàn cờ con, ta lại tiếp tục thực hiện cho đến khi mọi ô trong bàn cờ bị đánh dấu Như vậy, bài toán này trở thành 4 bài toán con tương tự với 4 lần gọi đệ quy
Thuật toán
Proc Tromino( {a, b}, {x, y}, n)
/* trong do {a, b} là tọa độ góc trên bến trái của bàn cờ
{x, y} là tọa độ ô bị đánh dấu
n là kích thước của bàn cờ */
if(n>1){
s = n/2 ; // slà kích thước của 4 bàn cờ con
xác định các bộ giá trị của 4 bàn cờ con ({a i , b i }, {x i , y i }) với i = 1 4
call Tromino({a 1 ,b 1 }, {x 1 , y 1 }, s);
call Tromino({a 2 ,b 2 }, {x 2 , y 2 }, s);
call Tromino({a 3 ,b 3 }, {x 3 , y 3 }, s);
call Tromino({a 4 ,b 4 }, {x 4 , y 4 }, s); ,
}
Return
Từ đây, ta xây dựng cấu trúc của mỗi phần tử được lưu trong stack để giải quyết bài Tromino như sau:
struct Item_Tromino{
int a, b, x, y, n;
void assign(int a1, int b1, int x1, int y1, int n1){
a = a1, b = b1, x = x1, y = y1, n = n1;
}
};
void Tromino(){
Trang 8Stack <Item_Tromino> sTromino;
Item_Tromino t;
// đưa dữ liệu của bàn cờ đầu tiên vào stack
t.assign(1,1,x,y,n);
sTromino.push(t);
d = 0; //dùng để đánh dấu
while (sTromino.isEmpty()==false){
Item_Tromino temp = sTromino.topOfStack();
sTromino.pop();
if(temp.n>1){
int a, b, x, y, d, a1, b1, a2, b2, a3, b3, a4, b4, x1, y1, x2, y2, x3, y3, x4, y4, s;
s = temp.n/2; // s là kích thước của mỗi bàn cờ con
a = temp.a; b = temp.b; x = temp.x; y = temp.y;
//Xác định tọa độ trên bên trái của 4 bàn cờ con
a1 = a; b1 = b;
a2 = a; b2 = b + s ;
a3 = a1 + s; b3 = b1 + s;
a4 = a + s ; b4 = b;
//xác định vị trí đánh dấu của các bàn cờ con
x1 = a + s -1; y1 = b + s -1;
x2 = x1, y2 = y1 + 1;
x3 = x1 + 1; y3 = y2;
x4 = x1 + 1; y4 = y1;
// đánh dấu vào các ô bị đánh dấu ở mỗi bàn cờ con (trừ ô ban đầu)
Trang 9d++;
if( x< a + s && y< b+ s ) {x1 = x, y1 =y ;M[x2][y2] = M[x3][y3] = M[x4][y4] = d;} if( x< a + s && y>= b+ s ) {x2 = x, y2 =y ;M[x1][y1] = M[x3][y3] = M[x4][y4] = d;} if( x>= a + s && y< b+ s ) {x4= x, y4 =y ;M[x1][y1] = M[x2][y2] = M[x3][y3] = d;} if( x>= a + s && y>= b+ s ) {x3 = x, y3 =y ;M[x1][y1] = M[x2][y2] = M[x4][y4] = d;} // Lần lượt đưa các tham số ({ai, bi}{xi, yi}, s) vào stack
t.assign(a4,b4,x4,y4,s); // góc phần tư thứ 4
sTromino.push(t);
t.assign(a3,b3,x3,y3,s); // góc phần tư thứ 3
sTromino.push(t);
t.assign(a2,b2,x2,y2,s); // góc phần tư thứ 2
sTromino.push(t);
t.assign(a1,b1,x1,y1,s); // góc phần tư thứ 1
sTromino.push(t);
}
}
}
Trang 10Một kết quả khi chạy chương trình
Trang 11Ví dụ 3:Thuật toán QuickSort
tăng dần với độ phức tạp O(nlgn)
Đầu vào: dãy Ml, Ml+1, Ml+2,…,Mh chưa có thứ tự
Đầu ra: dãy Ml, Ml+1, Ml+2,…,Mh có thứ tự không giảm ( Mi ≤Mj với l≤i<j≤h)
Ý tưởng:
Dựa trên nền tảng thuật toán chia để trị
thành 2 dãy con không trống là dãy M l ,…,M pos-1 và M pos+1 ,…,M h cỡ
n/2 phần tử sao cho mỗi thành phần của dãy Ml,…,Mpos-1 nhỏ hơn
Mpos và mỗi thành phần của dãy Mpos+1,…,Mh lớn hơn hoặc bằng Mpos
Vị trí pos là kết quả của quá trình phân hoạch
dãy ban đầu cho đến khi mỗi dãy con chỉ còn tối đa 1 phần tử
phần tử Mpos nằm đúng vị trí của nó trong dãy, nên sẽ không tốn thời gian và chi phí để tổ hợp kết quả lại
Xây dựng thuật toán
Thuật toán Phân hoạch dãy Ml, Ml+1, Ml+2,…,Mh như sau:
Chọn một khoá X=Mk trong dãy (có thể chọn k=l), dựa vào khoá M để phân hoạch dãy thành 3 phần như sau:
• Phần 1 chứa các khoá có giá trị <X. Các khoá này nằm bên trái của X trong dãy
• Phần 2 là X
• Phần 3 chứa các khoá có giá trị ≥X. Các khoá này nằm bên phải của
X trong dãy
M l M l+1 … … M pos-1 X M pos+1 … … … … M h
Các khoá < M pos Pos Các khoá ≥ M pos
Trang 12Func Partition(low, hight) // low chỉ số cận trái và hight chỉ số cận phải của dãy
X = Mlow // X là giá trị được dùng để phân hoạch thành 3 dãy con last_small = low //là chỉ số cuối cùng của dãy <=X
i = low+1
{
If Mi<X Then
{
last_small = last_small + i
Mlast_small↔ Mi
}
i ← i+1
}
Mlast_small↔ Mlow
Partition = last_small
Return
Như vậy sau khi phân hoạch khoá Mpos sẽ nằm đúng vị trí của nó khi dãy đã được sắp xếp xong
Thuật toán QuickSort được xây dựng như sau
Proc QuickSort(Left, Right) //Left,Right là chỉ số thấp và cao của dãy cần SX
If Left<Right Then
{
Pos ←Partition(Left,Right) //gọi thuật toán Partition
Trang 13Call QuickSort(Pos+1, Right) //gọi đệ qui cho nửa ≥MPos
} Return
Vậy ta dùng 1 stack để lưu trữ 2 chỉ số của cận dưới và cận trên của dãy cần sắp xếp Chỉ cần phân hoạch đối với dãy có hơn 1 giá trị
Mỗi phần tử của stack được định nghĩa như sau:
struct Item_QS{
int left, right;
void assign(int a, int b){
left = a; right = b;
}
};
Thuật toán quicksort không đệ qui như sau:
void quicksort( int l, int r ){
Stack<Item_QS> sQS;
Item_QS t;
t.assign(l, r);
sQS.push(t); // đưa chỉ số cận trái và cận phải vào stack
while(sQS.isEmpty()==false)
{ Item_QS temp;
temp = sQS.topOfStack(); // lấy ra cặp chỉ số của dãy
sQS.pop();
if (temp.left < temp.right){
int pos = partition(temp.left, temp.right);
if(pos+1 < temp.right){ // nếu dãy con bên phải có >1 phần tử t.assign(pos +1, temp.right);
Trang 14sQS.push(t); // đưa cặp chỉ số dãy con bên phải vào stack }
if(temp.left < pos-1){ // nếu dãy con bên trái có >1 phần tử
t.assign(temp.left, pos -1);
sQS.push(t); // đưa cặp chỉ số dãy con bên phải vào stack }
}
}
}