栈和队列

在学习栈和队列前,先补充两个概念:物理结构和逻辑结构。

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的知识后面会进行研究和学习的!

原文地址:https://www.cnblogs.com/JackWeTa/p/13391835.html