大话数据结构 - 队列

代码GitHub地址

队列

  • 队列和栈一样是特殊的线性表。区别只是它能尾进头出而已
  • 学习队列需要清楚的认识到frontrear两指针什么情况下分别变动。

队列也分成两种:

  • 静态队列(数组实现)
  • 动态队列(链表实现)

需要注意一下:静态队列会遇到 “假溢出” 情况,即:当我们为了避免静态队列在进行元素删除操作性能差的时候(所有队列元素向前移位,来补数组删除元素后空出来的位置),我们通过移动front指针移动来定位新的队头元素,而不是先前的移动剩余节点来确定数组下标为0的位置为队头。直到front=rear的时候认为该队列为空。但是这种优化方式会出现假溢出的现象,即我们front一直向后移,而队尾rear已经到数组尾了。当再插入元素的时候该存到哪里呢,此时就会出现数组越界异常。而此时我们的数组从下标为0的地方到front的位置却是没有任何元素的。

解决办法:循环队列

解决思路:rear原先的处理方式是指向尾节点的下一位置。当出现上述情况的时候rear指向数组下标为0的位置。形成循环队列,这样rear就不会指向数组arrayLength位置外的位置了。

新问题:当rear=front时,可能有队满和队空两种情况。rear一直加元素向后走顶到front处(队满),和front一直删顶到rear处(队空)。

解决办法:添加元素的时候rear最多添加元素到front - 1的索引处。这样frontrear就不可能重合了。我们要熟记这个关系。front = rear即空队,满队即front + 1 = rear。后续有很多操作会用到

队列常用三个判断因式

  • 队满判断因式:(rear + 1) % QueueSize == front
  • 队空的判断因式:rear == front
  • 通用队长计算因式:(QueueSize - front + rear) % QueueSize

联想图进行理解很好理解。

静态队列

循环队列的实现由于是基于数组实现,所以我们没有链表中尾节点next指针指向头节点的操作。但是可以通过rear字段来记录尾节点下标来达到循环队列的目的。

数组实现的方式在进行元素删除时的操作都是不对元素处理,通过移动“记录变量”的指向来无视该被删除的值。即便他在数组空间中任然真实存在,但是已然不会被该数据结构的索引方式所索引到。

  • 需要注意的是静态队列我们一般都会做成循环队列,这样才能更好地使用内存空间规避掉“假溢出”的现象

静态队列的操作

队列

public class Queue {

    private static final int QUEUE_MAX_SIZE = 100;

    /**
     * 队头指针
     */
    public int front = 0;
    /**
     * 队尾指针
     */
    public int rear = 0;
    /**
     * 队列元素容器
     */
    public int[] queueData = new int[QUEUE_MAX_SIZE];
}

返回循环队列的队长

/**
 * 返回循环队列的队长
 *
 * @param queue
 * @return
 */
public static int queueLength(Queue queue) {
    // |   | rear  |   |   | front  |   |
    return (QUEUE_MAX_SIZE - queue.front + queue.rear) % QUEUE_MAX_SIZE;
}

只要记住这个逻辑图或按公式来,根据% QUEUE_MAX_SIZE来适配全部情况即可。

循环队列入队

/**
 * 循环队列入队
 *
 * @param queue
 * @param value
 * @return
 */
public static int insertQueue(Queue queue, int value) {
    if ((queue.rear + 1) % QUEUE_MAX_SIZE == queue.front) {
        return 0;
    }
    queue.queueData[queue.rear] = value;
     /*这里需要注意`queue.rear++`后要对总数QUEUE_MAX_SIZE取余
      这样才满足了循环队列的情况 否则可能会出现数组越界的异常*/
    queue.rear = queue.rear++ % QUEUE_MAX_SIZE;
    return 1;
}

这里也是根据那个逻辑图想逻辑就可,也可以直接套公式。

循环队列出队

/**
 * 循环队列出队
 *
 * @param queue
 * @return
 */
public static int removeQueue(Queue queue) {
    if (queue.front == queue.rear) {
        return 0;
    }
    // 不处理,移指针来规避
    queue.front = queue.front++ % QUEUE_MAX_SIZE;
    return 1;
}

需要特别提醒的一点就是我们在任何数组实现的数据结构中,在底层删除数据的时候的处理都是不做处理,只是移动“标识指针”,进而达到审查不到该元素的目的。其实最好的处理方式当然是根据数组的元素类型来直接置空或者为0来释放对象。

遍历循环队列

/**
 * 遍历循环队列
 *
 * @param queue
 * @return
 */
public static void traverse(Queue_ queue) {
    int index = queue.front;
    // 这里需要注意 rear 指向的是队尾节点的下一位置,而非队尾的位置,所以 rear 处不输出
    while (index != queue.rear) {
        int value = queue.queueData[index];
        System.out.println(value);

        index = index++ % queue.queueData.length;
    }
}

这里需要注意rear指向的是队尾节点的下一位置,而非队尾的位置,所以 rear处不输出

判断该队列是否为空

/**
 * 判断该队列是否为空
 *
 * @param queue
 * @return
 */
public static int isEmpty(Queue_ queue) {
    if (queue.front == queue.rear) {
        return 1;
    }
    return 0;
}

只要清楚我们已经让rear指向了队尾后一位的位置,以及当front=rear时队为空即可。

链式队列

链队列根本上还是链表。只是玩的是“节点标识“的把戏,并且被规定节点只能从队尾进队首出。我们凭借标识节点来确定这个数据结构

链式队列的操作

队列

public class Queue_ {

    /**
     * 队头指针
     */
    public Node front;
    /**
     * 队尾指针
     */
    public Node rear;

}

链队列元素入队

/**
 * 链队列元素入队
 *
 * @param queue
 * @param value
 * @return
 */
public static int insert(Queue_ queue, int value) {
    Node node = new Node(value, null);
    queue.rear.nextNode = node;
    queue.rear = node;
    return 1;
}

链队列不存在数据越界的情况,给rear尾节点加值即可。然后记得再移动尾节点的指针

链队列的出队

/**
 * 链队列的出队
 *
 * @param queue
 * @return
 */
public static int remove(Queue_ queue) {
    // 空队
    if (queue.front == queue.rear) {
        return 0;
    }
    Node firstNode = queue.front.nextNode;
    // 只剩一个节点的时候 
    if (firstNode.nextNode == null && queue.rear == firstNode) {
        queue.front.nextNode = null;
        // 队尾指针指向虚拟头结点
        queue.rear = queue.front;
    }
    queue.front.nextNode = firstNode.nextNode;
    return 1;
}

删除节点的时候需要判断一下,边界条件。考虑当该队列为空队的情况,以及队中只剩一个元素时rearfront的指向。因为front一般指向头结点而非队头节点,所以在链队列中front不会动,只是在删除元素时不断的变更front.nextNode属性,而不会去动rear。只剩一个元素的特殊情况时,我们就需要把rear考虑进来了。由于rear指向的是队尾元素,删除掉最后一个元素之后就不存在了,所以它只能到front队头的位置。而front.nextNode = null

    原文作者:HikariCP
    原文地址: https://www.jianshu.com/p/f4a27bbc57f0
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