146-LRU缓存机制

image.png
代码:

  1. package com.bxk.leetcode.design;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. import java.util.Set;
  5. public class LRUCache_146 {
  6. /**
  7. *
  8. * 容量限制为capacity的Map,删除最近最少未使用的元素
  9. *
  10. * put和get操作
  11. * 随机读:hash数据结构
  12. * 支持删除和插入:链表结构,node(key,value)
  13. *
  14. * get逻辑:
  15. * 1、key存在,直接返回value,并将链表中该node移到头部
  16. * 2、key不存在,返回-1
  17. *
  18. * put逻辑:
  19. * 1、key不存在,插入数据到链表头部,如果链表大小超过capacity容量限制,删除链表尾部元素
  20. * 2、key存在,更新hash中对应key的value,并将链表中该node移到头部
  21. */
  22. // 定义双向链表结构
  23. class DLinkedNode {
  24. int key;
  25. int value;
  26. DLinkedNode prev;
  27. DLinkedNode next;
  28. public DLinkedNode(){}
  29. public DLinkedNode(int key, int value) {
  30. this.key = key;
  31. this.value = value;
  32. }
  33. }
  34. // 定义hash结构
  35. Map<Integer, DLinkedNode> cache = new HashMap<>();
  36. int size;
  37. int capacity;
  38. DLinkedNode head, tail;
  39. public LRUCache_146() {}
  40. public LRUCache_146(int capacity) {
  41. this.capacity = capacity;
  42. head = new DLinkedNode();
  43. tail = new DLinkedNode();
  44. head.next = tail;
  45. tail.prev = head;
  46. }
  47. public int get(int key) {
  48. DLinkedNode node = cache.get(key);
  49. if (node == null) {
  50. return -1;
  51. }
  52. moveToHead(node);
  53. return node.value;
  54. }
  55. public void put(int key, int value) {
  56. DLinkedNode node = cache.get(key);
  57. // key 不存在
  58. if (node == null){
  59. DLinkedNode newNode = new DLinkedNode(key,value);
  60. cache.put(key, newNode);
  61. addToHead(newNode);
  62. size++;
  63. if (size > capacity) {
  64. // 注意先后顺序,先删除hash中链表的最后一个元素,再从链表中移除
  65. cache.remove(tail.prev.key);
  66. removeTail();
  67. --size;
  68. }
  69. } else {
  70. // key 存在
  71. node.value = value;
  72. moveToHead(node);
  73. }
  74. }
  75. /**
  76. * get存在或put存在时,将node移动到链表头部
  77. * @param node
  78. */
  79. public void moveToHead(DLinkedNode node) {
  80. // 删除node
  81. node.prev.next = node.next;
  82. node.next.prev = node.prev;
  83. addToHead(node);
  84. }
  85. /**
  86. * 更新node到链表头部
  87. * @param node
  88. */
  89. public void addToHead(DLinkedNode node) {
  90. // 添加到 head 头部
  91. node.prev = head;
  92. node.next = head.next;
  93. head.next.prev = node;
  94. head.next = node;
  95. }
  96. /**
  97. * 删除链表尾部元素
  98. */
  99. public void removeTail() {
  100. tail.prev.prev.next = tail;
  101. tail.prev = tail.prev.prev;
  102. }
  103. public void printDLinked(DLinkedNode head) {
  104. System.out.print("linked out: ");
  105. while (head.next != tail) {
  106. System.out.print(head.next.value + " ");
  107. head = head.next;
  108. }
  109. System.out.println();
  110. }
  111. public void printCache() {
  112. Set<Map.Entry<Integer, DLinkedNode>> set = cache.entrySet();
  113. System.out.print("cache out: ");
  114. for (Map.Entry entry : set) {
  115. System.out.print(entry.getKey() + " ");
  116. }
  117. System.out.println();
  118. }
  119. public void out() {
  120. LRUCache_146 lRUCache = new LRUCache_146(2);
  121. lRUCache.put(1, 1); // 缓存是 {1=1}
  122. lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
  123. lRUCache.printDLinked(lRUCache.head); lRUCache.printCache();
  124. System.out.println(lRUCache.get(1)); // 返回 1
  125. lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
  126. lRUCache.printDLinked(lRUCache.head); lRUCache.printCache();
  127. System.out.println(lRUCache.get(2)); // 返回 -1 (未找到)
  128. lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
  129. lRUCache.printDLinked(lRUCache.head); lRUCache.printCache();
  130. /*
  131. lRUCache.get(1); // 返回 -1 (未找到)
  132. lRUCache.get(3); // 返回 3
  133. lRUCache.get(4); // 返回 4*/
  134. }
  135. public static void main(String[] args) {
  136. LRUCache_146 lruCache = new LRUCache_146();
  137. lruCache.out();
  138. }
  139. }

460-LFU缓存

image.png
image.png
题意分析:

  • LRU是超容量删除最近最少使用的元素,LFU稍微复杂一点,在LRU基础上添加元素访问频次,先根据频次删除元素,再根据最近最少使用删除元素。

解题思路:

  • 思路一:借助 cache HashMap和双向链表。双向链表用于元素访问频次的节点。
    • 时间复杂度:O(capacity),每次 get/put 都要更新元素的频次(访问链表位置)
    • 空间复杂度:O(capacity) ,Map需要存储所有节点信息
  • 思路二:借助 cache HashMap和频次 freqMap HashMap。freqMap 存储 > 信息,更新 freqMap 只需要执行 Set 的 remove 和 add 操作。
    • 时间复杂度:O(1)
    • 空间复杂度:O(capacity)

注意点:

  • node 属性不一定需要用数据结构存储,可以采用 class Node 方式存储 node 属性。
  • LinkedHashSet 添加元素是有序的,执行 insert/remove 的时间复杂度都为 O(1)。LinkedHashSet 也可以用 LinkedList 实现。

