数据结构与基础算法-环形队列

一、什么是环形队列。

其实在内存上并没有所谓的环形队列,环形队列只是基于数组线性空间来实现。

环形队列优点

  1. 避免假溢出现象。(因为在数组里,头尾指针只增加不减少,被删元素的空间再也不能被重新利用。会造成尾指针已经到达了队列的最后一位,而头指针前面没有满的情况。)
  2. 广泛用于网络数据的收发。和不同程序之间的数据交换。
  3. 首尾相连的FIFO数据结构,采用数据的线性空间,能快速的知道队列是否满或者空。

二、环形队列的实现。

其实环形队列的实现核心点就是处理队列尾指针达到最后一位该怎么处理。
本文提供的方案是在队列的尾指针达到最后一位时,将其指向0的位置,采用取模(%)运算进行实现。

首先声明属性,其中rear属性需要重点关注,非常重要,它指向队尾元素的后一位,预留了一个数据空间作为一个约定。

    /** 头 指向当前元素*/
    private int front;

    /** 尾 指向队尾元素的后一位 相当于空了一位 区分队列空和队列满*/
    private int rear;

    /** 最大容量 (实例化是传入4,即代表该队列只会存放3个数据,有一个空余约定) */
    private int maxSize;

    private int[] arr;

创建构造器:

public CircleArrayQueue(int maxSize){
        this.maxSize = maxSize;
        arr = new int[maxSize];
    }

几个比较重要的方法:

1. 判断队列是否为空:

数据结构与基础算法-环形队列_第1张图片
数据结构与基础算法-环形队列_第2张图片

头指针=尾指针说明队列为空。

	public Boolean isEmpty(){
        	return rear == front;
   	 }

2. 判断队列是否为满(关键):

判断环形队列是否满之前需要知道,在环形队列中front头指针能不能和普通队列中的头指针一样,伴随数据的写入而一直进行着自增?答案是不行。设计环形队列的初衷便是可以重复利用已经删除数据的空间,避免假溢现象的出现。
而且在判断一个环形队列是否满时,只需要关心尾指针以及头指针的关系即可。头指针必定会跟随着数据的写入而进行着自增,这样在队列满时就会出现和队列为空时一样的情况。都为front = rear,即头指针 = 尾指针,那么我们无法分辨究竟是队列满还是对列空,存在一定的歧义。
所以我们需要用一种新的方式来判断队列是否为满,同时与队列是否为空做出区别。

本文采用取模运算:
数据结构与基础算法-环形队列_第3张图片

柱状图也许不方便理解环形队列已经满的情况。

通过下图可以看到在队列已经满的情况下rear指向了最后一个元素,但其实还有一个空的位置,这个时候需要将rear+1,空出来的位置是作为一个约定存在,是动态变化的。而在环形队列中数据一直写入会造成尾指针rear一直自增造成数组越界。所以需要加下标控制在一个范围以内,而在本文中,这个范围就是头指针front至最大容量maxSize的范围。而取模操作恰好能满足这个需求点。

例如:

  1. 在front = 0,rear = 3,maxSize = 7的情况下,(rear + 1) % maxSize = 0.6,而0.6 != 0,所以数组没有满,依然可以存储数据。
  2. 在front = 0,rear = 6,maxSize = 7的情况下,(rear + 1) % maxSize =0,而0 = 0,数组已经存满。

数据结构与基础算法-环形队列_第4张图片

取模的意义在于将arr下标控制在front和maxSize之间,如果不取模,rear会一直加1,超出队列容量。

    public Boolean isFull(){
        return (rear + 1) % maxSize == front;
    }

3. 往环形数组添加数据的方法:

添加数据时先判断是否已满。
因为rear就是代表元素的位置,所以直接将要添加的数据放在尾指针rear处即可。
但是数据存放完毕后要将rear向后移动一位,根据前文说到的数组越界问题依旧存在,所以也要进行取模运算。

 public void add(int n){

    if(isFull()){
        throw new RuntimeException("队列已满。");
    }

    /** 将数据放在rear的位置 */
    arr[rear] = n;

    /** 因为是环形队列,需要考虑下标越界的问题,根据前面的逻辑,需要考虑取模。 */
    rear = (rear + 1) % maxSize;
}

4. 从环形数组获取数据的方法:

在从环形队列获取数据的时候要先判断队列是否为空。
获取数据时直接获取头指针front所在位置的数据即可。
剩下的逻辑与添加数据的逻辑一样,也得在取模后把指针往后移动一位。
不一样的一点在于取值的时候需要吧值保存在一个临时变量里面。

public int getNum(){
        if(isEmpty()){
            System.out.println("队列为空。");
        }

        /** 将当前位置的数据取出来,放在一个临时变量中,同时将指针移往后一位 */
        int num = arr[front];

        front = (front + 1) % maxSize;

        return num;
    }

5. 打印环形数组(重要):

