• home > theory > algorithm > leetcode >

    队列集训:科普队列概念与leetcode相关题目精讲

    Author:zhoulujun Date:

    普通队列符合「先进先出(First in, First out)」的规则,优先队列符合 「最高级先出(First in, Largest out)」 的规则。普通队列与优先队列对应的题目有哪些?解题模板是啥呢?

    队列(Queue)

    队列(Queue):一种线性表数据结构,是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。

    队列结构

    简单来说,队列是一种 「先进先出(First In First Out)」 的线性表,简称为 「FIFO 结构」。

    更详细的,推荐阅读:https://algo.itcharge.cn/04.Queue/01.Queue-Basic/01.Queue-Basic/

    优先队列(Priority Queue)

    优先队列(Priority Queue):一种特殊的队列。在优先队列中,元素被赋予优先级,当访问队列元素时,具有最高优先级的元素最先删除

    优先队列与普通队列最大的不同点在于 出队顺序。

    • 普通队列的出队顺序跟入队顺序相关,符合「先进先出(First in, First out)」的规则。

    • 优先队列的出队顺序跟入队顺序无关,优先队列是按照元素的优先级来决定出队顺序的。优先级高的元素优先出队,优先级低的元素后出队。优先队列符合 「最高级先出(First in, Largest out)」 的规则。

    优先队列的适用场景

    优先队列的应用场景非常多,比如:

    • 数据压缩:赫夫曼编码算法;

    • 最短路径算法:Dijkstra 算法;

    • 最小生成树算法:Prim 算法;

    • 任务调度器:根据优先级执行系统任务;

    • 事件驱动仿真:顾客排队算法;

    • 排序问题:查找第 k 个最小元素。


    更详细的推荐阅读:https://algo.itcharge.cn/04.Queue/02.Priority-Queue/01.Priority-Queue/



    leetcode题目:

    LeetCode上有很多题目涉及到队列(Queue)的使用,包括普通队列、循环队列、双端队列(Deque)、优先队列等。下面是一些与队列相关的典

     队列的顺序存储与链式存储

    队列实现

    1. 232. 用栈实现队列 (Implement Queue using Stacks):实现一个队列,只允许使用栈来完成。

    2. 622. 设计循环队列 (Design Circular Queue):设计一个循环队列的数据结构,包括队列的插入、删除、检查是否为空和检查是否已满等

    3. 641. 设计循环双端队列 (Design Circular Deque):设计一个循环双端队列,允许在两端进行插入和删除操作

    4. 1670. 设计前中后队列 (Design Front Middle Back Queue):设计一个队列,允许在队列的前端、中间和后端进行插入和删除操作

    队列题

    1. 933. 最近的请求次数 (Number of Recent Calls):记录最近的N个请求,并返回在过去3000毫秒内被调用的次数。

    2. 859. 亲密字符串 (Buddy Strings):检查两个字符串是否可以通过交换一次字符变成相同的字符串。

    3. 860. 柠檬水找零 (Lemonade Change):模拟顾客购买柠檬水的过程,处理找零问题,确保有足够的零钱找给顾客。

    4. 969. 煎饼排序 (Pancake Sorting):通过一系列翻转操作将一个数组按升序排序。

    5. 621. 任务调度器 (Task Scheduler):给定一个任务列表和冷却时间,确定完成所有任务所需的最短时间。

    6. 338. 比特位计数 (Counting Bits):计算从0到给定整数n之间的每个数字的二进制表示中1的个数。

    优先队列相关题目

    1. 215. 数组中的第K个最大元素 (Kth Largest Element in an Array):找到未排序数组中的第k个最大元素。

    2. 347.前K个高频元素(Top K Frequent Elements)

    3. 23. 合并K个排序链表 (Merge k Sorted Lists):合并多个已排序的链表。

    4. 973.最接近原点的 K 个点

    5. 703.数据流中的第K大元素(Top K Frequent Elements)

    6. 451.根据字符出现频率排序

    7. 239. 滑动窗口最大值 (Sliding Window Maximum):在给定数组的每个滑动窗口中找到最大值。

    8. 621.任务调度器(Task Scheduler)

    9. 253.会议室 II(Meeting Rooms II )

    10. 1296 划分数组为连续数字的集合

    11. 0218. 天际线问题 - 力扣



    前 K 个高频元素

    给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

    示例 1:   输入: nums = [1,1,1,2,2,3], k = 2      输出: [1,2]

    示例 2:   输入: nums = [1], k = 1                    输出: [1]

    最简单粗暴的思路就是 使用排序算法对元素按照频率由高到低进行排序,然后再取前 k 个元素。

    排序中,可以使用桶排序(推荐阅读《再谈桶排排序算法—从计数排序到桶排序》)

    桶排序法

    首先依旧使用哈希表统计频率,统计完成后,创建一个数组,将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标即可。

    347. 前 K 个高频元素 桶排序法

    //基于桶排序求解「前 K 个高频元素」
    class Solution {
        public List<Integer> topKFrequent(int[] nums, int k) {
            List<Integer> res = new ArrayList();
            // 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值
            HashMap<Integer,Integer> map = new HashMap();
            for(int num : nums){
                if (map.containsKey(num)) {
                   map.put(num, map.get(num) + 1);
                 } else {
                    map.put(num, 1);
                 }
            }
            
            //桶排序
            //将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
            List<Integer>[] list = new List[nums.length+1];
            for(int key : map.keySet()){
                // 获取出现的次数作为下标
                int i = map.get(key);
                if(list[i] == null){
                   list[i] = new ArrayList();
                } 
                list[i].add(key);
            }
            
            // 倒序遍历数组获取出现顺序从大到小的排列
            for(int i = list.length - 1;i >= 0 && res.size() < k;i--){
                if(list[i] == null) continue;
                res.addAll(list[i]);
            }
            return res;
        }
    }


    具体参看:https://leetcode.cn/problems/top-k-frequent-elements/solutions/11201/leetcode-di-347-hao-wen-ti-qian-k-ge-gao-pin-yuan-/

    哈希表 + 优先队列

    1. 使用哈希表统计元素频率: 首先遍历数组,使用哈希表统计每个元素出现的频率。

    2. 使用优先队列获取前 K 高频元素: 将哈希表中的元素和对应的频率存入优先队列中。优先队列可以用 JavaScript 中的最小堆来实现。

    3. 取出前 K 高频元素: 从优先队列中取出前 K 个元素即可。

    var topKFrequent = function (nums, k) {
      // Step 1: Count frequency using a hash map
      const freqMap = new Map();
      nums.forEach(num => {
        freqMap.set(num, (freqMap.get(num) || 0) + 1);
      });
    
      // Step 2: Create PriorityQueue
      const pq = new PriorityQueue((a, b) => a.freq - b.freq);
      for (const [num, freq] of hashTable.entries()) {
        if (pq.size < k) {
          pq.push({ num, freq });
        } else if (freq > pq.top().freq) {
          pq.pop();
          pq.push({ num, freq });
        }
      }
    
    
      // Step 3: Collect the result
      const result = [];
      while (pq.size > 0) {
        result.push(pq.pop().num);
      }
      return result;
    };


    有先队列JavaScript实现(一般使用第三方库)

    class PriorityQueue {
      constructor() {
        this.data = [];
      }
    
      enqueue(element) {
        this.data.push(element);
        this.bubbleUp(this.data.length - 1);
      }
    
      dequeue() {
        if (this.data.length === 0) return null;
        const top = this.data[0];
        this.data[0] = this.data[this.data.length - 1];
        this.data.pop();
        this.sinkDown(0);
        return top;
      }
    
      size() {
        return this.data.length;
      }
    
      bubbleUp(index) {
        while (index > 0 && this.compare(this.data[Math.floor((index - 1) / 2)], this.data[index])) {
          const parentIndex = Math.floor((index - 1) / 2);
          [this.data[index], this.data[parentIndex]] = [this.data[parentIndex], this.data[index]];
          index = parentIndex;
        }
      }
    
      sinkDown(index) {
        let childIndex = (index * 2) + 1;
        while (childIndex < this.data.length) {
          const rightChildIndex = childIndex + 1;
          if (
            rightChildIndex < this.data.length &&
            this.compare(this.data[childIndex], this.data[rightChildIndex])
          ) {
            childIndex = rightChildIndex;
          }
          if (this.compare(this.data[index], this.data[childIndex])) {
            return;
          }
          [this.data[index], this.data[childIndex]] = [this.data[childIndex], this.data[index]];
          index = childIndex;
        }
      }
    
      compare(a, b) {
        return a.count < b.count; // 降序排列
      }
    }




    根据字符出现频率排序

    给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。

    返回 已排序的字符串 。如果有多个答案,返回其中任何一个。

    示例 1:   输入: s = "tree"    输出: "eert"

    解释: 'e'出现两次,'r'和't'都只出现一次。因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。

    示例 2:   输入: s = "cccaaa"  输出: "cccaaa"

    解释: 'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。 注意"cacaca"是不正确的,因为相同的字母必须放在一起


    优先队列

    1. 使用哈希表 s_dict 统计字符频率。

    2. 然后遍历哈希表 s_dict,将字符以及字符频数存入优先队列中。

    3. 将优先队列中频数最高的元素依次加入答案数组中。

    4. 最后拼接答案数组为字符串,将其返回。

    function sortCharactersByFrequency(str) {
      const hashTable = new Map();
      for (const char of str) {
        hashTable.set(char, hashTable.get(char) || 0) + 1;
      }
    
      const priorityQueue = new PriorityQueue((a, b) => b.count - a.count);
      for (const [char, count] of hashTable.entries()) {
        priorityQueue.enqueue({ char, count });
      }
    
      let result = "";
      while (priorityQueue.size()) {
        const { char } = priorityQueue.dequeue();
        result += char;
      }
      return result;
    }



    滑动窗口最大值

    给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

    返回 滑动窗口中的最大值 。

    示例 1: 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3   输出:[3,3,5,5,6,7]

    这个和直接滑动窗口是不一样的(回忆一下《双指针:对撞/快慢/分离/滑动窗口的案例(数组/回文/求和等) 》),这个是是如何求一个区间里的最大值呢?

    此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。

    每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。

    239.滑动窗口最大值-2.gif


    function maxSlidingWindow(nums, k) {
      if (k > nums.length) return [];
    
      const pq = new PriorityQueue((a, b) => a - b); 
      const result = [];
      for (let i = 0; i < nums.length; i++) {
        pq.enqueue(nums[i]);
    
        if (pq.size() > k) {
          pq.dequeue();
        }
    
        if (i >= k - 1) {
          result.push(pq.top());
        }
      }
    
      return result;
    }


    使用优先队列求上面的解,代码非常简洁。




    参考文章:

    https://algo.itcharge.cn/04.Queue/02.Priority-Queue/01.Priority-Queue/

    https://programmercarl.com/0239.滑动窗口最大值.html





    转载本站文章《队列集训:科普队列概念与leetcode相关题目精讲》,
    请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/leetcode/9102.html