第03篇 链表应用-01 约瑟夫环

一、问题描述

N个人围成一圈,从第K个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,K=1,M=5, 被杀掉的顺序是:5,4,6,2,3,1。也就是说,最终1会活下来,编程求出出圈的顺序。

二、涉及知识点

  1. 单向循环链表:https://blog.csdn.net/mail_liuxing/article/details/97976000
  2. 数组
    我们可以通过上述两种知识中的某一个来解题。

三、思路分析

  1. 单向循环链表LinkedList:(solveByJdkLinkedList)
    使用单向循环链表的话,结构上和题目描述很像。
    ①. 使用LinkedList是最简单的,因为LinkedList提供了很多丰富的方法,比如:public E remove(int index) 可以删除指定下标的元素,同时返回这个位置的元素。
    ②. 我们只需要一直计算要被删除的元素的下标,然后通过remove方法删除即可,一直重复到链表为空就结束了。
    ③. 计算下标的时候,需要注意的时候,我们需要对list.size()取模来获取正确的下标:
    比如现在被删除的元素位置是targetIndex,那么下一个位置应该是:(tartgetIndex + m - 1) % list .size()
    ④.当list.isEmpty() == true的时候,表示没有元素了,程序结束。

  2. 数组实现:solveByArray
    其实使用数组的方式和LinkedList类似,需要不停的计算下标,不过有几个不同的地方:
    ①.数组没有类似于remove(i)的方法,这个我们可以自己提供一个;
    ②.数组的长度不会像LinkedList在删除元素之后会自动-1,所以我们需要通过一个变量来计算删除元素后数组的长度,而不能直接使用length方法;
    ③.对于结束条件来说,不能像LinkedList一样直接使用isEmpty这种方法,我们可以通过判断出圈的人个数count是否等于n来结束循环。

  3. 数组优化实现(solveByArrayefficient)
    上面的方式,有一个不好的地方,就是需要不同删除元素(通过移动元素实现),效率较低,可以优化如下:
    ①. 元素的删除,不再真正的删除,而是把对应位置给上一个标志值如0来表示
    ②. 因为元素不删除,所以每次计算的下标应该是真实的没有移动的下标(0到n-1),所以永远使用n进行取模;
    ③. 数数的时候,只有元素不为标志值的时候才计入count统计。

  4. 自定义单向循环链:solveBySelfLinkedList
    ①. 通过单向链表构成一个环形结构
    ②. 通过遍历把初始指针tmp指移动到第K个元素,注意的是:K可能大于N,不过没关系,毕竟,是循环链表,到最后一个的时候,会回到头部;
    ③. 通过一个计数器cout,来记录数了几下,当count等于M-1了,就表明下一个节点要被删除了,此时tmp指向被删除节点的前一个节点,接下来,进行删除即可
    ④. 当链表为空了结束程序(empty方法判断)
    这种方法需要查看自定义单向循环链表的实现:
    https://blog.csdn.net/mail_liuxing/article/details/97976000

  5. 总结比较:1方法使用系统的API,对API很熟,3方法效率较高,比较推荐。

四、代码实现

package com.firewolf.javabase.s003_linkedlist.realcase;

import com.firewolf.javabase.s003_linkedlist.CyclicSingleLinkedList;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * 约瑟夫环 问题描述 N个人围成一圈,从第K个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,K=1,M=5, 被杀掉的顺序是:5,4,6,2,3,1。也就是说,最终1会活下来
 */
public class Josephus {

  public static void main(String[] args) {

//    Scanner scanner = new Scanner(System.in);
//
//    System.out.println("请输入总人数:");
//    int N = scanner.nextInt();
//
//    System.out.println("请输入开始位置:");
//    int K = scanner.nextInt();
//
//    System.out.println("数到第几个展出死?");
//    int M = scanner.nextInt();

    int N = 10, K = 1, M = 5;
    System.out.printf("%d个人参与游戏,从第%d个人开始数,每数%d个出圈!\n", N, K, M);
    solveBySelfLinkedList(N, K, M);
    solveByJdkLinkedList(N, K, M);
    solveByArray(N, K, M);
    solveByArrayefficient(N, K, M);
  }


