双向循环链表练习--约瑟夫问题.

1.约瑟夫问题

我们先来说一下何为约瑟夫问题
其实就是n个人围坐在一起 然后每数到3就死亡一个人 接着重新开始数 一直循环直至链表为空为止 要求我们获取一下这些人的死亡顺序
这边我们用循环链表解决这道题很合适 不管是单向的 还是双向的 其实都可以接受
我们就用双向的来解决这个问题

2.解决方式

在这里插入图片描述
现在给定8个人 围成一个圈 每数到3就一个人死亡 直到最后链表为空为止 求得最后的死亡顺序
我们可以在原先双向循环链表的基础上添加一个私有成员以及三个成员方法

1.私有成员

该成员变量其实就是起到指向链表中任意一个节点的作用 我们命名为current

private Node<E> current;

2.成员方法

1.reset()方法

该方法用于将current指针重置为first指向的节点

public void reset(){
	current = first;
}

2.next()方法

该方法用于将current指针后移一步 并且返回移动后的节点

public E next(){
	// 首先要对current进行判空操作
	if(current == null)return null;
	current = current.next;
	return current.val;
}

3.remove()方法

该方法用于删除current节点 并且返回被删除的节点值
但是现在摆在我们面前的有两种选择 一种是直接调用当前现成的remove(int index)方法 一种就是实现remove(Node node)然后让remove(int index)调用当前remove(Node node)方法
其实经过比较 我们可以发现 当然是后者更加高效 拿删除指定节点来说 因为前者需要经历节点->索引->节点的过程 而后者只需要经历节点一个过程 孰优孰劣大家心知肚明
那么我们就先实现remove(Node node)方法 然后让其他的remove方法调用他来得以实现

private E remove(Node<E> node){
	// 但是考虑到一种特殊情况就是删除以后链表为空 如果套用正常情况下的逻辑的话 那么永远都删除不掉待删除的节点 所以说这个逻辑得另外实现
	if(size == 1){
		first = last = null;
	}else{
		// 首先我们还是按照正常逻辑进行处理 我们通过参数已知的待删除节点获取待删除节点的前置节点以及后置节点 
		Node<E> pre = node.pre;
		Node<E> next = node.next;
		// 接下去要改变指向待删除节点的两根线 由于是循环链表 所以无需担心前置节点和后置节点为空的情况 我们只需要额外考虑到头删和尾删两种情况的更新头指针和尾指针的操作
		pre.next = next;
		next.pre = pre;
		// 如果是头删的话 更新头指针 不过判断的条件不推荐在使用索引
		if(node == first){
			first = next;
		}
		// 如果是尾删的话 更新尾指针 不过判断的条件也不推荐使用索引
		if(node == last){
			last = pre;
		}
	}
	// 更新链表长度
	size--;
	// 返回待删除节点值
	return node.val;
}

那么remove(int index)的实现就可以如下所示了

public E remove(int index){
	// 首先对指定索引进行索引越界检查
	rangeCheck(index);
	// 接着直接套用remove(Node<E> node)的逻辑即可
	return remove(node(index));
}

最后我们再来实现一下remove()这个方法吧 该方法就是删除current指向的节点 并且更新current为被删除节点的下一节点 并且返回待删除节点值
而且我们需要考虑到一种最特殊的情况就是删除以后链表为空 那么我们更新current的操作如果按照正常的逻辑current = next的话 那么就永远得不到正确的答案 因为我们都知道一旦删除以后链表为空的话 那么current也势必为空 但是如果走的是正常的逻辑的话 那么无论怎么删除current都将指向那个待删除的节点 所以这种情况也得特殊处理

public E remove(){
	// 为下一条语句进行判空操作
	if(current == null)return null;
	// 我们首先要保留一下待删除节点的下一节点
	Node<E> next = current.next;
	E val = remove(current);
	// 当删除以后链表为空的话 为什么判空链表为空的语句和remove(Node<E> node)不一样是size == 1 而是size == 0了呢 因为执行完remove(Node<E> node)以后的size已经变成了0 所以我们不能在按照以前的思维去看待问题了
	if(size == 0){
		current = null;
	}else{
		current = next;
	}
	return val;
}

3.完整代码