扩展:
代码:

  1. package com.bxk.leetcode.design;
  2. import java.util.HashMap;
  3. import java.util.LinkedHashSet;
  4. import java.util.Map;
  5. /**
  6. *
  7. * https://cloud.tencent.com/developer/article/1645636
  8. *
  9. * 设计思路:(先思考设计变量数据结构,再开始具体设计逻辑实现)
  10. * 1、存储数据用于 put 和 get 操作。put => hashMap. 容量超过限制就需要溢出 最近最少使用的
  11. * 2、双向链表 cache(容量:capacity)维护最后一次使用的顺序(多 move 操作)
  12. * 3、数组 cnt[capacity] 统计每个 key 的访问次数(复杂度高:每次 put 超容量删除时都需要比较所有 key 访问次数)
  13. *
  14. *
  15. */
  16. public class LFUCache_460 {
  17. public static void main(String[] args) {
  18. LFUCache1 cache = new LFUCache1(2);
  19. cache.put(1, 1);
  20. cache.put(2, 2);
  21. // 返回 1
  22. System.out.println(cache.get(1));
  23. cache.put(3, 3); // 去除 key 2
  24. // 返回 -1 (未找到key 2)
  25. System.out.println(cache.get(2));
  26. // 返回 3
  27. System.out.println(cache.get(3));
  28. cache.put(4, 4); // 去除 key 1
  29. // 返回 -1 (未找到 key 1)
  30. System.out.println(cache.get(1));
  31. // 返回 3
  32. System.out.println(cache.get(3));
  33. // 返回 4
  34. System.out.println(cache.get(4));
  35. System.out.println("----------");
  36. LFUCache2 cache2 = new LFUCache2(2);
  37. cache2.put(1, 1);
  38. cache2.put(2, 2);
  39. // 返回 1
  40. System.out.println(cache2.get(1));
  41. cache2.put(3, 3); // 去除 key 2
  42. // 返回 -1 (未找到key 2)
  43. System.out.println(cache2.get(2));
  44. // 返回 3
  45. System.out.println(cache2.get(3));
  46. cache2.put(4, 4); // 去除 key 1
  47. // 返回 -1 (未找到 key 1)
  48. System.out.println(cache2.get(1));
  49. // 返回 3
  50. System.out.println(cache2.get(3));
  51. // 返回 4
  52. System.out.println(cache2.get(4));
  53. }
  54. }
  55. /**
  56. * 借助自定义的双向链表 + HashMap
  57. *
  58. * 时间复杂度:O(1)
  59. * 空间复杂度:O(capacity)
  60. *
  61. * 设计启发:
  62. * 1、Node 对象优先用 Class 存储,比如 key/value/freq,数据结构以Node类型作为存储对象
  63. */
  64. class LFUCache1 {
  65. private Map<Integer, DLinkedNode> cache;
  66. int size;
  67. int capacity;
  68. DLinkedNode head, tail;
  69. public LFUCache1(int capacity) {
  70. this.capacity = capacity;
  71. this.cache = new HashMap<>();
  72. head = new DLinkedNode();
  73. tail = new DLinkedNode();
  74. head.next = tail;
  75. tail.prev = head;
  76. }
  77. public int get(int key) {
  78. DLinkedNode node = cache.get(key);
  79. if (node == null) return -1;
  80. node.freq++;
  81. // 移动到频次排序位置
  82. moveToPosition(node);
  83. return node.value;
  84. }
  85. public void put(int key, int value) {
  86. if (capacity == 0) return;;
  87. DLinkedNode node = cache.get(key);
  88. if (node != null) {
  89. node.value = value;
  90. node.freq++;
  91. moveToPosition(node);
  92. } else {
  93. // 如果容量满了
  94. if (size == capacity) {
  95. cache.remove(head.next.key);
  96. removeNode(head.next);
  97. size--;
  98. }
  99. DLinkedNode newNode = new DLinkedNode(key, value);
  100. addNode(newNode);
  101. cache.put(key, newNode);
  102. size++;
  103. }
  104. }
  105. /**
  106. * 添加节点到头部,然后根据 freq 移动到指定位置
  107. * @param node
  108. */
  109. private void addNode(DLinkedNode node) {
  110. node.prev = head;
  111. node.next = head.next;
  112. head.next.prev = node;
  113. head.next = node;
  114. moveToPosition(node);
  115. }
  116. /**
  117. * 只要当前 node 的 freq 大于等于后边的节点,就一直向后找,知道找到第一个比 node freq 大的节点,插入。
  118. * @param node
  119. */
  120. private void moveToPosition(DLinkedNode node) {
  121. DLinkedNode nextNode = node.next;
  122. // 链表中删除当前节点
  123. removeNode(node);
  124. // 遍历到符合要求的节点
  125. while (node.freq >= nextNode.freq && nextNode != tail) {
  126. nextNode = nextNode.next;
  127. }
  128. // 把当前节点插入到 nextNode 前面
  129. node.prev = nextNode.prev;
  130. node.next = nextNode;
  131. nextNode.prev.next = node;
  132. nextNode.prev = node;
  133. }
  134. /**
  135. * 移除元素
  136. * @param node
  137. */
  138. private void removeNode(DLinkedNode node) {
  139. node.prev.next = node.next;
  140. node.next.prev = node.prev;
  141. }
  142. // 定义双向链表结构
  143. class DLinkedNode {
  144. int key;
  145. int value;
  146. int freq = 1;
  147. DLinkedNode prev;
  148. DLinkedNode next;
  149. public DLinkedNode(){}
  150. public DLinkedNode(int key, int value) {
  151. this.key = key;
  152. this.value = value;
  153. }
  154. }
  155. }
  156. class LFUCache2 {
  157. private Map<Integer, DLinkedNode> cache;
  158. int size;
  159. int capacity;
  160. private Map<Integer, LinkedHashSet<DLinkedNode>> freqMap;
  161. // 记录最次频次
  162. int minFreq;
  163. public LFUCache2(int capacity) {
  164. this.capacity = capacity;
  165. cache = new HashMap<>();
  166. freqMap = new HashMap<>();
  167. }
  168. public int get(int key) {
  169. DLinkedNode node = cache.get(key);
  170. if (node == null) return -1;
  171. // 更新访问频次
  172. freqUpdate(node);
  173. return node.value;
  174. }
  175. public void put(int key, int value) {
  176. if (capacity == 0) return;
  177. DLinkedNode node = cache.get(key);
  178. if (node != null) {
  179. node.value = value;
  180. freqUpdate(node);
  181. } else {
  182. if (size == capacity) {
  183. DLinkedNode delNode = freqRemoveNode();
  184. cache.remove(delNode.key);
  185. size--;
  186. }
  187. DLinkedNode newNode = new DLinkedNode(key, value);
  188. cache.put(key, newNode);
  189. freqAddNode(newNode);
  190. size++;
  191. }
  192. }
  193. /**
  194. * freqMap 超容量删除元素
  195. *
  196. * 逻辑:
  197. * 1、删除频次最小的
  198. * 2、删除相同频次最久未被访问的
  199. * @return
  200. */
  201. private DLinkedNode freqRemoveNode() {
  202. LinkedHashSet<DLinkedNode> set = freqMap.get(minFreq);
  203. // 迭代找到第一个元素,就是最久未访问的元素,删除
  204. DLinkedNode node = set.iterator().next();
  205. set.remove(node);
  206. // 如果当前node频次等于最小频次,并且移除元素后,set为空,则 min+1
  207. if (node.freq == minFreq && set.size() == 0) {
  208. minFreq++;
  209. }
  210. return node;
  211. }
  212. /**
  213. * freqMap 新增元素
  214. *
  215. * 逻辑:
  216. * 1、freq=1 频次是否存在,存在直接获取并添加到set,不存在创建新的freq=1的set
  217. * @param node
  218. */
  219. private void freqAddNode(DLinkedNode node) {
  220. LinkedHashSet set = freqMap.get(1);
  221. if (set == null) {
  222. set = new LinkedHashSet();
  223. freqMap.put(1, set);
  224. }
  225. set.add(node);
  226. // 更新最小频次
  227. minFreq = 1;
  228. }
  229. /**
  230. * 更新 freqMap 的频次
  231. *
  232. * 逻辑:
  233. * 1、从原来freq表中删除node
  234. * 2、添加到新的频次表
  235. * @param node
  236. */
  237. private void freqUpdate(DLinkedNode node) {
  238. LinkedHashSet<DLinkedNode> set = freqMap.get(node.freq);
  239. // 从原来频次表中删除当前node
  240. if (set != null) {
  241. set.remove(node);
  242. }
  243. if (node.freq == minFreq && set.size() == 0) {
  244. minFreq = node.freq + 1;
  245. }
  246. // 添加到新的频次对应的链表
  247. node.freq++;
  248. LinkedHashSet<DLinkedNode> newSet = freqMap.get(node.freq);
  249. if (newSet == null) {
  250. newSet = new LinkedHashSet<>();
  251. freqMap.put(node.freq, newSet);
  252. }
  253. newSet.add(node);
  254. }
  255. // 定义双向链表结构
  256. class DLinkedNode {
  257. int key;
  258. int value;
  259. int freq = 1;
  260. public DLinkedNode(){}
  261. public DLinkedNode(int key, int value) {
  262. this.key = key;
  263. this.value = value;
  264. }
  265. }
  266. }

380-O(1)时间插入/删除和获取随机元素

image.png
题意分析:

  • 选取合适的数据结构,将常数时间复杂度内完成元素的插入、删除和获取。

解题思路:

  • 数据结构选取(动态数组 ArrayList 和 哈希表 HashMap)。熟悉各种数据结构对于插入/删除/获取等等操作的时间复杂度,比如数组读取高效,链表插入/删除高效。HasMap 对于读取索引位置数据高效。
  • 插入操作和获取元素实现比较简单,最复杂的是删除元素,需要调整元素在 List 和 Map 中的位置。

注意点:

  • 熟悉数据结构不常用的操作。
    • ArrayList 如何交换两个位置元素。Collections.swap(list, fromIdx, toIdx)
    • ArrayList 如何更新某个索引位置元素。list.set(idx, data)
    • ArrayList 如何在O(1)元素。list.remove(list.size() - 1)
    • HashMap 如何删除某个元素。map.remove(key)
    • 调试。如何简单打印 ArrayList 和 HashMap。

