Dynamic Array: mảng động và cách tự cài đặt
Ôn lại trước khi học
Giới thiệu
Mảng động là vùng nhớ liên tục có dự trữ chỗ trống. Hết chỗ thì cấp vùng gấp đôi và sao chép sang. Bài này chỉ học một thứ: tự cài đặt Dynamic Array.
Kiến thức cần nhớ
Size vs capacity
size: số phần tử đang dùng.capacity: số ô đã cấp phát.size == capacitykhipush→ phải cấp phát lại.
Vì sao nhân đôi
Cấp vùng mới gấp đôi, copy toàn bộ, rồi mới thêm. Nhờ dự trữ dư nên n lần push chỉ tốn trung bình.
Bảng độ phức tạp
| Thao tác | Độ phức tạp | Lý do |
|---|---|---|
get/set theo chỉ số | tính địa chỉ trực tiếp | |
push/pop cuối | trung bình | thỉnh thoảng copy khi đầy |
insert/erase giữa | phải dồn phần tử giữ liên tục | |
reserve/shrink | cấp mới + copy một lần |
Khung struct
// C
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
// C++: class DynamicArray với data_, size_, capacity_ + destructor
Quy tắc bắt buộc:
reallocqua con trỏ tạm, thất bại thì mảng cũ còn nguyên.- Mọi
get/set/insert/erase/popkiểm tra biên, không crash. destroyfree đúng 1 lần, gánNULL, không leak.
Ví dụ
DynamicArray a; da_init(&a, 2);
da_push(&a, 1); da_push(&a, 2); da_push(&a, 3);
// capacity: 2 -> 4, size = 3
da_get(&a, 1); // = 2
da_insert(&a, 1, 9); // 1, 9, 2, 3
da_erase(&a, 1); // 1, 2, 3
da_pop(&a); // 1, 2
da_destroy(&a);
💻 Bài tập
Ngôn ngữ: C cho Bài 1-2, C++ cho Bài 3. Nộp vào thư mục day-001/ trong repo của bạn.
Bài 1: Cài đặt Dynamic Array bằng C
Viết dynamic_array.h và dynamic_array.c. Mảng lưu phần tử kiểu int.
typedef struct {
int *data;
size_t size; // số phần tử đang dùng
size_t capacity; // số ô đã cấp phát
} DynamicArray;
API bắt buộc. Mọi hàm trả bool đều trả false khi lỗi và giữ mảng nguyên trạng.
| Hàm | Việc phải làm | Độ phức tạp |
|---|---|---|
bool da_init(DynamicArray *a, size_t cap) | Cấp cap ô (cap == 0 thì dùng 1). size = 0. | |
void da_destroy(DynamicArray *a) | free, gán data = NULL, size = capacity = 0. Gọi 2 lần vẫn an toàn. | |
bool da_push(DynamicArray *a, int x) | Thêm cuối. Đầy thì nhân đôi capacity trước. | trung bình |
bool da_pop(DynamicArray *a, int *out) | Lấy phần tử cuối vào *out. Rỗng thì trả false. | |
bool da_get(const DynamicArray *a, size_t i, int *out) | Đọc ô i. i >= size thì trả false. | |
bool da_set(DynamicArray *a, size_t i, int x) | Ghi ô i. Ngoài biên thì trả false. | |
bool da_insert(DynamicArray *a, size_t i, int x) | Chèn tại i, với 0 <= i <= size. Dồn phần sau sang phải. | |
bool da_erase(DynamicArray *a, size_t i) | Xóa ô i, dồn phần sau sang trái. | |
bool da_reserve(DynamicArray *a, size_t cap) | Đảm bảo capacity >= cap. Không bao giờ giảm. | |
bool da_shrink_to_fit(DynamicArray *a) | Đưa capacity về max(size, 1). |
Ràng buộc
- Chỉ dùng
malloc,realloc,free,memmove. Không dùng thư viện container. reallocluôn qua con trỏ tạm. Cấp phát thất bại thì dữ liệu cũ còn nguyên.- Thư viện không
printf. Lỗi báo qua giá trị trả về.
Test mẫu. Viết trong test_da.c bằng assert.
init(cap=2); push 1, 2, 3 -> size=3, capacity=4, [1, 2, 3]
get(1) -> true, out=2
insert(1, 9) -> [1, 9, 2, 3]
insert(4, 7) -> [1, 9, 2, 3, 7] (chèn cuối hợp lệ)
erase(1) -> [1, 2, 3, 7]
pop -> true, out=7, [1, 2, 3]
get(5) / set(5, 0) / erase(5) -> false, mảng không đổi
insert(9, 0) -> false
pop 3 lần, rồi pop tiếp -> lần thứ 4 trả false
reserve(100); shrink_to_fit -> capacity 100, rồi về 1
insert dùng memmove(&data[i+1], &data[i], (size-i) * sizeof(int)). Không dùng memcpy vì hai vùng nhớ chồng nhau.Bài 2: Đo vết capacity và chi phí copy
Thêm vào struct biến đếm size_t copies: tổng số phần tử bị sao chép khi cấp phát lại.
- Khởi tạo
cap = 1,push10 phần tử. In ra mỗi lầncapacityđổi. Kết quả phải là1 -> 2 -> 4 -> 8 -> 16. - Viết thêm 2 chiến lược tăng:
+1 ôvàx1.5. Chọn chiến lược qua tham số ởda_init. - Với mỗi chiến lược,
pushphần tử rồi in bảng:
chien_luoc n copies copies/n
x2 100000 ... ...
x1.5 100000 ... ...
+1 100000 ... ...
Kiểm tra: nhân đôi cho copies < 2n. Chiến lược +1 cho copies xấp xỉ . Viết 3 dòng trong README.md giải thích kết quả.
Bài 3: Bản C++ Vec<T> (nâng cao)
Viết class template Vec<T> trong vec.hpp, tự quản lý bộ nhớ. Không dùng std::vector.
| Thành phần | Yêu cầu |
|---|---|
Vec(), explicit Vec(size_t cap) | Khởi tạo rỗng |
~Vec() | Hủy đúng các phần tử đang dùng rồi giải phóng vùng nhớ |
| Copy constructor, copy assignment | Deep copy. Tự gán v = v không hỏng |
| Move constructor, move assignment | Chuyển con trỏ, để nguồn ở trạng thái rỗng hợp lệ |
push_back(const T&), push_back(T&&), pop_back() | Như Bài 1 |
T& operator[](size_t) | Không kiểm tra biên |
T& at(size_t) | Ném std::out_of_range khi ngoài biên |
insert(size_t i, const T&), erase(size_t i) | Như Bài 1 |
size(), capacity(), empty(), reserve(n) | Như Bài 1 |
T* begin(), T* end() | Để dùng được for (auto &x : v) |
Test mẫu: dùng Vec<std::string>. Push 1000 chuỗi, copy sang Vec khác, sửa bản copy, kiểm tra bản gốc không đổi. Move sang biến mới, kiểm tra nguồn có size() == 0.
::operator new(cap * sizeof(T)) để cấp vùng nhớ thô, rồi placement new (&data[i]) T(x) để dựng phần tử. Như vậy capacity dư không gọi constructor thừa.✅ Tiêu chí hoàn thành
- Toàn bộ test
assertcủa Bài 1 pass. - Build với
gcc -Wall -Wextra -g -fsanitize=address,undefinedkhông có cảnh báo, không lỗi runtime. -
valgrind --leak-check=full ./test_dabáo0 bytes in 0 blocksbị leak. - Bảng đo Bài 2 có số thật và 3 dòng giải thích.
- Nói được bằng lời: vì sao nhân đôi cho
pushtrung bình, còn+1cho tổng .
❓ Câu hỏi ôn tập
sizekháccapacityở điểm nào? Khi nào phải cấp phát lại?- Vì sao
pushcuối là trung bình nhưnginsertgiữa là ? - Vì sao
reallocphải qua con trỏ tạm? - Khi nào dùng
reserve, khi nào dùngshrink_to_fit?
🌕 Bạn đã xong ngày 1. Cài đặt xong Dynamic Array chạy sạch thì sang Day 2.