04-队列

nobility 发布于 2020-04-11 1967 次阅读


队列

  • 队列对应的操作,属于是数组的子集,因为只能从一端添加元素(队尾),从另一端取出元素(队头)
  • 队列是一种先进先出的数据结构(FIFO),用户无法获取中间的元素
  • 由于栈的实现方式不仅是一种,所以写成接口的形式,方便多种实现
/**
 * @InterfaceName: Queue
 * @Description: 自定义队列接口
 */
public interface Queue<E> {
  /**
   * @MethodName: enqueue
   * @Description: 将元素从队尾入队
   * @Param element: 入队元素
   * @Return void
   */
  void enqueue(E element);

  /**
   * @MethodName: dequeue
   * @Description: 将元素从队头出队
   * @Return E
   */
  E dequeue();

  /**
   * @MethodName: getFront
   * @Description: 获取队头元素
   * @Return E
   */
  E getFront();

  /**
   * @MethodName: getSize
   * @Description: 获取队列中的元素个数
   * @Return int
   */
  int getSize();

  /**
   * @MethodName: isEmpty
   * @Description: 判断栈是否为空
   * @Return boolean
   */
  boolean isEmpty();
}

基于动态数组的队列

/**
 * @ClassName: ArrayQueue
 * @Description: 基于自定义数组实现的自定义队列
 */
public class ArrayQueue<E> implements Queue<E> {
  private Array<E> array;

  /**
   * @MethodName: ArrayQueue
   * @Description: 根据用户需求创建指定大小容量的队列
   * @Param capacity: 用户指定的容量
   */
  public ArrayQueue(int capacity) {
    this.array = new Array<>(capacity);
  }

  /**
   * @MethodName: ArrayQueue
   * @Description: 为用户设置默认的容量大小,为10
   */
  public ArrayQueue() {
    this.array = new Array<>(10);
  }

  /**
   * @MethodName: enqueue
   * @Description: 入队操作实现
   * @Param element: 入队元素
   * @Return void
   */
  @Override
  public void enqueue(E element) {
    this.array.add(this.getSize(), element);  //直接向数组尾部添加元素
  }

  /**
   * @MethodName: dequeue
   * @Description: 出队操作实现
   * @Return E
   */
  @Override
  public E dequeue() {
    return this.array.remove(0);  //直接删除数组头部元素
  }

  /**
   * @MethodName: getFront
   * @Description: 查看队头元素操作实现
   * @Return E
   */
  @Override
  public E getFront() {
    return this.array.get(0); //直接返回数组第一个元素
  }

  /**
   * @MethodName: getSize
   * @Description: 查看队列中元素个数操作实现
   * @Return int
   */
  @Override
  public int getSize() {
    return this.array.getSize();  //返回当前数组中元素的个数
  }

  /**
   * @MethodName: isEmpty
   * @Description: 查看队列是否为空操作实现
   * @Return boolean
   */
  @Override
  public boolean isEmpty() {
    return this.array.isEmpty();  //返回当前数组是否为空
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < this.array.getSize(); i++) {
      sb.append(array.get(i));
      if (i != this.array.getSize() - 1) {
        sb.append(",");
      }
    }
    return "ArrayQueue{" +
        "Queue= front [" + sb.toString() +
        "] tail}";
  }
}

基于静态数组的循环队列

循环机制:在添加元素时将循环增量的值与数组长度取余就会自动循环,比如:

String[] arr = new String[3]; //只能承载三个元素的数组
for (int i = 2; i < 4; i++) {
  arr[i % arr.length] = "i=" + i;  //将循环增量与数组长度取余
}
System.out.println(Arrays.toString(arr)); //[i=3, null, i=2]
//或,这两种方式的区别在于循环增量一个是一直递增,和一个是永远在一定范围内
String[] arr = new String[3]; //只能承载三个元素的数组
for (int i = 2; i != 1; i = (i + 1) % arr.length) {  //将循环增量与数组长度取余
  arr[i] = "i=" + i;
}
System.out.println(Arrays.toString(arr)); //[i=0, null, i=2]

