一。栈和栈的应用 撤销操作和系统栈
1. 简介
1.1 栈Stack
(1)栈是一种线性结构
(2)相比数组,栈对应的操作是数组操作的子集。
(3)栈的本质就是一个数组,我们把数据排开来放,但是规定:
添加元素的时候只能从一端添加元素,也只能从一端取出元素。
这一段通常称为“栈顶”
1.2 添加数据的规则
(1) 相当于在在一个桶中添加数据。这个操作叫入栈。
(2) 第一个数据在栈的底部
(3) 取出元素的时候只能取最顶部的元素。
(4) 最后一个数据在栈顶
1.3 栈的特质
(1) 栈是一种后进先出的数据结构[LIFO](Last In First Out)
(2) 计算机的世界中,栈拥有着不可思议的作用
2.栈和栈的应用
2.1 无处不在的Undo操作(撤销)
(1) 实质:向栈中推入元素和向栈顶中取出元素。
(2) 例如:
a. 输入“沉迷” (bottom [沉迷] top)
b. 输入“学习” (bottom [沉迷 学习] top)
c. 输入“不法” (bottom [沉迷 学习 不法] top)
d. 撤销输入“不法” (bottom [沉迷 学习] top)
--从编辑器的栈中拿出栈顶的元素
--通过栈顶的元素确定最近的操作是什么
--发现是最近的操作是输入了“不法”,这次撤销操作就是删除“不法”
--做完撤销后,输入“不法”这个动作也不需要被栈保留。所以就出栈了
2.2 栈的应用:程序调用的系统栈
(1)例如:
func A(){*,B(),*}
func B(){*,C(),*}
func C(){*,*,*}
(2) 运行顺序
a. 开始执行A()函数,执行第一行,第二行
b. 在执行A()的第二行的时候,要调用B()子程序,暂时中断A()的执行.
栈: (bottom [A(2)] top)
c. 开始执行B()函数,执行第一行,第二行
d. 在执行B()的第二行的时候,要调用C()子程序,暂时中断B()的执行.
栈: (bottom [A(2) B(2)] top)
e. 开始执行C()函数,执行第一行,第二行,第三行。执行完毕。
f. 对于系统栈来说,栈顶的元素是B(2).计算机就知道了:当时是执行B()到第二行中断了,中断跳到C(),但是此时C()已经执行完毕了,就可以跳回到B(2)的位置。这样,系统栈就成功给计算机找到了上一次执行的位置。
g. 回到B(2)继续执行,记录的B(2)也就没用了,它就可以出栈了。
栈: (bottom [A(2)] top)
h. 开始执行C(),第三行。执行完毕。
i. 查看栈顶元素为A(2) .....[f,g]
栈: (bottom [ ] top)
j. 开始执行A(),第三行。执行完毕。
k. 执完之后计算机又没有什么可执行了,这个时候查看系统栈为空。说明已经没有中断执行到一半的调用过程了。计算机就知道了,整个过程就已经执行完了。
(3) 总结
这就是在我们编程进行子过程调用的时候,当一个子过程执行完成之后,可以自动的回到上层调用中断的位置继续执行下去的背后原因。因为有一个系统栈来记录每一次调用过程中[中断]的调用点。
也就是:子过程子逻辑的调用在我们的编译器内部运行实现的机理。
二。栈的实现
1. 需要实现的方法--Stack<E>
具体的底层实现用户不关心。而且实际底层有多种实现方法。
1.1 Void push(E) 向栈中添加一个元素(入栈) -- O(1):即使有resize,也是O(1)
1.2 E pop() 从栈中取出栈顶的元素
1.3 E peek() 查看栈顶的元素是什么
1.4 int getSize()
1.5 boolean isEmpty()
2. 这个代码设计的思想
为了让整个程序的逻辑更加清晰,同时也是为了支持面向对象的一些特性。比如说:多态。
2.1 设计一个结构Stack<E>,定义了以上5种操作
2.2 用ArrayStack<E>实现Stack<E>,具体实现操作。
2.3 使用写好的数据ArrayDemo类中的方法。
3. 时间复杂度分析
所有的都是O(1)的
4. 代码实现
4.1 接口
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
4.2 底层实现
public class ArrayStack<E> implements Stack {
ArrayDemo<E> array;
public ArrayStack(int capacity){
array = new ArrayDemo<E>(capacity);
}
public ArrayStack(){
array = new ArrayDemo<E>();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
//对于我们的底层是实现是一个静态数组,所以用户可能需要查看数组容积。getCapacity()不是接口的一部分,是因为Stack接口是和栈的具体实现无关的,只有在我们使用动态数组来实现栈的时候,才存在容积整个概念。
public int getCapacity(){
return array.getCapacity();
}
public void push(Object e) {
array.addLast((E)e);
}
@Override
public E pop() {
return array.removeLast();
}
@Override
public E peek() {
return array.getLast();
}
@Override
public String toString(){
StringBuilder res = new StringBuilder("Stack: [");
for(int i=0;i<array.getSize();i++){
res.append(array.get(i));
if(i!=getSize()-1){
res.append(", ");
}
}
res.append("] top");
return res.toString();
}
}
4.3 应用
public class ArrayStackTest {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<Integer>();
for(int i=0;i<5;i++){
stack.push(i);
System.err.println(stack);
}
stack.pop();
System.err.println(stack);
}
}
三。 栈的应用:括号匹配
1. LeetCode
1.1 介绍
面向IT公司面试的在线平台。可以判断代码逻辑是否正确
1.2 中英文版本
中文 : https://leetcode-cn.com/
英文 : https://leetcode.com/
1.3 比较
(1)题目数量
(2)Explore里面的内容更多
(3)Contest(竞赛) 每周一次
(4)Articles有大量的文章
(5)**Problems:某一个问题的Discuss功能
2. 括号匹配
2.1 题目
给定一个只包括'(', ')', '{', '}', '[', ']'的字符串,判断字符串是否有效,括号必须正确匹配。
2.2 解答
给定字符串"{[( ) ]}"
(1)我们逐一遍历字符串中的每个字符,如果是左括号,就将之压住栈
Stack 【{ [ ( 】
(2)如果遇到的字符是右括号,查看栈顶的元素是否可以和右括号进行匹配。
当前字符是')',我们就查看栈顶元素是不是左侧的'(',是的话')'就匹配成功,那么栈顶的'('就可以出栈了。
(3)---(2)
(4)---(2)
(5)这个时候整个字符串已经遍历完了,但是此时栈是空的。说明当前字符串是合法的。
给定字符串"{[ }]"
2.3 结论
栈顶元素反映了在嵌套的层次关系中,最近的需要匹配的元素。
2.4 代码
public boolean isVaild(String s){
Stack<Character> stack = new Stack<Character>();
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();
}