剑指Offer面试题:09 用两个栈实现队列

Posted 一只前端小马甲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指Offer面试题:09 用两个栈实现队列相关的知识,希望对你有一定的参考价值。

算法不是金庸武侠小说里硬核的”九阳真经“,也不是轻量的”凌波微步“,它是程序员的基本功,如同练武之人需要扎马步一般。功夫好不好,看看马步扎不扎实;编程能力强不强,看看算法能力有没有。本系列采用leetcode题号,使用javascript为编程语言,每篇文章都会逐步分析解题思路,最终给出代码。文章一方面是记录笔者在刷题中的思路,已备学而时习之,另一方面也希望能跟大牛们多交流。有更高效的解法,或者文章有什么问题,都欢迎提出来,望诸位不吝赐教。

一、题目:用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTaildeleteHead,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,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次调用

二、小马甲思路

2.1 理解题目

如果你能自己理解题目,那直接进入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. 生成一个CQueue队列,不需要参数,返回null;
  2. appendTail入队操作,传入3,返回null
  3. deleteHead出队操作,不需要参数,返回3;
  4. deleteHead出队操作,不需要参数,此时队列为空,返回-1

2.2 思路分析

这道题的关键在于理解数据结构“栈”和“队列”的特点:前者是“先进后出”,后者是“先进先出”。我们都理解栈像一个量杯,只有一个出入口;队列像个水管,有两个口,所以唯物主义来说我们很容易想象到如下思路。

猜测的实现思路
这样当然不行!两个栈底相接,但是并没有连通,我们想当然的把一个口变成了两个口,认为就是队列。我们重新梳理下,从栈的特点出发,现有一堆数字,连续入栈的顺序是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 斐波那契数列

以上是关于剑指Offer面试题:09 用两个栈实现队列的主要内容,如果未能解决你的问题,请参考以下文章

[剑指offer]面试题7:用两个栈实现队列

剑指Offer面试题:09 用两个栈实现队列

剑指Offer面试题:09 用两个栈实现队列

剑指Offer面试题:09 用两个栈实现队列

LeetCode | 面试题09. 用两个栈实现队列剑指OfferPython

剑指offer编程题Java实现——面试题7相关题用两个队列实现一个栈