第六节:手撸“循环链表”及约瑟夫问题的解决
1. 约瑟夫问题
加斯帕·蒙日是法国数学家,他在《数学的游戏问题》中讲了一个故事:15个教徒和15个非教徒在深海上遇险,必须将一半的人投入海中,其余人才能幸免于难,于是想了一个方法,30个人 围城一圈,从第一个人开始依次报数,每数到第九个的人就将他扔向大海,如此循环进行直到仅剩余15个人为止。问怎样的排法,才能使每次投入大海的都是非教徒?这就是著名的约瑟夫问题,又称约瑟夫环。
我们将问题进行一下抽象:N个人进行手尾排列,从第一个开始数数,数到第X个停止,然后把第X个剔除;然后接着数,数到第X个停止,并将其移除,以此类推,直到达到要剔除的个数数目,把这些剔除位置的记录下来,就是我们所需要的位置。很显然,循环链表非常适合解决这个问题。
2. 解决思路
(1). 先抽象出一个单向的循环链表,要有尾节点,尾节点指向头节点;可以用当前节点的前驱节点来表示当前节点。
代码如下:
节点

1 /// <summary> 2 /// 循环链表节点 3 /// </summary> 4 public class CircleNode<T> 5 { 6 /// <summary> 7 /// 存放数据 8 /// </summary> 9 public T Data { get; set; } 10 11 /// <summary> 12 /// 下一个节点 13 /// </summary> 14 public CircleNode<T> Next { get; set; } 15 16 public CircleNode() 17 { 18 19 } 20 21 public CircleNode(T data) 22 { 23 Data = data; 24 } 25 }
循环链表

