date: 2016-08-18 9:11:00
title: 数据结构之栈和队列
categories: 数据结构
版权声明:本站采用开放的[知识共享署名-非商业性使用-相同方式共享 许可协议]进行许可
所有文章出现的代码,将会出现在我的github中,名字可以根据类全名来找,我在github中的文件夹也会加目录备注。
数据结构中的栈和队列的异同点
相同点
栈和队列都属于线性结构,即除了首尾元素之外,其他元素都只有一个直接前趋和直接后继,用数学表示为:{a1,a2,a3,…,an}。
栈和队列通常采用顺序存储结构和链式存储结构
不同点
栈:先进后出,不清楚的可以用子弹夹来理解,先进去的子弹被压在下面,但是开枪的时候在上面的子弹先被发射;队列:先进先出,跟我们平常排队一样,先到的先服务。
栈只能在线性结构的一端进行操作 ,并且弹栈和压栈都在栈尾操作;但是队列在队头出队,在队尾入队。同时,在使用队列的时候,最好使用循环队列,因为在平常的队列中,进行入队和出队操作的时候,只是队列的前指针跟后指针(或理解成脚标)在移动,对于之前被移动走的元素不可以复用,到后面就会出现前指针紧挨后指针,这时候再要插入的话,就会抛出溢出的异常。
假溢出
由于队列拥有固定的长度,并且每当元素出队都是从front中移出,并且fornt的脚标向后移,这时我们看到前面的空位置理应可以插入元素,但是当我们执行插入操作的时候却被抛出异常,队列溢出,这种溢出被称为假溢出。
但是可以通过循环队列实现:通过把队列的首尾连接起来,并且在插入元素的时候先通过判断当前尾脚标(rear)的位置,或者在出队的时候判断前脚标(front)的位置是否在(队列的长度-1)上,因为这里从0开始。如果要插入元素,如果前脚标在队列最后面的时候,即队列最大长度-1的位置因为从0开始,把前脚标front赋值为0,这样相当于把前脚标放到队头。如果元素出队的时候判断当前脚标(front)是否在队尾,即最大队列长度-1的位置,如果是,把前脚标对应的元素移出之后,再赋值为队头,即front=0,但是这种方法判断队列是否满就要再定义一个变量来记录当前队列有多少个元素,这样就需要每次进队/出队的时候都要进行自增(++/自减(–)操作。
另外一种大众的方法:取模。元素入队的时候,让尾脚标(rear)+1再跟队列的长度取余,得到的数就是要插入元素将存储的位置。在元素出队的时候,用前脚标(front)+当前队列元素个数再跟队列长度取余。在java的集合框架中,也有关于queue(jdk 1.5更新)跟stack的实现,其中queue是接口,但是stack是类。
两个栈怎样实现一个队列
曾经看到一位面试官问队列和栈谁更基本?这个问题,我觉得要看从哪个方面出发,因为两个队列可以实现一个栈,同理,两个栈也可以实现一个队列。
下面我就来说说两个栈怎样实现一个队列。
第一次看到这个问题的时候,我就把一个栈作为存储栈,另外一个栈作为缓存栈。
当要往里面添加元素的时候,直接添加到存储栈。我之前想过要不要考虑哪个栈有元素,哪个栈没元素,但是后来发现想多了,因为在移出时,先判断缓存栈有没有元素,如果有元素直接弹栈;若缓存栈没有元素,则把存储栈的全部元素放到缓存栈中,然后再弹栈,这时元素弹栈的顺序跟元素进栈一样,这种实现的时间效率是线性的,即O(n)
有些人考虑到优化问题,说把存储栈中的n-1个元素放到缓存栈中,存储栈的最后一个元素可以不用放到缓存栈,可以直接弹出,这样就避免了把存储栈中的最后一个元素先压到缓存栈,然后再弹栈的行为。
其实这种解决方法不妥。虽然说可以减少一次压栈的操作,但是代码实现起来……
public Integer deQueue() {
if ((s1.size() + s2.size()) != 0) {
if (s2.size() == 0) {
// 缓冲的栈为空,直接把s1中的元素倒入到s2
while (!s1.isEmpty()) {
if (s1.size() == 1) {// 若存储栈只剩下一个元素,直接弹栈
return s1.pop();
} else {// 不为1时,放到存储栈
return s2.push(s1.pop());
}
}
throw new RuntimeException();
} else {// 如果缓存栈不为空,直接返回
return s2.pop();
}
} else {// 若s1,s2都没有元素的时候要求弹栈,抛异常
throw new RuntimeException("the queue is null !");
}
}
如果要按照这样做,那么每遍历一次,都要判断存储栈是否只剩下一个元素……那还不如直接扔过去算了。
下面是线性时间效率的实现:
package com.xinpaninjava.queueimplement;
import java.util.Stack;
public class Stack2QueueInO1 {
// use s1 to keep the value and s2 to cache
private Stack<Integer> s1 = new Stack<Integer>();
private Stack<Integer> s2 = new Stack<Integer>();
// 插入时直接放到存储栈
public boolean enQueue(Integer integer) {
s1.push(integer);
return true;
}
// 弹栈时,先判断缓存栈是否为空,不为空直接返回,如果为空,先倒入缓存栈再弹栈
public Integer deQueue() {
if (s2.isEmpty()) {
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
}
return s2.pop();
}
// 判断是否为空
public boolean isEmpty() {
return (s1.size() + s2.size()) == 0;
}
}
两个队列如何实现一个栈
思路:由于队列是先进先出,在java中的实现最简单的方法就是借用LinkedList类,这个类实现了DeQue接口,而DeQue接口是Double Ended Queue的缩写,双端队列可以在两段进行插入和删除操作,但是同时DeQue接口继承了Queue接口,所以也具有队列的基本实现。要在java中实现,直接用一个LinkedList插入元素,然后返回最后一个元素即可。
如果使用两个队列来实现的话,(图解见下面)首先把一堆元素放到队列1中,比如a,b,c,d,由于队列插入元素在队尾插入,但是移出元素在队头,每次要保证两个队列中有一个队列是空队列,这样就可以把有元素的队列中n-1个元素移动到另外一个队列中,这样,刚刚的队列就剩下最后插入的元素,这时,把最后一个元素出队,这样,刚刚出队元素所在的队列就为空了,就这样反复的找空队列,每次移动n-1个元素,当然这里的n是会变化的。这样可以确保每一次出队的都是后面加进来的元素
代码实现:
package com.xinpaninjava.stackimplement;
import java.util.LinkedList;
public class Queue2Stack {
private LinkedList<Integer> l1 = new LinkedList<Integer>();
private LinkedList<Integer> l2 = new LinkedList<Integer>();
// 添加元素,直接添加到存储队列
public void push(Integer integer) {
l1.add(integer);
}
// 实现弹栈
public Integer pop() {
// 首先判断两个队列中有没有元素,若无,抛异常
if ((l1.size() + l2.size()) == 0) {
throw new RuntimeException(
"the stack is null that can't pop the element in it");
} else {
// 有元素:找空队列,只要一个为空队列,另外一个就往空队列倒n-1个元素
if (l1.isEmpty()) {// 存储队列为空的情况
while (l2.size() > 1) {// 倒元素
l1.add(l2.poll());
}
return l2.poll();
} else {// 缓存队列为空的情况
while (l1.size() > 1) {// 倒元素
l2.add(l1.poll());
}
return l1.poll();
}
}
}
public boolean isEmpty() {
return (l1.size() + l2.size()) == 0;
}
}
最后,不要为了使用数据结构而是用数据结构,比如说字符串的反转,没有必要使用栈/队列倒入找出,可以使用两边交换的方法。当然如果你要使用LinkedList类的话也可以,直接得到反序元素输出。
参考资料:
编程之美 第三章 第七节 队列中取最大值操作问题 PDF284页,这个是PDF下载地址