栈(Stack)是一种抽象数据类型,有数组(Array)和链表(LinkedList)两种实现方式。
栈的常用方法包括入栈(push)、出栈(pop)、清空(clear),以及相关辅助方法,如显示数据(print)、判断是否为空(isEmpty)等,用数组实现的栈还需要判断是否已满(isFull)和扩大容量(resize)方法。
数组实现栈(Stack backed by Array)
基本思路:栈是一种“后入先出”(Last In First Out, LIFO)的模型。在入栈时,符合“后来者居上”的规则,后添加的数据放在先添加的数据之上;在出栈时,数据自上而下被逐个取出,所以后添加的数据先取出(即入栈和出栈均作用于栈的同一端)。栈顶(top)指向最后添加的数据,栈底(bottom)指向最初添加的数据。
如果用数组实现栈,则数组的头在栈底,数组的尾在栈顶。入栈时,新的数据添加在数组的尾部(即栈顶),数组不断向上扩展。但是,需注意数组的大小是既定的,必要时需要对其扩容,因此需要定义isFull()方法判断数组是否已满,和resize()方法用于调整大小。出栈时,提取数组末端的数据即可,同时,也注意到“数组为空”的边界情况时,没有数据可以提取,因此需要定义一个isEmpty()方法来判断是否为空数组。
在显示数据时,如果想正确反映栈的结构(即第一行打印的是最后一个数据,第二行是倒数第二个数据,以此类推,最后一行打印的是第一个数据),应当先打印“后添加的数据”,即应当从后向前遍历数组。关于clear()方法的实现方式,文末详述。
![](https://img-blog.csdnimg.cn/img_convert/61909fb20b069cda8b47aaff9dab9562.png)
class ArrayStack {
private int size; // 数组大小
private int[] stack; // 数组
private int top = -1; // 栈顶在数组为空时,值为-1
public ArrayStack(int initialSize) {
// 保证数组大小为正
if (initialSize <= 0)
throw new RuntimeException("The size should be positive");
this.size = initialSize;
stack = new int[initialSize];
}
// // 通过栈顶所在位置判断数组是否已满或为空
public boolean isFull() { return top == size - 1; }
public boolean isEmpty() {
return top == -1;
}
public void push(int value) {
// 如果数组已满,先调整大小
if (isFull()) {
resize();
}
top++;
stack[top] = value;
}
public int pop() {
if (isEmpty()) {
throw new NoSuchElementException("The stack is empty now");
}
int value = stack[top--];
return value;
}
public void print() {
if (isEmpty()) {
System.out.println("The stack is empty now");
return;
}
for (int i=top; i>=0; i--) {
System.out.printf("stack[%d] = %d\n", i, stack[i]);
}
}
public void resize() {
// 定义一个大小为原数组两倍的新数组
int[] newArray = new int[size * 2];
// 复制原数组的数据至新数组
for (int i=0; i<size; i++)
newArray[i] = stack[i];
stack = newArray;
}
public void clear() {
int[] newArray = new int[top+1];
stack = newArray;
}
}
![](https://img-blog.csdnimg.cn/img_convert/61909fb20b069cda8b47aaff9dab9562.png)
2. 链表实现栈(Stack backed by Linked List)
基本思路:用链表实现栈,关键是要保证链表头部位于栈顶,这将为入栈、出栈和清空操作带来极大的便利。
入栈操作中,从栈的角度讲,新数据放在旧数据之上,而从链表的角度讲,就是新数据添加在链表头之前,再将链表头指向新数据。我们不断将链表头移至新数据的位置,形象地说,链表头是随着链表的扩展而不断向上移动的。这样,只需返回链表头指向的数据,就可以实现出栈功能。
在显示数据时,如果想正确反映栈的结构,我们知道应当“从新至旧”打印,对应到这里的链表,就是从链表头开始向后遍历,这样打印出来,自然就是“自上而下”遍历栈的顺序。所以,我们只需单链表即可实现上述功能。
class LinkedListStack {
private LinkedListNode head = null;
private int size = 0;
public boolean isEmpty() {
return size == 0;
}
public void push(int value) {
LinkedListNode node = new LinkedListNode(value);
node.setNext(head);
head = node;
size++;
}
public int pop() {
if (isEmpty()) {
throw new NoSuchElementException("The stack is empty now");
}
int value = head.getData();
head = head.getNext();
size--;
return value;
}
public void print() {
if (isEmpty())
throw new NoSuchElementException("The stack is empty now");
LinkedListNode curr = head;
int index = size - 1;
while (curr != null) {
System.out.printf("stack[%d] = %d\n", index--, curr.getData());
curr = curr.getNext();
}
}
public void clear() {
head = null;
size = 0;
}
}
class LinkedListNode {
private int data;
private LinkedListNode next;
public LinkedListNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public LinkedListNode getNext() {
return next;
}
public void setNext(LinkedListNode next) {
this.next = next;
}
}
时间复杂度(Time Complexity)
数组实现栈:入栈和出栈都作用在数组的尾部,不涉及其他元素的平移,因此均为O(1),但是入栈偶尔涉及数组的扩容(O(n)),因此准确来说是O(1)*。
链表实现栈:入栈和出栈都作用在链表的头部,因此均为O(1)。
数组和链表,对clear()方法的实现方式不同。
对于数组来说,要想实现clear()方法至少有三种方式:(1)第一种是通过遍历的手段,将所有元素逐个设置为零或null,但这样做的时间复杂度为O(n),耗时太高。(2)第二种是不对数组做任何处理,在加入新的数据时,直接将其覆盖在原数据上。这种方式固然简单,但实际上原数据仍保留在数组中,用户仍可以获取到,这在一些实际问题中,可能是开发者不希望看到的。(3)第三种方式是创建一个同样大小的新数组,并将其赋给原数组。由于数组元素默认为零,那么清空后的数组所有元素均为零,而原数组则被垃圾处理机制回收了。本文上述代码即采用了这一种方式。
而清空链表就简单得多,只需要把head设置为null,原有的数据就自动被清理掉了。
这样,无论是用数组,还使用链表来实现栈,除显示所有数据的print()方法必须遍历所有数据之外,其余方法的时间复杂度均为O(1)。
注:本文章截图来自【尚硅谷】“数据结构与算法”视频,由韩顺平老师讲授。