public class DoubleLinkedList<E>{
    // 私有成员
    private Node<E> first;// 头节点
    private Node<E> last;// 尾节点
    private Node<E> current;// 当前节点
    private int size;// 链表长度
    // 常量
    private static final int ELEMENT_NOT_FOUND = -1;
    // 节点类
    private static class Node<E>{
        // 节点值
        private E val;
        // 上一个节点
        private Node<E> pre;
        // 下一个节点
        private Node<E> next;
        // 构造方法
        public Node(E val, Node<E> pre, Node<E> next){
            this.val = val;
            this.pre = pre;
            this.next = next;
        }
    }
    public void reset(){
        current = first;
    }
    public E next(){
        // 首先要对current进行判空操作
        if(current == null)return null;
        current = current.next;
        return current.val;
    }
    public void rangeCheck(int index){
        // 判断指定索引是否处在指定范围内
        if(index < 0 || index >= size)
            outOfBounds(index);
    }
    public void rangeCheckForAdd(int index){
        // 判断指定索引是否处在指定范围内
        if(index < 0 || index > size)
            outOfBounds(index);
    }
    public void outOfBounds(int index){
        throw new IndexOutOfBoundsException("索引越界了");
    }
    public void add(int index, E val){
        // 首先是对指定索引进行索引越界检查
        rangeCheckForAdd(index);
        // 然后进行正常情况下的编写 然后从一般到特殊 一般情况是中间插入 看一下头插这种情况 其实大致符合中间插入的逻辑 只有少部分不一样 但是尾插需要另外编写
        // 然后我们也需要考虑到特殊情况 就是往空链表中进行插入操作 他走的是if的逻辑 所以我们需要在if语句中进行分类讨论
        if(index == size){
            // 我们首先要获取的是待插入节点的前置节点
            Node<E> pre = last;
            // 然后设置待插入节点
            last = new Node<>(val, pre, first);
            // 接着就是解决指向待插入节点的两根线 由于尾插的缘故 所以只需要一根线就行 如果是特殊情况的话 那么我们就不需要执行一下语句
            if(size == 0){
                first = last;
                first.next = first;
                first.pre = first;
            }else{
                pre.next = last;
                first.pre = last;
            }
        }else{
            // 首先获取待插入节点的后置节点 头插的话 是不需要设置pre.next的 取而代之的是更新头节点的操作
            Node<E> next = node(index);
            // 接着通过后置节点获取前置节点
            Node<E> pre = next.pre;
            // 然后通过前两个节点设置待插入节点
            Node<E> node = new Node<>(val, pre, next);
            // 接着就是设置余下的两根线 就是指向待插入节点的线
            next.pre = node;
            pre.next = node;
            if(index == 0){
                first = node;
            }
        }
        // 更新链表长度
        size++;
    }
    private Node<E> node(int index){
        // 首先对指定索引进行索引越界检查
        rangeCheck(index);
        // 接着判断指定索引在链表中的大致位置 是在左半部分 还是在右半部分
        Node<E> node;
        if(index < (size >> 1)){
            // 这个条件说明指定节点在链表中的左半部分 那么就选择从头节点开始往后遍历
            node = first;
            for(int i = 0; i < index; ++i){
                node = node.next;
            }
        }else{
            node = last;
            for(int i = size - 1; i > index; --i){
                node = node.pre;
            }
        }
        return node;
    }
    private E remove(Node<E> node){
        // 但是考虑到一种特殊情况就是删除以后链表为空 如果套用正常情况下的逻辑的话 那么永远都删除不掉待删除的节点 所以说这个逻辑得另外实现
        if(size == 1){
            first = last = null;
        }else{
            // 首先我们还是按照正常逻辑进行处理 我们通过参数已知的待删除节点获取待删除节点的前置节点以及后置节点
            Node<E> pre = node.pre;
            Node<E> next = node.next;
            // 接下去要改变指向待删除节点的两根线 由于是循环链表 所以无需担心前置节点和后置节点为空的情况 我们只需要额外考虑到头删和尾删两种情况的更新头指针和尾指针的操作
            pre.next = next;
            next.pre = pre;
            // 如果是头删的话 更新头指针 不过判断的条件不推荐在使用索引
            if(node == first){
                first = next;
            }
            // 如果是尾删的话 更新尾指针 不过判断的条件也不推荐使用索引
            if(node == last){
                last = pre;
            }
        }
        // 更新链表长度
        size--;
        // 返回待删除节点值
        return node.val;
    }
    public E remove(int index){

        // 首先对指定索引进行索引越界检查
        rangeCheck(index);
        // 接着直接套用remove(Node<E> node)的逻辑即可
        return remove(node(index));

    }
    public E remove(){
        // 为下一条语句进行判空操作
        if(current == null)return null;
        // 我们首先要保留一下待删除节点的下一节点
        Node<E> next = current.next;
        E val = remove(current);
        // 当删除以后链表为空的话 为什么判空链表为空的语句和remove(Node<E> node)不一样是size == 1 而是size == 0了呢 因为执行完remove(Node<E> node)以后的size已经变成了0 所以我们不能在按照以前的思维去看待问题了
        if(size == 0){
            current = null;
        }else{
            current = next;
        }
        return val;
    }
    public int indexOf(E element){
        Node<E> node = first;
        if(element == null){
            for(int i = 0; i < size; ++i){
                if(node.val == null)return i;
                node = node.next;
            }
        }else{
            for(int i = 0; i < size; ++i){
                if(element.equals(node.val))return i;
                node = node.next;
            }
        }
        return ELEMENT_NOT_FOUND;
    }
    public E get(int index){
        // 由于等一下的node方法提供了索引越界检查 所以这个方法中无需提供额外的索引越界检查
        return node(index).val;
    }
    public E set(int index, E val){
        // 首先获取指定位置处的旧值
        E oldVal = node(index).val;
        node(index).val = val;
        return oldVal;
    }
    public void clear(){
        first = null;
        last = null;
        size = 0;
    }
    public boolean contains(E element){
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }
    public boolean isEmpty(){
        return size == 0;
    }
    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("size = ").append(size).append(" [");
        Node<E> node = first;
        for(int i = 0; i < size; ++i){
            if(i != 0)sb.append(", ");
            sb.append(node.val);
            node = node.next;
        }
        sb.append("]");
        return sb.toString();
    }
    public int size(){
        return size;
    }
}

