数据结构篇——队列扩展(环形链表解决约瑟夫问题)

上一篇我们详细的讲了包,栈,队列的高性能实现方案。本篇博客,我们一起来讨论一下队列的扩展应用。

循环队列

顾名思义,循环队列就是一个首尾相连的队列。我们可以在写之前总结出它的一些特点。

  1. 根据首尾相连,则在逻辑上必有两个指针分别指向,且尾的下一个就是头;
  2. 根据首位相连,则该队列必定是长度固定的;
  3. 根据我们前篇博客,链表相较于数组在队列的实现上优势较为明显。如果使用链表来实现,则在这里链表也变成了循环链表,也可称作是
    大概的特点就总结出这么多,在实际动手的时候应该会有更多的细节。

CircleQueue

public class CircleQueue<T> implements Iterable<T> {

    private Node first;

    private Node last;

    private int size;

    /**
     * 队列的总长度
     */
    private int maxSize;

    CircleQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    /**
     * 循环队列的enqueue
     * 一直移动last
     * 然后要时刻保证last.next = first
     * 当队列放满了 如果要继续添加就要往下移动一个first 再增加last 相当于覆盖了第一个节点
     */
    public void enqueue(T value) {
        boolean ifAddSize = false;
        if (size + 1 <= maxSize) {
            ifAddSize = true;
        }
        Node tempNode = new Node(value);
        if (isEmpty()) {
            first = tempNode;
            last = tempNode;
            first.next = last;
            size++;
            return;
        }
        if (isFull()) {
            first = first.next;
        }
        last.next = tempNode;
        last = tempNode;
        last.next = first;
        if (ifAddSize) {
            size++;
        }
    }

    private boolean isEmpty() {
        return first == null;
    }

    private boolean isFull() {
        return size == maxSize;
    }

    public T dequeue() {
        //如果队列空了则返回null
        if (isEmpty()) {
            System.out.println("队列空了");
            return null;
        }

        T topValue = first.item;
        //如果只剩一个了 就全部变成null
        if (first == last) {
            first = last = null;
        } else {
            first = first.next;
            last.next = first;
        }
        size--;
        return topValue;
    }

    public int size() {
        return this.size;
    }

    @Override
    public Iterator<T> iterator() {
        return new QueueIterator();
    }

    class QueueIterator implements Iterator<T> {

        Node current = first;

        int currentSize;

        @Override
        public boolean hasNext() {
            return currentSize < maxSize;
        }

        @Override
        public T next() {
            T tempItem = current.item;
            current = current.next;
            currentSize++;
            return tempItem;
        }
    }

    class Node {
        Node(T item) {
            this.item = item;
        }

        Node next;
        T item;
    }

    /**
     * 用于测试泛型的自定义类
     */
    private static class Fruit {
        public Fruit(String name, Double price) {
            this.name = name;
            this.price = price;
        }

        String name;

        Double price;
    }

    public static void main(String[] args) {
        CircleQueue<Fruit> fruitQueue = new CircleQueue<>(3);
        Fruit apple = new Fruit("苹果", 5.1);
        Fruit grape = new Fruit("葡萄", 6.7);
        Fruit orange = new Fruit("橘子", 3.2);
        fruitQueue.enqueue(apple);
        fruitQueue.enqueue(grape);
        fruitQueue.enqueue(grape);
        fruitQueue.enqueue(orange);
        fruitQueue.enqueue(orange);
        fruitQueue.enqueue(orange);
        int size = fruitQueue.size();
        System.out.println(size);
        for (Fruit fruit : fruitQueue) {
            System.out.println(fruit.name + " " + fruit.price);
        }
        System.out.println("开始dequeue数据");
        for (int i = 0; i < size; i++) {
            System.out.println(fruitQueue.dequeue().name);
        }
        //最后一次检查有没有空
        System.out.println(fruitQueue.dequeue());
    }
}

该有的注释都已经写在代码中了。其实与一般队列的差一点主要在于

  1. enqueue()的时候,如果超过最大长度,则会发生覆盖。被覆盖的数据就从队列头部开始。
  2. 迭代器的hasNext()方法,需要根据当前长度与maxSize进行比较

循环队列的应用——约瑟夫问题

约瑟夫问题简介

约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。

——摘自百度百科

通俗的讲,就是一个环形队列。从first开始,每间隔M个,就从队列中dequeue出来。直到size()==1为止。

单链表解决方法

  /**
     * 解决约瑟夫问题
     *
     * @param interval 间隔
     */
    public void solveJosephusProblem(int interval) {
        Node temp = first;
        //单项链表删除节点需要辅助节点 要删除节点的前一个节点
        Node pre = first;
        while (temp.next != temp) {
            int i = 1;
            while (i <= interval) {
                pre = temp;
                temp = temp.next;
                i++;
            }
            System.out.print(temp.item + " ");
            pre.next = temp.next;
            temp = pre.next;
        }
        System.out.println();
        System.out.println("最后留在队列中的是:" + temp.item);
    }

测试代码:

   public static void main(String[] args) {
        CircleQueue<Integer> josephusCircle = new CircleQueue<>(6);
        for (int i = 1; i <= 6; i++) {
            josephusCircle.enqueue(i);
        }
        //第5个被干掉 则说明是间隔4个
        josephusCircle.solveJosephusProblem(4);
    }

