数据结构之队列

1,177 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 25 天,点击查看活动详情

日积月累,水滴石穿 😄

前言

上一篇讲到了数据结构中的栈,说到栈是一种受限线性表,其实,本文要讲的队列也是一种受限线性表

队列的定义

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

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

比如:可以把队列想成排队买票,先来的先排队,后来的人只能站末尾,不允许插队。先来者先买票,这就是典型的“队列”。

队列的分类

(1)顺序(单向)队列:(Queue) 只能在一端插入数据,另一端删除数据。

image.png

(2)循环(双向)队列(Deque):每一端都可以进行插入数据和删除数据操作

自定义队列

数组方式实现

package queue;

/**
 * 队列:先进先出
 * 使用数组实现队列
 */
public class MyArrayQueue {

    /**
     * 数据
     */
    String[] data;

    /**
     * 头部元素下标
     */
    int head;

    /**
     * 尾部元素下标
     */
    int tail;

    /**
     * 元素个数
     */
    int size;

    /**
     * 初始化数组容量
     */
    public MyArrayQueue(int cap) {
        data = new String[cap];
    }
}

准备四个属性,分别是 data、head、tail、size。解释一下 head、tail的含义,我们已经知道,队列的存储结构是对一头进行删除,对一尾进行添加操作。所以用 head、tail 分别代表队头元素、队尾元素的下标。

以及一个有参构造方法用来初始化队列的长度。

offer

/**
 * 入队 如果队列已满,返回 false
 */
public boolean offer(String e){
    //判断队列是否已满
    if(size == data.length){
      return false;
    }
    data[tail] = e;
    tail++;
    size++;
    return true;
}

poll

/**
 * 出队,当队列为空时,返回 null
 */
public String poll(){
    //判断队列是否已空
    if(isEmpty()){
        return null;
    }
    String value = data[head];
    data[head] = null;
    head++;
    size--;
    return value;
}

peek

/**
 * 获得队列头部元素,如果队列为空,则返回null
 */
public String peek(){
    if(isEmpty()){
        return null;
    }
    return data[head];
}

isEmpty

/**
 * 队列是否已空
 */
public boolean isEmpty(){
    //也可以使用 head == tail
    return size == 0;
}

print

/**
 * 打印数据
 */
public void print() {
    if (isEmpty()) {
        System.out.println("队列为空");
        return;
    }
    System.out.print("队列元素为:");
    for (int i = 0; i < data.length; i++) {
        System.out.print(data[i] +" ");
    }
    System.out.println();
}

测试

public static void main(String[] args) {
    MyArrayQueue myQueue = new MyArrayQueue(4);
    System.out.println("获得头元素:"+myQueue.peek());
    System.out.println("插入元素:" +myQueue.offer("a"));
    System.out.println("插入元素:" +myQueue.offer("b"));
    System.out.println("插入元素:" +myQueue.offer("c"));
    System.out.println("插入元素:" +myQueue.offer("d"));
    System.out.println("插入元素:" +myQueue.offer("e"));
    System.out.println("获得头元素:"+myQueue.peek());
    myQueue.print();

    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("获得头元素:"+myQueue.peek());
    
    System.out.println("插入元素:" +myQueue.offer("a"));
}

结果:
获得头元素:null
插入元素:true
插入元素:true
插入元素:true
插入元素:true
插入元素:false
获得头元素:a
队列元素为:a b c d 
移除元素:a
移除元素:b
移除元素:c
移除元素:d
移除元素:null
获得头元素:null
插入元素:false

最开始创建了一个长度为 4 的队列,队列中的属性关系如图所示:

1637151312.jpg

然后执行offer(a)方法,向队列中插入元素,结构如图: 1637154802(1).jpg

可以看到,插入一个元素后 size、tail 两个属性的值发生了变化。tail 向后移动了一个位置指向第二个元素位置。然后我们再将队列添满,结构如下:

image.png

tail指向一个不存在的位置,这时候,我们再执行offer("e"),满足队列是否已满的判断,返回 false。

接下来再看看下出队情况,执行 myQueue.poll()

image.png

出队的时候,size 的数值减一,并将 head 位置的值赋值为 null,然后将 head 向后移动了一个位置指向第二个元素位置。

执行第四个 myQueue.poll(),结构如下: image.png

队列中的 size = 0,或者 tail = head,代表队列中已经没有元素了。

执行最后一个 myQueue.poll(),这时满足 size = 0的判断,返回 null。

所有元素移除之后,这时再进行插入操作,会发现元素插入不了,返回了 false。按道理是要插入成功的,但是由于 tail == data.length满足了队列已满的判断,这个问题就是我们常说的“假溢出”。

那这个“假溢出”问题怎么解决呢?

  • 解决方式1:当有元素出队时,出队元素后面的所有元素都向前移动,保证下次出队的元素处在队列头部,也就是下标为0的位置,但是这种方式会大大增加时间复杂度。
  • 解决方式2:循环队列,头尾相接的循环结构。其实就是修改 tail、head 的值!那什么时候进行修改呢?当 tail、head等于队列长度就重置为 0image.png

解决假溢出

修改 offerpoll方法,内容如下:

offer

/**
 * 入队 如果队列已满,返回 false
 */
public boolean offer(String e){
	//判断队列是否已满 这是使用 size 判断
	if(size == data.length){
	  return false;
	}
	data[tail] = e;
	tail++;
        //如果 tail 的值 等于 data.length
	//说明 tail 需要被重置
	if(tail == data.length){
		tail = 0;
	}
	size++;
	return true;
}

