Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
🌱🌱个人主页:奋斗的明志
🌱🌱所属专栏:数据结构
📚本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。
文章目录
- 前言
- 一、栈(Stack)
- 1.概念
- 2.栈在现实生活中的例子
- 二、栈的使用
- 1.方法
- 2.代码
- 三、栈的模拟实现
- 1.入栈图解
- 2.出栈图解
- 3.数组实现的栈
- 4.链表实现的栈
- 5.push(链表实现)
- 6.pop(链表实现)
- 四、栈的应用场景
- 1.改变元素的序列
- 2.将递归转化为循环
- 2.1 递归方式
- 2.2 循环方式
- 五、了解中缀表达式、后缀表达式
- 总结
前言
一、栈(Stack)
1.概念
栈
:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作
。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO( Last In First Out)的原则。压栈
:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
。出栈
:栈的删除操作叫做出栈。出数据在栈顶
。栈顶栈底
: 这个描述是偏向于逻辑上的内容,因为大家知道数组
在末尾插入删除更容易,而单链表
通常在头插入删除更容易。所以数组可以用末尾做栈顶,而链表可以头做栈顶
。
2.栈在现实生活中的例子
栈的应用广泛,比如你的程序执行查看调用堆栈、计算机四则加减运算、算法的非递归形式、括号匹配问题等等。所以栈也是必须掌握的一门数据结构。最简单大家都经历过,你拿一本书上下叠在一起,就是一个后进先出的过程,你可以把它看成一个栈。下面我们介绍数组实现的栈
和链表实现的栈
。
二、栈的使用
1.方法
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
E push(E e) | 将e入栈,并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
2.代码
代码如下(示例):
public static void main(String[] args) {Stack<Integer> s = new Stack();s.push(1);s.push(2);s.push(3);s.push(4);System.out.println(s.size()); // 获取栈中有效元素个数---> 4 System.out.println(s.peek()); // 获取栈顶元素---> 4s.pop(); // 4出栈 ,栈中剩余1 2 3 ,栈顶元素为3System.out.println(s.pop()); // 3出栈 ,栈中剩余1 2 栈顶元素为3 if(s.empty()){if (s.empty()) {System.out.println("栈空");} else {System.out.println(s.size());}
}
三、栈的模拟实现
从上图中可以看到, Stack
继承了Vector
,Vector
和ArrayList
类似,都是动态的顺序表
,不同的是Vector是线程安全的。
1.入栈图解
2.出栈图解
3.数组实现的栈
代码如下(示例):
package stackdemo;import java.util.Arrays;public class MyStack {//用什么来组织呢?// 数组、链表// 目前先用数组//先创建数组public int[] elem;public int usedSize;//表示有效个数,也可以当下标使用public static final int DEFAULT_CAPACITY = 10;public MyStack() {//初始化数组容量this.elem = new int[DEFAULT_CAPACITY];}//压栈 入栈public void push(int val){if (isFull()){//扩容this.elem = Arrays.copyOf(elem, 2 * elem.length);}elem[usedSize++] = val;}/*** 判断数组是否满了*/public boolean isFull(){return usedSize == this.elem.length;}/*** 出栈* @return*/public int pop(){if (isEmpty()){throw new EmptyStackException("栈为空");}usedSize--;return elem[usedSize];}public boolean isEmpty(){return usedSize == 0;}public int peek(){if (isEmpty()){throw new EmptyStackException("栈为空");}return elem[usedSize - 1];}
}
测试类
import stackdemo.MyStack;import java.util.LinkedList;
import java.util.Stack;public class Test {public static void main(String[] args) {LinkedList<Integer> stack = new LinkedList<>();stack.push(1);stack.push(2);stack.push(3);System.out.println(stack.pop());System.out.println(stack.peek());}public static void main01(String[] args) {
// Stack<Integer> stack = new Stack<>();MyStack stack = new MyStack();//向栈里面添加元素stack.push(12);stack.push(23);stack.push(34);stack.push(45);//先进后出//出栈,有两个方法// pop 弹出,有一个返回值 直接从栈里面删除元素int ret = stack.pop();System.out.println(ret);//45// peek 也有返回值// peek 只是获取栈顶元素 ,不删除// 元素还在栈里面int peek = stack.peek();System.out.println(peek);// 判断栈空不空System.out.println(stack.isEmpty());}
}
4.链表实现的栈
像数组那样在尾部插入删除。大家都知道链表效率低在查询,而查询到尾部效率很低,就算用了尾指针,可以解决尾部插入效率,但是依然无法解决删除效率(删除需要找到前驱节点),还需要双向链表。前面虽然详细介绍过双向链表,但是这样未免太复杂!
所以我们先采用带头节点的单链表在头部插入删除,把头当成栈顶,插入直接在头节点后插入,删除也直接删除头节点后第一个节点即可,这样就可以完美的满足栈的需求。
代码如下(示例):
package stackdemo;public class lisStack<T> {static class Node<T> {T data;Node next;public Node() {}public Node(T value) {this.data = value;}}int length;Node<T> head;//头节点public lisStack() {head = new Node<>();length = 0;}boolean isEmpty() {return head.next == null;}int length() {return length;}public void push(T value) {//近栈Node<T> team = new Node<T>(value);if (length == 0) {head.next = team;} else {team.next = head.next;head.next = team;}length++;}public T peek() throws Exception {if (length == 0) {throw new Exception("链表为空");} else {//删除return (T) head.next.data;}}public T pop() throws Exception {//出栈if (length == 0) {throw new Exception("链表为空");} else {//删除T value = (T) head.next.data;head.next = head.next.next;//va.nextlength--;return value;}}public String toString() {if (length == 0) {return "";} else {String va = "";Node team = head.next;while (team != null) {va += team.data + " ";team = team.next;}return va;}}
}
5.push(链表实现)
push插入
与单链表头插入一致,如果不太了解可以看看前面写的线性表有具体讲解过程。
和数组形成的栈有个区别,链式实现的栈理论上栈没有大小限制(不突破内存系统限制),不需要考虑是否越界,而数组则需要考虑容量问题。
- 如果一个节点team入栈:
- 空链表入栈head.next=team;
- 非空入栈team.next=head.next;head.next=team;
6.pop(链表实现)
pop弹出
与单链表头删除一致,如果不太了解请先看前面单链表介绍的。
和数组同样需要判断栈是否为空,如果节点team出栈:head指向team后驱节点。
四、栈的应用场景
1.改变元素的序列
-
若进栈序列为 1,2,3,4 ,进栈过程中可以出栈 ,则下列不可能的一个出栈序列是 ()
A: 1,4,3,2
B: 2,3,4,1
C: 3,1,4,2
D: 3,4,2,1 -
一个栈的初始状态为空。现将元素1、2、3、4、5、A、 B、C、 D、 E依次入栈 ,然后再依次出栈 ,则元素出栈的顺 序是( )。
A: 12345ABCDE
B: EDCBA54321
C: ABCDE12345
D: 54321EDCBA
2.将递归转化为循环
2.1 递归方式
思路解析:
- 如果 head 不为 null,递归调用 printList(head.next) 先递归到链表的末尾。
- 当递归回溯时,打印当前节点 head 的值。
工作原理:
- 当 printList(head.next) 运行到链表末尾时,开始逐层回溯。
- 每次回溯时,会依次打印每个节点的值,实现了链表的逆序输出。
// 递归方式
void printList(Node head) {if (null != head) {printList(head.next);System.out.print(head.val + " ");}
}
2.2 循环方式
思路解析:
- 如果 head 为 null,直接返回。
- 使用一个栈 s 来存储链表中的节点。
- 遍历链表,将每个节点依次压入栈中。
- 最后,依次弹出栈中的节点并打印其值,实现了链表的逆序输出。
工作原理:
- 遍历链表的过程中,将节点依次压入栈中,因为栈的特性是后进先出(LIFO)。
- 当遍历完成后,栈中的节点顺序是链表的逆序。
- 依次弹出栈中的节点并打印,即可实现链表元素值的逆序输出。
// 循环方式
void printList(Node head) {if (null == head) {return;}Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中Node cur = head;while (null != cur) {s.push(cur);cur = cur.next;}// 将栈中的元素出栈while (!s.empty()) {System.out.print(s.pop().val + " ");}
}
五、了解中缀表达式、后缀表达式
-
下面以 a + b * c + ( d * e + f ) * g 为例子
-
讲下应该怎么把中缀表达式转换成后缀表达式。
-
按先加减后乘除的原则给表达式加括号
-
结果:((a+(bc))+(((de)+f)*g))
-
由内到外把每个括号里的表达式换成后缀
-
最终结果:a b c * + d e * f + g * +
-
这样就得到了中缀表达式转后缀表达式的最终结果。
-
此法应付考试有神效。
总结
LinkedKist 就可以当做栈来使用
- 递归方式:简单、优雅,但可能会面临栈溢出的风险,特别是在链表非常长的情况下。
- 循环方式:使用了额外的栈来辅助逆序输出,空间复杂度略高,但是可以避免递归深度过深导致的栈溢出问题。