Problems

Master hash tables by solving these carefully selected problems. Each problem demonstrates different hash table patterns and builds your data structure expertise progressively.

Ready to apply the hash table patterns you’ve learned? This section provides a structured progression from basic to advanced problems. Each problem includes a detailed explanation, visual representation, and complete multi-language solutions.

For reference, check out our code templates for reusable implementations across different languages.

Problem List

Easy Problems

1. Contains Duplicate

Brief: Determine if an array contains any duplicate values.

Why this pattern?: This is the classic “existence checking” problem where we need to track seen elements efficiently.

Key Insight: Use a Set to track elements we’ve already encountered. The first duplicate found means we can return true immediately.

Visual Flow:

  flowchart TD
    A[Start with empty Set] --> B[Process each element]
    B --> C{Element in Set?}
    C -->|Yes| D[Return true - duplicate found]
    C -->|No| E[Add to Set]
    E --> F{More elements?}
    F -->|Yes| B
    F -->|No| G[Return false - no duplicates]
function containsDuplicate(nums) {
    const seen = new Set();

    for (const num of nums) {
        if (seen.has(num)) {
            return true;
        }
        seen.add(num);
    }

    return false;
}
def containsDuplicate(nums):
    seen = set()

    for num in nums:
        if num in seen:
            return True
        seen.add(num)

    return False
public boolean containsDuplicate(int[] nums) {
    Set<Integer> seen = new HashSet<>();

    for (int num : nums) {
        if (seen.contains(num)) {
            return true;
        }
        seen.add(num);
    }

    return false;
}
bool containsDuplicate(vector<int>& nums) {
    unordered_set<int> seen;

    for (int num : nums) {
        if (seen.find(num) != seen.end()) {
            return true;
        }
        seen.insert(num);
    }

    return false;
}
func containsDuplicate(nums []int) bool {
    seen := make(map[int]bool)

    for _, num := range nums {
        if seen[num] {
            return true
        }
        seen[num] = true
    }

    return false
}
def contains_duplicate(nums)
    seen = Set.new

    nums.each do |num|
        return true if seen.include?(num)
        seen.add(num)
    end

    false
end

2. Two Sum

Brief: Find indices of two numbers that add up to a specific target.

Why this pattern?: This introduces the “complement search” pattern where we look for pairs that satisfy a condition.

Key Insight: For each number, calculate what value would complete the pair (complement), then check if we’ve seen it before.

Visual Flow:

  flowchart TD
    A[Initialize empty Map] --> B[Process each number]
    B --> C[Calculate complement]
    C --> D{Complement in Map?}
    D -->|Yes| E[Return indices]
    D -->|No| F[Store current number with index]
    F --> G{More numbers?}
    G -->|Yes| B
    G -->|No| H[No solution found]
function twoSum(nums, target) {
    const map = new Map();

    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];

        if (map.has(complement)) {
            return [map.get(complement), i];
        }

        map.set(nums[i], i);
    }

    return [];
}
def twoSum(nums, target):
    seen = {}

    for i, num in enumerate(nums):
        complement = target - num

        if complement in seen:
            return [seen[complement], i]

        seen[num] = i

    return []
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();

    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];

        if (map.containsKey(complement)) {
            return new int[]{map.get(complement), i};
        }

        map.put(nums[i], i);
    }

    return new int[]{};
}
vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int, int> map;

    for (int i = 0; i < nums.size(); i++) {
        int complement = target - nums[i];

        if (map.find(complement) != map.end()) {
            return {map[complement], i};
        }

        map[nums[i]] = i;
    }

    return {};
}
func twoSum(nums []int, target int) []int {
    seen := make(map[int]int)

    for i, num := range nums {
        complement := target - num

        if idx, exists := seen[complement]; exists {
            return []int{idx, i}
        }

        seen[num] = i
    }

    return []int{}
}
def two_sum(nums, target)
    seen = {}

    nums.each_with_index do |num, i|
        complement = target - num

        return [seen[complement], i] if seen.key?(complement)

        seen[num] = i
    end

    []
end

3. Valid Anagram

Brief: Determine if one string is an anagram of another.

Why this pattern?: This demonstrates frequency counting where we need to track character occurrences.

Key Insight: Count character frequencies in both strings and compare. If they match exactly, the strings are anagrams.