poll

/**
 * 出队,当队列为空时,返回 null
 */
public String poll(){
	
	//判断队列是否已空
	if(isEmpty()){
		return null;
	}
	String value = data[head];
	data[head] = null;
	head++;
        //如果 head 的值 等于 data.length
	//说明 head 需要被重置
	if(head == data.length){
		head = 0;
	}
	size--;
	return value;
}

测试

public static void main(String[] args) {
    MyArrayQueue myQueue = new MyArrayQueue(4);
    System.out.println("获得头元素:"+myQueue.peek());
    System.out.println("插入元素:" +myQueue.offer("a"));
    System.out.println("插入元素:" +myQueue.offer("b"));
    System.out.println("插入元素:" +myQueue.offer("c"));
    System.out.println("插入元素:" +myQueue.offer("d"));
    System.out.println("插入元素:" +myQueue.offer("e"));

    System.out.println("获得头元素:"+myQueue.peek());
    myQueue.print();

    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("插入元素:" +myQueue.offer("a2"));
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("插入元素:" +myQueue.offer("c2"));
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("插入元素:" +myQueue.offer("d2"));
    System.out.println("移除元素:" + myQueue.poll() + " 第一次插入的 a、b、c、d 移除完毕");
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("获得头元素:"+myQueue.peek());

    System.out.println("插入元素:" +myQueue.offer("a"));
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
    System.out.println("移除元素:" + myQueue.poll());
}

对于循环队列小杰还是画图来表示它们的过程吧!

  • 初始化

image.png

  • 执行完第三次 offer,tail = 3。 image.png
  • 执行完第四次 offer,满足 tail == data.length条件,将 tail 置为 0 。

image.png

  • 执行完第五次 offer,满足 size == data.length条件,拒绝添加元素。
  • 执行完第一次 poll,head = 1。

image.png

  • 再执行 offer,各位应该还记得 tail 已经置为 0 了。 image.png
  • 间隔执行两次 polloffer image.png
  • 执行完第四次poll,满足 head == data.length条件,将 head 置为 0 。

image.png

  • 执行第五次poll,这时候的 head 为 0,所以移除第一个元素。执行完后,head = 1。 image.png

后面就是周而复始了,tail、head 等于了数组的长度就进行重置。

链表实现

链表实现队列很简单,就是单链表的添加和删除操作。

public class MyLinkedListQueue {

    /**
     * 头节点,存储整个链表的元素,可以从头节点遍历整个链表
     */
    private MyNode head;
    /**
     * 尾节点,存储单独的尾节点
     */
    private MyNode tail;
    /**
     * 队列元素个数
     */
    private int size;


    private static class MyNode{

        String item;

        MyNode next;

        public MyNode(String item){
            this.item = item;
        }

    }
}

offer

/**
 * 入队:添加尾节点
 */
public boolean offer(String e){
	MyNode newNode = new MyNode(e);
	//如果队列为空
	if(isEmpty()){
		//头节点、尾节点 指向同一个节点
		head = tail = newNode;
	}else{
		//将新节点作为尾节点的下个节点
		//由于这尾节点与头结点的内存地址是一致的
                //尾节点,则也是修改头节点
		tail.next = newNode;
		//新节点作为尾节点
		tail = newNode;
	}
	size++;
	return true;
}

image.png

poll

/**
 * 移除头部节点
 */
public String poll(){
	//如果队列为空
	if(isEmpty()){
		return null;
	}
	MyNode oldHead =  head;
	//获得头节点的值
	String item = oldHead.item;
	//获得头节点的下个节点,并将下个节点赋值给头节点
	head = oldHead.next;
	//把旧的表头的下一个节点指向设置为null,让gc回收
	oldHead.next = null;
	//队列为空,尾节点也需要置为 null
	if(head == null){
		tail = null;
	}
	size--;
	return item;
}

peek

/**
 * 获得队列头部元素,如果队列为空,则返回null
 */
public Object peek(){
    return isEmpty() ? null : head.item;
}

isEmpty

/**
 * 队列是否已空
 */
public boolean isEmpty(){
    //也可以使用 head == tail
    return size == 0;
}

print

public void print(){
    System.out.print("链表元素:");
    MyNode cur = head;
    while (cur != null){
        System.out.println(cur.item + " ");
        cur = cur.next;
    }
}

测试

public static void main(String[] args) {
    MyLinkedListQueue queue = new MyLinkedListQueue();
    System.out.println("获得头元素:" + queue.peek());
    System.out.println("插入元素:" + queue.offer("a"));
    System.out.println("插入元素:" + queue.offer("b"));
    System.out.println("插入元素:" + queue.offer("c"));
    System.out.println("插入元素:" + queue.offer("d"));
    queue.print();
    System.out.println("移除元素:" + queue.poll());
    System.out.println("移除元素:" + queue.poll());
    System.out.println("移除元素:" + queue.poll());
    System.out.println("移除元素:" + queue.poll());
    System.out.println("移除元素:" + queue.poll());
    System.out.println("获得头元素:" + queue.peek());
}

两者区别

  • 使用数组是有边界的队列,而单链表是无界的。
  • 初始化时,数组需要分配空间,而链表则不需要。
  • 单链表存储数据同时还需要额外存储下一个节点的地址,空间开销比较大。

队列的其他应用

  • 优先队列
  • 阻塞队列

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。