数据结构和算法——队列

项目介绍

在现实生活中,排队是必不可少的元素,政府机关、银行、商场、食堂等,只要是需要获取服务的,都需要排队等待。服务资源有限,要对每一个人提供相对公平的服务,就要涉及到排队系统。排队一般遵循先来后到原则,与数据结构中的队列有类似的属性。下面我们用银行排号系统具体实现一下队列实现客户先来先服务的功能,并对其进行优化。

队列实现银行排号系统

实例:银行排号系统
功能:先到的客户先使用服务窗口。在本次实例中,将功能抽象为:创建队列、加入数据、取出数据、显示队列和查看队列头部。

方法:使用队列记录数据

队列:队列是一种【特殊的线性表】,它只允许在表的前端(front)进行删除操作,表的后端(rear)进行插入操作,和栈一样,队列是一种【操作受限制】的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
特点:先进先出
介绍:队列是一个有序列表,可以用【数组】或者【链表】来实现。遵循先入先出的原则(先存入队列的数据先取出,后存入队列的数据后取出)。
示意图:(使用数组模拟队列示意图)
数组结构存储队列

  1. 队列本身是有序表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图,其中MaxSize是该队列的最大容量。
  2. 因为队列的输出、输入时分别从前后端来处理,因此需要两个指针front和rear分别记录队列前后端,front会随着数据输出而改变,而rear会随着数据输出而改变。

常用操作

  1. 创建队列(createQueue)
    ①定义数组最大容量、队列头部、队列尾部和用于模拟队列的数组。
    ②创建队列的构造器。将构造器的值赋给上述属性。
  2. 往队列添加数据(addQueue)
    ①将尾指针往后移:rear+1,当front==rear时,队列为空。
    ②若尾指针rear小于队列的最大下标MaxSize-1,则将数据存入rear所指的数组元素中,否则无法存入数据。rear == MaxSize-1【队列满】。
  3. 从队列中取出数据(getQueue)
    ①判断队列是否为空,空列表不能获取数据,并抛出异常,提示用户队列为空。
    ②若队列不为空,取出数据后,front指针后移,取出的数据为arr[front]
  4. 显示队列(showQueue)
    ①判断队列是否为空,空列表不能显示数据,并打印【队列为空,没有数据】。
    ②若队列不为空,用for循环遍历数组,取出所有的值并格式化输出。
  5. 查看队列头部(headQueue)
    ①判断队列是否为空,空列表不能显示队列头部,并抛出异常,提示用户队列为空。
    ②若队列不为空,则返回front指针的后一数据arr[front + 1]

代码实现

判断队列是否为空是否满

    //判断队列是否满
    public boolean isFull(){
        return rear == maxSize - 1;
    }

    //判断队列是否为空
    public boolean isEmpty(){
        return rear == front;
    }

创建队列

    private int maxSize; //表示数组最大容量
    private int front; //队列头部
    private int rear; //队列尾部
    private int[] arr; //该数据用于存放数据,模拟队列

    //创建队列的构造器
    public ArrayQueue(int arrMaxSize){
        maxSize = arrMaxSize;
        arr = new int[maxSize];
        front = -1; //指向队列头部,front是指向队列头的前一个位置
        rear = -1; //指向队列尾部,指向队列尾的数据(就是队列最后一个数据)
    }

往队列添加数据

添加数据时front不动,rear后移。

//添加数据到队列
    public void addQueue(int n){
        //判断队列是否满
        if (isFull()){
            System.out.println("队列已满,不能加入数据");
            return;
        }
        rear++; //让rear后移
        arr[rear] = n;
    }

从队列取出数据

取出数据时rear不动,front后移。取出数据为head,即front的后一位数据,这里先【front++】,返回时就不需要【front+1】了。

//获取队列的数据,出队列
    public int getQueue(){
        //判断队列是否为空
        if (isEmpty()){
            //通过抛出异常来处理
            throw new RuntimeException("队列为空,不能取出数据");
        }
        front++; //让front后移
        return arr[front];
    }

显示队列

用for循环遍历数组

//显示队列的所有数据
    public void showQueue(){
        //遍历
        if (isEmpty()){
            System.out.println("队列为空,不能显示数据");
            return;
        }
        for (int i = 0; i < arr.length; i++){
            System.out.printf("arr[%d]=%d\n", i, arr[i]);
        }
    }

显示队列头部

显示队列的头部为front指针的前一个数据。

//显示队列的头部,注意不是取出数据
    public int headQueue(){
        if (isEmpty()){
            throw new RuntimeException("队列为空,不能取出数据");
        }
        return arr[front + 1];
    }

创建测试队列

创建一个最大容量为3的队列,并定义一个scanner用于接收用户输出命令。

	//初始化,创建一个队列
    ArrayQueue queue = new ArrayQueue(3);
    char key = ' '; //接收用户输入
    Scanner scanner = new Scanner(System.in);
    boolean loop = true;

