Ngăn xếp bằng mảng
Sau bài này bạn sẽ làm được
- Cài đủ push, pop, peek, kiểm tra rỗng và đầy
- Phân biệt tràn trên và tràn dưới
- Chọn kiểu cho chỉ số top và tránh lỗi khi ngăn xếp rỗng
- Giải thích vì sao ngăn xếp mảng nhanh hơn ngăn xếp liên kết
Ngăn xếp chỉ cho phép đụng vào một đầu duy nhất. Ràng buộc nghe có vẻ thiệt thòi đó lại chính là thứ làm nó hữu ích: nó khớp chính xác với cách lời gọi hàm, cách biểu thức lồng nhau, và cách mọi thứ có cấu trúc ngoặc hoạt động.
#LIFO và bốn thao tác
| Thao tác | Ý nghĩa | Độ phức tạp |
|---|---|---|
| push | Đặt một phần tử lên đỉnh | O(1) |
| pop | Lấy phần tử đỉnh ra và bỏ nó khỏi ngăn xếp | O(1) |
| peek | Xem phần tử đỉnh mà không lấy ra | O(1) |
| rỗng | Hỏi ngăn xếp có phần tử nào không | O(1) |
#Cài đặt bằng mảng
#ifndef STACK_H
#define STACK_H
#include <stddef.h>
#define STACK_MAX 1000
/* Bất biến:
1. -1 <= top < STACK_MAX
2. Các phần tử hợp lệ là data[0] tới data[top]
3. top == -1 nghĩa là rỗng */
typedef struct {
int data[STACK_MAX];
long top; /* chỉ số phần tử đỉnh, -1 khi rỗng */
} Stack;
void stack_khoi_tao(Stack *s);
int stack_rong(const Stack *s);
int stack_day(const Stack *s);
size_t stack_so_phan_tu(const Stack *s);
int stack_push(Stack *s, int v);
int stack_pop(Stack *s, int *out);
int stack_peek(const Stack *s, int *out);
#endif#include "stack.h"
void stack_khoi_tao(Stack *s)
{
s->top = -1;
}
int stack_rong(const Stack *s)
{
return s->top < 0;
}
int stack_day(const Stack *s)
{
return s->top >= STACK_MAX - 1;
}
size_t stack_so_phan_tu(const Stack *s)
{
return (size_t)(s->top + 1);
}
/* Trả về 0 nếu đẩy được, -1 nếu ngăn xếp đầy. */
int stack_push(Stack *s, int v)
{
if (stack_day(s)) return -1;
s->data[++s->top] = v; /* tăng top TRƯỚC rồi ghi */
return 0;
}
/* Trả về 0 nếu lấy được, -1 nếu ngăn xếp rỗng. */
int stack_pop(Stack *s, int *out)
{
if (stack_rong(s)) return -1;
if (out != NULL) *out = s->data[s->top];
--s->top; /* đọc TRƯỚC rồi giảm top */
return 0;
}
int stack_peek(const Stack *s, int *out)
{
if (stack_rong(s)) return -1;
if (out != NULL) *out = s->data[s->top];
return 0;
}#Chọn kiểu cho chỉ số top
typedef struct {
int data[STACK_MAX];
size_t top; /* không dấu */
} Stack;
void stack_khoi_tao(Stack *s) { s->top = (size_t)-1; } /* SIZE_MAX */
int stack_rong(const Stack *s) { return s->top == (size_t)-1; }typedef struct {
int data[STACK_MAX];
long top; /* có dấu, -1 nghĩa là rỗng */
} Stack;
void stack_khoi_tao(Stack *s) { s->top = -1; }
int stack_rong(const Stack *s) { return s->top < 0; }Với size_t thì --s->top khi top bằng 0 cho ra SIZE_MAX, một con số khổng lồ, chứ không phải âm một. Điều đó không phải lỗi, vì kiểu không dấu quấn vòng có định nghĩa rõ ràng theo Bài 3.5. Nhưng nó biến một lỗi lập trình thành một giá trị hợp lệ khó nhận ra, và mọi phép so sánh top < 0 đều thành sai vĩnh viễn.
sau khi pop tren stack rong: top = 18446744073709551615 top < 0 ? khong s->data[top] -> Segmentation fault (core dumped)
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 4 at 0x7ffd4c8a1234 thread T0
#0 in stack_pop top-khong-dau.c:22#Tràn trên và tràn dưới
| Lỗi | Xảy ra khi | Hậu quả nếu không kiểm |
|---|---|---|
| Tràn trên | push khi ngăn xếp đã đầy | Ghi ra ngoài mảng, phá dữ liệu kề bên hoặc phá ngăn xếp lời gọi |
| Tràn dưới | pop hoặc peek khi ngăn xếp rỗng | Đọc data[-1], tức đọc ngoài mảng |
Chương trình thử
#include <stdio.h>
#include "stack.h"
int main(void)
{
Stack s;
int v;
stack_khoi_tao(&s);
printf("pop tren stack rong : %d\n", stack_pop(&s, &v));
printf("peek tren stack rong: %d\n", stack_peek(&s, &v));
for (int i = 1; i <= 5; ++i)
if (stack_push(&s, i * 10) != 0) { printf("day\n"); break; }
printf("so phan tu = %zu\n", stack_so_phan_tu(&s));
stack_peek(&s, &v);
printf("dinh = %d\n", v);
printf("lay ra: ");
while (stack_pop(&s, &v) == 0) printf("%d ", v);
printf("\nso phan tu = %zu\n", stack_so_phan_tu(&s));
return 0;
}pop tren stack rong : -1 peek tren stack rong: -1 so phan tu = 5 dinh = 50 lay ra: 50 40 30 20 10 so phan tu = 0
Thứ tự lấy ra đúng là ngược với thứ tự đẩy vào. Đó là toàn bộ nội dung của chữ LIFO.
#Bản mảng tự lớn
Mảng cố định có giới hạn cứng. Ghép ý tưởng mảng động ở Bài 14.8 vào thì ngăn xếp lớn tùy ý mà vẫn giữ được ưu thế về bộ nhớ đệm so với danh sách liên kết.
#ifndef STACK_DONG_H
#define STACK_DONG_H
#include <stddef.h>
typedef struct {
int *data;
size_t n; /* số phần tử đang có */
size_t suc_chua; /* số phần tử chứa được */
} StackD;
void stack_khoi_tao(StackD *s);
void stack_huy(StackD *s);
int stack_push(StackD *s, int v);
int stack_pop(StackD *s, int *out);
int stack_peek(const StackD *s, int *out);
int stack_rong(const StackD *s);
#endif#include <stdint.h>
#include <stdlib.h>
#include "stack-dong.h"
void stack_khoi_tao(StackD *s)
{
s->data = NULL;
s->n = 0;
s->suc_chua = 0;
}
void stack_huy(StackD *s)
{
free(s->data);
stack_khoi_tao(s);
}
int stack_rong(const StackD *s)
{
return s->n == 0;
}
/* Nhân đôi sức chứa khi hết chỗ. Trả về 0 nếu ổn. */
static int bao_dam(StackD *s)
{
if (s->n < s->suc_chua) return 0;
size_t moi = s->suc_chua ? s->suc_chua * 2 : 8;
if (moi > SIZE_MAX / sizeof *s->data) return -1; /* tránh tràn */
int *tam = realloc(s->data, moi * sizeof *tam);
if (tam == NULL) return -1; /* data cũ vẫn nguyên */
s->data = tam;
s->suc_chua = moi;
return 0;
}
int stack_push(StackD *s, int v)
{
if (bao_dam(s) != 0) return -1;
s->data[s->n++] = v;
return 0;
}
int stack_pop(StackD *s, int *out)
{
if (s->n == 0) return -1;
if (out != NULL) *out = s->data[s->n - 1];
--s->n;
return 0;
}
int stack_peek(const StackD *s, int *out)
{
if (s->n == 0) return -1;
if (out != NULL) *out = s->data[s->n - 1];
return 0;
}Tự làm thử
- Cài
Stackmảng cố định theo cả hai quy ướctop, rồi so xem bản nào ít chỗ dễ sai hơn. - Viết bản
pushkhông kiểm đầy, chạy dưới-fsanitize=addressvà đọc báo cáo tràn. - Cài
StackDtự lớn, rồi insuc_chuasau mỗi lần push để thấy dãy 8, 16, 32. - Đo thời gian push một triệu phần tử vào
StackDvới hệ số lớn lên là 1.5, 2 và 4. Giải thích chênh lệch. - Cài
stack_dao(StackD *s)đảo ngược ngăn xếp chỉ dùng thêm một ngăn xếp phụ, rồi thử với ngăn xếp rỗng và một phần tử.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Ngăn xếp chỉ cho đụng vào một đầu, nên push, pop và peek đều là O(1) và không phải dịch phần tử nào.
- Quy ước
toplà chỉ số ô trống kế tiếp kèmsize_ttránh được giá trị canh âm và nên dùng trong mã thật. - Tràn trên là ghi ra ngoài mảng, một lỗ hổng bảo mật thật. Tràn dưới là đọc
data[-1]. Cả hai chỉ tránh được bằng cách kiểm trước khi thao tác. - Hàm
popnên trả về mã lỗi và ghi giá trị qua tham số ra, vì không có giá trịintnào an toàn để làm dấu hiệu rỗng. - Bản mảng tự lớn cho push chi phí khấu hao O(1), và phải thu nhỏ ở ngưỡng một phần tư để tránh cấp phát lại liên tục.