代码:

  1. package com.bxk.leetcode.design;
  2. import java.util.ArrayList;
  3. import java.util.HashMap;
  4. import java.util.List;
  5. import java.util.Map;
  6. import java.util.Random;
  7. /**
  8. *
  9. 设计一个支持在平均 时间复杂度 O(1) 下,执行以下操作的数据结构。
  10. insert(val):当元素 val 不存在时,向集合中插入该项。
  11. remove(val):元素 val 存在时,从集合中移除该项。
  12. getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
  13. */
  14. public class RandomizedSet_380 {
  15. /**
  16. * 解题思路:
  17. * 1、确定数据结构选用。数组。ArrayList
  18. * 2、数组长度如何确定,以及如何扩容。(ArrayList 动态数组)
  19. * 3、是否需要辅助数据结构。哈希表
  20. */
  21. List<Integer> list;
  22. Map<Integer, Integer> map;
  23. /** Initialize your data structure here. */
  24. public RandomizedSet_380() {
  25. list = new ArrayList<>();
  26. map = new HashMap<>();
  27. }
  28. /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
  29. public boolean insert(int val) {
  30. // 如果集合中包含该 val,则 insert 失败
  31. if (map.containsKey(val)) {
  32. return false;
  33. }
  34. map.put(val, list.size());
  35. list.add(val);
  36. // System.out.println("list = " + list.toString());
  37. // System.out.println("key = " + map.keySet().toString() + "; valus = " + map.values().toString());
  38. return true;
  39. }
  40. /** Removes a value from the set. Returns true if the set contained the specified element. */
  41. public boolean remove(int val) {
  42. // 如果集合中不包含 val,则remove失败
  43. if (!map.containsKey(val)) {
  44. return false;
  45. }
  46. // 获取集合的最后一个元素
  47. int lastEle = list.get(list.size() - 1);
  48. // 获取哈希表中 val 对应集合的索引
  49. int idx = map.get(val);
  50. // 交换后更新末尾元素在集合中的索引
  51. map.put(lastEle, idx);
  52. map.remove(val);
  53. // 交换 idx 元素和集合中末尾元素,并删除末尾元素。
  54. list.set(idx, lastEle); // 等效Collections.swap(list, idx, list.size() - 1);
  55. list.remove(list.size() - 1);
  56. // System.out.println("list = " + list.toString());
  57. // System.out.println("key = " + map.keySet().toString() + "; valus = " + map.values().toString());
  58. return true;
  59. }
  60. /** Get a random element from the set. */
  61. public int getRandom() {
  62. int rand = (new Random()).nextInt(list.size());
  63. return list.get(rand % list.size());
  64. }
  65. public static void main(String[] args) {
  66. RandomizedSet_380 obj = new RandomizedSet_380();
  67. System.out.println("-------- insert operation ---------");
  68. System.out.println("insert ret = " + obj.insert(1));
  69. System.out.println("insert ret = " + obj.insert(2));
  70. System.out.println("insert ret = " + obj.insert(3));
  71. System.out.println("insert ret = " + obj.insert(3));
  72. System.out.println("-------- get random ---------");
  73. for (int i = 0; i < 5; i++) {
  74. System.out.println("get random= " + obj.getRandom());
  75. }
  76. System.out.println("-------- remove operation ---------");
  77. System.out.println("list = " + obj.list.toString());
  78. System.out.println("key = " + obj.map.keySet().toString() + "; valus = " + obj.map.values().toString());
  79. System.out.println("remove ret = " + obj.remove(1));
  80. System.out.println("remove ret = " + obj.remove(1));
  81. System.out.println("remove ret = " + obj.remove(2));
  82. }
  83. }

381-O(1)时间插入、删除和获取随机元素-允许重复

image.png
题意分析:

  • 和 380 题类似,增加了允许元素重复的条件,这要 HashMap 就需要重新设计,保存某个 value 的在 numsList 中的所有索引。

解题思路:

  • 元素信息用 ArrayList numsList 存储。
  • 元素位置信息用 HashMap> valueToIdxMap 存储,记录某个 value 的所有 idx 信息,remove 时随机删除一个。

注意点:

  • remove 时如果是删除 numsList 末尾元素,修改该元素的 idx 信息要判断是否是删除最后一个元素。

扩展:
代码:

  1. /**
  2. * 思路:
  3. * 1、get 操作需要 ArrayList 或者 HashMap 才能实现 O(1) 时间复杂度。(只有 value,所以这里选取 ArrayList)
  4. * 2、remove 操作需要知道要删除的元素在 ArrayList 中的位置, 所以需要辅助 HashMap 结构,由于这里元素可以重复,
  5. * 因此同一个 value 在 HashMap 中的值可能存在多个。
  6. */
  7. public class RandomizedCollection_381 {
  8. List<Integer> numsList;
  9. Map<Integer, HashSet<Integer>> valueToIdxMap;
  10. public RandomizedCollection_381() {
  11. numsList = new ArrayList<>();
  12. valueToIdxMap = new HashMap<>();
  13. }
  14. /**
  15. * insert 逻辑:
  16. * 1、更新 numsList 集合
  17. * 2、更新 valueToIdxMap 集合
  18. * 3、返回 valueToIdxMap 的 value(HashSet).size 是否等于 1 (即是否是第一次添加)
  19. * @param val
  20. * @return
  21. */
  22. public boolean insert(int val) {
  23. // numsList 中添加元素
  24. numsList.add(val);
  25. // HashMap 中添加元素对应索引
  26. HashSet<Integer> set = valueToIdxMap.getOrDefault(val, new HashSet<>());
  27. set.add(numsList.size()-1);
  28. valueToIdxMap.put(val, set);
  29. return set.size() == 1;
  30. }
  31. /**
  32. * remove 逻辑:
  33. * 1、元素不存在则直接返回 false。
  34. * 2、由于 numsList 删除元素必须要知道元素 idx 才能实现 O(1) 的时间复杂度。
  35. * 所以先得访问 valueToIdxMap 获取元素 value 对应在 numsList 的 idx,再值具体删除操作
  36. * @param val
  37. * @return
  38. */
  39. public boolean remove(int val) {
  40. if (!valueToIdxMap.containsKey(val))
  41. return false;
  42. // 获取 val 对应的 idx 集合,并拿到要删除的元素位置 idx
  43. HashSet<Integer> set = valueToIdxMap.get(val);
  44. int idx = set.iterator().next();
  45. // 获取 numList 最后一个元素,用作与 idx 位置元素交换,然后删除末尾元素
  46. int lastEle = numsList.get(numsList.size() - 1);
  47. // 交换后,删除 idx 元素在 valueToIdxMap 信息,并更新 lastEle 元素在 valueTOIdxMap 信息
  48. set.remove(idx);
  49. valueToIdxMap.get(lastEle).remove(numsList.size() - 1);
  50. // 如果不是删除最后一个元素,则更新 lastEle 元素在 valueToIdxMap 的位置
  51. if (idx < numsList.size() - 1) {
  52. valueToIdxMap.get(lastEle).add(idx);
  53. }
  54. // 删除元素后 idx 的 Set 为空,则删除 valueToIdxMap 中 key 为 idx 的对象
  55. if (set.size() == 0) {
  56. valueToIdxMap.remove(val);
  57. }
  58. // 交换 numList 中 idx 元素和末尾元素
  59. numsList.set(idx, lastEle);
  60. numsList.remove(numsList.size() - 1);
  61. return true;
  62. }
  63. public int getRandom() {
  64. return numsList.get((int)(Math.random() * numsList.size()));
  65. }
  66. public static void main(String[] args) {
  67. // 初始化一个空的集合。
  68. RandomizedCollection_381 collection = new RandomizedCollection_381();
  69. // 向集合中插入 1 。返回 true 表示集合不包含 1 。
  70. collection.insert(1);
  71. // 向集合中插入另一个 1 。返回 false 表示集合包含 1 。集合现在包含 [1,1] 。
  72. collection.insert(1);
  73. // 向集合中插入 2 ,返回 true 。集合现在包含 [1,1,2] 。
  74. collection.insert(2);
  75. // getRandom 应当有 2/3 的概率返回 1 ,1/3 的概率返回 2 。
  76. collection.getRandom();
  77. // 从集合中删除 1 ,返回 true 。集合现在包含 [1,2] 。
  78. collection.remove(1);
  79. // getRandom 应有相同概率返回 1 和 2 。
  80. collection.getRandom();
  81. }
  82. }

1603-设计停车系统

image.png
题意分析:

  • 判断空车位与停车的容量,不同大小车位停放不同汽车类型。

解题思路:

  • if判断或者switch语句的使用。

注意点:
扩展:

  • 如果考虑大容量车位可以停放满足它需求的汽车,该如何解决。比如 big 车位可以执行addCar(3)/addCar(2)/addCar(1),如何设计能够停放最多的车。
  • 如果考虑到有 removeCar 操作,又该如何设计?

