数据结构-队列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]
一般情况下就是使用这个窗口在数组的 合法区间 内进行滑动,同时 动态地 记录一些有用的数据.
图示如下
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 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中取出当前字符串之前出现过的索引,并与start比较大小,取其中的较大的,赋值给start
- 把当前字符和索引记录到map中
- 更新max:取当前窗口字符串长度和max中较大者
- 右边界指针end加1
- 如果存在,更新start
代码实现
/**
* @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
}
两个栈实现队列
用两个栈实现一个队列。
队列的声明如下:
-
请实现它的两个函数
appendTail和deleteHead,分别完成在队列尾部插入整数和在队列头部删除整数的功能。 (若队列中没有元素,deleteHead操作返回-1)
思路
- 栈后进先出,队列先进先出,栈只支持一端操作
- 需要在移除元素时,保证顺序和添加时一致,先进来的会被先移除
- 如
stack1=[1, 2, 3] - 移除也要是
1,2,3,那么可以把stack1中元素出栈,压入栈stack2中,变为stack2=[3, 2, 1]
实现思路
- 元素添加在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 ≤ 输入数组的大小。
方式一:暴力破解法
- 定义窗口的左右边界指针
left和right - 遍历数组,结束条件
right >= 数组长度- 每次循环的
max初始值为-Infinity,在后面遍历窗口内元素,不断更新max - 当
i <= right时,取出窗口内元素,更新max - 完成一次窗口元素遍历,把
max加入到结果数组中,right和left分别加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
}
方式二:使用单调队列
参考
先说下双端队列定义,可以在首部和尾部新增或者移除元素的队列。
单调队列:在双端队列基础上,头部到尾部的元素是递减或者递增的
思路:
- 定义左右边界指针
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
}