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的目的
另外其实这个案例也可以通过单向循环链表去实现 其实套路也是一样的 就是在单向循环链表的基础上添加一个私有成员还有三个方法罢了