Back to blog

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

algorithmsdata-structuresarraysstringsinterview-prepdsa
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


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ácThời gianKhông gianGhi chú
Truy cập theo indexO(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ốiO(1)*O(1)Amortized O(1), O(n) khi resize
Chèn vào đầuO(n)O(1)Phải dịch chuyển tất cả phần tử
Chèn vào giữaO(n)O(1)Phải dịch chuyển các phần tử sau
Xóa ở cuốiO(1)O(1)Chỉ giảm length
Xóa ở đầuO(n)O(1)Phải dịch chuyển tất cả phần tử
Xóa ở giữaO(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])); // 6

Bà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")); // true

Bà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"))  # False

Bà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ếpTwo Pointers, Binary Search
Tìm cặp/bộ baTwo Pointers, Hash Map
Subarray/substringSliding Window
Tổng max/minThuật toán Kadane, Sliding Window
Đếm frequencyHash Map
Modification in-placeTwo Pointers

Bài tập Thực hành

Dễ

  1. LeetCode 1: Two Sum
  2. LeetCode 26: Remove Duplicates
  3. LeetCode 125: Valid Palindrome
  4. LeetCode 242: Valid Anagram

Trung bình

  1. LeetCode 3: Longest Substring Without Repeating
  2. LeetCode 53: Maximum Subarray
  3. LeetCode 189: Rotate Array

Khó

  1. LeetCode 42: Trapping Rain Water
  2. LeetCode 76: Minimum Window Substring

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

📬 Đă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.