约瑟夫环的多种解法

312 阅读2分钟

一、问题描述

image.png

二、循环链表

思路:构建一个循环链表,从0开始往下报数,遇到报数为m-1的节点就删除,计数从0重新开始,直到剩下最后一个节点。

    ListNode pre;
    public int lastRemaining(int n, int m) {
        if(n==1||m==n-1)
            return n-1;
        //  构造环形链表
        ListNode head = createList(n);
        int count = 1;
        ListNode cur = head;
        while(cur.next!=cur){
            // 第m个数 执行删除逻辑
            if(count%m==0){
                pre.next = cur.next;
                cur = cur.next;
            }else{
                // 指针在链表上移动
                pre = cur;
                cur = cur.next;
            }
            count++;

        }
        return cur.val;


    }

    public ListNode createList(int n){
        ListNode head = new ListNode(0);
        ListNode cur = head;
        for(int i=1;i<n;i++){
            cur.next = new ListNode(i);
            cur = cur.next;
        }
        //环形链表
        pre = cur;
        cur.next = head;
        return head;
    }


}

class ListNode{
    int val;
    ListNode next;

    public ListNode(int val){
        this.val = val;
    }
}

时间复杂度:每次删除一个节点指针要移动m次,一共要删除n-1个节点,时间复杂度为O(nm),显然按照题目中n和m的范围,这种解法会超时

三、利用数组下标

用链表需要遍历定位到要删除的节点,时间复杂度为O(m),而使用数组的随机访问特性,可以直接访问,也就是定位操作的时间复杂度变为O(1),由于数组需要动态扩展的功能,时间复杂度为O(n),所以整体的时间复杂度为O(n²)。但是因为数组可以利用CPU缓存,性能会比链表好一点

class Solution {
    public int lastRemaining(int n, int m) {
    ArrayList<Integer> list = new ArrayList<>();
    for(int i=0;i<n;i++)
        list.add(i);
    int idx = 0;
    while(n>1){
        idx = (idx+m-1)%n;
        list.remove(idx);
        n--;
    }
    // 剩下最后一个元素
    return list.get(0);
    }

}

四、数学公式推导

以n=5,m=3为例,定义f(n,m)为最后存活的人的下标,整个过程如下图:

ysf.drawio.png

我们可以知道,存活到最后的人的下标最后会变成0,也就是f(1,m)=0是可以确定的,只要找到f(1,m)->f(2,m)->f(3,m)->f(4,m)->f(5,m)的规律,即可推导出最后存活的人是谁。 以f(4,m)->f(5,m)为例,分析f(n-1,m)->f(n,m)的规律:

ysf1.drawio.png

class Solution {
    public int lastRemaining(int n, int m) {
        // f(1,m)=0
        int index = 0;
        for(int i=2;i<=n;i++){
            // f(n,m) = (f(n-1,m)+m)%n);
            index = (index+m)%i;
        }
        return index;
    }
}

时间复杂度:每次运算的事件复杂度为O(1),一共执行n-1次运算,所以时间复杂度为O(n)。

五、总结

  • 环形链表:容易理解,时间复杂度为O(NM),数据量大时不宜采用。
  • 数组:避免了链表需要O(M)的时间复杂度定位的开销,但需要O(N)的时间复杂度进行动态扩展,总体时间复杂度为O(N²),由于数组可以利用CPU缓存,性能会比链表稍微好一点
  • 数学公式:时间复杂度为O(N)