当前位置:   article > 正文

C/数据结构 - 链表约瑟夫环问题

C/数据结构 - 链表约瑟夫环问题

写在前面:链表是数据结构线性表中的一种,用于动态解决因数据量不确定而造成的频繁插入、删除操作的情况。同时因其特殊的物理结构,也被广泛用于队列,栈,图等其他数据结构,以及大数据处理,数据缓存当中。本文通俗易懂,深入解析算法,手撕代码,适用于有一定编程语言基础并且刚接触数据结构算法的uu们。

一. 预备知识 

1. 结构体&指针

(1)typedef重定义

  1. #include <stdio.h>
  2. // 定义一个结构体用于表示矩形的长和宽
  3. typedef struct {
  4. int length;
  5. int width;
  6. } Rectangle;
  7. int main() {
  8. // 使用typedef重新定义int为Length, 表示长度
  9. typedef int Length;
  10. // 使用Rectangle结构体类型定义rect1和rect2变量
  11. Rectangle rect1 = {10, 5};
  12. Rectangle rect2 = {8, 6};
  13. // 使用Length类型定义length1和length2变量
  14. Length length1 = 10;
  15. Length length2 = 5;
  16. // 输出矩形的长和宽
  17. printf("Rectangle 1: Length = %d, Width = %d\n", rect1.length, rect1.width);
  18. printf("Rectangle 2: Length = %d, Width = %d\n", rect2.length, rect2.width);
  19. // 输出长度
  20. printf("Length 1: %d\n", length1);
  21. printf("Length 2: %d\n", length2);
  22. return 0;
  23. }

(2)结构体指针&解引用操作

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. // 定义一个结构体表示学生信息
  4. struct Student {
  5. char name[20];
  6. int age;
  7. };
  8. // 函数:打印学生信息
  9. void printStudentInfo(struct Student* ptr) {
  10. printf("Student Name: %s\n", ptr->name);
  11. printf("Student Age: %d\n", ptr->age);
  12. }
  13. int main() {
  14. // 声明并初始化一个结构体变量
  15. struct Student student1 = {"Alice", 20};
  16. // 声明一个结构体指针,并指向结构体变量student1
  17. struct Student* ptr_student = &student1;
  18. // 通过结构体指针访问和修改结构体成员
  19. printf("Before modification:\n");
  20. printStudentInfo(ptr_student);
  21. // 修改结构体成员
  22. strcpy(ptr_student->name, "Bob");
  23. ptr_student->age = 25;
  24. // 打印修改后的学生信息
  25. printf("\nAfter modification:\n");
  26. printStudentInfo(ptr_student);
  27. return 0;
  28. }

2. 动态开辟内存

(1)malloc的使用

原型:void* malloc (size_t size);

用途:向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。size为字节数。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. // 定义一个结构体表示学生信息
  4. struct Student {
  5. char name[20];
  6. int age;
  7. };
  8. int main() {
  9. // 使用malloc动态分配内存给结构体指针
  10. struct Student* ptr_student = (struct Student*)malloc(sizeof(struct Student));
  11. // 检查内存是否成功分配
  12. if (ptr_student == NULL) {
  13. printf("Memory allocation failed.\n");
  14. return 1;
  15. }
  16. // 设置结构体成员值
  17. strcpy(ptr_student->name, "Alice");
  18. ptr_student->age = 20;
  19. // 打印结构体成员值
  20. printf("Student Name: %s\n", ptr_student->name);
  21. printf("Student Age: %d\n", ptr_student->age);
  22. // 释放动态分配的内存
  23. free(ptr_student);
  24. return 0;
  25. }

3. 链表

(1)单链表 - 不带头单向不循环链表

链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表中每个最小组成单元称为节点,每个节点包含数据域和指针域。对于单链表,最后一个节点的指针域为NULL。同时,按照一般习惯,我们会定义一个指针plist指向第一个节点。

(2)循环链表

循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

(3) 分类 

1. 是否带头:带头 不带头

2. 方向判断:单向 双向

3. 是否循环:循环 不循环

PS:带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,仅用于避免链表在遍历时出现死循环。

下图为双向链表 - 带头双向循环链表

二. 问题背景

据说著名犹太历史学家Josephus(弗拉维奥·约瑟夫斯)有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

三. 算法思路

具体问题:编号为 1 到 n 的 n 个人围成一圈。从编号为 1 的人开始报数,报到 m 的人离开。下一个人继续从 1 开始报数。n - 1 轮结束以后,只剩下一个人,问最后留下的这个人编号是多少?

1. 步骤一 :初始化 - 创建带环链表

以5个人,每次报2的人淘汰为例,构造下列循环链表:

 

设置prev指针的目的:因后续链表中存在删除操作,当前节点需要被销毁释放,为了能够继续找到pcur的下一个节点,因此需要prev指针来记录pcur的位置。

 2. 步骤二 :遍历与删除 - 计数问题

开始遍历,当2号节点数到2时被删除。在删除2号节点之前,将prev的指针域指向3号节点(即pcur的下个节点),然后释放2号节点。由于此时prev的下个节点正好是3号节点,所以立即将pcur置为3号节点,接着继续重复此步骤,直至只剩下最后一个节点。最后返回其中的值。

遍历过程需要:

  • prev和pcur不断移动
  • 若当前节点涉及删除,则删除后应该从1重新报数,否则继续报数