1 /// <summary> 2 /// 手写单链表 3 /// </summary> 4 public class MySingleLinkedList<T> 5 { 6 private Node<T> _header; //头结点 7 private int _count; //元素个数 8 public MySingleLinkedList() 9 { 10 11 } 12 public MySingleLinkedList(T data) 13 { 14 _header = new Node<T>(data); 15 _count++; 16 } 17 //只能读 18 public int Count 19 { 20 get 21 { 22 return _count; 23 } 24 } 25 /// <summary> 26 /// 根据索引获取元素 27 /// </summary> 28 /// <param name="index"></param> 29 /// <returns></returns> 30 public Node<T> GetNodeByIndex(int index) 31 { 32 if (index < 0 || index>=_count) 33 { 34 throw new ArgumentOutOfRangeException("索引越界"); 35 } 36 Node<T> tempNode = _header; 37 //当index为0的时候,不进入for循环 38 for (int i = 0; i < index; i++) 39 { 40 tempNode = tempNode.Next; 41 } 42 return tempNode; 43 } 44 45 //索引器获取和设置数据 46 public T this[int index] 47 { 48 get 49 { 50 return GetNodeByIndex(index).Data; 51 } 52 set 53 { 54 GetNodeByIndex(index).Data = value; 55 } 56 } 57 58 /// <summary> 59 /// 添加元素(最后) 60 /// </summary> 61 /// <param name="data"></param> 62 public void Add(T data) 63 { 64 Node<T> newNode = new Node<T>(data); 65 if (_header==null) //表示添加的是第一个节点 66 { 67 _header = newNode; 68 } 69 else 70 { 71 var lastNode = GetNodeByIndex(_count - 1); 72 lastNode.Next = newNode; 73 } 74 _count++; 75 } 76 77 /// <summary> 78 /// 插入元素 79 /// </summary> 80 /// <param name="index">索引</param> 81 /// <param name="data">数据</param> 82 public void Insert(int index,T data) 83 { 84 if (index < 0||index>_count) 85 { 86 throw new ArgumentOutOfRangeException("索引越界"); 87 } 88 var newNode = new Node<T>(data); //新节点 89 if (index==0) 90 { 91 //头结点为空,直接插入头结点即可 92 if (_header==null) 93 { 94 _header = newNode; 95 } 96 //头结点有元素,需要把头结点的位置让出来 97 else 98 { 99 newNode.Next = _header; 100 _header = newNode; 101 } 102 } 103 else 104 { 105 var preNode = GetNodeByIndex(index-1); //查找插入位置的前驱节点 106 var nextNode = preNode.Next; //插入位置的后继节点 107 preNode.Next = newNode; //前驱结点的后继节点为新节点 108 newNode.Next = nextNode; //新节点的后继节点执行原来前驱的后继 109 } 110 _count++; 111 } 112 113 /// <summary> 114 /// 根据索引删除元素 115 /// </summary> 116 /// <param name="index">索引</param> 117 public void RemoveAt(int index) 118 { 119 if (index < 0 || index >= _count) 120 { 121 throw new ArgumentOutOfRangeException("索引越界"); 122 } 123 if (index==0) //删除头结点 124 { 125 if (_header==null) 126 { 127 throw new ArgumentOutOfRangeException("索引越界"); 128 } 129 _header = _header.Next; 130 } 131 else 132 { 133 var preNode = GetNodeByIndex(index - 1); //删除节点的前驱节点 134 if (preNode.Next==null) 135 { 136 throw new ArgumentOutOfRangeException("索引越界"); 137 } 138 preNode.Next = preNode.Next.Next; //如果删除的是最后一个节点,那么它的前一个节点指向null 139 } 140 _count--; 141 } 142 /// <summary> 143 /// 元素输出 144 /// </summary> 145 /// <returns></returns> 146 public override string ToString() 147 { 148 string s = ""; 149 Node<T> temp = _header; 150 while (temp != null) 151 { 152 s += temp.ToString() + " "; 153 temp = temp.Next; 154 } 155 return s; 156 } 157 158 }
测试链表
1 { 2 MyCircleLinkedList<int> cList = new MyCircleLinkedList<int>(); 3 cList.Add(1); 4 cList.Add(2); 5 cList.Add(3); 6 cList.Add(4); 7 cList.Add(5); 8 cList.Add(6); 9 cList.Add(7); 10 Console.WriteLine($"当前节点值为:{cList.currentNodeValue}"); 11 cList.Move(2); 12 Console.WriteLine($"当前节点值为:{cList.currentNodeValue}"); 13 cList.RemoveCurrentNode(); 14 cList.Move(3); 15 Console.WriteLine($"当前节点值为:{cList.currentNodeValue}"); 16 var str1 = cList.ToString(); 17 Console.WriteLine(str1); 18 Console.ReadKey(); 19 }
运行结果:
(2). 利用循环链表的环解决该问题, 需要记录当前节点,默认当前节点为第一个,所以它的前驱节点为尾节点,然后设计移动算法,将当前节点移动n步,然后删除当前节点,那么下一个节点就变为当前节点了,然后接着移动,直到人数符合要求为止。
1 { 2 MyCircleLinkedList<int> cList = new MyCircleLinkedList<int>(); 3 string result = string.Empty; 4 Console.WriteLine("请输入总人数:"); 5 int count = int.Parse(Console.ReadLine()); 6 Console.WriteLine("请输入出队的位置:"); 7 int outPosition = int.Parse(Console.ReadLine()); 8 Console.WriteLine("请输入出队的个数:"); 9 int outPerson = int.Parse(Console.ReadLine()); 10 11 Console.WriteLine("游戏开始"); 12 for (int i = 1; i <= count; i++) 13 { 14 cList.Add(i); 15 } 16 Console.WriteLine($"所有人的人:{cList.ToString()} "); 17 //此处的业务用来表示剩下多少个人 18 while (cList.Count > (count - outPerson)) 19 { 20 //移动的步数=位置数-1 21 cList.Move(outPosition-1); 22 result += cList.currentNodeValue.ToString() + " "; 23 cList.RemoveCurrentNode(); //删掉当前节点出队 24 if (cList.Count == (count - outPerson)) 25 { 26 Console.WriteLine($"剩余的人为:{cList.ToString()} "); 27 } 28 else 29 { 30 Console.WriteLine($"剩余的人为:{cList.ToString()} 开始报数的人为:{cList.currentNodeValue.ToString()} "); 31 } 32 33 } 34 Console.WriteLine("出队位置的顺序: " + result); 35 }
运行结果:
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix