基础知识
栈是一种LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。ECMAScript为数组专门提供了 push() 和 pop() 方法,以便实现类似栈的行为。 push() 方法可以接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而 pop() 方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项。
队列数据结构的访问规则是FIFO(Fist-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项。ECMAScript为数组专门提供了 shift() 和 unshift() 方法,以便实现类似队列的行为。由于 push() 是向数组末端添加数组项的方法,因此要模拟队列只需一个从数组前端取得数组项的方法。实现这一操作的数组方法就是 shift() ,它能够移除数组中的第一个项并返回该项,同时将数组长度减1。unshift() 与 shift() 的用途相反:它能在数组前端添加任意个数组项并返回新数组的长度。因此,同时使用 unshift() 和 pop() 方法,可以从相反的方向来模拟队列,即在数组的前端添加数组项,从数组末端移除数组项。
将push()和pop()结合在一起,我们就可以实现类似栈的行为。将shift()和push()方法结合在一起,可以像使用队列一样使用数组。
技巧
1.由于栈结构的特殊性,非常适合做对称匹配类的题目。
2.递归的实现是栈:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
3.一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
4.单调队列。
5.优先级队列。
题目
1.用栈实现队列
简单
思路:
两个栈一个输入栈,一个输出栈。在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
var MyQueue = function() {
this.stackIn = [];
this.stackOut = [];
};
/**
* @param {number} x
* @return {void}
*/
MyQueue.prototype.push = function(x) {
this.stackIn.push(x);
};
/**
* @return {number}
*/
MyQueue.prototype.pop = function() {
if(this.stackOut.length){
return this.stackOut.pop();
}
while(this.stackIn.length){
this.stackOut.push(this.stackIn.pop());
}
return this.stackOut.pop();
};
/**
* @return {number}
*/
MyQueue.prototype.peek = function() {
const x = this.pop();
this.stackOut.push(x);
return x;
};
/**
* @return {boolean}
*/
MyQueue.prototype.empty = function() {
if(!this.stackIn.length&&!this.stackOut.length)return true;
return false;
};
/**
* Your MyQueue object will be instantiated and called as such:
* var obj = new MyQueue()
* obj.push(x)
* var param_2 = obj.pop()
* var param_3 = obj.peek()
* var param_4 = obj.empty()
*/
2.用队列实现栈
简单
思路:
1.两个队。用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
2.一个队。一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。
var MyStack = function() {
this.queue1 = [];
this.queue2 = [];
};
/**
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
this.queue1.push(x);
};
/**
* @return {number}
*/
MyStack.prototype.pop = function() {
if(!this.queue1.length&&this.queue2.length){
[this.queue1,this.queue2] = [this.queue2,this.queue1];
}
while(this.queue1.length>1){
this.queue2.push(this.queue1.shift());
}
return this.queue1.shift();
};
/**
* @return {number}
*/
MyStack.prototype.top = function() {
const x = this.pop();
this.queue1.push(x);
return x;
};
/**
* @return {boolean}
*/
MyStack.prototype.empty = function() {
if(!this.queue1.length&&!this.queue2.length){
return true;
}
return false;
};
/**
* Your MyStack object will be instantiated and called as such:
* var obj = new MyStack()
* obj.push(x)
* var param_2 = obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.empty()
*/
var MyStack = function() {
this.queue = [];
};
/**
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
this.queue.push(x);
};
/**
* @return {number}
*/
MyStack.prototype.pop = function() {
let len = this.queue.length;
while(len>1){
this.queue.push(this.queue.shift());
len--;
}
return this.queue.shift();
};
/**
* @return {number}
*/
MyStack.prototype.top = function() {
let x = this.pop();
this.push(x);
return x;
};
/**
* @return {boolean}
*/
MyStack.prototype.empty = function() {
return !this.queue.length;
};
3.有效的括号
简单
思路:
这里有三种不匹配的情况,1.字符串里左方向的括号多余了 ,所以不匹配。2.括号没有多余,但是 括号的类型没有匹配上。3.第三种情况,字符串里右方向的括号多余了,所以不匹配。第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false。第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false。第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false。字符串遍历完之后,栈是空的,就说明全都匹配了。
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
let stack = [];
for (let i = 0; i <s.length; i++){
switch (s[i]){
case '(':
stack.push(')');
break;
case '[':
stack.push(']');
break;
case '{':
stack.push('}');
break;
default:
if(stack.pop()!=s[i]) return false;
}
}
return !stack.length;
};
4.删除字符串中的所有相邻重复项
简单
思路:
1.双指针。
2.栈。在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素。用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。然后再去做对应的消除操作。从栈中弹出剩余元素,因为从栈里弹出的元素是倒序的,所以在对字符串进行反转一下,就得到了最终的结果。
/**
* @param {string} s
* @return {string}
*/
var removeDuplicates = function(s) {
let stack = [];
for(let i = 0;i<s.length;i++){
let x = stack.pop();
if( x == s[i])continue;
stack.push(x);
stack.push(s[i]);
}
return stack.join('');
};
5.删除字符串中的所有相邻重复项
简单
思路:
遇到数字压入栈中,遇到符号将栈顶两个元素弹出并运算的结果再压入栈。
/**
* @param {string[]} tokens
* @return {number}
*/
var evalRPN = function(tokens) {
let calculate = new Map([
//如果不加number()会变成字符串相加 '2'+'1'=‘21’
['+',(a,b)=>Number(b)+Number(a)],
['-',(a,b)=>b-a],//注意第二个运算数会被先弹出来 所以是b-a 除法同理
['*',(a,b)=>b*a],
//不能用 Math.floor, 因为 Math.floor 是向下取整,parseInt 可以去掉小数部分。如果是计算结果是负数 Math.floor(-1.5) = -2 。 parseInt(-1.5) = -1。
//位运算中的 | 也可以做到。这里用到了位运算中的或。整数与0的位或运算,都是本身。浮点数不支持位运算,过程中会自动转化成整数,利用这一点,可以将浮点数与0进行位或运算即可达到取整目的。
['/',(a,b)=>parseInt(b/a)]//["/", (a, b) => (b / a) | 0]
]);
let stack = [];
for(let i = 0; i<tokens.length; i++){
//数字压入栈
if(!calculate.has(tokens[i])){
stack.push(tokens[i]);
continue;
}
stack.push(calculate.get(tokens[i])(stack.pop(),stack.pop()));
}
return stack.pop();
};
6.滑动窗口最大值
困难
思路:
主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
单调队列,即单调递减或单调递增的队列。对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。设计单调队列的时候,pop,和push操作要保持如下规则:
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
使用单调队列的时间复杂度是 O(n)。nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。空间复杂度因为我们定义一个辅助队列,所以是O(k)。
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
//定义一个单调递减队列
class maxQueue{
queue;
constructor(){
this.queue = [];
}
enqueue(value){
//向单调递减队列中添加新元素 将比该元素小的值弹出 队列一直保持单调递减
let back = this.queue.length-1;
//此处注意队列为空时取值为undefined 判断时一定要写成this.queue[back]!=undefined而不是!this.queue[back]
while(this.queue[back]!=undefined&&value>this.queue[back]){
this.queue.pop();
back = this.queue.length-1;
}
this.queue.push(value);
}
dequeue(value){
//窗口向右滑动后队列中元素已经被记录过,位于窗口左侧,则进行删除
//如果前面的元素仍在队列中 则一定是最大值的位置 因为enqueue操作一定会将比它小的元素弹出
if(value==this.queue[0]){
this.queue.shift();
}
}
front(){
return this.queue[0]
}
}
let res = [];
let myqueue = new maxQueue();
let i = j = 0;
//双指针表示滑动窗口 先将右指针滑到右边
while(j<k){
myqueue.enqueue(nums[j]);
j++;
}
//添加第一个窗口最大值
res.push(myqueue.front());
//窗口滑动
while(j<nums.length){
//新加入元素到窗口 队列应该如何改动
myqueue.enqueue(nums[j]);
//从窗口删除遍历过的元素 队列应该如何改动
myqueue.dequeue(nums[i]);
//记录每个窗口的最大值
res.push(myqueue.front());
i++;
j++;
}
return res;
};
7.前 K 个高频元素
中等
思路:
统计元素出现的频率,这一类的问题可以使用map来进行统计。
优先级队列是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
使用优先级队列来对部分频率进行排序。 注意这里是对部分数据进行排序而不需要对所有数据排序!所以排序的过程的时间复杂度是
O
(
log
k
)
O(\log k)
O(logk),整个算法的时间复杂度是
O
(
n
log
k
)
O(n\log k)
O(nlogk)。
#总结
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
//优先级队列是一个完全二叉树
class Heap{
constructor(compareFn){
//实现一个小顶堆
this.compareFn = compareFn;
this.heap = [];
}
//添加元素
push(x){
//将元素加入堆底
this.heap.push(x);
//记录插入元素的索引
let index = this.heap.length-1;
//父节点的索引
let parent = Math.floor((index-1)/2);
//上浮 父节点比插入家电大 则将插入节点上浮
while(parent>=0&&this.compare(parent,index)>0){
[this.heap[parent], this.heap[index]] = [this.heap[index], this.heap[parent]];
//更新索引
index = parent;
parent = Math.floor((index-1)/2);
}
}
//弹出栈顶元素并返回值
pop(){
let top = this.heap[0];
//弹出堆低元素并赋值到栈顶 相当于将栈顶元素删除,栈低元素移动到栈顶
this.heap[0] = this.heap.pop();
//index记录索引
let index = 0;
//左子节点索引为left 则右节点为left+1
let left = 2*index+1;
//下沉操作只需考虑子结点中最小的那个 如果更小则该节点下沉 与子节点交换位置
let smallerChild = this.compare(left,left+1)>0?left+1:left;
while(this.heap[smallerChild]!=undefined&&this.compare(index,smallerChild)>0){
[this.heap[index],this.heap[smallerChild]] = [this.heap[smallerChild],this.heap[index]];
//更新索引
index= smallerChild;
left = 2*index+1;
smallerChild = this.compare(left,left+1)>0?left+1:left;
}
return top;
}
//元素排序方式 决定是小顶堆还是大顶堆
compare(index1,index2){
if(this.heap[index1]==undefined)return 1;
if(this.heap[index2]==undefined)return -1;
return this.compareFn(this.heap[index1],this.heap[index2]);
}
size(){
return this.heap.length;
}
}
//使用map记录整数及其出现次数
let itemToCount = new Map();
for(let i = 0; i<nums.length;i++){
//get用来获取一个Map对象指定的元素,返回的是键所对应的值,如果不存在则会返回undefined
itemToCount.set(nums[i],(itemToCount.get(nums[i])||0)+1);
}
let minHeap = new Heap((a,b)=>a[1]-b[1]);
//entries()方法返回一个新的包含 [key, value]的 Iterator对象,返回的迭代器的迭代顺序与 Map对象的插入顺序相同。
for(let entry of itemToCount.entries()){
minHeap.push(entry);
if(minHeap.size()>k){
minHeap.pop();
}
}
let ret = [];
for(let i = minHeap.size()-1;i>=0;i--){
ret[i] = minHeap.pop()[0];
}
return ret;
};