算法课堂——历史重现(队列)

190 阅读4分钟

算法课堂——历史重现(队列)

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

通过前几节课我们学习了数组和链表,大家对于一些运用到数组和链表的算法题应该能有自己的理解,那么今天我们就更近一步,学习一个新的数据结构-队列


一、什么是队列?

什么是队列,故名思义,队列和我们生活中吃饭排队一样,先排队的人先吃饭;同样的,对于队列中的元素,也要遵循先入先出的原则。

不过需要注意的是,队列和数组、链表都不一样,它属于一种逻辑结构,与之对应的,数组、链表属于物理结构。即队列是我们在物理结构上抽象出来的概念,我们今天的队列,用数组、链表都可以实现。你可以把链表想象成一种特殊的数组(即:增加了一些约束的数组或是链表)。

二、队列的应用

为什么今天的标题叫做历史重现呢?因为队列遵循着先入先出的原则,所以我们可以通过队列将我们已经发生的事情重演一遍,所以叫做:历史重现。

三、真题详解

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

请你实现 RecentCounter 类:

RecentCounter() 初始化计数器,请求数为 0 。 int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。 保证 每次对 ping 的调用都使用比之前更大的 t 值。

 

示例 1:

输入: ["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),并且返回在过去3000ms内请求的次数。假设第1毫秒我们请求一次,我们就返回1,因为过去的3000ms中我们就请求了一次;假设我们在第2毫秒又请求了一次,我们就返回2,因为过去的3000ms中我们就请求了两次,以此类推。。。

既然题干中已经提到过去的3000ms请求了几次,是不是我们就可以使用我们能够进行历史重现的数据结构--队列呢?

好,那么我们就使用队列存放我们请求的数据,然后每次请求,把新的请求放进去,把不是最近3000ms的请求踢出去, 因为队列是先进先出的,所以最早踢出去的请求肯定是最先请求的。符合我们的要求。

class RecentCounter {
    Queue<Integer> q;
    public RecentCounter() {
        q = new LinkedList();
    }

    public int ping(int t) {
        q.add(t);
        while (q.peek() < t - 3000)
            q.poll();
        return q.size();
    }
}

好的,第一题我们已经见识到队列能够历史重现的本事了,那么我们来看下一道题:

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例 1:

输入:s = "abaccdeff"
输出:'b'

示例 2:

输入:s = "" 
输出:' '

题目理解起来很简单,我们需要找出只出现了一次的字符,而且是第一个。

简单思考一下,我们很容易就能想到,既然想知道哪个字符是第一个出现一次的,我们就先统计出每个字符出现的次数。然后找到第一个次数是1的字符就能找到答案了。

class Solution {
    public char firstUniqChar(String s) {
        Map<Character, Integer> frequency = new HashMap<Character, Integer>();
        //第一次循环,统计处每个字符出现的次数
        for (int i = 0; i < s.length(); ++i) {
            char ch = s.charAt(i);
            frequency.put(ch, frequency.getOrDefault(ch, 0) + 1);
        }
        //循环哈希表,找到第一个出现次数是1的字符。
        for (int i = 0; i < s.length(); ++i) {
            if (frequency.get(s.charAt(i)) == 1) {
                return s.charAt(i);
            }
        }
        return ' ';
    }
}

我们这道题的时间复杂度是O(n),那么空间复杂度呢?我们的哈希表最长存放26个字符,也就是哈希表最长为26,所以我们的空间复杂度记为O(∣Σ∣)。

那么我们能不能用队列解决这个问题呢?

由于我们说队列具有重现历史的功能,我们就依次把字符放到队列中,每次放的时候能,我们就看这个字符有没有出现过,如果出现过,就看这个字符是不是在最前面,如果这个字符在最前面,那么这个字符就不能排在第一位,我们就踢出它,如果它出现过但是不是排在第一,那么我们大可不用管它。代码如下:

class Solution {
    public char firstUniqChar(String s) {
        Map<Character, Integer> position = new HashMap<Character, Integer>();
        Queue<Pair> queue = new LinkedList<Pair>();
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            char ch = s.charAt(i);
                if (!position.containsKey(ch)) {//如果这个字符没出现过
                position.put(ch, i);
                queue.offer(new Pair(ch, i));
                } else {  //如果这个字符已经出现过
                position.put(ch, -1);
                while (!queue.isEmpty() && position.get(queue.peek().ch) == -1) {
                    queue.poll();
                }
            }
        }
        return queue.isEmpty() ? ' ' : queue.poll().ch;
    }

    //定义一个字符本身和位置的对象
    class Pair {
        //存放字符
        char ch;
        //存放位置
        int pos;

        Pair(char ch, int pos) {
            this.ch = ch;
            this.pos = pos;
        }
    }
}

四、总结

今天我们学习了队列的基本概念和用法,并用两道题解释了队列怎样进行历史重现的,以后在题目中遇到相关的问题可以考虑到用队列来帮助解题。明天我们会向大家介绍另一个逻辑结构--栈,它和队列又有哪些相同,哪些不同呢?它和队列之间又能碰撞出怎样的火花呢?请关注,明天的算法课堂

算法课堂--时光倒流(栈)