线性表基础:队列(一)基础知识、经典实现、典型应用场景

一、基础知识

1.1 简介

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表

1.2 逻辑实现

在这里插入图片描述
如图,这是一个队列的逻辑实现,这片连续的存储区可以是数组,也可以是链表,其中length代表队列的长度,head是头指针,指向队列的第一个元素,tail是尾指针,指向队列的最后一个元素,data_type代表队列存储元素的数据类型。

接下来我们看下队列的出队操作:
在这里插入图片描述

普通队列只支持从队首出队,头指针向后移动一步,在逻辑层面我们就认为头指针前面的元素出队了。

入队操作:
在这里插入图片描述

入队就是尾指针向后移动一步,并把元素放进去即可。

1.3 假溢出和循环队列

在这里插入图片描述

如图,按照上面流程当添加到第10个元素的时候,我们会发现第10个元素没空间放了:

  • 就普通队列而言,通常情况上图就认为是队列满的情况,即队列溢出
  • 但实际上队列有9个空间,只存了6个有效元素,这种情况的队列溢出叫做假溢出

为了解决队列假溢出问题,提出了新的结构:循环队列
在这里插入图片描述
如图添加第10个元素的时候,只需要将尾指针重新指向这块连续存储空间的头部即可:
在这里插入图片描述

1.4 总结:

  • 队列是连续的存储区,可以存储一系列的元素。是FIFO(先入先出,First-In-First-Out)结构。

  • 队列通常具有头尾指针(左闭右开区间),头指针指向第一个元素,尾指针指向最后一个元素的下一位

    正常情况队列的尾指针应该是指向最后一个元素的下一位,而上面的图中tail指针是指向尾部元素的指针,都行,看具体实现。

    为什么通常情况下tail指针指向的是最后一个元素的下一位?

    • 其实我们可以发现,各种语言中当在描述一个区间的时候,通常采用左闭右开的区间描述方式,而队列的头尾指针其实标记出的就是队列的区间,也是左闭右开的,在尾指针指向的位置实际上是不存值的
    • 还有另一个好处,tail - head的值刚好就是队列中元素的数量
  • 队列支持(从队尾)入队(enqueue)、(从队首)出队(dequeue)操作。

  • 循环队列可以通过取模操作更有效的利用空间。

二、经典实现

2.1 简单队列

使用数组实现一个简单队列,支持如下操作:

  • 入队 offer
  • 出队 pop
  • 判空 empty
  • 判满 full
  • 查看队首元素 front
  • 查看元素个数 size
  • 打印队列所有元素 print
public class SimpleQueue {
    
    

    private int[] arr;
    //头指针指向第一个元素
    private int head;
    //尾指针指向最后一个元素的下一位
    private int tail;

    public SimpleQueue(int size) {
    
    
        this.arr = new int[size];
        head = tail = 0;
    }

    /**
     * 入队
     *
     * @param e
     * @return
     */
    public boolean offer(int e) {
    
    
        if (full()) {
    
    
            // 满了添加失败
            System.out.println("queue is full");
            return false;
        }
        arr[tail++] = e;
        return true;
    }

    /**
     * 出队
     *
     * @return 队列为空返回-1
     */
    public int pop() {
    
    
        if (empty()) {
    
    
            System.out.println("queue is empty");
            return -1;
        }
        int value = arr[head++];
        return value;
    }

    /**
     * 判断队列是否为空
     *
     * @return
     */
    public boolean empty() {
    
    
        //head=tail说明队列为空
        return head == tail;
    }

    /**
     * 判断队列是否满了
     *
     * @return
     */
    public boolean full() {
    
    
        //当tail指向最后一位下一位的时候说明队列满了
        return tail == arr.length;
    }

    /**
     * 查看队首元素
     *
     * @return
     */
    public int front() {
    
    
    	//查看队首元素就是返回head所指向位置的元素值
        if (empty()) {
    
    
        	return -1;
        }
        return arr[head];
    }

    /**
     * 查看队列元素个数
     *
     * @return
     */
    public int size() {
    
    
    	//tail-head值就是队列元素个数
        return tail - head;
    }

    /**
     * 打印队列中队元素
     */
    public void print() {
    
    
        String out = "Queue:";
        for (int i = head; i < tail; i++) {
    
    
            out += arr[i] + " ";
        }
        System.out.println(out);
    }

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
    
    
        SimpleQueue simpleQueue = new SimpleQueue(5);
        Scanner sc = new Scanner(System.in);
        outer:while (true) {
    
    
            String operate = sc.nextLine();
            switch (operate) {
    
    
                case "offer":
                    simpleQueue.offer(Integer.valueOf(sc.nextLine()));
                    break;
                case "pop":
                    simpleQueue.pop();
                    break;
                case "front":
                    System.out.println("front:" + simpleQueue.front());
                    break;
                case "size":
                    System.out.println("size:" + simpleQueue.size());
                    break;
                case "quit":
                    break outer;
            }
            simpleQueue.print();
        }
    }
}

测试:
在这里插入图片描述

2.2 循环队列

在简单队列基础上进行修改:

public class CycleQueue {
    
    

    private int[] arr;
    // 头指针指向第一个元素
    private int head;
    // 尾指针指向最后一个元素的下一位
    private int tail;

    public CycleQueue(int size) {
    
    
        // 由于尾指针指向最后一个元素的下一位,所以当队列满的时候
        // 因为是循环队列,尾指针刚好走一圈,回到head
        // 此时head == tail,与队列为空判断条件一致
        // 所以这里通过在原有大小的基础上+1,
        // 当尾指针刚好在头指针后一位,我们则认为队列是满的
        // 此时根据尾指针定义,尾指针是不存值的,所以指向的元素应该是空的/无效的
        this.arr = new int[size + 1];
        head = tail = 0;
    }

