队列(Queue)的实现及分析

目录

1.队列的介绍:

2.常用关于队列的操作:

 3.顺序队列的设计及实现(数组实现):

不使用顺序循环队列:

使用顺序循环队列:

4.链式队列的设计与分析:

4.1链表(Linked List)介绍:

4.2 单链表的应用实例

4.3 双向链表应用实例

4.4链式队列: 

5.优先队列的设置与实现(双链表实现)

6.队列应用的举例:

 🔺Josephu 问题:

7.解题时需要注意的点:


1.队列的介绍:

1) 队列是一个有序列表,可以用数组或是链表来实现。

2) 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出。

3) 我们把向队列中插入元素的过程称为入队(Enqueue),删除元素的过程称为出队(Dequeue)并把允许入队的一端称为队尾,允许出的的一端称为队头,没有任何元素的队列则称为空队。其一般结构如下:

2.常用关于队列的操作:

package com.zejian.structures.Queue;

/**
* 队列抽象数据类型
*/
public interface Queue<T> {

   /**
    * 返回队列长度
    * @return
    */
   int size();

   /**
    * 判断队列是否为空
    * @return
    */
   boolean isEmpty();

   /**
    * data 入队,添加成功返回true,否则返回false,可扩容
    * @param data
    * @return
    */
   boolean add(T data);

   /**
    * offer 方法可插入一个元素,这与add 方法不同,
    * 该方法只能通过抛出未经检查的异常使添加元素失败。
    * 而不是出现异常的情况,例如在容量固定(有界)的队列中
    * NullPointerException:data==null时抛出
    * @param data
    * @return
    */
   boolean offer(T data);

   /**
    * 返回队头元素,不执行删除操作,若队列为空,返回null
    * @return
    */
   T peek();

   /**
    * 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
    * @return
    */
   T element();

   /**
    * 出队,执行删除操作,返回队头元素,若队列为空,返回null
    * @return
    */
   T poll();

   /**
    * 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
    * @return
    */
   T remove();

   /**
    * 清空队列
    */
   void clearQueue();
}

 3.顺序队列的设计及实现(数组实现):

不使用顺序循环队列:

🔺在顺序表中添加一个头指向下标front和尾指向下标,出队和入队时只要改变front、rear的下标指向取值即可,此时无需移动元素,因此出队的时间复杂度也就变为O(1)。其过程如下图所示。

🔺从图的演示过程:

(a)操作时,是空队列此时front和rear都为-1,同时可以发现虽然我们通过给顺序表添加front和rear变量记录下标后使用得出队操作的时间复杂度降为O(1),但是却出现了另外一个严重的问题,那就是空间浪费,从图中的(d)和(e)操作可以发现,20和30出队后,遗留下来的空间并没有被重新利用,反而是空着,所以导致执行(f)操作时,出现队列已满的假现象,这种假现象我们称之为假溢出,之所以出现这样假溢出的现象是因为顺序表队列的存储单元没有重复利用机制,而解决该问题的最合适的方式就是将顺序队列设计为循环结构

使用顺序循环队列:

顺序循环队列就是将顺序队列设计为在逻辑结构上收尾相接的循环结构,这样我们就可以重复利用存储单元:

简单分析一下:
其中采用循环结构的顺序表,可以循环利用存储单元,因此有如下计算关系(其中size为队列长度):

//使用取模运算,其中front、rear的下标的取值范围是0~size-1,不会造成假溢出,从而实现循环的效果。
front=(front+1)%size;//队头下标
rear=(rear+1)%size; 

1.front为队头元素的下标,rear则指向下一个入队元素的下标
2.当front=rear时,我们约定队列为空
3.出队操作改变front下标指向,入队操作改变rear下标指向,size代表队列容量。
4.约定队列满的条件为front=(rear+1)%size,注意此时队列中仍有一个空的位置(rear+1),此处留一个空位主要用于避免与队列空的条件front=rear相同
5.队列内部的数组可扩容,并按照原来队列的次序复制元素数组

★入队add方法的实现:

/**
  * data 入队,添加成功返回true,否则返回false,可扩容
  * @param data
  * @return
  */
 @Override
 public boolean add(T data) {
     //判断是否满队
     if (this.front==(this.rear+1)%this.elementData.length){
         ensureCapacity(elementData.length*2+1);
     }
     //添加data
     elementData[this.rear]=data;
     //更新rear指向下一个空元素的位置
     this.rear=(this.rear+1)%elementData.length;
     size++;
     return true;
 }

