目录
1 栈的结构形式与操作原则
栈是仅允许在一端进行增删操作的线性表,由于进出都在同一端,栈一般被形象化为一个纵向的容器,第一个进栈的元素位于栈底(bottom),最后进入的元素位于栈顶(top),只有栈顶能进行数据增删的操作。至于查找操作,和链表一样,只能遍历,别无他法。我们用一个存储了八大行星的栈来展示栈的结构形式。
向栈中存入数据的操作被称为压栈(push,也叫进栈),从栈中取出元素的操作被称为弹栈(pop,也叫出栈)。由于栈只有一个数据出入口,因此遵循后进先出(last in first out, 简写为LIFO)的操作原则,因为一旦有新数据进入,原来的栈顶元素就出不来了,如同只有一个门的公共汽车要求先下后上,否则想要下车的人就会被已经上车的人堵住去路。下图展示了压栈的过程,向栈中依次存入八大行星。
准备存入地球时出现了错误,按照八大行星的顺序,地球(Earth)应该在金星(Venus)后面,然后才是火星(Mars),即地球在金星和火星之间。然而,由于栈的LIFO原则,不允许在金星和火星之间插入地球,为了保持顺序,唯一的办法是先弹栈取出火星(先出),再压栈存入地球(后进),然后重复压栈过程,依次存入火星、木星、土星、天王星和海王星,即可得到上述八大行星栈。
从栈的结构形式和增删操作原则来看,栈本质上是一种功能受限的线性表,不如结构形式差不多、但支持从任意位置操作元素的数组和链表灵活。然而,任何事物的存在都有其理由,正由于链表或数组的灵活性,暴露了太多可操作性的接口,对于仅涉及端点数据处理的场景,这是没有必要的,同时也是不安全的。
2 两种类型栈的增删查操作以及代码实现(Java)
栈根据存储方式分为两种类型,一是基于顺序存储的顺序栈,二是基于链式存储的链式栈。下面用java模拟八大行星栈的操作。
2.1 顺序栈增删查操作及其代码实现
顺序栈可以用数组来模拟,数组的第0个元素为栈底元素,最后一个元素为栈顶元素,有一个top指针始终指向栈顶元素。由于顺序栈是用数组模拟的,因此top指针对应于数组最后一个非空元素的索引,栈为空时top=-1。数组需要指定容量,因此需要根据栈内数据量扩充或收缩数组的最大容量。
我们在顺序栈中定义了一系列用于增删查的公共方法,包括:push()压栈,pop()弹栈并取回元素,remove()弹栈不取回元素,peek()获取栈顶元素、get(int distance)根据与栈顶距离获取任意位置的元素、printAll()从栈顶到栈底遍历并打印所有的元素。
MyArrayStack.java:准备工作1,创建一个顺序栈的类
package com.notes.data_structure2;
import java.util.EmptyStackException;
public class MyArrayStack<T> {
private T[] array = (T[]) new Object[8]; // 数组的最大容量初始值设定为8
private int top = -1; // 栈顶指针,对应数组索引,初始值为-1,此时栈为空
/**
* 获取当前 栈 中元素的个数
* @return top+1
*/
public int size() {
return top + 1;
}
/**
* 模拟 压栈 操作
*/
public void push(T data) {
if(size() == array.length) {
expend(); // 如果 栈 的数据量达到数组最大容量,扩容数组
}
array[++top] = data; // 栈顶指针上移一位,然后新增元素
}
/**
* 弹栈 并 取回 元素
* @return 弹出的元素
*/
public T pop() {
if(top==-1){ // 栈为空,抛出异常
throw new EmptyStackException();
}
T ele = array[top]; // 将元素取回
array[top--] = null; // 将元素弹出,栈顶指针下移一位
if(array.length-size()>8) {
shrink(); // 数组的空余容量超过8,缩容数组
}
return ele;
}
/**
* 出栈不取回
*/
public void remove() {
if(top==-1){ // 栈为空,抛出异常
throw new EmptyStackException();
}
array[top--] = null; // 将元素弹出,栈顶指针下移一位
if(array.length-size()>8) {
shrink(); // 数组的空余容量超过8,缩容数组
}
}
/**
* 获取栈顶元素
* @return 栈顶元素
*/
public T peek() {
if(top==-1){ // 栈为空,抛出异常
throw new EmptyStackException();
}
return array[top];
}
/**
* 获取任意位置的元素,从栈顶向下查找
* @param distance 与栈顶的距离,从栈顶开始数第几个元素,0即栈顶本身
* @return
*/
public T get(int distance) throws DistanceOutOfBoundsException {
// 这里故意写成循环,模拟指针移动的过程
int temp = top; // 移动的指针,起始位置为栈顶
if(top-distance>=0) { // 距离未超出栈容量限制
for(int i=0;i<distance;i++) {
temp--; // 指针不断下移,直到指定的位置
}
return array[temp]; // 取出指定位置的元素
}else { // 距离超出栈容量限制,抛出自定义异常
throw new DistanceOutOfBoundsException("栈顶与栈低的距离为"+top);
}
}
public void printAll() {
if(top==-1){ // 栈为空,抛出异常
throw new EmptyStackException();
}else{
int temp = top; // 移动的指针,起始位置为栈顶
while (temp>-1) { // 不断下移,直到栈底
System.out.println(array[temp--]);
}
}
}
/**
* 数组的动态扩容
*/
private void expend() {
T[] newArray = (T[]) new Object[array.length + 8];
System.arraycopy(array,0,newArray,0,size());
array = newArray;
}
/**
* 数组的动态缩容
*/
private void shrink() {
int newSize = array.length - 8;
T[] newArray = (T[]) new Object[newSize];
System.arraycopy(array,0,newArray,0,newSize);
array = newArray;
}
}
DistanceOutOfBoundsException.java:准备工作2,针对get()方法自定义一个异常类
package com.notes.data_structure2;
public class DistanceOutOfBoundsException extends Exception {
public DistanceOutOfBoundsException(String message) {
super(message);
}
}
ArrayStackDemo1.java:模拟后进先出的操作(结合上一节的图示)
package com.notes.data_structure2;
public class ArrayStackDemo1 {
public static void main(String[] args) throws DistanceOutOfBoundsException {
// 实例化 MyArrayStack 类
MyArrayStack<String> myStack = new MyArrayStack<>();
/**
* 模拟 后进先出 的过程
*/
// 压栈:新增元素
myStack.push("Mercury"); // 水星
myStack.push("Venus"); // 金星
myStack.push("Mars"); // 火星
// myStack.printAll(); // 打印验证
// 弹栈并取回:将 火星 从栈中弹出并取回(先出,红色箭头)
String mars = myStack.pop();
// 压栈:将 地球 放在金星的上面(后进:绿色箭头)
myStack.push("Earth");
// 重新压栈:将 火星 重新增加到栈里面
myStack.push(mars);
myStack.printAll(); // 打印验证
}
}
ArrayStackDemo2.java:验证两个查找方法:peek() 和 get(int distance)
package com.notes.data_structure2;
public class ArrayStackDemo2 {
public static void main(String[] args) throws DistanceOutOfBoundsException {
MyArrayStack<String> myStack2 = new MyArrayStack<>();
myStack2.push("Mercury"); // 水星
myStack2.push("Venus"); // 金星
myStack2.push("Earth"); // 地球
myStack2.push("Mars"); // 火星
myStack2.push("Jupiter"); // 木星
myStack2.push("Saturn"); // 土星
myStack2.push("Uranus"); // 天王星
myStack2.push("Neptune"); // 海王星
/**
* 验证查找元素的方法:peek() 和 get(int distance)
*/
// 验证 peek() 方法
String topElement = myStack2.peek();
System.out.println(topElement); // 输出栈顶元素:Neptune
// 验证 get(int distance) 方法
String ele1 = myStack2.get(0);
System.out.println(ele1); // 输出栈顶元素:Neptune
String ele2 = myStack2.get(3);
System.out.println(ele2); // 输出从与栈顶距离为3的元素:Jupiter
String ele3 = myStack2.get(10); // 报出异常:DistanceOutOfBoundsException: 栈顶与栈低的距离为7
System.out.println(ele3);
}
}
2.2 链式栈增删查操作及其代码实现
链式栈就是用链表的结构形式组织数据。为方便理解,将其形象化为一个自上而下的链条,从栈顶结点开始,每一个结点都有一个指针指向它的下一个结点,栈底结点的指针为空。所有结点的指针都不允许修改,无法通过改变指针指向实现增删操作。有一个top指针始终指向栈顶结点,用以支持压栈与弹栈的操作。
压栈时,先让新结点的指针指向原栈顶,再让栈顶指针指向新结点。弹栈时,让栈顶指针指向原栈顶的下一个结点,使下一个结点成为栈顶。
我们在链式栈中定义了一系列用于增删查的公共方法,包括:push()压栈,pop()弹栈并取回元素,remove()弹栈不取回元素,peek()获取栈顶元素、get(int distance)根据与栈顶距离获取任意位置的元素、printAll()从栈顶到栈底遍历并打印所有的元素。
MyLinkStack.java:准备工作,创建一个链式栈的类
package com.notes.data_structure2;
public class MyLinkedStack<T> {
private Node top; // 栈顶指针
private Node topNode; // 栈顶结点
// 定义一个结点类
public class Node {
// 结点的两个要素:数据和指针
private T data;
private Node next;
// 构造方法,初始化data属性
public Node(T data) {
this.data = data;
}
@Override
public String toString() { // 用于打印结点中的data属性
return "Node{" +
"data=" + data +
'}';
}
}
/**
* 压栈
* @param data
*/
public void push(T data) {
Node newNode = new Node(data);
if (top == null) { // 链栈为空,新结点为栈底结点
newNode.next = null; // 栈底结点的指针为空
}else { // 新结点指向原来的栈顶结点
newNode.next = topNode;
}
topNode = newNode; // 新结点成为顶结点
top = topNode; // 栈顶指针指向栈顶结点
}
/**
* 弹栈 并 取回 结点的数据
* @return
*/
public T pop() {
Node node = topNode; // 将原栈顶结点取回
top = topNode.next; // 栈顶指针指向原栈顶的下一个结点
topNode = top; // 原栈顶的下一个结点成为栈顶
return node.data; // 为方便使用,返回结点的数据
}
/**
* 弹栈不取回
*/
public void remove() {
top = topNode.next; // 栈顶指针指向原栈顶的下一个结点
topNode = top; // 原栈顶的下一个结点成为栈顶
}
/**
* 获取栈顶结点的数据
* @return
*/
public T peek() {
return topNode.data; // 为方便使用,返回结点的数据
}
/**
* 获取任意位置的结点,从栈顶向下查找
* 如果距离超出限制,系统会报空指针异常
* @param distance 与栈顶的距离,从栈顶开始数第几个元素,0即栈顶本身
* @return Node
*/
public T get(int distance) {
Node temp = top; // temp是一个移动的指针
for(int i=0;i<distance;i++){
temp = temp.next; // 不断下移,直到指定位置
}
return temp.data; // 为方便使用,返回结点的数据
}
/**
* 遍历打印所的元素
*/
public void printAll() {
Node temp = top; // temp是一个移动的指针
while (temp!=null) { // 不断下移,直到null
System.out.println(temp); // 打印时自动调用toString()方法
temp = temp.next;
}
}
}
LinkedStackDemo1.java:模拟后进先出的操作(结合上一节的图示)
package com.notes.data_structure2;
public class LinkedStackDemo1 {
public static void main(String[] args) {
// 实例化 MyLinkedStack 类
MyLinkedStack myStack = new MyLinkedStack();
/**
* 模拟 后进先出 的过程
*/
myStack.push("Mercury"); // 水星
myStack.push("Venus"); // 金星
myStack.push("Mars"); // 火星
// myStack.printAll(); // 打印验证
// 弹栈并取回:将 火星 结点的数据从栈中弹出并取回(先出,红色箭头)
String marsData = (String) myStack.pop();
// 压栈:将 地球 放在金星的上面(后进:绿色箭头)
myStack.push("Earth");
// 重新压栈:将 火星 重新增加到栈里面
myStack.push(marsData);
myStack.printAll(); // 打印验证
}
}
LinkedStackDemo2.java:验证两个查找方法:peek() 和 get(int distance)
package com.notes.data_structure2;
public class LinkedStackDemo2 {
public static void main(String[] args) {
MyLinkedStack<String> myStack2 = new MyLinkedStack<>();
myStack2.push("Mercury"); // 水星
myStack2.push("Venus"); // 金星
myStack2.push("Earth"); // 地球
myStack2.push("Mars"); // 火星
myStack2.push("Jupiter"); // 木星
myStack2.push("Saturn"); // 土星
myStack2.push("Uranus"); // 天王星
myStack2.push("Neptune"); // 海王星
/**
* 验证查找元素的方法:peek() 和 get(int distance)
*/
// 验证 peek() 方法
String topElement = myStack2.peek();
System.out.println(topElement); // 输出栈顶元素:Neptune
// 验证 get(int distance) 方法
String ele1 = myStack2.get(0);
System.out.println(ele1); // 输出栈顶元素:Neptune
String ele2 = myStack2.get(3);
System.out.println(ele2); // 输出从与栈顶距离为3的元素:Jupiter
String ele3 = myStack2.get(10); // 系统报出空指针异常:NullPointerException
System.out.println(ele3);
}
}