代码:

  1. public class ParkingSystem {
  2. private int big;
  3. private int medium;
  4. private int small;
  5. public ParkingSystem(int big, int medium, int small) {
  6. this.big = big;
  7. this.medium = medium;
  8. this.small = small;
  9. }
  10. public boolean addCar(int carType) {
  11. if (!(carType == 1 || carType == 2 || carType == 3))
  12. return false;
  13. switch (carType) {
  14. case 1:
  15. if (big >= 1) {
  16. big--;
  17. return true;
  18. }
  19. break;
  20. case 2:
  21. if (medium >= 1) {
  22. medium--;
  23. return true;
  24. }
  25. break;
  26. case 3:
  27. if (small >= 1) {
  28. small--;
  29. return true;
  30. }
  31. break;
  32. }
  33. return false;
  34. }
  35. }

1472-设计浏览器历史记录

image.png
题意分析:

  • 实现浏览器的访问/前进/后退功能。

解题思路:

  • 双向链表。既要走 next 也要走 prev,双向链表是最好的选择。

注意点:

  • 访问过程中新 visit 节点,需要将其后续节点置空,因此此时从当前 page 只能后退不能前进。即代码中 addNode 方法 node.next = tail 操作。

扩展:
代码:

  1. package com.bxk.leetcode.design;
  2. /**
  3. * 设计思路:
  4. * 1、双向链表记录浏览器访问记录,需要设计链表 addNode 操作。
  5. * 2、back 操作至多 steps 步,如果 back 到链表 head 节点则返回 head。
  6. * 3、forward 操作至多 steps 步,如果 forward 到链表 tail 节点则返回 tail。
  7. */
  8. public class BrowserHistory {
  9. // 记录单标签浏览器page页
  10. private String homepage;
  11. // 设计双向链表
  12. private DLinkedNode head;
  13. private DLinkedNode tail;
  14. // 记录链表当前访问节点
  15. private DLinkedNode curr;
  16. public BrowserHistory(String homepage) {
  17. // homepage 添加到链表中
  18. this.homepage = homepage;
  19. // 增加哨兵初始化双向链表
  20. head = new DLinkedNode();
  21. tail = new DLinkedNode();
  22. head.next = tail;
  23. tail.prev = head;
  24. curr = head;
  25. visit(homepage);
  26. }
  27. /**
  28. * 设计逻辑:
  29. * 1、新访问 page 后先将 page 添加到链表中
  30. * 2、并将 curr 当前访问页面指向新访问的页面
  31. * @param url
  32. */
  33. public void visit(String url) {
  34. DLinkedNode visitNode = new DLinkedNode(url);
  35. addNode(visitNode);
  36. curr = visitNode;
  37. }
  38. /**
  39. * 设计逻辑:
  40. * 1、向链表 head 节点方向开始走,遇到 head 节点直接结束
  41. * @param steps
  42. * @return
  43. */
  44. public String back(int steps) {
  45. while (steps > 0) {
  46. if (curr.prev == head) {
  47. break;
  48. } else {
  49. curr = curr.prev;
  50. }
  51. steps--;
  52. }
  53. return curr.value;
  54. }
  55. /**
  56. * 设计逻辑:
  57. * 1、向链表 tail 节点方向开始走,遇到 tail 节点直接结束
  58. * steps = 3
  59. * a-b-c(cur)-d-e-f-g
  60. * @param steps
  61. * @return
  62. */
  63. public String forward(int steps) {
  64. while (steps > 0) {
  65. if (curr.next == tail) {
  66. break;
  67. } else {
  68. curr = curr.next;
  69. }
  70. steps--;
  71. }
  72. return curr.value;
  73. }
  74. /**
  75. * visit 时将 page 添加到链表(尾插法)
  76. * @param node
  77. */
  78. public void addNode(DLinkedNode node) {
  79. node.prev = curr;
  80. node.next = curr.next;
  81. curr.next.prev = node;
  82. curr.next = node;
  83. // head->a->b(curr)->c->tail,此时访问 d 时,c 应该被删除掉
  84. node.next = tail;
  85. }
  86. class DLinkedNode {
  87. String value;
  88. DLinkedNode prev;
  89. DLinkedNode next;
  90. public DLinkedNode() {}
  91. public DLinkedNode(String value) {
  92. this.value = value;
  93. }
  94. }
  95. public static void main(String[] args) {
  96. String homepage = "leetcode.com";
  97. BrowserHistory browserHistory = new BrowserHistory("leetcode.com");
  98. browserHistory.visit("google.com"); // 你原本在浏览 "leetcode.com" 。访问 "google.com"
  99. browserHistory.visit("facebook.com"); // 你原本在浏览 "google.com" 。访问 "facebook.com"
  100. browserHistory.visit("youtube.com"); // 你原本在浏览 "facebook.com" 。访问 "youtube.com"
  101. browserHistory.back(1); // 你原本在浏览 "youtube.com" ,后退到 "facebook.com" 并返回 "facebook.com"
  102. browserHistory.back(1); // 你原本在浏览 "facebook.com" ,后退到 "google.com" 并返回 "google.com"
  103. browserHistory.forward(1); // 你原本在浏览 "google.com" ,前进到 "facebook.com" 并返回 "facebook.com"
  104. browserHistory.visit("linkedin.com"); // 你原本在浏览 "facebook.com" 。 访问 "linkedin.com"
  105. browserHistory.forward(2); // 你原本在浏览 "linkedin.com" ,你无法前进任何步数。
  106. browserHistory.back(2); // 你原本在浏览 "linkedin.com" ,后退两步依次先到 "facebook.com" ,然后到 "google.com" ,并返回 "google.com"
  107. browserHistory.back(7); // 你原本在浏览 "google.com", 你只能后退一步到 "leetcode.com" ,并返回 "leetcode.com"
  108. }
  109. }

1396-设计地铁系统

image.png
image.png
题意分析:

  • 思路比较明确,就是记录用户进站和出站的信息,然后对其进行分析,比如求平均时间。

解题思路:

  • 问题关键点在于数据结构的选取,进出站数据结构如何选取,存储哪些信息。这道题用了两个 Map 结构,一个是 Map>,一个是 Map<, >,比较难想到了是第二个 Map 用 kv 健值对作为 key,记录了所有进出站相同的用户信息。

注意点:

  • Map 采用 健值对 作为key,不太容易想到的思路。
  • 判断 Map 是否包含某个 key 是根据 key 的 hashCode 比较的,因此这里要重写 hashcoe 和 equals 方法。

扩展:(很有实际意义的一道题,有了进出站用户信息,可以来进行大量数据分析)

  • 如果记录相同进出站的所有用户?
  • 每个站(进站/出战)每个时间段的人流量有多少?找出人流量最大 Top-k 个站。
  • 进站/出站相同的用户有多少?
  • 如果涉及到多条地铁线路换乘呢,同样的进出站又该如何判断,是根据时间收费还是根据进站-出站点收费呢?