4.测试代码

我们想要看一下每走三步(相当于每调用两次next)时的死亡顺序
测试代码如下所示:

private static void josephus(){
    // 首先就是创建一个双向循环链表
    DoubleLinkedList<Integer> sll = new DoubleLinkedList<>();
    // 首先我们要填充一下这个链表 我们一开始假设的链表长度就是8个 所以我们就初始化为8个
    for(int i = 1; i <= 8; ++i){
        sll.add(sll.size(), i);
    }
    // 重置一下 让原本为空的current指针指向first节点
    sll.reset();
    // 接着我们可以自行选择每n步杀一次人的n 说白了我们可以通过循环调节这个n

    while(!sll.isEmpty()){
        int n = 3;
        while(--n > 0){
            sll.next();
        }
        System.out.println(sll.remove());
    }
}

这只是其中一种测试案例 其实我们还可以调节next的次数 从而达到控制频率n的目的
另外其实这个案例也可以通过单向循环链表去实现 其实套路也是一样的 就是在单向循环链表的基础上添加一个私有成员还有三个方法罢了

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
约瑟夫问题是一个经典的问题,我们不妨将这个经典问题进行扩展,变成一个双向约瑟夫问题。   已知n个人(不妨分别以编号1,2,3,…,n 代表 )围坐在一张圆桌周围,首先从编号为 k 的人从1开始顺时针报数,1, 2, 3, ...,记下顺时针数到 m 的那个人,同时从编号为 k 的人开始逆时针报数,1, 2, 3, ...,数到 m 后,两个人同时出列。然后从出列的下一个人又从 1 开始继续进行双向报数,数到m的那两个人同时出列,…;。依此重复下去,直到圆桌周围的人全部出列。直到圆桌周围只剩一个人为止。   如果双向报数报到 m 时落在同一个人身上,那本次出列的只有一个人。   例如:5,1,2。则总共5个人,从1开始顺时针报数,数到2,定位编号2;同时从1开始报数数到2,定位编号5;2和5同时出列。然后继续开始报数,顺时针报数3,4,定位到4;逆时针报数4,3,定位3;4和3同时出列。最后剩余的为编号1。输出为:2-5,4-3,1,。   如果输入:6,2,3。则输出:4-6,2,1-3,5,。其中第2次只输出一个2,表示第二次双向报数时,恰好都落在编号2上,所以只有一个编号出列。 输入 n,k,m 输出 按照出列的顺序依次输出编号。同时出列编号中间用减号“-”连接。 非法输入的对应输出如下 a) 输入:n、k、m任一个为0 输出:n,m,k must bigger than 0. b) 输入:k>n 输出:k should not bigger than n.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axihaihai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值