要知道基于数组的普通队列中出队操作是从数组头部删除元素,就需要遍历所有元素将元素向前移动,导致出队效率极低,若在删除队头元素不进行所有元素的移动,其实数组中元素的位置的前后关系还是那样,根本没有必要移动所有元素的位置也可以,这时只需要将当前队头和队尾是谁告诉用户即可(当队头和队尾指向相同时说明队列为空),此时前面元素出队会导致空间浪费,所以采用循环插入机制来避免,这就是循环队列

循环队列又出现了新问题,就是当队列满时首位指向相同,同时当队列为空时首位指向也相同,所以需要让队列永远不要填满整个数组,而是预留出一个位置,当还有一个位置时,就认为队列已经满了,这样就可以避免队满和队空判断条件一致,这时由于基于的是静态数组,所以扩容缩容操作要自己实现(记着改变头尾指针和修改指针的顺序)

  • 若队头和队尾指向相同时,队列为空
  • 若队尾加1和队尾指向相同时,队列为满,但是要注意若此时队尾加1需要循环带到队头,所以需要与数组长度取余在进行等值判断
/**
 * @ClassName: LoopQueue
 * @Description: 基于自定义数组实现的自定义队列
 */
public class LoopQueue<E> implements Queue<E> {
  private E[] data; //存储队列元素的数组,和数组的容量使用data.length-1表示,因为要空一个位置
  private int front, tail; //队头指针和队尾指针

  /**
   * @MethodName: LoopQueue
   * @Description: 根据用户需求创建指定大小容量的队列
   * @Param capacity: 用户指定的容量
   */
  public LoopQueue(int capacity) {
    this.data = (E[]) new Object[capacity + 1]; //由于要预留一个空间,所以此操作需要对用户屏蔽
    this.front = 0;  //初始头指针指向0
    this.tail = 0;  //初始尾指针指向0
  }

  /**
   * @MethodName: LoopQueue
   * @Description: 为用户设置默认的容量大小,为10
   */
  public LoopQueue() {
    this(10);
  }

  /**
   * @MethodName: enqueue
   * @Description: 入队操作实现
   * @Param element: 入队元素
   * @Return void
   */
  @Override
  public void enqueue(E element) {
    if ((this.tail + 1) % this.data.length == this.front) {
      //若队列为满(有可能尾指针要循环所以要取余),则扩容2倍
      resize((this.data.length - 1) * 2);
    }
    this.data[this.tail] = element; //将元素入队
    this.tail = (this.tail + 1) % this.data.length; //头指针向后循环移动
  }

  /**
   * @MethodName: dequeue
   * @Description: 出队操作实现
   * @Return E
   */
  @Override
  public E dequeue() {
    if (this.isEmpty()) { //若队列为空则抛出异常
      throw new IllegalArgumentException("Dequeue failed, Queue is empty");
    }
    E element = this.data[this.front]; //将出队元素暂存
    this.data[this.front] = null;  //同样需要将位置职位空,这样垃圾回收机制会更快回收
    this.front = (this.front + 1) % this.data.length; //头指针循环移动
    if ((this.data.length - 1) / 2 == this.getSize() && (this.data.length - 1) / 2 != 0) {
      //若当前数组容量过大就缩容一半
      this.resize((this.data.length - 1) / 2);
    }
    return element; //返回出队元素
  }

  /**
   * @MethodName: getFront
   * @Description: 查看队头元素操作实现
   * @Return E
   */
  @Override
  public E getFront() {
    return this.data[this.front];  //返回队头元素
  }

  /**
   * @MethodName: getSize
   * @Description: 查看队列中元素个数操作实现
   * @Return int
   */
  @Override
  public int getSize() {
    return this.tail >= this.front ? //如果头指针在前,尾指针在后
        this.tail - this.front : //这之间的元素是队列中元素
        this.data.length - (this.front - this.tail); //否则两边元素是队列元素
  }

  /**
   * @MethodName: isEmpty
   * @Description: 查看队列是否为空操作实现
   * @Return boolean
   */
  @Override
  public boolean isEmpty() {
    return this.front == this.tail;
  }