输出一个菜单作为交互界面,用于接收用户想要对队列的操作。

	//输出一个菜单
    while (loop){
        System.out.println("s(show):显示队列");
        System.out.println("e(exit):退出程序");
        System.out.println("a(add):添加数据到队列");
        System.out.println("g(get):从队列取出数据");
        System.out.println("h(head):查看队列头部数据");
        key = scanner.next().charAt(0); //接收一个字符
        switch (key){
            case 's':
               queue.showQueue();
               break;
            case 'a':
               System.out.println("输入一个数:");
               int value = scanner.nextInt();
               queue.addQueue(value);
               break;
            case 'g': //取出数据
               try{
                    int res = queue.getQueue();
                    System.out.printf("取出数据是%d\n",res);
                }catch (Exception e){
                    System.out.println(e.getMessage());
                }
                break;
            case 'h': //查看队列头的数据
                try{
                    int res = queue.headQueue();
                    System.out.printf("这个队列头的数据是%d\n", res);
                }catch (Exception e){
                    System.out.println(e.getMessage());
                }
                break;
            case 'e':
                scanner.close();
                loop = false;
                break;
            default:
                break;
        }

运行结果

  1. 运行程序显示菜单
s(show):显示队列
e(exit):退出程序
a(add):添加数据到队列
g(get):从队列取出数据
h(head):查看队列头部数据
  1. 选择【a】,加入数据【10】
a
输入一个数:
10
  1. 选择【s】,查看当前队列全部数据
s
arr[0]=10
arr[1]=0
arr[2]=0

这里只加入了一个数据,所以arr[0]=10,其他两个数据还未赋值。此时arr[0]为队列头(head)。

  1. 选择【a】,加入数据【20】
a
输入一个数:
20
  1. 选择【s】,再次查看当前队列全部数据
s
arr[0]=10
arr[1]=20
arr[2]=0

队列中第二个数据加入成功。

  1. 选择【a】,加入数据【30】
a
输入一个数:
30
  1. 选择【s】,再次查看当前队列数据
s
arr[0]=10
arr[1]=20
arr[2]=30

3个数据全部加入队列,此时队列已满。

  1. 选择【a】,添加数据【40】
a
输入一个数:
40
队列已满,不能加入数据

在已满队列中执行添加数据到队列操作时,不会插入新数据并提示【队列已满,不能加入数据】。

  1. 选择【h】,查看此时队列头部数据
h
这个队列头的数据是10

此时队列头部数据依然是【10】。

  1. 选择【g】,从队列中取出数据
g
取出数据是10

将head数据取出了,head是最先进入队列的,所以最先取出。

  1. 选择【h】,查看此时队列头部数据
h
这个队列头的数据是20

取出数据【10】后,【20】成为新队列的头部。

  1. 选择【g】,从队列中取出数据
g
取出数据是20

再次取出数据,依然取出了新队列的头部数据。

  1. 选择【g】,从队列中取出数据,然后再选择【g】
g
取出数据是30
g
队列为空,不能取出数据

第三次执行【g】取数据命令后,队列中所有数据已经全部取出,队列成为空队列,再次执行【g】取数据命令后会提示【队列为空,不能取出数据】。

实验进行到这里,你是否以为整个实例功能已经全部实现了呢?
其实不然,此时队列为空,那么我们试着再次执行【a】添加数据操作看看会发生什么呢~

  1. 最后选择【a】,试图往【空队列】中加入数据
a
输入一个数:
40
队列已满,不能加入数据

很迷惑的事情发生了。。。
为什么队列为空我们却不能添加数据呢。

问题分析并优化

上面用普通队列的方法实现的排队功能好像并没有达到我们预期的结果。在上述的队列中,加入数据rear指针上移,取出数据front指针也上移,最终的结果就是rear指针上移到【maxSize-1】(此时队列尾部满,不能继续添加数据),front指针也上移到【maxSize-1】(此时取出了所有数据,队列为空),此时rear和front都在队列尾部,不能继续添加数据,也就不能再执行取出数据操作了。

问题分析:目前队列使用一次就不能再用,没有达到复用的效果。
优化:通过对指针运算取模的方式,将原队列改进成一个环形队列。

改进方法:使用循环队列实现复用

循环队列:为充分利用向量空间,克服【假溢出】现象的方法。将向量空间想象为一个【首尾相接的圆环】,并称这种向量为循环向量。存储在其中的队列称为【循环队列】。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
特点:队列首尾相连,实现队列的复用。
介绍:通过取模运算重新定义

使用数组模拟环形队列的思路

  1. front含义变化:指向队列第一个元素,即arr[front]是队列的第一个元素,front初始值为0。
  2. rear含义变化:指向队列最后一个元素的后一个位置,空出一个位置作为预留位。rear初始值为0。
  3. 队列满判定条件:(rear + 1)% maxSize = front。
  4. 队列空判定条件:rear == front。
  5. 队列中有效数据个数:(rear + maxSize + front)% maxSize。

代码修改部分

所有包含rear和front计算的地方,将计算公式套换成思路中的公式即可。新加入了计算队列有效数据个数的函数。
贴出核心代码

class CircleArray {
    private int maxSize; //表示数组最大容量
    private int front; //队列头部 front的含义做调整:指向队列第一个元素,front初始值为0
    private int rear; //队列尾部 rear的含义做调整:指向最后一个元素的后一个位置,希望空出一个位置作为缓冲区,初始值为0
    private int[] arr; //该数据用于存放数据,模拟队列

    public CircleArray(int arrMaxSize){
        maxSize = arrMaxSize;
        arr = new int[maxSize];
        //front 和 rear 默认为 0 可以不写
    }

    //判断队列是否满
    public boolean isFull(){
        return (rear + 1) % maxSize == front;
    }

    //判断队列是否为空
    public boolean isEmpty(){
        return rear == front;
    }

    //添加队列到数据
    public void addQueue(int n){
        //判断队列是否满
        if (isFull()){
            System.out.println("队列已满,不能加入数据");
            return;
        }
        //直接将数据加入
        arr[rear] = n;
        //将rear后移,这里必须考虑取模
        rear = (rear + 1) % maxSize;
    }

    //获取队列数据,出队列
    public int getQueue(){
        //判断队列是否为空
        if (isEmpty()){
            //通过抛出异常
            throw new RuntimeException("队列为空,不能取出数据");
        }
        //这里需要分析出front是指向队列的第一个元素
        //1.先把front对应的的值保存到一个临时变量
        //2.将front后移,考虑取模
        //3.将临时保存的变量返回
        int value = arr[front];
        front = (front + 1) % maxSize;
        return value;
    }

    public void showQueue(){
        //遍历
        if (isEmpty()){
            System.out.println("队列为空,不能取出数据");
            return;
        }
        //思路:从front开始遍历,遍历多少个元素
        for (int i = front; i < front + size(); i++){
            System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
        }
    }

    //显示队列的头数据,注意不是取数据
    public int headQueue(){
        //判断是否为空
        if (isEmpty()){
            throw new RuntimeException("队列为空,不能取出数据");
        }
        return arr[front + 1];
    }

    //队列中有效数据的个数
    public int size(){
        return (rear + maxSize - front) % maxSize;
    }
}

重点判定是否为空是否满,rear和front移动时考虑取模就ok啦😁

完美代码测试结果

说明:由于设计思路时空出一个位置作为预留位,所以在创建队列时,创建maxSize为4的队列有效值最大为3。

测试数组模拟环形队列的案例:
s(show):显示队列
e(exit):退出程序
a(add):添加数据到队列
g(get):从队列取出数据
h(head):查看队列头部数据
a
输入一个数:
10

a
输入一个数:
20

a
输入一个数:
30

s
arr[0]=10
arr[1]=20
arr[2]=30

g
取出数据是10

s
arr[1]=20
arr[2]=30

a
输入一个数:
40

s
arr[1]=20
arr[2]=30
arr[3]=40

可以看到,在使用循环队列以后,队列达到复用的效果,只要队列不为空,就能加入新数据,且4个位置都可作为预留位,方便操作的执行。

总结

生活中随处可见排队问题,没有排队系统一切社会活动都会变得混乱。当然除了先来后到的原则,现实中依然存在其他排队方式,就像计算机操作系统中的短作业优先算法(SJF)1 、最短剩余时间优先算法(SRT)2 、最高响应比优先算法(HRRN)3、基于优先数的调度算法(HPF)4 等调度算法。这些方法都是在特定情况有特殊需求时所涉及的算法,而现实中,一般情况下(不紧急),为了尊重每个公民的合法权益,社交场合一般都采取类似先来先服务算法(FCFS)5 的先来后到次序。当然如果你有特殊情况(比如你只需要很短时间就能完成服务,又有急事需要处理😨),这个时候就可以采取和他人协商的方式,采取短作业优先算法。生活处处充满算法😋,学好算法为人类生产生活提供更多便利和服务吧!🤗


  1. 按照作业的长短顺序进行调度,短作业优先。 ↩︎

  2. 称为抢占式的短作业优先算法,允许比当前剩余时间更短的进程来抢占,抢占时机为新作业加入队列时。 ↩︎

  3. 选出响应比最高的作业投入执行。响应比=(等待时间W+要求执行时间T)/(要求执行时间T)。 ↩︎

  4. 用户提交作业时,根据急迫程度规定适当的优先数,作业调度程序根据JCB优先数决定调度顺序。 ↩︎

  5. 最简单的调度算法,按照作业的先后次序进行调度。 ↩︎

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值