Problem Solving
- LRU Cache - Medium - LeetCode 146
- Longest Palindromic Substring - Medium - LeetCode 5
- Find Median from Data Stream - Hard - LeetCode 295
- Word Break II - Hard - LeetCode 140
- Alien Dictionary - Hard - LeetCode 269
LRU Cache - Medium - LeetCode 146🔗
Question
Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.
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.
Explanation
LRU cache is a challenging problem, the problem is mostly focused on how to store the keys efficiently for removal by time inserted. Therefore in addition to the k/v hashtable we need an additional data-structure that allows retrieval, deletion and modification. Heap / Tree based solution are all applicable solutions but requires O(Log(N)) for insertion and removal.
In this problem it is possible to do a constant time when choosing a doubly linked list which allows to add to head and evicti from tail. It also possible to perform modification in constant (move to HEAD).
This is a java implemention of DoublyLinkedList and the LRU Cache.
graph TD
subgraph "LRU Cache Structure"
Map["HashMap<Integer, Node>"]
DLL["Doubly Linked List"]
end
Map -- "Points to" --> DLL
subgraph DLL
Head["Head (Most Recently Used)"]
Tail["Tail (Least Recently Used)"]
N1[Node 1]
N2[Node 2]
N3[Node 3]
Head --> N1
N1 <--> N2
N2 <--> N3
N3 --> Tail
end
Solution
class DoublyLinkedList {
public Node root;
public Node tail;
public Node addToHead(final int key) {
if (root == null) {
this.root = new Node(key, null, null);
this.tail = this.root;
} else {
Node node = new Node(key, this.root, null);
this.root.previous = node;
this.root = node;
}
return this.root;
}
public void moveToHead(final Node node) throws IllegalArgumentException {
if (this.root == null) {
throw new RuntimeException("Invalid head state");
}
if (this.root == node) { // node is already head
return;
}
if (this.tail == node) { // node is the tail - fix it
this.tail = this.tail.previous;
}
if (node.previous != null) { // fix existing previous of node
node.previous.next = node.next;
}
if (node.next != null) { // fix existing next of node
node.next.previous = node.previous;
}
// set as head
node.previous = null;
node.next = this.root;
this.root.previous = node;
this.root = node;
}
public Node evictTail() {
if (this.root == null) { // list is empty
return null;
}
if (this.root == this.tail) {
Node reference = this.root;
this.root = null;
this.tail = null;
return reference;
}
Node existingTail = this.tail;
existingTail.previous.next = null;
this.tail = existingTail.previous;
return existingTail;
}
}
class LRUCache {
final int capacity;
int size;
final Map<Integer, CacheEntry> cache;
final DoublyLinkedList sortedTtlList;
static class CacheEntry {
public CacheEntry(int value, Node node) {
this.value = value;
this.node = node;
}
public int value;
public Node node;
}
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
this.cache = new HashMap<>();
this.sortedTtlList = new DoublyLinkedList();
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
CacheEntry entry = cache.get(key);
sortedTtlList.moveToHead(entry.node);
return entry.value;
}
public void put(int key, int value) {
if (cache.containsKey(key)) {
CacheEntry entry = cache.get(key);
sortedTtlList.moveToHead(entry.node);
entry.value = value;
} else {
if (this.size < this.capacity) {
this.size = this.size + 1;
} else {
Node nodeToRemove = sortedTtlList.evictTail();
cache.remove(nodeToRemove.key);
}
final Node newNode = sortedTtlList.addToHead(key);
cache.put(key, new CacheEntry(value, newNode));
}
}
}
Longest Palindromic Substring - Medium - LeetCode 5🔗
Question
Given a string s, return the longest palindromic substring in s.
Input: s = "babad"
Output: "bab"
Explanation: "aba" is also a valid answer.
Explanation
The simplest solution is calling isPalindrome for every substring. Checking palindrome is O(N) and all substrings are O(N^2) - therefore O(N^3).
This problem can be converted to a dynamic programming problem where Palindrome(i, j) = Palindrome(i+1, j-1) && s[i] == s[j] (same for odd and even cases). This will require O(N^2) time and O(N^2) memory.
There is a simpler way to think about this problem. We can think about a palindrome as a center-based string and then check the palindrome from all of the centers.
Solution
public class LongestPalindrome {
private static class PalindromeSequence {
public int startIndex;
public int length;
PalindromeSequence() {
this.startIndex = 0;
this.length = 1;
}
}
private void checkAndUpdateLongestPalindrome(String s, int leftIndex, int rightIndex, PalindromeSequence maxPalindrome) {
while (leftIndex >= 0 && rightIndex < s.length() && s.charAt(leftIndex) == s.charAt(rightIndex)) {
leftIndex -= 1;
rightIndex += 1;
}
int existingLength = rightIndex - leftIndex - 1;
if (existingLength > maxPalindrome.length) {
maxPalindrome.startIndex = leftIndex + 1;
maxPalindrome.length = existingLength;
}
}
public String longestPalindrome(String s) {
PalindromeSequence maxPalindrome = new PalindromeSequence();
for (int i = 0; i < s.length(); i++) {
int rightIndex = i;
int leftIndex = i - 1;
checkAndUpdateLongestPalindrome(s, leftIndex, rightIndex, maxPalindrome);
leftIndex = i - 1;
rightIndex = i + 1;
checkAndUpdateLongestPalindrome(s, leftIndex, rightIndex, maxPalindrome);
}
return s.substring(maxPalindrome.startIndex, maxPalindrome.startIndex + maxPalindrome.length);
}
}
Dynamic programming alternative for reference
public class LongestPalindromeDynamicProgramming {
public String longestPalindrome(String s) {
int strLength = s.length();
int maxLength = 0;
int maxStartIndex = 0;
boolean[][] dp = new boolean[strLength][strLength];
for (int i = strLength - 1; i >= 0; i--) {
for (int j = i; j < strLength; j++) {
dp[i][j] = s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i+1][j-1]);
int existingLength = j - i + 1;
if (dp[i][j] && existingLength > maxLength) {
maxStartIndex = i;
maxLength = j - i + 1;
}
}
}
return s.substring(maxStartIndex, maxStartIndex + maxLength);
}
}
Find Median from Data Stream - Hard - LeetCode 295🔗
Question
The median is the middle value in an ordered integer list.
If the size of the list is even, there is no middle value and the median is the mean of the two middle values.
Implement the
MedianFinderclass:
MedianFinder()initializes the object.void addNum(int num)adds the integernumfrom the data stream to the data structure.double findMedian()returns the median of all elements so far.
Explanation
The standard solution is using two heaps:
- Max-heap (
low) stores the smaller half. - Min-heap (
high) stores the larger half.
Invariants:
low.size()is either equal tohigh.size()or exactly one larger.- Every element in
lowis<=every element inhigh.
Why median lookup is simple:
- Odd number of elements:
lowhas one extra item, so median islow.peek(). - Even number of elements: heaps are same size, so median is average of
low.peek()andhigh.peek().
This is why findMedian() is O(1) after maintaining heap balance on every insert.
This approach naturally handles duplicates and negative numbers because heap ordering is value-based.
Solution
import java.util.Collections;
import java.util.PriorityQueue;
class MedianFinder {
// Max-heap for lower half
private final PriorityQueue<Integer> low;
// Min-heap for upper half
private final PriorityQueue<Integer> high;
public MedianFinder() {
this.low = new PriorityQueue<>(Collections.reverseOrder());
this.high = new PriorityQueue<>();
}
public void addNum(int num) {
low.offer(num);
high.offer(low.poll());
// Keep low >= high and size difference at most 1
if (high.size() > low.size()) {
low.offer(high.poll());
}
}
public double findMedian() {
if (low.size() > high.size()) {
return low.peek();
}
return (low.peek() + high.peek()) / 2.0;
}
}
Complexity
addNum:O(log N)findMedian:O(1)- Space:
O(N)
Follow-ups
- Duplicates: no special handling needed.
- Negative numbers: no special handling needed.
- Space optimization: exact median over unbounded stream requires
O(N)memory; for bounded memory use streaming quantile sketches (e.g., t-digest/KLL), which are similar to online k-means style weighted-centroid compression of 1D values.
Word Break II - Hard - LeetCode 140🔗
Question
Given a string
sand a dictionary of stringswordDict, add spaces insto construct all possible sentences where each word is inwordDict.Return all such possible sentences in any order.
Explanation
The standard approach is DP + backtracking (memoized DFS).
- DFS tries every valid dictionary word starting at index
i. - Memoization stores all sentence suffixes from
ito avoid repeated work. - Backtracking builds full sentences by combining current word with suffix sentences.
Without memoization this can explode with repeated subproblems.
Solution
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class WordBreakII {
public List<String> wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
Map<Integer, List<String>> memo = new HashMap<>();
return dfs(s, 0, dict, memo);
}
private List<String> dfs(String s, int start, Set<String> dict, Map<Integer, List<String>> memo) {
if (memo.containsKey(start)) {
return memo.get(start);
}
List<String> result = new ArrayList<>();
if (start == s.length()) {
result.add("");
memo.put(start, result);
return result;
}
for (int end = start + 1; end <= s.length(); end++) {
String word = s.substring(start, end);
if (!dict.contains(word)) {
continue;
}
List<String> suffixes = dfs(s, end, dict, memo);
for (String suffix : suffixes) {
if (suffix.isEmpty()) {
result.add(word);
} else {
result.add(word + " " + suffix);
}
}
}
memo.put(start, result);
return result;
}
}
Follow-ups
- Time/space: output-sensitive; worst case is exponential due to number of valid sentences.
- Optimization: pre-check with Word Break I boolean DP to fail fast when no solution exists.
- Edge cases: empty input, duplicate dictionary entries, and very long strings with dense prefixes.
Alien Dictionary - Hard - LeetCode 269🔗
Question
There is a new alien language that uses the English alphabet, but the order of letters is unknown.
Given a list of words sorted lexicographically in this alien language, derive a possible order of letters.
Return
\"\"if the input is invalid or contains a cycle.
Explanation
Model characters as a graph:
- Compare adjacent words to find first differing character, which creates a directed edge
u -> v. - Build indegree counts for each character.
- Run topological sort (Kahn's algorithm).
- If processed character count is smaller than total unique characters, a cycle exists.
Important invalid case: prefix conflict (for example \"abc\" before \"ab\").
Solution
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
class AlienDictionary {
public String alienOrder(String[] words) {
Map<Character, Set<Character>> graph = new HashMap<>();
Map<Character, Integer> indegree = new HashMap<>();
for (String word : words) {
for (char c : word.toCharArray()) {
graph.putIfAbsent(c, new HashSet<>());
indegree.putIfAbsent(c, 0);
}
}
for (int i = 0; i < words.length - 1; i++) {
String first = words[i];
String second = words[i + 1];
if (first.length() > second.length() && first.startsWith(second)) {
return "";
}
int minLen = Math.min(first.length(), second.length());
for (int j = 0; j < minLen; j++) {
char from = first.charAt(j);
char to = second.charAt(j);
if (from != to) {
if (!graph.get(from).contains(to)) {
graph.get(from).add(to);
indegree.put(to, indegree.get(to) + 1);
}
break;
}
}
}
Queue<Character> queue = new ArrayDeque<>();
for (char c : indegree.keySet()) {
if (indegree.get(c) == 0) {
queue.offer(c);
}
}
StringBuilder order = new StringBuilder();
while (!queue.isEmpty()) {
char current = queue.poll();
order.append(current);
for (char next : graph.get(current)) {
indegree.put(next, indegree.get(next) - 1);
if (indegree.get(next) == 0) {
queue.offer(next);
}
}
}
if (order.length() != indegree.size()) {
return "";
}
return order.toString();
}
}
Follow-ups
- Complexity:
O(C + E)whereCis unique chars andEis precedence edges. - Invalid input checks: prefix rule and cycle detection are required.
- If multiple valid orders exist, returning any valid topological ordering is acceptable.