Leetcode: LFU Cache && Summary of various Sets: HashSet, TreeSet, LinkedHashSet

时间:2023-03-08 18:15:55
Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get and set.

get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
set(key, value) - Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted. Follow up:
Could you do both operations in O(1) time complexity? Example: LFUCache cache = new LFUCache( 2 /* capacity */ ); cache.set(1, 1);
cache.set(2, 2);
cache.get(1); // returns 1
cache.set(3, 3); // evicts key 2
cache.get(2); // returns -1 (not found)
cache.get(3); // returns 3.
cache.set(4, 4); // evicts key 1.
cache.get(1); // returns -1 (not found)
cache.get(3); // returns 3
cache.get(4); // returns 4

referred to: https://discuss.leetcode.com/topic/69137/java-o-1-accept-solution-using-hashmap-doublelinkedlist-and-linkedhashset

Two HashMaps are used, one to store <key, value> pair, another store the <key, node>.
I use double linked list to keep the frequent of each key. In each double linked list node, keys with the same count are saved using java built in LinkedHashSet. This can keep the order.
Every time, one key is referenced, first find the current node corresponding to the key, If the following node exist and the frequent is larger by one, add key to the keys of the following node, else create a new node and add it following the current node.
All operations are guaranteed to be O(1).

 public class LFUCache {
int cap;
ListNode head;
HashMap<Integer, Integer> valueMap;
HashMap<Integer, ListNode> nodeMap; public LFUCache(int capacity) {
this.cap = capacity;
this.head = null;
this.valueMap = new HashMap<Integer, Integer>();
this.nodeMap = new HashMap<Integer, ListNode>();
} public int get(int key) {
if (valueMap.containsKey(key)) {
increaseCount(key);
return valueMap.get(key);
}
return -1;
} public void set(int key, int value) {
if (cap == 0) return;
if (valueMap.containsKey(key)) {
valueMap.put(key, value);
increaseCount(key);
}
else {
if (valueMap.size() < cap) {
valueMap.put(key, value);
addToHead(key);
}
else {
removeOld();
valueMap.put(key, value);
addToHead(key);
}
} } public void increaseCount(int key) {
ListNode node = nodeMap.get(key);
node.keys.remove(key);
if (node.next == null) {
node.next = new ListNode(node.count+1);
node.next.prev = node;
node.next.keys.add(key);
}
else if (node.next.count == node.count + 1) {
node.next.keys.add(key);
}
else {
ListNode newNode = new ListNode(node.count+1);
newNode.next = node.next;
node.next.prev = newNode;
newNode.prev = node;
node.next = newNode;
node.next.keys.add(key);
}
nodeMap.put(key, node.next);
if (node.keys.size() == 0) remove(node);
} public void remove(ListNode node) {
if (node.next != null) {
node.next.prev = node.prev;
}
if (node.prev != null) {
node.prev.next = node.next;
}
else { // node is head
head = head.next;
}
} public void addToHead(int key) {
if (head == null) {
head = new ListNode(1);
head.keys.add(key);
}
else if (head.count == 1) {
head.keys.add(key);
}
else {
ListNode newHead = new ListNode(1);
head.prev = newHead;
newHead.next = head;
head = newHead;
head.keys.add(key);
}
nodeMap.put(key, head);
} public void removeOld() {
if (head == null) return;
int old = 0;
for (int keyInorder : head.keys) {
old = keyInorder;
break;
}
head.keys.remove(old);
if (head.keys.size() == 0) remove(head);
valueMap.remove(old);
nodeMap.remove(old);
} public class ListNode {
int count;
ListNode prev, next;
LinkedHashSet<Integer> keys;
public ListNode(int freq) {
count = freq;
keys = new LinkedHashSet<Integer>();
prev = next = null;
}
}
} /**
* Your LFUCache object will be instantiated and called as such:
* LFUCache obj = new LFUCache(capacity);
* int param_1 = obj.get(key);
* obj.set(key,value);
*/

Summary of LinkedHashSet: http://www.programcreek.com/2013/03/hashset-vs-treeset-vs-linkedhashset/

A Set contains no duplicate elements. That is one of the major reasons to use a set. There are 3 commonly used implementations of Set: HashSet, TreeSet and LinkedHashSet. When and which to use is an important question. In brief, if you need a fast set, you should use HashSet; if you need a sorted set, then TreeSet should be used; if you need a set that can be store the insertion order, LinkedHashSet should be used.

1. Set Interface

Set interface extends Collection interface. In a set, no duplicates are allowed. Every element in a set must be unique. You can simply add elements to a set, and duplicates will be removed automatically.

Leetcode: LFU Cache     &&      Summary of various Sets: HashSet, TreeSet, LinkedHashSet

2. HashSet vs. TreeSet vs. LinkedHashSet

HashSet is Implemented using a hash table. Elements are not ordered. The add, remove, and contains methods have constant time complexity O(1).

TreeSet is implemented using a tree structure(red-black tree in algorithm book). The elements in a set are sorted, but the add, remove, and contains methods has time complexity of O(log (n)). It offers several methods to deal with the ordered set like first(), last(), headSet(), tailSet(), etc.

LinkedHashSet is between HashSet and TreeSet. It is implemented as a hash table with a linked list running through it, so it provides the order of insertion. The time complexity of basic methods is O(1).

3. TreeSet Example

TreeSet<Integer> tree = new TreeSet<Integer>();
tree.add(12);
tree.add(63);
tree.add(34);
tree.add(45);
 
Iterator<Integer> iterator = tree.iterator();
System.out.print("Tree set data: ");
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}

Output is sorted as follows:

Tree set data: 12 34 45 63 

4. HashSet Example

HashSet<Dog> dset = new HashSet<Dog>();
dset.add(new Dog(2));
dset.add(new Dog(1));
dset.add(new Dog(3));
dset.add(new Dog(5));
dset.add(new Dog(4));
Iterator<Dog> iterator = dset.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}

Output:

5 3 2 1 4

Note the order is not certain.

5. LinkedHashSet Example

LinkedHashSet<Dog> dset = new LinkedHashSet<Dog>();
dset.add(new Dog(2));
dset.add(new Dog(1));
dset.add(new Dog(3));
dset.add(new Dog(5));
dset.add(new Dog(4));
Iterator<Dog> iterator = dset.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}

The order of the output is certain and it is the insertion order:

2 1 3 5 4