🪙
Leetcode
  • Content
  • Algorithms
    • Linear Search
    • Binary Search
    • Counting Sort
    • Merge Sort
    • Insertion Sort
    • Selection Sort
  • Array and String
    • Introduction to Array
      • Introduction to Array
      • Introduction to Dynamic Array
      • Find Pivot Index
      • Largest Number At Least Twice of Others
      • Plus One
    • Introduction to 2D Array
      • Introduction to 2D Array
      • Diagonal Traverse
      • Spiral Matrix
      • Pascal's Triangle
    • Introduction to String
      • Introduction to String
      • Immutable String - Problems & Solutions
      • Add binary
      • Implement strStr()
      • Longest Common Prefix
    • Two-Pointer Technique
      • Two-pointer Technique - Scenario I
      • Reverse String
      • Array Partition I
      • Two Sum II - Input array is sorted
      • Two-pointer Technique - Scenario II
      • Remove Element
      • Max Consecutive Ones
      • Minimum Size Subarray Sum
    • Conclusion
      • Array-related Techniques
      • Rotate Array
      • Pascal's Triangle II
      • Reverse Words in a String
      • Reverse Words in a String III
      • Remove Duplicates from Sorted Array
      • Move Zeroes
  • Linked List
    • Singly Linked List
      • Introduction - Singly Linked List
      • Add Operation - Singly Linked List
      • Delete Operation - Singly Linked List
      • Design Linked List
    • Two Pointer Technique
      • Two-Pointer in Linked List
      • Linked List Cycle
      • Linked List Cycle II
      • Intersection of Two Linked Lists
      • Remove Nth Node From End of List
      • Summary - Two-Pointer in Linked List
  • Problems
    • 1. Two Sum
    • 2. Add Two Numbers
    • 7. Reverse Integer
    • 9. Palindrome Number
    • 11. Container With Most Water
    • 12. Integer to Roman
    • 13. Roman to Integer
    • 14. Longest Common Prefix
    • 15. 3Sum
    • 21. Merge Two Sorted Lists
    • 26. Remove Duplicates from Sorted Array
    • 27. Remove Element
    • 28. Find the Index of the First Occurrence in a String
    • 34. Find First and Last Position of Element in Sorted Array
    • 35. Search Insert Position
    • 43. Multiply Strings
    • 49. Group Anagrams
    • 50. Pow(x, n)
    • 54. Spiral Matrix
    • 58. Length of Last Word
    • 66. Plus One
    • 67. Add Binary
    • 69. Sqrt(x)
    • 73. Set Matrix Zeroes
    • 75. Sort Colors
    • 88. Merge Sorted Array
    • 104. Maximum Depth of Binary Tree
    • 121. Best Time to Buy and Sell Stock
    • 122. Best Time to Buy and Sell Stock II
    • 136. Single Number
    • 146. LRU Cache
    • 189. Rotate Array
    • 206. Reverse Linked List
    • 217. Contains Duplicate
    • 219. Cotains Duplicate II
    • 226. Invert Binary Tree
    • 238. Product of Array Except Self
    • 242. Valid Anagram
    • 268. Missing Number
    • 283. Move Zeroes
    • 350. Intersection of Two Arrays II
    • 383. Ransom Note
    • 389. Find the Difference
    • 412. Fizz Buzz
    • 414. Third Maximum Number
    • 445. Add Two Numbers II
    • 448. Find All Numbers Disappeared in an Array
    • 459. Repeated Substring Pattern
    • 485. Max Consecutive Ones
    • 509. Fibonacci Number
    • 637. Average of Levels in Binary Tree
    • 657. Robot Return to Origin
    • 682. Baseball Game
    • 704. Binary Search
    • 705. Design HashSet
    • 709. To Lower Case
    • 724. Find Pivot Index
    • 876. Middle of the Linked List
    • 896. Monotonic Array
    • 860. Lemonade Change
    • 905. Sort Array By Parity
    • 916. Word Subsets
    • 941. Valid Mountain Array
    • 976. Largest Perimeter Triangle
    • 977. Squares of a Sorted Array
    • 1041. Robot Bounded In Circle
    • 1051. Height Checker
    • 1089. Duplicate Zeros
    • 1232. Check If It Is a Straight Line
    • 1275. Find Winner on a Tic Tac Toe Game
    • 1295. Find Numbers with Even Number of Digits
    • 1299. Replace Elements with Greatest Element on Right Side
    • 1342. Number of Steps to Reduce a Number to Zero
    • 1346. Check If N and Its Double Exist
    • 1476. Subrectangle Queries
    • 1480. Running Sum of 1d Array
    • 1491. Average Salary Excluding the Minimum and Maximum Salary
    • 1502. Can Make Arithmetic Progression From Sequence
    • 1523. Count Odd Numbers in an Interval Range
    • 1572. Matrix Diagonal Sum
    • 1672. Richest Customer Wealth
    • 1768. Merge Strings Alternately
    • 1752. Check if Array Is Sorted and Rotated
    • 1769. Minimum Number of Operations to Move All Balls to Each Box
    • 1790. Check if One String Swap Can Make Strings Equal
    • 1800. Maximum Ascending Subarray Sum
    • 1822. Sign of the Product of an Array
    • 1930. Unique Length-3 Palindromic Subsequences
    • 1991. Find the Middle Index in Array
    • 2185. Counting Words With a Given Prefix
    • 2235. Add Two Integers
    • 2236. Root Equals Sum of Children
    • 2270. Number of Ways to Split Array
    • 2381. Shifting Letters II
    • 2559. Count Vowel Strings in Ranges
    • 2610. Convert an Array Into a 2D Array With Conditions
    • 2657. Find the Prefix Common Array of Two Arrays
    • 3042. Count Prefix and Suffix Pairs I
    • 3105. Longest Strictly Increasing or Strictly Decreasing Subarray
    • 3151. Special Array I
    • 3223. Minimum Length of String After Operations
