数据结构-----栈队列
目录
栈
什么是栈?
时间复杂度
栈的常见操作
入栈
出栈
栈的常见应用场景
浏览器的回退和前进
虚拟机栈
出栈顺序
栈例题
检查符号是否成对出现
反转字符串
队列
什么是队列?
时间复杂度
单队列的常见操作
入队
出队
“假溢出”
循环队列
队列应用场景
KTV点歌列表
阻塞队列
线程池的任务队列
队列例题
使用队列实现栈
使用栈实现队列
栈
什么是栈?
栈 (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。
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();
}
反转字符串
思路:将字符串中的每个字符先全部入栈,然后再逐个出栈。
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)% 数组长度
/*** 循环队列*/
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)。
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类,使用两个栈来模拟队列的实现,对外提供三个接口< 入队列,出队列,判空 >。
/** 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;}
}
测试:
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()); // 1System.out.println(myQueue.poll()); // 2System.out.println(myQueue.poll()); // 3// 入队myQueue.offer(6);myQueue.offer(7);myQueue.offer(8);// 出队System.out.println(myQueue.poll()); // 4System.out.println(myQueue.poll()); // 5System.out.println(myQueue.poll()); // 6
}