数据结构|Java栈和队列的实现

1. 栈 1.1 概念 一种特殊的线性表,说明了具有前驱和后继关系。只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵循后进先出 LIFO 原则【Last In First Out】
压栈:栈的插入/进栈/压栈,入数据在栈顶
出栈:栈的删除操作叫做出栈,出数据也在栈顶
后进先出【类似于浏览器的回退,只会返回上一个浏览后的页面而不是返回第一个打开的页面;弹夹中的子弹】
数据结构|Java栈和队列的实现
文章图片

邪恶的小技巧:合理的出栈顺序(画栈图)
已知一个栈的入栈序列是m, n, x, y, z.则不可能出现的出栈顺序是: C
A mnxyz
B xnyzm
C nymxz
D nmyzx
A
数据结构|Java栈和队列的实现
文章图片

B
数据结构|Java栈和队列的实现
文章图片

C
数据结构|Java栈和队列的实现
文章图片

m要想出栈必须让x出栈才行,所以是不可能的出栈顺序
D
数据结构|Java栈和队列的实现
文章图片

邪恶的小技巧:中缀表达式转后缀表达式
有一个中缀表达式为a*(b-(c+d)),它的后缀表达式可以是什么: A
A abcd+-*
B abc+d-*
C ab-cd+*
D abd+c-*
前缀表达式: 把运算符放前边
中缀表达式:a*(b-(c+d)
后缀表达式: 把运算符放后边
按照运算优先级给操作符号放在括号外边
a*(b-(c+d))确定优先级
(a*(b-(c+d))) 打括号
+ 先算
(a*(b- ( cd )+ ))移动符号
(a* ( b(cd)+ )- )
( a(b(cd)+)- )*
后缀表达式: abcd+-*
这样就不用像第一题那样每次都画操作栈
方法总结:从左往右,根据运算符的优先级(注意括号问题),额外加括号,把每个对应的运算符移到对应的括号外面。去除所有的括号,结果就是对应的前后缀表达式【后缀放后边,前缀放前边即可】
2*3(4+5)-6
(2*3(4+5)-6)
(2*3(45)+-6)
(2*(345)+*-6)
(2((345)+*)-6)
((2((345)+*)
)-6)
((2((345)+*)
)*6)-
2345 +**6-
数据结构|Java栈和队列的实现
文章图片

应用
int func(后缀表达式)
1.2 实现
public static void main(String[] args) {Stack stack = new Stack<>(); stack.push(1); // 增加元素 stack.push(2); stack.push(3); System.out.println(stack.peek()); // 获取栈顶元素但不删除 3 System.out.println(stack.pop()); // 弹出栈顶元素 删除3 System.out.println(stack.peek()); // 获取栈顶元素但不删除2 System.out.println(stack.empty()); // 栈自带的 empty 方法 System.out.println(stack.isEmpty()); // 继承 vector 的方法 System.out.println(stack.search(2)); // 查找下标 System.out.println(stack.search(3)); // 没有就返回 -1 }3 3 2 false false 1 -1

Stack 源码
数据结构|Java栈和队列的实现
文章图片

【数据结构|Java栈和队列的实现】发现只有 push, peek, pop, empty, search 方法
为何可以使用 isEmpty 呢?
为何可以使用 isEmpty 方法呢。
数据结构|Java栈和队列的实现
文章图片

数据结构|Java栈和队列的实现
文章图片

因为 Stack 继承 Vector,所以可以使用父类的 isEmpty方法
分析一下 push 压栈操作,来查看它的底层是如何实现的
数据结构|Java栈和队列的实现
文章图片

查看 addElementr 方法
数据结构|Java栈和队列的实现
文章图片

如果不清楚源代码内容的可以查看我的博客——List的使用中 add 方法一步步源码解析,了解栈的底层数组如何扩容如何增加元素
我们查看一下 Stack 继承和实现
数据结构|Java栈和队列的实现
文章图片

发现这个体系是:Stack 类继承 Veator 类, Vector 类继承 AbstractList 类, AbstractList 类继承自 AbstractCollection 类, AbstractCollection 类实现 Collection 接口, Collection 接口继承 Iterable 接口
1.2.1 数组实现Stack
import java.util.Arrays; public class MyStackList {//public static void main1(String[] args) {//Stack stack = new Stack<>(); //stack.push(1); // 增加元素 //stack.push(2); //stack.push(3); //System.out.println(stack.peek()); // 获取栈顶元素但不删除 3 //System.out.println(stack.pop()); // 弹出栈顶元素 删除3 //System.out.println(stack.peek()); // 获取栈顶元素但不删除2 //System.out.println(stack.empty()); // 栈自带的 empty 方法 //System.out.println(stack.isEmpty()); // 继承 vector 的方法 //System.out.println(stack.search(2)); // 查找下标 //System.out.println(stack.search(3)); // 没有就返回 -1 //}private E[] elem; private int usedSize = 0; private int capacity = 4; MyStackList() {this.elem = (E[]) new Object[capacity]; }// 扩容 private void checkCapacity() {if (this.usedSize == this.capacity) {this.capacity *= 2; this.elem = Arrays.copyOf(this.elem, this.capacity); } }E push(E e) {checkCapacity(); this.elem[this.usedSize++] = e; return this.elem[this.usedSize - 1]; }E pop() {if (this.usedSize == 0) {throw new ArrayIndexOutOfBoundsException("Stack is empty !!!"); } else {return this.elem[--this.usedSize]; } }E peek() {if (this.usedSize == 0) {return null; } else {return this.elem[this.usedSize - 1]; } }boolean empty() {return this.usedSize == 0; }void display() {for (int i = this.usedSize-1; i >= 0; i--) {System.out.print(this.elem[i] + " "); } System.out.println(); }private static void testPush() {MyStackList stack = new MyStackList(); for (int i = 0; i < 10; i++) {System.out.print(stack.push(i) + " "); } }private static void testPop() {System.out.println("Push:"); MyStackList stack = new MyStackList(); for (int i = 0; i < 10; i++) {stack.push(i); } stack.display(); System.out.println("Pop:"); for (int i = 0; i < 15; i++) {System.out.print(stack.pop() + " "); }System.out.println("Residue: "); stack.display(); }private static void testPeek() {MyStackList stack = new MyStackList(); System.out.println("Push:"); for (int i = 0; i < 10; i++) {stack.push(i); } stack.display(); System.out.println("Peek:"); for (int i = 5; i < 10; i++) {System.out.print(stack.peek() + " "); }System.out.println("Residue:"); stack.display(); }public static void main(String[] args) {// 放置测试用例 testPop(); } }

数组实现栈, 为了提高效率该如何插入删除数据呢?
  1. 尾插尾删: 避免了数组元素的移动, 时间复杂度O(1)
  2. 头插头删: 还要移动数据, 时间复杂度O(N)
1.2.2 单链表实现Stack
class Node {private E val; private Node next; public Node(E val) {this.val = val; }Node head; E push(E val) {Node node = new Node(val); if (head == null) {head = node; return (E) head.val; } else {node.next = head; head = node; return (E) head.val; } }E pop() {if (head == null) {throw new NullPointerException("NULL !!!"); } else {E ret = (E) head.val; head = head.next; return ret; } }E peek() {if (head == null) {throw new NullPointerException("NULL !!!"); } else {return (E) head.val; } }void display() {Node cur = this.head; while (cur != null) {System.out.print(cur.val + " "); cur = cur.next; } } }public class MyStackLinked {private static void testPush() {Node node = new Node(null); for (int i = 0; i < 8; i++) {System.out.print(node.push(i) + " "); } }private static void testPop() {Node node = new Node(null); for (int i = 0; i < 8; i++) {node.push(i); } for (int i = 0; i < 5; i++) {System.out.print(node.pop() + " "); } }private static void testPeek() {Node node = new Node(null); for (int i = 0; i < 8; i++) {node.push(i); } for (int i = 0; i < 5; i++) {System.out.print(node.peek() + " "); } }public static void main(String[] args) {testPeek(); } }

单链表实现栈, 为了提高效率该如何插入删除数据呢?
  1. 头插头删: 时间复杂度O(1)
  2. 尾插尾删: 不带尾节点单链表时间是复杂度O(N);带尾节点尾结点 tail 可以把入栈push操作的时间复杂度降到O(1)但是无法把出栈pop时间复杂度降到O(1)【需要找尾节点的前一个节点,所以是O(N)】
2. 队列 2.1 概念 只允许一端进行数据插入操作,另一端进行数据删除操作的特殊线性表.队列具有先进先出功能FIFO(First In First Out)
入队列: 进行插入操作的一端称为队尾
出队列: 进行删除操作的一端称为对头
数据结构|Java栈和队列的实现
文章图片

先进先出【操作系统的程序点开执行顺序,客服电话,键盘的输入,记事本的输出】
2.2 实现 数据结构|Java栈和队列的实现
文章图片

发现 Queue 只有方框中的方法
Queue 接口继承了 Collection 接口, Collection 接口继承 Iterable 接口【实现过程比 Stack 简单许多】
数组实现Queue
数组实现队列有没有什么问题呢?
因为是先进先出的, 所以会导致先出空间的内容造成浪费。而不是像栈那样一直处于一端操作,不会造成空间浪费。如果元素移动的话来决解空间问题的话,时间复杂度会变为O(N)效率太低
数据结构|Java栈和队列的实现
文章图片

因此数组的话可以通过“循环”的方式实现队列
什么是“循环队列”呢?
数据结构|Java栈和队列的实现
文章图片

分析解释环形图
  1. 0~7: 数组的引用
  2. 11, 12, 13, 14, 15, 16, 17存储的元素值
  3. rear:指向插入元素的引用【带插入元素的下一个索引】,图中代表的是刚插入元素17还未移动rear下标,此时插入元素操作完毕后的rear应该移动到下一个索引0处
  4. front:指向对头元素的索引,每次出队列一个元素就移动一次front
思考: 何时代表队列满了, 何时代表队列空?
  1. front==rear 的时候,有两种可能: 要么队列满要么队列空
  2. 如果 rear 的下一个是 front 就代表满了【现在可以区front==rear】的两种情况了
思考: rear走到数组末尾,该如何跳转到数组0下标开头呢?
最简单的方法就是 if 判断
if (re a r== elem.length-1){ // 走到了队尾 rear = 0; }

这时候如果利用取余运算呢? 会发现有不同的运算效果
假设一边有插入一边有删除,那么这个时候即可实现循环操作对立达到堆空间的利用的同时复杂度还是O(1)
队列长度为8, rear+1 取余结果
1%8 1
2%8 2
3%8 3
4%8 0
5%8 5
6%8 8
7%8 8
8%8 0
9%8: 不会出现这种情况,rear+1==front的话就已经是满队列 1
数据结构|Java栈和队列的实现
文章图片

数据结构|Java栈和队列的实现
文章图片

2.2.1 数组实现循环队列
这是一个 OJ 链接
方案1会浪费一个位置的队列空间,代码如下:
class MyCircularQueue {private int[] elem; private int front, rear; public MyCircularQueue(int k) {this.elem = new int[k+1]; }/** * 入队列 * * @param value 值 * @return */ public boolean enQueue(int value) {if ((this.rear + 1) % this.elem.length == this.front) {return false; //满了 } else {this.elem[this.rear] = value; this.rear = (this.rear + 1) % this.elem.length; return true; } }/** * 出队列 * * @return */ public boolean deQueue() {if (this.front == this.rear) {return false; } else {int val = this.elem[this.front]; //this.front = (this.front + 1) % this.elem.length; // 因为是循环,所以不能直接 front+1 return true; } }/** * 取队头元素【相当于 peek 】 * * @return */ public int Front() {if (this.front == this.rear) {return -1; // leetcode 抛异常会认为是你的代码错误,所以不能抛异常 } else {int val = this.elem[this.front]; return val; } }/** * 取队尾元素 * rear处于0位置则返回this.elem.length-1 * 所以不能直接进行 -1 操作 * * @return */ public int Rear() {if (this.rear == this.front) {return -1; } else {if (this.rear == 0) {return this.elem[this.elem.length - 1]; } else {return this.elem[this.rear - 1]; } } }public boolean isEmpty() {return this.front == this.rear; }public boolean isFull() {return this.rear + 1 == this.front; } }

方案2,通过一个额外的变量来记录队列的状态【并非 OJ 题目代码,只是提供一个思路,把最后的队列空间利用完】
public class my_Queue_list {E[] elem = (E[]) new Object[5]; // 队列有效区间[head, tail) private int head = 0; private int tail = 0; private int size = 0; // 1.入队列 boolean offer(E val) {if (size == elem.length) {return false; } else {elem[tail++] = val; tail = tail % elem.length; ++size; return true; } }// 2.出队列 E poll() {if (size == 0) {return null; } else {E ret = (E) elem[head++]; head = head % elem.length; --size; return ret; } }// 3.取队首元素 E peek() {if (size == 0) {return null; } else {return (E) elem[head]; } }// 4.打印 void display() {for (int i = 0; i < size; i++) {System.out.print(elem[i] + " "); } }private static void test_offer() {my_Queue_list my_queue_list = new my_Queue_list(); for (int i = 0; i < 5; i++) {my_queue_list.offer(Integer.valueOf(i)); } my_queue_list.display(); }private static void test_poll() {my_Queue_list my_queue_list = new my_Queue_list(); for (int i = 0; i < 5; i++) {my_queue_list.offer(Integer.valueOf(i)); } for (int i = 0; i < 5; i++) {System.out.print(my_queue_list.poll() + " "); } }private static void test_peek() {my_Queue_list my_queue_list = new my_Queue_list(); for (int i = 0; i < 5; i++) {my_queue_list.offer(Integer.valueOf(i)); } System.out.println(my_queue_list.peek()); }public static void main(String[] args) {// 放置测试用例 test_peek(); } }

2.2.2 单链表实现Queue
class Node{private E val; private Node next; private int usedSize; public Node(E val) {this.val = val; this.usedSize = 0; } Node front; Node rear; /** * 入队列 * @param val 值 * @return */ E offer(E val){Node node = new Node(val); if (this.front == null){this.front = node; this.rear = node; }else{this.rear.next = node; this.rear = node; } ++this.usedSize; return (E) this.rear.val; }/** * 出队列 * @return */ E poll(){if (this.front == null){throw new NullPointerException("Queue is empty !!!"); }else{E ret = (E) this.front.val; if (this.front.next == null){this.front = null; this.rear = null; }else{this.front = this.front.next; } --this.usedSize; return ret; } }/** * 出对头元素但不删除 * @return */ E peek(){if (this.front == null){throw new NullPointerException("Queue is empty !!!"); }else{E ret = (E) this.front.val; return ret; } }/** * 为空 * @return */ boolean isEmpty(){return this.usedSize == 0; }/** * 队列长度 * @return */ int size(){return this.usedSize; } } public class MyQueueLinked {private static void tstOffer(){Node node = new Node(null); for (int i = 0; i < 8; i++) {System.out.print(node.offer(i) + " "); } }private static void testPoll(){Node node = new Node(null); for (int i = 0; i < 8; i++) {node.offer(i); } for (int i = 0; i < 9; i++) {System.out.print(node.poll()+" "); } }private static void testPeek(){Node node = new Node(null); for (int i = 0; i < 8; i++) {node.offer(i); } for (int i = 0; i < 9; i++) {System.out.print(node.peek()+" "); } } public static void main(String[] args) {// 放置测试用例 testPeek(); } }

3. Stack 和 Queue 常用 API 总结 Stack
方法 解释
E push(E item) 压栈并返回压入的元素
E pop() 弹出栈顶元素并返回弹出的元素
E peek() 查看顶元素且不弹出,保留在栈中
boolean empty() 栈是否为空
Queue
抛出异常 返回值
入队列 boolean add(e) boolean offer(e)
出队列 E remove() E poll()
队首元素 E element() E peek()
4. 结尾语 两栈共享空间这种操作我还不会,以后会学会了会继续更新栈内容
栈和队列说简单也简单,说难也有一丝的难度。
下面是我的感悟:
人生,就像是一个很大的栈演变。
自下而上的增长,不仅仅是自然万物,也是我们的成长经历过程,
从出生时我们赤裸裸的来到这个世间,渐渐地长大,知道我们变老,经历的周围人的生老病死,最终我们还的赤裸裸地衣锦还“乡”。
人生,又放佛是一天一天小小的栈重现。
呀呀学语的时候,父母每天为抱着我们不断的进出家门,青少年的时候有叛逆,青年的时候高中和大学的接轨,是否和父母一样保持着童年时期的联系,壮年时候将会奔波于家和事业之间,老年了,我想我会小园香径独徘徊于养老院门里屋前。
人生,更需要有栈的精神体现。
在哪里跌倒就在哪里爬起,无论是否见到栈底,我们始终应该保持栈定的仰望天空。只要有希望,不断进取,出头之日将会重现。困难不会永远存在,强者才会勇往直前。
人生,其实就是一个大大的队列演变。
无知童年,快乐少年,稚嫩青年,成熟中年,安逸晚年。
人生,也是一个又一个小小的队列重现。
春夏秋冬轮回年年,早中晚夜循环天天。变化的是时间,不变的是对未来执着的信念。
人生,更需要有队列精神的体现。
南极到北极,不过时南纬 90 度到北纬 90 度的队列,如果中途犹豫,临时转向,也许只能与企鹅或北极熊相伴。可事实是无论坚持哪个方向,你都会到达终点。
大部分人都是普通人,学习数据结构,我很笨,想着宏伟的计划,却发现别人学一遍的我需要两遍才能弄明白。队列精神对于普通人来说,已经很宝贵,聪明人可以转向,绕过许多沟壑以最快的速度到达终点,但是普通人只能脚踏实地的一步一个脚印走完该走的路。

    推荐阅读