Powered by GitBook
On this page
  • Example 1
  • Constraints
  • Solution
  • Optimal Solution
  • Approach Analysis
  • Visualization of Both Approaches
  • Complexity Analysis
  • Why Solution Works
  • When to Use
  • Common Patterns & Applications
  • Interview Tips

Was this helpful?

Edit on GitHub
  1. Problems

146. LRU Cache

Previous136. Single NumberNext189. Rotate Array

Last updated 5 months ago

Was this helpful?

Medium

Design a data structure that follows the constraints of a .

Implement the LRUCache class:

  • LRUCache(int capacity) Initialize the LRU cache with positive size capacity.

  • int get(int key) Return the value of the key if the key exists, otherwise return -1.

  • void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.

The functions get and put must each run in O(1) average time complexity.

Example 1

Input: ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] Output: [null, null, null, 1, null, -1, null, -1, 3, 4] Explanation: LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // cache is {1=1} lRUCache.put(2, 2); // cache is {1=1, 2=2} lRUCache.get(1); // return 1 lRUCache.put(3, 3); // LRU key was 2, evicts key 2, cache is {1=1, 3=3} lRUCache.get(2); // returns -1 (not found) lRUCache.put(4, 4); // LRU key was 1, evicts key 1, cache is {4=4, 3=3} lRUCache.get(1); // return -1 (not found) lRUCache.get(3); // return 3 lRUCache.get(4); // return 4

Constraints

  • 1 <= capacity <= 3000

  • 0 <= key <= 10^4

  • 0 <= value <= 10^5

  • At most 2 * 10^5 calls will be made to get and put.

Solution

My Solution

type LRUCache struct {
    Head, Tail *Node
    M map[int]*Node
    Capacity int
}

type Node struct {
    Prev, Next *Node
    Key, Value int
}

func Constructor(capacity int) LRUCache {
    head, tail := &Node{Key:0, Value:0}, &Node{Key:0, Value:0}
    head.Next = tail
    tail.Prev = head
    return LRUCache{
        Capacity:capacity,
        M: make(map[int]*Node),
        Head: head,
        Tail: tail,
    }
}


func (this *LRUCache) Get(key int) int {
    if n, ok := this.M[key]; ok {
        this.remove(n)
        this.insert(n)
        return n.Value
    }

    return -1
}


func (this *LRUCache) Put(key int, value int)  {
    if _, ok := this.M[key]; ok {
        this.remove(this.M[key])
    }

    if len(this.M) == this.Capacity {
        this.remove(this.Tail.Prev)
    }

    this.insert(&Node{Key:key, Value: value})
}

func (this *LRUCache) remove(node *Node) {
    delete(this.M, node.Key)
    node.Prev.Next = node.Next
    node.Next.Prev = node.Prev
}

func (this *LRUCache) insert(node *Node) {
    this.M[node.Key] = node
    next := this.Head.Next
    this.Head.Next = node
    node.Prev = this.Head
    next.Prev = node
    node.Next = next
}


/**
 * Your LRUCache object will be instantiated and called as such:
 * obj := Constructor(capacity);
 * param_1 := obj.Get(key);
 * obj.Put(key,value);
 */

Optimal Solution

The optimal solution for an LRU Cache uses a combination of:

  1. HashMap for O(1) key-value lookups

  2. Doubly Linked List for O(1) insertion/deletion

  3. Pointer manipulation for updating access order

// The implementation above is already optimal in terms of time complexity
// Here's an alternative implementation with better readability and encapsulation

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