  /**
   * 使用jdk的链表来实现
   *
   * @param n 人数
   * @param k 开始位置
   * @param m 数几个
   */
  private static void solveByJdkLinkedList(int n, int k, int m) {
    LinkedList<Integer> list = new LinkedList<>();
    int i = 1;
    while (i <= n) {
      list.add(i++);
    }
    List<Integer> result = new ArrayList<>();
    //要判断的位置,其实就是从开始位置后面再加上m-1个元素(这样就是数到m个),第k个的下标为k-1,所以第一次要统计的位置是 (k-1)+(m-1) = k+m -2
    int tartgetIndex =
        (k + m - 2) % list.size(); //第一个被点到的下标位置,由于下标从0开始,需要减去2,假设n,k,m分别为6,1,5,那么此时targetIndex = 4(也就是第5个元素)
    Integer remove = list.remove(tartgetIndex);
    result.add(remove);
    while (!list.isEmpty()) {
      tartgetIndex = (tartgetIndex + m - 1) % list
          .size(); //这个时候,targetIndex往后加上m-1个就是m个了,因为删除了原来的targetIndex位置的元素,后面的会补上来,所以这里也有一个
      remove = list.remove(tartgetIndex);
      result.add(remove);
    }
    System.out.println("Jdk数组结果顺序为:" + result);
  }


  /**
   * 使用数组来解决,不需要不停的删除数组中元素,提高效率
   *
   * @param n 人数
   * @param k 开始位置
   * @param m 数几个
   */
  private static void solveByArrayefficient(int n, int k, int m) {
    int[] array = new int[n];
    for (int i = 0; i < n; i++) {
      array[i] = i + 1;
    }

    List<Integer> result = new ArrayList<>();
    int target = k - 1;
    while (result.size() != n) {
      int count = 0;
      while (count < m) {
        target = (target) % n;
        if (array[target] != 0) { //表示没有出去的
          count++; //如果这个位置没有出去,就计数
        }
        target++; //查看下一个元素是否满足
      }
      int tmp = target - 1; //因为在while里面target到了下一个位置,所以为了拿到需要被删除的位置,需要-1
      result.add(array[tmp]);
      array[tmp] = 0; //已经出去了的,清空为0
    }
    System.out.println("高效数组结果顺序为:" + result);

  }


  /**
   * 使用数组来解决,需要不停的删除数据,也就是需要不停的移动元素 思路和使用jdk中的LinkedList类似
   *
   * @param n 人数
   * @param k 开始位置
   * @param m 数几个
   */
  private static void solveByArray(int n, int k, int m) {

    int[] array = new int[n];

    for (int i = 0; i < n; i++) {
      array[i] = i + 1;
    }

    List<Integer> result = new ArrayList<>();
    int count = 0;
    int tartgetIndex = (k + m - 2) % (array.length - count);
    Integer remove = deleteArrayByIndex(tartgetIndex, array);
    result.add(remove);
    count++;
    while (count < n) {
      tartgetIndex = (tartgetIndex + m - 1) % (array.length - count);
      remove = deleteArrayByIndex(tartgetIndex, array);
      result.add(remove);
      count++;
    }
    System.out.println("数组计算结果顺序为:" + result);

  }


  private static int deleteArrayByIndex(int index, int[] array) {
    int value = array[index];
    for (int i = index + 1; i < array.length; i++) {
      array[i - 1] = array[i];
    }
    return value;
  }

  /**
   * 使用自定义的链表来实现
   *
   * @param n 人数
   * @param k 开始位置
   * @param m 数几个
   */
  private static void solveBySelfLinkedList(int n, int k, int m) {
    CyclicSingleLinkedList<Integer> personNos = new CyclicSingleLinkedList<>();
    int i = 1;
    while (i <= n) {
      personNos.addLast(i++);
    }

    List<Integer> list = personNos.josephusRemove(k, m);
    System.out.println("自定义链表计算结果顺序为:" + list);
  }
}

其中com.firewolf.javabase.s003_linkedlist.CyclicSingleLinkedList#josephusRemove如下:

/**
   * @param from 开始位置
   * @param count 数几下
   * @return 返回被删除元素的顺序
   */
  public List<T> josephusRemove(int from, int count) {
    if (empty()) {
      return new ArrayList<>();
    }
    List<T> deletes = new ArrayList<>();
    //找到开始数数的位置
    int from1 = 1;
    CyclicNode tmp = first;
    while (from1++ < from) {
      tmp = tmp.next;
    }

    while (!empty()) {
      //找到要被删除的前一个节点
      int count1 = 1;
      while (count1 < count - 1) {
        count1++;
        tmp = tmp.next;
      }

      //执行删除
      T data = tmp.next.data;
      if (tmp.next == first) {
        removeFirst();
      } else if (tmp.next == last) {
        removeLast();
      } else {
        tmp.next = tmp.next.next;
        size--;
      }
      if (!empty()) { //后移下不标,从被删除的下一个元素开始数
        tmp = tmp.next;
      }
      deletes.add(data);
    }

    return deletes;
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值