2.1 Giới thiệu Stack – ngăn xếp
2.1.3 Các ví dụ minh họa
• Chương trình xuất chuỗi ký tự theo thứ tự ngược lại
Sử dụng stack chứa dữ liệu là các ký tự, khi đó ta sẽ push lần lượt các ký tự của chuỗi vào stack và sau đó lấy (Pop) lần lượt các ký tự này ra. Kết quả là chuỗi được xuất dưới dạng đảo ngược lại.
#include "stdio.h"
#include "conio.h"
#include "string.h"
#define TRUE 1
#define FALSE 0
#define MAX 100 typedef char Data;
typedef struct{
int top;
Data S[MAX];
} Stack;
void Push(Stack & st, Data x);
Data Pop(Stack &st);
void InitStack(Stack &st);
int IsEmpty(Stack st);
int IsFull(Stack st);
Data Top(Stack st);
void Push(Stack & st, Data x) {
if (IsFull(st))
printf("\nStack is full!");
else
st.S[++st.top] = x;
}
Data Pop(Stack &st) {
if (IsEmpty(st))
printf("\nStack is empty!");
else
return (st.S[st.top--]);
}
void InitStack(Stack &st) {
st.top = -1;
}
int IsEmpty(Stack st) {
if (st.top == -1) return TRUE;
else
return FALSE;
}
int IsFull(Stack st) {
if (st.top >= MAX) return TRUE;
else
return FALSE;
}
Data Top(Stack st) {
Data d;
if (IsEmpty(st))
printf("\n Stack is empty!");
else
d = st.S[st.top];
return d;
}
void main() {
char s[MAX];
Stack st;
clrscr();
gets(s);
InitStack(st);
for(int i=0; i < strlen(s); i++) Push(st, s[i]);
printf("\n Chuoi nguoc lai: ");
while (!IsEmpty(st))
printf("%c", Pop(st));
getch();
}
• Chương trình đổi số sang hệ cơ số bất kỳ .
#include "stdio.h"
#include "conio.h"
#include "string.h"
#define MAX 100
#define TRUE 1
#define FALSE 0
typedef unsigned int Data;
typedef struct{
int top;
Data S[MAX];
} Stack;
void Push(Stack & st, Data x);
Data Pop(Stack &st);
void InitStack(Stack &st);
int IsEmpty(Stack st);
int IsFull(Stack st);
Data Top(Stack st);
void Push(Stack & st, Data x) {
if (IsFull(st))
printf("\nStack is full!");
else
st.S[++st.top] = x;
}
Data Pop(Stack &st) {
if (IsEmpty(st))
printf("\nStack is empty!");
else
return (st.S[st.top--]);
}
void InitStack(Stack &st) {
st.top = -1;
}
int IsEmpty(Stack st) {
if (st.top == -1) return TRUE;
else
return FALSE;
}
int IsFull(Stack st) {
if (st.top >= MAX) return TRUE;
else
return FALSE;
}
Data Top(stack st) {
Data d;
if (IsEmpty(st))
printf("\n Stack is empty!");
else
d = st.S[st.top];
return d;
}
void main() {
char s[MAX];
int coso, so, du;
stack st;
clrscr();
printf("Nhap co so can doi: ");
scanf("%d", &coso);
printf("Nhap so:");
scanf("%d",&so);
InitStack(st);
while (so != 0)
{
du = so % coso;
so = so/coso;
}
printf("\n Co so : ");
while (!IsEmpty(st)) printf("%3X", Pop(st));
getch();
}
• Bài toán tháp Hanoi
Bài toán tháp Hanoi thường được giải bằng phương pháp đệ quy, trong chương trước đã trình bày. Tuy nhiên có thể giải bằng cách dùng stack để khử đệ quy. Để thực hiện việc lưu trữ tạm trong quá trình di chuyển chúng ta dùng một stack.
Trong đó mỗi phần tử của stack này chứa các thông tin gồm: số đĩa di chuyển (N), cột nguồn bắt đầu di chuyển (Nguon) và cột đích là nơi cần di chuyển đến (Dich). Ở đây không cần lưu cột trung gian vì có 3 cột đánh số là 1, 2 và 3 thì cột trung gian để di chuyển là: 6 – (Nguon+Dich).
Đầu tiên đưa vào stack thông tin di chuyển {n, 1, 2}, tức là di chuyển n đĩa từ cột 1 sang cột thứ 2 qua cột trung gian là 6-(1+2) = 3.
Tại mỗi bước khi lấy trong stack ra một phần tử, thực hiện như sau:
Nếu N = 1: ⇒ di chuyển đĩa từ cột Nguon ⇒ cột Dich
Ngược lại (nếu N > 1):
• Xác định cột trung gian TrungGian = 6 – (Nguon+Dich)
• Push ⇒ stack thông tin di chuyển {N-1, TrungGian, Dich}
• Push ⇒ stack thông tin di chuyển {1, Nguon, Dich}
• Push ⇒ stack thông tin di chuyển {N-1, Nguon, TrungGian}
Quá trình còn thực hiện khi stack khác rỗng.
Nhận xét: lưu ý thứ tự khi đưa vào thông tin di chuyển vào stack. Trong phần trên thông tin {N-1, Nguon, TrungGian} được đưa vào stack sau cùng nên chúng sẽ được lấy ra trước tiên, kế đến là thông tin di chuyển {1, Nguon, Dich} và cuối cùng là thông tin di chuyển {N-1, TrungGian, Dich}.
Chương trình minh họa:
#include "stdio.h"
#include "conio.h"
#define MAX 100
#define TRUE 1
#define FALSE 0 typedef struct {
int N; // số đĩa cần di chuyển int Nguon; // cột bắt đầu di chuyển int Dich; // cột đích đến
} Data;
typedef struct{
int top;
Data S[MAX];
} stack;
void Push(stack & st, Data x);
Data Pop(stack &st);
void InitStack(stack &st);
int IsEmpty(stack st);
int IsFull(stack st);
Data Top(stack st);
void Push(stack & st, Data x) {
if (IsFull(st))
printf("\nStack full!");
else
st.S[++st.top] = x;
}
Data Pop(stack &st) {
if (IsEmpty(st))
printf("\nStack empty!");
else
return (st.S[st.top--]);
}
void InitStack(stack &st) {
st.top = -1;
}
int IsEmpty(stack st) {
return TRUE;
else
return FALSE;
}
int IsFull(stack st) {
if (st.top >= MAX) return TRUE;
else
return FALSE;
}
Data Top(stack st) {
Data d;
if (IsEmpty(st))
printf("\n Stack is empty!");
else
d = st.S[st.top];
return d;
}
void Hanoi(int n) {
stack st; // stack chứa thông tin di chuyển InitStack(st); // khởi tạo stack
Data d,d1,d2,d3; // các biến tạm chứa thông tin di chuyển.
int TrungGian; // cột trung gian
// đưa thông tin ban đầu vào stack {n, 1, 2} : di chuyển n đĩa từ cột 1 đến 2 d.N = n;
d.Nguon = 1;
d.Dich = 2;
Push(st, d); // đưa vào stack
while (!IsEmpty(st)) // lặp khi stack còn phần tử
{
d = Pop(st); // lấy một phần tử đầu stack ra thực hiện
if (d.N == 1) // nếu chỉ có một đĩa thì di chuyển Nguồn → Đích printf("\nDi chuyen: %d -> %d", d.Nguon, d.Dich);
else // nhiều hơn một đĩa
{
TrungGian = 6 - (d.Nguon+d.Dich); // lấy cột Trung gian /* Đưa vào stack theo thứ tự ngược lại. Tức là đưa vào thông tin: di chuyển N-1 đĩa từ cọc trung gian đến cọc đích, di chuyển 1 đĩa từ nguồn sang đích, và cuối cùng đưa N-1 đĩa từ cọc nguồn sang trung gian. Khi đó lấy từ stack ra chúng ta sẽ có thứ tự ngược lại là: di chuyển N-1 đĩa từ cọc nguồn sang trung gian, di chuyển 1 đĩa từ cọc nguồn sang đích và cuối cùng di chuyển N-1 đĩa từ cọc trung gian sang đích. Đây chính là thứ tự mà chúng ta cần thực hiện chuyển N đĩa từ cọc nguồn sang đích. */
// đưa thông tin di chuyển N-1 đĩa từ Trung gian → Đích d1.N = d.N-1;
d1.Nguon = TrungGian;
d1.Dich = d.Dich;
Push(st,d1);
// đưa thông tin di chuyển 1 đĩa từ Nguồn → Đích d2.N = 1;
d2.Nguon = d.Nguon;
d2.Dich = d.Dich;
Push(st, d2);
// đưa thông tin di chuyển N-1 đĩa từ Nguồn → Trung gian d3.N = d.N -1;
d3.Nguon = d.Nguon;
d3.Dich = TrungGian;
Push(st,d3);
}
} // end while stack <> ∅ } // end Hanoi
int main() {
int n;
clrscr();
printf("Nhap vao so cot: "); scanf("%d",&n);
Hanoi(n);
getch();
return 0;
}
• Thuật toán QuickSort sử dụng stack
Như chúng ta đã biết thì thuật toán QuickSort thường được cài đặt bằng giải thuật đệ quy. Tuy nhiên một cách khác là dùng cấu trúc dữ liệu stack để cài đặt cũng tốt. Cách thực hiện là tạo một cấu trúc stack để lưu trữ hai biên trái và phải của một đoạn.
Ví dụ như khi phân đoạn left đến right, chúng ta được ba đoạn là [left...i] là các phần tử nhỏ hơn x, đoạn [i+1..j-1] là các phần tử bằng x, và đoạn cuối là [j...right]. Khi đó chúng ta sẽ đưa vào stack đoạn bên phải, nếu đoạn bên trái nhiều hơn một phần tử ta cập nhật lại right = i, khi đó chúng ta sẽ lặp lại với đoạn [left...i] một cách tương tự, khi đoạn bên trái hết thì chúng ta sẽ lấy từ trong stack ra những đoạn được lưu giữ để thực hiện tiếp tục...quá trình thực hiện cho đến khi stack rỗng.
#include "stdio.h"
#include "conio.h"
#include "stdlib.h"
#define TRUE 1
#define FALSE 0
#define MAX 100 typedef struct {
int left;
int right;
} Data;
typedef struct{
int top;
Data S[MAX];
} stack;
void Push(stack & st, Data x);
Data Pop(stack &st);
void InitStack(stack &st);
int IsEmpty(stack st);
int IsFull(stack st);
Data Top(stack st);
void Swap(int &a, int &b) {
int tmp =a;
a = b;
b = tmp;
}
void Push(stack & st, Data x) {
if (IsFull(st))
printf("\nStack is full!");
else
st.S[++st.top] = x;
}
Data Pop(stack &st) {
if (IsEmpty(st))
printf("\nStack is empty!");
else
return (st.S[st.top--]);
}
void InitStack(stack &st) {
st.top = -1;
}
int IsEmpty(stack st) {
if (st.top == -1) return TRUE;
else
return FALSE;
}
int IsFull(stack st) {
if (st.top >= MAX) return TRUE;
else
return FALSE;
}
Data Top(stack st) {
Data d;
if (IsEmpty(st))
printf("\n Stack is empty!");
else
d = st.S[st.top];
return d;
}
{
int i, j, l, r;
int x;
l = 0;
r = n-1;
stack st;
InitStack(st);
while (l < r ) {
x = a[(l+r)/2];
i = l;
j = r;
do{
while (a[i] < x) i++;
while (a[j] > x) j--;
if ( i <= j) {
Swap(a[i], a[j]);
i++;
j--;
} } while (i<j);
if (r > i) {
Data d;
d.left = i;
d.right = r;
Push(st,d);
} if (l < j) {
r = j;
}
else if (!IsEmpty(st)) {
Data d = Pop(st);
l = d.left;
r = d.right;
} else
break; // thoat khoi vong lap -> while (l < r) }
}
void main() {
int a[MAX] ; int n = 30;
clrscr();
randomize();
printf("Mang truoc khi sap: \n");
for(int i=0; i < n;i++) {
a[i] = random(100);
printf("%4d", a[i]);
}
QuickSort(a,n);
printf("\nMang sau khi sap:\n");
for(i=0; i <n; i++) printf("%4d",a[i]);
getch();
}
• Bài toán chuyển biểu thức trung tố sang hậu tố.
Như chúng ta đã biết một biểu thức được biểu diễn dưới dạng hậu tố hay còn gọi là ký pháp nghịch đảo Ba Lan RPN (Reverse Polish Notation) giúp ích rất nhiều cho việc tính toán giá trị biểu thức tốt hơn là biểu thức dạng trung tố. Các trình biên dịch máy tính thường chuyển những biểu thức trung tố sang hậu tố để dễ xử lý hơn. Trong phần này chúng ta sẽ tìm hiểu thuật toán để chuyển một biểu thức dạng trung tố đơn giản sang biểu thức hậu tố tương ứng.
Ví dụ một biểu thức trung tố như sau: (6 / 2 + 3) * (7 - 4) được chuyển thành biểu thức hậu tố như sau: 6 2 / 3 + 7 4 - *. Chúng ta có thể thấy cách biểu diễn của trung tố cần phải xử lý dấu ngoặc trong khi biểu thức hậu tố thì không cần sử dụng. Đây là ưu điểm của biểu thức hậu tố.
Để chuyển đổi chúng ta sử dụng một Stack dùng lưu trữ các toán tử và dấu ngoặc mở.
Ngoài ra cũng quy định độ ưu tiên của các toán tử thông qua hàm Priority, trong đó phép toán *, / có độ ưu tiên cao nhất là 2, phép toán +, - có độ ưu tiên ít hơn là 1, và cuối cùng dấu ngoặc mở ‘(’ là 0.
Duyệt qua từng phần tử trong biểu thức trung tố, gọi C là phần tử đang xét:
Nếu C là ‘(’ thì push vào stack
Nếu C là ‘)’ thì lấy trong stack ra cho đến khi gặp ‘(‘: xuất ra những phần tử này.
Nếu C là toán tử ‘+’,’-‘,’*’,’/’: Lấy trong stack ra tất cả những toán tử có độ ưu tiên cao hơn C, xuất những phần tử này ra ngoài. Sau đó đưa C vào stack.
Cuối cùng lấy tất cả những phần tử còn lại trong stack xuất ra ngoài.
Thuật toán chuyển đổi từ trung tố sang hậu tố:
Stack = {∅}; // khởi tạo stack rỗng
for < phần tử C được đọc từ biểu thức trung tố> do {
if ( C == ‘(‘ )
Push(Stack, C); // đưa vào stack else if ( C== ‘)’)
{
do {
x = Pop(Stack); // lấy từ stack ra cho đến khi gặp ‘(‘
if (c != ‘(‘)
printf(“%c”, x);
} while ( x!= ‘(‘) }
else if (C == ‘+’ || C ==’-‘ || C == ‘*’ || C==’/’) {
while ( !IsEmpty(Stack) && Priority(C) <= Priority(Top(Stack))) printf(“%c”, Pop(Stack)); // nếu độ ưu tiên toán tử trong
// stack lớn hơn thì lấy ra.
Push(Stack, C); // đưa toán tử mới vào stack }
else // toán hạng printf(“%c”,C);
}
while ( !IsEmpty(Stack)) // lặp để lấy tất cả các phần tử trong stack ra printf(“%c”, Pop(Stack));
• Ví dụ khi thực hiện với biểu thức trung tố : (2 * 3 + 7 / 8) * (5 – 1)
Đọc Xử lý Stack Output
( Đẩy vào stack (
2 Xuất ( 2
* Do ‘*’ ưu tiên hơn ‘(‘ ở đỉnh stack nên đưa ‘*’ vào
stack ( * 2
3 Xuất ( * 2 3
+
Do ‘+’ ưu tiên thấp hơn ‘*’ ở đỉnh stack nên ta lấy
‘*’ ra.
Tiếp tục so sánh ‘+’ với ‘(‘ thì ‘+’ ưu tiên cao hơn nên đưa vào stack
( + 2 3 *
7 Xuất ( + 2 3 * 7
/ Do ‘/’ có độ ưu tiên cao hơn ‘+’ trên đỉnh stack
nên đưa ‘/’ vào stack. ( + / 2 3 * 7
8 Xuất ( + / 2 3 * 7 8
) Lấy trong stack ra cho đến khi gặp ngoặc (. 2 3 * 7 8 / +
* Đưa vào stack * 2 3 * 7 8 / +
( Đưa vào stack * ( 2 3 * 7 8 / +
5 Xuất * ( 2 3 * 7 8 / + 5
- Độ ưu tiên của ‘-‘ cao hơn ‘(‘ trong đỉnh stack
nên đưa ‘-‘ vào stack * ( - 2 3 * 7 8 / + 5
1 Xuất * ( - 2 3 * 7 8 / + 5 1
) Lấy trong stack ra cho đến khi gặp ngoặc đóng * 2 3 * 7 8 / + 5 1 -
Lấy những phần tử còn lại trong stack và hiển thị 2 3 * 7 8 / + 5 1 - *
• Bài toán tính giá trị biểu thức hậu tố .
Trong những năm đầu 1950 nhà logic học người Ba Lan Jan Lukasiewicz đã chứng minh rằng: biểu thức hậu tố không cần có dấu ngoặc vẫn có thể tính đúng bằng cách đọc lần lượt biểu thức từ trái qua phải và dùng một stack để lưu trữ kết quả trung gian.
Các bước thực hiện như sau:
Bước 1: Khởi tạo một stack = {∅}
Bước 2: Đọc lần lượt các phần tử của biểu thức hậu tố từ trái sang phải, với mỗi phần tử đó thực hiện kiểm tra:
Nếu phần tử này là toán hạng thì đẩy nó vào stack
Nếu phần tử này là toán tử thì ta lấy hai toán hạng trong stack ra và thực hiện phép toán với hai toán hạng đó. Kết quả tính được sẽ được lưu vào trong stack.
Bước 3: Sau khi kết thúc bước hai thì toàn bộ biểu thức đã được đọc xong, trong stack chỉ còn duy nhất một phần tử, phần tử đó chính là giá trị của biểu thức.
Ví dụ: tính giá trị biểu thức 10 2 / 3 + 7 4 - *, có biểu thức trung tố là: (10/2 + 3)*(7-4), ta có các bước thực hiện như sau:
Đọc Xử lý Stack Output
10 Đưa vào stack 10
2 Đưa vào stack 10 2
/
Lấy hai phần tử đầu stack là 2, 10 và thực hiện phép toán 10/2 = 5. Sau đó lưu kết quả 5 vào stack
5
3 Đưa 3 vào stack 5 3
+
Lấy hai giá trị 3, 5 ra khỏi stack, thực hiện phép cộng của hai số đó, kết quả là 8 được đưa vào lại stack
8
7 Đưa 7 vào stack 8 7
4 Đưa 4 vào stack 8 7 4
- Lấy hai giá trị 4, 7 ra khỏi stack, thực hiện phép tính 7 – 4 = 3, kết quả 3 được đưa vào lại stack 8 3
* Lấy hai giá trị 3, 8 ra khỏi stack, thực hiện phép tính 8 * 3 = 24, lưu kết quả vào stack 24
Lấy kết quả từ stack ⇒ đây chính là kết quả của
biểu thức. 24