在add方法中我们先通过this.front==(this.rear+1)%this.elementData.length判断队列是否满,在前面我们约定过队列满的条件为front=(rear+1)%size,如果队列满,则先通过ensureCapacity(elementData.length*2+1)扩容

★该方法实现如下:

/**
  * 扩容的方法
  * @param capacity
  */
 public void ensureCapacity(int capacity) {
     //如果需要拓展的容量比现在数组的容量还小,则无需扩容
     if (capacity<size)
         return;

     T[] old = elementData;
     elementData= (T[]) new Object[capacity];
     int j=0;
     //复制元素
     for (int i=this.front; i!=this.rear ; i=(i+1)%old.length) {
         elementData[j++] = old[i];
     }
     //恢复front,rear指向
     this.front=0;
     this.rear=j;
 }

这个方法比较简单,主要创建一个新容量的数组,并把旧数组中的元素复制到新的数组中,这里唯一的要注意的是,判断久数组是否复制完成的条件为i!=this.rear,同时循环的自增条件为i=(i+1)%old.length。扩容后直接通过rear添加新元素,最后更新rear指向下一个入队新元素。

★对于出队操作poll的实现如下:

/**
 * 出队,执行删除操作,返回队头元素,若队列为空,返回null
 * @return
 */
@Override
public T poll() {
    T temp=this.elementData[this.front];
    this.front=(this.front+1)%this.elementData.length;
    size--;
    return temp;

总体代码实现:

package com.zejian.structures.Queue;

import java.io.Serializable;
import java.util.NoSuchElementException;

/**
 * 顺序队列的实现
 */
public class SeqQueue<T> implements Queue<T> ,Serializable {


    private static final long serialVersionUID = -1664818681270068094L;
    private static final int  DEFAULT_SIZE = 10;

    private T elementData[];

    private int front,rear;

    private int size;


    public SeqQueue(){
        elementData= (T[]) new Object[DEFAULT_SIZE];
        front=rear=0;
    }

    public SeqQueue(int capacity){
        elementData= (T[]) new Object[capacity];
        front=rear=0;
    }

    @Override
    public int size() {
//        LinkedList
        return size;
    }

    @Override
    public boolean isEmpty() {
        return front==rear;
    }

    /**
     * data 入队,添加成功返回true,否则返回false,可扩容
     * @param data
     * @return
     */
    @Override
    public boolean add(T data) {
        //判断是否满队
        if (this.front==(this.rear+1)%this.elementData.length){
            ensureCapacity(elementData.length*2+1);
        }
        //添加data
        elementData[this.rear]=data;
        //更新rear指向下一个空元素的位置
        this.rear=(this.rear+1)%elementData.length;
        size++;
        return true;
    }

    /**
     * offer 方法可插入一个元素,这与add 方法不同,
     * 该方法只能通过抛出未经检查的异常使添加元素失败。
     * 而不是出现异常的情况,例如在容量固定(有界)的队列中
     * NullPointerException:data==null时抛出
     * IllegalArgumentException:队满,使用该方法可以使Queue的容量固定
     * @param data
     * @return
     */
    @Override
    public boolean offer(T data) {
        if (data==null)
            throw new NullPointerException("The data can\'t be null");
        //队满抛出异常
        if (this.front==(this.rear+1)%this.elementData.length){
            throw new IllegalArgumentException("The capacity of SeqQueue has reached its maximum");
        }

        //添加data
        elementData[this.rear]=data;
        //更新rear指向下一个空元素的位置
        this.rear=(this.rear+1)%elementData.length;
        size++;

        return true;
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,返回null
     * @return
     */
    @Override
    public T peek() {
        return elementData[front];
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T element() {
        if(isEmpty()){
            throw new NoSuchElementException("The SeqQueue is empty");
        }
        return peek();
    }

    /**
     * 出队,执行删除操作,返回队头元素,若队列为空,返回null
     * @return
     */
    @Override
    public T poll() {
        T temp=this.elementData[this.front];
        this.front=(this.front+1)%this.elementData.length;
        size--;
        return temp;
    }

    /**
     * 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T remove() {
        if (isEmpty()){
            throw new NoSuchElementException("The SeqQueue is empty");
        }
        return poll();
    }

    @Override
    public void clearQueue() {
        for (int i=this.front; i!=this.rear ; i=(i+1)%elementData.length) {
            elementData[i] = null;
        }
        //复位
        this.front=this.rear=0;
        size=0;
    }

    /**
     * 扩容的方法
     * @param capacity
     */
    public void ensureCapacity(int capacity) {
        //如果需要拓展的容量比现在数组的容量还小,则无需扩容
        if (capacity<size)
            return;

        T[] old = elementData;
        elementData= (T[]) new Object[capacity];
        int j=0;
        //复制元素
        for (int i=this.front; i!=this.rear ; i=(i+1)%old.length) {
            elementData[j++] = old[i];
        }
        //恢复front,rear指向
        this.front=0;
        this.rear=j;
    }
}

4.链式队列的设计与分析:

4.1链表(Linked List)介绍:

链表是有序的列表,但是它在内存中是存储如下

1) 链表是以节点的方式来存储,是链式存储

2) 每个节点包含 data 域, next 域:指向下一个节点.

3) 如图:发现链表的各个节点不一定是连续存储.

