哈希和队列专题—队列和 Hash 的特征 
通关进度
 
Hash 存储原理 
Hash 处理冲突的方法 
队列存储的基本特征 
使用链表实现队列 
 
参考资料
 
队列缓存、Hash 与缓存设计问题 
Hash 基础 
Hash 概念和基本特征
 
Hash(哈希)是一种将数据映射到固定长度的数字串(哈希值)的过程,通常用于数据的唯一标识、索引、加密等应用。哈希函数是执行这种映射的算法。
Hash 碰撞处理
 
链地址法(Separate Chaining):  该方法中,每个哈希桶都是一个链表。当多个键映射到同一个哈希码时,它们会被存储在同一个桶中的链表中。这样,相同哈希码的键值对可以共享同一个哈希桶,通过链表来存储。
开放寻址法(Open Addressing): 该方法中,当发生冲突时,程序会尝试寻找另一个空的哈希桶,而不是使用链表。 
线性探测(Linear Probing):  当发生哈希冲突时,线性探测会依次检查下一个哈希桶,直到找到一个空的桶或者遍历整个哈希表。这样,冲突的元素会被放置在找到的第一个空桶中。
 
二次探测(Quadratic Probing):  它使用二次方程来决定下一个探测的位置。这样可以更均匀地分布元素,避免线性探测的一些问题。
 
双重散列(Double Hashing):  它使用第二个哈希函数来决定下一个探测的位置。这可以提供更好的均匀性,减少冲突的概率。
 
 
队列基础 
队列概念和基本特征
 
队列是一种基本的数据结构,它遵循先进先出(FIFO,First In, First Out)的原则。在队列中,首先进入队列的元素将首先被移除,而最后进入队列的元素将最后被移除。
实现队列
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public  class  LinkQueue  {    private  ListNode front;     private  ListNode rear;     private  int  size;     public  LinkQueue ()  {         this .front = new  ListNode (0 );         this .rear = new  ListNode (0 );     }          public  void  push (int  value)  {         ListNode  newNode  =  new  ListNode (value);         ListNode  temp  =  front;         while  (temp.next != null ) {             temp = temp.next;         }         temp.next = newNode;         size++;     }          public  int  pull ()  {         if  (front.next == null ) {             throw  new  RuntimeException ("队列为空" );         }         ListNode  firstNode  =  front.next;         front.next = firstNode.next;         size--;         return  firstNode.val;     }          public  void  traverse ()  {         ListNode  temp  =  front.next;         while  (temp != null ) {             System.out.print(temp.val + "\t" );             temp = temp.next;         }     } } 
 
哈希和队列专题—队栈和 Hash 经典算法题 
通关进度
 
题目 
说明 
 
 
使用栈实现队列 
通关 
 
使用队列实现栈 
通关 
 
N 数之和 
通关 
 
 
 
使用栈实现队列  232. 用栈实现队列  
问题
 
【LeetCode 232】:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x):将元素 x 推到队列的末尾 
int pop():从队列的开头移除并返回元素 
int peek():返回队列开头的元素 
boolean empty():如果队列为空,返回 true ;否则,返回 false 
 
双栈
 
将一个栈作为输入栈,用于压入 push 传入的数据;另一个栈作为输出栈,用于 pop 和 peek 操作 
每次 pop 或 peek 时,若输出栈为空,则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public  class  MyQueue  {         private  Stack<Integer> inStack;          private  Stack<Integer> outStack;     public  MyQueue ()  {         inStack = new  Stack <>();         outStack = new  Stack <>();     }          public  void  push (int  x)  {         inStack.push(x);     }          public  int  pop ()  {                  if  (outStack.isEmpty()) {             inToOut();         }         return  outStack.pop();     }          public  int  peek ()  {         if  (outStack.isEmpty()) {             inToOut();         }         return  outStack.peek();     }          public  boolean  empty ()  {         return  inStack.isEmpty() && outStack.isEmpty();     }               public  void  inToOut ()  {         while  (!inStack.isEmpty()) {             outStack.push(inStack.pop());         }     } } 
 
使用队列实现栈  225. 用队列实现栈  
问题
 
【LeetCode 225】:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x):将元素 x 压入栈顶。 
int pop():移除并返回栈顶元素。 
int top():返回栈顶元素。 
boolean empty():如果栈是空的,返回 true ;否则,返回 false 。 
 
