一、什么是栈
前言:放假在家里看书的感觉真好呀~。栈作为一种特殊的线性表,操作较为简单,但是有着很多的应用场景。
(一)栈的定义
栈 (stack) 是限定仅在栈顶进行插入和删除操作的线性表。
(二)栈的特点
- 遵循后进先出 (LIFO) 的原则。
- 栈只允许访问一个数据(最后插入的数据),即只能对栈顶的数据进行操作。
- 一般的,栈的插入操作称为进栈 (push),栈的删除操作称为出栈 (pop)。
二、为什么要用栈
栈的引人简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而可能掩盖了问题的本质。
三、如何操作栈
栈作为一种抽象数据类型 (ADT),可通过数组和链表来实现。
首先提供的是Stack接口,其内部定义了栈的抽象方法。
/**
* 栈接口,遵循后进先出的原则。
*
* @author likezhen
* @version 1.0
*/
public interface Stack<E> {
/**
* 返回栈中存放的数据个数
*
* @return 栈中存放的数据个数
*/
int size();
/**
* 判断是否为空栈
*
* @return 是否为空栈
*/
boolean isEmpty();
/**
* 将数据插入栈顶
* @param e 待入栈的数据
*/
void push(E e);
/**
* 移除并返回栈顶数据
*
* @return 栈顶数据(如果为空栈则返回Null)
*/
E pop();
/**
* 返回栈顶数据
*
* @return 栈顶数据(如果为空栈则返回Null)
*/
E peek();
}
(一)顺序栈
用数组来实现的栈称为顺序栈。特别地,数组的尾部为栈顶,索引为0的位置为栈底。
顺序栈类 ArrayStack 实现栈接口。顺序栈和数组一样,需要先指明其容量,一旦确定,就不能更改。
/**
* 顺序栈类,通过数组来实现栈结构
* 提供了pop(), push(), peek()等方法
*
* @author likezhen
* @version 1.0
*/
class ArrayStack<E> implements Stack<E> {
public final static int CAPACITY = 10; //默认容量为10
private E[] data; //存放数据的数组
private int t = -1; //空栈时计数器为-1
public ArrayStack() {
this(CAPACITY);
}
public ArrayStack(int capacity) { //可自定义栈的容量
data = (E[]) new Object[capacity];
}
@Override
public int size() {
return (t + 1);
}
@Override
public boolean isEmpty() {
return t == -1;
}
@Override
public void push(E e) throws IllegalStateException {
if (size() == data.length) throw new IllegalStateException("栈已满");
data[++t] = e; //先自增,后赋值
}
@Override
public E peek() {
if (isEmpty()) return null;
return data[t];
}
@Override
public E pop() {
if (isEmpty()) return null;
E answer = data[t]; //先赋值
data[t] = null; //让垃圾回收器回收
t--; //后自减
return answer;
}
}
在顺序栈的测试类中,和我之前的一篇总结文章(【数据结构与算法】之链表)一样,用Person类作为待储存的数据。进栈顺序为 A->B->C,栈顶数据为C,出栈顺序为 C->B->A,符合栈的后进先出原则。
/**
* 顺序栈的测试类
*
* @author likezhen
* @version 1.0
*/
public class ArrayStackApp {
public static void main(String[] args) {
ArrayStack<Person> arrayStack = new ArrayStack<>(3); //空栈,容量为3
arrayStack.push(new Person("A", 1)); //进栈
arrayStack.push(new Person("B", 2)); //进栈
arrayStack.push(new Person("C", 3)); //进栈
System.out.println("--------进栈顺序-------\n" +
"Person{name='A', age=1}\n" +
"Person{name='B', age=2}\n" +
"Person{name='C', age=3}");
System.out.println("--------栈顶元素--------");
System.out.println(arrayStack.peek());
System.out.println("--------出栈顺序--------");
while (!arrayStack.isEmpty()) {
System.out.println(arrayStack.peek());
arrayStack.pop();
}
}
}
测试结果:
D:\Java\jdk\bin\java.exe
--------进栈顺序-------
Person{name='A', age=1}
Person{name='B', age=2}
Person{name='C', age=3}
--------栈顶元素--------
Person{name='C', age=3}
--------出栈顺序--------
Person{name='C', age=3}
Person{name='B', age=2}
Person{name='A', age=1}
Process finished with exit code 0
(二)链栈
用单链表来实现的栈称为链栈。特别地,与顺序栈相反,单链表的表头为栈顶,表尾为栈底。原因之一是在栈的所有操作中时间复杂度都为 O(1),在单链表表头增加和删除一个结点的时间复杂度也都为 O(1),但在单链表的表尾删除一个结点的时间复杂度为 O(N),所以表头为栈顶更适合。
链栈类 LinkedStack 实现了栈接口,封装了一个单链表对象。通过调用单链表的各种方法,即可实现链栈的操作。关于单链表的源代码,可参考我之前总结的一篇关于链表的文章——【数据结构与算法】之链表。
栈方法 | 单链表方法 | 描述 |
---|---|---|
size() | list.getSize() | 获取栈(或链表)中的数据个数 |
isEmpty() | list.isEmpty() | 判断是否为空栈(或空链表) |
push(E e) | list.addFirst(E e) | 在栈顶(或表头)添加数据 |
pop() | list.removeFirst() | 移除栈顶(或表头)的数据 |
peek() | list.first() | 获取栈顶(或表头)存放的数据 |
/**
* 链栈类,通过链表来实现栈结构
* 提供了pop(), push(), peek()等方法
*
* @author likezhen
* @version 1.0
*/
class LinkedStack<E> implements Stack<E> {
private SinglyLinkedList<E> list = new SinglyLinkedList<>(); //私有的单链表对象
public LinkedStack() {
}
@Override
public int size() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public void push(E e) {
list.addFirst(e);
}
@Override
public E pop() {
return list.removeFirst();
}
@Override
public E peek() {
return list.first();
}
}
在链栈的测试类中,用Person类作为待储存的数据。进栈顺序为 A->B->C,栈顶数据为C,出栈顺序为 C->B->A,符合栈的后进先出原则。
/**
* 链栈的测试类
*
* @author likezhen
* @version 1.0
*/
public class LinkedStackApp {
public static void main(String[] args) {
LinkedStack<Person> linkedStack = new LinkedStack<>(); //空栈
linkedStack.push(new Person("A", 1)); //进栈
linkedStack.push(new Person("B", 2)); //进栈
linkedStack.push(new Person("C", 3)); //进栈
System.out.println("--------进栈顺序-------\n" +
"Person{name='A', age=1}\n" +
"Person{name='B', age=2}\n" +
"Person{name='C', age=3}");
System.out.println("--------栈顶元素--------");
System.out.println(linkedStack.peek());
System.out.println("--------出栈顺序--------");
while (!linkedStack.isEmpty()) {
System.out.println(linkedStack.peek());
linkedStack.pop();
}
}
}
测试结果:
D:\Java\jdk\bin\java.exe
--------进栈顺序-------
Person{name='A', age=1}
Person{name='B', age=2}
Person{name='C', age=3}
--------栈顶元素--------
Person{name='C', age=3}
--------出栈顺序--------
Person{name='C', age=3}
Person{name='B', age=2}
Person{name='A', age=1}
Process finished with exit code 0
- 时间复杂度分析:
栈因为其特殊的结构,永远只能操作一个数据,所以其进出栈等所有栈操作的时间复杂度都为 O(1),与数据量无关。 - 空间复杂度分析:
顺序栈的空间复杂度为 O(N),与数组的长度成正比。
链栈的空间复杂度为 O(N),与数据量成正比。