算法不是金庸武侠小说里硬核的”九阳真经“,也不是轻量的”凌波微步“,它是程序员的基本功,如同练武之人需要扎马步一般。功夫好不好,看看马步扎不扎实;编程能力强不强,看看算法能力有没有。本系列采用leetcode题号,使用JavaScript为编程语言,每篇文章都会逐步分析解题思路,最终给出代码。文章一方面是记录笔者在刷题中的思路,已备学而时习之,另一方面也希望能跟大牛们多交流。有更高效的解法,或者文章有什么问题,都欢迎提出来,望诸位不吝赐教。
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数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]
提示:
如果你能自己理解题目,那直接进入2.2看解题分析。
我个人觉得这道题的题目出得不太好,不容易理解,所以如果读第一遍不懂没关系,我们简单分析下。我们看看示例1,有两行输入,一行输出,均是数组。
输入:
[“CQueue”, “appendTail”, “deleteHead”, “deleteHead”]
[[], [3], [], []]
输出:[null, null, 3, -1]
光看输入,你可能比较懵,但是看看我们的输出,数组中有个 -1 元素。
[null, null, 3, -1]
结合题干的条件:若队列中没有元素,deleteHead操作返回-1,此时再看我们的输入第一行。
[“CQueue”, “appendTail”, “deleteHead”, “deleteHead”]
是不是刚好最后一个元素是deleteHead,呀,这说明第一行输入代表操作。针对数组最最后一个元素,实际上就是进行了一次deleteHead操作,然后返回了-1。
我们再来看看第二行输入。
[[], [3], [], []]
数组第二个元素很特殊,是个数字数组,我们对应看下对应的第一行是appendTail操作。
[“CQueue”, “appendTail”, “deleteHead”, “deleteHead”]
结合示例2可以发现,appendTail操作一定对应数字数组,所以第二行输入是参数。
现在翻译下示例1,共进行了四步操作:
这道题的关键在于理解数据结构“栈”和“队列”的特点:前者是“先进后出”,后者是“先进先出”。我们都理解栈像一个量杯,只有一个出入口;队列像个水管,有两个口,所以唯物主义来说我们很容易想象到如下思路。
这样当然不行!两个栈底相接,但是并没有连通,我们想当然的把一个口变成了两个口,认为就是队列。我们重新梳理下,从栈的特点出发,现有一堆数字,连续入栈的顺序是1、2、3、4,连续出栈的顺序是4、3、2、1,这不正好相反么。
因此:出入栈一次顺序相反,出栈两次那不是“负负得正”了。这就是这道题的核心思想。现在假设两个栈a和b,入栈的时候全进入栈a,出栈的时候从a出栈,然后全压入栈b,然后依次从栈b出栈,这不成功模拟了一个队列吗。
我们给两个栈取个名字,栈a叫做尾栈tailStack,栈b叫做头栈headStack,模拟入队操作,必须先进入尾栈,模拟出队操作,必须最后从头栈出,这跟队列的“尾进头出”的概念是对应的。
整体思路有了,如何具体操作?显然我们要分情况来处理:
首先来模拟出队操作。
情况一,当尾栈和头栈都为空,这时候很简单,直接返回-1即可。
情况二,尾栈不空,头栈为空,此时我们需要将尾栈的元素全都压入头栈,然后再从头栈出栈一个元素。
情况三,尾栈为空,头栈不空,此时直接头栈出栈一个元素即可。
情况四,尾栈和头栈都不空,此时也只需头栈出栈一个元素即可。
接着来模拟“入队”操作。
入队操作相对来说很简单,对于4中情况都只要在尾栈入栈即可。
首先我们得有个模拟队列的构造函数,构造函数中需要初始化两个栈。
function CQueue(){
// 初始化头栈
this.headStack = [];
// 初始化尾栈
this.tailStack = [];
return null;
}
解题分析我们先说”出队“,再说”入队“,从难到易,具体写代码我们从易开始,先实现"入队"操作,只需要尾栈入栈元素即可。
CQueue.propotype.appendTail = function(value){
// 尾栈入栈
this.tailStack.push(value);
return null;
}
再来实现"出队"操作,对应前面我们说的四种情况分别实现。
情况一,当尾栈和头栈都为空,直接返回-1。
CQueue.prototype.deleteHead = function(){
// 头栈长度
let headStackLength = this.headStack.length;
// 尾栈长度
let tailStackLength = this.tailStack.length;
let result;
if(headStackLength === 0 && tailStackLength === 0){
// 头尾栈均为空
result = -1;
}
return result;
}
情况二,尾栈不空,头栈为空,将尾栈的元素全都压入头栈,然后头栈出栈一个元素。
CQueue.prototype.deleteHead = function(){
// 头栈长度
let headStackLength = this.headStack.length;
// 尾栈长度
let tailStackLength = this.tailStack.length;
let result;
// if(headStackLength === 0 && tailStackLength === 0){ // 头尾栈均为空
// result = -1;
}else if(headStackLength === 0 && tailStackLength === 0){
// 尾栈不空,头栈为空
this.headStack.push(...this.tailStack.reverse());
this.tailStack.length = 0;
result = this.headStack.pop();
}
return result;
}
情况三,尾栈为空,头栈不空,头栈出栈一个元素。
CQueue.prototype.deleteHead = function(){
// 头栈长度
let headStackLength = this.headStack.length;
// 尾栈长度
let tailStackLength = this.tailStack.length;
let result;
// if(headStackLength === 0 && tailStackLength === 0){ // 头尾栈均为空
// result = -1;
// }else if(headStackLength === 0 && tailStackLength === 0){ // 尾栈不空,头栈为空
// this.headStack.push(...this.tailStack.reverse());
// this.tailStack.length = 0;
// result = this.headStack.pop();
}else if(headStackLength > 0 && tailStackLength === 0){
// 尾栈为空,头栈不空
result = this.headStack.pop();
}
return result;
}
情况四,尾栈和头栈均不为空,头栈出栈一个元素。
CQueue.prototype.deleteHead = function(){
// 头栈长度
let headStackLength = this.headStack.length;
// 尾栈长度
let tailStackLength = this.tailStack.length;
let result;
// if(headStackLength === 0 && tailStackLength === 0){ // 头尾栈均为空
// result = -1;
// }else if(headStackLength === 0 && tailStackLength === 0){ // 尾栈不空,头栈为空
// this.headStack.push(...this.tailStack.reverse());
// this.tailStack.length = 0;
// result = this.headStack.pop();
// }else if(headStackLength > 0 && tailStackLength === 0){ // 尾栈为空,头栈不空
// result = this.headStack.pop();
}else if(headStackLength > 0 && tailStackLength > 0){
// 头尾栈均不空
result = this.headStack.pop();
}
return result;
}
这是我们的最终代码。
var CQueue = function() {
this.headStack = [];
this.tailStack = [];
return null;
};
CQueue.prototype.appendTail = function(value) {
this.tailStack.push(value);
return null;
};
CQueue.prototype.deleteHead = function() {
var headStackLength = this.headStack.length;
var tailStackLength = this.tailStack.length;
var result = -1; // 头尾栈均为空
if(headStackLength===0 && tailStackLength>0){
// 尾栈不空,头栈为空
this.headStack.push(...(this.tailStack.reverse()));
this.tailStack = [];
result = this.headStack.pop();
}
else if(headStackLength>0 && tailStackLength===0){
// 尾栈为空,头栈不空
result = this.headStack.pop();
}else if(headStackLength>0 && tailStackLength>0){
// 头尾栈均不空
result = this.headStack.pop();
}
return result;
};
当然代码可以做些优化,今天知识点很多,我们先到此为止。
本文先分析了下不太容易理解的题干,然后从栈和队列的概念入手,提出了两个栈模拟队列的核心思想。接着分别对模拟的入队和出队操作,并按照四种情况进行讨论,给出了解题思路。因为栈和队列数据结构比较简单,解题思路基本就是伪代码,很容易得到最终的算法程序。
基础知识关键字:栈、队列
上一篇涨薪知识点传送门:剑指Offer面试题:07 重建二叉树
下一篇涨薪知识点传送门:剑指Offer面试题:10- I 斐波那契数列