type Node struct {
    key, value int
    prev, next *Node
}

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

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

func (lru *LRUCache) Put(key int, value int) {
    if node, exists := lru.cache[key]; exists {
        node.value = value
        lru.moveToFront(node)
        return
    }
    
    newNode := &Node{key: key, value: value}
    lru.cache[key] = newNode
    lru.addToFront(newNode)
    
    if len(lru.cache) > lru.capacity {
        lru.removeLRU()
    }
}

func (lru *LRUCache) moveToFront(node *Node) {
    lru.removeFromList(node)
    lru.addToFront(node)
}

func (lru *LRUCache) addToFront(node *Node) {
    node.prev = lru.head
    node.next = lru.head.next
    lru.head.next.prev = node
    lru.head.next = node
}

func (lru *LRUCache) removeFromList(node *Node) {
    node.prev.next = node.next
    node.next.prev = node.prev
}

func (lru *LRUCache) removeLRU() {
    lruNode := lru.tail.prev
    lru.removeFromList(lruNode)
    delete(lru.cache, lruNode.key)
}

Approach Analysis

The LRU Cache implementation uses two main data structures:

  1. HashMap (map[int]*Node):

    • Provides O(1) access to cache entries

    • Maps keys to doubly-linked list nodes

    • Enables quick lookups for get/put operations

  2. Doubly Linked List:

    • Maintains access order of elements

    • Most recently used items at front

    • Least recently used items at back

    • Allows O(1) removal/insertion of nodes

Visualization of Both Approaches

Initial state (capacity = 2):
HEAD ⟷ TAIL
map[]

After put(1,1):
HEAD ⟷ [1,1] ⟷ TAIL
map[1 → Node(1,1)]

After put(2,2):
HEAD ⟷ [2,2] ⟷ [1,1] ⟷ TAIL
map[1 → Node(1,1), 2 → Node(2,2)]

After get(1): // moves 1 to front
HEAD ⟷ [1,1] ⟷ [2,2] ⟷ TAIL
map[1 → Node(1,1), 2 → Node(2,2)]

After put(3,3): // evicts 2 (LRU)
HEAD ⟷ [3,3] ⟷ [1,1] ⟷ TAIL
map[1 → Node(1,1), 3 → Node(3,3)]

Complexity Analysis

Time Complexity:

  • Constructor: O(1)

  • Get: O(1)

  • Put: O(1)

  • All internal operations (moveToFront, removeLRU): O(1)

Space Complexity:

  • O(capacity) for both HashMap and Linked List

  • Each entry requires:

    • HashMap entry: O(1)

    • Linked List node: O(1)

    • Total per entry: O(1)

Why Solution Works

  1. HashMap + Linked List Synergy:

    • HashMap provides instant access to nodes

    • Linked List maintains order without requiring shifts

    • Both structures complement each other's weaknesses

  2. Sentinel Nodes (Head/Tail):

    • Eliminate edge cases in node manipulation

    • Simplify insertion/deletion logic

    • No need to check for null pointers

  3. Encapsulated Operations:

    • moveToFront handles the "recently used" aspect

    • removeLRU maintains capacity constraint

    • Clean separation of concerns

When to Use

Use LRU Cache when:

  1. Need O(1) access time for cache operations

  2. Want to evict least recently used items when capacity is reached

  3. Memory usage can be bounded by a fixed capacity

  4. Recent access patterns are good predictors of future accesses

Common applications:

  • Database query caching

  • Web browser page caching

  • File system caching

  • Memory management in operating systems

Common Patterns & Applications

  1. Caching Pattern:

    • Fast access to recent items

    • Automatic eviction of stale items

    • Fixed memory footprint

  2. Doubly Linked List Pattern:

    • O(1) removal from any position

    • O(1) insertion at any position

    • Bidirectional traversal capability

  3. HashMap + Linked List Pattern:

    • Common in cache implementations

    • Combines fast lookup with ordered access

    • Used in many system designs

Interview Tips

  1. Implementation Strategy:

    • Start with data structure definitions

    • Implement basic operations first

    • Add edge case handling

    • Consider thread safety if asked

  2. Key Points to Mention:

    • Why you chose the data structures

    • Time/space complexity analysis

    • How you handle edge cases

    • Possible optimizations

  3. Common Follow-up Questions:

    • How to make it thread-safe?

    • How to implement LFU Cache instead?

    • How to handle variable entry sizes?

    • How to implement distributed LRU Cache?

  4. Gotchas to Watch For:

    • Memory leaks in evicted entries

    • Proper linking/unlinking of nodes

    • HashMap and List consistency

    • Capacity edge cases

Leetcode:

Least Recently Used (LRU) cache
link
result