Bài 22.220 phút đọc
Ngăn xếp bằng danh sách liên kết
Sau bài này bạn sẽ làm được
- Cài ngăn xếp liên kết với top là con trỏ
- So sánh hai cách cài trên bốn tiêu chí
- Xử lý hết bộ nhớ trong push
- Hủy ngăn xếp không rò rỉ
Ngăn xếp bằng danh sách liên kết không có giới hạn kích thước và không bao giờ phải cấp phát lại cả khối. Đổi lại mỗi lần push là một lần malloc, và duyệt thì mất hết lợi thế bộ nhớ đệm.
#Cài đặt
stack-lk.h
#ifndef STACK_LK_H
#define STACK_LK_H
#include <stddef.h>
typedef struct SNode {
int data;
struct SNode *next;
} SNode;
/* Bất biến:
1. top == NULL khi và chỉ khi size == 0
2. size == số nút đi được từ top theo next
Không cần con trỏ đáy: mọi thao tác chỉ đụng vào đỉnh. */
typedef struct {
SNode *top;
size_t size;
} StackLK;
void stack_khoi_tao(StackLK *s);
void stack_huy(StackLK *s);
int stack_rong(const StackLK *s);
size_t stack_so_phan_tu(const StackLK *s);
int stack_push(StackLK *s, int v);
int stack_pop(StackLK *s, int *out);
int stack_peek(const StackLK *s, int *out);
#endifstack-lk.c
#include <stdlib.h>
#include "stack-lk.h"
void stack_khoi_tao(StackLK *s)
{
s->top = NULL;
s->size = 0;
}
int stack_rong(const StackLK *s)
{
return s->top == NULL;
}
size_t stack_so_phan_tu(const StackLK *s)
{
return s->size;
}
/* Trả về 0 nếu ổn, -1 nếu hết bộ nhớ. Không bao giờ đầy vì lý do khác. */
int stack_push(StackLK *s, int v)
{
SNode *n = malloc(sizeof *n);
if (n == NULL) return -1;
n->data = v;
n->next = s->top; /* nút mới trỏ vào đỉnh cũ */
s->top = n; /* nút mới thành đỉnh */
++s->size;
return 0;
}
int stack_pop(StackLK *s, int *out)
{
if (s->top == NULL) return -1;
SNode *n = s->top;
if (out != NULL) *out = n->data;
s->top = n->next; /* đọc next TRƯỚC khi free */
free(n);
--s->size;
return 0;
}
int stack_peek(const StackLK *s, int *out)
{
if (s->top == NULL) return -1;
if (out != NULL) *out = s->top->data;
return 0;
}
void stack_huy(StackLK *s)
{
while (stack_pop(s, NULL) == 0) { }
}#Vì sao mã gọn hơn hẳn
So với Bài 21.3 và 21.4, mã ở đây không có nhánh nào cho trường hợp danh sách rỗng. Lý do là ngăn xếp chỉ đụng vào một đầu, nên không có tail nào phải giữ đồng bộ.
| Danh sách đầy đủ (Bài 21) | Ngăn xếp liên kết | |
|---|---|---|
| Con trỏ trong struct | head và tail | Chỉ top |
| Số bất biến | 4 | 2 |
| Nhánh xử lý danh sách rỗng trong push | Có | Không |
| Nhánh xử lý danh sách rỗng trong pop | Có | Không |
| Số dòng của push | 10 | 8 |
#So sánh hai cách cài
| Tiêu chí | Mảng | Danh sách liên kết |
|---|---|---|
| Tốc độ push và pop | Nhanh hơn nhiều | Chậm hơn, mỗi lần một malloc |
| Giới hạn kích thước | Cố định, hoặc phải realloc | Chỉ giới hạn bởi bộ nhớ hệ thống |
| Bộ nhớ mỗi phần tử | 4 byte | 32 byte thực tế |
| Cấp phát | Một lần, trước | Mỗi phần tử một lần |
| Chi phí xấu nhất của một push | O(n) khi phải nhân đôi | O(1) nhưng malloc có thể chậm |
| Con trỏ tới phần tử có bền không | Không, realloc làm treo | Có, nút không bao giờ dịch |
| Thân thiện bộ nhớ đệm | Rất | Kém |
#Đo trên máy thật
do-hai-cach.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 5000000
/* --- Bản mảng động --- */
typedef struct { int *d; size_t n, cap; } SA;
static int sa_push(SA *s, int v)
{
if (s->n == s->cap) {
size_t moi = s->cap ? s->cap * 2 : 8;
int *t = realloc(s->d, moi * sizeof *t);
if (t == NULL) return -1;
s->d = t; s->cap = moi;
}
s->d[s->n++] = v;
return 0;
}
/* --- Bản liên kết --- */
typedef struct SNode { int data; struct SNode *next; } SNode;
typedef struct { SNode *top; } SL;
static int sl_push(SL *s, int v)
{
SNode *n = malloc(sizeof *n);
if (n == NULL) return -1;
n->data = v; n->next = s->top; s->top = n;
return 0;
}
int main(void)
{
SA a = { NULL, 0, 0 };
SL l = { NULL };
clock_t t0 = clock();
for (int i = 0; i < N; ++i) if (sa_push(&a, i) != 0) return 1;
double g_a = (double)(clock() - t0) / CLOCKS_PER_SEC;
t0 = clock();
for (int i = 0; i < N; ++i) if (sl_push(&l, i) != 0) return 1;
double g_l = (double)(clock() - t0) / CLOCKS_PER_SEC;
/* Đo cả pop, tức duyệt ngược */
long long tong = 0;
t0 = clock();
while (a.n > 0) tong += a.d[--a.n];
double p_a = (double)(clock() - t0) / CLOCKS_PER_SEC;
t0 = clock();
while (l.top != NULL) {
SNode *t = l.top->next;
tong += l.top->data;
free(l.top);
l.top = t;
}
double p_l = (double)(clock() - t0) / CLOCKS_PER_SEC;
printf("push mang : %.3f s\n", g_a);
printf("push danh sach : %.3f s (%.1f lan cham hon)\n", g_l, g_l / g_a);
printf("pop mang : %.3f s\n", p_a);
printf("pop danh sach : %.3f s (%.1f lan cham hon)\n", p_l, p_l / p_a);
printf("bo nho mang : %zu byte\n", a.cap * sizeof *a.d);
printf("bo nho danh sach: %zu byte\n", (size_t)N * 32);
printf("(tong %lld)\n", tong);
free(a.d);
return 0;
}terminal
gcc -std=c17 -O2 do-hai-cach.c -o t && ./t
push mang : 0.024 s push danh sach : 0.187 s (7.8 lan cham hon) pop mang : 0.005 s pop danh sach : 0.042 s (8.4 lan cham hon) bo nho mang : 33554432 byte bo nho danh sach: 160000000 byte (tong 24999995000000)
Chương trình thử tính đúng
main.c
#include <stdio.h>
#include "stack-lk.h"
int main(void)
{
StackLK s;
int v;
stack_khoi_tao(&s);
printf("pop tren rong: %d\n", stack_pop(&s, &v));
for (int i = 1; i <= 5; ++i)
if (stack_push(&s, i * 10) != 0) { stack_huy(&s); return 1; }
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));
/* Đẩy lại rồi hủy giữa chừng để kiểm rò rỉ */
for (int i = 0; i < 1000; ++i)
if (stack_push(&s, i) != 0) { stack_huy(&s); return 1; }
stack_huy(&s);
stack_huy(&s); /* gọi lần hai vẫn an toàn */
return 0;
}terminal
gcc -std=c17 -Wall -Wextra -g stack-lk.c main.c -o t && ./t
pop tren rong: -1 so phan tu : 5 dinh : 50 lay ra : 50 40 30 20 10 so phan tu : 0
# Valgrind chạy trên bản không bật sanitizer
valgrind --leak-check=full ./t
All heap blocks were freed -- no leaks are possible ERROR SUMMARY: 0 errors from 0 contexts
#Chọn cách nào
Phần tử nhỏ, dùng liên kết
/* Ngăn xếp int, dùng danh sách liên kết
32 byte hạ tầng cho 4 byte dữ liệu, và mỗi push một malloc */
typedef struct SNode { int data; struct SNode *next; } SNode;Phần tử nhỏ, dùng mảng
/* Ngăn xếp int, dùng mảng động
Bộ nhớ liên tiếp, một lần cấp phát cho mỗi mốc nhân đôi */
typedef struct { int *d; size_t n, cap; } StackD;Tự làm thử
- Cài
StackLKđầy đủ và chạy dưới valgrind cho tới khi không còn rò rỉ. - Viết
stack_poptheo thứ tự sai, tứcfreetrước rồi đọcnext, và xem-fsanitize=addressbáo gì. - Chạy
do-hai-cach.ctrên máy bạn với N bằng 1000, 100000 và 5 triệu. Giải thích vì sao tỷ lệ thay đổi. - Sửa cả hai bản để phần tử là struct 256 byte, rồi đo lại. Xem kết luận có đổi không.
- Cài
stack_chuyen(StackLK *tu, StackLK *toi, size_t k)chuyển k phần tử đỉnh sang ngăn xếp khác mà không cấp phát gì thêm.
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 liên kết chỉ cần một con trỏ
top, và không có nhánh nào cho trường hợp rỗng vìn->next = toptự đúng. - Mọi thao tác vẫn là O(1), nhưng chậm hơn bản mảng khoảng tám lần và tốn bộ nhớ gấp năm.
- Bản mảng có lần push cực chậm ở mỗi mốc nhân đôi. Bản liên kết không, nên nó thắng khi cần độ trễ tối đa thấp.
- Nút của bản liên kết không bao giờ dịch chỗ, nên con trỏ tới phần tử vẫn dùng được sau khi ngăn xếp lớn lên.
- Mặc định hãy dùng mảng động. Chỉ chuyển sang liên kết khi có một trong bốn lý do cụ thể ở cuối bài.