Skip to content

Commit f4b2ba9

Browse files
authored
SOLN: LRU cache and others (#193)
1 parent a212b2d commit f4b2ba9

File tree

8 files changed

+277
-91
lines changed

8 files changed

+277
-91
lines changed

src/array/group-anagrams.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export { groupAnagrams };
1515
//
1616
// COMPLEXITY:
1717
//
18-
// Runs in O(n * m * log(m)) time, where n is the number of strings and m is the length of the longest string. This
19-
// is because we have to sort each string's characters in O(m * log(m)), and there are n strings.
18+
// Runs in O(n * m * log m) time, where n is the number of strings and m is the length of the longest string. This
19+
// is because we have to sort each string's characters in O(m * log m), and there are n strings.
2020
function groupAnagrams(texts: string[]): string[][] {
2121
type Canonical = string;
2222
type Anagram = string;

src/array/merge-sorted-array.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export { merge };
1414

1515
// SOLUTION:
1616
//
17+
// TLDR: Work backwards. Send the largest elements from the smaller array to the end of the bigger array.
18+
//
1719
// This would be VERY easy, if you could use extra memory. To merge without using extra memory, we do need to merge
1820
// into the bigger array, nums1.
1921
//

src/array/top-k-frequent-elements.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ function topKFrequent(xs: number[], k: number) {
2525
map.set(x, frequency + 1);
2626
}
2727

28-
// Sort the map keys by their frequency.
29-
//
30-
// Note that map.get(a)! - map.get(b)! would sort by increasing frequency; we want decreasing frequency because we
31-
// want the top k elements.
32-
const keys = Array.from(map.keys());
33-
const sorted = keys.sort((a, b) => map.get(b)! - map.get(a)!);
28+
// Now get all unique elements from the list.
29+
const uniques = [...map.keys()];
30+
31+
// Sort the unique values by their frequency. Since we want the most frequent elements, we sort in decreasing order.
32+
uniques.sort((a, b) => map.get(b)! - map.get(a)!);
3433

3534
// Return the first k elements.
36-
return sorted.slice(0, k);
35+
return uniques.slice(0, k);
3736
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// DIFFICULTY: MEDIUM
2+
//
3+
// Given an n x n matrix where each of the rows and columns is sorted in ascending order, return the kth smallest
4+
// element in the matrix.
5+
//
6+
// Note that it is the kth smallest element in the sorted order, not the kth distinct element.
7+
//
8+
// You must find a solution with a memory complexity better than O(n^2).
9+
//
10+
// See {@link https://leetcode.com/problems/kth-smallest-element-in-a-sorted-matrix/}
11+
export { kthSmallest };
12+
13+
// SOLUTION:
14+
//
15+
// We can't flatten the list because that would take O(n^2) memory.
16+
//
17+
// Use binary search to narrow down where the kth smallest element is. Here, the left value is the smallest element,
18+
// and the right value is the largest element.
19+
//
20+
// We can use the mid element to tell us how close or far we are from the kth smallest element. The matrix is sorted,
21+
// so for any mid === matrix[i][j] we can figure out how many elements are less than or equal to mid.
22+
//
23+
// COMPLEXITY:
24+
//
25+
// We are using binary search on a range between the smallest and largest elements in the matrix, call it k. We will
26+
// do O(log k) iterations, but in each iteration, we do have to count how many elements are less than or equal to the
27+
// mid target.
28+
//
29+
// The countLessThanOrEqualTo() function takes at most n steps (where n is the number of rows/columns in the matrix).
30+
// Since this is done at every single iteration, the total time is O(n log k).
31+
//
32+
// Space complexity is O(1).
33+
function kthSmallest(matrix: number[][], k: number): number {
34+
// Use insertion sort binary search to find exactly where the kth smallest element is; don't use the standard binary
35+
// search algorithm because we're not looking for an exact value.
36+
let left = matrix[0][0];
37+
let right = matrix[matrix.length - 1][matrix.length - 1];
38+
while (left < right) {
39+
const mid = Math.floor((left + right) / 2);
40+
const count = countLessThanOrEqualTo(matrix, mid);
41+
42+
if (count < k) {
43+
left = mid + 1;
44+
} else {
45+
right = mid;
46+
}
47+
}
48+
49+
return left;
50+
}
51+
52+
function countLessThanOrEqualTo(matrix: number[][], target: number) {
53+
// We can leverage the properties of the matrix to count how many elements are less than or equal to the target.
54+
// Use a modified two pointer technique to count up elements.
55+
let count = 0;
56+
57+
// Start at the bottom left corner of the matrix:
58+
//
59+
// - - - -
60+
// - - - -
61+
// - - - -
62+
// x - - -
63+
//
64+
// If the value is less than or equal to the target, then all elements from all previous rows are also less than our
65+
// target (since the matrix is sorted). To count up that specific column, add (row + 1) to the count (the rows are
66+
// 0 indexed, but the count is not, so we add 1).
67+
let row = matrix.length - 1;
68+
let column = 0;
69+
while (row >= 0 && column < matrix.length) {
70+
// Add up all the elements in the column. That is:
71+
//
72+
// x - - -
73+
// x - - -
74+
// x - - -
75+
// x - - -
76+
//
77+
// All x's get added to the count, which means all elements in the column get added to the count. Because the
78+
// matrix is sorted, all elements in the above column MUST be less than or equal to the target.
79+
//
80+
// Stop when we reach column === matrix.length - 1.
81+
if (matrix[row][column] <= target) {
82+
count += row + 1;
83+
column++;
84+
}
85+
// If the element is greater than the target, we can move up a row and try again:
86+
//
87+
// x - - -
88+
// x - - -
89+
// x - - -
90+
// - - - -
91+
//
92+
// Stop when we reach row === 0.
93+
else {
94+
row--;
95+
}
96+
}
97+
98+
return count;
99+
}

src/linked-list/lru-cache.ts

Lines changed: 41 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@ export { LRUCache };
2222
// This class definition is provided by LeetCode and must be implemented by you. An LRU cache can be implemented
2323
// using a hashmap and a linked list.
2424
class LRUCache {
25-
private head?: Node;
26-
27-
private last?: Node;
28-
2925
private map: Map<number, Node>;
30-
3126
private capacity: number;
27+
private head: Node;
28+
private tail: Node;
3229

3330
constructor(capacity: number) {
3431
this.map = new Map();
3532
this.capacity = capacity;
33+
34+
// These are sentinel nodes that will never be directly accessed, but help us avoid certain undefined checks.
35+
this.head = new Node(-1, -1);
36+
this.tail = new Node(-1, -1);
37+
this.head.next = this.tail;
38+
this.tail.previous = this.head;
3639
}
3740

3841
get(key: number): number {
@@ -44,105 +47,61 @@ class LRUCache {
4447
// storing the key on the node, we can just delete the last element from the list and then check for an undefined
4548
// value here in case we deleted the key from being at capacity.
4649
const node = this.map.get(key)!;
47-
48-
// If the node is already at the front of the list, just remove it and return the value.
49-
if (node.key === this.head?.key) {
50-
return node.value;
51-
}
52-
53-
// Detach the node from its position in the list, and connect the previous and next nodes to each other instead.
54-
this.unlink(node);
55-
56-
// Move the node to the front of the list.
57-
this.unshift(node);
58-
50+
this.moveToHead(node);
5951
return node.value;
6052
}
6153

6254
put(key: number, value: number): void {
6355
// If we already have this value in the map, just update it and don't bother mucking with the capacity.
6456
if (this.map.has(key)) {
6557
const node = this.map.get(key)!;
58+
this.moveToHead(node);
6659
node.value = value;
67-
68-
// Call get to update timestamp on the node, but throw away the result.
69-
const _ = this.get(key);
7060
return;
7161
}
7262

73-
// If there's no capacity, we don't have to do anything.
74-
if (this.capacity <= 0) {
75-
return;
76-
}
77-
78-
// If we are at capacity, we will first have to evict an entry from the cache by finding the last accessed element
79-
// from our list.
80-
if (this.map.size === this.capacity) {
81-
this.pop();
82-
}
83-
8463
const node = new Node(key, value);
8564
this.map.set(key, node);
86-
87-
// I am assuming that a put will also update the last accessed timestamp of the node, so move it to the front of
88-
// the list.
89-
this.unshift(node);
90-
}
91-
92-
// Remove a node that is NOT the head node.
93-
private unlink(node: Node) {
94-
// If this was the last node, update the last pointer.
95-
if (node.key === this.last?.key) {
96-
this.last = node.previous!;
97-
this.last.next = undefined;
98-
return;
99-
}
100-
101-
// Otherwise, this is not the last node and not the head node, so stitch the previous and next nodes together.
102-
const left = node.previous!;
103-
const right = node.next;
104-
left.next = right;
105-
if (right !== undefined) {
106-
right.previous = left;
65+
this.addToHead(node);
66+
67+
// If we've exceed capacity, we have to remove the least used element, aka the tail. At this point, there is
68+
// guaranteed to be at least one other element in the list because we just added one. Also, unless the capacity
69+
// is 0, we will always at least have another.
70+
//
71+
// So we can safely access this.tail.previous!
72+
if (this.map.size > this.capacity) {
73+
const tail = this.tail.previous!;
74+
this.removeNode(tail);
75+
this.map.delete(tail.key);
10776
}
10877
}
10978

110-
// Unshift a node that is NOT the head node.
111-
private unshift(node: Node) {
112-
node.previous = undefined;
79+
private addToHead(node: Node): void {
80+
const a = this.head;
81+
const b = node;
82+
const c = this.head.next;
11383

114-
if (this.head === undefined) {
115-
this.head = node;
116-
} else {
117-
this.head.previous = node;
118-
node.next = this.head;
119-
this.head = node;
120-
}
84+
// Updates the pointers for the node itself.
85+
b.next = c;
86+
b.previous = a;
12187

122-
if (this.last === undefined) {
123-
this.last = node;
124-
}
88+
// Inserts the node at the front of the list.
89+
a.next = b;
90+
c!.previous = b;
12591
}
12692

127-
// Unlink the last node (also possibly the head node) and remove it from the map.
128-
private pop() {
129-
const node = this.last;
130-
131-
if (node === undefined) {
132-
return;
133-
}
134-
135-
this.map.delete(node.key);
93+
private moveToHead(node: Node): void {
94+
this.removeNode(node);
95+
this.addToHead(node);
96+
}
13697

137-
// If this was the head node, just set both nodes to undefined and be done with it.
138-
if (node.key === this.head?.key) {
139-
this.head = undefined;
140-
this.last = undefined;
141-
return;
142-
}
98+
private removeNode(node: Node): void {
99+
const b = node;
100+
const a = b.previous!;
101+
const c = b.next!;
143102

144-
// Otherwise, unlink the last node.
145-
this.unlink(node);
103+
a.next = c;
104+
c.previous = a;
146105
}
147106
}
148107

0 commit comments

Comments
 (0)