简单的算法:环形数组实现队列详细分析,

楔子

2021-1-12,天气晴
学习算法的第二天,嗯,头发还是一如既往的疯狂的掉,昨天在学习队列的过程中,我本以为:队列?就这啊!不就是一个先进先出,后进后出吗?这不有手就行?然后一写代码…(懵)啊,这是人学的玩意!!!,偶受不了了!哼╭(╯^╰)╮不管了,劳资今天不写了,想影响我的心态,不存在的。如果明天还拿不下你,我就去搞前端…。第二天:完了完了,还是不会呀,怎么办,偶不想写前端呀,就在这时,我的天灵盖发出一阵耀眼的光芒,随之,一道宛如远古英灵般的碎语在我的脑海中响起:8点了,起床写代码了,8点了,起床写代码了;然后我就醒了。起床洗漱,打开电脑,当我再次拿起昨天的代码,我发现我的思路异常清晰,好了,我编不下去了,直接进入正题吧!


数组实现队列:思路一(有缺陷)

  1. 设置maxSize表示队列的最大存储数
  2. 设置两个指针,分别代表头部(head)和尾部(tail),front表示head,默认值为-1,指向队列的head元素(Element)的前一个位置;rear表示tail,默认值为-1,指向队列的tail的有效元素(注意,这里并不是指向队列的尾部,而是有效数据也就是你存入的数据中,最后一个元素);
  3. 将尾部指针往下移:rear+1
    • 当rear = front时,表示队列为空
    • 当尾部指针rear小于队列的最大下标maxSize-1时,表示队列还未满,则可以进行数据存入;
    • 当尾部指针rear大于队列的最大下标时,rear = maxSize - 1,表示队列已满,这时候无法存入数据

代码实现

package com.fufu.queue;
/**
 * @function 队列demo
 * @author fufu
 * @version 1.8
 */
public class ArrayQueue {
	private int maxSize;//最大存储数
	private int rear;//队列头
	private int front;//队列尾
	 int[] queueArr;//用来存数据的数组,模拟队列
	
	public ArrayQueue() {
		maxSize = 10;//默认队列的最大存储数为10
		rear = -1;
		front = -1;
		queueArr = new int[maxSize];
	}
	
	public ArrayQueue(int maxSize) {
		this.maxSize = maxSize;//初始化队列最大存储数
		rear = -1;//指向队列的尾部数据(队列的最后一个数据)
		front = -1;//指向队列头部,分析出front是指向队列头部的前一个位置 
		queueArr = new int[maxSize];
	}
	
	//判断队列是否为空
	public boolean isEmpty() {
		return rear==front;
	}
	
	//判断队列的数据是否已满
	public boolean isFull() {
		return rear==maxSize-1;
	}
	
	//添加一个数据到队列
	public void addDataToQueue(int num) {
		//如果队列已经满了,则抛出异常
		if(isFull()) {
			throw new RuntimeException("队列溢出...~~");
		}
		rear++;
		queueArr[rear] = num;
	}
	
	//获取\取出一个数据
	public int getDataFromQueue() {
		if(isEmpty()) {
			throw new RuntimeException("PointNullException~~队列为null");
		}
		front++;
		return queueArr[front];
	}
	
	//显示队列数据
	public void showQueueData() {
		if(isEmpty()) {
			System.out.println("队列为null");
			return ;
		}
		for(int i=0;i<queueArr.length;i++) {
			System.out.printf("queueArr[%d]=%d\t",i,queueArr[i]);
		}
	}
	
	//显示队列头部数据
	public int showQueueHeadData() {
		if(isEmpty()) {
			System.out.println("数据为空");
			return -1;
		}
		front++;
		return queueArr[front];
	}
}

测试代码

import java.util.InputMismatchException;
import java.util.Random;
import java.util.Scanner;

public class ArrayQueueTest {
	public static void main(String[] args) {
		ArrayQueue aq = new ArrayQueue(3);
		Scanner sc = new Scanner(System.in);
		char c ;
		boolean flag = true;
		while(flag) {
			System.out.println("按下'a'添加队列数据");
			System.out.println("按下's'显示队列全部数据");
			System.out.println("按下'h'显示队列顶部数据");
			System.out.println("按下'e'退出");
			System.out.println("按下'g'获取一个数据");
			c = sc.next().charAt(0);
			switch(c) {
			case 'a':
				try {
					System.out.println("请输入:");
					int i = sc.nextInt();
					aq.addDataToQueue(i);
				}catch(InputMismatchException e) {
					throw new RuntimeException("输入数异常");
				}
				break;
			case 's':
				aq.showQueueData();
				break;
			case 'h':
				int num = aq.showQueueHeadData();
				System.out.println(num);
				break;
			case 'e':
				flag=!flag;
				break;
			case 'g':
				int num1 = aq.getDataFromQueue();
				System.out.println(num1);
				break;
			}
		}
		System.out.println("程序结束!!");
	}
}

