不就几种数据类型么?03-栈与队列

hi~各位元旦快乐,经过前两篇的学习,我想大家对数据结构的认知不是那么陌生了吧,苗某这几天整理资料,写文章,对数据结构慢慢有了了解。好了,我们前两期学了链表和数组(其实只是基础介绍,还有很多深层次挖掘需要各位看官自己去深入研究啦)。本期我们学习栈与队列。

1、物理结构和逻辑结构

啥是物理结构,逻辑结构。看名字有点迷。没事,我们慢慢了解。

1.1 什么是数据存储的物理结构呢?

如果说把数据结构比作活生生的人,那么物理结构就是人的血肉和骨骼,看的见,摸得着,实实在在。例如我们刚刚学过的数组和链表,都是内存中实实在在的存储结构。

而在物质的人体之上,还存在着人的思想和精神,它们看不见,摸不着。看过电影《阿凡达》吗? 男主角的思想意识从一个瘦弱残疾的人类身上被移植到一个高大威猛皮肤外星人身上,虽然承载思想意识的肉身改变了,但是人格却是唯一的。

如果把物质层面的人体比作数据存储的物理结构,那么精神层面的人格则是数据存储的逻辑结构。逻辑结构是抽象的概念,它依赖于物理结构而存在。

下面我们来讲解两个常用数据结构:栈和队列。这两者都属于逻辑结构,它们 的物理实现既可以利用数组,也可以利用链表来完成。在后面的章节中,我们会学习到二叉树,这也是一种逻辑结构。同样地,二叉 树也可以依托于物理上的数组或链表来实现。

1.2 什么是栈

要弄明白什么是栈,我们需要先举一个生活中的例子。
假如有一个又细又长的圆筒,圆筒一端封闭,另一端开口。往圆筒里放入乒乓 球,先放入的靠近圆筒底部,后放入的靠近圆筒入口。

那么,要想取出这些乒乓球,则只能按照和放入顺序相反的顺序来取,先取出 后放入的,再取出先放入的,而不可能把最里面最先放入的乒乓球优先取出。(后进先出0.0)

栈(stack)是一种线性数据结构,它就像一个上图所示的放入乒乓球的圆筒容器,栈中的元素只能先入后出(First In Last Out,简称FILO)。最早进入的元 素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶 (top)。

我们开始说了栈是逻辑结构,所以他的实现方式不止一种。栈可以用数组来实现,也可以用链表来实现。

数组的实现方式如下:

链表的实现方式如下:

那么,栈可以进行哪些操作呢?我们下面来看看。

2、栈的基本操作

2.1. 入栈

入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。
这里我们以数组实现为例。

2.2. 出栈

出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素 的前一个元素将会成为新的栈顶。
这里我们以数组实现为例。

由于栈操作的代码实现比较简单,这里就不再展示代码了,有兴趣的朋友可以自己写写看。

小结:入栈和出栈只会影响到最后一个元素,不涉及其他元素的整体移动,所以无论是以数组还是以链表实现,入栈、出栈的时间复杂度 都是O(1)。

3、什么是队列

要弄明白什么是队列,我们同样可以用一个生活中的例子来说明。
假如公路上有一条单行隧道,所有通过隧道的车辆只允许从隧道入口驶入,从隧道出口驶出,不允许逆行。


因此,要想让车辆驶出隧道,只能按照它们驶入隧道的顺序,先驶入的车辆先驶出,后驶入的车辆后驶出,任何车辆都无法跳过它前面的车辆提前驶出。



队列(queue)是一种线性数据结构,它的特征和行驶车辆的单行隧道很相似。 不同于栈的先入后出,队列中的元素只能先入先出(First In First Out,简称 FIFO)。队列的出口端叫作队头(front)队列的入口端叫作队尾(rear)

与栈类似,队列这种数据结构既可以用数组来实现,也可以用链表来实现。
用数组实现时,为了入队操作的方便,把队尾位置规定为最后入队元素的下一个位置。

队列的数组实现如下:


队列的链表实现如下:



4、队列的基本操作

对于链表实现方式,队列的入队、出队操作和栈是大同小异的。但对于数组实 现方式来说,队列的入队和出队操作有了一些有趣的变化。怎么有趣呢?我们后面会看到。

4.1. 入队

入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素, 新元素的下一个位置将会成为新的队尾。


4.2. 出队

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



思考:如果像这样不断出队,队头左边的空间失去作用,那队列的容量岂不是越来越小了?例如像下面这样。