  /**
   * @MethodName: resize
   * @Description: 将data扩容指定大小
   * @Param capacity: 指定的大小
   * @Return void
   */
  private void resize(int capacity) {
    E[] newData = (E[]) new Object[capacity + 1]; //创建新数组,需要让该数组容量多一个,因为要留出空位
    for (int i = 0; i < this.getSize(); i++) {
      //为了从0开始遍历整个队列,所以采用第一种循环机制的方式
      //这样整个队列元素就会靠数组头部对齐
      newData[i] = this.data[(this.front + i) % this.data.length];
    }
    this.tail = this.getSize(); //修改尾指针
    this.front = 0;  //修改头指针
    // 注意这两个的顺序,由于getSize()方法中要使用头指针进行计算,若提前修改头指针会导致错误
    this.data = newData;  //更换容器数组
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (int i = this.front; i != this.tail; i = (i + 1) % this.data.length) {
      //从队列头遍历到尾部,采用第一种循环机制
      sb.append(this.data[i]);
      if (i != this.tail - 1) { //若当前尾指针的前一次与当前头指针的i重合时为最后一个元素
        sb.append(",");
      }
    }
    return "LoopQueue{" +
        "capacity=" + (this.data.length - 1) +
        ",Queue= front [" + sb +
        "] tail}";
  }
}

基于单向链表的队列

由于队列是对头部删除和对尾部删除的操作,而链表仅对头部操作效率较高,若也为尾部也增加一个指针,此时对尾部操作也就不再需要遍历整个链表了,但是就不能在复用之前的链表了,要从新实现一个带有尾指针的链表,新实现的链表能完成队列的操作即可,无需多余的实现其他功能

但是要注意的是,只是添加尾部指针仅仅针对添加操作,因为删除操作是要找到要删除的节点的前一个节点,由于单向链表中并没有记录前一个节点的指针,所以无法达到删除尾节点的目的;对于队列来说,能够添加节点就足够了,因为只是一侧添加(入队)一侧删除(出队),将链表尾部当做队列头部即可

/**
 * @ClassName: LinkedListQueue
 * @Description: 基于单向链表的自定义队列,由于在单向链表中增加了一个尾指针,所以要从新实现一下链表
 */
public class LinkedListQueue<E> implements Queue<E> {
  private class Node { //私有的内部类,不希望暴漏给外部使用,使用该类创建节点对象并进行链表的组装
    private E data; //存储节点数据
    private Node next;  //存储下一个节点

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,指定数据和下一个节点
     * @Param data: 数据
     * @Param next: 下一个节点
     */
    public Node(E data, Node next) {
      this.data = data;
      this.next = next;
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,只指定数据,下一个节点为null,即最后一个节点
     * @Param data:
     */
    public Node(E data) {
      this(data, null);
    }

    /**
     * @MethodName: Node
     * @Description: 创建节点对象,此时数据和下一个节点都是null,即最后一个节点并且数据也是null
     */
    public Node() {
      this(null, null);
    }

    @Override
    public String toString() {
      return data.toString(); //只打印数据,屏蔽掉节点指针
    }
  }

  private Node head, tail;  //头尾节点指针
  // 队头指针是tail,队尾指针是head,是反的
  // 队头指针指向链表尾部,而队尾指针指向链表头部
  private int size; //链表长度

  /**
   * @MethodName: LinkedListQueue
   * @Description: 创建一个队列
   */
  public LinkedListQueue() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }

  /**
   * @MethodName: enqueue
   * @Description: 入队操作实现
   * @Param element: 入队元素
   * @Return void
   */
  @Override
  public void enqueue(E element) {
    if (this.tail == null) {  //若当前队头没有节点
      this.tail = new Node(element);  //创建节点,并使得队头指针指向当前节点
      this.head = this.tail;  //只有一个节点时,队头尾指针应该指向相同
    } else {  //若当前队列有节点
      this.tail.next = new Node(element); //当前队头元素的next指向新节点,这样队头就一个是新节点
      this.tail = this.tail.next; //新节点已经插入到链表,所以要改变队头指针指向新节点
    }
    this.size++;  //入队后队列中的元素加1
  }

