Stack Queue
栈(Stack)
概念
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
- 出栈:栈的删除操作叫做出栈。出数据在栈顶
栈的使用
- Stack() 构造一个空的栈
- E push(E e) 将e入栈,并返回e
- E pop() 将栈顶元素出栈并返回
- E peek() 获取栈顶元素
- int size() 获取栈中有效元素个数
- boolean empty() 检测栈是否为空
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()); // 获取栈中有效元素个数---> 4System.out.println(s.peek()); // 获取栈顶元素---> 4s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3if(s.empty()){System.out.println("栈空");}else{System.out.println(s.size());}
}
栈的模拟实现
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的
//模拟实现栈
public class MyStack {
//使用数组模拟栈int[] array;int size;
//初识容量为3public MyStack() {array = new int[3];}
//入栈 插入末尾元素,同时计数器+1public int push(int e) {ensureCapacity();array[size++] = e;}
//弹出栈顶元素并且计数器-1,因为要删除栈顶元素public int pop() {return array[--size];}
//弹出栈顶元素不删除public int peek() {return array[size - 1];}
//获得当前栈中元素的数量public int size() {return size;}
//判断栈是否为空public boolean empty() {return size == 0;}
//如果满了就扩容private void ensureCapacity() {if (size == array.length) {Arrays.copyOf(array, array.length * 2);}}
}
栈、虚拟机栈、栈帧有什么区别呢?
通过AI可知:
栈(Stack)
- 栈是一种遵循后进先出(LIFO,Last In First Out)原则的数据结构。
- 在编程中,栈通常用于存储临时变量、函数调用的上下文信息等。
- 栈的典型操作包括“压栈”(push)和“弹栈”(pop)
虚拟机栈(VM Stack):
- 虚拟机栈是虚拟机(如Java虚拟机)中的一个概念,用于存储局部变量和部分结果,并在方法调用时,存储执行的上下文。
- 每个线程在创建时都会创建一个虚拟机栈,其内部包含了多个栈帧(Stack Frame)。
- 虚拟机栈只与方法的调用有关,每个方法从调用到执行完成的过程,都对应着虚拟机栈上的一个栈帧的入栈到出栈。
栈帧(Stack Frame)
- 栈帧是虚拟机栈中的一个单元,用于存储方法的局部变量、操作数栈、动态链接、方法返回地址和一些额外的栈空间。
- 每个方法调用都会创建一个新的栈帧,并将这个栈帧压入虚拟机栈顶。
- 当方法执行完成并返回时,其对应的栈帧会从虚拟机栈中弹出。
区别:
- 作用范围:栈是一个通用的数据结构概念,可以用于多种不同的场合;虚拟机栈是特定于虚拟机的概念,用于方法调用和执行;栈帧是虚拟机栈中的一个单元,用于存储单个方法调用的信息。
- 生命周期:栈的生命周期通常与程序的生命周期相同;虚拟机栈的生命周期与线程的生命周期相同;栈帧的生命周期与方法的调用周期相同。
- 存储内容:栈可以存储任何类型的数据;虚拟机栈专门用于存储栈帧;栈帧存储方法的局部变量、操作数栈等。
- 在程序执行过程中,当一个方法被调用时,会创建一个新的栈帧,并将必要的信息(如参数、局部变量等)放入这个栈帧中,然后将栈帧压入虚拟机栈。当方法执行结束时,栈帧会从虚拟机栈中弹出,释放相应的资源。这个过程在程序运行期间不断重复,支持了程序的执行和内存管理。
队列(Queue)
概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
- 入队列:进行插入操作的一端称为队尾(Tail/Rear)
- 出队列:进行删除操作的一端称为队头 (Head/Front)
- 队列的使用 在Java中,Queue是个接口,底层是通过链表实现的
- boolean offer(E e) 入队列
- E poll() 出队列
- peek() 获取队头元素
- int size() 获取队列中有效元素个数
- boolean isEmpty() 检测队列是否为空
注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口
public static void main(String[] args) {Queue<Integer> q = new LinkedList<>();q.offer(1);q.offer(2);q.offer(3);q.offer(4);q.offer(5); // 从队尾入队列System.out.println(q.size());System.out.println(q.peek()); // 获取队头元素q.poll();System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回if (q.isEmpty()) {System.out.println("队列空");} else {System.out.println(q.size());}
}
队列模拟实现
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,通过前面线性表的学习了解到常见的空间类型有两种:顺序结构和链式结构,队列的实现使用顺序结构还是链式结构好?
顺序结构(数组实现)
优点:
- 内存连续:元素在内存中存储是连续的,这有利于CPU缓存的优化,可以提高访问效率。
- 随机访问:可以快速地通过索引访问队列中的任意元素。
- 简单实现:使用数组实现队列相对简单,易于理解和实现。
缺点:
- 固定大小:数组的大小通常是固定的,这意味着队列的大小有限制。虽然可以通过数组扩容来解决,但这涉及到内存重新分配和数据迁移,有一定的开销。
- 入队和出队操作可能涉及数据移动:在数组的前端进行出队操作时,需要将所有元素向前移动一位,这在元素较多时效率较低。
链式结构(链表实现)
优点:
- 动态大小:链表的大小是动态的,可以根据需要进行扩展,没有固定的大小限制。
- 出队和入队效率高:在链表的头部进行出队操作和在尾部进行入队操作都是常数时间复杂度(O(1)),不需要移动其他元素。
- 空间利用率高:链表中的每个节点只存储必要的数据和指向下一个节点的指针,没有数组中未使用空间的浪费。
缺点:
- 内存非连续:链表的元素在内存中不是连续存储的,这可能导致CPU缓存命中率降低,影响访问效率。
- 不支持随机访问:链表不支持快速随机访问,访问任意元素需要从头开始遍历,时间复杂度为O(n)实现复杂度较高:相比于数组,链表的实现更为复杂,需要额外处理指针和节点的创建与销毁。
综上,使用链式结构更有效率一些,不然进行插入删除的时候需要移动整个元素列表
public class Queue {// 双向链表节点public static class ListNode {ListNode next;ListNode prev;int value;ListNode(int value) {this.value = value;}}ListNode first; // 队头ListNode last; // 队尾int size = 0;// 入队列---向双向链表位置插入新节点public void offer(int e) {if (size == 0) {first = last = new ListNode(e);}ListNode newNode = new ListNode(e);last.prev.next = newNode;last = newNode;}// 出队列---将双向链表第一个节点删除掉public int poll() {if (size == 0) {return -1;}if (size == 1) {first = last = null;return 0;}first = first.next;first.prev = null;size--;return first.value;}// 获取队头元素---获取链表中第一个节点的值域public int peek() {if (size == 0) {return -1;}return first.value;}public int size() {return size;}public boolean isEmpty() {return size == 0;}
}
循环队列
实际中我们有时还会使用一种队列叫循环队列,如操作系统讲解生产者消费者模型时可以就会使用循环队列。 环形队列通常使用数组实现
数组下标循环的小技巧
下标最后再往后(offset 小于 array.length): index = (index + offset) % array.length
下标最前再往前(offset 小于 array.length): index = (index + array.length - offset) % array.length
如何区分空与满
- 通过添加 size 属性记录
- 保留一个位置
- 使用标记
双端队列 (Deque)
双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。 那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
- Deque是一个接口,使用时必须创建LinkedList的对象
- 在实际工程中,使用Deque接口是比较多的,栈和队列均可以使用该接口。
Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现