vlambda博客
学习文章列表

《剑指Offer:专项突破版》 - 队列部分 JavaScript 题解

《剑指Offer:专项突破版》是一个算法题集。该题单包含了程序员在准备面试过程中必备的数据结构与算法知识。具体包含:

  • 数据结构:整数、数组、字符串、链表、栈、栈、队列、树、堆和前缀树。
  • 算法:二分查找、排序、回溯法、动态规划和图搜索。

本文来分享下队列部分题的解法~

队列介绍

队列是操作受限的数组。只能在数组的尾部插入元素,在数组的头部删除元素。它的特点是“先入先出”:先入队列的元素先出来。

基于 js 的数组,很容易实现队列:

class Queue {
  constructor() {
    this.list = [];
    this.size = 0;
  }
  // 入队
  enqueue(item) {
    this.size++;
    this.list.push(item);
  }
  // 出队
  dequeue() {
    this.size--;
    return this.list.shift();
  }
}

题1 - 剑指 Offer II 041. 滑动窗口的平均值

题目:请实现如下类型MovingAverage,计算滑动窗口中所有数字的平均值,该类型构造函数的参数确定滑动窗口的大小,每次调用成员函数next时都会在滑动窗口中添加一个整数,并返回滑动窗口中所有数字的平均值。

可以用队列来存滑动窗口的数据。添加整数时入队,超过窗口大小时出队。为了降低求平均值的时间复杂度,要缓存窗口数字的和。调用成员函数next时,当前的和 = 缓存的和 + 入队的数字 - 出队的数字平均值 = 当前的和 / 数字数量。代码如下:

