算法-队列

182 阅读4分钟

数据结构-队列Queue

队列是遵循先进先出(FIFO)原则的有序集合。

队列的基本结构:

  • enqueue(e) 进队
  • dequeue() 出队
  • isEmpty() 是否是空队
  • front() 获取队头元素
  • clear() 清空队
  • size() 获取队列长度

代码实现

class Queue {
  constructor() {
    this.items = []
  }
  enqueue(val) {
    this.items.push(val)
  }
  dequeue() {
    return this.items.shift()
  }
  isEmpty() {
    return this.items.length === 0
  }
  // 获取对头元素
  front() {
    return this.items[0]
  }
  clear() {
    this.items = []
  }
  size() {
    return this.items.length
  }
}
  • 使用队列
  const q1 = new Queue()
  q1.enqueue(233)
  q1.enqueue('aaa')
  console.log(q1.dequeue());
  q1.enqueue('ccc')
  console.log(q1)

双端队列Deque

Deque在基本队列基础上, 在两端添加和移除元素

双端队列结构:

  • addFront(e) 头部进队
  • removeFront() 头部出队
  • addBack(e) 尾部进队
  • removeBack() 尾部出队
  • front() 获取队头元素
  • back() 获取队尾元素
  • isEmpty() 是否是空队
  • clear() 清空队
  • size() 获取队列长度

代码实现

  // 双端队列
  class Deque {
    constructor() {
      this.items = []
    }
    addFront(e) {
      this.items.unshift(e)
    }
    removeFront() {
      return this.items.shift()
    }
    addBack(e) {
      this.items.push(e)
    }
    removeBack() {
      return this.items.pop()
    }
    isEmpty() {
      return this.items.length === 0
    }
    // 获取对头元素
    front() {
      return this.items[0]
    }
    // 获取对尾元素
    back() {
      if(this.isEmpty()) {
        return null
      }
      return this.items[this.items.length-1]
    }
    clear() {
      this.items = []
    }
    size() {
      return this.items.length
    }
  }
  • 使用双端队列
  const q1 = new Deque()
  q1.addFront(233)
  q1.addBack('aaa')
  q1.addBack('bbb')
  q1.addFront('666')
  console.log(q1.queue);
  console.log(q1.front())
  console.log(q1.back())
  console.log(q1.isEmpty())
  console.log(q1.size())
  q1.removeFront()
  q1.removeBack()
  console.log(q1)

应用-翻转字符串里的单词

给你一个字符串s,逐个翻转字符串中的所有单词

单词是由非空格字符组成的字符串。s中使用至少一个空格将字符串中的单词分隔开。

请你返回一个翻转s中单词顺序并用单个空格相连的字符串。

说明:

  • 输入字符串 s 可以在前面、后面或者单词间包含多余 - 的空格。
  • 翻转后单词间应当仅用一个空格分隔。
  • 翻转后的字符串中不应包含额外的空格。

示例1

输入: "the sky is blue"
输出: "blue is sky the"

示例2

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例3

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

思路

  • 移除字符串首尾的多余空格
  • 遍历字符串,取出一个字符,判断字符为空字符
    • 如果是空字符,判断word是否有值
      • word有值,就把word推入队列的头部,重置word
  • 当前字符不是空字符,把word和当前字符合并成新字符串

代码实现

/**
 * @param {string} s
 * @return {string}
 */
const reverseWords = function (s) {
  // 移除首尾空格
  const newStr = s.trim()
  const deque = []
  let word = ''
  for (let i = 0; i < newStr.length; i++) {
    const ch = newStr[i]
    // 遇到空格
    if (ch === ' ') {
      if (word) {
        // word有值,就把word推入队列中
        deque.unshift(word)
        word = ''
      }
    } else {
      word += ch
    }
  }
  // 最后一个单词需要手动处理
  if (word) {
    deque.unshift(word)
  }

  return deque.join(' ')
}

滑动窗口

滑动窗口就是一个运行在一个大数组上的子列表,该数组是一个底层元素集合。

假设有数组[a b c d e f g h ],一个大小为 3 的 滑动窗口在其上滑动,则有:

[a b c]
  [b c d]
    [c d e]
      [d e f]
        [e f g]
          [f g h]

一般情况下就是使用这个窗口在数组的 合法区间 内进行滑动,同时 动态地 记录一些有用的数据.

图示如下

image.png

无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

思路

  • 准备两个指针start,end分别是窗口的左右边界,准备一个map存储每个字符串和对应的索引
  • 遍历字符串
  • 取出一个字符,判断是否在map中
    • 如果存在,更新start
      • 从map中取出当前字符串之前出现过的索引,并与start比较大小,取其中的较大的,赋值给start
        • 主要为了应对"abba"情况,需要保持start为较大的值,不要倒退以前的旧值
    • 把当前字符和索引记录到map中
    • 更新max:取当前窗口字符串长度和max中较大者
    • 右边界指针end加1

代码实现

/**
 * @param {string} s
 * @return {number}
 */