4) 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定  

★单链表(带头结点) 逻辑结构示意图如下

4.2 单链表的应用实例

        使用带 head 头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作, 注: 删除和修改,查找 可以考虑学员独立完成,也可带学员完成

1) 第一种方法在添加英雄时,直接添加到链表的尾部 思路分析示意图:

2) 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示) 思路的分析示意图:

 3) 修改节点功能 思路

(1) 先找到该节点,通过遍历

(2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname

4) 删除节点 思路分析的示意图:

4.3 双向链表应用实例

4.3.1双向链表的操作分析和实现 使用带 head 头的双向链表实现 –水浒英雄排行榜

❤ 管理单向链表的缺点分析:

1) 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

2) 单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除 时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).

3) 分析了双向链表如何完成遍历,添加,修改和删除的思路 

对上图的说明: 分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现

1) 遍历 方和 单链表一样,只是可以向前,也可以向后查找

2) 添加 (默认添加到双向链表的最后)

(1) 先找到双向链表的最后这个节点

(2) temp.next = newHeroNode

(3) newHeroNode.pre = temp;

3) 修改 思路和 原来的单向链表一样.

4) 删除

(1) 因为是双向链表,因此,我们可以实现自我删除某个节点

(2) 直接找到要删除的这个节点,比如 temp

(3) temp.pre.next = temp.next

(4) temp.next.pre = temp.pre; 

4.4链式队列: 

对于链式队列,将使用带头指针front和尾指针rear的单链表实现,front直接指向队头的第一个元素,rear指向队尾的最后一个元素,其结构如下:

        之所以选择单链表(带头尾指针)而不采用循环双链表或者双链表主要是双链表的空间开销(空间复杂度,多前继指针)相对单链表来说了不少,而单链表只要新增头指针和尾指针就可以轻松实现常数时间内(时间复杂度为O(1))访问头尾结点。下面我们来看看如何设计链式队列:

1.以上述的图为例分别设置front和rear指向队头结点和队尾结点,使用单链表的头尾访问时间复杂度为O(1)。
2.设置初始化空队列,使用front=rear=null,并且约定条件front==null&&rear==null成立时,队列为空。
3.出队操作时,若队列不为空获取队头结点元素,并删除队头结点元素,更新front指针的指向为front=front.next
4.入队操作时,使插入元素的结点在rear之后并更新rear指针指向新插入元素。
当第一个元素入队或者最后一个元素出队时,同时更新front指针和rear指针的指向。
这一系列过程如下图所示:

链式队列

★具体代码实现:

package com.zejian.structures.Queue;

import com.zejian.structures.LinkedList.singleLinked.Node;

import java.io.Serializable;
import java.util.*;

/**
 * 链式队列的实现
 */
public class LinkedQueue<T> implements Queue<T> ,Serializable{
    private static final long serialVersionUID = 1406881264853111039L;
    /**
     * 指向队头和队尾的结点
     * front==null&&rear==null时,队列为空
     */
    private Node<T> front,rear;

    private int size;
    /**
     * 用于控制最大容量,默认128,offer方法使用
     */
    private int maxSize=128;

    public LinkedQueue(){
        //初始化队列
        this.front=this.rear=null;
    }

    @Override
    public int size() {
        return size;
    }

    public void setMaxSize(int maxSize){
        this.maxSize=maxSize;
    }

    @Override
    public boolean isEmpty() {
        return front==null&&rear==null;
    }

