什么是栈
栈可以理解为一个放盘子的架子,盘子一个叠一个放进去,要取的时候只能从最顶上的开始取。后进先出就是典型的栈结构。
栈有什么用
作为一个基础的数据结构,一个典型的应用就是函数调用栈。OS给每个线程分配了一块独立的内存空间,这块内存空间被组织成了栈这种结构,用来存储函数调用时的临时变量。每进一个函数,就会将临时变量作为一个栈帧入栈,当函数被调用完成时将这个函数对应的栈帧出栈。
另一个应用场景是表达式求值。如32 + 10 * 200 + 12。 编译器无法像人一样自然而然先算乘法再算加法,计算机中通过两个栈实现,一个保存操作数,一个保存运算符。从左向右遍历表达式,当遇到数字,就直接压入操作数栈,当遇到运算符,就与运算符栈顶元素进行比较。如果栈顶运算符优先级高,就将当前运算符压栈,反之则取运算符栈顶元素以及从操作数栈取两个数做运算,将结果压到操作数栈,继续比较。
如何实现栈
栈是一个操作受限的线性表,只有压栈(push),出栈(pop),取栈顶元素(peek), 判断栈是否为空(isEmpty)等操作。因为本身基础是线性表,而线性表既可以用数组实现,也可以用链表实现。用数组实现的栈是顺序栈,用链表实现的栈叫链式栈。
以下为使用Java语言实现的顺序栈
package win.elegentjs.algorithms.stack;
/**
* 演示顺序栈(基于数组的实现)
*/
public class ArrayStackSample {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(10);
stack.push("a");
stack.push("b");
stack.push("c");
stack.push("d");
stack.push("e");
String item = stack.peek();
System.out.println("栈顶元素是:" + item);
item = stack.pop();
System.out.println("出栈元素是:" + item);
item = stack.peek();
System.out.println("栈顶元素是:" + item);
}
}
class ArrayStack {
private String[] contents;
private int available;
private int count;
public ArrayStack(int available) {
this.available = available;
contents = new String[available];
}
/**
* 执行压栈
*/
public boolean push(String item) {
if (count == available) {
return false;
}
contents[count++] = item;
return true;
}
/**
* 执行出栈
*/
public String pop() {
if (count == 0) {
return null;
}
return contents[-- count];
}
/**
* 取栈顶元素
*/
public String peek() {
if (count == 0) {
return null;
}
return contents[count - 1];
}
}
// result:
栈顶元素是:e
出栈元素是:e
栈顶元素是:d
分析栈操作时间复杂度
空间复杂度:O(1),因为不需要额外的存储空间,只涉及到个别的变量
时间复杂度:
push: O(1)
pop/peek: O(1)
顺序栈 vs 链式栈
数组的最大缺点是两点:
1)初始化后大小固定,无法扩容
2)从中间插入,删除元素会涉及到移位
同时它的优点是存储空间连续,方便随机查找。还有节省空间,不需要存储指向上一个元素和下一个元素的指针。
对应到栈这个数据结构,它的特点是只在栈顶操作,插入和删除都在栈顶,涉及不到中间元素的位移,所以可以避开数组的缺点。所以通常使用数组实现栈即可。当然如果经常对栈的深度不确定,需要频繁扩容的话还是链式栈实现方便,比较链表天生就是无限扩容的。
小结
本章讲解了栈这个基础的数据结构,本质它是一个受限的线性表,只能从一端操作元素,具有先进先出的特点。Java Collection API中使用LinkedList来实现栈。后面分析源码时再详细讲。