遍历停止的条件:当prev和pcur重合时,即指向自身。

四. 代码详解

1. 定义节点

  1. typedef struct ListNode {
  2. int val;
  3. struct ListNode* next;
  4. }ListNode;

该结构体(节点)包含一个整型数据和下个节点的地址。

考虑到struct ListNode太难写,我们使用typedef重命名节点名称为ListNode。

2. 创建节点

根据题目中的元素个数n创建节点。因节点不止一个,可以将其封装为一个函数,方便后续在创建更多节点时使用。

  1. ListNode* BuyNode(int x) {
  2. ListNode* node = (ListNode*)malloc(sizeof(ListNode));
  3. if (node == NULL) {
  4. exit(1);
  5. }
  6. node->val = x;
  7. node->next = NULL;
  8. return node;
  9. }

其中传入的参数x即节点的数据域。

3. 创建带环链表

函数原型:

ListNode* CreateCircle(int n) ;

链表初始为空,头节点和尾节点均指向同一节点。

在遍历过程中,ptail会不断改变,而phead会一直记录第一个节点的位置。

  1. ListNode* phead = BuyNode(1);
  2. ListNode* ptail = phead;

不断创建新节点。与此同时,实时更新新的尾节点,将其依次插入到ptail的下一个位置。

  1. for (int i = 2; i <= n; i++) {
  2. ptail->next = BuyNode(i);
  3. ptail = ptail->next;
  4. }

将其首尾相连,即可成环,最终返回ptail。

  1. ptail->next = phead;
  2. return ptail;

注意,在链表的遍历过程当中,指向当前节点的指针要和指向前一个节点的指针同步移动。若返回phead则无法找到ptail。

4. 计数

函数原型:

int ysf(int n, int m);

接收带环链表的返回值,下一个节点即为头节点。同时定义一个计数器。

  1. ListNode* prev = CreateCircle(n);
  2. ListNode* pcur = prev->next;
  3. int count = 1;

根据步骤二,在循环中判断报的数是不是m,若是则删除,否则保留并继续循环。

  1. while (pcur->next!=pcur) {
  2. if (count == m) {
  3. prev->next = pcur->next;
  4. free(pcur);
  5. pcur = prev->next;
  6. count = 1;
  7. }
  8. else {
  9. prev = pcur;
  10. pcur = pcur->next;
  11. count++;
  12. }
  13. }

注意:

  • pcur在释放后的确成为了一个野指针,但pcur本身只是个节点指针,指向的位置的节点被释放了换下个节点的指向就好了,在释放后只要没有对指针解引用都不会有问题
  • 重置时由于pcur已经来到了下个节点的位置,所以count应该从1开始,而不是0
  • 若不存在删除时,指针都要向下一个位置挪动。关于挪动的顺序则应先移动prev进行位置存储。若是先移动pcur则会导致prev找不到原先的pcur而出现问题

返回最后一个节点里的值。

return pcur->val;

五. 整体代码

  1. typedef struct ListNode {
  2. int val;
  3. struct ListNode* next;
  4. }ListNode;
  5. //创建节点
  6. ListNode* BuyNode(int x) {
  7. ListNode* node = (ListNode*)malloc(sizeof(ListNode));
  8. if (node == NULL) {
  9. exit(1);
  10. }
  11. node->val = x;
  12. node->next = NULL;
  13. return node;
  14. }
  15. //创建循环链表
  16. ListNode* CreateCircle(int n) {
  17. ListNode* phead = BuyNode(1);
  18. ListNode* ptail = phead;
  19. for (int i = 2; i <= n; i++) {
  20. ptail->next = BuyNode(i);
  21. ptail = ptail->next;
  22. }
  23. //成环
  24. ptail->next = phead;
  25. return ptail;
  26. }
  27. int ysf(int n, int m) {
  28. //根据n创建带环链表
  29. ListNode* prev = CreateCircle(n);
  30. ListNode* pcur = prev->next;
  31. int count = 1;
  32. //直到链表中只有一个节点时
  33. while (pcur->next!=pcur) {
  34. if (count == m) {
  35. //销毁pcur节点
  36. prev->next = pcur->next;
  37. free(pcur);
  38. pcur = prev->next;
  39. count = 1; //返回默认值
  40. }
  41. else {
  42. //不销毁
  43. prev = pcur;
  44. pcur = pcur->next;
  45. count++;
  46. }
  47. }
  48. //此时剩下的一个节点是要返回节点里的值
  49. }
  50. int main() {
  51. int n = 5;
  52. int m = 2;
  53. printf("留下的人的编号是:%d\n", ysf(n, m));
  54. return 0;
  55. }

六. 思路总结

定义结构体和基本操作:

  • 定义链表节点的结构体 ListNode。
  • 实现创建新节点的函数 BuyNode。

创建循环链表:

实现 CreateCircle 函数,根据给定的节点数量 n 创建一个包含 n 个节点的循环链表。
解决约瑟夫问题:

  • 实现 ysf 函数,通过遍历和删除节点的方式解决约瑟夫问题。
  • 每隔 m 个节点,删除当前节点,直到链表中只剩下一个节点。

实现主函数:

在 main 函数中调用 ysf 函数并打印结果。


若文中有错误之处,望各位学术同道不吝赐教。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/922043
推荐阅读
相关标签
  

闽ICP备14008679号