    /**
     * data 入队,添加成功返回true,否则返回false,可扩容
     * @param data
     * @return
     */
    @Override
    public boolean add(T data) {
        Node<T> q=new Node<>(data,null);
        if (this.front==null) {//空队列插入
            this.front = q;
        } else {//非空队列,尾部插入
            this.rear.next=q;
        }
        this.rear=q;
        size++;
        return true;
    }

    /**
     * offer 方法可插入一个元素,这与add 方法不同,
     * 该方法只能通过抛出未经检查的异常使添加元素失败。
     * 而不是出现异常的情况,例如在容量固定(有界)的队列中
     * NullPointerException:data==null时抛出
     * IllegalArgumentException:队满,使用该方法可以使Queue的容量固定
     * @param data
     * @return
     */
    @Override
    public boolean offer(T data) {
        if (data==null)
            throw new NullPointerException("The data can\'t be null");
        if (size>=maxSize)
            throw new IllegalArgumentException("The capacity of LinkedQueue has reached its maxSize:128");

        Node<T> q=new Node<>(data,null);
        if (this.front==null) {//空队列插入
            this.front = q;
        } else {//非空队列,尾部插入
            this.rear.next=q;
        }
        this.rear=q;
        size++;
        return false;
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,返回null
     * @return
     */
    @Override
    public T peek() {
        return this.isEmpty()? null:this.front.data;
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T element() {
        if(isEmpty()){
            throw new NoSuchElementException("The LinkedQueue is empty");
        }
        return this.front.data;
    }

    /**
     * 出队,执行删除操作,返回队头元素,若队列为空,返回null
     * @return
     */
    @Override
    public T poll() {
        if (this.isEmpty())
            return null;
        T x=this.front.data;
        this.front=this.front.next;
        if (this.front==null)
            this.rear=null;
        size--;
        return x;
    }

    /**
     * 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T remove() {
        if (isEmpty()){
            throw new NoSuchElementException("The LinkedQueue is empty");
        }
        T x=this.front.data;
        this.front=this.front.next;
        if (this.front==null)
            this.rear=null;
        size--;
        return x;
    }

    @Override
    public void clearQueue() {
        this.front= this.rear=null;
        size=0;
    }
}

5.优先队列的设置与实现(双链表实现)

仅了解

        在某些情况下,有些应用系统要求不仅需要按照“先来先服务”的原则进行,而且还需按照任务的重要或紧急程度进行排队处理,此时就需要使用到优先队列。比如在操作系统中进行进程调度管理,每个进程都具备一个优先级值以表示进程的紧急程度,优先级高的进行先执行,同等级进程按照先进先出的原则排队处理,此时操作系统使用的便是优先队列管理和调度进程。
  ★优先级队列也是一种特殊的数据结构,队列中的每个元素都
有一个优先级,若每次出队的是具有最高优先级的元素,则称为降序优先级队列(总是先删除最大的元素)。若每次出队的是值最小的元素,则称为升序优先级队列(总是先删除最小的元素),通常情况下我们所说的优先队列,一般是指降序优先级队列。关于优先队列的实现,可以使用有序数组或者有序链表,也可以使用二叉树(二叉堆)实现,这里我们仅给出有序链表的简单实现方案。而二叉树的实现,留着后面我们分析完树时再给出。好~,这里使用之前分析过的MyLikedList作为基底,实现一个排序的SortLinkedList继承自MyLinkedList,这里需要注意的是排序链表中的T类型必须是实现了Comparable接口的类型,在SortLinkedList中主要重写添加的add方法,插入逻辑是,通过比较元素的大小加入,而非简单下标或尾部插入,其实现如下:
 

package com.zejian.structures.LinkedList.MyCollection;

import java.io.Serializable;
import java.util.Iterator;
import java.util.ListIterator;

/**
 * 排序list的简单实现
 */
public class SortMyLinkedList<T extends Comparable<? extends T>> extends MylinkeList<T> implements Serializable {

    private static final long serialVersionUID = -4783131709270334156L;

    @Override
    public boolean add(T data) {
        if(data==null)
            throw new NullPointerException("data can\'t be null");

        Comparable cmp =data;//这里需要转一下类型,否则idea编辑器上检验不通过.

        if(this.isEmpty() || cmp.compareTo(this.last.prev.data) > 0){
             return super.add(data);//直接尾部添加,last不带数据的尾结点
        }

        Node<T> p=this.first.next;
        //查找插入点
        while (p!=null&&cmp.compareTo(p.data)>0)
            p=p.next;

        Node<T> q=new Node<>(p.prev,data,p);
        p.prev.next=q;
        p.prev=q;

        size++;
        //记录修改
        modCount++;

        return true;
    }

    /**
     * 不根据下标插入,只根据比较大小插入
     * @param index
     * @param data
     */
    @Override
    public void add(int index, T data) {
        this.add(data);
    }


    /**
     * 未实现
     * @param index
     * @return
     */
    @Override
    public ListIterator<T> listIterator(int index) {
        return null;
    }

    /**
     * 未实现
     * @return
     */
    @Override
    public Iterator<T> iterator() {
        return null;
    }

    //测试
    public static void main(String[] args){
        SortMyLinkedList<Integer> list=new SortMyLinkedList<>();
        list.add(50);
        list.add(40);
        list.add(80);
        list.add(20);
        print(list);
    }

    public static void print(SortMyLinkedList mylinkeList){
        for (int i=0;i<mylinkeList.size();i++) {
            System.out.println("i->"+mylinkeList.get(i));
        }
    }
}

接着以SortMyLinkedList为基底实现优先队列PriorityQueue,实现源码如下:

package com.zejian.structures.Queue;

import com.zejian.structures.LinkedList.MyCollection.SortMyLinkedList;

import java.io.Serializable;
import java.util.NoSuchElementException;

/**
 * 优先队列的简单实现,采用排序双链表,T必须实现Comparable接口
 */
public class PriorityQueue<T extends Comparable<? extends T>> implements Queue<T> ,Serializable {

    private static final long serialVersionUID = 8050142086009260625L;

    private SortMyLinkedList<T> list;//排序循环双链表

    private boolean asc;//true表示升序,false表示降序

    /**
     * 用于控制最大容量,默认128,offer方法使用
     */
    private int maxSize=128;
    /**
     * 初始化队列
     * @param asc
     */
    public PriorityQueue(boolean asc){
        this.list=new SortMyLinkedList<>();
        this.asc=asc;//默认升序
    }

    public int getMaxSize() {
        return maxSize;
    }

    public void setMaxSize(int maxSize) {
        this.maxSize = maxSize;
    }

    @Override
    public int size() {
        return list.size();
    }

    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }

    /**
     * data 入队,添加成功返回true,否则返回false
     * @param data
     * @return
     */
    @Override
    public boolean add(T data) {
        return list.add(data);
    }

    /**
     * offer 方法可插入一个元素,这与add 方法不同,
     * 该方法只能通过抛出未经检查的异常使添加元素失败。
     * 而不是出现异常的情况,例如在容量固定(有界)的队列中
     * NullPointerException:data==null时抛出
     * IllegalArgumentException:队满,使用该方法可以使Queue的容量固定
     * @param data
     * @return
     */
    @Override
    public boolean offer(T data) {
        if (data==null)
            throw new NullPointerException("The data can\'t be null");
        if (list.size()>=maxSize)
            throw new IllegalArgumentException("The capacity of PriorityQueue has reached its maxSize:128");

        return add(data);
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,返回null
     * @return
     */
    @Override
    public T peek() {
        if(isEmpty()){
            return null;
        }
        return this.asc ? this.list.get(0):this.list.get(size()-1);
    }

    /**
     * 返回队头元素,不执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T element() {
        if(isEmpty()){
            throw new NoSuchElementException("The PriorityQueue is empty");
        }
        return peek();
    }

    /**
     * 出队,执行删除操作,返回队头元素,若队列为空,返回null
     * @return
     */
    @Override
    public T poll() {
        if(isEmpty()){
            return null;
        }
        return this.asc ? this.list.remove(0): this.list.remove(list.size()-1);
    }

    /**
     * 出队,执行删除操作,若队列为空,抛出异常:NoSuchElementException
     * @return
     */
    @Override
    public T remove() {
        if (isEmpty()){
            throw new NoSuchElementException("The PriorityQueue is empty");
        }
        return poll();
    }

    @Override
    public void clearQueue() {
        this.list.clear();
    }

    //测试
    public static void main(String[] args){
        PriorityQueue<Process> priorityQueue=new PriorityQueue<>(false);

        System.out.println("初始化队列");
        priorityQueue.add(new Process("进程1",10));
        priorityQueue.add(new Process("进程2",1));
        priorityQueue.add(new Process("进程3",8));
        priorityQueue.add(new Process("进程4",3));
        priorityQueue.add(new Process("进程5"));
        System.out.println("队列中的进程执行优先级:");
        while (!priorityQueue.isEmpty()){
            System.out.println("process:"+priorityQueue.poll().toString());
        }

    }

}

6.队列应用的举例:

  1. 模拟现实世界中的队列,如售票柜台的队列以及其他先到先服务的场景。
  2. 计算客户在呼叫中心等待的时间。
  3. 异步数据的传输(文件输入输出、管道、嵌套字)。
  4. 操作系统中的优先级任务执行。
  5. 短信群体发送 应用的发布订阅模式

 🔺Josephu 问题:

约瑟夫问题的示意图:

★ Josephu 问题 Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此 产生一个出队编号的序列。

★ 提示 用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开 始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 结点从链表中删除算法结束。

★ 约瑟夫问题-创建环形链表的思路图解

 ★约瑟夫问题-小孩出圈的思路分析图

具体代码实现: 

public class Josepfu {
public static void main(String[] args) {
// 测试一把看看构建环形链表,和遍历是否 ok
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(125);// 加入 5 个小孩节点
circleSingleLinkedList.showBoy();
//测试一把小孩出圈是否正确
circleSingleLinkedList.countBoy(10, 20, 125); // 2->4->1->5->3
}
}

// 创建一个环形的单向链表
class CircleSingleLinkedList {
// 创建一个 first 节点,当前没有编号
private Boy first = null;
// 添加小孩节点,构建成一个环形的链表
public void addBoy(int nums) {
// nums 做一个数据校验
if (nums < 1) {
System.out.println("nums 的值不正确");
return;
}
Boy curBoy = null; // 辅助指针,帮助构建环形链表
// 使用 for 来创建我们的环形链表
for (int i = 1; i <= nums; i++) {
// 根据编号,创建小孩节点
Boy boy = new Boy(i);
// 如果是第一个小孩
if (i == 1) {
first = boy;
first.setNext(first); // 构成环
curBoy = first; // 让 curBoy 指向第一个小孩
} else {
curBoy.setNext(boy);//
boy.setNext(first);//
curBoy = boy;
}
}
}


// 遍历当前的环形链表
public void showBoy() {
// 判断链表是否为空
if (first == null) {
System.out.println("没有任何小孩~~");
return;
}
// 因为 first 不能动,因此我们仍然使用一个辅助指针完成遍历
Boy curBoy = first;
while (true) {
System.out.printf("小孩的编号 %d \n", curBoy.getNo());
if (curBoy.getNext() == first) {// 说明已经遍历完毕
break;
}
curBoy = curBoy.getNext(); // curBoy 后移
}
}


// 根据用户的输入,计算出小孩出圈的顺序
/**
*
* @param startNo
* 表示从第几个小孩开始数数
* @param countNum
* 表示数几下
* @param nums * 表示最初有多少小孩在圈中
*/
public void countBoy(int startNo, int countNum, int nums) {
// 先对数据进行校验
if (first == null || startNo < 1 || startNo > nums) {
System.out.println("参数输入有误, 请重新输入");
return;
}
// 创建要给辅助指针,帮助完成小孩出圈
Boy helper = first;
// 需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点
while (true) {
if (helper.getNext() == first) { // 说明 helper 指向最后小孩节点
break;
}
helper = helper.getNext();
}
//小孩报数前,先让 first 和 helper 移动 k - 1 次
for(int j = 0; j < startNo - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让 first 和 helper 指针同时 的移动 m - 1 次, 然后出圈
//这里是一个循环操作,知道圈中只有一个节点
while(true) {
if(helper == first) { //说明圈中只有一个节点
break;
}
//让 first 和 helper 指针同时 的移动 countNum - 1
for(int j = 0; j < countNum - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//这时 first 指向的节点,就是要出圈的小孩节点
System.out.printf("小孩%d 出圈\n", first.getNo());
//这时将 first 指向的小孩节点出圈
first = first.getNext();
helper.setNext(first); //
}
System.out.printf("最后留在圈中的小孩编号%d \n", first.getNo());
}
}


// 创建一个 Boy 类,表示一个节点
class Boy {
private int no;// 编号
private Boy next; // 指向下一个节点,默认 null
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}

7.解题时需要注意的点:

1.通常在算法题中都是使用(泛型都是用包装类)

Queue<Integer> queue = new LinkedList();

        即使用链表来实现

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值