打印环形队列有两点需要注意:

1、不能直接i< arr.length,现在是环形队列,是一直循环存储的。

环形队列有两种情况:

  1. front < rear (队列没满) —> maxSize - front = 有效数据个数。
  2. front > rear (队列已经存满) —> 0 + rear
  3. 两个式子合起来为:( maxSize - front ) + ( 0 + rear )
  4. 即maxSize - front + rear
  5. front的作用是提供一个起始位置。而取模是为了使下标数字一直在front和maxSize的范围以内。
2、打印的时候不能使用i = 0,要使用数据坐标作为遍历起始值。
public void showArr(){

        if(isEmpty()){
            throw new RuntimeException("队列为空。");
        }
       
        for (int i = front ; i < front + (maxSize - front + rear) % maxSize ; i++){
            System.out.println("i = " + i);
            /** i % maxSize的意义在于将i控制在front以及maxSize之间,避免数组越界。 */
            System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
        }
    }

6. 获取头部数据:

直接根据下标获取即可。

 public int getHeadNum(){
        if(isEmpty()){
            System.out.println("队列为空。");
        }
        return arr[front];
    }

三、完整测试代码

package com.wyg.queue;

import java.util.Scanner;

/**
 * @program: CircleArrayQueueDemoV2
 * @description:环形队列
 * @author: wyg
 * @create: 2022/8/28
 **/
public class CircleArrayQueueDemoV2 {
    public static void main(String[] args) {
    	/** maxSize = 4 代表该队列只能存放3个数据(有一个空余) */
        CircleArrayQueueV2 arrayQueue = new CircleArrayQueueV2(4);
        char key = ' ';
        Scanner scanner = new Scanner(System.in);
        Boolean b = true;
        while (b){
            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':
                    arrayQueue.showArr();
                    break;
                case 'e':
                    scanner.close();
                    b = false;
                    break;
                case 'a':
                    System.out.println("输入一个数。");
                    int i = scanner.nextInt();
                    arrayQueue.add(i);
                    break;
                case 'g' :
                    try{
                        int num = arrayQueue.getNum();
                        System.out.printf("取出的数据是%d\t",num);
                        break;
                    }catch (Exception e){
                        System.out.println(e.getMessage());
                    }
                case 'h' :
                    try{
                        int num = arrayQueue.getHeadNum();
                        System.out.printf("取出的头部数据是%d\t",num);
                        break;
                    }catch (Exception e){
                        System.out.println(e.getMessage());
                    }
                    break;
            }
        }

    }
}

class CircleArrayQueueV2{
    /** 头 指向当前元素*/
    private int front;

    /** 尾 指向队尾元素的后一位 相当于空了一位 区分队列空和队列满*/
    private int rear;

    /** 最大容量 */
    private int maxSize;

    private int[] arr;

    public CircleArrayQueueV2(int maxSize){
        this.maxSize = maxSize;
        arr = new int[maxSize];
    }
    /** 取模的意义在于将arr下标控制在rear和maxSize之间,如果不取模,rear会一直加1,超出队列容量。 */
    public Boolean isFull(){
        return (rear + 1) % maxSize == front;
    }

    public Boolean isEmpty(){
        return rear == front;
    }

    public void add(int n){

        if(isFull()){
            throw new RuntimeException("队列已满。");
        }

        /** 将数据放在rear的位置 */
        arr[rear] = n;

        /** 因为是环形队列,需要考虑下标越界的问题,根据前面的逻辑,需要考虑取模。 */
        rear = (rear + 1) % maxSize;
    }

    public int getNum(){
        if(isEmpty()){
            System.out.println("队列为空。");
        }

        /** 将当前位置的数据取出来,放在一个临时变量中,同时将指针移往后一位 */
        int num = arr[front];

        front = (front + 1) % maxSize;

        return num;
    }

    public void showArr(){

        if(isEmpty()){
            throw new RuntimeException("队列为空。");
        }
        /** 打印环形队列需要注意;
         *  不能直接i< arr.length,现在是环形队列,是一直循环存储的。
         *  环形队列有两种情况:
         *  1. front < rear (队列没满) ---> maxSize - front = 有效数据个数。
         *  2. front > rear (队列已经存满) ---> 0 + rear
         *  3. 两个式子合起来为:( maxSize - front ) + ( 0 + rear )
         *  4. 即maxSize - front + rear
         *  5. front的作用是提供一个起始位置。而取模是为了使下标数字一直在front和maxSize的范围以内。
         */
        for (int i = front ; i < front + (maxSize - front + rear) % maxSize ; i++){
            System.out.println("i = " + i);
            /** i % maxSize的意义在于将i控制在front以及maxSize之间,避免数组越界。 */
            System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
        }
    }

    public int getHeadNum(){
        if(isEmpty()){
            System.out.println("队列为空。");
        }
        return arr[front];
    }
}

你可能感兴趣的:(数据结构,算法,java)