第三章 栈和队列
3-1 栈和栈的应用:撤销操作和系统栈
3-2 栈的基本实现
3-3 栈的另一个应用:括号匹配 (Leetcode原题)
3-4 关于Leetcode的更多说明
3-1 栈和栈的应用:撤销操作和系统栈
- 栈是一种线性结构,相比数组,栈对应的操作是数组的子集,而栈只能从一端添加元素(入栈),也只能从一端取出元素(出栈),这一端称为栈顶。
- 栈是一种后进先出的线性数据结构 - Last In First Out (LIFO)
栈的应用:
(1)系统中常用的“撤销”操作:
举个栗子:在word中依次输入:“小白兔”、“爱吃”、“白萝卜” -->小白兔爱吃白萝卜。
我们知道这只小白兔爱吃胡萝卜,所以要撤销“白萝卜”(比如我们就按delete键,直接删除“白萝卜”),然后重新输入“胡萝卜”。
这个过程中,我们按delete键的时候,会直接删除“白萝卜”,而不是“小白兔”、“爱吃”,因为“白萝卜”是最后的操作。
(2)程序调用的系统栈(记录子程序调用时中断点的位置)
假设有三个函数A、B、C,对于A这个函数执行到一半的时候要调用子函数B,B函数执行到一半的时候要调用子函数C,C函数直接运行,不需要调用子函数。当我们开始执行A函数,到第二行时,程序会调用B函数;跳转到B函数,暂时中断函数A的执行,此时系统栈中记录为A2(即A2进栈,A2:A的第二行)。同样的,当我们开始执行B函数,到第二行时,程序会调用C函数;跳转到C函数,暂时中断函数B的执行,此时系统栈中记录为B2(即B2进栈,B2:B的第二行)。C函数运行结束后,接下来看到系统栈中,栈顶的元素是B2,因此程序跳转到B2位置(B函数第二行)继续执行,此时B2这个信息就没用了(即B2出栈);B函数运行结束后,系统栈中的栈顶元素为A2,因此程序继续跳转到A2位置继续执行(此时A2出栈),A函数运行结束后,系统栈中没有元素了,系统便知道整个过程已经执行结束。
简单点说:在编程进行子过程的调用时(当fun A进行到2号中断点时,程序会调用fun B,即A2进栈),当一个子过程执行完成之后会自动转入到上层调用中断的位置(当fun C结束后,会转到fun B的2号位置,即B2出栈),继续执行下去。
–>类似于程序的递归调用
3-2 栈的实现
栈的实现 Stack< E >只涉及五个基本操作:
- void push (E):向栈中添加元素(入栈) —>O(1) 均摊
- E pop ( ):从栈中拿出栈顶元素(出栈) —>O(1) 均摊
- E peek ( ):查看栈顶元素 —>O(1)
- int getSize ( ):获取栈中元素的个数 —>O(1)
- boolean isEmpty ( ):判断栈是否为空 —>O(1)
public interface Stack<E> {
// 栈的接口中相应的方法
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
基于对象多态性的理念,在代码设计上设计一个Stack< E >的接口,并在接口中定义以上五种方法。在上一章实现数组的条件下,设计一个ArrayStack< E >的类,实际上是实现了Stack< E >的接口后,具体完成了接口中相应的方法逻辑。ArrayStack基于动态数组Array的实现创建的栈:
public class ArrayStack<E> implements Stack<E>{
Array<E> array; // 成员变量
// 构造函数1 - 用户可以传入一个已知的capacity
public ArrayStack(int capacity){
array = new Array<>(capacity);
}
// 构造函数2 - 用户不知道要传入多大的capacity
public ArrayStack(){
array = new Array<>();
}
//ArrayStack这个类它要实现接口相应的方法的具体实现:
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
@Override
public void push(E e){
array.addLast(e);
}
@Override
public E pop(){
return array.removeLast();
}
@Override
public E peek(){
return array.getLast();
}
// 用户可能想知道capacity的大小,不属于stack接口中的一部分
public int getCapacity(){
return array.getCapacity();
}
// 重写覆盖object中的toString方法
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack:");
res.append('[');
for (int i = 0; i < array.getSize(); i ++){
res.append(array.get(i));
if (i != array.getSize() - 1)
res.append(", ");
}
res.append("] top");
return res.toString();
}
}
- 这里要注意的是对于peek方法,可以直接return最后一个索引对应的元素。我们选择在Array.java文件中添加getLast和getFirst两个方法,而不是在类外直接调用get方法传入index,更好地体现了封装性。因为如果需要外界传入index时,就需要获取size等相关属性,使得调用较为混乱。
public E getLast(){
return get(size-1);
}
public E getFirst(){
return get(0);
}
以上我们已经实现了一个栈,接下来在Main方法中测试一下栈,其中入栈5次(stack.push()),出栈1次(stack.pop()):
public class Main {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>();
//入栈5次
for (int i = 0; i < 5; i ++){
stack.push(i);
System.out.println(stack);
}
//出栈1次
stack.pop();
System.out.println(stack);
}
}
结果如下:
Stack:[0] top
Stack:[0, 1] top
Stack:[0, 1, 2] top
Stack:[0, 1, 2, 3] top
Stack:[0, 1, 2, 3, 4] top
Stack:[0, 1, 2, 3] top
3-3 栈的另一个应用:括号匹配
undo操作 -> 编辑器; 系统调用栈 -> 操作系统; 括号匹配 -> 编译器
这一集的内容,我们主要是做了一个Leetcode的题目:
Leetcode - Valid Parentheses
import java.util.Stack;
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i = 0; i < s.length(); i ++){
char c = s.charAt(i);
if(c == '(' || c == '[' || c == '{')
stack.push(c);
else{
if(stack.isEmpty())
return false;
char topChar = stack.pop();
if(c == ')' && topChar != '(')
return false;
if(c == ']' && topChar != '[')
return false;
if(c == '}' && topChar != '{')
return false;
}
}
return stack.isEmpty();
}
}
3-4 关于Leetcode的更多说明
- Leetcode提交时,编写的类必须是public,不能是private
- 可以将自己写的类作为内部类放在原有的类里边
接下来讲一讲学习方法:
- 学习中不要追求完美主义,要掌握好“度”。
- 学习本着自己的目标去。
- 对于这个课程,大家的首要目标,是了解各个数据结构的底层实现原理。
学习不是要么0分,要么100分的。80分是收获,60分是收获,20分也是收获。有收获最重要。
我们都不是小升初考了满分,才能上初中的;也不是中考考了满分,才能读高中的;更不是高考考了满分,才能考大学的;将来也不会是大学所有科目都是满分,才能出来工作。不完美其实是常态,根本不会影响我们学习更多更深入的内容。