const MovingAverage = function (size{
  this.queue = new Queue();
  this.size = size;
  this.sum = 0;
};

MovingAverage.prototype.next = function (val{
  if (this.queue.size === this.size) {
    this.sum -= this.queue.dequeue();
  }
  this.sum += val;
  this.queue.enqueue(val);
  const average = this.sum / this.queue.size;
  return average;
};

题2 - 剑指 Offer II 042. 最近请求次数

题目:请实现如下类型RecentCounter,它是统计过去3000ms内的请求次数的计数器。该类型的构造函数RecentCounter初始化计数器,请求数初始化为0;函数ping(int t)在时间t添加一个新请求(t表示以毫秒为单位的时间),并返回过去3000ms内(时间范围为[t-3000,t])发生的所有请求数。假设每次调用函数ping的参数t都比之前调用的参数值大。

用队列来存之前的请求时间。当添加新的请求时:

  1. 将新的请求时间和队列中的请求时间比较,超出时间范围的时间的出队。
  2. 将新的请求时间入队。

代码如下:

const PERIOD = 3000;
const RecentCounter = function ({
  this.list = [];
};

RecentCounter.prototype.ping = function (t{
  while (t - this.list[0] > PERIOD) {
    this.dequeue();
  }
  this.enqueue(t);
  return this.list.length;
};

RecentCounter.prototype.enqueue = function (t{
  this.list.push(t);
};

RecentCounter.prototype.dequeue = function ({
  return this.list.shift();
};

题3 - 剑指 Offer II 043. 往完全二叉树添加节点

题目:在完全二叉树中,除最后一层之外其他层的节点都是满的(第n层有2n-1个节点)。最后一层的节点可能不满,该层所有的节点尽可能向左边靠拢。例如,图7.3中的4棵二叉树均为完全二叉树。实现数据结构CBTInserter有如下3种方法。
● 构造函数CBTInserter(TreeNode root),用一棵完全二叉树的根节点初始化该数据结构。

● 函数insert(int v)在完全二叉树中添加一个值为v的节点,并返回被插入节点的父节点。例如,在下图a所示的完全二叉树中添加一个值为7的节点之后,二叉树下图b所示,并返回节点3。在下图b所示的完全二叉树中添加一个值为8的节点之后,二叉树下图c所示,并返回节点4。在下图c所示的完全二叉树中添加节点9会得到下图d所示的二叉树并返回节点4。

● 函数get_root()返回完全二叉树的根节点。

可以用队列来存可以添加子节点的节点(左节点或右节点是空的)。添加节点时:

  1. 将新节点添加在队列的第一个节点上。
  2. 添加后,若该节已满(左右节点都有元素)。该节点出列,节点的左右节点入列。

查找可以添加子节点的节点要遍历树。存在多种遍历树节点的顺序。结合本题的情况,适合用广度优先来遍历树。

广度优先遍历指按顺序访问一层的每个节点,访问完后,再访问下一个层。

用队列很容易实现树的广度优先遍历。算法为:

  1. 将树的根节点入列。
  2. 出列。访问出列的节点。将节点的子节点入列。
  3. 重复上一步,直到队列为空。

二叉树的数据结构如下:

function TreeNode(val, left, right{
  this.val = (val===undefined ? 0 : val)
  this.left = (left===undefined ? null : left)
  this.right = (right===undefined ? null : right)
}

完整代码如下:

const CBTInserter = function (root{
  this.root = root;
  this.list = [root];
  let curr;
  while (this.list[0].left !== null && this.list[0].right !== null) {
    curr = this.dequeue();
    this.enqueue(curr.left);
    this.enqueue(curr.right);
  }
};

CBTInserter.prototype.insert = function (v{
  const parentNode = this.list[0];
  const node = new TreeNode(v);
  if (parentNode.left === null) {
    parentNode.left = node;
  } else {
    parentNode.right = node;
    this.dequeue();
    this.enqueue(parentNode.left);
    this.enqueue(parentNode.right);
  }
  return parentNode.val;
};

CBTInserter.prototype.get_root = function ({
  return this.root;
};

CBTInserter.prototype.enqueue = function (t{
  this.list.push(t);
};

CBTInserter.prototype.dequeue = function ({
  return this.list.shift();
};

题4 - 剑指 Offer II 044. 二叉树每层的最大值

题目:输入一棵二叉树,请找出二叉树中每层的最大值。例如,输下图的二叉树,返回各层节点的最大值[3,4,9]。

《剑指Offer:专项突破版》 - 队列部分 JavaScript 题解

题目要求找树每层的最大值。树的广度优先遍历正好是按层的顺序来的。因此,只要在遍历每层节点的过程中,找出最大值即可。

可以用队列实现广度优先遍历树。要知道当前节点在哪一层,可以通过在节点入队时,传入层的信息。代码如下:

const node = curr.node;
const level = curr.level;
if (node.left) {
  list.push({
    node: node.left,
    level: level + 1,
  });
}
if (node.right) {
  list.push({
    node: node.right,
    level: level + 1,
  });
}

完整代码如下:

const largestValues = function (root{
  if(!root) return [];
  const list = [
    {
      node: root,
      level1,
    },
  ];
  const maxValueArr = [];
  let curr;
  while (list.length > 0) {
    curr = list.shift();
    const node = curr.node;
    const level = curr.level;
    maxValueArr[level - 1] = Math.max(maxValueArr[level - 1] === undefined ? Number.NEGATIVE_INFINITY : maxValueArr[level - 1], node.val);
    if (node.left) {
      list.push({
        node: node.left,
        level: level + 1,
      });
    }
    if (node.right) {
      list.push({
        node: node.right,
        level: level + 1,
      });
    }
  }
  return maxValueArr;
};

题5 - 剑指 Offer II 045. 二叉树最底层最左边的值

题目:如何在一棵二叉树中找出它最低层最左边节点的值?假设二叉树中最少有一个节点。

本题和上题类似,用队列实现树的广度优先遍历。存每层最左边的节点值,最后取最后一层的。代码如下:

const findBottomLeftValue = function (root{
  if (!root) return null;
  if (!root.left && !root.right) return root.val;
  const levelLeftValue = [];
  const list = [
    {
      node: root,
      level1,
      isLefttrue,
    },
  ];
  let curr;
  while (list.length > 0) {
    curr = list.shift();
    const node = curr.node;
    const level = curr.level;
    if (levelLeftValue[level - 1] === undefined) {
      levelLeftValue[level - 1] = node.val;
    }

    if (node.left) {
      list.push({
        node: node.left,
        level: level + 1,
      });
    }
    if (node.right) {
      list.push({
        node: node.right,
        level: level + 1,
      });
    }
  }

  // 取最后一个
  const res = levelLeftValue
    .filter((item) => item !== undefined)
    .slice(-1)[0];
  return res;
};

题6 - 剑指 Offer II 046. 二叉树的右侧视图

题目:给定一棵二叉树,如果站在该二叉树的右侧,那么从上到下看到的节点构成二叉树的右侧视图。例如,下图中二叉树的右侧视图包含节点8、节点10和节点7。请写一个函数返回二叉树的右侧视图节点的值。

本题和上题类似,用队列实现树的广度优先遍历。存每层最右边的节点。代码如下:

const rightSideView = function(root{
  if (!root) return [];
  if (!root.left && !root.right) return [root.val];
  const levelRightValue = [];
  const list = [
    {
      node: root,
      level1,
      isLefttrue,
    },
  ];
  let curr;
  while (list.length > 0) {
    curr = list.shift();
    const node = curr.node;
    const level = curr.level;
    levelRightValue[level - 1] = node.val;

    if (node.left) {
      list.push({
        node: node.left,
        level: level + 1,
      });
    }
    if (node.right) {
      list.push({
        node: node.right,
        level: level + 1,
      });
    }
  }

  return levelRightValue;
};

总结

可以用队列实现树的广度优先遍历。在遍历时,获取第几层之类的信息,可以通过节点入队时传入。

当然,根据队列 “先入先出” 的特点,队列还会被用在接口的并发控制等场景。

相关阅读

参考资料

[1] [2] [3] [4] [5] [6]