双队列
 
  
为满足栈的特性,即最后入栈的元素最先出栈,在使用队列实现栈时,应满足队列前端的元素是最后入栈的元素。可以使用两个队列实现栈的操作,其中 queue1 用于存储栈内的元素,queue2 作为入栈操作的辅助队列。
入栈操作时,首先将元素入队到 queue2,然后将 queue1 的全部元素依次出队并入队到 queue2,此时 queue2 的前端的元素即为新入栈的元素,再将 queue1 和 queue2 互换,则 queue1 的元素即为栈内的元素,queue1 的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保 queue1 的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除 queue1 的前端元素并返回即可,获得栈顶元素操作只需要获得 queue1 的前端元素并返回即可(不移除元素)。
由于 queue1 用于存储栈内的元素,判断栈是否为空时,只需要判断 queue1 是否为空即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public  class  MyStack  {         private  Queue<Integer> queue1;          private  Queue<Integer> queue2;     public  MyStack ()  {         queue1 = new  LinkedList <>();         queue2 = new  LinkedList <>();     }          public  void  push (int  x)  {                  queue2.offer(x);                           while  (!queue1.isEmpty()) {             queue2.offer(queue1.poll());         }                           Queue<Integer> temp = queue1;         queue1 = queue2;         queue2 = temp;     }               public  int  pop ()  {         return  queue1.poll();     }               public  int  top ()  {         return  queue1.peek();     }               public  boolean  empty ()  {         return  queue1.isEmpty();     } } 
 
单队列
 
使用一个队列时,为了满足栈的特性,即最后入栈的元素最先出栈,同样需要满足队列前端的元素是最后入栈的元素。
入栈操作时,首先获得入栈前的元素个数 nnn,然后将元素入队到队列,再将队列中的前 nnn 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。
由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除队列的前端元素并返回即可,获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。
由于队列用于存储栈内的元素,判断栈是否为空时,只需要判断队列是否为空即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class  MyStack  {    Queue<Integer> queue;          public  MyStack ()  {         queue = new  LinkedList <Integer>();     }               public  void  push (int  x)  {         int  n  =  queue.size();         queue.offer(x);         for  (int  i  =  0 ; i < n; i++) {             queue.offer(queue.poll());         }     }               public  int  pop ()  {         return  queue.poll();     }               public  int  top ()  {         return  queue.peek();     }               public  boolean  empty ()  {         return  queue.isEmpty();     } } 
 
N 数之和 
类似问题
 
两数之和
 
 1. 两数之和  
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
推荐回答 
三数之和
 
 LCR 007. 三数之和  
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
推荐回答 
哈希和队列专题—缓存设计与实现 
通关进度
 
题目 
说明 
 
 
LRU 缓存 
通关 
 
LFU 缓存 
通关 
 
 
 
LRU 缓存(重点)  146. LRU 缓存  
问题
 
【LeetCode 146】:请你设计并实现一个满足 LRU (最近最少使用)  缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity):以 正整数  作为容量 capacity 初始化 LRU 缓存 
int get(int key):如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 
void put(int key, int value):如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出  最久未使用的关键字。 函数 get 和 put 必须以 $O(1)$ 的平均时间复杂度运行。 
 
实例
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1);    // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2);    // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1);    // 返回 -1 (未找到) lRUCache.get(3);    // 返回 3 lRUCache.get(4);    // 返回 4 
 
哈希表 + 双向链表
 
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用 的 
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置 
 
首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 $O(1)$ 的时间内完成 get 或者 put 操作。具体的方法如下:
对于 get 操作,首先判断 key 是否存在:
如果 key 不存在,则返回 −1  
如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值 
 
对于 put 操作,首先判断 key 是否存在:
如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项; 
如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部  
 
上述各项操作中,访问哈希表的时间复杂度为 $O(1)$,在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 $O(1)$。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 $O(1)$ 时间内完成。
Tips
 
在双向链表的实现中,使用一个伪头部 (dummy head)和伪尾部 (dummy tail)标记界限,这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 public  class  LRUCache  {         class  DLinkedNode  {         private  int  key;         private  int  value;         private  DLinkedNode prev;         private  DLinkedNode next;         public  DLinkedNode ()  {         }         public  DLinkedNode (int  _key, int  _value)  {             this .key = _key;             this .value = _value;         }     }          private  Map<Integer, DLinkedNode> cache = new  HashMap <>();          private  int  size;          private  int  capacity;     private  DLinkedNode head, tail;     public  LRUCache (int  capacity)  {         this .size = 0 ;         this .capacity = capacity;                  head = new  DLinkedNode ();         tail = new  DLinkedNode ();         head.next = tail;         tail.prev = head;     }     public  int  get (int  key)  {         DLinkedNode  node  =  cache.get(key);         if  (node == null ) {             return  -1 ;         }                  moveToDLHead(node);         return  node.value;     }     public  void  put (int  key, int  value)  {         DLinkedNode  node  =  cache.get(key);         if  (node == null ) {                          DLinkedNode  newNode  =  new  DLinkedNode (key, value);                          cache.put(key, newNode);                          addToDLHead(newNode);             ++size;             if  (size > capacity) {                                  DLinkedNode  tail  =  removeDLTail();                                  cache.remove(tail.key);                 --size;             }         } else  {                          node.value = value;             moveToDLHead(node);         }     }          private  void  addToDLHead (DLinkedNode node)  {         node.prev = head;         node.next = head.next;         head.next.prev = node;         head.next = node;     }          private  void  removeDLNode (DLinkedNode node)  {         node.prev.next = node.next;         node.next.prev = node.prev;     }               private  DLinkedNode removeDLTail ()  {         DLinkedNode  res  =  tail.prev;         removeDLNode(res);         return  res;     }          public  void  moveToDLHead (DLinkedNode node)  {         removeDLNode(node);         addToDLHead(node);     } } 
 
