队列(Queue)


队列, 其实在我们的开发的过程中, 我们很少使用java自己内部的队列, 因为我们好多时候是使用我们开发过程的一些中间件, 这个有很多成熟的产品, 性能也很好, 比如: kafka, rabbitmq, redis的队列, 这些都比我们自己用内部的队列简单的多, 但是我们是研究算法的, 可能就真的需要好好看看leetcode的上一些题目了

队列的特点: 和栈不同, 队列的最大特点是先进先出(FIFO), 就好像按顺序排队一样, 对于队列的数据来说, 我们只允许在队尾查看和添加数据, 在对头查看和删除数据

实现: 可以结束双链表来实现队列. 双链表的头指针允许在对头查看和删除数据, 而双链表的尾指针允许我们在队尾查看和添加数据

应用场景: 直观来看, 当我们需要按照一定的顺序来处理数据, 而该数据的数据量在不断的变化的时候, 则需要队列来帮助解题. 在算法面试中, 广度优先搜索(Breadth-First Search)是运用队列最多的地方.

双端队列(Deque)


特点: 双端队列和普通队列最大的不同在于, 它允许我们在队列的头尾两端都在O(1)的时间内进行数据的查看, 添加和删除

实现: 与队列相似, 我们可以利用一个双链表来实现双端队列.

应用场景: 双端队列最常用的地方就是是吸纳一个长度动态变化的窗口或者连续区间, 而动态窗口这种数据结构在很多题目里都有运用.

例题分析


leetcode 第239题

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 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

提示:

你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

方法1: 暴力法

最简单的方法就是遍历每个滑动窗口, 找到每个窗口的最大值. 一共有N - K + 1个滑动窗口, 每个有k个元素, 于是算法的时间复杂度为O(NK), 表现较差

class Solution {
 public int[] maxSlidingWindow(int[] nums, int k) {
   int n = nums.length;
   if (n * k == 0) return new int[0];
   
   int[] output = new int[n - k + 1];
   for (int i = 0; i < n - k + 1; i++) {
     int max = Integer.MIN_VALUE;
     for(int j = i; j < i + k; j++) {
       max = Math.max(max, nums[j]);
    }
     output[i] = max;
  }
   return output;
}
}

自我思考


我们想一想, 上面的算法还有什么可以优化的地方吗?

首先, 我们想到的是, 如果存放我们的k个元素是一个排序好的东西, 那我们有新的元素的时候, 先把下标过界的元素删除掉, 然后排序, 是不是就很简单来找出来最大的元素呢?

第二: 比如我们的k个元素是: [10, 2, 3, 4, 8], 在8永远比其中的2, 3, 4大, 在后面移动中, 2, 3, 4是永远不可能是最大值的, 只要8的下标不过界, 没有2, 3, 4, 什么事情;

所以, 还有一个优化的点是: 移除比当前元素小的所有元素,它们不可能是最大的

第三: 新加的元素, 都是加入到末尾的,但是如果有比末尾大的元素, 即[最大值, a, b, c], 新加入的x, 首先处理最大值过界的问题, 然后逐渐比较, c, b, a, 比x小的都删除掉, 因为下标在x前面, 还没有x, 那肯定是没有机会当最大值了

(其实你细细想, 如果会出现a, b, c, 那肯定是a> b> c, 要不然不能保存在链表中的. )

如果形象比喻一下的话, 就是列表是排序的好, 头是最大的, 尾是最小的, 要是每新加一个元素, 都是看看最大的元素是否过界,过界就删除最大元素, 如果新来的是个大佬, 其他的小搂搂全部淘汰, 如果来的是最小的搂搂, 那就排在最后把, 如果来的是中间的, 那就把比他不行的干掉, 他当最后的小弟就好了, 就这三种情况, 就全部包含了.

所以官网给出的提示是:

算法非常直截了当:

  • 处理前 k 个元素,初始化双向队列。
  • 遍历整个数组。在每一步 :
  • 清理双向队列 :
  • 只保留当前滑动窗口中有的元素的索引。

  • 移除比当前元素小的所有元素,它们不可能是最大的

  • 将当前元素添加到双向队列中。
  • 将 deque[0] 添加到输出中。
  • 返回输出数组。

方法二: 双向队列

我们先来理一下思路:

class Solution {
 // 先定义一个双向队列
 ArrayDeque<Integer> deq = new ArrayDeque<Integer>();
 int [] nums;

 public void clean_deque(int i, int k) {
   // remove indexes of elements not from sliding window
   // 判断第一个元素时最大值, 如果过界, 就需要删除.
   if (!deq.isEmpty() && deq.getFirst() == i - k)
     deq.removeFirst();

   // remove from deq indexes of all elements
   // which are smaller than current element nums[i]
   while (!deq.isEmpty() && nums[i] > nums[deq.getLast()]) {
     deq.removeLast();
  }
}

 public int[] maxSlidingWindow(int[] nums, int k) {
   
  // 先进行一波参数的校验
   int n = nums.length;
   if (n * k == 0) return new int[0];
   if (k == 1) return nums;

   // init deque and output
   this.nums = nums;
   int max_idx = 0;
   for (int i = 0; i < k; i++) {
     clean_deque(i, k);
     deq.addLast(i);
     // compute max in nums[:k]
     if (nums[i] > nums[max_idx]) {
       max_idx = i;
    }
  }
   int [] output = new int[n - k + 1];
   output[0] = nums[max_idx];

   // build output
   for (int i  = k; i < n; i++) {
     clean_deque(i, k);
     deq.addLast(i);
     output[i - k + 1] = nums[deq.getFirst()];
  }
   return output;
}
}

故事凌 明天能否加个鸡腿! 喜欢作者