1、剑指 Offer 41.数据流中的中位数

2、剑指 Offer 59 - I.滑动窗口的最大值

2.1 题目

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 输出: [3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值


[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7

2.2 思路

题目是用到了单调队列实现O(1)时间复杂度求最大值,特别注意滑动窗口left是从1 - k开始滑动,left的起始位置是数组外面,即要保证right是从0开始滑动的。
算法流程:

  1. 初始化: 双端队列 queue,结果数组 res;
  2. 滑动窗口: 左边界left的范围是[1- k, nums.length - k] ,右边界right范围是[0, nums.length - 1];

    1. 若 left > 0 且 队列queue的首元素 == 被删除元素 nums[left - 1],则队首元素出队;
    2. 删除 queue 内所有 < nums[right] 的元素,以保持队列queue的单调递减;
    3. 将 nums[right] 添加至 qeque 尾部;
    4. 若已形成窗口(即 left >= 0 ),将窗口最大值(即队列queue首元素 )添加至res ;
    5. 返回值: 返回结果数组 res 。

      2.3 代码

      1. class Solution {
      2. public int[] maxSlidingWindow(int[] nums, int k) {
      3. if (nums.length == 0 || k == 0) {
      4. return new int[0];
      5. }
      6. int[] res = new int[nums.length - k + 1];
      7. // 单调递减队列。res值为每次循环的队列首元素
      8. LinkedList<Integer> queue = new LinkedList<>();
      9. for (int right = 0, left = 1 - k; right < nums.length; ++left, ++right) {
      10. if (left > 0 && queue.peekFirst() == nums[left - 1]) {
      11. queue.pollFirst();
      12. }
      13. while (!queue.isEmpty() && queue.peekLast() < nums[right]) {
      14. queue.pollLast();
      15. }
      16. queue.addLast(nums[right]);
      17. if (left >= 0) {
      18. res[left] = queue.peekFirst();
      19. }
      20. }
      21. return res;
      22. }
      23. }

      3、剑指 Offer 16. 数值的整数次方

      快速幂,真难顶。

      3.1 题目

      实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
      示例 1:

      输入:x = 2.00000, n = 10 输出:1024.00000

示例 2:

输入:x = 2.10000, n = 3 输出:9.26100

示例 3:

输入:x = 2.00000, n = -2 输出:0.25000 解释:2-2 = 1/22 = 1/4 = 0.25

3.2 思路

二分法的应用,需要注意一点是n是负数的情况。image.png

3.3 代码

  1. class Solution {
  2. public double myPow(double x, int n) {
  3. return n > 0? quick(x, n): 1 / quick(x, n);
  4. }
  5. private double quick(double x, int n) {
  6. if (n == 0) {
  7. return 1.0;
  8. }
  9. double y = quick(x, n / 2);
  10. return (n & 1) == 0? y * y: y * y * x;
  11. }
  12. }

4、48. 旋转图像

4.1 题目

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
image.png

4.2 思路

本题必须在原数组上操作,即原地旋转图像。

  1. 求矩阵的转置matrixT(注意第二层循环的起始值为j = i);
  2. 沿Y轴对称交换matrixT的列。

    4.3 代码

    1. class Solution {
    2. public void rotate(int[][] matrix) {
    3. int m = matrix.length, n = matrix[0].length;
    4. // 求matrix的转置
    5. for (int i = 0; i < m; ++i) {
    6. for (int j = i; j < n; ++j) {
    7. int tmp = matrix[i][j];
    8. matrix[i][j] = matrix[j][i];
    9. matrix[j][i] = tmp;
    10. }
    11. }
    12. // y轴镜像交换转置矩阵的列
    13. int left = 0, right = n - 1;
    14. while (left < right) {
    15. for (int i = 0; i < m; ++i) {
    16. int tmp = matrix[i][left];
    17. matrix[i][left] = matrix[i][right];
    18. matrix[i][right] = tmp;
    19. }
    20. ++left;
    21. --right;
    22. }
    23. }
    24. }

    5、22. 括号生成

    5.1 题目

    数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
    示例 1:

    输入:n = 3 输出:[“((()))”,”(()())”,”(())()”,”()(())”,”()()()”]

示例 2:

输入:n = 1 输出:[“()”]

5.2 思路

看上去像回溯,本地用递归……

  1. 指定两个变量:
    1. leftRemain:剩余左括号数目;
    2. rightRemain:剩余右括号数目;

符合要求的字符串,左括号出现的数目=右括号出现的数目=n,只是左括号和右括号出现的顺序有限制。

  1. 当leftRemain == 0且rightRemain ==0时,代表符合要求的字符串已出现,此时添加到res,并return;
  2. 当leftRemain == rightRemain 时,此时只能添加左括号;
  3. 当leftRemain < rightRemain 时,此时有两种情况:

    1. 如果还有剩余左括号,添加左括号;
    2. 如果没有剩余左括号,添加右括号。

      这两种情况都要搜索到,因此不是if-else的关系。

  4. 不存在leftRemain > rightRemain 的场景,因为此时已用左括号数目 < 已用右括号数目,这种字符串不是符合要求的字符串,前面的递归如果正确,后面运行到这里是不会允许这种情况出现的。

    5.3 代码

    1. class Solution {
    2. private List<String> res = new ArrayList<>();
    3. public List<String> generateParenthesis(int n) {
    4. if (n <= 0) {
    5. return res;
    6. }
    7. helper("", n, n);
    8. return res;
    9. }
    10. private void helper(String str, int leftRemain, int rightRemain) {
    11. if (leftRemain == 0 && rightRemain == 0) {
    12. res.add(str);
    13. return;
    14. }
    15. // 当剩余左括号数目等于剩余右括号数目时,只能添加左括号
    16. if (leftRemain == rightRemain) {
    17. helper(str + "(", leftRemain - 1, rightRemain);
    18. }
    19. // 当剩余左括号数目小于剩余右括号数目时,可以添加左括号,也可以添加右括号
    20. if (leftRemain < rightRemain) {
    21. // 当还有剩余左括号时,先尝试添加左括号
    22. if (leftRemain > 0) {
    23. helper(str + "(", leftRemain - 1, rightRemain);
    24. }
    25. // 当剩余左括号用完时,再尝试添加右括号
    26. helper(str + ")", leftRemain, rightRemain - 1);
    27. }
    28. }
    29. }

    6、406. 根据身高重建队列

    妙啊,最好记住答案…….

    6.1 题目

    假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
    请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
    示例 1:

    输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 解释: 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

6.2 思路

  1. 对people数组,先按照hi降序排序,再按照ki升序排序;
  2. 遍历重新排序后的people数组,依据people数组中的每个元素的ki,插入到结果list中,注意ki即元素插入到list中的下标。

    6.3 代码

    1. class Solution {
    2. public int[][] reconstructQueue(int[][] people) {
    3. List<int[]> res = new ArrayList<>();
    4. Arrays.sort(people, (o1, o2) -> o1[0] == o2[0]? o1[1] - o2[1]: o2[0] - o1[0]);
    5. for (int[] p: people) {
    6. res.add(p[1], p);
    7. }
    8. return res.toArray(new int[people.length][2]);
    9. }
    10. }

    7、240. 搜索二维矩阵 II

    7.1 题目

    编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  3. 每行的元素从左到右升序排列;

  4. 每列的元素从上到下升序排列。

image.png

7.2 思路

该题有一种固定解法:

  1. 从矩阵的左下角或者右上角作为搜索起点;
  2. 当matrix[i][j] > target,则向上搜索一格;
  3. 当matrix[i][j] < target,则向右搜索一格;
  4. 当matrix[i][j] = target,则返回true;
  5. 跳出while循环,则返回false。

    7.3 代码

    1. class Solution {
    2. public boolean searchMatrix(int[][] matrix, int target) {
    3. int m = matrix.length;
    4. int n = matrix[0].length;
    5. int i = m - 1, j = 0;
    6. while (i >= 0 && i < m && j >= 0 && j < n) {
    7. if (matrix[i][j] > target) {
    8. --i;
    9. } else if (matrix[i][j] < target) {
    10. ++j;
    11. } else {
    12. return true;
    13. }
    14. }
    15. return false;
    16. }
    17. }

    8、146. LRU 缓存

    好题!

    8.1 题目

    请你设计并实现一个满足 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) 的平均时间复杂度运行。
示例:

输入 [“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

8.2 思路

本题有两种思路:

  1. 直接使用jdk中封装好的实现了LRU缓存的数据结构:LinkedHashMap,覆写removeEldestEntry方法,将触发LRU缓存的时机和逻辑在这个方法中实现,注意题目中触发LRU的场景是如果插入操作导致关键字数量超过 初始容量capacity,则应该逐出最久未使用的关键字;面试官肯定不希望这种解法;
  2. 自己定义一个数据结构实现LRU,HashMap + 双向链表。

主要介绍一下第二种解法:

  1. 由于get的时间复杂度为O(1),因此可以引入一个hashMap,从这个map里获取value(map的get方法时间复杂度为O(1)),map的key存放LRU数据结构的key,value存放key对应的双向链表中的node);
  2. 由于put的时间复杂度为O(1),因此:
    1. 不能使用数组,因为数组插入删除会伴随数组中元素的移动,时间复杂度不满足O(1);
    2. 由于插入删除的时间复杂度为O(1),可以考虑链表,但不能使用单链表,因为单链表的删除操作需要维护一个pre指针,因此使用双向链表;
  3. 双向链表是由内部类DLinkedNode的实例组成,在DLinkedNode类里,定义pre和next指针,同时维护key和value;
  4. 这里需要注意一个链表题目的小技巧:由于链表在头节点和尾节点做插入和删除操作时需要考虑左和右边是否为null,而在链表中间做插入和删除操作则无需考虑这些,为了统一处理,生成一个仅有指针,没有k-v的伪头节点head和伪尾节点tail,这样链表插入和删除操作无需考虑左右两边是否为null;
  5. 注意插入和删除操作时,由于map和双向链表都存有node数据,因此需要结合具体场景考虑是否同时删除和插入这两部分的数据,如果单纯地向到达LRU把节点已到头结点处,则无需更新map;
  6. 在写代码中需要合理划分private方法,本题的最佳划分方法为划分以下4个private方法:
    1. removeAndHead(DLinkedNode node):先将node从双向链表中移除,再将node插入到双向链表的头节点处,实现起来为b和c的组合;
    2. addHead(DLinkedNode node):将node插入到双向链表的头节点处;
    3. removeNode(DLinkedNode node):将node从双向链表中移除;
    4. DLinkedNode removeTail():移除双向链表的真正的尾结点(不是伪尾结点tail),并返回尾结点。
  7. 需要定义个数相关的成员属性:
    1. size:LRUCache中元素的个数;
    2. capacity:LRUCache集合的容量。
  8. 面试时,可能面试官会问你为什么选择HashMap + 双向链表?为什么不能用数组和单链表?此时要从题目要求的get和put的时间复杂度去介绍。

    8.3 代码

    直接使用LinkedHashMap:

    1. class LRUCache extends LinkedHashMap<Integer, Integer> {
    2. private int capacity;
    3. public LRUCache(int capacity) {
    4. super(capacity, 0.75f, true);
    5. this.capacity = capacity;
    6. }
    7. public int get(int key) {
    8. return super.getOrDefault(key, -1);
    9. }
    10. public void put(int key, int value) {
    11. super.put(key, value);
    12. }
    13. @Override
    14. protected boolean removeEldestEntry(Map.Entry<Integer,Integer> eldest) {
    15. return super.size() > this.capacity;
    16. }
    17. }

    HasMap + 双向链表:

    1. class LRUCache {
    2. /**
    3. * 引入hashmap,目的是保证get的时间复杂度为O(1)
    4. */
    5. private Map<Integer, DLinkedNode> map;
    6. /**
    7. * 集合中元素的数量
    8. */
    9. private int size;
    10. /**
    11. * 规定的集合初始化时的容量
    12. */
    13. private int capacity;
    14. /**
    15. * 双向链表头节点,仅有指针没有值
    16. */
    17. private DLinkedNode head;
    18. /**
    19. * 双向链表尾节点,仅有指针没有值
    20. */
    21. private DLinkedNode tail;
    22. public LRUCache(int capacity) {
    23. this.size = 0;
    24. this.capacity = capacity;
    25. map = new HashMap<>(capacity);
    26. // 定义没有值的头节点和尾节点,避免头插和删除时考虑左右两边是否为null
    27. this.head = new DLinkedNode();
    28. this.tail = new DLinkedNode();
    29. head.next = tail;
    30. tail.pre = head;
    31. }
    32. public int get(int key) {
    33. DLinkedNode node = map.get(key);
    34. if (node == null) {
    35. return -1;
    36. } else {
    37. removeAndHead(node);
    38. return node.val;
    39. }
    40. }
    41. public void put(int key, int value) {
    42. DLinkedNode node = map.get(key);
    43. if (node != null) {
    44. node.val = value;
    45. removeAndHead(node);
    46. } else {
    47. ++size;
    48. // 头插到双向链表
    49. DLinkedNode pNode = new DLinkedNode(key, value);
    50. addHead(pNode);
    51. // map里新增k-v
    52. map.put(key, pNode);
    53. // 判断是否触发LRU
    54. if (size > capacity) {
    55. --size;
    56. // 移除双向链表的尾结点
    57. DLinkedNode last = removeTail();
    58. map.remove(last.key);
    59. }
    60. }
    61. }
    62. private void removeAndHead(DLinkedNode node) {
    63. // 先在双向链表中删除当前node
    64. removeNode(node);
    65. // 再将最近访问的node移动到双向链表的头节点
    66. addHead(node);
    67. }
    68. private void removeNode(DLinkedNode node) {
    69. node.pre.next = node.next;
    70. node.next.pre = node.pre;
    71. }
    72. private void addHead(DLinkedNode node) {
    73. node.pre = head;
    74. node.next = head.next;
    75. head.next.pre = node;
    76. head.next = node;
    77. }
    78. private DLinkedNode removeTail() {
    79. DLinkedNode last = tail.pre;
    80. removeNode(last);
    81. return last;
    82. }
    83. class DLinkedNode {
    84. DLinkedNode pre;
    85. DLinkedNode next;
    86. int key;
    87. int val;
    88. DLinkedNode(){}
    89. DLinkedNode(int _key, int _val) {
    90. key = _key;
    91. val = _val;
    92. }
    93. }
    94. }