测试如下

按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
a
请输入:
1
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
a
请输入:
2
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
a
请输入:
3
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
s
queueArr[0]=1	queueArr[1]=2	queueArr[2]=3	按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
h
1
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
g
2
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
h
3
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据
h
数据为空
-1
按下'a'添加队列数据
按下's'显示队列全部数据
按下'h'显示队列顶部数据
按下'e'退出
按下'g'获取一个数据

可以看到,当我显示头部元素和获取一个元素,它的指针都会往后走,当指针超过了队列的最大存储数时,就会发生异常,也就是说,这种写法只能使用一次,也就是一次性代码

推论:
Q:显示头部元素为什么要让指针移动,不让它的指针移动不就行了;

A:不行,front在设定上是指向头部元素的前一个位置,所以在显示的时候必须+1,当然,你可以设置一个boolean标志来让它只加一次,比如这样:

//显示队列头部数据
	int i=0;
	public int showQueueHeadData() {
		
		if(isEmpty()) {
			System.out.println("数据为空");
			return -1;
		}
		if(i==0) {
			front++;
			i++;
		}
		return queueArr[front];
	}

但是,如果这时候队列里只有一个数据,那么我们显示了一次之后,front + 1会等于rear,就会出现明明还有一个数据,却显示为空,多个数据也是如此

Q:可是让它每显示一次front就移动一次的话,那我如果一直显示就会造成front>=rear的情况呀

A:额!关于这一点,我也辩解不了,只能说是这个垃圾算法的问题吧(啊,哈哈哈哈);

Q:好,这个先不提,就拿存数据来说,我先存3个数据,然后再取出数据3次,这时候显示队列为空,然后,我再添加数据,这时候却显示队列已满,数据溢出,什么鬼?

A:那肯定呀!因为你压根就没有把指针调回去,当存入三个数据时,rear(初始值为-1)指针会移动3次,这时候rear = maxSize - 1 = 2(在代码中我设置的最大存储数为3),接着,又取数据3次,front又移动3次,3次之后,就会出现一个神奇的问题,front = rear = 2,根据我们之前的队列为空判断,front = rear的时候队列为空,但是,明明队列是空的却不能添加数据,显示队列已满,这不是自相矛盾吗?(再次感叹一句,垃圾算法!)

Q:那你就不能加个判断把指针调回去吗?

A:好,我试试看,唉,经过我半小时的研究,发现还是不可行,又引申出来好多问题,比如说,在添加数据的时候,当我们添加了3次数据之后,再取出第一个数据,这时候,front = 0,rear = 2;因为取出了一个数据,按道理来说,我们应该是可以再添加一个数据的,于是乎,我推出了可以添加数据的条件,当rear>2&&front!=-1时,才能添加数据,也就是rear>最大存储数时,而front刚好移动过,说明取出过数据,这时候,我们就可以把数据添加到数组的第一位,而这样的想法想要成立,需要考虑另外几种情况,如果我们要将数据添加进去,那么front的指针必须回调,重新指向-1,而rear将作为添加的数据的下标,为0,但是rear是指向末尾的,如果是按照环形的来看的话,新添加的数据确实是为末尾,但是,如果这时候我们再取出一个数据,front指针上移,front = 0,这时候,front又=>rear,那么,我们又不能添加数据了,按照我之前推论的条件rear>2&&front!=-1,很明显是不成立的。我塔喵的心态崩了呀

Q:那…

A:打住,别钻牛角尖了,后面我也想过用一个变量来代替rear作为赋值的下标,但又会引出很多其他问题,这么下去浪费的时间太多了,先打住吧,以后有机会再回看,我们先换一种方法来试试

Q:难道,你…唉,终于还是要用了吗?

A:没错,我已经决定了,即便耗尽此身头发,也要追求算法的极致

数组实现队列:思路二

由于思路一无法完全的模拟队列,故引申出第二张思路:环形数组模拟队列实现
思路分析:

  1. front变量调整:front指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素,front的初始值为0
  2. rear变量调整:rear指向队列的最后一个元素的后一个位置,因为希望它空出一个空间作为约定。rear的初始值为0
  3. 当队列满时:应有条件(rear + 1) % maxSize = front【满】
    解释: 假设maxSize的值为5,那么,当我们有3个数据的时候,rear指向其后一个位置,及rear=4,这时候还需要留一个空间作为,故rear + 1,其值刚好等于maxSize,front的初始值为0,所以条件成立;%是为了实现环形的一个效果,我们知道,一个数%另一个数,其值一定在0到这个数之间;
  4. 队列为空时:应有条件rear == front【空】
  5. 我们还需要分析,队列中的有效数据的个数:应有(rear + maxSize - front) % maxSize;