LFU 缓存(可选)  460. LFU 缓存  
问题
 
【LeetCode 460】:请你为 最不经常使用(LFU) 缓存算法设计并实现数据结构。
实现 LFUCache 类:
LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象 
int get(int key) - 如果键 key 存在于缓存中,则获取键的值,否则返回 -1   
void put(int key, int value) - 如果键 key 已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量 capacity 时,则应该在插入新项之前,移除最不经常使用的项。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用  的键。 
 
为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器  。使用计数最小的键是最久未使用的键。
当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。
函数 get 和 put 必须以 $O(1)$ 的平均时间复杂度运行。
实例
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 输入: ["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]] 输出: [null, null, null, 1, null, -1, 3, null, -1, 3, 4] 解释: // cnt(x) = 键 x 的使用计数 // cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的) LFUCache lfu = new LFUCache(2); lfu.put(1, 1);   // cache=[1,_], cnt(1)=1 lfu.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1 lfu.get(1);      // 返回 1                  // cache=[1,2], cnt(2)=1, cnt(1)=2 lfu.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小                  // cache=[3,1], cnt(3)=1, cnt(1)=2 lfu.get(2);      // 返回 -1(未找到) lfu.get(3);      // 返回 3                  // cache=[3,1], cnt(3)=2, cnt(1)=2 lfu.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用                  // cache=[4,3], cnt(4)=1, cnt(3)=2 lfu.get(1);      // 返回 -1(未找到) lfu.get(3);      // 返回 3                  // cache=[3,4], cnt(4)=1, cnt(3)=3 lfu.get(4);      // 返回 4                  // cache=[3,4], cnt(4)=2, cnt(3)=3 
 
哈希表 + 平衡二叉树
 
用哈希表 key_table 以键 key 为索引存储缓存,建立一个平衡二叉树 S 来保持缓存根据 (cnt,time) 双关键字,其中 cnt 表示缓存使用的频率,time 表示缓存的使用时间,key 和 value 表示缓存的键值。
对于 get(key) 操作,我们只要查看一下哈希表 key_table 是否有 key 这个键即可,有的话需要同时更新哈希表和集合中该缓存的使用频率以及使用时间,否则返回 -1。 
 
对于 put(key, value) 操作,首先需要查看 key_table 中是否已有对应的键值。如果有的话操作基本等同于 get(key),不同的是需要更新缓存的 value 值。如果没有的话相当于是新插入一个缓存,这时候需要先查看是否达到缓存容量 capacity,如果达到了的话,需要删除最近最少使用 的缓存,即平衡二叉树中最左边的结点,同时删除 key_table 中对应的索引,最后向 key_table 和 S 插入新的缓存信息即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 public  class  LFUCache  {         private  int  capacity, time;          private  Map<Integer, Node> key_table;          private  TreeSet<Node> S;     public  LFUCache (int  capacity)  {         this .capacity = capacity;         this .time = 0 ;         key_table = new  HashMap <Integer, Node>();         S = new  TreeSet <Node>();     }     public  int  get (int  key)  {         if  (capacity == 0 ) {             return  -1 ;         }                  if  (!key_table.containsKey(key)) {             return  -1 ;         }                  Node  cache  =  key_table.get(key);                  S.remove(cache);                  cache.cnt += 1 ;         cache.time = ++time;                  S.add(cache);         key_table.put(key, cache);         return  cache.value;     }     public  void  put (int  key, int  value)  {         if  (capacity == 0 ) {             return ;         }         if  (!key_table.containsKey(key)) {                          if  (key_table.size() == capacity) {                                  key_table.remove(S.first().key);                 S.remove(S.first());             }                          Node  cache  =  new  Node (1 , ++time, key, value);                          key_table.put(key, cache);             S.add(cache);         } else  {                          Node  cache  =  key_table.get(key);             S.remove(cache);             cache.cnt += 1 ;             cache.time = ++time;             cache.value = value;             S.add(cache);             key_table.put(key, cache);         }     } } class  Node  implements  Comparable <Node> {                   int  cnt, time, key, value;     Node(int  cnt, int  time, int  key, int  value) {         this .cnt = cnt;         this .time = time;         this .key = key;         this .value = value;     }     public  boolean  equals (Object anObject)  {         if  (this  == anObject) {             return  true ;         }         if  (anObject instanceof  Node) {             Node  rhs  =  (Node) anObject;             return  this .cnt == rhs.cnt && this .time == rhs.time;         }         return  false ;     }     public  int  compareTo (Node rhs)  {         return  cnt == rhs.cnt ? time - rhs.time : cnt - rhs.cnt;     }     public  int  hashCode ()  {         return  cnt * 1000000007  + time;     } }