Visual Flow:

  flowchart TD
    A[Count chars in s] --> B[Decrement counts for t]
    B --> C{Any negative counts?}
    C -->|Yes| D[Not anagrams]
    C -->|No| E{All counts zero?}
    E -->|Yes| F[Valid anagrams]
    E -->|No| D
function isAnagram(s, t) {
    if (s.length !== t.length) return false;

    const count = new Map();

    for (const char of s) {
        count.set(char, (count.get(char) || 0) + 1);
    }

    for (const char of t) {
        if (!count.has(char)) return false;
        count.set(char, count.get(char) - 1);
        if (count.get(char) < 0) return false;
    }

    return true;
}
def isAnagram(s, t):
    if len(s) != len(t):
        return False

    count = {}

    for char in s:
        count[char] = count.get(char, 0) + 1

    for char in t:
        if char not in count:
            return False
        count[char] -= 1
        if count[char] < 0:
            return False

    return True
public boolean isAnagram(String s, String t) {
    if (s.length() != t.length()) return false;

    Map<Character, Integer> count = new HashMap<>();

    for (char c : s.toCharArray()) {
        count.put(c, count.getOrDefault(c, 0) + 1);
    }

    for (char c : t.toCharArray()) {
        if (!count.containsKey(c)) return false;
        count.put(c, count.get(c) - 1);
        if (count.get(c) < 0) return false;
    }

    return true;
}
bool isAnagram(string s, string t) {
    if (s.length() != t.length()) return false;

    unordered_map<char, int> count;

    for (char c : s) {
        count[c]++;
    }

    for (char c : t) {
        if (count.find(c) == count.end()) return false;
        count[c]--;
        if (count[c] < 0) return false;
    }

    return true;
}
func isAnagram(s string, t string) bool {
    if len(s) != len(t) {
        return false
    }

    count := make(map[rune]int)

    for _, char := range s {
        count[char]++
    }

    for _, char := range t {
        if count[char] == 0 {
            return false
        }
        count[char]--
    }

    return true
}
def is_anagram(s, t)
    return false if s.length != t.length

    count = Hash.new(0)

    s.each_char { |char| count[char] += 1 }
    t.each_char { |char| count[char] -= 1 }

    count.values.all? { |c| c == 0 }
end

Medium Problems

4. Group Anagrams

Brief: Group anagrams from an array of strings together.

Why this pattern?: This showcases the “grouping by property” pattern where we categorize elements based on shared characteristics.

Key Insight: Sort each string to create a canonical form (key), then group strings with the same key together.

Visual Flow:

  flowchart TD
    A[Process each string] --> B[Sort characters to create key]
    B --> C{Key exists in Map?}
    C -->|No| D[Create new group]
    C -->|Yes| E[Add to existing group]
    D --> F[Store in Map]
    E --> F
    F --> G{More strings?}
    G -->|Yes| A
    G -->|No| H[Return all groups]
function groupAnagrams(strs) {
    const groups = new Map();

    for (const str of strs) {
        const key = str.split('').sort().join('');

        if (!groups.has(key)) {
            groups.set(key, []);
        }

        groups.get(key).push(str);
    }

    return Array.from(groups.values());
}
def groupAnagrams(strs):
    groups = {}

    for s in strs:
        key = ''.join(sorted(s))
        if key not in groups:
            groups[key] = []
        groups[key].append(s)

    return list(groups.values())
public List<List<String>> groupAnagrams(String[] strs) {
    Map<String, List<String>> groups = new HashMap<>();

    for (String str : strs) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        String key = new String(chars);

        groups.computeIfAbsent(key, k -> new ArrayList<>()).add(str);
    }

    return new ArrayList<>(groups.values());
}
vector<vector<string>> groupAnagrams(vector<string>& strs) {
    unordered_map<string, vector<string>> groups;

    for (const string& str : strs) {
        string key = str;
        sort(key.begin(), key.end());
        groups[key].push_back(str);
    }

    vector<vector<string>> result;
    for (auto& pair : groups) {
        result.push_back(pair.second);
    }

    return result;
}
func groupAnagrams(strs []string) [][]string {
    groups := make(map[string][]string)

    for _, str := range strs {
        key := sortString(str)
        groups[key] = append(groups[key], str)
    }

    result := [][]string{}
    for _, group := range groups {
        result = append(result, group)
    }

    return result
}