代码:

  1. package com.bxk.leetcode.design;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. /**
  5. * 设计地铁系统
  6. * 逻辑:
  7. * 1、<id,<startStation, time>> 记录进站用户信息
  8. * 2、<<startStation,endStation>, List<time>> 记录进出站对应的时间消耗
  9. * 3、时耗 List 可优化,不然 getAverageTime 需要遍历集合. <<startStation,endStation>, <totoalTime, personNum>>
  10. *
  11. * 扩展:
  12. * 1、进出站相同的用户有哪些?
  13. * 2、相同进站/出站的用户有哪些?
  14. */
  15. public class UndergroundSystem_1396 {
  16. Map<Integer, StartInfo> startMap;
  17. Map<CrossInfo, TimeSum> crossMap;
  18. public UndergroundSystem_1396() {
  19. startMap = new HashMap<>();
  20. crossMap = new HashMap<>();
  21. }
  22. /**
  23. * 进站
  24. * @param id
  25. * @param stationName
  26. * @param t
  27. */
  28. public void checkIn(int id, String stationName, int t) {
  29. startMap.put(id, new StartInfo(stationName,t));
  30. }
  31. /**
  32. * 出站
  33. * @param id
  34. * @param stationName
  35. * @param t
  36. */
  37. public void checkOut(int id, String stationName, int t) {
  38. // 获取进站信息
  39. StartInfo startInfo = startMap.get(id);
  40. // 构造进出站时间信息
  41. // 问题:这个构建 key 时 new 出新对象,<a,b>,<a,b> 当作两个对象放进了 Map 中,导致结果错误,应该重写 equal 方法
  42. CrossInfo crossInfo = new CrossInfo(startInfo.startStation, stationName);
  43. TimeSum timeSum = crossMap.getOrDefault(crossInfo, new TimeSum(0,0));
  44. timeSum.totalSum += t - startInfo.time;
  45. timeSum.personNum++;
  46. crossMap.put(crossInfo, timeSum);
  47. }
  48. /**
  49. * 相同进出站的平均耗时(总耗时/总人数)
  50. * @param startStation
  51. * @param endStation
  52. * @return
  53. */
  54. public double getAverageTime(String startStation, String endStation) {
  55. TimeSum timeSum = crossMap.get(new CrossInfo(startStation, endStation));
  56. return 1.0 * timeSum.totalSum / timeSum.personNum;
  57. }
  58. // 用户 id 对应的进站信息
  59. class StartInfo {
  60. String startStation;
  61. int time;
  62. public StartInfo(String startStation, int time) {
  63. this.startStation = startStation;
  64. this.time = time;
  65. }
  66. }
  67. /**
  68. * 维护以 <进站,出站> 为 key 的 map 结构,value 为所有耗时
  69. */
  70. class CrossInfo {
  71. String startStation;
  72. String endStation;
  73. public CrossInfo(String startStation, String endStation) {
  74. this.startStation = startStation;
  75. this.endStation = endStation;
  76. }
  77. @Override
  78. public int hashCode() {
  79. return (startStation + endStation).hashCode();
  80. }
  81. @Override
  82. public boolean equals(Object obj) {
  83. if (obj instanceof CrossInfo) {
  84. CrossInfo crossInfo2 = (CrossInfo)obj;
  85. if (this.startStation.equals(crossInfo2.startStation)
  86. && this.endStation.equals(crossInfo2.endStation)) {
  87. return true;
  88. } else {
  89. return false;
  90. }
  91. } else {
  92. return false;
  93. }
  94. }
  95. }
  96. /**
  97. * 总耗时,总人数
  98. */
  99. class TimeSum {
  100. int totalSum;
  101. int personNum;
  102. public TimeSum(int totalSum, int personNum) {
  103. this.totalSum = totalSum;
  104. this.personNum = personNum;
  105. }
  106. }
  107. public static void main(String[] args) {
  108. UndergroundSystem_1396 undergroundSystem = new UndergroundSystem_1396();
  109. undergroundSystem.checkIn(45, "Leyton", 3);
  110. undergroundSystem.checkIn(32, "Paradise", 8);
  111. undergroundSystem.checkIn(27, "Leyton", 10);
  112. undergroundSystem.checkOut(45, "Waterloo", 15);
  113. undergroundSystem.checkOut(27, "Waterloo", 20);
  114. undergroundSystem.checkOut(32, "Cambridge", 22);
  115. undergroundSystem.getAverageTime("Paradise", "Cambridge"); // 返回 14.0。从 "Paradise"(时刻 8)到 "Cambridge"(时刻 22)的行程只有一趟
  116. undergroundSystem.getAverageTime("Leyton", "Waterloo"); // 返回 11.0。总共有 2 躺从 "Leyton" 到 "Waterloo" 的行程,编号为 id=45 的乘客出发于 time=3 到达于 time=15,编号为 id=27 的乘客于 time=10 出发于 time=20 到达。所以平均时间为 ( (15-3) + (20-10) ) / 2 = 11.0
  117. undergroundSystem.checkIn(10, "Leyton", 24);
  118. undergroundSystem.getAverageTime("Leyton", "Waterloo"); // 返回 11.0
  119. undergroundSystem.checkOut(10, "Waterloo", 38);
  120. undergroundSystem.getAverageTime("Leyton", "Waterloo"); // 返回 12.0
  121. }
  122. }

1797-设计一个验证系统

image.png
image.png
题意分析:

  • 根据 token 的生成时间和更新时间,统计当前时间点的有效 token 数。

解题思路:

  • 构造 Map 存储 token 的有效时间,并在 generate 和 renew 时判断 token 是否过期以更新 token 的最新有效时间。

注意点:

  • 过期 token 要考虑清理掉,防止 Map 结构 OOM。
  • Map 遍历方式,根据要获取的 key/value 信息选择最简单的遍历方式。比如 keys()/values()/entrySet.iterator() 几种方式。

扩展:

  • 能够实现时间复杂度为 O(1) 的 countUnexpireTokens

代码:

  1. /**
  2. * 设计验证码系统
  3. */
  4. public class AuthenticationManager {
  5. private int timeToLive;
  6. private Map<String, Integer> map;
  7. public AuthenticationManager(int timeToLive) {
  8. this.timeToLive = timeToLive;
  9. map = new HashMap<>();
  10. }
  11. /**
  12. * 逻辑:
  13. * 1、生成 token,要考虑 token 是否存在,以及是否重复申请
  14. */
  15. public void generate(String tokenId, int currentTime) {
  16. if (map.containsKey(tokenId)) {
  17. // 更新 tokenId 时间
  18. if (map.get(tokenId) <= currentTime) {
  19. map.put(tokenId, currentTime);
  20. }
  21. } else {
  22. map.put(tokenId, currentTime);
  23. }
  24. }
  25. /**
  26. * 逻辑:
  27. * 1、更新时关键是要判断 token 是否过期。过期的不需要移除,因为更新时间不一定是真实时间。
  28. */
  29. public void renew(String tokenId, int currentTime) {
  30. if (map.containsKey(tokenId)) {
  31. int lifecycle = map.get(tokenId) + timeToLive;
  32. // 更新时也要考虑过期时间,并且过期时间优于其他操作
  33. // 比如 lifecycle = 4 + 5, currentTime = 9,此时 token 已过期,更新无效
  34. if (lifecycle > currentTime) {
  35. map.put(tokenId, currentTime);
  36. }
  37. }
  38. }
  39. /**
  40. * 逻辑:(如何不遍历 Map,实现 O(1) 复杂度
  41. * 1、遍历 map,判断没过期的 token 数量。
  42. */
  43. public int countUnexpiredTokens(int currentTime) {
  44. int count = 0;
  45. for(int value : map.values()) {
  46. // 题目逻辑,过期时间优于其他操作
  47. if (value + timeToLive > currentTime) {
  48. count++;
  49. } else {
  50. // 小于等于 currentTime 的 token 都已过期,需要清除掉,避免 map 结构过大
  51. map.remove(value);
  52. }
  53. }
  54. return count;
  55. }
  56. public static void main(String[] args) {
  57. AuthenticationManager authenticationManager = new AuthenticationManager(10); // 构造 AuthenticationManager ,设置 timeToLive = 5 秒。
  58. authenticationManager.renew("aaa", 1); // 时刻 1 时,没有验证码的 tokenId 为 "aaa" ,没有验证码被更新。
  59. authenticationManager.generate("aaa", 2); // 时刻 2 时,生成一个 tokenId 为 "aaa" 的新验证码。
  60. int count = authenticationManager.countUnexpiredTokens(6); // 时刻 6 时,只有 tokenId 为 "aaa" 的验证码未过期,所以返回 1 。
  61. System.out.println("count = " + count);
  62. authenticationManager.generate("bbb", 7); // 时刻 7 时,生成一个 tokenId 为 "bbb" 的新验证码。
  63. authenticationManager.renew("aaa", 8); // tokenId 为 "aaa" 的验证码在时刻 7 过期,且 8 >= 7 ,所以时刻 8 的renew 操作被忽略,没有验证码被更新。
  64. authenticationManager.renew("bbb", 11); // tokenId 为 "bbb" 的验证码在时刻 10 没有过期,所以 renew 操作会执行,该 token 将在时刻 15 过期。
  65. count = authenticationManager.countUnexpiredTokens(15); // tokenId 为 "bbb" 的验证码在时刻 15 过期,tokenId 为 "aaa" 的验证码在时刻 7 过期,所有验证码均已过期,所以返回 0
  66. System.out.println("count = " + count);
  67. }
  68. }

705-设计哈希集合

image.png
题意分析:

  • 实现 HashSet 的基本功能。

解题思路:

  • 问题的关键在于如何选取数据结构。举一反三,参考 Java 中 HashSet 和 HashMap 的实现,选取合适的数组结构:数组 + 链表。

注意点:

  • LinkedList#remove() 操作分为 remove(int index) 和 remove(Object o) 两种,前者会进行 index 检测,如果元素数据小于 index 会抛异常。
  • LinkedList 的 Iterator 遍历方法,平时用得少。

