Самая известная задача на LeetCode. Классика использования HashMap.
<?php
declare(strict_types=1);
/**
* @param int[] $nums
* @return int[]
*/
function twoSum(array $nums, int $target): array
{
$seen = []; // value -> index
foreach ($nums as $i => $num) {
$complement = $target - $num;
if (isset($seen[$complement])) {
return [$seen[$complement], $i];
}
$seen[$num] = $i;
}
return [];
}
// nums = [2, 7, 11, 15], target = 9
// i=0: complement=9-2=7, 7 not in []. seen=[2=>0]
// i=1: complement=9-7=2, 2 in seen! -> [0, 1]
func twoSum(nums []int, target int) []int {
seen := map[int]int{} // value -> index
for i, num := range nums {
complement := target - num
if j, ok := seen[complement]; ok {
return []int{j, i}
}
seen[num] = i
}
return nil
}
// nums = [2, 7, 11, 15], target = 9
// i=0: complement=9-2=7, not found. seen={2:0}
// i=1: complement=9-7=2, found! -> [0, 1]
using System.Collections.Generic;
static int[] TwoSum(int[] nums, int target)
{
var seen = new Dictionary<int, int>(); // value -> index
for (int i = 0; i < nums.Length; i++)
{
int complement = target - nums[i];
if (seen.TryGetValue(complement, out int j))
{
return [j, i];
}
seen[nums[i]] = i;
}
return [];
}
// nums = [2, 7, 11, 15], target = 9
// i=0: complement=9-2=7, not found. seen={2:0}
// i=1: complement=9-7=2, found! -> [0, 1]
def two_sum(nums: list[int], target: int) -> list[int]:
seen: dict[int, int] = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
# nums = [2, 7, 11, 15], target = 9
# i=0: complement=9-2=7, not found. seen={2: 0}
# i=1: complement=9-7=2, found! -> [0, 1]
**Сложность:** O(n) времени и O(n) памяти vs O(n²) наивного решения.
using System.Collections.Generic;
using System.Linq;
static Dictionary<int, int> FrequencyCount(int[] arr)
{
var freq = new Dictionary<int, int>();
foreach (int x in arr)
{
freq[x] = freq.GetValueOrDefault(x) + 1;
}
return freq;
}
// Or using LINQ:
var freq = new[] { 1, 2, 2, 3, 3, 3 }
.GroupBy(x => x)
.ToDictionary(g => g.Key, g => g.Count());
// { 1: 1, 2: 2, 3: 3 }
from collections import Counter, defaultdict
def frequency_count(arr: list[int]) -> dict[int, int]:
freq: defaultdict[int, int] = defaultdict(int)
for x in arr:
freq[x] += 1
return dict(freq)
# Or using built-in:
freq = Counter([1, 2, 2, 3, 3, 3])
# Counter({3: 3, 2: 2, 1: 1})
### Задача: Top K Frequent Elements
<?php
declare(strict_types=1);
/**
* @param int[] $nums
* @return int[]
*/
function topKFrequent(array $nums, int $k): array
{
$count = array_count_values($nums);
// Bucket sort: index = frequency, value = list of numbers
$buckets = array_fill(0, count($nums) + 1, []);
foreach ($count as $num => $freq) {
$buckets[$freq][] = $num;
}
$result = [];
for ($i = count($buckets) - 1; $i > 0; $i--) {
foreach ($buckets[$i] as $num) {
$result[] = $num;
if (count($result) === $k) {
return $result;
}
}
}
return $result;
}
// nums = [1,1,1,2,2,3], k = 2
// count = [1=>3, 2=>2, 3=>1]
// buckets = [[], [3], [2], [1], [], [], []]
// Go from end: freq=3 -> 1, freq=2 -> 2
// Answer: [1, 2]
func topKFrequent(nums []int, k int) []int {
count := map[int]int{}
for _, num := range nums {
count[num]++
}
// Bucket sort: index = frequency, value = list of numbers
buckets := make([][]int, len(nums)+1)
for num, freq := range count {
buckets[freq] = append(buckets[freq], num)
}
result := []int{}
for i := len(buckets) - 1; i > 0; i-- {
for _, num := range buckets[i] {
result = append(result, num)
if len(result) == k {
return result
}
}
}
return result
}
// nums = [1,1,1,2,2,3], k = 2
// Answer: [1, 2]
using System.Collections.Generic;
static int[] TopKFrequent(int[] nums, int k)
{
var count = new Dictionary<int, int>();
foreach (int num in nums)
{
count[num] = count.GetValueOrDefault(num) + 1;
}
// Bucket sort: index = frequency, value = list of numbers
var buckets = new List<int>?[nums.Length + 1];
foreach (var (num, freq) in count)
{
(buckets[freq] ??= []).Add(num);
}
var result = new List<int>(k);
for (int i = buckets.Length - 1; i > 0; i--)
{
if (buckets[i] is not { } bucket)
{
continue;
}
foreach (int num in bucket)
{
result.Add(num);
if (result.Count == k)
{
return [.. result];
}
}
}
return [.. result];
}
// nums = [1,1,1,2,2,3], k = 2
// Answer: [1, 2]
from collections import Counter
def top_k_frequent(nums: list[int], k: int) -> list[int]:
count = Counter(nums)
# Bucket sort: index = frequency, value = list of numbers
buckets: list[list[int]] = [[] for _ in range(len(nums) + 1)]
for num, freq in count.items():
buckets[freq].append(num)
result: list[int] = []
for freq in range(len(buckets) - 1, 0, -1):
for num in buckets[freq]:
result.append(num)
if len(result) == k:
return result
return result
# nums = [1, 1, 1, 2, 2, 3], k = 2
# count = Counter({1: 3, 2: 2, 3: 1})
# buckets = [[], [3], [2], [1], [], [], []]
# Go from end: freq=3 -> 1, freq=2 -> 2
# Answer: [1, 2]
#
# Counter(nums).most_common(k) gives the same answer in one line,
# but costs O(n log k) instead of O(n).
import "sort"
func groupAnagrams(strs []string) [][]string {
groups := map[string][]string{}
for _, s := range strs {
// Key — sorted string (anagrams produce the same key)
chars := []byte(s)
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
key := string(chars)
groups[key] = append(groups[key], s)
}
result := make([][]string, 0, len(groups))
for _, group := range groups {
result = append(result, group)
}
return result
}
// ["eat", "tea", "tan", "ate", "nat", "bat"]
// Keys: "aet" -> ["eat","tea","ate"]
// "ant" -> ["tan","nat"]
// "abt" -> ["bat"]
using System;
using System.Collections.Generic;
static List<List<string>> GroupAnagrams(string[] strs)
{
var groups = new Dictionary<string, List<string>>();
foreach (string s in strs)
{
// Key — sorted string (anagrams produce the same key)
char[] chars = s.ToCharArray();
Array.Sort(chars);
string key = new(chars);
if (!groups.TryGetValue(key, out var group))
{
group = [];
groups[key] = group;
}
group.Add(s);
}
return [.. groups.Values];
}
// ["eat", "tea", "tan", "ate", "nat", "bat"]
// Keys: "aet" -> ["eat","tea","ate"]
// "ant" -> ["tan","nat"]
// "abt" -> ["bat"]
from collections import defaultdict
def group_anagrams(strs: list[str]) -> list[list[str]]:
groups: defaultdict[str, list[str]] = defaultdict(list)
for s in strs:
# Key — sorted string (anagrams produce the same key)
key = "".join(sorted(s))
groups[key].append(s)
return list(groups.values())
# ["eat", "tea", "tan", "ate", "nat", "bat"]
# Keys: "aet" -> ["eat","tea","ate"]
# "ant" -> ["tan","nat"]
# "abt" -> ["bat"]
Альтернативный ключ через подсчёт символов (более эффективно):
<?php
declare(strict_types=1);
/**
* @param string[] $strs
* @return string[][]
*/
function groupAnagramsV2(array $strs): array
{
$groups = [];
foreach ($strs as $s) {
// Key — count of each letter
$count = array_fill(0, 26, 0);
for ($i = 0; $i < strlen($s); $i++) {
$count[ord($s[$i]) - ord('a')]++;
}
$key = implode(',', $count);
$groups[$key][] = $s;
}
return array_values($groups);
}
func groupAnagramsV2(strs []string) [][]string {
groups := map[[26]int][]string{}
for _, s := range strs {
// Key — count of each letter (array is comparable in Go)
var count [26]int
for i := 0; i < len(s); i++ {
count[s[i]-'a']++
}
groups[count] = append(groups[count], s)
}
result := make([][]string, 0, len(groups))
for _, group := range groups {
result = append(result, group)
}
return result
}
using System.Collections.Generic;
static List<List<string>> GroupAnagramsV2(string[] strs)
{
var groups = new Dictionary<string, List<string>>();
foreach (string s in strs)
{
// Key — count of each letter. Arrays compare by reference in C#,
// so the counts are folded into a string key.
var count = new int[26];
foreach (char c in s)
{
count[c - 'a']++;
}
string key = string.Join(',', count);
if (!groups.TryGetValue(key, out var group))
{
group = [];
groups[key] = group;
}
group.Add(s);
}
return [.. groups.Values];
}
from collections import defaultdict
def group_anagrams_v2(strs: list[str]) -> list[list[str]]:
groups: defaultdict[tuple[int, ...], list[str]] = defaultdict(list)
for s in strs:
# Key — count of each letter (a tuple is hashable, a list is not)
count = [0] * 26
for ch in s:
count[ord(ch) - ord("a")] += 1
groups[tuple(count)].append(s)
return list(groups.values())
func subarraySumEqualsK(nums []int, k int) int {
count := 0
prefixSum := 0
prefixMap := map[int]int{0: 1}
for _, num := range nums {
prefixSum += num
if v, ok := prefixMap[prefixSum-k]; ok {
count += v
}
prefixMap[prefixSum]++
}
return count
}
using System.Collections.Generic;
static int SubarraySumEqualsK(int[] nums, int k)
{
int count = 0;
int prefixSum = 0;
var prefixMap = new Dictionary<int, int> { [0] = 1 };
foreach (int num in nums)
{
prefixSum += num;
if (prefixMap.TryGetValue(prefixSum - k, out int seen))
{
count += seen;
}
prefixMap[prefixSum] = prefixMap.GetValueOrDefault(prefixSum) + 1;
}
return count;
}
from collections import defaultdict
def subarray_sum_equals_k(nums: list[int], k: int) -> int:
count = 0
prefix_sum = 0
prefix_map: defaultdict[int, int] = defaultdict(int, {0: 1})
for num in nums:
prefix_sum += num
# .get() instead of [] so a missing key is not inserted
count += prefix_map.get(prefix_sum - k, 0)
prefix_map[prefix_sum] += 1
return count
## Паттерн 6: Longest Consecutive Sequence
<?php
declare(strict_types=1);
/**
* @param int[] $nums
*/
function longestConsecutive(array $nums): int
{
$numSet = array_flip($nums); // O(1) lookup via isset
$maxLength = 0;
foreach ($numSet as $num => $_) {
// Start only from the beginning of a sequence
if (!isset($numSet[$num - 1])) {
$current = $num;
$length = 1;
while (isset($numSet[$current + 1])) {
$current++;
$length++;
}
$maxLength = max($maxLength, $length);
}
}
return $maxLength;
}
// nums = [100, 4, 200, 1, 3, 2]
// Set: {100, 4, 200, 1, 3, 2}
// 1 has no (1-1)=0 -> start: 1,2,3,4 -> length 4
// 100 has no 99 -> start: 100 -> length 1
// 200 has no 199 -> start: 200 -> length 1
// Answer: 4
func longestConsecutive(nums []int) int {
numSet := map[int]bool{}
for _, num := range nums {
numSet[num] = true
}
maxLength := 0
for num := range numSet {
// Start only from the beginning of a sequence
if !numSet[num-1] {
current := num
length := 1
for numSet[current+1] {
current++
length++
}
if length > maxLength {
maxLength = length
}
}
}
return maxLength
}
// nums = [100, 4, 200, 1, 3, 2]
// Answer: 4
using System;
using System.Collections.Generic;
static int LongestConsecutive(int[] nums)
{
var numSet = new HashSet<int>(nums); // O(1) lookup
int maxLength = 0;
foreach (int num in numSet)
{
// Start only from the beginning of a sequence
if (numSet.Contains(num - 1))
{
continue;
}
int current = num;
int length = 1;
while (numSet.Contains(current + 1))
{
current++;
length++;
}
maxLength = Math.Max(maxLength, length);
}
return maxLength;
}
// nums = [100, 4, 200, 1, 3, 2]
// Answer: 4
def longest_consecutive(nums: list[int]) -> int:
num_set = set(nums) # O(1) lookup
max_length = 0
for num in num_set:
# Start only from the beginning of a sequence
if num - 1 in num_set:
continue
current = num
length = 1
while current + 1 in num_set:
current += 1
length += 1
max_length = max(max_length, length)
return max_length
# nums = [100, 4, 200, 1, 3, 2]
# Set: {100, 4, 200, 1, 3, 2}
# 1 has no 0 -> start: 1, 2, 3, 4 -> length 4
# 100 has no 99 -> start: 100 -> length 1
# 200 has no 199 -> start: 200 -> length 1
# Answer: 4
**Сложность:** O(n) — каждый элемент проверяется максимум два раза.
Паттерн 7: Кеширование результатов
<?php
declare(strict_types=1);
// Memoization with HashMap
function fibonacci(int $n, array &$memo = []): int
{
if (isset($memo[$n])) {
return $memo[$n];
}
if ($n <= 1) {
return $n;
}
$memo[$n] = fibonacci($n - 1, $memo) + fibonacci($n - 2, $memo);
return $memo[$n];
}
// fibonacci uses memoization with a map.
func fibonacci(n int, memo map[int]int) int {
if v, ok := memo[n]; ok {
return v
}
if n <= 1 {
return n
}
memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
return memo[n]
}
// Usage: fibonacci(10, map[int]int{})
using System.Collections.Generic;
// Memoization with a Dictionary
static long Fibonacci(int n, Dictionary<int, long> memo)
{
if (memo.TryGetValue(n, out long cached))
{
return cached;
}
if (n <= 1)
{
return n;
}
long value = Fibonacci(n - 1, memo) + Fibonacci(n - 2, memo);
memo[n] = value;
return value;
}
// Usage: Fibonacci(10, []);
from functools import cache
# Explicit memoization with a dict — same as the PHP/Go versions
def fibonacci(n: int, memo: dict[int, int] | None = None) -> int:
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
return memo[n]
# Idiomatic variant: @cache keeps the same dict for you
@cache
def fibonacci_cached(n: int) -> int:
if n <= 1:
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
## Сводная таблица паттернов
Паттерн
Задача-пример
Ключ HashMap
Сложность
Complement search
Two Sum
значение -> индекс
O(n)
Frequency count
Top K Frequent
элемент -> частота
O(n)
Group by
Group Anagrams
sorted key -> list
O(n*k)
Mapping
Isomorphic Strings
char -> char
O(n)
Prefix + HashMap
Subarray Sum = K
prefix_sum -> count
O(n)
Set membership
Longest Consecutive
Set lookup
O(n)
Memoization
Fibonacci
input -> result
varies
Запомни: HashMap превращает поиск из O(n) в O(1). Основные паттерны: 1) «Есть ли пара с суммой X?» — ищи complement. 2) «Сгруппировать по свойству» — используй свойство как ключ. 3) «Подсчитать подмассивы с суммой K» — prefix sum + HashMap. 4) «Найти последовательность» — Set для O(1) проверки.
Итоги
Two Sum паттерн: complement = target - current
Frequency Counter: подсчёт + bucket sort для Top K
Group By: свойство как ключ, элементы как значения
Prefix Sum + HashMap: подсчёт подмассивов за O(n)
Set для O(1) проверки принадлежности
Проверь себя
Что вернёт subarraySumEqualsK для `nums = [1, 1, 1], k = 2`?
В функции longestConsecutive проверяется `!isset($numSet[$num - 1])`. Зачем?
Строки `"paper"` и `"title"` изоморфны (isIsomorphic). Почему нужны ДВА маппинга (sToT и tToS)?
В задаче Group Anagrams какой ключ HashMap эффективнее — отсортированная строка или подсчёт символов?
В задаче Two Sum `nums = [3, 2, 4], target = 6`. Какой ответ и почему HashMap эффективнее наивного решения?