func sortString(s string) string {
    chars := []rune(s)
    sort.Slice(chars, func(i, j int) bool {
        return chars[i] < chars[j]
    })
    return string(chars)
}
def group_anagrams(strs)
    groups = Hash.new { |h, k| h[k] = [] }

    strs.each do |str|
        key = str.chars.sort.join
        groups[key] << str
    end

    groups.values
end

5. Intersection of Two Arrays II

Brief: Find intersection of two arrays, including duplicates.

Why this pattern?: This combines frequency counting with intersection logic, showing how to handle multiple occurrences.

Key Insight: Count frequencies in one array, then iterate through the second array to find common elements while respecting occurrence counts.

Visual Flow:

  flowchart TD
    A[Count frequencies in nums1] --> B[Process nums2 elements]
    B --> C{Element in frequency map?}
    C -->|Yes and count > 0| D[Add to result, decrement count]
    C -->|No or count = 0| E[Skip element]
    D --> F{More elements?}
    E --> F
    F -->|Yes| B
    F -->|No| G[Return result]
function intersect(nums1, nums2) {
    const freq = new Map();
    const result = [];

    for (const num of nums1) {
        freq.set(num, (freq.get(num) || 0) + 1);
    }

    for (const num of nums2) {
        if (freq.has(num) && freq.get(num) > 0) {
            result.push(num);
            freq.set(num, freq.get(num) - 1);
        }
    }

    return result;
}
def intersect(nums1, nums2):
    freq = {}
    result = []

    for num in nums1:
        freq[num] = freq.get(num, 0) + 1

    for num in nums2:
        if num in freq and freq[num] > 0:
            result.append(num)
            freq[num] -= 1

    return result
public int[] intersect(int[] nums1, int[] nums2) {
    Map<Integer, Integer> freq = new HashMap<>();
    List<Integer> result = new ArrayList<>();

    for (int num : nums1) {
        freq.put(num, freq.getOrDefault(num, 0) + 1);
    }

    for (int num : nums2) {
        if (freq.containsKey(num) && freq.get(num) > 0) {
            result.add(num);
            freq.put(num, freq.get(num) - 1);
        }
    }

    return result.stream().mapToInt(Integer::intValue).toArray();
}
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
    unordered_map<int, int> freq;
    vector<int> result;

    for (int num : nums1) {
        freq[num]++;
    }

    for (int num : nums2) {
        if (freq[num] > 0) {
            result.push_back(num);
            freq[num]--;
        }
    }

    return result;
}
func intersect(nums1 []int, nums2 []int) []int {
    freq := make(map[int]int)
    result := []int{}

    for _, num := range nums1 {
        freq[num]++
    }

    for _, num := range nums2 {
        if freq[num] > 0 {
            result = append(result, num)
            freq[num]--
        }
    }

    return result
}
def intersect(nums1, nums2)
    freq = Hash.new(0)
    result = []

    nums1.each { |num| freq[num] += 1 }

    nums2.each do |num|
        if freq[num] > 0
            result << num
            freq[num] -= 1
        end
    end

    result
end

6. Top K Frequent Elements

Brief: Return the k most frequent elements from an array.

Why this pattern?: This demonstrates advanced frequency counting combined with bucket sort for optimal O(n) time complexity.

Key Insight: Use bucket sort where each bucket represents a frequency count, allowing us to extract top k elements efficiently.

Visual Flow:

  flowchart TD
    A[Count frequencies] --> B[Create buckets by frequency]
    B --> C[Fill buckets with elements]
    C --> D[Extract from highest frequency]
    D --> E{Got k elements?}
    E -->|No| D
    E -->|Yes| F[Return result]
function topKFrequent(nums, k) {
    const freq = new Map();
    const buckets = Array.from({length: nums.length + 1}, () => []);

    for (const num of nums) {
        freq.set(num, (freq.get(num) || 0) + 1);
    }

    for (const [num, count] of freq) {
        buckets[count].push(num);
    }

    const result = [];

    for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) {
        if (buckets[i].length > 0) {
            const remaining = k - result.length;
            result.push(...buckets[i].slice(0, remaining));
        }
    }

    return result;
}
def topKFrequent(nums, k):
    freq = {}
    buckets = [[] for _ in range(len(nums) + 1)]

    for num in nums:
        freq[num] = freq.get(num, 0) + 1

    for num, count in freq.items():
        buckets[count].append(num)

    result = []

    for i in range(len(buckets) - 1, -1, -1):
        if buckets[i]:
            result.extend(buckets[i])
            if len(result) >= k:
                return result[:k]

    return result