const lengthOfLongestSubstring = function (s) {
  let max = 0
  const map = new Map()
  let start = 0
  let end = 0
  while (end < s.length) {
    const ch = s[end]
    if (map.has(ch)) {
      // Math.max获取start,
      // 是为了应对"abba"情况,需要保持start为较大的值,不要倒退以前的旧值
      //    此时start = 2, map.get('a') = 0 ,应该取start=2
      start = Math.max(map.get(ch) + 1, start)
    }
    map.set(ch, end)
    max = Math.max(max, end - start + 1)
    end++
  }
  
  return max
}

两个栈实现队列

用两个栈实现一个队列。

队列的声明如下:

  • 请实现它的两个函数 appendTaildeleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。 (若队列中没有元素,deleteHead 操作返回 -1 )

思路

  • 栈后进先出,队列先进先出,栈只支持一端操作
  • 需要在移除元素时,保证顺序和添加时一致,先进来的会被先移除
  • stack1=[1, 2, 3]
  • 移除也要是1,2,3,那么可以把stack1中元素出栈,压入栈stack2中,变为stack2=[3, 2, 1]

image.png

实现思路

  • 元素添加在stack1中
  • 移除元素在stack2中,判断stack2是否为空
    • stack2为空,把stack1中所有元素出栈,放入stack2
    • 再次判断stack2为是否为空,如果还是为空,返回-1,否则移除栈顶元素并返回该元素

代码实现

class CQueue {
  constructor() {
    this.stack1 = []
    this.stack2 = []
  }
  appendTail(value) {
    // 添加元素,压入栈1
    this.stack1.push(value)
  }
  deleteHead() {
    // 判断栈2是否为空,为空,就把栈1元素放入栈2中
    if (this.stack2.length === 0) {
      while (this.stack1.length) {
        this.stack2.push(this.stack1.pop())
      }
    }
    if (this.stack2.length === 0) {
      return -1
    }
    return this.stack2.pop()
  }
}

滑动窗口最大值问题

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
 [1  3  -1] -3  5  3  6  7      3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例2

输入:nums = [1,-1], k = 1
输出:[1,-1]

提示:你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

方式一:暴力破解法

  • 定义窗口的左右边界指针leftright
  • 遍历数组,结束条件 right >= 数组长度
    • 每次循环的max初始值为-Infinity,在后面遍历窗口内元素,不断更新max
    • i <= right时,取出窗口内元素,更新max
    • 完成一次窗口元素遍历,把max加入到结果数组中,rightleft分别加1,把窗口往右移动一个位置

代码实现

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
const maxSlidingWindow = function (nums, k) {
  let left = 0
  let right = k - 1
  const maxList = []
  while (right < nums.length) {
    let max = -Infinity
    // 在窗口内,使用第三枚指针,从左边界不断往右移动,更新最大值
    for (let i = left; i <= right; i++) {
      const item = nums[i]
      max = Math.max(max, item)
    }
    // 记录当前窗口的最大值
    maxList.push(max)
    // 窗口整体往右边移动一个位置
    right++
    left++
  }
  return maxList
}

方式二:使用单调队列

参考

leetcode-cn.com/problems/sl…

先说下双端队列定义,可以在首部和尾部新增或者移除元素的队列。

单调队列:在双端队列基础上,头部到尾部的元素是递减或者递增的

思路:

  • 定义左右边界指针left,right,准备一个单调队列queue,一个结果数组result
  • 形成窗口阶段:
    • 取出元素,直到right大于等于k结束循环
      • 判断元素是否大于等于队尾元素
        • 如果成立,移除队尾元素
      • 把当前元素的索引存储到队尾
  • 取出队首的元素,存储到结果中,这是第一个窗口的最大值
  • 移动窗口阶段:直到right等于k结束循环
    • 取出元素,判断元素是否大于等于队尾元素
      • 如果成立,移除队尾元素
    • 把当前元素的索引存储到队尾
    • 计算窗口左边界的索引值left
    • 判断left是否大于队首,如果大于,说明队首元素已经不再窗口内了,那就移除队首
    • 存储当前窗口最大值到结果数组中:取出队首,获取对应的值,存储到数组
    • right加1,窗口往后移动一个位置

代码实现

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
const maxSlidingWindow = function (nums, k) {
  if (nums.length < 1) {
    return []
  }
  const n = nums.length
  // 单调队列
  const queue = []
  const result = []
  let left = 0
  let right = 0
  // 形成窗口阶段
  while (right < k) {
    // 队列不为空且当前元素大于队尾元素,就移除队尾元素
    while (queue.length && nums[queue[queue.length - 1]] <= nums[right]) {
      queue.pop()
    }
    // 当前元素的索引添加到队尾
    queue.push(right)
    right++
  }
  // 取出队首的元素,存储到结果中,这是第一个窗口的最大值
  result.push(nums[queue[0]])

  // 移动窗口阶段(从k位置移动到n-1)
  while (right < n) {
    while (queue.length && nums[queue[queue.length - 1]] <= nums[right]) {
      queue.pop()
    }
    queue.push(right)

    // 计算窗口左边界的索引值
    left = right - k + 1
    // 判断队首元素是否在窗口内,如果不在窗口内,就移除队首,
    if (queue[0] < left) {
      queue.shift()
    }
    // 把当前窗口最大值添加到数组中
    result.push(nums[queue[0]])
    right++
  }
  return result
}