Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Day 1 — Arrays, Hashing & Time Complexity


Learning Objectives

  • Nắm vững cách đo lường hiệu suất thuật toán qua Big O Notation.
  • Phân biệt Static ArrayDynamic Array (Vector) trong bộ nhớ.
  • Sử dụng HashSet để tối ưu hóa tìm kiếm từ O(n^2) xuống O(n).

Key Concepts & Glossary

Big O Notation

Big O mô tả kịch bản tệ nhất (worst-case) về sự tăng trưởng của thời gian chạy (T) hoặc bộ nhớ (S) khi kích thước đầu vào (n) tăng lên.

  • O(1): Constant (Array index access).
  • O(n): Linear (Single loop).
  • O(n log n): Log-linear (Sorting like Quicksort/Mergesort).
  • O(n^2): Quadratic (Nested loops).

The Hashing Trade-off

Sử dụng HashSet hoặc HashMap là một sự đánh đổi kinh điển: Dùng bộ nhớ để mua lấy thời gian. Chúng ta tốn thêm không gian để lưu bảng băm nhưng đổi lại tốc độ tìm kiếm chỉ còn O(1) trung bình.

Các thuật ngữ quan trọng:

  • Static Array: Mảng có kích thước cố định, nằm trên Stack.
  • Dynamic Array (Vec): Mảng có thể co giãn, dữ liệu nằm trên Heap, quản lý bởi một pointer trên Stack.
  • Amortized Time Complexity: Độ phức tạp "trung bình tích lũy". Ví dụ: push vào Vector thường là O(1), nhưng khi đầy sẽ tốn O(n) để cấp phát lại, trung bình vẫn coi là O(1).
  • Hashing Function: Hàm biến một giá trị đầu vào thành một chỉ số duy nhất trong bộ nhớ.

Example Implementation

Cargo.toml

[package]
name = "algo01_arrays_hashing"
version = "0.1.0"
edition = "2024"

[dependencies]
# No external crates needed for basic exercises

src/main.rs

use std::collections::HashSet;

fn main() {
    let nums = vec![1, 2, 3, 1];
    
    // Task: Check for duplicates (LeetCode 217)
    let has_duplicate = contains_duplicate(&nums);
    println!("Has duplicate: {has_duplicate}");

    // Task: Valid Anagram (LeetCode 242)
    let s = String::from("anagram");
    let t = String::from("nagaram");
    println!("Is anagram: {}", is_anagram(s, t));
}

/// O(n) approach using a HashSet
fn contains_duplicate(nums: &[i32]) -> bool {
    let mut seen = HashSet::with_capacity(nums.len());
    for &num in nums {
        // insert returns false if the value was already present
        if !seen.insert(num) {
            return true;
        }
    }
    false
}

/// O(n) approach using a Frequency Table (Array of 26 integers)
fn is_anagram(s: String, t: String) -> bool {
    if s.len() != t.len() { 
        return false; 
    }
    
    let mut counts = [0i32; 26];
    
    // Use zip to iterate through both strings simultaneously in O(n)
    for (sb, tb) in s.bytes().zip(t.bytes()) {
        counts[(sb - b'a') as usize] += 1;
        counts[(tb - b'a') as usize] -= 1;
    }
    
    // Ensure all frequencies are zero
    counts.iter().all(|&x| x == 0)
}

Hands-on Exercises

  1. two_sum(nums: Vec, target: i32) -> Vec Tìm chỉ số của hai số có tổng bằng target. Sử dụng HashMap để đạt O(n). Ví dụ: [2, 7, 11, 15], target = 9 -> [0, 1].
  2. is_alphanumeric_palindrome(s: &str) -> bool Kiểm tra chuỗi đối xứng, bỏ qua ký tự đặc biệt và hoa thường. Sử dụng kỹ thuật Two Pointers. Ví dụ: "A man, a plan, a canal: Panama" -> true.
  3. replace_elements(arr: Vec) -> Vec Thay thế mỗi phần tử bằng phần tử lớn nhất bên phải nó. Phần tử cuối cùng thay bằng -1. Ví dụ: [17, 18, 5, 4, 6, 1] -> [18, 6, 6, 6, 1, -1].

Common Pitfalls & Debugging Tips

Common Pitfalls

  • Tràn số nguyên (Integer Overflow): Khi tính toán chỉ số (index) hoặc tổng, hãy cẩn trọng với giới hạn của kiểu dữ liệu. Ví dụ: giá trị 5 * 10^4 (50.000) nằm gọn trong kiểu i32, nhưng bình phương của nó (2.500.000.000) sẽ gây ra lỗi tràn số vì vượt quá giới hạn tối đa của i32 (khoảng 2 * 10^9).
  • Truy cập chỉ số thủ công (Manual Indexing): Hạn chế sử dụng for i in 0..arr.len(). Trong Rust, bạn nên ưu tiên sử dụng .iter(), .enumerate() hoặc .zip(). Cách làm này giúp ngăn chặn lỗi "vượt quá phạm vi" (out-of-bounds) và giúp trình biên dịch tối ưu hóa mã tốt hơn bằng cách loại bỏ các bước kiểm tra giới hạn không cần thiết (Bounds-check elimination).

Q&A — Common Questions

Q1. Tại sao HashSet trong Rust đôi khi chạy chậm hơn C++?

  • Mặc định Rust dùng SipHash để chống tấn công HashDoS. Nếu bạn cần tốc độ tối đa cho các bài tập thuật toán (nơi không lo bị tấn công), bạn có thể dùng FxHash từ crate rustc-hash.

Q2. Khi nào nên dùng Sorting thay vì HashMap?

  • Khi bộ nhớ (Space) bị giới hạn cực kỳ khắt khe. Sorting thường tốn O(1) hoặc O(log n) bộ nhớ phụ trợ, trong khi HashMap tốn O(n) để lưu các keys.

Wisdom Note

Đạo Đức Kinh — Chương 11

Tam thập phúc, cộng nhất cốc, đương kỳ vô, hữu xa chi dụng. Diên thực dĩ vi khí, đương kỳ vô, hữu khí chi dụng. Tạc hộ dũ dĩ vi thất, đương kỳ vô, hữu thất chi dụng. Cố hữu chi dĩ vi lợi, vô chi dĩ vi dụng.