Arrays và Strings: Nền tảng của Cấu trúc Dữ liệu

Giới thiệu
Arrays và strings là những cấu trúc dữ liệu cơ bản nhất trong lập trình. Chúng xuất hiện trong hầu hết mọi cuộc phỏng vấn coding và ứng dụng thực tế. Trước khi tiếp cận các cấu trúc phức tạp hơn như trees và graphs, bạn cần nắm vững những khối xây dựng cơ bản này.
Bạn Sẽ Học Được
✅ Hiểu cách arrays hoạt động (static vs dynamic)
✅ Phân tích độ phức tạp thời gian của các thao tác array
✅ Nắm vững các kỹ thuật xử lý string
✅ Áp dụng mẫu Two Pointers cho bài toán array
✅ Sử dụng Sliding Window cho bài toán subarray
✅ Giải các bài toán phỏng vấn kinh điển từng bước
Điều kiện tiên quyết
- Hiểu về Big O Notation & Phân tích Độ phức tạp
- Quen thuộc với Mẫu Giải quyết Vấn đề
1. Hiểu về Arrays
Array là một khối bộ nhớ liên tục lưu trữ các phần tử cùng loại. Mỗi phần tử có thể được truy cập tức thì bằng index của nó.
Static vs Dynamic Arrays
Static Arrays (C, Java primitive arrays):
- Kích thước cố định khi tạo
- Bộ nhớ được cấp phát trước
- Không thể resize
Dynamic Arrays (JavaScript arrays, Python lists, Java ArrayList):
- Có thể phát triển hoặc thu nhỏ
- Tự động resize khi đầy (thường gấp đôi dung lượng)
- Linh hoạt hơn nhưng có chi phí resize O(n) đôi khi
// TypeScript - Dynamic array (tự động phát triển)
const arr: number[] = [1, 2, 3];
arr.push(4); // Thêm vào cuối - thường O(1), đôi khi O(n) khi resize
arr.push(5);# Python - Lists là dynamic arrays
arr = [1, 2, 3]
arr.append(4) # O(1) amortized
arr.append(5)2. Các Thao tác Array & Độ phức tạp
Bảng Độ phức tạp
| Thao tác | Thời gian | Không gian | Ghi chú |
|---|---|---|---|
| Truy cập theo index | O(1) | O(1) | Truy cập bộ nhớ trực tiếp |
| Tìm kiếm (chưa sắp xếp) | O(n) | O(1) | Phải kiểm tra từng phần tử |
| Tìm kiếm (đã sắp xếp) | O(log n) | O(1) | Binary search |
| Chèn vào cuối | O(1)* | O(1) | Amortized O(1), O(n) khi resize |
| Chèn vào đầu | O(n) | O(1) | Phải dịch chuyển tất cả phần tử |
| Chèn vào giữa | O(n) | O(1) | Phải dịch chuyển các phần tử sau |
| Xóa ở cuối | O(1) | O(1) | Chỉ giảm length |
| Xóa ở đầu | O(n) | O(1) | Phải dịch chuyển tất cả phần tử |
| Xóa ở giữa | O(n) | O(1) | Phải dịch chuyển các phần tử sau |
3. Các Kỹ thuật Array Phổ biến
Kỹ thuật Two Pointers
Kỹ thuật Two Pointers sử dụng hai chỉ số để giải quyết vấn đề hiệu quả.
Mẫu 1: Hướng Đối lập (Trái và Phải)
// TypeScript - Two Sum trong Array đã sắp xếp
function twoSumSorted(nums: number[], target: number): number[] {
let left = 0;
let right = nums.length - 1;
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) {
return [left, right];
} else if (sum < target) {
left++; // Cần tổng lớn hơn
} else {
right--; // Cần tổng nhỏ hơn
}
}
return [-1, -1]; // Không tìm thấy
}
console.log(twoSumSorted([2, 7, 11, 15], 9)); // [0, 1]# Python - Two Sum trong Array đã sắp xếp
def two_sum_sorted(nums: list[int], target: int) -> list[int]:
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1
else:
right -= 1
return [-1, -1]Mẫu 2: Cùng Hướng (Slow và Fast)
// TypeScript - Xóa Duplicates từ Array đã sắp xếp (in-place)
function removeDuplicates(nums: number[]): number {
if (nums.length === 0) return 0;
let slow = 0; // Trỏ đến phần tử unique cuối cùng
for (let fast = 1; fast < nums.length; fast++) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1; // Độ dài phần unique
}Kỹ thuật Sliding Window
// TypeScript - Tổng lớn nhất của k phần tử liên tiếp
function maxSumSubarray(nums: number[], k: number): number {
if (nums.length < k) return -1;
// Tính tổng của window đầu tiên
let windowSum = 0;
for (let i = 0; i < k; i++) {
windowSum += nums[i];
}
let maxSum = windowSum;
// Trượt window
for (let i = k; i < nums.length; i++) {
windowSum += nums[i]; // Thêm phần tử mới
windowSum -= nums[i - k]; // Xóa phần tử cũ
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
console.log(maxSumSubarray([2, 1, 5, 1, 3, 2], 3)); // 9 (5+1+3)4. Hiểu về Strings
Strings là chuỗi các ký tự. Trong hầu hết ngôn ngữ, chúng được lưu trữ như arrays của ký tự nhưng có các đặc tính đặc biệt.
Tính Bất biến của String
Trong nhiều ngôn ngữ, strings là immutable — bạn không thể thay đổi chúng tại chỗ.
// TypeScript/JavaScript - Strings là immutable
let s = "hello";
// s[0] = "H"; // Điều này không hoạt động!
s = "H" + s.slice(1); // Tạo string mới
console.log(s); // "Hello"# Python - Strings là immutable
s = "hello"
# s[0] = "H" # TypeError!
s = "H" + s[1:] # Tạo string mới
# Để có thể thay đổi, dùng list rồi join
chars = list("hello")
chars[0] = "H"
s = "".join(chars)
print(s) # "Hello"5. Bài toán Array Kinh điển
Bài 1: Two Sum
// TypeScript - Two Sum dùng Hash Map - O(n)
function twoSum(nums: number[], target: number): number[] {
const seen = new Map<number, number>();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) {
return [seen.get(complement)!, i];
}
seen.set(nums[i], i);
}
return [-1, -1];
}
console.log(twoSum([2, 7, 11, 15], 9)); // [0, 1]# Python - Two Sum dùng Hash Map - O(n)
def two_sum(nums: list[int], target: int) -> list[int]:
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return [-1, -1]Bài 2: Maximum Subarray (Thuật toán Kadane)
// TypeScript - Thuật toán Kadane - O(n)
function maxSubArray(nums: number[]): number {
let maxSum = nums[0];
let currentSum = nums[0];
for (let i = 1; i < nums.length; i++) {
// Hoặc mở rộng subarray hiện tại hoặc bắt đầu mới
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6Bài 3: Xoay Array
// TypeScript - Xoay Array - O(n) time, O(1) space
function rotate(nums: number[], k: number): void {
k = k % nums.length;
// Đảo ngược toàn bộ array
reverse(nums, 0, nums.length - 1);
// Đảo ngược k phần tử đầu
reverse(nums, 0, k - 1);
// Đảo ngược các phần tử còn lại
reverse(nums, k, nums.length - 1);
}
function reverse(nums: number[], start: number, end: number): void {
while (start < end) {
[nums[start], nums[end]] = [nums[end], nums[start]];
start++;
end--;
}
}
const arr = [1, 2, 3, 4, 5, 6, 7];
rotate(arr, 3);
console.log(arr); // [5, 6, 7, 1, 2, 3, 4]6. Bài toán String Kinh điển
Bài 1: Valid Palindrome
// TypeScript - Valid Palindrome - O(n)
function isPalindrome(s: string): boolean {
let left = 0;
let right = s.length - 1;
while (left < right) {
// Bỏ qua non-alphanumeric
while (left < right && !isAlphanumeric(s[left])) {
left++;
}
while (left < right && !isAlphanumeric(s[right])) {
right--;
}
if (s[left].toLowerCase() !== s[right].toLowerCase()) {
return false;
}
left++;
right--;
}
return true;
}
function isAlphanumeric(char: string): boolean {
return /[a-zA-Z0-9]/.test(char);
}
console.log(isPalindrome("A man, a plan, a canal: Panama")); // trueBài 2: Valid Anagram
# Python - Valid Anagram dùng Counter - O(n)
from collections import Counter
def is_anagram(s: str, t: str) -> bool:
if len(s) != len(t):
return False
return Counter(s) == Counter(t)
print(is_anagram("anagram", "nagaram")) # True
print(is_anagram("rat", "car")) # FalseBài 3: Longest Substring Without Repeating
// TypeScript - Chuỗi con dài nhất không có ký tự lặp
function lengthOfLongestSubstring(s: string): number {
const charIndex = new Map<string, number>();
let maxLength = 0;
let left = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
// Nếu char đã thấy và nằm trong window hiện tại
if (charIndex.has(char) && charIndex.get(char)! >= left) {
left = charIndex.get(char)! + 1; // Thu nhỏ window
}
charIndex.set(char, right);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
console.log(lengthOfLongestSubstring("abcabcbb")); // 3 ("abc")Tóm tắt và Điểm Chính
Arrays
- Bộ nhớ liên tục cho phép truy cập O(1) theo index
- Chèn/xóa ở đầu là O(n) do phải dịch chuyển
- Dynamic arrays tự động resize (amortized O(1) append)
- Dùng Two Pointers cho arrays đã sắp xếp
- Dùng Sliding Window cho bài toán subarray
Strings
- Thường immutable — modifications tạo strings mới
- Chuyển thành char array để modifications in-place
- Nhiều bài string dùng cùng mẫu với arrays
- Hash maps cho frequency counting và anagram detection
Nhận diện Mẫu
| Nếu bạn thấy... | Cân nhắc... |
|---|---|
| Array đã sắp xếp | Two Pointers, Binary Search |
| Tìm cặp/bộ ba | Two Pointers, Hash Map |
| Subarray/substring | Sliding Window |
| Tổng max/min | Thuật toán Kadane, Sliding Window |
| Đếm frequency | Hash Map |
| Modification in-place | Two Pointers |
Bài tập Thực hành
Dễ
- LeetCode 1: Two Sum
- LeetCode 26: Remove Duplicates
- LeetCode 125: Valid Palindrome
- LeetCode 242: Valid Anagram
Trung bình
- LeetCode 3: Longest Substring Without Repeating
- LeetCode 53: Maximum Subarray
- LeetCode 189: Rotate Array
Khó
Tiếp theo
Bây giờ bạn đã nắm vững arrays và strings, bạn sẵn sàng học về Linked Lists trong bài viết tiếp theo.
Tiếp tục hành trình DSA: Linked Lists Complete Guide
Tài nguyên Bổ sung
Các Bài Viết Trước trong Series
- DSA Roadmap - Lộ trình học tập hoàn chỉnh
- Big O Notation & Phân tích Độ phức tạp - Hiểu độ phức tạp thời gian và không gian
- Mẫu Giải quyết Vấn đề - Các mẫu thiết yếu cho phỏng vấn coding
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.