如何做呢:用数组实现的队列,可以采用循环队列的方式来维持队列容量的恒定。

如下:

假设一个队列经过反复的入队和出队操作,还剩下2个元素,在“物理”上分布 于数组的末尾位置。这时又有一个新元素将要入队。

在数组不做扩容的前提下,如何让新元素入队并确定新的队尾位置呢?我们可 以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。


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


一直到(队尾下标+1)%数组长度 = 队头下标时,代表此队列真的已经满了。 需要注意的是,队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小 1。如队列最大容量为10,那么数组的长度就是11 因为第十一个位置是放队尾指针。


 这就是所谓的循环队列,下面让我们来看一看它的代码实现。

public class MyQueue {
    //数组-存储数据
    private int[] array;
    //队头
    private int front;
    //队尾
    private int rear;

    //构造函数 初始化容量
    public MyQueue(int capacity) {
        this.array = new int[capacity];
    }

    /**
     * 入队
     *
     * @Prame element 入队的元素
     */
    public void enQueue(int element) throws Exception {
        //队尾+1 循环队列,初始化时队尾==队头,当队尾再次等于队头时,说明,rear+1是数组长度的倍数取余等于0
        if ((rear + 1) % array.length == front) {
            throw new Exception("队列已满");
        }
        array[rear] = element;
        //rear=rear+1;循环队列的设计
        rear = (rear + 1) % array.length;
    }

    /**
     * 出队
     */
    public int deQueue() throws Exception {
        if (rear == front) {
            throw new Exception("队列已空");
        }
        int deQueueElement = array[front];
        front = (front + 1) % array.length;
        return deQueueElement;
    }

    /**
     *输出队列
     */
    public void output(){
        for (int i = 0; i!=rear ; i=(i+1)%array.length) {
            System.out.println(array[i]);
        }
    }

    public static void main(String[] args) throws Exception {
        MyQueue myQueue = new MyQueue(6);
        myQueue.enQueue(1);
        myQueue.enQueue(3);
        myQueue.enQueue(5);
        myQueue.enQueue(7);
        myQueue.enQueue(9);
        myQueue.output();

        myQueue.deQueue();
        myQueue.deQueue();
        myQueue.deQueue();
        myQueue.deQueue();
        myQueue.deQueue();
        myQueue.output();

        myQueue.enQueue(2);
        myQueue.enQueue(4);
        myQueue.enQueue(6);
        myQueue.enQueue(8);
        myQueue.enQueue(10);
        myQueue.output();
    }
}

循环队列不但充分利用了数组的空间,还避免了数组元素整体移动的麻烦,还真是有点意思呢!至于入队和出队的时间复杂度,也 同样是O(1)。

下面我们来看一看栈和队列都可以应用栈和队列的应用在哪些地方。

5、栈和队列的应用

5.1. 栈的应用
栈的输出顺序和输入顺序相反,所以栈通常用于对“历史”的回溯,也就是逆流而上追溯“历史”。就好像一段视频的倒放。

例如实现递归的逻辑,就可以用栈来代替,因为栈可以回溯方法的调用链。

栈还有一个著名的应用场景是面包屑导航,使用户在浏览页面时可以轻松地回 溯到上一级或更上一级页面。

 5.2  栈的应用队列的应用

队列的输出顺序和输入顺序相同,所以队列通常用于对“历史”的回放,也就 是按照“历史”顺序,把“历史”重演一遍。就好像你拍个视屏,自己再去看一遍。

我们在开发中也会用到比如:

在多线程中,争夺公平锁的等待队列,就是按照访问顺序来决定线程在队列中的次序的。

那么,有没有办法把栈和队列的特点结合起来,既可以先入先出,也可以先入后出呢?有的那就是...

6、双端队列


 双端队列这种数据结构,可以说综合了栈和队列的优点,对双端队列来说,从 队头一端可以入队或出队,从队尾一端也可以入队或出队。其实也好理解,因为栈和队列可以用链表和数组来实现。数组和链表的两头都是可以增加数据和减少数据的。

7、 优先队列

啥子叫优先队列呢,还有一种队列,它遵循的不是先入先出,而是谁的优先级最高,谁先出队。这个东西其实就要结合后面的树的知识来讲解了。

好了本章学习了栈和队列,都是逻辑结构,其实现是数组和链表。也不难的吧,很好理解。至于优先队列的原理是如何实现的呢,欲听后事如何,啪~,请听下回分解。

下期预告:散列表的神奇之处!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值