Mercer-Lee的空间

vuePress-theme-reco Mercer-Lee的空间    2018 - 2024
Mercer-Lee的空间 Mercer-Lee的空间

Choose mode

  • dark
  • auto
  • light
TimeLine
分类
  • 数据结构和算法
  • 后端
  • 运维
  • 前端
  • 工具
  • 语言
标签
我的GitHub (opens new window)
author-avatar

Mercer-Lee的空间

27

文章

29

标签

TimeLine
分类
  • 数据结构和算法
  • 后端
  • 运维
  • 前端
  • 工具
  • 语言
标签
我的GitHub (opens new window)
  • 经典算法之队列

    • 队列
      • 最近请求次数
      • 字符串中的第一个唯一字符
      • 队列的最大值

经典算法之队列

vuePress-theme-reco Mercer-Lee的空间    2018 - 2024

经典算法之队列


Mercer-Lee的空间 2020-09-13 算法 数据结构 队列

# 队列

队列是一种数据结构,它遵循先进先出(FIFO)原则的一组有序的项。队列在尾部添加新元素,并从顶部移除元素。最新添加的元素必须排在队列的末尾。我们利用LeetCode的题目来看下队列在算法中的应用:

# 最近请求次数

这道题题目如下:

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

请实现 RecentCounter 类:

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

示例:

输入:
inputs = ["RecentCounter", "ping", "ping", "ping", "ping"]
inputs = [[], [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

这道题其实不难,但是说实话我一开始也没看明白,这道题属于阅读理解题:看代码就能理解:

var RecentCounter = function() {
  this.q = [];    // 初始化队列
};

RecentCounter.prototype.ping = function(t) {
  this.q.push(t);   // 每次进来都放入队列
  while(this.q[0] < t - 3000){    // 遍历队列,如果值小于这次进来的值减去3000就移除出队列
    this.q.shift();
  }
  return this.q.length;   // 返回队列的长度
};

这道题没啥难度,能理解问题就行,但是这道题是最典型的队列问题,用来入门是相当好的。

# 字符串中的第一个唯一字符

题目如下:

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

 
示例:

s = "leetcode"
返回 0

s = "loveleetcode"
返回 2
 

提示:你可以假定该字符串只包含小写字母。

这道题是LeetCode的一道很经典的题目,这道题有很多解法,这边我们了解两种解法就可以了,首先是哈希算法,不了解哈希算法可以看这里 (opens new window),我以前写过哈希算法的文章。哈希版本:

var firstUniqChar = function(s) {
  let map = new Map();    // 声明一个Map
  let traget;
  // 首先把每个字符串的字符遍历,如果Map不存在就set,存在就+1
  for(let i = 0; i < s.length; i++) {
    if(map.has(s[i])){
      map.set(s[i], map.get(s[i])+1);
    }
    else{
      map.set(s[i], 1);
    }
  }
  // 遍历Map,只要找到了只存在过一次的字符立马返回它在字符串中的索引
  for (let [key, value] of map.entries()) {
    if(value === 1) {
      traget = key;
      return s.indexOf(traget);
    }
  }

  return -1
};

这个版本比较简单,而且很好看懂,所以大部分情况下都是用这个算法版本,接下来就是队列的版本:

var firstUniqChar = function(s) {
  const position = new Map();   // 声明Map结构
  const q = [];   // 声明一个队列
  // for...of循环字符串,如果Map中不存在这个字符,那么Map中set这个值和队列push进去一个数组。
  for (let [i, ch] of Array.from(s).entries()) {
    if (!position.has(ch)) {
      position.set(ch, i);
      // 队列是个二维数组,存的值是第一个值是字符,第二个值是这个字符在字符串中的索引的数组。
      q.push([s[i], i]);    
    } else {
      // 如果Map存在,证明这个字符重复了,那么Map中此值变为-1
      position.set(ch, -1);
      // 这里做了一个队列的优化处理,只要队列不是空的且第一个值不是-1(不是重复的)那么就不处理
      while (q.length && position.get(q[0][0]) === -1) {
        q.shift();    // 如果第一个是重复了的就在队列中移除掉
      }
    }
  }
  return q.length ? q[0][1] : -1;   // 如果队列不是空的,返回队列第一个的值的第二个值(即是索引)
};

这个版本相对于上面一个版本比较复杂,利用了队列,很多人难理解的应该是二维数组和为什么要while处理,二维数组自己在纸上手动代入值一下就知道了。while处理其实是队列的一个“延迟删除”技巧,因为题目要求是字符串中的第一个唯一字符,而我们是按顺序遍历的,所以只要不是队首的字符重复了,那么就不用去管。

# 队列的最大值

这道题在LeetCode中给出了中等:

请定义一个队列并实现函数 max_value 得到队列里的最大值,
要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

这道题有两种解法,第一种是暴力算法(BF)来处理队列: 算法代码如下:

var MaxQueue = function() {
    this.q = [];    // 声明一个队列
    this.bengin = 0;    // 队列的队头指针
    this.end = 0;   // 队列的队尾指针
};

MaxQueue.prototype.max_value = function() {
    let anx = -1;
    // 暴力遍历每个值,遍历出最大的返回
    for(let i = this.bengin; i < this.end; i++){
        anx = Math.max(anx,this.q[i])
    }
    return anx;
};

MaxQueue.prototype.push_back = function(value) {
    this.q[this.end++] = value;    // 队尾指针增加,并且入队
};

MaxQueue.prototype.pop_front = function() {
    if(this.bengin === this.end){
        return -1;
    }
    return this.q[this.bengin++];   //  队头指针增加
}

第二种解法则是利用双端队列和单调递减来解决:

var MaxQueue = function() {
    this.queue = [];    // 数值数组
    this.deque = [];    // 双端队列(指允许两端都可以进行入队和出队操作的队列)
};

MaxQueue.prototype.max_value = function() {
    return this.queue.length ? this.deque[0] : -1   // 双端队列第一个就是最大值
};

MaxQueue.prototype.push_back = function(value) {
  // 先放入数值数组里面
    this.queue.push(value);
    
    while(this.deque.length && this.deque[this.deque.length - 1] < value){
      // 只要队列不是空的且队尾的值小于传入的值,那么就把队尾出队,
      // 这样就能保证队头永远都是最大的值,而且队列是单调递减的。
        this.deque.pop();
    }
    this.deque.push(value);   // 把值入队到队尾
};

MaxQueue.prototype.pop_front = function() {
    if(this.queue.length === 0) return -1;
    const val = this.queue.shift();
    // 如果值跟队列的队头的值一样的,那么就需要把它出队
    if(this.deque[0] === val) {
         this.deque.shift();
    }
    return val
}