代码实现:

package com.fufu.circlequeue;

public class ArrayCricleQueue {
	private int front;//指向队列的第一个元素
	private int rear;//指向队列的最后一个元素的后一个位置
	private int maxSize;//队列的最大存储数
	private int[] cricleArr;//模拟环形队列存储数据的数组
	
	//构造
	public ArrayCricleQueue(int maxSize) {
		this.maxSize = maxSize;
		cricleArr = new int[maxSize];
	}
	
	//判断队列是否已满
	public boolean isFull() {
		return (rear+1)%maxSize==front;
	}
	
	//判断队列是否为空
	public boolean isEmpty() {
		return front == rear;
	}
	
	//添加一个元素到队列中
	public void addElement(int n) {
		if(isFull()) {
			throw new RuntimeException("DataOverFlowException~~~数据溢出异常");
		}
		cricleArr[rear] = n;
		rear = (rear+1) % maxSize;
//		System.out.println("rear:"+rear);
	}
	
	//获取队列的一个元素
	public int get() {
		if(isEmpty()) {
			throw new RuntimeException("PointNullException~~~队列为空");
		}
		int value = cricleArr[front];
		front = (front+1) % maxSize;
//		System.out.println("front:"+front);
		return value;
	}
	
	//显示当前队列的全部元素
	public void showAllElement() {
		if(isEmpty()) {
			System.out.println("当前队列为空");
		}
//		System.out.print("front:"+front+" "+"rear:"+rear+" ");
		for(int i=front;i<front+size();i++) {
			System.out.printf("cricleArr[%d]=%d\t",i%maxSize,cricleArr[i%maxSize]);
		}
		System.out.println();
	}
	
	//获取队列的有效数据
	public int size() {
		return (rear + maxSize - front) % maxSize;
	}

}

运行测试

package com.fufu.circlequeue;

public class ArrayCricleQueueTest {
	public static void main(String[] args) {
		ArrayCricleQueue acq = new ArrayCricleQueue(3);
		acq.addElement(2);
		acq.addElement(5);
//		acq.addElement(10);有一块空间需要留作约定,所以当可存数据为n时,实践的可用空间为n-1
		acq.showAllElement();
		acq.get();
		acq.get();
		acq.showAllElement();
		acq.addElement(3);
		acq.addElement(4);
		acq.showAllElement();
	}

}

运行结果

rear:1
rear:2
front:0 rear:2 cricleArr[0]=2	cricleArr[1]=5	
front:1
front:2
当前队列为空
front:2 rear:2 
rear:0
rear:1
front:2 rear:1 cricleArr[2]=3	cricleArr[0]=4	

嗯,这样就基本实现了队列的一个效果了;


总结

在第一种思路中,它的缺陷在于无法重复的使用,只能使用一次,其根本原因在于指针的指向一直在往上走,没有重新调零,如果想要实现第一种思路,可以根据特定条件对指针进行调0,但是这样引申出来的问题太多了,从实用性来说,并不适合,但是从算法性来说的话,这是一个思考的过程,有兴趣可以研究研究。

在第二种思路中,在思路一的方式上进行了巧妙的改变,将数组想象成一个环形的,也就是以环形数组的思维去实现它;在思路一的基础上,对指针进行了一个调整,front指向当前队列第一个元素,rear指向队列的最后一个数据的后一个位置,这样调整的意义在于,我们每次添加一个元素,rear都会改变,让rear指向最后一个元素的后一个位置,因为需要空一个一个空间来作为约定,这个约定具体是干嘛的?我只能说它是该算法的核心思想,是一种比较抽象的思维理念,不好用言语来形容,当rear的指向超过了队列最大存储数的范围,那么就对它进行调0,这么说也不太合适,可以把它想象成一个环形的,无论它怎么走都是在这个环里面,这里巧妙的使用了%,我们知道,一个数%另一个数,其取值范围始终在0到这个数之间,而%【取模】也是环形思想的最重要的部分,当然,环形数组其本质还是数组,当我们进行取值的时候,如果之前存入了值,取出并不代表数组里的元素被取出了,实际上,它是通过不停的对数组进行复制操作来完成存值和取值的;

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值