一、问题描述
二、循环链表
思路:构建一个循环链表,从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)为最后存活的人的下标,整个过程如下图:
我们可以知道,存活到最后的人的下标最后会变成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)的规律:
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)。