在学习栈和队列前,先补充两个概念:物理结构和逻辑结构。
1.什么是数据存储的物理结构呢?
举个易懂的例子,如果把数据结构比作活生生的人,那么物理结构就是人的血肉和骨骼,看得见,摸得着,实实在在。那么,数组和链表就是实实在在的存储结构。
2.什么是数据存储的逻辑结构呢?
就刚刚的例子来说,人体之上,还存在着人的思想和精神,它看不见摸不着。那么,栈和队列以及数和图等数据结构都是属于逻辑结构。虽然栈和队列是属于逻辑结构的,但是他们的物理实现既可以利用数组,也可以利用链表来实现。
3.什么是栈?
栈(stack)是一种线性数据结构,它就像一个放羽毛球的圆筒容器,栈中的元素只能先入后出,最早进入的元素存放的位置叫做栈底(bottom),最后进入的元素存放的位置叫做栈顶(top)。
3.1 栈的基本操作
3.1.1 入栈
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会称为新的栈顶。
3.1.2 出栈
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会称为新的栈顶。
java自己封装的栈:
1 package com.algorithm.test; 2 3 import java.util.Stack; 4 5 /** 6 * @Author Jack丶WeTa 7 * @Date 2020/7/28 12:06 8 * @Description 关于栈的简单操作(入栈和出栈) 9 */ 10 public class StackTest { 11 public static void main(String[] args) { 12 Stack<Integer> stack = new Stack<>(); 13 14 for (int i = 0; i <= 10; i++) { 15 stack.push(i); 16 } 17 18 while (!stack.isEmpty()) { 19 System.out.println("栈:" + stack.toString() + " 栈大小为:" + stack.size() + " 出栈元素为:" + stack.pop()); 20 } 21 } 22 }
我们自己也可以用数组来简单实现一下:
package com.algorithm.test; /** * @Author Jack丶WeTa * @Date 2020/7/28 12:06 * @Description 关于栈的简单操作(入栈和出栈) */ public class MyStackTest { private int[] array; private int top; //栈顶 public MyStackTest(int capacity){ this.array = new int[capacity]; } public void push(int element){ if(top >= array.length){ throw new IndexOutOfBoundsException("满栈了,无法进行入栈操作!"); } array[top] = element; top++; } public int pop() { if(top <= 0){ throw new RuntimeException("栈为空,无法进行出栈操作!"); } int popElement = array[top-1]; top--; return popElement; } public void output(){ for (int i = 0; i < top; i++){ System.out.print(array[i] + ","); } } public static void main(String[] args) { MyStackTest myStackTest = new MyStackTest(6); myStackTest.push(5); myStackTest.push(2); myStackTest.push(4); myStackTest.push(8); myStackTest.push(0); myStackTest.push(9); myStackTest.output(); System.out.println(); System.out.println("-----开始出栈操作-----"); for (int i = 0; i < 6; i++){ System.out.print("出栈第" + (i+1) + "次:"); myStackTest.pop(); myStackTest.output(); System.out.println(); } } }
总结下栈操作的时间复杂度。由于入栈和出栈指挥影响到最后一个元素,不涉及到其他元素的整体移动,所以无论是以数组还是链表实现,入栈和出栈的时间复杂度都是O(1)。
4.什么是队列呢?
队列(queue)是一种线性数据结构,它的特征和行驶车辆的单行隧道很相似。不同于栈的先入后出,队列中的元素只能先入先出。队列的出口端叫做队头(front),队列的入口端叫队尾(rear)。
4.1 队列的基本操作
4.1.1 入队
入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放置元素,新元素的下一个位置将会成为新的队尾。
4.1.2 出队
出队(dequeue)就是把元素移出队列,只允许在对头一侧移出元素,出队元素的后一个元素将会成为新的对头。但是,如果像这样子不断出队的话,对头左边的空间将失去作用,那队列的容量岂不是越来越小了?那么,我们就需要采取一些措施,用数组实现的队列可以采用循环队列的方式来维持队列容量的恒定。
5.什么是循环队列呢?
假设队列经过反复的入队和出队操作,还剩下2个元素,在“物理”上分布于数组的末尾位置,这时又有一个新元素将要入队。在数组不做扩容的前提下,如何让新元素入队并确定新的队尾位置呢?我们可以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。这样一来,整个队列的元素就“循环”起来了。在物理存储上,队尾的位置也可以在对头之前。当再有元素入队时,将其放在数组的首位,队尾指针继续后移即可。一直到(队尾下标+1)%数组长度 = 对头下标 时,代表此队列真的已经满了。需要注意的是,队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小1。
那么我们也可以用代码自己实现一下所谓的循环队列:
package com.algorithm.test; /** * @Author Jack丶WeTa * @Date 2020/7/28 14:57 * @Description 循环队列的实现demo */ public class MyQueueTest { private int[] array; //队头 private int front; //队尾 private int rear; public MyQueueTest(int capacity) { this.array = new int[capacity]; } /** * @param element 入队的元素 * @throws Exception */ public void enQueue(int element) throws Exception { if ((rear + 1) % array.length == front) { // (队尾下标+1)% 数组长度 == 队头下标 //队列已经满了 throw new Exception("队列已满!"); } array[rear] = element; rear = (rear + 1) % array.length; //入队后,队尾向后挪一位 } /** * @return 出队的元素 * @throws Exception */ public int deQueue() throws Exception { if (rear == front) { throw new Exception("队列已空!"); } int deQueueElement = array[front]; //出队的元素 front = (front + 1) % array.length; //出队后,队头向后挪1位 return deQueueElement; } /** * 输出队列 */ public void output() { for (int i = front; i != rear; i = (i+1)%array.length){ System.out.print(array[i] + ","); } } public static void main(String[] args) throws Exception { MyQueueTest myQueueTest = new MyQueueTest(6); myQueueTest.enQueue(3); myQueueTest.enQueue(5); myQueueTest.enQueue(6); myQueueTest.enQueue(8); myQueueTest.enQueue(1); myQueueTest.output(); System.out.println("--------------------------------------"); myQueueTest.deQueue(); myQueueTest.deQueue(); myQueueTest.deQueue(); myQueueTest.output(); System.out.println("--------------------------------------"); myQueueTest.enQueue(2); myQueueTest.enQueue(4); myQueueTest.enQueue(9); myQueueTest.output(); } }
循环队列不仅充分利用了数组的空间,还避免了数组元素整体移动的麻烦,那么入队和出队的时间复杂度也同样是O(1)。
6.栈和队列的应用
6.1 栈的应用
栈的输出顺序和输入顺序相反,所以栈通常用于对“历史”的回溯,也就是逆流而上追溯“历史”。
例如实现递归的逻辑,就可以用栈来代替,因为栈可以回溯方法的调用链。
栈还有一个著名的应用场景是面包屑导航,使用户在浏览页面时可以轻松的回溯到上一级或更上一级的页面。
6.2 队列的应用
队列的输出顺序和输入顺序相同,所以队列通常用于对“历史”的回放,也就是按照“历史”的顺序把“历史”重演一遍。
例如在多线程中,争夺公平锁的等待队列,就是按照访问顺序来决定线程在队列中的次序的。
再如网络爬虫实现网络抓取时,也是把抓取的网站URL存入队列中,在按照存入队列的顺序来依次抓取和解析的。
6.3 双端队列
双端队列这种数据结构可以说是综合了栈和队列的优点,对双端队列来说,从对头一端可以入队或出队,从队尾一端也可以入队或出队。
6.4 优先队列
还有一种队列,它遵循的不是先入先出,而是谁的优先级最高,谁先出队。这种队列叫做优先队列。有限队列已经不属于线性数据机构的范畴了,他是基于二叉堆来实现的。
总结一下,本次主要学习栈和简单的队列的概念和运用,6.3和6.4的知识后面会进行研究和学习的!