《剑指 Offer (第 2 版)》队列部分 JavaScript 题解
《剑指 Offer(第 2 版)》通行全球的程序员经典面试秘籍。剖析典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。
最近,把「队列」部分的题刷完了。本文来分享下这些题的解法
09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail
和 deleteHead
,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead
操作返回 -1 )
「示例 1:」
输入: ["CQueue","appendTail","deleteHead","deleteHead"] [[],[3],[],[]] 输出:[null,null,3,-1]
「示例 2:」
输入: ["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"] [[],[],[5],[2],[],[]] 输出:[null,-1,null,null,5,2]
「提示:」
1 <= values <= 10000
最多会对 appendTail、deleteHead 进行 10000 次调用
题目地址:https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/
「解题思路」
首先我们知道,队列是先入先出,栈是后入先出,所以知道了这两个东西的特性,我们很容易就能根据题意使用两个栈来模拟队列。
首先,两个栈分工不同,一个为入队栈,一个为出队栈,各自负责入队和出队。
入队操作,直接压入入队栈即可,出队操作需要优先检查出队栈是否有数据,若无,需要从入队栈倒入后再操作。
var CQueue = function() { this.enterStack = []; this.leaveStack = []; }; /** * @param {number} value * @return {void} */ CQueue.prototype.appendTail = function(value) { this.enterStack.push(value); }; /** * @return {number} */ CQueue.prototype.deleteHead = function() { if (this.leaveStack.length) { return this.leaveStack.pop(); } else { while(this.enterStack.length) { this.leaveStack.push(this.enterStack.pop()); } return this.leaveStack.length ? this.leaveStack.pop() : -1; } }; /** * Your CQueue object will be instantiated and called as such: * var obj = new CQueue() * obj.appendTail(value) * var param_2 = obj.deleteHead() */
50. 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。s 只包含小写字母。
「示例 1:」
输入:s = "abaccdeff" 输出:'b'
「示例 2:」
输入:s = "" 输出:' '
「限制:」
0 <= s 的长度 <= 50000
题目地址:https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/
「解题思路」
利用循环找出第一个数的位置,再次查询这个数位置之后是否还存在相同的数字,存在则继续,不存在则输出。
/** * @param {string} s * @return {character} */ var firstUniqChar = function(s) { for(let i = 0;i < s.length; i++) { const k = s.indexOf(s[i]); const e = s.indexOf(s[i], k+1); if(e === -1) return s[i]; } return " "; };
9 - I. 滑动窗口的最大值
给定一个数组 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
「提示:」
你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。
题目地址:https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/
「解题思路」
由于我们需要求出的是滑动窗口的最大值,如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j 的左侧(i<j),并且 i 对应的元素不大于 j 对应的元素(nums[i]≤nums[j]
),那么会发生什么呢?
当滑动窗口向右移动时,「只要 i 还在窗口中,那么 j 一定也还在窗口中」,这是 i 在 j 的左侧所保证的。因此,由于 nums[j]
的存在,nums[i]
一定不会是滑动窗口中的最大值了,我们可以将 nums[i]
永久地移除。
因此我们可以使用一个队列存储所有还没有被移除的下标。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组 nums
中对应的值是严格单调递减的。因为如果队列中有两个相邻的下标,它们对应的值相等或者递增,那么令前者为 i,后者为 j,就对应了上面所说的情况,即 nums[i]
会被移除,这就产生了矛盾。
当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果前者大于等于后者,那么队尾的元素就可以被永久地移除,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。但与方法一中相同的是,此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。
为了可以同时弹出队首和队尾的元素,我们需要使用双端队列。满足这种单调性的双端队列一般称作「单调队列」。
/** * @param {number[]} nums * @param {number} k * @return {number[]} */ var maxSlidingWindow = function(nums, k) { if (!k) return []; const n = nums.length; const q = []; for (let i = 0; i < k; i++) { while (q.length && nums[i] >= nums[q[q.length - 1]]) { q.pop(); } q.push(i); } const ans = [nums[q[0]]]; for (let i = k; i < n; i++) { while (q.length && nums[i] >= nums[q[q.length - 1]]) { q.pop(); } q.push(i); while (q[0] <= i - k) { q.shift(); } ans.push(nums[q[0]]); } return ans; }
59 - II. 队列的最大值
请定义一个队列并实现函数 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]
「限制:」
- 1 <= push_back,pop_front,max_value的总操作数 <= 10000
- 1 <= value <= 10^5
题目地址:https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/
「解题思路」为了解决上述问题,我们只需记住当前最大值出队后,队列里的下一个最大值即可。
具体方法是使用一个双端队列 deque
,在每次入队时,如果 deque
队尾元素小于即将入队的元素 value
,则将小于 value
的元素全部出队后,再将 value
入队;否则直接入队。
这时,辅助队列 deque
队首元素就是队列的最大值。
var MaxQueue = function() { this.queue = []; this.deque = []; }; /** * @return {number} */ MaxQueue.prototype.max_value = function() { return this.deque.length ? this.deque[0] : -1; }; /** * @param {number} value * @return {void} */ MaxQueue.prototype.push_back = function(value) { while(this.deque.length && this.deque[this.deque.length - 1] < value) { this.deque.pop(); } this.queue.push(value); this.deque.push(value); }; /** * @return {number} */ MaxQueue.prototype.pop_front = function() { if (!this.queue.length) return -1; const front = this.queue.shift(); if (this.deque[0] === front) { this.deque.shift(); } return front; }; /** * Your MaxQueue object will be instantiated and called as such: * var obj = new MaxQueue() * var param_1 = obj.max_value() * obj.push_back(value) * var param_3 = obj.pop_front() */