学习数据结构中的栈(Stack)及Java代码实现
之前总结了顺序表和链表的相关知识,这次我们来总结一下线性表的另一种表现方式** “栈” **
- 栈的基本概念
- 什么是栈?
- 栈的“先进先出”原则
- 栈的两种存储方式
- 栈顶和栈底的概念
- 出栈和入栈的概念
- 上溢和下溢的概念
- 顺序栈的概念与实现
- 什么是顺序栈?
- 栈顶下标
- 顺序栈的Java实现
- 链栈的概念与实现
- 什么链栈?
- 头指针和栈顶指针
- 链栈的Java实现
- 实现一
- 实现二
栈的基本概念
什么是栈?
"栈" 是线性表中的一种特殊的存储结构。与学习过的线性表的不同之处在于栈只能从表的固定一端对数据进行插入和删除操作,另一端是封死的。
栈的“先进先出”原则
当我们使用栈来存储数据元素时。栈对数据元素的存和取是有严格的规定的。数据按一定的顺序存储到栈中,当我们需要取出栈中的下层的数据时,需要将要取的数据之后入栈的数据先出栈,才能轮到我们需要取的数据出栈。
如上图,里面存放了4个数据,数据A位于栈底,数D位于栈顶。如果我们想要取出数据B,首先要将数据D出栈,然后数据C出栈,才能将我们要的数据B出栈。
就好比只有一个门的车库(每次仅允许一辆车通过),每辆车好比一个数据元素,只有离门最近的车先开出来,里边的车才能出来;最里边的车是最先开进去的,注定要最后出来。
栈的两种存储方式
因为栈也是线性表的一种表现形式,所以线性表的两种存储方式也能在栈中体现出来。采用顺序存储的栈称为顺序栈,采用链式存储的栈称位链栈
两者的区别在于存储的数据元素在物理结构上是否是相互紧挨着的。顺序栈存储元素预先申请连续的存储单元;链栈需要即申请,数据元素不紧挨着。
栈顶和栈底的概念
由于栈只有一边开口存取数据,称开口的那一端为**“栈顶”**,封死的那一端为 “栈底” 。我们只能从栈顶存入数据,也只能从栈顶取出数据
出栈和入栈的概念
栈操作数据元素只有两种动作:
我们在栈中将一个数据元素插入栈顶的过程就叫入栈,也称压栈
我们在将一个数据元素从栈顶位置取出的过程就叫出栈,也称弹栈
上溢和下溢的概念
我们在栈结构上存取数据的时候要避免处理两种情况:
- 上溢
在栈已经存满数据元素的情况下,如果继续向栈内存入数据,栈存储就会出错。(往上溢出) - 下溢
在栈内为空的状态下,如果对栈继续进行取数据的操作,就会出错。(取不到还取,往下溢出)
注意:
对于栈的两种表示方式来说,顺序栈两种情况都有可能发生;而链栈由于“随时需要,随时申请空间”的存储结构,不会出现“上溢”的情况。
小结:
栈满还存是上溢,栈空还取是下溢。顺序都有可能会发生,链式只能是下溢
顺序栈的概念与实现
什么是顺序栈?
采用顺序存储结构的栈就是顺序栈,一般顺序栈的底层采用数组
栈顶下标
一般我们在实现顺序表的时候,都需要定一个变量用来存储当前栈顶位置的下标,既top
。空栈时top的下标默认为-1。它的意义就如栈顶指针的作用,我需要知道栈顶元素在数组中的位置。
顺序栈的Java实现
顺序栈的实现有数组来完成。在顺序栈中设定一个随时指向栈顶元素的变量(一般命名为 top ),当 top 的值为 -1 时,说明数组中没有数据,即栈中没有数据元素,为“空栈”;只要数据元素进栈,top 就加 1 ;数据元素出栈, top 就减 1 。
要明确的地方:
栈是栈,数组是数组,我们要明确的区分他们之间的关系,栈是一种特定的数据结构,数组是我们用来构造顺序栈的底层结构。所以说当我们用代码实现时,要清楚的意思到我们的栈顶和栈底分别对应数组的什么位置。而且不要与数组尾和数组的头混淆。
注意:
我这里的实现是顺序栈的栈顶对应数组的尾部,数组的头部位置,既[0]位置就是栈底
所以顺序栈的最重要的要点即使栈下标
/**
* 顺序栈(不能扩容)
*
* @author SnailMann
*
* @param <T>
*/
public class SeqStack<T> {
private Object[] element; // 栈的底层数组
private int top; // 栈顶元素的下标
private int size; // 栈的最大内存容量
private int length; // 当前顺序栈的长度(含数据)
/**
* 顺序栈的默认构造方法
*
* @param size
*/
public SeqStack(int size) { // 构造空栈
this.element = new Object[size];
this.size = size;
this.length = 0; // 空栈的长度为0
this.top = -1; // 空栈时,栈顶元素下标默认为-1
}
/**
* 栈是否空栈
*
* @return
*/
public boolean isEmpty() {
return this.top == -1; // 判断当前栈顶元素的下标是否为-1
}
/**
* 返回栈的长度
*
* @return
*/
public int length() {
return this.length;
}
/**
* 入栈,存数据元素入栈顶
*
* @param t
* @throws Exception
*/
public void push(T t) throws Exception {
if(t == null)
throw new Exception("入栈数据不允许为NULL");
if(this.length == this.size)
throw new Exception("栈上溢");
top++;
this.length++;
this.element[this.top] = (Object) t; // 栈顶元素
}
/**
* 出栈,返回栈顶元素
*
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public T pop() throws Exception {
if(isEmpty())
throw new Exception("栈下溢");
this.length--;
return this.top == -1 ? null : (T) element[top--];
}
/**
* 获得栈顶元素,不出栈,仅是查询若栈空则返回null
* @return
*/
public T get() {
return this.top == -1 ? null : (T) element[top];
}
/**
* 重写toString方法
*/
public String toString(){
StringBuffer buffer = new StringBuffer("(");
for(int i = this.length - 1; i >= 0; i--){
if(this.element[i] == null) //如果是空栈,则直接返回()
return "()";
buffer.append(this.element[i].toString());
if(i != 0){
buffer.append(",");
}
}
buffer.append(")");
return buffer.toString();
}
private void checkException(){
}
public static void main(String[] args) throws Exception {
SeqStack<Integer> stack = new SeqStack<>(4);
System.out.println(stack.toString());
stack.pop();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack.length());
System.out.println(stack.toString());
}
/**
* 1、栈的顺序是与数组的索引顺序不同的。最先入栈的数据存放在数组的头部,最后入栈的数据放在数组的尾部
* 2、栈出栈时,从数组的尾部开始出栈,实际效果只是通过top下标的标识栈顶,数组中的出栈的数据是依然存在的,是无用垃圾数据,下次入栈则会被覆盖掉
*/
}
链栈的概念与实现
什么链栈?
采用链式存储的栈为链栈
链栈的头指针和栈顶指针
头指针
头指针永远指向链表的第一个结点
栈顶指针
栈顶指针永远指向栈顶
在单向链表实现的链栈,可以由两种方式来实现栈:
- 用链头作为栈顶
当用链头作为栈顶时,就容易很多,只需要一个指针空间。既头指针和栈顶指针合二唯一。 - 用链尾作为栈顶
需要两个指针空间,头指针用来指向链表第一个结点。栈顶指针指向栈顶,此时为链表的尾部,既尾结点。
链栈的Java实现
注意:
- 链栈一般不需要创建头结点,头结点会增加程序的复杂性,只需要创建一个头指针就可以了。
- 用链表表示栈时,用链表首元结点的一端作为栈的栈顶结点,这样做的好处是当数据元素压栈或者弹栈时,直接使用头指针就可以完成,不需要增设额外的指针。既栈顶指针和头指针合为一体
同样我们要明确的区分出链表和栈的关系,避免混淆,同时要知道栈顶和栈底对应链表的什么位置?
所以链栈最重要的要点就是栈顶位于链表的什么位置?既栈顶指针指向哪里?
- 实现一中,栈顶对应链表的首元结点,既头部
- 实现二中,栈顶对应链表的尾部
实现一:
用链表的头部作为栈顶,仅需要一个指针空间
/**
* 链栈二(无头结点单向链表)
* 用链表的首元结点来当栈顶,仅需要一个指针空间,链表头指针就是栈顶指针
* @author SnailMann
*
*/
public class LinkedStack2<T> {
private Node<T> top; //栈顶指针,既链表头指针
public LinkedStack2() {
this.top = null;
}
/**
* 链栈是否为空栈
* @return
*/
public boolean isEmpty(){
return this.top == null; //如果栈顶指针为null,则表示空栈
}
/**
* 返回链表的长度
* @return
*/
public int length(){
int len = 0;
Node<T> node = this.top; //获得首元结点,既栈顶结点
if(node == null)
return 0;
while(node != null){
len++;
node = node.next;
}
return len;
}
/**
* 新数据元素入栈,加入栈顶
* @param t
* @throws Exception //链栈没有上溢,没有固定长度
*/
public void push(T t) throws Exception{
if(t == null)
throw new Exception("入栈元素不允为null");
this.top = new Node<T>(t, this.top);// 头插入,新插入结点作为新的栈顶结点,其指针域指向原来的栈顶结点
}
/**
* 出栈,将栈顶元素弹出
* @throws Exception
*/
public T pop() throws Exception{
if(isEmpty())
throw new Exception("栈下溢");
Node<T> node = this.top; //获得栈顶结点,等待返回其数据域
this.top = this.top.next; //栈顶指针指向原栈顶结点的后继结点
return node.data;
}
public String toString(){
StringBuffer buffer = new StringBuffer("(");
Node<T> node = this.top; //获得链表的首元结点
if(isEmpty()) //如果空栈,所以返回();
return "()";
while(node != null){
buffer.append(node.data.toString());
if(node.next != null){
buffer.append(",");
}
node = node.next;
}
buffer.append(")");
return buffer.toString();
}
public static void main(String[] args) throws Exception {
LinkedStack2<Integer> stack = new LinkedStack2<>();
System.out.println(stack.length());
System.out.println(stack.toString());
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.pop();
stack.push(4);
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack.toString());
}
}
实现二
用链表的尾部作为栈顶,需要两个指针空间,一个是头指针,指向链表的首元结点。一个栈顶指针,执行栈顶结点(既链表尾结点)
/**
* 链栈一(无头结点单向链表)
* 用链表的尾部来当栈顶,所以需要两个指针空间,一个表示栈顶(链尾),一个表示首元结点
* @author SnailMann
*
* @param <T>
*/
public class LinkedStack1<T> {
private Node<T> top; //栈顶指针,是指向栈顶的指针,既链表的尾结点
private Node<T> head; //头指针,指向链表首元结点
/**
* 链栈的默认构造函数
*/
public LinkedStack1() { //构造空链栈,因为没有头结点,所以栈顶指针和头指针都指向null
this.top = null; //栈顶指针指向null
this.head = null; //链表头指针指向null
}
/**
* 链栈是否为空栈
* @return
*/
public boolean isEmpty(){
return this.top == null; //如果栈顶指针为null,则表示空栈
}
/**
* 返回链表的长度
* @return
*/
public int length(){
int len = 0;
Node<T> node = this.head; //获得首元结点
if(node == null)
return 0;
while(node != null){
len++;
node = node.next;
}
return len;
}
/**
* 新数据元素入栈,加入栈顶
* @param t
* @throws Exception //链栈没有上溢,没有固定长度
*/
public void push(T t) throws Exception{
if(t == null)
throw new Exception("入栈元素不允为null");
if(isEmpty()){ //如果是空表的情况
this.top = new Node<T>(t,null); //如果是空表,入栈的元素既为链表的首元结点,也即是栈顶元素
this.head = this.top; //初始化链表头指针,指向当前栈顶元素,既链表首元结点
} else { //非空表的情况
Node<T> node = this.top; //获得栈顶元素,既链表尾结点
node.next = new Node<T>(t,null); //链表尾结点指针域指向新的结点
this.top = node.next; //更新栈顶指针,指向新的链表尾结点
}
}
/**
* 出栈,将栈顶元素弹出
* @throws Exception
*/
public T pop() throws Exception{
if(isEmpty())
throw new Exception("栈下溢");
if(this.length() == 1){//链栈只剩一个数据元素时
Node<T> node = this.top; //获得栈顶元素,既首元结点
this.top = null; //栈顶元素指向null,链栈成为空表
this.head = null; //链表同步更新
return (T)node.data;
} else { //其他情况
Node<T> node = this.head; //获得首元结点
Node<T> temp = this.top; //获得栈顶结点(既要弹出的结点),等待返回
while(node.next.next != null){ //获得栈顶(链表尾结点)结点的前驱结点
node = node.next;
}
node.next = null; //栈顶结点的前驱结点的指针域指向null,链表删除栈顶结点
this.top = node; //栈顶指针指向栈顶结点的前驱结点
return temp.data;
}
}
public String toString(){
StringBuffer buffer = new StringBuffer("(");
Node<T> node = this.head; //获得链表的首元结点
if(node == null) //如果链表首元结点为null,说明空栈,所以返回();
return "()";
while(node != null){
buffer.append(node.data.toString());
if(node != this.top){
buffer.append(",");
}
node = node.next;
}
buffer.append(")");
return buffer.toString();
}
public static void main(String[] args) throws Exception {
LinkedStack1<Integer> stack = new LinkedStack1<>();
System.out.println(stack.length());
System.out.println(stack.toString());
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.pop();
stack.push(4);
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println(stack.toString());
}
/**
* 1、区别栈顶指针和链表头指针
* 2、栈顶其实就相当于链表的尾结点,所以栈顶指针永远执行的就是链表尾结点。链表头指针则指向链表首元结点
*/
}
参考资料
首先强烈推荐学习数据结构的朋友,直接去看该网站学习,作者是@严长生。里面的资料是基于严蔚敏的《数据结构(C语言版)》,本文的概念知识都是基于该网站和《数据结构(C语言版)》这个书来分析的。
数据结构概述 - 作者:@严长生
代码实现部分,主要根据自己的实现再参考大佬,发现不足,加以修改的。(原作更好!!)
Github - doubleview/data-structure