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

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

一. 预备知识 

1. 结构体&指针

(1)typedef重定义

#include <stdio.h>

// 定义一个结构体用于表示矩形的长和宽
typedef struct {
    int length;
    int width;
} Rectangle;

int main() {
    // 使用typedef重新定义int为Length, 表示长度
    typedef int Length;
    
    // 使用Rectangle结构体类型定义rect1和rect2变量
    Rectangle rect1 = {10, 5};
    Rectangle rect2 = {8, 6};

    // 使用Length类型定义length1和length2变量
    Length length1 = 10;
    Length length2 = 5;

    // 输出矩形的长和宽
    printf("Rectangle 1: Length = %d, Width = %d\n", rect1.length, rect1.width);
    printf("Rectangle 2: Length = %d, Width = %d\n", rect2.length, rect2.width);

    // 输出长度
    printf("Length 1: %d\n", length1);
    printf("Length 2: %d\n", length2);

    return 0;
}

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

#include <stdio.h>
#include <stdlib.h>

// 定义一个结构体表示学生信息
struct Student {
    char name[20];
    int age;
};

// 函数:打印学生信息
void printStudentInfo(struct Student* ptr) {
    printf("Student Name: %s\n", ptr->name);
    printf("Student Age: %d\n", ptr->age);
}

int main() {
    // 声明并初始化一个结构体变量
    struct Student student1 = {"Alice", 20};

    // 声明一个结构体指针,并指向结构体变量student1
    struct Student* ptr_student = &student1;

    // 通过结构体指针访问和修改结构体成员
    printf("Before modification:\n");
    printStudentInfo(ptr_student);

    // 修改结构体成员
    strcpy(ptr_student->name, "Bob");
    ptr_student->age = 25;

    // 打印修改后的学生信息
    printf("\nAfter modification:\n");
    printStudentInfo(ptr_student);
   
    return 0;
}

2. 动态开辟内存

(1)malloc的使用

原型:void* malloc (size_t size);

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

#include <stdio.h>
#include <stdlib.h>

// 定义一个结构体表示学生信息
struct Student {
    char name[20];
    int age;
};

int main() {
    // 使用malloc动态分配内存给结构体指针
    struct Student* ptr_student = (struct Student*)malloc(sizeof(struct Student));
    
    // 检查内存是否成功分配
    if (ptr_student == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    // 设置结构体成员值
    strcpy(ptr_student->name, "Alice");
    ptr_student->age = 20;

    // 打印结构体成员值
    printf("Student Name: %s\n", ptr_student->name);
    printf("Student Age: %d\n", ptr_student->age);

    // 释放动态分配的内存
    free(ptr_student);

    return 0;
}

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. 定义节点

typedef struct ListNode { 
	int val;  
	struct ListNode* next;
}ListNode;

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

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

2. 创建节点

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

ListNode* BuyNode(int x) {
	ListNode* node = (ListNode*)malloc(sizeof(ListNode)); 
	if (node == NULL) {
		exit(1);
	}
	node->val = x;
	node->next = NULL;
	return node;
}

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

3. 创建带环链表

函数原型:

ListNode* CreateCircle(int n) ;

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

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

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

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

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

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

ptail->next = phead;
return ptail;

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

4. 计数

函数原型:

int ysf(int n, int m);

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

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

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

while (pcur->next!=pcur) {
		if (count == m) {
			prev->next = pcur->next;
			free(pcur);
			pcur = prev->next;
			count = 1;  
		}
		else {
			prev = pcur;
			pcur = pcur->next;
			count++;
		}
	}

注意:

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

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

return pcur->val;

五. 整体代码

typedef struct ListNode { 
	int val;  
	struct ListNode* next;
}ListNode;

//创建节点
ListNode* BuyNode(int x) {
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL) {
		exit(1);
	}
	node->val = x;
	node->next = NULL;
	return node;
}

//创建循环链表
ListNode* CreateCircle(int n) {
	ListNode* phead = BuyNode(1);
	ListNode* ptail = phead;
	for (int i = 2; i <= n; i++) {
		ptail->next = BuyNode(i);
		ptail = ptail->next;
	}
	//成环
	ptail->next = phead;
	return ptail;  

}

int ysf(int n, int m) {
	//根据n创建带环链表
	ListNode* prev = CreateCircle(n);
	ListNode* pcur = prev->next;
	int count = 1;
	//直到链表中只有一个节点时
	while (pcur->next!=pcur) {
		if (count == m) {
			//销毁pcur节点
			prev->next = pcur->next;
			free(pcur);
			pcur = prev->next;
			count = 1;  //返回默认值
		}
		else {
			//不销毁
			prev = pcur;
			pcur = pcur->next;
			count++;
		}
	}
	//此时剩下的一个节点是要返回节点里的值  
	 
}

int main() { 
	int n = 5; 
	int m = 2; 
	printf("留下的人的编号是:%d\n", ysf(n, m));
	return 0; 
}

六. 思路总结

定义结构体和基本操作:

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

创建循环链表:

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

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

实现主函数:

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


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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值