栈
栈的概括
首先,相对栈这种数据结构而言,我们可以理解成,它本质就是一个数组,我们把数据排开来放,但是我们规定添加元素的时候,我们只能从栈的一端去添加元素,而取出元素的时候,也只能从同一端来取出元素 (取出一端通常我们称为栈顶)。在形成了栈这种数据结构之后,在我们的计算机世界中,对于组建逻辑有着非常重要的作用。
在这里我进行了一个简单的图示,我可以理解成,如图示:
这样一个栈,栈顶就在上方。我们向栈中添加一个数据(即把它放进栈中),这个过程通常称之为是入栈。
栈的应用
在我们学习无论是经典的算法也好,还是算法设计也好,都将不可避免再次接触到栈这种看似很简单,但其实应用非常广泛的数据结构,在这里我向大家讲两个栈相关的应用。
第一个栈的应用
首先说的第一个栈的应用就是大家都是用过编辑器,无论是word
,还是写代码用的IDE
,都会有一个叫做undo
的操作 (即撤销),比如在文本中输入一句话,或者修改一个内容,修改后发现之前的修改可能有问题,我想撤销到修改前怎么办呢?就需要执行下undo
操作了
对于编辑器来说,undo操作的原理是什么呢?
其实就是靠一个栈来进行维护的。
有这样一个过程,比如现在输入一句话,
- 输入第一个词
沉迷
,编辑器就会记录这个动作。这个记录的方式,其实就是把这个动作放进一个栈中,记录了沉迷两个字一下 - 然后,我在输入第二个词
学习
,编辑的栈就会再做一次记录,以同样的方式压入这个栈中 - 下面我可能要输入无法,但不小心输成无天,栈中也记录了这个动作。但你意识到输错,需要撤销这个操作。这个操作无论是点击撤销按钮还是快捷键
ctrl+z
这里做的是什么事情呢?
其实是从编辑器的这个栈中拿出栈顶的元素,而栈顶元素记录的操作是输入了无天
,执行了撤销就删除了两个字,这即便是撤消的原理。
如图所示:
通过这个例子,我们向栈顶推入新的元素和从栈顶中取出元素,就可以作出撤销这样一个看起来很高级的逻辑
第二个栈的应用
这个例子便是,程序调用所使用的系统栈。如图所示:
事实上,在我们程序调用的过程中,经常会出现在一个逻辑的中间,先终止然后跳到另外的一个逻辑去执行。也就是所谓的子函数调用的这个过程。本质是使用一个栈这样的数据结构来记录我们程序的调用过程。而这个调用过程如图所示:
- 首先,程序执行A这个函数,开始执行a的第1行第2行,在第2行的时候,程序要跳转去执行B函数而暂时中断A这个函数。此时在我们的系统栈中就会记录一个信息,这个信息暂时叫做A2(之前程序执行到A函数的第2行,下同),在这里进行了中断
- 现在开始执行B函数了,依然是执行第1行第2行。在第2行要调用C这个子函数,同样在系统栈中压入一个信息并中断B这个函数,这个信息暂时叫做B2
- 现在开始执行C这个函数,C函数顺着它第1行第2行第3行,就直接执行完成了
- 下一步要执行谁?此时要看一下系统栈,对于系统栈来说,栈顶的元素是B2,计算机就知道了刚才是执行B函数的第2行时中断了,就跳回到B2这个位置继续执行
- 这样通过这个系统栈,就成功的帮助我们的计算机找到了上一次中断的位置。当然了,当回到B2这个位置继续执行时,这个记录的B2也就没用了,它就可以出栈了
- 下一步就是执行B3,然后B函数也执行完成了。再次查看系统栈了,栈顶元素还有一个A2,就回到A这个函数的第2行去继续执行,整个计算机的程序逻辑就正确的跳到了这里,而这个记录的A2也就没用了,它就可以出栈了
- 紧接着继续执行A的第3行,然后A执行完成,最后再次查看系统栈已为空,没有可执行的了。继而这整个过程都已经执行完了。
这就是在子函数进行调用的过程,当一个子过程执行完成之后,可以自动的回到上层,调用中断的位置,继续执行下去。而背后的原因,是因为有这样的一个系统栈,来记录每一次调用过程中所中断的那个点
栈应用的总结
通过这个例子可以看到,我们使用栈这样的一个看似非常简单的数据结构,解释清楚了一个在计算机领域非常复杂的问题,就是这种子过程子逻辑的调用,在我的编译器内部运行实现的这个机制是什么。
栈的实现
针对栈这种数据结构来说,基本上写了只涉及这5个操作,分别是
- 向栈中添加一个元素,也都是入栈,通常叫做
push
- 从栈中拿出栈顶的元素,也叫做出栈,通常叫做
pop
- 看一下栈顶的元素是谁,这个动作通常叫做
peek
,有些命名叫做top
- 看一下栈里一共有多少个元素
getSize
- 判断一下栈是否为空
isEmpty
在这里,为了让我们整个程序的逻辑更加的清晰,同时也是为了支持面向对象的一些特性,比如说支持多态性。那么,我在这个代码设计上,我设计一个Stack的接口,这个接口中定义了这5种操作。现在,基于我们动态数组来实现的一个栈,这个栈叫做ArrayStack(实际上是实现了我们设计的这个Stack这个接口)。
Stack接口设计如下:
|
ArrayStack实现代码如下:
public class ArrayStack<E> implements Stack<E>{
Array<E> array;
public ArrayStack(){
array = new Array();
}
public ArrayStack(int capacity){
array = new Array(capacity);
}
@Override
public void push(E e) {
array.addLast(e);
}
@Override
public E pop() {
return array.removeLast();
}
@Override
public E peek() {
return array.getLast();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
}
栈的复杂度分析
基于动态数组,真正的实现栈是非常方便的。最后我们还可以再看一下,ArrayStack所有操作对应的时间复杂度,它们都是O(1)的,但是在这里关键就是,对于push(),pop()操作来说,每一次我只向栈的最后一个位置去推入元素,删除元素也只从最后一个位置拿出元素,所以这个时间复杂度是O(1)的。而如果一旦触发了resize的话,经过均摊复杂度分析之后,它的时间复杂度依然是O(1) ,所以对于我们这个栈来说,在时间性能上是非常的好的。
案例实现:
- Array类的业务逻辑如下:
-
public class Array<E> { private E[] data; //设置为private,不希望用户从外部直接获取这些信息,防止用户篡改数据 private int size; //构造函数,传入数组的容量capacity构造Array public Array(int capacity) { data = (E[]) new Object[capacity]; size = 0; } //无参数构造函数,默认数组容量capacity=10 public Array() { this(10); //这里的capacity是IDE自动添加的提示信息,实际不存在 } //获取数组中的元素个数 public int getSize() { return size; } //获取数组的容量 public int getCapacity() { return data.length; } //判断数组是否为空 public boolean isEmpty() { return size == 0; } //向数组末尾添加一个新元素e public void addLast(E e) { add(size, e); } //向数组开头添加一个新元素e public void addFirst(E e) { add(0, e); } //在index位置插入一个新元素e public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size"); } if (size == data.length) { resize(2 * size); //扩大为原容量的2倍 } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++; } //获取index位置的元素 public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } //获取最后一个元素 public E getLast() { return get(size - 1); } //获取开头的元素 public E getFirst() { return get(0); } //修改index位置的元素为e public void set(int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Set failed. Index is illegal."); } data[index] = e; } //查找数组中是否存在元素e public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; } //查看数组中元素e的索引,若找不到元素e,返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; } //删除掉index位置的元素,并且返回删除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; //data[size]会指向一个类对象,这部分空间不会被释放loitering objects data[size] = null; if (size == data.length / 4 && data.length / 2 != 0) { resize(data.length / 2); //被利用的空间等于总空间的一半时,将数组容量减少一半 } return ret; } //删除掉数组开头的元素,并返回删除的元素 public E removeFirst() { return remove(0); } //删除掉数组末尾的元素,并返回删除的元素 public E removeLast() { return remove(size - 1); } //如果数组中有元素e,那么将其删除,否则什么也不做 public void removeElement(E e) { int index = find(e); if (index != -1) { remove(index); } } @Override public String toString() { //覆盖父类的toString方法 StringBuilder res = new StringBuilder(); res.append(String.format("Array: size=%d, capacity=%d\n", size, data.length)); res.append('['); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(", "); } } res.append(']'); return res.toString(); } private void resize(int newCapacity) { E[] newData = (E[]) new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; } }
4.. 对我们实现的栈进行测试:
public class Main {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>();
//测试入栈push
for (int i = 0; i < 5; i++) {
stack.push(i);
System.out.println(stack);
}
//测试出栈
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
5.. 栈的时间复杂度分析
-
Stack<E> ·void push(E) O(1) 均摊 ·E pop() O(1) 均摊 ·E peek() O(1) ·int getSize() O(1) ·boolean isEmpty() O(1)
6.. 栈的另外一个应用——括号匹配(leecode的第 20题)
-
分析:第一:初始化stack,遍历字符序列,将所有左括号的字符存放到stack中;
-
第二:如果stack为空,表示没有左括号返回false;
-
第三:弹出stack顶元素与剩余字符匹配,匹配都不成功,返回false ;
-
第四:匹配完后只有stack为空才表示匹配成功。
-
注意:栈顶元素反映在嵌套的层次关系中,最近的需要匹配的元素。
- 业务逻辑如下:
-
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 (topChar == '(' && c != ')') { return false; } if (topChar == '[' && c != ']') { return false; } if (topChar == '{' && c != '}') { return false; } } } return stack.isEmpty(); //这里很巧妙 //此时不能return true,因为如果还有元素,表示匹配失败 } //测试 public static void main(String[] args){ System.out.println((new Solution()).isValid("()")); //true System.out.println((new Solution()).isValid("()[]}{")); //false System.out.println((new Solution()).isValid("({[]})")); //true System.out.println((new Solution()).isValid("({)}[]")); //false } }