Problem Solving
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.
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);
}
}