  /**
   * @MethodName: dequeue
   * @Description: 出队操作实现
   * @Return E
   */
  @Override
  public E dequeue() {
    if (this.isEmpty()) { //若当前队列为空,出队抛出异常
      throw new IllegalArgumentException("Dequeue failed, Queue is empty");
    }
    Node delNode = this.head; //暂存当前要删除的队尾节点
    this.head = this.head.next; //改变队尾指针为下一个节点,即跳过要删除的节点
    //若是最后一个节点时,队尾指针自动会指向null,但是队头指针不会,所以需要判断一下,这里是一个坑
    delNode.next = null;  //将要删除节点与链表脱离,方便垃圾回收机制回收
    if (this.head == null) {  //若删除的节点是最后一个节点时
      this.tail = null; //队首尾指针应该都指向空
    }
    this.size--;  //出队后队列中的元素减1
    return delNode.data;  //返回删除的节点数据
  }

  /**
   * @MethodName: getFront
   * @Description: 查看队头元素操作实现
   * @Return E
   */
  @Override
  public E getFront() {
    if (head == null) {
      throw new IllegalArgumentException("Get failed, Queue is empty");
    }
    return this.tail.data;
  }

  /**
   * @MethodName: getSize
   * @Description: 查看队列中元素个数操作实现
   * @Return int
   */
  @Override
  public int getSize() {
    return this.size;
  }

  /**
   * @MethodName: isEmpty
   * @Description: 查看队列是否为空操作实现
   * @Return boolean
   */
  @Override
  public boolean isEmpty() {
    return this.size == 0;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    Node cur = this.head; //存储当前节点的指针,从队尾(链表头)开始遍历
    while (cur != null) { //若当前节点已经为null,说明当前节点指针已经指到了末尾
      sb.append(cur.data + ",");
      cur = cur.next; //当前节点指针后移
    }
    if (sb.length() > 0) {  //若队列中有元素
      sb.deleteCharAt(sb.length() - 1); //将最后那个多余的逗号删除
    }
    return "LinkedList{" +
        "Queue=front [" + sb +
        "] tail}";
  }
}

基于堆的优先队列

与普通队列入对是相同的,出队时选择优先级较高(较大或较小)的元素先出队,而不是先进先出,若基于线性结构来实现优先队列,则需要在入队时对元素排序或出队时扫描每个元素找出最大值,时间复杂度较高

堆的堆顶就是全部元素中最大(最小)的元素,在添加和删除时无需将所有元素都遍历一遍,所以使用堆来实现优先队列时间复杂度较低

/**
 * @ClassName: PriorityQueue
 * @Description: 基于堆实现的优先队列
 */
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
  private Heap<E> heap;

  /**
   * @MethodName: PriorityQueue
   * @Description: 为用户设置默认的容量大小,为10
   */
  public PriorityQueue() {
    this.heap = new Heap<>(10);
  }

  /**
   * @MethodName: PriorityQueue
   * @Description: 根据用户需求创建指定大小容量的队列
   * @Param capacity: 用户指定的容量
   */
  public PriorityQueue(int capacity) {
    this.heap = new Heap<>(capacity);
  }

  /**
   * @MethodName: enqueue
   * @Description: 入队操作实现
   * @Param element: 入队元素
   * @Return void
   */
  @Override
  public void enqueue(E element) {
    this.heap.add(element);
  }

  /**
   * @MethodName: dequeue
   * @Description: 出队操作实现
   * @Return E
   */
  @Override
  public E dequeue() {
    return this.heap.remove();
  }

  /**
   * @MethodName: getFront
   * @Description: 查看队头元素操作实现
   * @Return E
   */
  @Override
  public E getFront() {
    return this.heap.peek();
  }

  /**
   * @MethodName: getSize
   * @Description: 查看队列中元素个数操作实现
   * @Return int
   */
  @Override
  public int getSize() {
    return this.heap.getSize();
  }

  /**
   * @MethodName: isEmpty
   * @Description: 查看队列是否为空操作实现
   * @Return boolean
   */
  @Override
  public boolean isEmpty() {
    return this.heap.isEmpty();
  }
}
此作者没有提供个人介绍
最后更新于 2020-04-11