【知识框架】
栈
栈介绍
栈(Stack)是一个线性表,线性存储结构,插入和删除位置限制在线性表的尾部进行插入,只能插入在最后一个元素的后面,也只能删除最后一个元素,栈是插入和删除受到限制的线性表,通常将允许删除,插入操作的一端称为栈顶,另一端称为栈底。限定只能在栈顶进行插入和删除操作。
栈是一种先进后出(First In Last Out)的线性表,简称 FILO。
栈的相关概念
假设某个栈S = { a1, a2, … , an },如上图所示,则 a1 为栈底元素,an 为栈顶元素。由于只能在栈顶进行插入和删除操作,故进栈顺序为 a1, a2, … , an,出栈顺序为 an, … , a2, a1。
假如,栈中存放了 4 个数据元素,进栈的顺序是 A 先进栈,然后 B 进,然后 C 进,最后 D 进栈;当需要调取 A 时,首先 D 出栈,然后 C 出栈,然后 B 出栈,最后 A 才能出栈被调用。
栈顶(Top):线性表允许进行插入和删除的一端。
栈底(Bottom):固定的,不允许进行插入和删除的另一端。
空栈: 不含任何一个元素的栈称为空栈。
压栈:栈的插入操作,叫做进栈,也称压栈、入栈,压栈通常命名为push。
进行入栈操作的时候,每次放入一个数据后,栈顶指针依次向上移动一位即可,如图所示:
弹栈:栈的删除操作,也叫做出栈、退栈、弹出,弹栈通常命名为pop。
进行出栈操作时,取出栈顶元素后,栈顶指针依次向下移动一位,如下所示:
核心类库中的栈结构有 Stack 和 LinkedList。
- Stack就是顺序栈,它是 Vector 的子类。
- LinkedList 是链式栈。
体现栈结构的操作方法:
- peek()方法:查看栈顶元素,不弹出
- pop()方法:弹出栈
- push(E e)方法:压入栈
时间复杂度:
- 索引: O(n)
- 搜索: O(n)
- 插入: O(1)
- 移除: O(1)
栈的常见分类
(1)基于数组的栈——以数组为底层数据结构时,通常以数组头为栈底,数组尾为栈顶,数组头到数组尾为栈顶的生长方向;
(2)基于单链表的栈——以链表为底层的数据结构时,以链表头为栈顶,便于节点的插入与删除,压栈产生的新节点将一直出现在链表的头部。
栈的“上溢”和“下溢”
栈存储结构调取栈中数据元素时,要避免出现“上溢”和“下溢”的情况:
“上溢”:在栈已经存满数据元素的情况下,如果继续向栈内存入数据,栈存储就会出错。
“下溢”:在栈内为空的状态下,如果对栈继续进行取数据的操作,就会出错。
对于栈的两种表示方式来说:
- 顺序栈两种情况都有可能发生;
- 而链栈由于“随时需要,随时申请空间”的存储结构,不会出现“上溢”的情况。
提醒:
入栈:先判断栈是否满了,没满,top再加1,之后再入栈。
出栈:先判断栈是否空了,没空,再出栈,之后top再减1。
栈的顺序存储结构及实现
1、确定用数组的哪一端表示栈底;
2、定义一个变量top来指示栈顶元素在数组中的位置。
top可以定义为指针类型也可以定义为整型。
如果定义为整型类型,我们可以让top的初始值为-1,第一个元素进栈后,top的值再加1,变为0。从而top就指向了第一个元素的位置。
3、入栈操作
总结:入栈操作,top先加1,然后在top所指向的位置存入元素。
4、出栈操作
总结:出栈操作,先将top所指向的位置的元素出栈,top值再减1。
4、栈空
top = -1。
5、栈满
6、栈的上溢和下溢
图中的an,...,az,a1,a[0]来存储。
需要定义一个top变量存储栈顶指针的位置,它存储的是最后处理的那个元素的下标。
栈顶
还需要定义一个MAXSIZE表示栈的最大容量。
上溢的判断
当栈为空的时候,top的值为-1。当往栈中压入一个元素时,top的值会加1。这样,a[0]就代表第一个进栈的元素,a[i-1]代表第i个进栈的元素,a[top]则表示栈顶元素。
当top = MAX SIZE -1时表示栈满。如果再有元素进栈时,则栈会溢出,这时称为“上溢”。
下溢的判断
反之,当top等于-1时,再将栈顶元素弹出,就要发生“下溢”。
因此在进行栈处理的时候,我们应该以要判断是否会发生“上溢”和“下溢”的情况,并做出相应处理。
存储结构
栈的基本介绍
顺序栈基本介绍
顺序栈(基于数组)
栈接口
public interface Stack{
/**
* 入栈
* @param object
*/
public void push(Object object);
/**
* 出栈
* @return
*/
public Object pop();
/**
* 获取栈顶元素
* @return
*/
public Object getTop();
/**
* 判断栈是否为空
* @return
*/
public boolean isEmpty();
}
实现类构造方法
public class SequenceStack implements Stack {
Object[] stack; // 对象数组
final int defaultSize = 10; // 默认最大长度
int top; // 栈顶位置元素的下标
int maxSize; //最大长度
//定义无参构造函数初始化栈
public SequenceStack() {
init(defaultSize);
}
//带有指定长度的构造函数初始化栈
public SequenceStack(int maxSize) {
init(maxSize);
}
//初始化方法
private void init(int maxSize) {
this.maxSize = maxSize;
top = 0;
stack = new Object[maxSize];
}
}
入栈
@Override
public void push(Object object) {
//判断栈是否已经存满
if (top == maxSize) {
System.out.println("栈空间不足,入栈失败");
return;
}
stack[top] = object;
top++;
}
出栈
@Override
public Object pop() {
//判断栈是否为空
if (isEmpty()) {
System.out.println("栈为空");
return;
}
top--;
return stack[top];
}
获取栈顶元
@Override
public Object getTop() {
//判断栈是否为空
if (isEmpty()) {
System.out.println("栈为空");
return;
}
return stack[top - 1];
}
判断栈是否为空
@Override
public boolean isEmpty() {
return top == 0;
}
判断栈是否满
@Override
public Boolean isFull() {
return top == maxSize;
}
清除
@Override
public void clear() {
top = 0;
}
链式栈基本介绍
链栈是一种基于链表实现的栈,其特点是无需事先分配固定长度的存储空间,栈的长度可以动态增长或缩小,避免了顺序栈可能存在的空间浪费和存储溢出问题。
链栈中的每个元素称为“节点”,每个节点包括两个部分:数据域和指针域。数据域用来存储栈中的元素值,指针域用来指向栈顶元素所在的节点。
链栈的基本操作包括入栈、出栈、获取栈顶元素和遍历等,相比顺序栈而言,链栈的实现难度稍高,但其在某些情况下有着更好的灵活性和效率,特别适用于在动态存储空间较为紧缺的场合。
链栈的进栈push和出栈pop操作都很简单,时间复杂度均为O(1)。
注意:如果栈的使用过程中元素变化不可预料,那么最好使用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈。
链栈的种类
链栈按照链表的实现方式可分为单链栈和双链栈。实际应用通常采用单链栈。
单链栈使用单链表实现,每个节点只含有一个指向下一个节点的指针。因此,单链栈只能从栈顶进行插入和删除操作。
采用链式存储的栈称为链栈。链栈的优点是便于多个栈共享存储空间和提高效率,且不存在栈满上溢的清空。通常采用单链表实现,并且规定所有操作都是在单链表的表头进行上的(因为头结点的 next
指针指向栈的栈顶结点)。
特点
- 采用链式存储,便于结点的插入和删除,但是对于栈这种只能在一端操作的数据结构来说,顺序栈和链栈插入和删除的时间复杂度区别不大。
- 链栈的操作和顺序栈的操作类似,出栈和入栈的操作都在栈顶进行。
- 对于带头结点和不带头结点的链栈,具体实现有所不同,常用带头结点。
链栈的进栈
push
:将元素入栈。以 stack=[11, 22, 33]; ele=44
为例如图所示:
链栈的出栈
pop
:将元素出栈,如果是空栈则不能出栈,并且将出栈元素保存到 ele
中。以 stack=[44, 33, 22, 11]
为例如图所示:
链栈的结构体定义
链式栈(基于单链表)
链式堆栈是由一个个节点组成的,每个节点有两个域组成,一个是存放数据元素的数据元素域data,另一个是存放指向下一个节点的对象引用域next。
节点类
/**
* 节点类
*/
public class Node {
/**
* 存放数据
*/
Object element;
/**
* 存放下一个节点
*/
Node next;
public Node() {
}
public Node(Node next) {
this.next = next;
}
public Node(Object element, Node next) {
this.element = element;
this.next = next;
}
public Object getElement() {
return element;
}
public void setElement(Object element) {
this.element = element;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
实现类
public class LinkStack implements Stack {
Node head; // 栈顶指针
int size; // 结点的个数
}
入栈
@Override
public void push(Object object) {
//新的object,老的head 一起赋给新的head
head = new Node(object, head);
size++;
}
出栈
@Override
public Object pop() {
if (isEmpty()) {
System.out.println("栈为空");
return;
}
Object element = head.getElement();
head = head.getNext();
size--;
return element;
}
获取栈顶元素
@Override
public Object getTop() {
if (isEmpty()) {
System.out.println("栈为空");
return;
}
return head.getElement();
}
判断栈是否为空
@Override
public boolean isEmpty() {
return head == null;
}
清除
@Override
public void clear() {
head = null;
size = 0;
}
Stack使用举例
public class TestStack {
/*
* 测试Stack
* */
@Test
public void test1(){
Stack<Integer> list = new Stack<>();
list.push(1);
list.push(2);
list.push(3);
System.out.println("list = " + list);
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
/*
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException
*/
while(!list.empty()){
System.out.println("list.pop() =" + list.pop());
}
}
/*
* 测试LinkedList
* */
@Test
public void test2(){
LinkedList<Integer> list = new LinkedList<>();
list.push(1);
list.push(2);
list.push(3);
System.out.println("list = " + list);
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
/*
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException
*/
while(!list.isEmpty()){
System.out.println("list.pop() =" + list.pop());
}
}
}
自定义栈
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws Exception {
if(index >= elements.length - 1){
//方式1:
//System.out.println("压栈失败,栈已满!");
//return;
//方式2:
throw new Exception("压栈失败,栈已满!");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public Object pop() throws Exception {
if (index < 0) {
//方式1:
//System.out.println("弹栈失败,栈已空!");
//return;
//方式2:
throw new Exception("弹栈失败,栈已空!");
}
// 程序能够执行到此处说明栈没有空。
Object obj = elements[index];
System.out.print("弹栈" + obj + "元素成功,");
elements[index] = null;
// 栈帧向下移动一位。
index--;
return obj;
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}