您的位置:首页 > Web前端

[剑指offer学习心得]之:圆圈中最后剩下的数字

2016-10-10 16:37 375 查看
之前学习的都是简单的单向链表,这次要开始学习复杂一点的,比如本文想要接触的就是形成一个环形链表(链表末尾结点指针指向头结点)来解决问题。

题目:0, 1, … , n-1 这 n 个数字排成一个圈圈,从数字 0 开始每次从圆圏里删除第 m 个数字。求出这个圈圈里剩下的最后一个数字。

这是有名的约瑟夫环问题。有两种方法:一种是经典的用环形链表模拟圆圈的解法;一种是分析每被删除的数字的规律并肢解计算出圆圈中最后剩下的数字。

测试用例

功能测试(输入的m小于n,比如从最初有5个数字的圆圈删除每次第2、3个数字;输入的m大于或者等于n,比如从最初有6个数字的圆圈删除每次第6、7个数字)

特殊输入测试(圆圈中有0个数字)

性能测试(从最初有4000个数字的圆圈中每次删除第997个数字)

经典解法:用环形链表模拟圆圈

创建一个总共有 n 个结点的环形链表,然后每次在这个链表中删除第 m 个结点。

private static int lastRemaining(int n,int m){
if(n<1||m<1){
return -1;
}

List<Integer> list=new LinkedList<Integer>();
for(int i=0;i<n;i++){
list.add(i);//构造链表
}

int indexBeDeleted=0;
int start=0;
while(list.size()>1){
for(int i=1;i<m;i++){
indexBeDeleted=(indexBeDeleted+1)%list.size();
}
list.remove(indexBeDeleted);
}

return list.get(0);
}


但是分析一下发现我们实际上要在这个环形链表里重复遍历很多遍。重复遍历当然对时间效率有很负面的影响。这种方法每删除一个数字需要m步运算,总共有n个数字,因此总的时间复杂度威O(mn),而且这种思路还要一个辅助链表来模拟圆圈,空间复杂度又是O(n)。所以我们可以尝试另外一种方法:找到每次被删除的数字有哪些规律,然后找出更高效的算法。

创新解法:找规律

首先我们定义一个关于 n 和 m 的方程f(m,n),表示每次在 n 个数字 0,1, … ,n-1中每次删除第 m 个数字最后剩下的数字。

在这 n个数字中, 第一个被删除的数字是(m-1)%n。为了简单起见,我们把(m- 1)%n 记为 k,那么删除k之后剩下的 n-1 个数字为 0,1,… ,k-1,k+1,… ,n-1,并且下一次删除从数字 k+1 开始计数。相当于在剩下的序列中, k+1 排在最前面,从而形成 k+1,… ,n- 1,0,I,… ,k-1 。该序列最后剩下的数字也应该是关于 n 和 m 的函数。由于这个序列的规律和前面最初的序列不一样(最初的序列是从 0 开始的连续序列),因此该函数不同于前面的函数,记为 f’(n-1,m)。最初序列最后剩下的数字 f(n, m)一定是删除一个数字之后的序列最后剩下的数字,即 f(n, m)=f’(n-1, m)。

接下来我们把剩下的这 n-1 个数字的序列 k-1, …,n-1,0,1,… ,k-1 做一个映射,映射的结果是形成一个从 0 到 n-2 的序列:  


 

代码展示:

private static int lastRemaining2(int n,int m){
if(n<1||m<1){
return -1;
}

int last=0;
for(int i=2;i<=n;i++){
last=(last+m)%i;
}
return last;
}


虽然这种思路的分析过程复杂,但是写出的代码却非常简洁,这就是数学的魅力。而且这种算法时间复杂度是O(n),空间复杂度是O(1)。所以不管是在时间效率还是在空间效率上都优于第一种方法。

完整代码展示:

import java.util.LinkedList;
import java.util.List;

public class LastRemaining {

public static void main(String[] args){
test01();
System.out.println();
test02();
}

private static void test01() {
System.out.println(lastRemaining(5, 3)); // 最后余下3
System.out.println(lastRemaining(5, 2)); // 最后余下2
System.out.println(lastRemaining(6, 7)); // 最后余下4
System.out.println(lastRemaining(6, 6)); // 最后余下3
System.out.println(lastRemaining(0, 0)); // 最后余下-1
}

private static void test02() {
System.out.println(lastRemaining2(5, 3)); // 最后余下3
System.out.println(lastRemaining2(5, 2)); // 最后余下2
System.out.println(lastRemaining2(6, 7)); // 最后余下4
System.out.println(lastRemaining2(6, 6)); // 最后余下3
System.out.println(lastRemaining2(0, 0)); // 最后余下-1
}

private static int lastRemaining(int n,int m){ if(n<1||m<1){ return -1; } List<Integer> list=new LinkedList<Integer>(); for(int i=0;i<n;i++){ list.add(i);//构造链表 } int indexBeDeleted=0; int start=0; while(list.size()>1){ for(int i=1;i<m;i++){ indexBeDeleted=(indexBeDeleted+1)%list.size(); } list.remove(indexBeDeleted); } return list.get(0); }

private static int lastRemaining2(int n,int m){ if(n<1||m<1){ return -1; } int last=0; for(int i=2;i<=n;i++){ last=(last+m)%i; } return last; }
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  链表