扩展:
代码:

  1. /**
  2. * 举一反三:
  3. * HashMap 底层实现:数组 + 链表(红黑树)。
  4. * HashSet 底层实现:HashMap,因此 Hash 集合的设计也要利用“数组+链表”。
  5. *
  6. * Map 特点:k-v 对,key 不重复,key 允许空
  7. * Set 特点:元素不重复,无序
  8. *
  9. * 思路:
  10. * 1、Hash 问题第一反应就是要设计哈希函数,然后利用数组进行寻址,用链表存储元素
  11. */
  12. public class MyHashSet {
  13. private static final int HASH_FACTOR = 769;
  14. LinkedList<Integer>[] data;
  15. public MyHashSet() {
  16. data = new LinkedList[HASH_FACTOR];
  17. for (int i = 0; i < HASH_FACTOR; i++) {
  18. data[i] = new LinkedList<>();
  19. }
  20. }
  21. /**
  22. * 思路:
  23. * 1、计算 hash 值,判断对应数组位置的链表是否存在相同 key
  24. */
  25. public void add(int key) {
  26. int h = hash(key);
  27. // 遍历链表,判断元素是否存在
  28. Iterator<Integer> iter = data[h].iterator();
  29. while (iter.hasNext()) {
  30. Integer ele = iter.next();
  31. if (ele == key) {
  32. return;
  33. }
  34. }
  35. data[h].offer(key);
  36. }
  37. /**
  38. * 思路:
  39. * 1、计算 hash 值,找到对应的数组链表,不存在则直接结束
  40. */
  41. public void remove(int key) {
  42. int h = hash(key);
  43. Iterator<Integer> iter = data[h].iterator();
  44. while (iter.hasNext()) {
  45. Integer ele = iter.next();
  46. if (ele == key) {
  47. // 注意这里 remove 调用,按 index 移除时 index 必须小于调用对象(data[h]的大小
  48. // remove(int index) 抛异常:java.lang.IndexOutOfBoundsException: Index: 2, Size: 1
  49. data[h].remove(ele);
  50. return;
  51. }
  52. }
  53. }
  54. public boolean contains(int key) {
  55. int h = hash(key);
  56. Iterator<Integer> iter = data[h].iterator();
  57. while (iter.hasNext()) {
  58. Integer ele = iter.next();
  59. if (ele == key) {
  60. return true;
  61. }
  62. }
  63. return false;
  64. }
  65. /**
  66. * 计算 hash 值
  67. */
  68. public int hash(int key) {
  69. return key % HASH_FACTOR;
  70. }
  71. public void printSet() {
  72. for (int i = 0; i < HASH_FACTOR; i++) {
  73. // data[i] != null: new 对象后 data[i] 就不是 null,要判断元素大小
  74. if (data[i].size() != 0) {
  75. System.out.println(data[i]);
  76. }
  77. }
  78. }
  79. public static void main(String[] args) {
  80. MyHashSet myHashSet = new MyHashSet();
  81. boolean flag;
  82. myHashSet.add(1); // set = [1]
  83. myHashSet.add(2); // set = [1, 2]
  84. flag = myHashSet.contains(1); // 返回 True
  85. System.out.println(flag);
  86. flag = myHashSet.contains(3); // 返回 False ,(未找到)
  87. System.out.println(flag);
  88. myHashSet.add(2); // set = [1, 2]
  89. flag = myHashSet.contains(2); // 返回 True
  90. System.out.println(flag);
  91. myHashSet.printSet();
  92. myHashSet.remove(2); // set = [1]
  93. flag = myHashSet.contains(2); // 返回 False ,(已移除)
  94. System.out.println(flag);
  95. }
  96. }

706-设计哈希映射

image.png
题意分析:

  • 实现 HashMap 的基本功能。

解题思路:

  • 参考 HashSet 的实现,无非就是链表的 Integer 对象改为了 Node 存储,其他逻辑基本一致。

注意点:
扩展:
代码:

  1. public class MyHashMap {
  2. private static final int HASH_FACTOR = 769;
  3. LinkedList<Node>[] data;
  4. class Node {
  5. int key;
  6. int value;
  7. public Node(int key, int value) {
  8. this.key = key;
  9. this.value = value;
  10. }
  11. public int getKey() {
  12. return key;
  13. }
  14. public void setValue(int value) {
  15. this.value = value;
  16. }
  17. public int getValue() {
  18. return value;
  19. }
  20. }
  21. public MyHashMap() {
  22. data = new LinkedList[HASH_FACTOR];
  23. for (int i = 0; i < HASH_FACTOR; i++) {
  24. data[i] = new LinkedList<>();
  25. }
  26. }
  27. public void put(int key, int value) {
  28. int h = hash(key);
  29. Iterator<Node> iter = data[h].iterator();
  30. while (iter.hasNext()) {
  31. Node node = iter.next();
  32. // key 存在,更新 value
  33. if (node.getKey() == key) {
  34. node.setValue(value);
  35. return;
  36. }
  37. }
  38. // key 不存在,添加新 Node
  39. data[h].offerLast(new Node(key, value));
  40. }
  41. public int get(int key) {
  42. int h = hash(key);
  43. Iterator<Node> iter = data[h].iterator();
  44. while (iter.hasNext()) {
  45. Node node = iter.next();
  46. if (node.getKey() == key) {
  47. return node.getValue();
  48. }
  49. }
  50. return -1;
  51. }
  52. public void remove(int key) {
  53. int h = hash(key);
  54. Iterator<Node> iter = data[h].iterator();
  55. while (iter.hasNext()) {
  56. Node node = iter.next();
  57. if (node.getKey() == key) {
  58. data[h].remove(node);
  59. return;
  60. }
  61. }
  62. }
  63. /**
  64. * 计算 hash 值
  65. */
  66. public int hash(int key) {
  67. return key % HASH_FACTOR;
  68. }
  69. public static void main(String[] args) {
  70. MyHashMap myHashMap = new MyHashMap();
  71. int res;
  72. myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]]
  73. myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]]
  74. res = myHashMap.get(1); // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]]
  75. System.out.println(res);
  76. res = myHashMap.get(3); // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]]
  77. System.out.println(res);
  78. myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值)
  79. res= myHashMap.get(2); // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]]
  80. System.out.println(res);
  81. myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]]
  82. res = myHashMap.get(2); // 返回 -1(未找到),myHashMap 现在为 [[1,1]]
  83. System.out.println(res);
  84. }
  85. }

707-设计链表

image.png
题意分析:

  • 链表的增删查功能。

解题思路:

  • 双链表实现。定义好数据结构,按功能实现。

注意点:

  • addAtIndex 需要判断链表长度,来决定是否将 index 位置数据添加到链表中。
  • 根据 index 遍历链表可以分为正向遍历和反向遍历,以缩短遍历次数。

