返回

踏入前行的阶梯:JavaScript栈与队列系列专题之239题和347题巧妙解题思路

前端

算法世界的大门:栈与队列

对于初入算法编程领域的探索者来说,栈和队列是两个不可忽视的数据结构。栈遵循“后进先出”(LIFO)的原则,就像一叠盘子,后放的盘子先取出来。队列则遵循“先进先出”(FIFO)的原则,就像排队等候,先排队的人先被服务。

在计算机程序中,栈和队列可以用于解决一系列常见的问题。例如,栈可用于管理函数调用,确保先调用的函数先返回;队列可用于处理任务调度,保证先提交的任务先执行。理解栈和队列的工作方式,是算法编程入门的基础。

算法实战:滑动窗口最大值和前K个高频元素

  1. 239. 滑动窗口最大值

    滑动窗口最大值问题如下:给定一个数组nums和一个滑动窗口的尺寸k,请找出所有滑动窗口中的最大值。

    面对这个问题,Carl老师提出了两种解法。

    方法一:暴力求解

    这种方法简单直接,但效率较低。具体步骤如下:

    1. 从数组nums的开头开始,逐个元素地遍历整个数组。
    2. 对于每个元素,计算以该元素为结尾的滑动窗口中的最大值。
    3. 将计算出的最大值存储在结果数组中。

    方法二:使用双端队列

    双端队列(也称作deque)是一种特殊的队列,它允许在队头和队尾两端进行插入和删除操作。利用双端队列的特性,我们可以更高效地解决滑动窗口最大值问题。

    具体步骤如下:

    1. 将滑动窗口中的元素依次加入双端队列。
    2. 当队列中的元素个数超过k时,将队首元素(即最先加入队列的元素)弹出。
    3. 此时,队首元素就是滑动窗口中的最大值。
    4. 重复步骤2和步骤3,直到遍历完整个数组。

    示例代码:

    // 方法一:暴力求解
    const maxSlidingWindow1 = (nums, k) => {
      const result = [];
      for (let i = 0; i <= nums.length - k; i++) {
        let max = nums[i];
        for (let j = i + 1; j < i + k; j++) {
          max = Math.max(max, nums[j]);
        }
        result.push(max);
      }
      return result;
    };
    
    // 方法二:使用双端队列
    const maxSlidingWindow2 = (nums, k) => {
      const result = [];
      const queue = [];
      for (let i = 0; i < nums.length; i++) {
        while (queue.length > 0 && nums[queue[queue.length - 1]] < nums[i]) {
          queue.pop();
        }
        queue.push(i);
        if (i - queue[0] >= k) {
          queue.shift();
        }
        if (i >= k - 1) {
          result.push(nums[queue[0]]);
        }
      }
      return result;
    };
    
  2. 347. 前K个高频元素

    前K个高频元素问题如下:给定一个数组nums和一个整数k,请找出数组中出现频率前k高的元素。

    Carl老师同样提供了两种解决方法。

    方法一:哈希表

    使用哈希表来统计每个元素的出现频率。具体步骤如下:

    1. 创建一个哈希表,并将数组nums中的每个元素作为哈希表的键,元素出现的次数作为哈希表的值。
    2. 遍历哈希表,找出出现频率前k高的元素。
    3. 将这些元素按照出现频率从高到低排序。
    4. 返回排序后的前k个元素。

    方法二:桶排序

    桶排序是一种非比较排序算法,它将数组中的元素分配到多个桶中,然后对每个桶中的元素进行排序。利用桶排序,我们可以更高效地解决前K个高频元素问题。

    具体步骤如下:

    1. 计算数组nums中元素的最大值和最小值。
    2. 根据最大值和最小值,创建若干个桶。
    3. 将数组nums中的元素分配到相应的桶中。
    4. 对每个桶中的元素进行排序。
    5. 将各个桶中的元素按照出现频率从高到低连接起来。
    6. 返回排序后的前k个元素。

    示例代码:

    // 方法一:哈希表
    const topKFrequent1 = (nums, k) => {
      const map = new Map();
      for (const num of nums) {
        map.set(num, (map.get(num) || 0) + 1);
      }
      const sortedMap = new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
      const result = [];
      for (const [num, count] of sortedMap) {
        result.push(num);
        if (result.length === k) {
          break;
        }
      }
      return result;
    };
    
    // 方法二:桶排序
    const topKFrequent2 = (nums, k) => {
      const maxValue = Math.max(...nums);
      const minValue = Math.min(...nums);
      const bucketSize = Math.floor((maxValue - minValue) / k) + 1;
      const buckets = [];
      for (let i = 0; i <= k; i++) {
        buckets.push([]);
      }
      for (const num of nums) {
        const bucketIndex = Math.floor((num - minValue) / bucketSize);
        buckets[bucketIndex].push(num);
      }
      const result = [];
      for (const bucket of buckets) {
        bucket.sort((a, b) => b - a);
        result.push(...bucket);
        if (result.length >= k) {
          break;
        }
      }
      return result.slice(0, k);
    };
    

总结与展望

通过对以上两道经典算法题的深入解析,我们不仅学习了具体的问题求解方法,也对栈和队列等基础数据结构有了更深刻的理解。这些知识和经验将为我们日后的算法编程实践奠定坚实的基础。

值得一提的是,本文仅探讨了最基本的数据结构和算法的应用,算法世界还有着广阔的领域等待我们去探索。保持对算法的热情和好奇心,不断学习和实践,才能在算法思维的道路上不断精进,解锁更多难题的解决方案。