Các Khái Niệm Cốt Lõi và Ứng Dụng Của Hash Map
1. Bài Toán Trọng Tâm: Tìm kiếm giá trị tương ứng (value) một cách hiệu quả thông qua một khóa duy nhất (key).
2. Tình Huống Thường Gặp:
- Tìm hai số có tổng bằng một giá trị cho trước.
- Xác định phần tử lớn hơn tiếp theo trong một dãy.
- Tìm vị trí xuất hiện đầu tiên hoặc cuối cùng của một ký tự.
- Quản lý bộ nhớ đệm (cache), triển khai LRU (Least Recently Used), hoặc các hệ thống lưu trữ khóa-giá trị.
3. Cấu Trúc Dữ Liệu Phù Hợp: Sử dụng std::unordered_map<KeyType, ValueType> để xây dựng ánh xạ giữa khóa và giá trị.
KeyType: Kiểu dữ liệu của khóa dùng để tra cứu (ví dụ: số, ký tự).ValueType: Thông tin được ánh xạ (ví dụ: chỉ số, giá trị gốc, kết quả tính toán).
4. Mã Giả:
std::unordered_map<Key, Value> map;
for (int i = 0; i < data.size(); ++i) {
map[data[i]] = some_corresponding_value;
}
Các Bài Toán Minh Họa Với Hash Map
Bài Toán 1: Two Sum (Tổng Hai Số) - LeetCode 1
Ý Tưởng Giải Quyết: Sử dụng std::unordered_map<int, int> để tạo ánh xạ: giá trị → chỉ số.
Quy trình:
- Duyệt qua mảng.
- Với mỗi phần tử
nums[i], tính giá trị bù:target - nums[i]. - Kiểm tra xem giá trị bù này đã xuất hiện trong map chưa.
- Nếu có: trả về
{map[complement], i}. - Nếu chưa: thêm
nums[i]vào map với ánh xạnums[i] → i.
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> valueToIndex; // key: số, value: chỉ số
for (int i = 0; i < nums.size(); ++i) {
int complement = target - nums[i];
if (valueToIndex.count(complement)) {
return {valueToIndex[complement], i};
}
valueToIndex[nums[i]] = i;
}
return {};
}
Lưu ý:
- Việc sử dụng dấu ngoặc nhọn
{}để khởi tạostd::vectorcho phép chuyển đổi ngầm định khi trả về từ hàm. Ví dụ:return {1, 2};tương đươngreturn std::vector{1, 2};. - Thao tác
map[key] = valuesẽ lưu cặp khóa-giá trị(key, value)vào hash table, phục vụ cho việc truy vấn nhanh sau này.
Bài Toán 2: Next Greater Element I (Phần Tử Lớn Hơn Tiếp Theo I) - LeetCode 496
Ý Tưởng Giải Quyết: Kết hợp cấu trúc dữ liệu stack đơn điệu (monotonous stack) và hash map.
Quy trình:
- Tiền xử lý
nums2: - Sử dụng một stack đơn điệu giảm dần để lưu trữ các phần tử chưa tìm được phần tử lớn hơn tiếp theo.
- Khi gặp một phần tử mới lớn hơn phần tử trên cùng của stack, ta sẽ bật phần tử trên cùng ra khỏi stack và ghi nhận ánh xạ: phần tử bật ra → phần tử hiện tại vào hash map.
- Push phần tử hiện tại vào stack.
- Truy vấn với
nums1: Duyệt quanums1và tra cứu kết quả trong hash map đã xây dựng.
Stack Đơn Điệu Giảm Dần: Đảm bảo các phần tử trong stack luôn giảm dần từ đáy lên đỉnh, nghĩa là phần tử trên cùng luôn là phần tử nhỏ nhất.
std::vector<int> nextGreaterElement(std::vector<int>& nums1, std::vector<int>& nums2) {
std::unordered_map<int, int> nextGreaterMap; // key: phần tử, value: phần tử lớn hơn kế tiếp
std::stack<int> monoStack; // Stack đơn điệu giảm dần (đỉnh nhỏ nhất)
// Bước 1: Xây dựng bản đồ ánh xạ từ nums2
for (int num : nums2) {
while (!monoStack.empty() && num > monoStack.top()) {
nextGreaterMap[monoStack.top()] = num;
monoStack.pop();
}
monoStack.push(num);
}
// Bước 2: Truy vấn với nums1
std::vector<int> result;
for (int num : nums1) {
if (nextGreaterMap.count(num)) {
result.push_back(nextGreaterMap[num]);
} else {
result.push_back(-1); // Không tìm thấy phần tử lớn hơn
}
}
return result;
}
Ví dụ minh họa với nums2 = {2, 1, 2, 4, 3}:
| Bước | Phần tử hiện tại | Trạng thái Stack (Đáy → Đỉnh) | Ánh xạ Hash Map | Ghi chú |
|---|---|---|---|---|
| 1 | 2 | [2] | {} | Stack rỗng, push 2. |
| 2 | 1 | [2, 1] | {} | 1 < 2, giữ tính giảm dần, push 1. |
| 3 | 2 | [2] → [2, 2] | {1 → 2} | 2 > 1, pop 1, ghi nhận 1 → 2; 2 ≤ 2, push 2. |
| 4 | 4 | [2, 2] → [] → [4] | {1 → 2, 2 → 4} | 4 > 2, pop 2, ghi 2 → 4; lặp lại với 2 còn lại, ghi 2 → 4; push 4. |
| 5 | 3 | [4, 3] | {1 → 2, 2 → 4} | 3 < 4, push 3. |
| Kết thúc | - | [4, 3] | {1 → 2, 2 → 4, 3 → -1, 4 → -1} | Các phần tử còn lại trong stack (3, 4) không có phần tử lớn hơn, ánh xạ thành -1. |
Bài Toán 3: Isomorphic Strings (Chuỗi Đồng Cấu) - LeetCode 205
Ý Tưởng Giải Quyết: Sử dụng hai hash map để duy trì quan hệ ánh xạ hai chiều: s → t và t → s.
Quy trình:
- Duyệt qua cả hai chuỗi cùng lúc.
- Sử dụng
mapSTđể lưu ánh xạs[i] → t[i]. - Sử dụng
mapTSđể lưu ánh xạ ngượct[i] → s[i]. - Với mỗi cặp ký tự
s[i], t[i]:- Nếu
s[i]đã được ánh xạ nhưng không phải tớit[i], trả vềfalse. - Nếu
t[i]đã được ánh xạ ngược nhưng không phải tớis[i], trả vềfalse. - Nếu không có xung đột, thiết lập ánh xạ mới.
- Nếu
- Nếu hoàn thành vòng lặp mà không có xung đột, trả về
true.
bool isIsomorphic(std::string s, std::string t) {
std::unordered_map<char, char> mapST;
std::unordered_map<char, char> mapTS;
for (int i = 0; i < s.length(); ++i) {
char charS = s[i], charT = t[i];
if (mapST.count(charS) && mapST[charS] != charT) return false;
if (mapTS.count(charT) && mapTS[charT] != charS) return false;
mapST[charS] = charT;
mapTS[charT] = charS;
}
return true;
}
Các Kỹ Thuật Tiền Tố (Prefix) Kết Hợp Hash Map
1. Bài Toán Trọng Tâm: Ghi lại hoặc kiểm tra sự xuất hiện của một "trạng thái tiền tố" (prefix state) và đếm số lần xuất hiện của nó.
2. Tình Huống Thường Gặp:
- Tổng tiền tố (prefix sum) + hiệu số (ví dụ: bài toán tổng mảng con bằng K).
- Độ dài lớn nhất của mảng con có số lượng 0 và 1 bằng nhau.
- Tổng mảng con chia hết cho K.
- Trạng thái tần suất ký tự tiền tố (sử dụng bitmask hoặc mảng).
3. Cấu Trúc Dữ Liệu Phù Hợp: Sử dụng std::unordered_map<State, Count> hoặc std::unordered_map<State, FirstIndex>.
State: Trạng thái tiền tố.Count: Số lần xuất hiện của trạng thái đó.FirstIndex: Chỉ số xuất hiện đầu tiên của trạng thái đó.
4. Mã Giả:
std::unordered_map<int, int> prefixStateCount;
prefixStateCount[initial_state] = 1; // Thường là 1
int currentState = 0;
int result = 0;
for (int num : numbers) {
currentState = update_state(currentState, num);
// Truy vấn trạng thái trước đó
if (prefixStateCount.count(currentState)) {
result += prefixStateCount[currentState];
}
// Cập nhật số lần xuất hiện của trạng thái hiện tại
prefixStateCount[currentState]++;
}
return result;
Các Bài Toán Minh Họa Với Kỹ Thuật Tiền Tố
Bài Toán 4: Subarray Sum Equals K (Tổng Mảng Con Bằng K) - LeetCode 560
Ý Tưởng Giải Quyết: Sử dụng tổng tiền tố và hash map để đếm số lần xuất hiện của các tổng tiền tố.
Quy trình:
- Sử dụng
std::unordered_map<int, int> prefixSumCountđể lưu trữ tần suất xuất hiện của các tổng tiền tố. - Khởi tạo
prefixSumCount[0] = 1để xử lý trường hợp mảng con bắt đầu từ đầu mảng. - Duyệt qua mảng
nums, cập nhật tổng tiền tố hiện tạicurrentSum. - Với mỗi
currentSum, kiểm tra xemcurrentSum - kđã tồn tại trongprefixSumCountchưa. Nếu có, cộng giá trị tương ứng vào kết quảresult(vì điều này cho thấy có một mảng con có tổng bằngk). - Cập nhật tần suất của
currentSumvàoprefixSumCount. - Trả về
result.
int subarraySum(std::vector<int>& nums, int k) {
std::unordered_map<int, int> prefixSumCount;
prefixSumCount[0] = 1; // Khởi tạo cho trường hợp mảng con bắt đầu từ đầu
int currentSum = 0, result = 0;
for (int num : nums) {
currentSum += num;
// Kiểm tra xem có tổng tiền tố nào mà hiệu với currentSum bằng k không
if (prefixSumCount.count(currentSum - k)) {
result += prefixSumCount[currentSum - k];
}
// Cập nhật tần suất của tổng tiền tố hiện tại
prefixSumCount[currentSum]++;
}
return result;
}
Giải thích prefixSumCount[0] = 1:
Giả sử mảng bắt đầu bằng một số bằng k (ví dụ: nums = {2}, k = 2). Khi duyệt đến phần tử đầu tiên, currentSum = 2. Giá trị cần tìm là currentSum - k = 0. Nếu không khởi tạo prefixSumCount[0] = 1, chúng ta sẽ bỏ sót trường hợp này.
Giải thích result += prefixSumCount[currentSum - k]:
Nếu tại vị trí i, tổng tiền tố là sum_i, và tại một vị trí trước đó j, tổng tiền tố là sum_j, sao cho sum_i - sum_j = k (hay sum_j = sum_i - k), thì mảng con từ j+1 đến i có tổng bằng k. prefixSumCount[currentSum - k] cho biết có bao nhiêu vị trí j trước đó thỏa mãn điều kiện này, mỗi vị trí tương ứng với một mảng con hợp lệ.
Bài Toán 5: Subarrays Divisible by K (Mảng Con Chia Hết Cho K) - LeetCode 974
Ý Tưởng Giải Quyết: Sử dụng tổng tiền tố và phép toán modulo kết hợp với hash map để đếm tần suất các số dư.
Nguyên tắc cốt lõi: Nếu hai tổng tiền tố sum1 và sum2 có cùng số dư khi chia cho k (tức là sum1 % k == sum2 % k), thì hiệu của chúng (là tổng của một mảng con nào đó) sẽ chia hết cho k.
Quy trình:
- Sử dụng
std::unordered_map<int, int> modCountđể lưu tần suất xuất hiện của các số dư. - Khởi tạo
modCount[0] = 1để xử lý trường hợp mảng con bắt đầu từ đầu có tổng chia hết chok. - Duyệt qua mảng
nums, cập nhật tổng tiền tốcurrentSum. - Tính số dư
mod = (currentSum % k + k) % k(đảm bảo số dư luôn dương). - Nếu
modđã tồn tại trongmodCount, nghĩa là có các mảng con có tổng chia hết chok, cộng giá trị tương ứng vào kết quảresult. - Cập nhật tần suất của
modvàomodCount. - Trả về
result.
int subarraysDivByK(std::vector<int>& nums, int k) {
std::unordered_map<int, int> modCount;
modCount[0] = 1; // Khởi tạo số dư 0 xuất hiện 1 lần
int currentSum = 0, result = 0;
for (int num : nums) {
currentSum += num;
// Đảm bảo số dư luôn dương trong khoảng [0, k-1]
int mod = (currentSum % k + k) % k;
if (modCount.count(mod)) {
result += modCount[mod];
}
modCount[mod]++;
}
return result;
}
Ví dụ với nums = {4, 5, 0, -2, -3, 1}, k = 5:
| Phần tử | currentSum (Tổng tiền tố) |
mod = (currentSum % 5 + 5) % 5 |
Cập nhật modCount (Ban đầu {0:1}) |
Kết quả (result) |
|---|---|---|---|---|
| 4 | 4 | 4 % 5 = 4 | {0:1, 4:1} | 0 |
| 5 | 9 | 9 % 5 = 4 | Phát hiện 4 (tần suất 1) -> result += 1. Cập nhật: {0:1, 4:2} |
1 |
| 0 | 9 | 9 % 5 = 4 | Phát hiện 4 (tần suất 2) -> result += 2. Cập nhật: {0:1, 4:3} |
3 |
| -2 | 7 | 7 % 5 = 2 | Không phát hiện 2. Cập nhật: {0:1, 2:1, 4:3} | 3 |
| -3 | 4 | 4 % 5 = 4 | Phát hiện 4 (tần suất 3) -> result += 3. Cập nhật: {0:1, 2:1, 4:4} |
6 |
| 1 | 5 | 5 % 5 = 0 | Phát hiện 0 (tần suất 1) -> result += 1. Cập nhật: {0:2, 2:1, 4:4} |
7 |
Bài Toán 6: Contiguous Array (Mảng Liên Tục) - LeetCode 525
Ý Tưởng Giải Quyết: Chuyển đổi 0 thành -1, sau đó sử dụng tổng tiền tố và hash map lưu trữ chỉ số xuất hiện đầu tiên của mỗi tổng tiền tố.
Nguyên tắc cốt lõi: Nếu coi 0 là -1 và 1 là +1, thì mảng con có số lượng 0 và 1 bằng nhau sẽ có tổng bằng 0.
Quy trình:
- Chuyển đổi các phần tử 0 trong mảng thành -1.
- Sử dụng
std::unordered_map<int, int> firstIndexđể lưu trữ chỉ số xuất hiện đầu tiên của mỗi tổng tiền tố. - Khởi tạo
firstIndex[0] = -1để xử lý trường hợp mảng con hợp lệ bắt đầu từ chỉ số 0. - Duyệt qua mảng, cập nhật tổng tiền tố hiện tại
currentSum. - Nếu
currentSumđã tồn tại trongfirstIndex, nghĩa là chúng ta tìm thấy một mảng con có tổng bằng 0 (số lượng 0 và 1 bằng nhau). Cập nhật độ dài lớn nhất:maxLen = max(maxLen, currentIndex - firstIndex[currentSum]). - Nếu
currentSumchưa tồn tại, thêm nó vàofirstIndexvới chỉ số hiện tại. - Trả về
maxLen.
int findMaxLength(std::vector<int>& nums) {
std::unordered_map<int, int> firstIndex;
firstIndex[0] = -1; // Khởi tạo cho trường hợp mảng con bắt đầu từ index 0
int currentSum = 0, maxLen = 0;
for (int i = 0; i < nums.size(); ++i) {
// Chuyển đổi 0 thành -1, 1 giữ nguyên
currentSum += (nums[i] == 1) ? 1 : -1;
if (firstIndex.count(currentSum)) {
// Nếu đã gặp tổng tiền tố này trước đó, tính độ dài mảng con
maxLen = std::max(maxLen, i - firstIndex[currentSum]);
} else {
// Lần đầu tiên gặp tổng tiền tố này, lưu chỉ số
firstIndex[currentSum] = i;
}
}
return maxLen;
}
Giải thích firstIndex[0] = -1:
Điều này cho phép chúng ta tính đúng độ dài của các mảng con hợp lệ bắt đầu từ chỉ số 0. Ví dụ, nếu mảng là {0, 1} (tương ứng {-1, 1}), tổng tiền tố tại chỉ số 1 là 0. Nếu không có firstIndex[0] = -1, chúng ta sẽ không tính được độ dài là 2.