    /**
     * 入队
     *
     * @param e
     * @return
     */
    public boolean offer(int e) {
    
    
        if (full()) {
    
    
            // 满了添加失败
            System.out.println("queue is full");
            return false;
        }
        arr[tail] = e;
        // 循环队列,当尾指针在数组尾部的时候重新指向数组头部
        tail = (++tail) % arr.length;
        // 可以使用这种方式
        // if (++tail == arr.length) {
    
    
        // tail = 0;
        // }
        return true;
    }

    /**
     * 出队
     *
     * @return 队列为空返回-1
     */
    public int pop() {
    
    
        if (empty()) {
    
    
            System.out.println("queue is empty");
            return -1;
        }
        int value = arr[head];
        head = (++head) % arr.length;
        // 可以使用这种方式
        // if (++head == arr.length) {
    
    
        // head = 0;
        // }
        return value;
    }

    /**
     * 判断队列是否为空
     *
     * @return
     */
    public boolean empty() {
    
    
        // head=tail说明队列为空
        return head == tail;
    }

    /**
     * 判断队列是否满了
     *
     * @return
     */
    public boolean full() {
    
    
        // 循环队列,队列满,说明尾指针刚刚在头指针后一位
        return (tail + 1) % arr.length == head;
    }

    /**
     * 查看队首元素
     *
     * @return
     */
    public int front() {
    
    
        if (empty()) {
    
    
            return -1;
        }
        return arr[head];
    }

    /**
     * 查看队列元素个数
     *
     * @return
     */
    public int size() {
    
    
        return (tail - head + arr.length) % arr.length;
    }

    /**
     * 打印队列中队元素
     */
    public void print() {
    
    
        String out = "Queue:";
        int size = size();
        for (int i = 0, j = head; i < size; i++) {
    
    
            out += arr[j] + " ";
            j = (++j) % arr.length;
        }
        System.out.println(out);
    }

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
    
    
        CycleQueue simpleQueue = new CycleQueue(5);
        Scanner sc = new Scanner(System.in);
        outer:
        while (true) {
    
    
            String operate = sc.nextLine();
            switch (operate) {
    
    
                case "offer":
                    simpleQueue.offer(Integer.valueOf(sc.nextLine()));
                    break;
                case "pop":
                    simpleQueue.pop();
                    break;
                case "front":
                    System.out.println("front:" + simpleQueue.front());
                    break;
                case "size":
                    System.out.println("size:" + simpleQueue.size());
                    break;
                case "quit":
                    break outer;
            }
            simpleQueue.print();
        }
    }
}

测试,可以看到插入6,如果是普通队列就满了,但是循环队列可以继续插入:
在这里插入图片描述

如何判断循环队列是否满了?
我们还可以用额外变量记录队列中元素数量,来简化代码。

三、典型应用场景

3.1 CPU的超线程技术

在这里插入图片描述

当前图中一个CPU里有两个计算核心,这就是真双核处理器。

  • 每个CPU核心连着一个管道,一般称为指令发射管,一条条指令顺着管道从下往上,通过CPU之后这条指令就完成了计算,这个管子就可以当成是指令的队列,这些指令依次性的到达,依次性的被CPU处理。
  • 真双核处理器,是由两个核心,每个核心有一个指令队列

在这里插入图片描述

虚拟四核:这个CPU对外有四个管子,用户就会认为这是四核CPU,但实际上只有两个真正的核心,每个核心带了两个指令队列

  • 为什么可以一个核心处理两个任务队列?因为CPU核心的计算速度非常快,而一个普通的指令队列跟不上核心的供给要求,为了充分利用CPU核心的计算能力
  • 指令管道不是越多越好,从实验数据来看,一个核心最好只带两个指令队列,虚拟8核通常情况下是真四核
  • 通常电脑CPU说4核8线程,这个8线程指的就是任务(指令)队列数量

在这里插入图片描述

多路CPU:一个计算机只有一个CPU叫做一路CPU,多路就是一个计算机有多个CPU

由此对于计算能力,我们可以引申出四层概念:

  • 计算核心
  • CPU:一个CPU包含多个计算核心(企业中一般用的是32核/64核的,但是通常情况是用多个CPU拼起来的)
  • 多路:一个计算节点可以包含多个CPU
  • 集群:分布式计算

3.2 线程池的任务队列

在这里插入图片描述

通常在使用线程的过程中,需要线程就去申请,不需要就销毁,会涉及到反复频繁的进行线程的申请和销毁,这是普通的多线程程序,存在的问题:

  • 频繁的对线程申请和销毁,这两个动作对于程序的效率来说会大打折扣

因此引申出线程池概念:
在这里插入图片描述

在我的程序中有这样的一个结构或者类,可以管理多个线程的。
线程是用来做计算的,当过来一个计算资源后先随机分配给一个线程,又过来几个计算任务后再分配给其他线程
在这里插入图片描述

如果当前四个线程都被分配上计算任务后,这个时候又过来计算任务,当前的这个计算任务就不知道放哪
在这里插入图片描述

所以在线程池中就提出了一个概念,叫做任务队列

在这里插入图片描述

任务队列中存放的就是每一个计算任务,相当于放置任务的缓冲区,每个线程从任务队列中获取任务

在这里插入图片描述

总结:队列通常是用来做缓存区用途的

猜你喜欢

转载自blog.csdn.net/weixin_41947378/article/details/115283636