扩展:
代码:

  1. /**
  2. * 优化:
  3. * 1、while 循环改成双向遍历,减少遍历长度
  4. */
  5. public class MyLinkedList {
  6. DLinkedNode head, tail;
  7. // addAtIndex 会判断 index 与 链表长度关系
  8. int size;
  9. public MyLinkedList() {
  10. size = 0;
  11. head = new DLinkedNode();
  12. tail = new DLinkedNode();
  13. // 双链表哨兵节点
  14. head.next = tail;
  15. tail.prev = head;
  16. }
  17. /**
  18. * 思路:
  19. * 1、遍历双链表
  20. * @param index
  21. * @return
  22. */
  23. public int get(int index) {
  24. if (index < 0 || index >= size) return -1;
  25. /*DLinkedNode curr = head.next;
  26. int count = 0;
  27. while (curr != null && curr != tail) {
  28. if (count == index) {
  29. return curr.val;
  30. }
  31. count++;
  32. curr = curr.next;
  33. }*/
  34. DLinkedNode curr = head;
  35. if (index + 1 < size - index) {
  36. // 要遍历到 index 位置,所以遍历次数为 index + 1
  37. for (int i = 0; i < index + 1; i++) curr = curr.next;
  38. } else {
  39. curr = tail;
  40. for (int i = 0; i < size - index; i++) curr = curr.prev;
  41. }
  42. return curr.val;
  43. }
  44. /**
  45. * 链表头部添加元素
  46. * @param val
  47. */
  48. public void addAtHead(int val) {
  49. size++;
  50. DLinkedNode node = new DLinkedNode(val);
  51. node.next = head.next;
  52. node.prev = head;
  53. // head.next.prev 和 head.next 先修改前者,因为后者会改变 head.next 指向
  54. head.next.prev = node;
  55. head.next = node;
  56. }
  57. /**
  58. * 链表尾部添加节点
  59. * @param val
  60. */
  61. public void addAtTail(int val) {
  62. size++;
  63. DLinkedNode node = new DLinkedNode(val);
  64. node.next = tail;
  65. node.prev = tail.prev;
  66. // 顺序和 addAtHead 相同
  67. tail.prev.next = node;
  68. tail.prev = node;
  69. }
  70. /**
  71. * 指定 index 位置添加元素,找到其前驱节点
  72. *
  73. * 注意当 size=0,index=1 情况,链表插入失败。
  74. * @param index
  75. * @param val
  76. */
  77. public void addAtIndex(int index, int val) {
  78. if (index > size) return;
  79. if (index < 0 ) index = 0;
  80. // index 位置节点和前驱节点
  81. DLinkedNode pred, succ;
  82. if (index < size - index) {
  83. pred = head;
  84. // 遍历 index 位置的前驱节点,所以循遍历次数为 index
  85. for (int i = 0; i < index; i++) pred = pred.next;
  86. succ = pred.next;
  87. } else {
  88. succ = tail;
  89. for (int i = 0; i < size - index; i++) succ = succ.prev;
  90. pred = succ.prev;
  91. }
  92. // curr 位置插入新节点
  93. size++;
  94. DLinkedNode toAdd = new DLinkedNode(val);
  95. toAdd.next = succ;
  96. toAdd.prev = pred;
  97. pred.next = toAdd;
  98. succ.prev = toAdd;
  99. }
  100. /**
  101. * 删除执行 index 位置元素,找到其前驱节点
  102. * @param index
  103. */
  104. public void deleteAtIndex(int index) {
  105. if (index < 0 || index >= size) return;
  106. DLinkedNode pred, succ;
  107. if (index < size - index) {
  108. pred = head;
  109. for(int i = 0; i < index; ++i) pred = pred.next;
  110. succ = pred.next;
  111. }
  112. else {
  113. succ = tail;
  114. for (int i = 0; i < size - index; ++i) succ = succ.prev;
  115. pred = succ.prev;
  116. }
  117. size--;
  118. succ.next.prev = succ.prev;
  119. pred.next = succ.next;
  120. }
  121. /**
  122. * 双向链表定义
  123. */
  124. class DLinkedNode {
  125. int val;
  126. DLinkedNode prev;
  127. DLinkedNode next;
  128. public DLinkedNode() {}
  129. public DLinkedNode(int val) {
  130. this.val = val;
  131. }
  132. }
  133. public void printDLinkedList(DLinkedNode head) {
  134. DLinkedNode nHead = head;
  135. DLinkedNode p = nHead;
  136. System.out.print("打印链表:");
  137. while (p != null && p != tail){
  138. System.out.print(p.val + " ");
  139. p = p.next;
  140. }
  141. System.out.println();
  142. }
  143. public static void main(String[] args) {
  144. MyLinkedList linkedList = new MyLinkedList();
  145. int res;
  146. linkedList.addAtHead(1);
  147. linkedList.addAtTail(3);
  148. linkedList.printDLinkedList(linkedList.head.next);
  149. linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
  150. linkedList.printDLinkedList(linkedList.head.next);
  151. res = linkedList.get(1); //返回2
  152. System.out.println(res);
  153. linkedList.deleteAtIndex(1); //现在链表是1-> 3
  154. linkedList.printDLinkedList(linkedList.head.next);
  155. res = linkedList.get(1); //返回3
  156. System.out.println(res);
  157. }
  158. }

622-设计循环队列

image.png
题意分析:

  • 实现循环队列的功能。

解题思路:

  • 数组实现。
  • 单链表实现。

注意点:

  • 数组实现时分清队列空和队列满的情况,两者都是 front=tail。因为有 count 计数所以代码里不用单独区分。
  • 单链表实现时感受头插法和尾插法的区别,头插法在出队时尾部tail不能直接拿到其前驱节点。

扩展:
代码:(数组实现——精髓)

  1. /**
  2. * 设计循环队列:采用数组或者单链表实现
  3. *
  4. * 本代码主要用数组实现。
  5. * [0,2,3,4,5,6,0] front -> 2, tail -> 6,留有一个空位
  6. */
  7. public class MyCircularQueue_2 {
  8. int count;
  9. int capacity;
  10. int[] data;
  11. int front, tail; // 数组头/尾指针
  12. public MyCircularQueue_2(int k) {
  13. this.capacity = k;
  14. data = new int[capacity];
  15. count = 0;
  16. front = tail = 0;
  17. }
  18. /**
  19. * 向循环队列插入一个元素。如果成功插入则返回真。
  20. *
  21. * 头插法:存在一个问题,enQueue 没什么问题,但 deQueue 需要遍历整个链表。
  22. * 尾插法:enQueue 利用 tail 节点,deQueue 利用 head 节点,比较方便。(ok)
  23. * @param value
  24. * @return
  25. */
  26. public boolean enQueue(int value) {
  27. if (count == capacity) {
  28. return false;
  29. }
  30. data[tail] = value;
  31. tail = (tail + 1) % capacity;
  32. count++;
  33. return true;
  34. }
  35. /**
  36. * 从循环队列中删除一个元素。如果成功删除则返回真。
  37. *
  38. * @return
  39. */
  40. public boolean deQueue() {
  41. if (count == 0) {
  42. return false;
  43. }
  44. front = (front + 1) % capacity;
  45. count--;
  46. return true;
  47. }
  48. /**
  49. * 从队首获取元素。如果队列为空,返回 -1 。
  50. * @return
  51. */
  52. public int Front() {
  53. if (count == 0) {
  54. return -1;
  55. }
  56. return data[front];
  57. }
  58. /**
  59. * 获取队尾元素。如果队列为空,返回 -1 。
  60. * @return
  61. */
  62. public int Rear() {
  63. if (count == 0) {
  64. return -1;
  65. }
  66. return data[(front + count - 1) % capacity];
  67. // return data[(tail -1) % capacity]; // 先持续添加元素至容量满是 front=tail=0,tail-1 越界
  68. }
  69. public boolean isEmpty() {
  70. return count == 0;
  71. }
  72. public boolean isFull() {
  73. return count == capacity;
  74. }
  75. public static void main(String[] args) {
  76. MyCircularQueue_2 circularQueue = new MyCircularQueue_2(3); // 设置长度为 3
  77. boolean flag;
  78. int res;
  79. flag = circularQueue.enQueue(1); // 返回 true
  80. System.out.println(flag);
  81. flag = circularQueue.enQueue(2); // 返回 true
  82. System.out.println(flag);
  83. flag = circularQueue.enQueue(3); // 返回 true
  84. System.out.println(flag);
  85. flag = circularQueue.enQueue(4); // 返回 false,队列已满
  86. System.out.println(flag);
  87. res = circularQueue.Rear(); // 返回 3
  88. System.out.println(res);
  89. flag = circularQueue.isFull(); // 返回 true
  90. System.out.println(flag);
  91. flag = circularQueue.deQueue(); // 返回 true
  92. System.out.println(flag);
  93. flag = circularQueue.enQueue(4); // 返回 true
  94. System.out.println(flag);
  95. res = circularQueue.Rear(); // 返回 4
  96. System.out.println(res);
  97. }
  98. }