public int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    List<List<Integer>> buckets = new ArrayList<>();

    for (int i = 0; i <= nums.length; i++) {
        buckets.add(new ArrayList<>());
    }

    for (int num : nums) {
        freq.put(num, freq.getOrDefault(num, 0) + 1);
    }

    for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
        buckets.get(entry.getValue()).add(entry.getKey());
    }

    int[] result = new int[k];
    int index = 0;

    for (int i = buckets.size() - 1; i >= 0 && index < k; i--) {
        for (int num : buckets.get(i)) {
            result[index++] = num;
            if (index == k) return result;
        }
    }

    return result;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int, int> freq;
    vector<vector<int>> buckets(nums.size() + 1);

    for (int num : nums) {
        freq[num]++;
    }

    for (auto& pair : freq) {
        buckets[pair.second].push_back(pair.first);
    }

    vector<int> result;

    for (int i = buckets.size() - 1; i >= 0 && result.size() < k; i--) {
        for (int num : buckets[i]) {
            result.push_back(num);
            if (result.size() == k) return result;
        }
    }

    return result;
}
func topKFrequent(nums []int, k int) []int {
    freq := make(map[int]int)
    buckets := make([][]int, len(nums)+1)

    for _, num := range nums {
        freq[num]++
    }

    for num, count := range freq {
        buckets[count] = append(buckets[count], num)
    }

    result := []int{}

    for i := len(buckets) - 1; i >= 0 && len(result) < k; i-- {
        for _, num := range buckets[i] {
            result = append(result, num)
            if len(result) == k {
                return result
            }
        }
    }

    return result
}
def top_k_frequent(nums, k)
    freq = Hash.new(0)
    buckets = Array.new(nums.length + 1) { [] }

    nums.each { |num| freq[num] += 1 }
    freq.each { |num, count| buckets[count] << num }

    result = []

    (buckets.length - 1).downto(0) do |i|
        buckets[i].each do |num|
            result << num
            return result if result.length == k
        end
    end

    result
end

Hard Problems

7. LRU Cache

Brief: Design a cache with O(1) get and put operations that evicts least recently used items.

Why this pattern?: This combines hash tables with linked lists to create a sophisticated data structure with specific ordering requirements.

Key Insight: Use a hash map for O(1) access and a doubly linked list for O(1) ordering operations. The head is most recent, tail is least recent.

Visual Flow:

  flowchart TD
    A[Get Operation] --> B[Check Hash Map]
    B --> C{Key exists?}
    C -->|No| D[Return -1]
    C -->|Yes| E[Move to head of list]
    E --> F[Return value]

    G[Put Operation] --> H{Key exists?}
    H -->|Yes| I[Update value, move to head]
    H -->|No| J{At capacity?}
    J -->|Yes| K[Remove tail node]
    J -->|No| L[Create new node]
    L --> M[Add to head, update map]
    K --> M
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map();
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    get(key) {
        if (!this.cache.has(key)) return -1;

        const node = this.cache.get(key);
        this._remove(node);
        this._addToHead(node);
        return node.value;
    }

    put(key, value) {
        if (this.cache.has(key)) {
            const node = this.cache.get(key);
            node.value = value;
            this._remove(node);
            this._addToHead(node);
        } else {
            if (this.cache.size >= this.capacity) {
                const tail = this._removeTail();
                this.cache.delete(tail.key);
            }
            const node = new Node(key, value);
            this._addToHead(node);
            this.cache.set(key, node);
        }
    }

    _remove(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    _addToHead(node) {
        node.prev = this.head;
        node.next = this.head.next;
        this.head.next.prev = node;
        this.head.next = node;
    }

    _removeTail() {
        const node = this.tail.prev;
        this._remove(node);
        return node;
    }
}

class Node {
    constructor(key, value) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}