最后输出结果为:
5 4 6 2 3
最后留在队列中的是:1

双向链表实现环形队列

约瑟夫问题涉及到了队列的删除。从上面单向链表的实现中,我们发现删除一个节点必须要辅助节点的参与。其实解决删除更好的方式是双向链表,能够自己把自己从链表中删除。

/**
 * 双向链表实现循环队列
 */
public class DoubleLinkedListCircleQueue<T> implements Iterable<T> {

    public DoubleLinkedListCircleQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    private Node first;

    private Node last;

    private int size;

    private int maxSize;

    /**
     * 双向链表需要额外维护一个pre指针
     */
    public void enqueue(T value) {
        Node tempNode = new Node(value);
        if (isEmpty()) {
            first = tempNode;
            last = tempNode;
            first.next = last;
            first.pre = last;
            size++;
            return;
        }
        boolean ifAddSize = true;
        if (isFull()) {
            //先把当前节点指向下一个节点 再删除当前节点的上一个节点(也就是删除自己)
            first = first.next;
            dequeue(first.pre);
            ifAddSize = false;
        }
        last.next = tempNode;
        tempNode.pre = last;
        last = tempNode;
        last.next = first;
        first.pre = last;
        if (ifAddSize) {
            size++;
        }
    }

    /**
     * 删除first
     */
    public T dequeue() {
        //如果队列空了则返回null
        if (isEmpty()) {
            System.out.println("队列空了");
            return null;
        }

        T topValue = first.item;
        //如果只剩一个了 就全部变成null
        if (first == last) {
            first = last = null;
        } else {
            first = first.next;
            first.pre = last;
            last.next = first;
        }
        size--;
        return topValue;
    }

    /**
     * 解决约瑟夫问题
     *
     * @param interval 间隔
     */
    public void solveJosephusProblem(int interval) {
        Node temp = first;
        while (temp.next != temp) {
            int i = 1;
            while (i <= interval) {
                temp = temp.next;
                i++;
            }
            System.out.print(temp.item + " ");
            //先把当前节点指向下一个节点 再删除当前节点的上一个节点(也就是删除自己)
            temp = temp.next;
            dequeue(temp.pre);
        }
        System.out.println();
        System.out.println("最后留在队列中的是:" + temp.item);
    }


    /**
     * 删除链表中的指定节点
     */
    private T dequeue(Node aimNode) {
        T result = aimNode.item;
        aimNode.next.pre = aimNode.pre;
        aimNode.pre.next = aimNode.next;
        return result;
    }

    public boolean isEmpty() {
        return first == null;
    }

    public boolean isFull() {
        return size == maxSize;
    }

    public int size() {
        return this.size;
    }


    @Override
    public Iterator<T> iterator() {
        return new DoubleLinkedListCircleQueueIterator();
    }


    private class DoubleLinkedListCircleQueueIterator implements Iterator<T> {

        private int currentSize;

        Node current = first;

        @Override
        public boolean hasNext() {
            return currentSize < size;
        }

        @Override
        public T next() {
            T tempItem = current.item;
            current = current.next;
            currentSize++;
            return tempItem;
        }
    }

    private class Node {

        public Node(T item) {
            this.item = item;
        }

        Node pre;

        Node next;

        T item;
    }

    /**
     * 用于测试泛型的自定义类
     */
    private static class Fruit {
        public Fruit(String name, Double price) {
            this.name = name;
            this.price = price;
        }

        String name;

        Double price;
    }

    public static void main(String[] args) {
        DoubleLinkedListCircleQueue<Fruit> fruitQueue = new DoubleLinkedListCircleQueue<>(3);
        Fruit apple = new Fruit("苹果", 5.1);
        Fruit grape = new Fruit("葡萄", 6.7);
        Fruit orange = new Fruit("橘子", 3.2);
        fruitQueue.enqueue(apple);
        fruitQueue.enqueue(grape);
        fruitQueue.enqueue(grape);
        fruitQueue.enqueue(orange);
        fruitQueue.enqueue(orange);
        fruitQueue.enqueue(orange);
        int size = fruitQueue.size();
        System.out.println(size);
        for (Fruit fruit : fruitQueue) {
            System.out.println(fruit.name + " " + fruit.price);
        }
        System.out.println("开始dequeue数据");
        for (int i = 0; i < size; i++) {
            System.out.println(fruitQueue.dequeue().name);
        }
        //最后一次检查有没有空
        System.out.println(fruitQueue.dequeue());

        DoubleLinkedListCircleQueue<Integer> josephusCircle = new DoubleLinkedListCircleQueue<>(6);
        for (int i = 1; i <= 6; i++) {
            josephusCircle.enqueue(i);
        }
        //第5个被干掉 则说明是间隔4个
        josephusCircle.solveJosephusProblem(4);
    }
}

相比於单项链表,双向链表只是多了一个pre指针需要维护。但是带来的却是删除的直接方便。

总结

  1. 环形链表需要注意时刻维护last指针与first指针的关系,不能破坏环状结构;
  2. 当添加的节点超过maxSize大小时,会发生覆盖情况(从逻辑队列头开始覆盖,逻辑上导致first后移);
  3. 在解决约瑟夫问题时,需要添加删除节点的方法。此时使用双向链表实现的队列会更加方便。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章