代码:(单链表实现)

  1. /**
  2. * 设计循环队列:采用数组或者单链表实现
  3. *
  4. * 本代码主要用单链表实现。
  5. */
  6. public class MyCircularQueue {
  7. class Node {
  8. int val;
  9. Node next;
  10. public Node (int val) {
  11. this.val = val;
  12. }
  13. }
  14. // 思路链表长度
  15. int count;
  16. int capacity;
  17. Node head, tail;
  18. public MyCircularQueue(int k) {
  19. this.capacity = k;
  20. }
  21. /**
  22. * 向循环队列插入一个元素。如果成功插入则返回真。
  23. *
  24. * 头插法:存在一个问题,enQueue 没什么问题,但 deQueue 需要遍历整个链表。
  25. * 尾插法:enQueue 利用 tail 节点,deQueue 利用 head 节点,比较方便。(ok)
  26. * @param value
  27. * @return
  28. */
  29. public boolean enQueue(int value) {
  30. if (count == capacity)
  31. return false;
  32. Node node = new Node(value);
  33. if (count == 0) {
  34. head = tail = node;
  35. } else {
  36. tail.next = node;
  37. tail = node;
  38. }
  39. count++;
  40. return true;
  41. }
  42. /**
  43. * 从循环队列中删除一个元素。如果成功删除则返回真。
  44. *
  45. * @return
  46. */
  47. public boolean deQueue() {
  48. if (count == 0) {
  49. return false;
  50. }
  51. head = head.next;
  52. count--;
  53. return true;
  54. }
  55. /**
  56. * 从队首获取元素。如果队列为空,返回 -1 。
  57. * @return
  58. */
  59. public int Front() {
  60. if (count == 0)
  61. return -1;
  62. return head.val;
  63. }
  64. /**
  65. * 获取队尾元素。如果队列为空,返回 -1 。
  66. * @return
  67. */
  68. public int Rear() {
  69. if (count == 0)
  70. return -1;
  71. return tail.val;
  72. }
  73. public boolean isEmpty() {
  74. return count == 0;
  75. }
  76. public boolean isFull() {
  77. return count == capacity;
  78. }
  79. public static void main(String[] args) {
  80. MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
  81. boolean flag;
  82. int res;
  83. flag = circularQueue.enQueue(1); // 返回 true
  84. System.out.println(flag);
  85. flag = circularQueue.enQueue(2); // 返回 true
  86. System.out.println(flag);
  87. flag = circularQueue.enQueue(3); // 返回 true
  88. System.out.println(flag);
  89. flag = circularQueue.enQueue(4); // 返回 false,队列已满
  90. System.out.println(flag);
  91. res = circularQueue.Rear(); // 返回 3
  92. System.out.println(res);
  93. flag = circularQueue.isFull(); // 返回 true
  94. System.out.println(flag);
  95. flag = circularQueue.deQueue(); // 返回 true
  96. System.out.println(flag);
  97. flag = circularQueue.enQueue(4); // 返回 true
  98. System.out.println(flag);
  99. res = circularQueue.Rear(); // 返回 4
  100. System.out.println(res);
  101. }
  102. }

355-设计推特(朋友圈时间线功能)

image.png
题意分析:

  • 获取朋友圈最新消息,包括用户自己和用户关注的其他人的。

解题思路:

  • 面向对象编程的精髓 + 方法的行为者(关注/取关都是 User 的行为)。关键是设计 User 和 Tweet 两个对象,一个用户维护一个 Tweet 单链表(链表元素按时间头部插入到 head 节点)一个用户关注多个用户的 Set 集合。

注意点:

  • 这里的关注和取关是单向的,比如A关注B,A可以看到B的朋友圈,B无法看到A的朋友圈,取关逻辑同理。

扩展:
代码:

  1. /**
  2. * 设计朋友圈时间线功能
  3. */
  4. public class Twitter {
  5. // 我们需要一个映射将 userId 和 User 对象对应起来
  6. private Map<Integer, User> userMap = new HashMap<>();
  7. private static int timestamp = 0;
  8. public Twitter() {
  9. }
  10. /**
  11. * user 发表一条 tweet 动态
  12. * @param userId
  13. * @param tweetId
  14. */
  15. public void postTweet(int userId, int tweetId) {
  16. // 如果用户不存在,则新建
  17. if (!userMap.containsKey(userId)) {
  18. userMap.put(userId, new User(userId));
  19. }
  20. User user = userMap.get(userId);
  21. user.post(tweetId);
  22. }
  23. /**
  24. * 返回该 user 关注的人(包括他自己)最近的动态 id,最多 10 条,
  25. * 而且这些动态必须按从新到旧的时间线顺序排列。
  26. *
  27. * 拿到用户id及其关注的用户的所有 Tweet,取 Top 10
  28. * @param userId
  29. * @return
  30. */
  31. public List<Integer> getNewsFeed(int userId) {
  32. List<Integer> res = new ArrayList<>();
  33. if (!userMap.containsKey(userId)) return res;
  34. // 拿到关注的用户 id(包含用户本身)
  35. Set<Integer> users = userMap.get(userId).followed;
  36. /*PriorityQueue<Tweet> priorityQueue2 = new PriorityQueue<>(users.size(), new Comparator<Tweet>() {
  37. @Override
  38. public int compare(Tweet o1, Tweet o2) {
  39. return o2.time - o1.time;
  40. }
  41. });*/
  42. // 简写
  43. PriorityQueue<Tweet> priorityQueue = new PriorityQueue<>(users.size(), (a,b) -> (b.time - a.time));
  44. // 先将所有链表头节点插入优先级队列
  45. for (int id : users) {
  46. Tweet tweet = userMap.get(id).head;
  47. if (tweet == null)
  48. continue;
  49. priorityQueue.add(tweet);
  50. }
  51. // 取前 10
  52. while (!priorityQueue.isEmpty()) {
  53. if (res.size() == 10) break;
  54. // 弹出最近发表的
  55. Tweet tweet = priorityQueue.poll();
  56. res.add(tweet.id);
  57. if (tweet.next != null) {
  58. priorityQueue.add(tweet.next);
  59. }
  60. }
  61. return res;
  62. }
  63. /**
  64. * follower 关注 followee,如果 Id 不存在则新建
  65. * @param followerId
  66. * @param followeeId
  67. */
  68. public void follow(int followerId, int followeeId) {
  69. // 若 follower 不存在,则新建
  70. if (!userMap.containsKey(followerId)) {
  71. userMap.put(followerId, new User(followerId));
  72. }
  73. // 若 followee 不存在,则新建
  74. if (!userMap.containsKey(followeeId)) {
  75. userMap.put(followeeId, new User(followeeId));
  76. }
  77. // 开始关注
  78. userMap.get(followerId).follow(followeeId);
  79. }
  80. /**
  81. * follower 取关 followee,如果 Id 不存在则什么都不做
  82. * @param followerId
  83. * @param followeeId
  84. */
  85. public void unfollow(int followerId, int followeeId) {
  86. if (userMap.containsKey(followerId)) {
  87. userMap.get(followerId).unfollow(followeeId);
  88. }
  89. }
  90. /**
  91. * Tweet 信息
  92. */
  93. private class Tweet {
  94. private int id;
  95. private int time;
  96. private Tweet next;
  97. public Tweet(int id, int time) {
  98. this.id = id;
  99. this.time = time;
  100. this.next = null;
  101. }
  102. }
  103. /**
  104. * 面向对象的设计原则,「关注」「取关」和「发文」应该是 User 的行为,
  105. * 况且关注列表和推文列表也存储在 User 类中,
  106. * 所以我们也应该给 User 添加 follow,unfollow 和 post 这几个方法
  107. */
  108. private class User {
  109. private int id;
  110. // 用户关注的人
  111. private Set<Integer> followed;
  112. // 用户发表的推特链表头节点
  113. private Tweet head;
  114. public User(int userId) {
  115. followed = new HashSet<>();
  116. this.id = userId;
  117. this.head = null;
  118. follow(userId);
  119. }
  120. /**
  121. * 用户发送 tweet
  122. * @param tweetId
  123. */
  124. public void post(int tweetId) {
  125. Tweet tweet = new Tweet(tweetId, timestamp);
  126. timestamp++;
  127. // 将新建的 tweet 头插法插入到链表中
  128. tweet.next = head;
  129. head = tweet;
  130. }
  131. /**
  132. * 用户关注其他用户
  133. * @param userId
  134. */
  135. public void follow(int userId) {
  136. followed.add(userId);
  137. }
  138. /**
  139. * 用户取关其他用户
  140. * @param userId
  141. */
  142. public void unfollow(int userId) {
  143. // 不可以取关自己
  144. if (userId != this.id)
  145. followed.remove(userId);
  146. }
  147. }
  148. public static void main(String[] args) {
  149. Twitter twitter = new Twitter();
  150. List<Integer> res = new ArrayList<>();
  151. twitter.postTweet(1, 5); // 用户 1 发送了一条新推文 (用户 id = 1, 推文 id = 5)
  152. res = twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含一个 id 为 5 的推文
  153. System.out.println(res);
  154. twitter.follow(1, 2); // 用户 1 关注了用户 2
  155. twitter.postTweet(2, 6); // 用户 2 发送了一个新推文 (推文 id = 6)
  156. res = twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含两个推文,id 分别为 -> [6, 5] 。推文 id 6 应当在推文 id 5 之前,因为它是在 5 之后发送的
  157. System.out.println(res);
  158. twitter.unfollow(1, 2); // 用户 1 取消关注了用户 2
  159. res = twitter.getNewsFeed(1); // 用户 1 获取推文应当返回一个列表,其中包含一个 id 为 5 的推文。因为用户 1 已经不再关注用户 2
  160. System.out.println(res);
  161. }
  162. }

模板

题意分析:
解题思路:
注意点:
扩展:
代码: