数据结构-----栈&队列

目录

什么是栈?

时间复杂度

栈的常见操作

入栈

出栈

栈的常见应用场景

浏览器的回退和前进

虚拟机栈

出栈顺序

栈例题

检查符号是否成对出现

反转字符串

队列

什么是队列?

时间复杂度

单队列的常见操作

入队

出队

"假溢出"

循环队列

队列应用场景

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顺序进栈,出栈情况分析如下:

栈例题

检查符号是否成对出现

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断该字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。

比如: "()"、"()[]{}"、"{[]}" 都是有效字符串,而 "(]" 、"([)]" 则不是

思路:

  1. 首先我们将括号间的对应规则存放在 Map 中;
  2. 创建一个栈。遍历字符串,如果字符是左括号就直接加入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
}
相关推荐
Gorgous—l3 小时前
数据结构算法学习:LeetCode热题100-矩阵篇(矩阵置零、螺旋矩阵、旋转图像、搜索二维矩阵 II)
数据结构·学习·算法
Mr_Chester3 小时前
mybatis OGNL+优雅处理简单逻辑
java·tomcat·mybatis
道可到4 小时前
阿里面试原题 面试通关笔记05 | 异常、泛型与反射——类型擦除的成本与优化
java·后端·面试
神仙别闹4 小时前
基于Java(Spring Boot)+MySQL实现电商网站
java·spring boot·mysql
瀚高PG实验室4 小时前
HGDB集群(安全版)repmgr手动切换主备库
java·数据库·安全·瀚高数据库
刘新明19895 小时前
Frida辅助分析OLLVM虚假控制流程(下)
java·开发语言·前端
如意猴5 小时前
双向链表----“双轨联动,高效运行” (第九讲)
数据结构·链表
第二只羽毛5 小时前
重载和继承的实践
java·开发语言
王嘉俊9255 小时前
设计模式--适配器模式:优雅解决接口不兼容问题
java·设计模式·适配器模式