Array — Cấu trúc dữ liệu nguyên thủy nhất
Bạn đã dùng [] không biết bao nhiêu lần trong TypeScript. Array (Mảng) là cấu trúc dữ liệu quen thuộc nhất, có mặt ở mọi ngôn ngữ lập trình. Nhưng bạn đã bao giờ thắc mắc tại sao push() thì chạy rất nhanh, còn unshift() (thêm vào đầu mảng) lại khiến hiệu năng ứng dụng tụt dốc không phanh?
Chào mừng bạn đến với Phần 2: Linear Data Structures. Và chúng ta sẽ bắt đầu bằng việc "giải phẫu" Array.
📋 Agenda
Thời gian đọc ước tính: ~7 phút
Sau bài này, bạn sẽ:
- ✅ Hiểu được Array thực sự được lưu trữ như thế nào dưới RAM (Bộ nhớ vật lý).
- ✅ Giải thích được tại sao truy xuất Array qua index
arr[5]lại luôn mấtO(1)thời gian. - ✅ Phân biệt được mảng tĩnh (Static Array) và mảng động (Dynamic Array).
- ✅ Nắm bắt được hiệu năng Time Complexity của các thao tác thêm, xóa, sửa trên Array.
Yêu cầu đầu vào:
- 🔹 Đã đọc và hiểu về Big-O Notation.
❓ Tại sao Array tồn tại?
Vấn đề (Problem Statement): Giả sử bạn cần lưu trữ điểm thi của 5 học sinh. Nếu không có Array, bạn phải làm thế này:
let score1 = 8;
let score2 = 9;
let score3 = 7;
// ... và nếu có 10,000 học sinh, bạn phải khai báo 10,000 biến!
Hơn nữa, nếu bạn muốn tính điểm trung bình, bạn phải cộng tay từng biến một vì chúng nằm rải rác không liên quan gì đến nhau trong bộ nhớ.
Giải pháp (Solution): Array ra đời để giải quyết vấn đề nhóm dữ liệu. Nó yêu cầu hệ điều hành cấp phát một khối lượng bộ nhớ liên tiếp nhau. Nhờ đó, bạn chỉ cần giữ một biến duy nhất (con trỏ chỉ vào đầu khối bộ nhớ) và duyệt qua hàng vạn dữ liệu bằng một vòng lặp đơn giản.
📖 Array dưới góc nhìn của Hệ Điều Hành
Định nghĩa: Array là một cấu trúc dữ liệu lưu trữ một tập hợp các phần tử có cùng kiểu dữ liệu, được sắp xếp liên tiếp nhau (contiguous) trong bộ nhớ máy tính.
Trực quan hóa bộ nhớ (RAM)
Hãy tưởng tượng RAM như một dãy tủ đồ khóa số liền kề nhau:
- Index:
0, 1, 2, 3...— Số thứ tự (Mảng luôn bắt đầu từ 0). - Value: Dữ liệu thực tế.
- Memory Address: Địa chỉ ô nhớ (Ví dụ: cách nhau 4 bytes mỗi ô).
Tại sao truy xuất arr[2] lại cực nhanh (O(1))?
Bởi vì Array nằm liên tiếp, máy tính chỉ cần dùng một công thức toán học cực đơn giản:
Địa chỉ phần tử [i] = Địa chỉ ô đầu tiên + (i * Kích thước 1 phần tử)
Ví dụ, muốn tìm arr[2] khi biết ô đầu tiên ở 0x00A1 (và mỗi phần tử chiếm 4 bytes):
0x00A1 + (2 * 4) = 0x00A9. Máy tính phi thẳng đến 0x00A9 lấy dữ liệu. Chớp mắt, O(1).
🔨 Time Complexity của Array
Bây giờ hãy phân tích các thao tác quen thuộc trong TypeScript dưới lăng kính Big-O.
1. Thêm vào cuối mảng — push()
Đây là hành động lý tưởng nhất. Máy tính chỉ cần đến ô nhớ trống tiếp theo ở đuôi và nhét dữ liệu vào.
- Time Complexity:
O(1)
const arr = ['A', 'B', 'C'];
arr.push('D'); // O(1) - Rất nhanh
2. Thêm vào đầu mảng — unshift()
Đây là "cơn ác mộng" hiệu năng. Mảng là một khối bộ nhớ cố định. Nếu bạn muốn nhét thêm người vào đầu hàng, BẮT BUỘC toàn bộ những người phía sau phải lùi lại 1 bước để nhường chỗ.
- Time Complexity:
O(n)
const arr = ['A', 'B', 'C'];
// Để nhét 'X' vào vị trí 0:
// 1. Dời 'C' từ index 2 sang 3
// 2. Dời 'B' từ index 1 sang 2
// 3. Dời 'A' từ index 0 sang 1
// 4. Nhét 'X' vào index 0
arr.unshift('X'); // O(n) - Rất chậm nếu n lớn