Dynamic Array: mảng động và cách tự cài đặt

Hiểu size, capacity và cơ chế nhân đôi, tự cài đặt Dynamic Array bằng C/C++.
DễCấu trúc dữ liệu ~60 phút

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 == capacity khi push → 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 O(1)O(1) trung bình.

Bảng độ phức tạp

Thao tácĐộ phức tạpLý do
get/set theo chỉ sốO(1)O(1)tính địa chỉ trực tiếp
push/pop cuốiO(1)O(1) trung bìnhthỉnh thoảng copy khi đầy
insert/erase giữaO(n)O(n)phải dồn phần tử giữ liên tục
reserve/shrinkO(n)O(n)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:

  • realloc qua con trỏ tạm, thất bại thì mảng cũ còn nguyên.
  • Mọi get/set/insert/erase/pop kiểm tra biên, không crash.
  • destroy free đúng 1 lần, gán NULL, 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àmViệ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.O(1)O(1)
void da_destroy(DynamicArray *a)free, gán data = NULL, size = capacity = 0. Gọi 2 lần vẫn an toàn.O(1)O(1)
bool da_push(DynamicArray *a, int x)Thêm cuối. Đầy thì nhân đôi capacity trước.O(1)O(1) 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.O(1)O(1)
bool da_get(const DynamicArray *a, size_t i, int *out)Đọc ô i. i >= size thì trả false.O(1)O(1)
bool da_set(DynamicArray *a, size_t i, int x)Ghi ô i. Ngoài biên thì trả false.O(1)O(1)
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.O(n)O(n)
bool da_erase(DynamicArray *a, size_t i)Xóa ô i, dồn phần sau sang trái.O(n)O(n)
bool da_reserve(DynamicArray *a, size_t cap)Đảm bảo capacity >= cap. Không bao giờ giảm.O(n)O(n)
bool da_shrink_to_fit(DynamicArray *a)Đưa capacity về max(size, 1).O(n)O(n)

Ràng buộc

  • Chỉ dùng malloc, realloc, free, memmove. Không dùng thư viện container.
  • realloc luô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.

  1. Khởi tạo cap = 1, push 10 phần tử. In ra mỗi lần capacity đổi. Kết quả phải là 1 -> 2 -> 4 -> 8 -> 16.
  2. 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.
  3. Với mỗi chiến lược, push n=105n = 10^5 phầ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ỉ n2/2n^2/2. 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ầnYê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 assignmentDeep copy. Tự gán v = v không hỏng
Move constructor, move assignmentChuyể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.

Dùng ::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 assert của Bài 1 pass.
  • Build với gcc -Wall -Wextra -g -fsanitize=address,undefined không có cảnh báo, không lỗi runtime.
  • valgrind --leak-check=full ./test_da báo 0 bytes in 0 blocks bị 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 push O(1)O(1) trung bình, còn +1 cho tổng O(n2)O(n^2).

❓ Câu hỏi ôn tập

  • size khác capacity ở điểm nào? Khi nào phải cấp phát lại?
  • Vì sao push cuối là O(1)O(1) trung bình nhưng insert giữa là O(n)O(n)?
  • Vì sao realloc phải qua con trỏ tạm?
  • Khi nào dùng reserve, khi nào dùng shrink_to_fit?

🌕 Bạn đã xong ngày 1. Cài đặt xong Dynamic Array chạy sạch thì sang Day 2.

Bớt paste mù, hiểu sâu từng dòng mình viết.

180 Days Challenge. 180 ngày cai nghiện vibe code.