美文网首页刷穿剑指offer-专项突破
刷穿剑指offer-Day20-队列I 队列的使用与基础题型!

刷穿剑指offer-Day20-队列I 队列的使用与基础题型!

作者: 清风Python | 来源:发表于2021-09-21 23:05 被阅读0次

    队列的介绍

    队列(queue)是一种简单、常用的数据结构,在上一章栈的学习中,我们已经提到了队列这种数据结构。

    • 队列: 先入先出
    • 栈: 后入先出

    队列的操作和我们日常生活中的排队是很像的,先排队的人先得到服务。

    结合生活中的场景,我们应该理解,新来一个人加入排队大军,那么肯定是从队尾开始排起,而出队则是发生在队首。

    Python & Java 中的队列

    队列分为普通的单向队列双端队列,在Python中一般使用collections方法中内置的deque这种双端队列来解题。

    而在Java中接口Queue可以使用LinkedList实现单向队列,ArrayDeque实现双端队列。

    操作 Python Java 说明
    初始化 from collections import deque<br />queue = deque() Queue<Integer> queue = new LinkedList<>();
    插入元素 queue.append(1) queue.add(1); 将元素添加至队尾
    删除元素 queue.popleft() 为空时报错 queue.remove(); 为空时报错<br />queue.poll(); 为空时返回null 删除队首的元素
    获取队首 queue[0] 为空时报错 queue.element(); 为空时报错<br />queue.peek();为空时返回null 返回队首的元素

    队列基础操作就是这些,但和栈一样,虽然操作简单,但是重在解题的思路上。

    让我们先通过一道简单的题目,熟悉下它吧。

    剑指OfferII041.滑动窗口的平均值

    https://leetcode-cn.com/problems/qIsx9U/solution/shua-chuan-jian-zhi-offer-day20-dui-lie-09ber/

    难度:简单

    题目

    给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算滑动窗口里所有数字的平均值。

    实现 MovingAverage 类:

    • MovingAverage(int size) 用窗口大小 size 初始化对象。
    • double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数,
      请计算并返回数据流中最后 size 个值的移动平均值,即滑动窗口里所有数字的平均值。

    提示:

    • 1 <= size <= 1000
    • -10 ^ 5 <= val <= 10 ^ 5
    • 最多调用 next 方法 10 ^ 4 次

    示例

    输入:
    inputs = ["MovingAverage", "next", "next", "next", "next"]
    inputs = [[3], [1], [10], [3], [5]]
    输出:
    [null, 1.0, 5.5, 4.66667, 6.0]
    
    解释:
    MovingAverage movingAverage = new MovingAverage(3);
    movingAverage.next(1); // 返回 1.0 = 1 / 1
    movingAverage.next(10); // 返回 5.5 = (1 + 10) / 2
    movingAverage.next(3); // 返回 4.66667 = (1 + 10 + 3) / 3
    movingAverage.next(5); // 返回 6.0 = (10 + 3 + 5) / 3
    

    分析

    这是一道比较明朗的队列题目。
    这里只需要关注下,由于每次都需要求滑动窗口里所有数字的平均值。
    所以我们应该初始化一个sum,用于队列中所有数的总和。
    当队列满时,sum -= 出队的数字
    当队列未满时,sum += 本次入队的数字
    这样就可以通过O(1)的时间去计算平均值了。

    解题

    Python:

    class MovingAverage:
    
        def __init__(self, size: int):
            self.size = size
            self.queue = deque()
            self.sum = 0
    
        def next(self, val: int) -> float:
            self.sum += val
            self.queue.append(val)
            if len(self.queue) > self.size:
                self.sum -= self.queue.popleft()
            return self.sum / len(self.queue)
    

    Java:

    class MovingAverage {
        private int length;
        private Queue<Integer> queue;
        private double sum = 0;
    
    
        public MovingAverage(int size) {
            length = size;
            queue = new LinkedList<>();
            sum = 0;
        }
    
        public double next(int val) {
            if (queue.size() == length) {
                sum -= queue.remove();
            }
            queue.add(val);
            sum += val;
            return sum / queue.size();
        }
    }
    

    下来,我们来看一道比较隐晦的队列题目,反正之前刷这道题的时候,真的没想过用队列解题....分析后才发现队列更高效些。

    剑指OfferII042.最近请求次数

    https://leetcode-cn.com/problems/H8086Q/solution/shua-chuan-jian-zhi-offer-day18-zhan-ii-de51o/

    难度:简单

    题目

    写一个 RecentCounter 类来计算特定时间范围内最近的请求。
    请你实现 RecentCounter 类:

    • RecentCounter() 初始化计数器,请求数为 0 。
    • int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。
      确切地说,返回在 [t-3000, t] 内发生的请求数。

    保证 每次对 ping 的调用都使用比之前更大的 t 值。

    提示:

    • 1 <= t <= 10 ^ 9
    • 保证每次对 ping 调用所使用的 t 值都 严格递增
    • 至多调用 ping 方法 10 ^ 4 次

    示例

    输入:
    ["RecentCounter", "ping", "ping", "ping", "ping"]
    [[], [1], [100], [3001], [3002]]
    输出:
    [null, 1, 2, 3, 3]
    
    解释:
    RecentCounter recentCounter = new RecentCounter();
    recentCounter.ping(1);     // requests = [1],范围是 [-2999,1],返回 1
    recentCounter.ping(100);   // requests = [1, 100],范围是 [-2900,100],返回 2
    recentCounter.ping(3001);  // requests = [1, 100, 3001],范围是 [1,3001],返回 3
    recentCounter.ping(3002);  // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
    

    分析

    坦白说第一次见到这个题,真的没有往队列上面去想。既然是ping递增的,然后左边界又确定是t - 3000,那不是标准的二分查找么?
    创建一个数组,然后每次执行二分即可。
    当然这里有个小技巧,既然每次t都是递增的,我们可以初始化一个left,每次更新left,无需每次左边界从0开始二分.

    但根据题意分析,我们还可以使用队列来完成这道题目,由于每次获取的都是t - 3000的数值。
    所以在加入t后,我们持续判断队首数值,如果小于t- 3000就出队,然后返回队列的长度。

    二分解题

    Python:

    class RecentCounter:
        def __init__(self):
            self.req = []
            self.left = 0
    
        def ping(self, t):
            self.req.append(t)
            self.left = bisect.bisect_left(self.req, t - 3000, self.left)
            return len(self.req) - self.left
    

    Java:

    class RecentCounter {
        private ArrayList<Integer> arr;
        private int left = 0;
        
        public RecentCounter() {
            arr = new ArrayList<>();
        }
    
        public int ping(int t) {
            return bisect(t);
        }
    
        private int bisect(int t){
            arr.add(t);
            int right = arr.size();
            while (left < right) {
                int mid = (right - left) / 2 + left;
                if (arr.get(mid) < t - 3000){
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            return arr.size() - left;
        }
    }
    

    队列解题

    Python:

    class RecentCounter:
        def __init__(self):
            self.req = deque()
    
        def ping(self, t):
            self.req.append(t)
            while self.req[0] < t - 3000:
                self.req.popleft()
            return len(self.req)
    

    Java:

    class RecentCounter {
        private Queue<Integer> queue;
    
        public RecentCounter() {
            queue = new LinkedList<>();
        }
    
        public int ping(int t) {
            queue.add(t);
            while (queue.peek() < t - 3000) {
                queue.poll();
            }
            return queue.size();
        }
    }
    

    这道题使用二分和队列到底哪一种效率高呢?
    首先定义 ping调用10 ^ 4 次,即O(n) = 10000,然后计算总执行的时间复杂度。
    先来说说空间复杂度,对于空间复杂度,那肯定是二分会高一些,因为队列存在出队的情况。

    • 二分 O(n)
    • 队列 最坏O(3000) 最好O(1) 最好即每次t都比上一次大3000

    再来说说时间复杂度:

    • 二分 总的时间复杂度为 O(nlogn),执行N次,每次log(n)。
    • 队列 粗算O(n),最好呢?t每次比上一次大1,O(7000),如果ping调用3000次,最好为O(1).

    今天队列的题目就先做到这里,其实这些操作并不是队列最擅长的。明天我们将深入学习队列这个数据结构的擅长题目!

    欢迎关注我的公众号: 清风Python,带你每日学习Python算法刷题的同时,了解更多python小知识。

    我的个人博客:https://qingfengpython.cn

    力扣解题合集:https://github.com/BreezePython/AlgorithmMarkdown

    相关文章

      网友评论

        本文标题:刷穿剑指offer-Day20-队列I 队列的使用与基础题型!

        本文链接:https://www.haomeiwen.com/subject/ultwgltx.html