栈与队列
认识栈与队列
认识栈
栈的定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。
理解栈的定义需要注意:
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进的只能在栈底。
栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹
栈的删除操作,叫作出栈,有的也叫作弹栈。如同弹夹中的子弹出夹
入栈示意图:
出栈示意图:
进出栈变化形式及抽象数据类型
现在我要问问大家,这个最先进栈的元素,是不是就只能是最后出栈呢?
答案是不一定,要看什么情况。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
举例来说,如果我们现在是有3个整型数字元素1、2、3依次进栈,会有哪些出栈次序呢?
- 第一种:1、2、3进,再3、2、1出。这是最简单最好理解的一种,出栈次序为3、2、1。
- 第二种:1进,1出,2进,2出,3进,3出。也就是进一个就出一个,出栈次序为1、2、3。
- 第三种:1进,2进,2出,1出,3进,3出。出栈次序为2、1、3。
- 第四种:1进,1出,2进,3进,3出,2出。出栈次序为1、3、2。
- 第五种:1进,2进,2出,3进,3出,1出。出栈次序为2、3、1。
有没有可能是3、1、2这样的次序出栈呢?答案是肯定不会。因为3先出栈,就意味着,3曾经进栈,既然3都进栈了,那也就意味着,1和2已经进栈了,此时,2—定是在1的上面,就是更接近栈顶,那么出栈只可能是3、2、1,不然不满足1、2、3依次进栈的要求,所以此时不会发生1比2先出栈的情况。
从这个简单的例子就能看出,只是3个元素,就有5种可能的出栈次序,如果元素数量多,其实出栈的变化将会更多。这个知识点一定要弄明白。
栈的抽象数据类型
ADT栈(stack)
Data同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
OperationInitstack(*S):初始化操作,建立一个空栈S。
DestroyStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,返回true,否则返回false。
GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*s,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT
栈的顺序存储与链式存储
- 栈的顺序存储:利用数组来实现栈的结构
入栈操作:
出栈操作:
- 两栈共享空间
栈满条件:top1+1==top2
- 栈的链式存储
栈的链式存储结构实际上就是一个单链表,叫做链栈。插入和删除操作只能在链栈的栈顶进行。栈顶指针Top应该在链表的哪头?
假如按照正常逻辑,放在链表的尾部,插入操作要从头结点开始,挨着挨着遍历过去,到最后一个元素,也就是栈顶就可以实现插入。
删除操作呢?删除操作其实是没有办法进行的,因为链表的删除要知道被删除结点的前一个结点的信息,我们没法从链表的尾结点倒着回去找它的前一个结点,也没有办法确定它的前一个结点的信息,因此删除操作就无法实现。
因此经过前人的总结,将头指针所指的位置当做栈顶对于插入和删除操作都十分方便
认识队列
队列的定义及抽象数据类型
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是q=(a1,a2,…,an),那么a,就是队头元素,而a,是队尾元素。这样我们就可以删除时总是从a,开始,而插入时,列在最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后,如下图所示。
- 队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列Q。
DestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,返回true,否则返回false。
GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
endADT
循环队列
队满的判断:(Q.tail + 1) % Maxsize == Q.head
入队的操作为:
Q.head[Q.tail]=e;
将元素e放入Q.tail指向的空间
Q.head=(Q.tail+1)%Maxsize
tail指针向后移动一个单位
至于为什么要加对(Q.rear+1)取模,这是因为rear指针会一直增加到比队列容量大的数字,对其取模就是为了使其缩小,指向真正的位置例如 容量大小为5 队尾指向9 取模队尾指向的是4,说明队尾指正已经走过了一个圈
出队的操作:
e=Q.tail[Q.head];
Q.head=[Q.head+1]%Maxsize;
将头指针向着元素增长方向移动一个单元,原来的头指针指向的元素并没有真正的删除,只是逻辑上的删除,后序如果要用到这个空间,会用新的元素将其覆盖掉
栈与队列常见面试题
1.问题:给定一个只包括’(‘,’)‘,’{‘,’}‘,’[‘,’]'的字符串s,判断字符串是否有效
有效条件:
1.左括号必须用相同类型的右括号闭合
2.左括号必须以正确的顺序闭合
解:匹配时可能的情况:
1.( { } )匹配
2.(((()左括号多
3.())))右括号多
4.{(]}左右括号次序不一样
5.""空字符串
6.null
利用的数据结构:栈。遍历字符串s,遇到左括号入栈,遇到右括号与栈顶元素匹配。匹配成功,栈顶出栈,遍历坐标向后移
结果:
1.左右括号匹配成功的情况下栈和字符串都遍历完成
2.若左括号多,当字符串遍历完成的时候栈是不空的
3.若右括号多。当字符串遍历到右括号时,此时栈内左括号为空
4.遇到不匹配直接false
public boolean isValid(String s) {
if(s == null) {
return false;
}
if(s.length() == 0) {//""空字符串
return true;
}
Stack<Character> stack = new Stack<>();
for(int i = 0;i < s.length();i++) {//遍历字符串
char ch = s.charAt(i);
if(ch == '{' || ch == '(' || ch == '[') {//左括号入栈
stack.push(ch);
}else{
//遇到右括号了
if(stack.empty()) {
System.out.println("右括号多!");
return false;
}
//栈不为空:获取栈顶元素,查看是否匹配
char tmp = stack.peek();//获取栈顶元素
if(tmp == '{' && ch == '}' || tmp == '[' && ch == ']' || tmp == '(' && ch == ')' ) {//匹配
stack.pop();//匹配成功栈顶元素直接出栈
}else{
System.out.println("左右括号顺序不匹配!");
return false;
}
}
}
if(!stack.empty()) {//遍历完字符串后栈不为空
System.out.println("左括号多!");
return false;
}
return true;
}
2.用队列实现栈
解:
创建两个队列,入栈时入不为空的队列;出栈时出不为空的队列,然后弹出栈顶元素,两个整体类似于栈的先入后出
class MyStack {
private Queue<Integer> qu1 = new LinkedList<>();
private Queue<Integer> qu2 = new LinkedList<>();
// 入栈
public void push(int x) {
if(!qu1.isEmpty()){
qu1.offer(x);
}else if(!qu2.isEmpty()){
qu2.offer(x);
}else{//都为空自己选一个就好
qu1.offer(x);
}
}
//出栈
public int pop() {
if(empty()) {
return -1;
}
int e = -1;
if(!qu1.isEmpty()) {
int size = qu1.size();
for(int i = 0;i < size-1;i++){//将不为空的队列中的元素输入到为空的队列中,直到只剩最后一个元素
e = qu1.poll();
qu2.offer(e);
}
e = qu1.poll();//将最后一个元素弹出,此时两个整体类似于栈的先入后出
}else{
int size = qu2.size();
for(int i = 0;i < size-1;i++){
e = qu2.poll();
qu1.offer(e);
}
e = qu2.poll();
}
return e;
}
//得到栈顶元素,不删除
public int top() {//思路和出栈一样,只是直接到栈顶,弹出栈顶元素
if(empty()) {
return -1;
}
int e = -1;
if(!qu1.isEmpty()) {
int size = qu1.size();
for(int i = 0;i < size;i++){
e = qu1.poll();
qu2.offer(e);
}
}else{
int size = qu2.size();
for(int i = 0;i < size;i++){
e = qu2.poll();
qu1.offer(e);
}
}
return e;
}
public boolean empty() {
return qu1.isEmpty() && qu2.isEmpty();
}
}
3.栈实现队列
解:
入队时存放到s1这个栈,出队时出s2这个栈,整体类似于队列
class MyQueue {
private Stack<Integer> s1;
private Stack<Integer> s2;
public MyQueue() {
s1 = new Stack<>();
s2 = new Stack<>();
}
public void push(int x) {
s1.push(x);//指定push到第一个栈里面
}
public int pop() {
if(empty()) return -1;
if(s2.empty()) {
while(!s1.empty()) {//将栈s1中的所有元素放入s2栈中
s2.push(s1.pop());
}
}
return s2.pop();
}
public int peek() {
if(empty()) return -1;
if(s2.empty()) {
while(!s1.empty()) {
s2.push(s1.pop());
}
}
return s2.peek();
}
public boolean empty() {
return s1.empty() && s2.empty();
}
}
4.设计一个支持push pop top操作,并能在常数时间内检索到最小元素的栈
存放时就已经在刻意保持元素在出栈之后可以随时找到最小元素
注意:最小值一直在发生改变
class MinStack {
private Stack<Integer> stack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();
public void push(int val) {
stack.push(val);
if(minStack.empty()) {//第一次入栈
minStack.push(val);
}else{
int x = minStack.peek();
if(val <= x) {//以后再次入栈需要先比较
minStack.push(val);
}
}
}
public void pop() {
int x = stack.pop();
if(x == minStack.peek()){
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
5.设计一个循环队列
public class MyCircularQueue {
private int[] elem;
private int usedSize;
private int front;
private int rear;
public MyCircularQueue(int k) {
this.elem = new int[k];
}
/**
* 入队
* @param value
* @return
*/
public boolean enQueue(int value) {
if(isFull()) {
return false;
}
this.elem[this.rear] = value;
this.rear = (this.rear+1) % this.elem.length;
return true;
}
public boolean isFull() {
if( (this.rear+1) % this.elem.length == this.front) {
return true;
}
return false;
}
/**
* 出队
* @return
*/
public boolean deQueue() {
if(isEmpty()) {
return false;
}
this.front = (this.front+1) % this.elem.length;
return true;
}
public boolean isEmpty() {
if(this.front == this.rear){
return true;
}
return false;
}
/**
* 得到队头元素 相当于 peek()
* @return
*/
public int Front() {
if(isEmpty()) {
return -1;
}
int val = this.elem[this.front];
//this.front = (this.front+1) % this.elem.length; 不能删除的
return val;
}
/**
* 得到队尾元素
* @return
*/
public int Rear() {
if(isEmpty()) {
return -1;
}
if(this.rear == 0) {
return this.elem[this.elem.length-1];
}
return this.elem[this.rear-1];
}
}