class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = {}
        self.head = Node(0, 0)
        self.tail = Node(0, 0)
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_to_head(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.cache:
            return -1

        node = self.cache[key]
        self._remove(node)
        self._add_to_head(node)
        return node.value

    def put(self, key, value):
        if key in self.cache:
            node = self.cache[key]
            node.value = value
            self._remove(node)
            self._add_to_head(node)
        else:
            if len(self.cache) >= self.capacity:
                tail = self.tail.prev
                self._remove(tail)
                del self.cache[tail.key]

            node = Node(key, value)
            self._add_to_head(node)
            self.cache[key] = node
class Node {
    int key, value;
    Node prev, next;

    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}

class LRUCache {
    private int capacity;
    private Map<Integer, Node> cache;
    private Node head, tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    public int get(int key) {
        if (!cache.containsKey(key)) return -1;

        Node node = cache.get(key);
        remove(node);
        addToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (cache.containsKey(key)) {
            Node node = cache.get(key);
            node.value = value;
            remove(node);
            addToHead(node);
        } else {
            if (cache.size() >= capacity) {
                Node tail = removeTail();
                cache.remove(tail.key);
            }
            Node node = new Node(key, value);
            addToHead(node);
            cache.put(key, node);
        }
    }

    private void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void addToHead(Node node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private Node removeTail() {
        Node node = tail.prev;
        remove(node);
        return node;
    }
}
class Node {
public:
    int key, value;
    Node* prev;
    Node* next;

    Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    int capacity;
    unordered_map<int, Node*> cache;
    Node* head;
    Node* tail;

    void remove(Node* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void addToHead(Node* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }

    Node* removeTail() {
        Node* node = tail->prev;
        remove(node);
        return node;
    }

public:
    LRUCache(int capacity) {
        this->capacity = capacity;
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head->next = tail;
        tail->prev = head;
    }

    int get(int key) {
        if (cache.find(key) == cache.end()) return -1;

        Node* node = cache[key];
        remove(node);
        addToHead(node);
        return node->value;
    }

    void put(int key, int value) {
        if (cache.find(key) != cache.end()) {
            Node* node = cache[key];
            node->value = value;
            remove(node);
            addToHead(node);
        } else {
            if (cache.size() >= capacity) {
                Node* tail = removeTail();
                cache.erase(tail->key);
                delete tail;
            }
            Node* node = new Node(key, value);
            addToHead(node);
            cache[key] = node;
        }
    }
};
type Node struct {
    key, value int
    prev, next *Node
}

type LRUCache struct {
    capacity int
    cache    map[int]*Node
    head     *Node
    tail     *Node
}

func Constructor(capacity int) LRUCache {
    lru := LRUCache{
        capacity: capacity,
        cache:    make(map[int]*Node),
        head:     &Node{0, 0, nil, nil},
        tail:     &Node{0, 0, nil, nil},
    }
    lru.head.next = lru.tail
    lru.tail.prev = lru.head
    return lru
}

func (this *LRUCache) remove(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (this *LRUCache) addToHead(node *Node) {
    node.prev = this.head
    node.next = this.head.next
    this.head.next.prev = node
    this.head.next = node
}

func (this *LRUCache) Get(key int) int {
    if node, exists := this.cache[key]; exists {
        this.remove(node)
        this.addToHead(node)
        return node.value
    }
    return -1
}

func (this *LRUCache) Put(key int, value int) {
    if node, exists := this.cache[key]; exists {
        node.value = value
        this.remove(node)
        this.addToHead(node)
    } else {
        if len(this.cache) >= this.capacity {
            tail := this.tail.prev
            this.remove(tail)
            delete(this.cache, tail.key)
        }
        node := &Node{key, value, nil, nil}
        this.addToHead(node)
        this.cache[key] = node
    }
}
class Node
    attr_accessor :key, :value, :prev, :next

    def initialize(key, value)
        @key = key
        @value = value
        @prev = nil
        @next = nil
    end
end

class LRUCache
    def initialize(capacity)
        @capacity = capacity
        @cache = {}
        @head = Node.new(0, 0)
        @tail = Node.new(0, 0)
        @head.next = @tail
        @tail.prev = @head
    end

    def get(key)
        return -1 unless @cache.key?(key)

        node = @cache[key]
        remove(node)
        add_to_head(node)
        node.value
    end

    def put(key, value)
        if @cache.key?(key)
            node = @cache[key]
            node.value = value
            remove(node)
            add_to_head(node)
        else
            if @cache.size >= @capacity
                tail = remove_tail
                @cache.delete(tail.key)
            end
            node = Node.new(key, value)
            add_to_head(node)
            @cache[key] = node
        end
    end

    private

    def remove(node)
        node.prev.next = node.next
        node.next.prev = node.prev
    end

    def add_to_head(node)
        node.prev = @head
        node.next = @head.next
        @head.next.prev = node
        @head.next = node
    end

    def remove_tail
        node = @tail.prev
        remove(node)
        node
    end
end

8. Longest Substring Without Repeating Characters

Brief: Find the length of the longest substring without repeating characters.

Why this pattern?: This combines hash tables with sliding window technique, showing how to track state efficiently.

Key Insight: Use a sliding window with a hash set to track characters in the current window. When a duplicate is found, shrink the window from the left.

Visual Flow:

  flowchart TD
    A[Initialize window] --> B[Expand right pointer]
    B --> C{Character in set?}
    C -->|No| D[Add to set, update max]
    C -->|Yes| E[Shrink left pointer]
    D --> F{End reached?}
    E --> C
    F -->|No| B
    F -->|Yes| G[Return max length]
function lengthOfLongestSubstring(s) {
    const seen = new Set();
    let left = 0, maxLength = 0;

    for (let right = 0; right < s.length; right++) {
        while (seen.has(s[right])) {
            seen.delete(s[left]);
            left++;
        }

        seen.add(s[right]);
        maxLength = Math.max(maxLength, right - left + 1);
    }

    return maxLength;
}
def lengthOfLongestSubstring(s):
    seen = set()
    left = 0
    max_length = 0

    for right in range(len(s)):
        while s[right] in seen:
            seen.remove(s[left])
            left += 1

        seen.add(s[right])
        max_length = max(max_length, right - left + 1)

    return max_length
public int lengthOfLongestSubstring(String s) {
    Set<Character> seen = new HashSet<>();
    int left = 0, maxLength = 0;

    for (int right = 0; right < s.length(); right++) {
        while (seen.contains(s.charAt(right))) {
            seen.remove(s.charAt(left));
            left++;
        }

        seen.add(s.charAt(right));
        maxLength = Math.max(maxLength, right - left + 1);
    }

    return maxLength;
}
int lengthOfLongestSubstring(string s) {
    unordered_set<char> seen;
    int left = 0, maxLength = 0;

    for (int right = 0; right < s.length(); right++) {
        while (seen.find(s[right]) != seen.end()) {
            seen.erase(s[left]);
            left++;
        }

        seen.insert(s[right]);
        maxLength = max(maxLength, right - left + 1);
    }

    return maxLength;
}
func lengthOfLongestSubstring(s string) int {
    seen := make(map[byte]bool)
    left, maxLength := 0, 0

    for right := 0; right < len(s); right++ {
        for seen[s[right]] {
            seen[s[left]] = false
            left++
        }

        seen[s[right]] = true
        maxLength = max(maxLength, right-left+1)
    }

    return maxLength
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
def length_of_longest_substring(s)
    seen = Set.new
    left = 0
    max_length = 0

    s.each_char.with_index do |char, right|
        while seen.include?(char)
            seen.delete(s[left])
            left += 1
        end

        seen.add(char)
        max_length = [max_length, right - left + 1].max
    end

    max_length
end

Recommended Study Order

  1. Start with Easy Problems: Contains DuplicateTwo SumValid Anagram
  2. Progress to Medium: Group AnagramsIntersection of Two Arrays IITop K Frequent Elements
  3. Tackle Hard Problems: LRU CacheLongest Substring Without Repeating Characters

Common Patterns to Recognize

  • Frequency counting → Map for counts, Set for uniqueness
  • Complement search → Map for two sum, k sums
  • Grouping by property → Map with array values
  • Existence checking → Set for O(1) membership tests
  • Caching → Map for LRU, LFU implementations

Interview Tips

  • Choose between Map and Set - Map for key-value, Set for existence
  • Handle collision in interviews - Understand chaining vs open addressing
  • Consider time vs space - Sometimes arrays are faster for small datasets
  • Implement custom hash functions - When using objects as keys
  • Know built-in limitations - No ordering, possible hash conflicts

Remember: Hash tables provide excellent average performance but can have

O(n)
worst-case behavior. Modern language implementations handle this well, but understanding the underlying structure helps you choose the right tool for each problem.