目录
栈
什么是栈?
栈 (stack)是一种特殊的线性数据集合,只允许在栈顶 top进行加入数据(push)和移除数据(pop),按照 后进先出LIFO(Last In First Out) 的规则进行操作,也可以理解为先入后出FILO(First In Last Out);

栈的实现方式
栈的实现结构可以是一维数组 或链表来实现,用数组实现的栈叫作顺序栈 ,用链表实现的栈叫作链式栈 。在Java中,顺序栈使用java.util.Stack类实现,链式栈使用java.util.LinkedList类实现。


时间复杂度
假设栈中有n个元素,常见操作时间复杂度:
- 访问指定位置的元素,时间复杂度为O(n):因为最坏情况下,访问的元素在栈底,需要遍历所有元素。
- 入栈和出栈的时间复杂度为 O(1):因为只涉及栈顶top。
栈的常见操作
入栈
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素将会成为新的栈顶。

出栈
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。

栈的常见应用场景
浏览器的回退和前进
需要使用两个栈(Stack1 和 Stack2)和就能实现这个功能。比如:你按顺序查看了 1,2,3,4 这四个页面,我们依次把 1,2,3,4 这四个页面压入 Stack1中。当你想回头看 2 这个页面的时候,你点击2次回退按钮,我们依次把 4,3 这两个页面从 Stack1 弹出,然后压入 Stack2 中。假如你又想回到页面 3,你点击1次前进按钮,我们将 3 页面从 Stack2 弹出,然后压入到 Stack1 中。

虚拟机栈
每个线程拥有一块独立的内存空间,这块内存空间被设计成"栈"这种结构,被称为"虚拟机栈"。
JVM 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次方法调用都会有一个对应的栈帧被压入 VM Stack虚拟机栈,每一个方法调用结束后,代表该方法的栈帧会从VM Stack虚拟机栈中弹出。

出栈顺序
3个元素A,B,C顺序进栈,出栈情况分析如下:

4个元素A,B,C,D顺序进栈,出栈情况分析如下:

栈例题
检查符号是否成对出现
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断该字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
比如: "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]" 、"([)]" 则不是
思路:
- 首先我们将括号间的对应规则存放在 Map 中;
- 创建一个栈。遍历字符串,如果字符是左括号就直接加入stack中,否则将stack 的栈顶元素与这个括号做比较,如果不相等就直接返回 false。遍历结束,如果stack为空,返回 true。
java
public boolean isValid(String s){
// 括号之间的对应规则
HashMap<Character, Character> mappings = new HashMap<Character, Character>();
mappings.put(')', '(');
mappings.put('}', '{');
mappings.put(']', '[');
Stack<Character> stack = new Stack<Character>();
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (mappings.containsKey(chars[i])) {
char topElement = stack.empty() ? '#' : stack.pop();
if (topElement != mappings.get(chars[i])) {
return false;
}
} else {
stack.push(chars[i]);
}
}
return stack.isEmpty();
}
反转字符串
思路:将字符串中的每个字符先全部入栈,然后再逐个出栈。
java
public static void main(String[] args) {
String str = "just do it";
Stack<Character> stack = new Stack<>();
char[] chars = str.toCharArray();
for (char c : chars) {
stack.push(c);
}
StringBuilder result = new StringBuilder();
for (int i = 0, len = stack.size(); i < len; i++) {
result.append(stack.pop());
}
System.out.println(result.toString());
}
队列
什么是队列?
- 队列(queue)是一种线性数据结构,特点类似:行驶车辆的单向隧道
- 队列中的元素按照先入先出(First In First Out,简称FIFO)的规则操作
- 队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)
- 队列只允许在队头(front)进行出队 poll操作(删除)
- 队列只允许在队尾(rear)进行入队 offer操作(添加)
- 队列按照实现机制的不同分为:单队列和循环队列

队列的实现方式
- 数组实现的队列叫作顺序队列
- 链表实现的队列叫作链式队列
基于数组实现的顺序队列

基于链表实现的链式队列

时间复杂度
假设队列中有n个元素。
- 访问指定元素的时间复杂度是O(n):最坏情况下,遍历整个队列
- 插入删除元素的时间复杂度是O(1):只需要操作队头或队尾元素
单队列的常见操作
入队
入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素,新元素的下一个位置将会成为新的队尾。

出队
出队(dequeue)就是把元素移出队列,只允许在队头一侧移 出元素,出队元素的后一个元素将会成为新的队头。

"假溢出"
顺序队列存在"假溢出"的问题,也就是明明有位置却不能添加。
下图是一个顺序队列,我们将前两个元素 1,2 出队,并入队两个元素 7,8。当进行入队、出队操作的时候,front 和 rear 都会持续往后移动,当 rear 移动到最后的时候,我们无法再往队列中添加数据,即使数组中还有空余空间,这种现象就是 "假溢出" 。除了假溢出问题之外,如下图所示,当添加元素 8 的时候,rear 指针移动到数组之外(越界)。

