Day 2 — Categorization & Top K Elements
Learning Objectives
- Làm chủ kỹ thuật Phân nhóm (Categorization) dữ liệu bằng cách sử dụng
HashMap. - Sử dụng Mảng tĩnh (Static Array) làm Key cho
HashMapđể tối ưu hóa từ O(k log k) xuống O(k). - Hiểu và áp dụng kỹ thuật Bucket Sort để giải quyết các bài toán về tần suất (Top K) trong thời gian tuyến tính O(n).
Key Concepts & Glossary
Trong kỹ thuật này, ta sử dụng HashMap để nhóm các phần tử có cùng "đặc điểm chung" lại với nhau:
- Key (Khóa): Đặc điểm chung được chuẩn hóa (ví dụ: một chuỗi đã sắp xếp hoặc mảng tần suất).
- Value (Giá trị): Một danh sách (
Vec) chứa các phần tử gốc thuộc về nhóm đó.
Trong Rust, mảng tĩnh như [u8; 26] có thể được dùng làm Key cho HashMap vì chúng thực thi Trait Hash và Eq. Cách này giúp ta tránh việc phải sắp xếp chuỗi (O(k log k)), đưa việc tạo "chữ ký" về thời gian tuyến tính O(k).
Các thuật ngữ quan trọng:
- Entry API: Một tính năng mạnh mẽ của Rust giúp thực hiện thao tác "kiểm tra và cập nhật" (Check-and-update) trên
HashMapchỉ với một lần tìm kiếm (Single-lookup). - Bucket Sort: Kỹ thuật phân phối các phần tử vào các "xô" dựa trên tần suất xuất hiện của chúng để tìm Top K mà không cần sắp xếp toàn bộ mảng.
- Frequency Map: Một
HashMapdùng để đếm số lần xuất hiện của từng phần tử.
Example Implementation
Cargo.toml
[package]
name = "algo02_categorization"
version = "0.1.0"
edition = "2024"
[dependencies]
# Standard library is sufficient
src/main.rs
use std::collections::HashMap; fn main() { let strs = vec![ "eat".to_string(), "tea".to_string(), "tan".to_string(), "ate".to_string(), "nat".to_string(), "bat".to_string() ]; // Task: Group Anagrams (LeetCode 49) let grouped = group_anagrams(strs); println!("Grouped Anagrams: {:?}", grouped); } /// O(n * k) time complexity where n is number of strings and k is max length fn group_anagrams(strs: Vec<String>) -> Vec<Vec<String>> { // Key is a frequency array of 26 lowercase letters let mut map: HashMap<[u8; 26], Vec<String>> = HashMap::new(); for s in strs { let mut count = [0u8; 26]; for &b in s.as_bytes() { count[(b - b'a') as usize] += 1; } // Using Entry API for efficient insertion // If the key doesn't exist, it creates a new Vec map.entry(count).or_insert(Vec::new()).push(s); } // Transform map values into the final result vector map.into_values().collect() }
Hands-on Exercises
- top_k_frequent(nums: Vec
, k: i32) -> Vec Tìm K phần tử xuất hiện nhiều nhất trong mảng. Hãy thử giải bằng Bucket Sort để đạt O(n) thay vì O(n log n). Ví dụ:nums = [1,1,1,2,2,3], k = 2->[1, 2]. - product_except_self(nums: Vec
) -> Vec Trả về mảng kết quả sao cho mỗi phần tử tạiibằng tích của tất cả các số khác trừnums[i]. Không được dùng phép chia. Ví dụ:[1, 2, 3, 4]->[24, 12, 8, 6]. - longest_consecutive(nums: Vec
) -> i32 Tìm độ dài của dãy số liên tiếp dài nhất (không cần đúng thứ tự trong mảng). Phải đạt độ phức tạp O(n). Ví dụ:[100, 4, 200, 1, 3, 2]-> Dãy[1, 2, 3, 4]có độ dài4.
Common Pitfalls & Debugging Tips
- Sử dụng sai Key cho HashMap: Nếu bạn dùng
Stringđã sắp xếp làm Key, độ phức tạp sẽ là O(n * k log k). Nếu dùng[u8; 26], nó là O(n * k). Với dữ liệu lớn, sự khác biệt này rất đáng kể. - Double Lookup: Tránh việc viết
if map.contains_key(&k) { map.get_mut(&k)... } else { map.insert... }. Hãy dùng Entry API (map.entry(k).or_insert(...)) để tiết kiệm thời gian tìm kiếm mã băm hai lần. - Quản lý bộ nhớ với Bucket Sort: Khi dùng Bucket Sort, hãy nhớ rằng mảng "xô" (buckets) cần có kích thước bằng
nums.len() + 1.
Q&A — Common Questions
Q1. Khi nào nên dùng HashMap và khi nào nên dùng BTreeMap?
- Dùng
HashMapkhi bạn chỉ cần truy cập nhanh (O(1)) và không quan tâm đến thứ tự của các Key. - Dùng
BTreeMapkhi bạn cần các Key luôn được sắp xếp (ví dụ: để in ra báo cáo theo thứ tự bảng chữ cái), đổi lại tốc độ truy cập sẽ chậm hơn (O(log n)).
Q2. Làm sao để xử lý Key có chứa ký tự ngoài bảng chữ cái tiếng Anh?
- Nếu đầu vào chứa ký tự Unicode, bạn không thể dùng
[u8; 26]. Lúc này, hãy dùng chính chuỗi đã được chuẩn hóa (ví dụ:s.chars().sorted().collect()) làm Key củaHashMap<String, Vec<String>>.