循环队列
用数组实现的 队列,可以采用循环队列的方式来维持队列容量的恒定。
例如:
步骤1 : 一个队列经过反复的入队和出队操作,还剩下2个元素,在"物理"上分布于数组的末尾位置。这时又有一个新元素将要入队。

步骤2 : 在数组不做扩容的前提下,我们可以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。

步骤3 : 队尾指针指向数组首位后,整个队列的元素就"循环"起来了。在物理存储上,队尾的位置也可以在队头之前。当再有元素入队时,将其放入数组的首位, 队尾指针继续后移即可。

步骤4 : 直到(队尾下标+1)% 数组长度 = 队头下标,代表此队列真的已经满了。需要注意的是,队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小1。

综上所述,循环队列满足以下条件:
- 队空条件:rear == front
- 队满条件:(rear + 1) % 数组长度 == front
- 计算队列长度:(rear - front + 数组长度) % 数组长度
- 入队:(rear + 1)% 数组长度
- 出队:(front + 1)% 数组长度
java
/**
* 循环队列
*/
public class CircularQueue {
private int[] array; // 基于数组实现
private int front; // 队头
private int rear; // 队尾
public CircularQueue(int capacity) {
this.array = new int[capacity];
}
/**
* 入队
*
* @param element 入队的元素
*/
public void offer(int element) throws Exception {
if ((rear + 1) % array.length == front) {
throw new Exception(" 队列已满!");
}
array[rear] = element;
rear = (rear + 1) % array.length;
}
/**
* 出队
*
*/
public int poll() throws Exception {
if (rear == front) {
throw new Exception(" 队列已空!");
}
int deQueueElement = array[front];
front = (front + 1) % array.length;
return deQueueElement;
}
/**
* 输出队列
*/
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = front; i != rear; i = (i + 1) % array.length) {
sb.append(array[i] + "\t");
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
CircularQueue myQueue = new CircularQueue(6);
myQueue.offer(3);
myQueue.offer(5);
myQueue.offer(6);
myQueue.offer(8);
myQueue.offer(1);
System.out.println(myQueue);
myQueue.poll();
myQueue.poll();
myQueue.poll();
System.out.println(myQueue);
myQueue.offer(2);
myQueue.offer(4);
myQueue.offer(9);
System.out.println(myQueue);
}
}
队列应用场景
KTV点歌列表
使用队列保存已点歌曲列表,每次点歌时,将歌曲放入队尾。播放歌曲时,从队头取出。符合FIFO存取特点。
阻塞队列
阻塞队列可以看成在队列基础上,通过锁实现线程阻塞操作的特殊队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现"生产者 - 消费者"模型。
线程池的任务队列
线程池中没有空闲线程时,新的线程任务请求线程资源时,线程池会将这些线程任务放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。
队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :FixedThreadPool 使用无界队列 LinkedBlockingQueue。但是有界队列就不一样了,当队列已满的话,后面再有线程任务就会判断是否超出最大线程数,如果超出则执行拒绝策略。
队列例题
使用队列实现栈
实现 MyStack类,使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
java
public class MyStack {
Queue<Integer> queue1;
Queue<Integer> queue2;
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()) {
queue2.offer(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
使用栈实现队列
实现Queue类,使用两个栈来模拟队列的实现,对外提供三个接口< 入队列,出队列,判空 >。
java
/*
* Stack栈 LIFO
* Queue队列 FIFO
*
* 使用两个Stack栈实现Queue队列
*/
public class Queue {
// 入队栈
private Stack<Integer> inStack = new Stack<>();
// 出队栈
private Stack<Integer> outStack = new Stack<>();
// 入队
public void offer(int item) {
while(!outStack.empty()) {
inStack.push(outStack.pop());
}
// 新元素入队
inStack.push(item);
}
// 出队
public int poll() {
while(!inStack.empty()) {
outStack.push(inStack.pop());
}
return outStack.pop();
}
// 判断是否为空
public boolean empty() {
return outStack.size() == 0 && inStack.size() == 0;
}
}
测试:
java
public static void main(String[] args) {
Queue myQueue = new Queue();
// 入队
myQueue.offer(1);
myQueue.offer(2);
myQueue.offer(3);
myQueue.offer(4);
myQueue.offer(5);
// 出队
System.out.println(myQueue.poll()); // 1
System.out.println(myQueue.poll()); // 2
System.out.println(myQueue.poll()); // 3
// 入队
myQueue.offer(6);
myQueue.offer(7);
myQueue.offer(8);
// 出队
System.out.println(myQueue.poll()); // 4
System.out.println(myQueue.poll()); // 5
System.out.println(myQueue.poll()); // 6
}