目录

一、链表的概念

1、定义:

2、链表的性质特点

​编辑

二、单向链表

1、单向链表的构成

2、链表与数组的区别

3、两种单向链表的创建

4、带头结点的单向链表的接口

注意,注意,注意,在此处我想提醒各位链表操作的核心永远是node->next,此处的动用一定要慎重,因为单向链表中node = node->next的操作是不可逆的,且我们通常查找还是插入动用的都是node->next。

4.1链式表的通用接口

4.2头插

4.3任意位置插入

4.4尾插

4.5删除指定节点

4.6删除所有节点

4.7展现节点内数据

5、带头指针的单向链表的接口

4.1头插

4.2任意位置插入

4.3尾插

4.4删除指定节点

4.5删除所有节点

4.6展示节点内数据

6.66、单向链表的小彩蛋

7、总结

三、单向循环链表

1、循环链表的图示

2、循环链表的节点创建

3、循环链表的接口

3.1循环链表的通用接口

3.2初始化链表

3.3头插

3.4尾插

3.5删除指定节点

3.6删除所有节点

3.7展现节点中元素

4.循环链表的应用:约瑟夫环问题

四、双向循环链表

那么接下来大的就要来了

1、双向链表的节点创建

2、双向链表的各个接口

3、双向循环列表的接口

3.1双向循环链表的通用接口

3.2初始化链表

3.3头插

3.4尾插

3.5删除指定节点

3.6删除所有节点

3.7展现节点数据

五、结语


引用:本人所作数据结构系列语言只会用到c,且所有代码都会以工程接口化的形式写出,类似请看顺序表篇章:数据结构——顺序表

一、链表的概念

1、定义:

 链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构。

2、链表的性质特点

1、动态大小:链表的大小可以在运行时动态变化,初始化定不出链表的最终大小
2、节点:每个节点存储数据,并包含一个或多个指针指向列表中的其他节点。
3、无需连续内存:由于节点通过指针连接,它们不需要在内存中连续存放,这使得内存使用更加灵活。
4、增删操作:在链表中插入或删除节点通常比数组更快,因为这些操作只需要作用在插入位置的前驱和后继,而不是整体移动。

二、单向链表

1、单向链表的构成

链表由一个个节点构成,每个节点一般采用结构体的形式定义:

typedef int element_t;
typedef struct link_node
{
	element_t data;
	struct link_node* next;
} Lnode;

如上文,单向链表的节点一般有两个域构成:

1、数据域:存储该节点的信息

2、指针域:存放下一个节点的地址

2、链表与数组的区别

在上一次顺序表的讲解当中,我们谈及了顺序表和数组的区别,那么我们现在谈谈链表和数组的区别:

链表是通过节点的指针把离散的数据链接成一个表,并通过对节点的插入和删除操作从而实现数据的存取,而数组是通过开辟一段连续的内存空间来存储数据,这是数组和链表最大的区别。

3、两种单向链表的创建

我们在创建一个链表时,我们有两种链表操作可选

1、带头指针的链表

2、带头结点的链表

在对比其上两种操作,我想先插入一个小知识点,即我们在添加链表时,我们通常进行这样的操作:

1、创建新节点

2、先处理新结点的next再处理老结点的next

node->next = p->next
p->next = node;

这时候我们再想,如果我们用第一种带头指针的节点的话,那么我们对链表进行头插法是不是就需要进行if特判,因为对于头插且链表还没有元素时,我们进行node->next = p->next时(p此时就是头指针)p为空,我们对一个空指针访问就会发生段错误,那么此时就会出现不一致的情况,即需要用if判断,这的确比带头结点的链表处理起来要麻烦。

那么我们开始定义链表节点的结构体

/* 链式表 数据结构定义 先定义节点 */
typedef int element_t;
typedef struct link_node
{
	element_t data;
	struct link_node* next;
} Lnode;

/* 带头结点的链表 */
typedef struct
{
	Lnode head;
	int num;
} LinkTable;

/* 带头指针的链表 */
typedef struct
{
	Lnode* ptr_head;
	int num;
} LinkList;

由易到难,我们首先会讲带头结点的链表的一系列实现

4、带头结点的单向链表的接口

注意,注意,注意,在此处我想提醒各位链表操作的核心永远是node->next,此处的动用一定要慎重,因为单向链表中node = node->next的操作是不可逆的,且我们通常查找还是插入动用的都是node->next。

/* 带头节点的单向链表 接口 */
void insertHeadTable(LinkTable *table, element_t v);				// 头插
void insertRearTable(LinkTable* table, element_t v);				// 尾插
void insertPosTable(LinkTable* table, int pos, element_t v);		// 在指定pos位置插入

void deleteValueTable(LinkTable* table, element_t v);				//删除指定值
void deleteAllLinkTable(LinkTable* table);							//删除所有数据节点
void showLinkTable(const LinkTable* table);							//显示
4.1链式表的通用接口

考虑到不同的值的插入、删除、显示都是同一套操作,我们此处继续采用static定义三个专用接口

/* 链式表的通用接口 */
static int insertLnode(Lnode* p, element_t v)
{
	Lnode* node = malloc(sizeof(Lnode));
	if (node == NULL)
	{
		return 0;
	}
	node->data = v;
	node->next = p->next;
	p->next = node;
	return 1;
}

static void showLnode(const Lnode *start)
{
	while (start)
	{
		printf("%d ", start->data);
		start = start->next;
	}
	printf("\n");
}

static void deleteLnode(Lnode* p)
{
	Lnode* tmp = p->next;
	p->next = tmp->next;
	free(tmp);
}
4.2头插
void insertHeadTable(LinkTable* table, element_t v)
{
	Lnode* p = &table->head;
	if (insertLnode(p, v))
	{
		++table->num;
	}
}
4.3任意位置插入
void insertPosTable(LinkTable* table, int pos, element_t v)
{
	if (pos < 0||pos > table->num)
	{
		return;
	}
	Lnode* p = &table->head;
	int i = 0;				//找到pos-1的位置
	while (i < pos && p != NULL)
	{
		p = p->next;
		++i;
	}
	if (p && insertLnode(p, v))
	{
		++table->num;
	}
}
4.4尾插
void insertRearTable(LinkTable* table, element_t v)
{
	Lnode* p = &table->head;
	while (p->next)
	{
		p = p->next;
	}
	if (insertLnode(p, v))
	{
		++table->num;
	}
}
4.5删除指定节点
void deleteValueTable(LinkTable* table, element_t v)
{
	Lnode* p = &table->head;		//把头结点的地址,假设作为前置节点p
	// 站在p,往后看他下一个节点的data是否=v
	while (p && p->next)
	{
		if (p->next->data == v)
		{
			break;
		}
		p = p->next;
	}

	//找到v值前面的的节点首地址
	if (p && p->next)
	{
		deleteLnode(p);
		--table->num;
	} 
}
4.6删除所有节点
void deleteAllLinkTable(LinkTable* table)
{
	//以头结点为固定点,相当于删除节点的前置节点,一直删除后面的节点,直到头结点后面为空
	Lnode* p = &table->head;
	while (p->next)
	{
		deleteLnode(p);
		--table->num;
	}
	printf("table->num = %d", table->num);
}
4.7展现节点内数据
void showLinkTable(const LinkTable* table)
{
	printf("table[%d] : ", table->num);
	Lnode* start = table->head.next;
	showLnode(start);
}

5、带头指针的单向链表的接口

那么带头结点的单向链表就到这里,接下来我们进入带头指针的链表操作

老样子,先上接口

/* 带头指针的链表 接口 */
void insertHeadList(LinkList *List, element_t v);					// 头插
void insertRearList(LinkList *List, element_t v);					// 尾插
void insertPosList(LinkList* List, int pos, element_t v);			// 在指定pos位置插入

void deleteValueList(LinkList* List, element_t v);					// 删除指定值
void deleteAllLinkList(LinkList* List);
void showLinkList(const LinkList* List);							// 显示
4.1头插

此处我们有两种操作,第一种也就是在代码块被注释掉的这种就是上文所讲的if判断来操作,但是我在此处更提倡第二种,也就是先转换为带头结点的链表,再对链表进行操作,我们接下来的剩余操作也将围绕第二种进行

void insertHeadList(LinkList* List, element_t v)
{
	//Lnode* node = malloc(10);
	//if (List->ptr_head == NULL)
	//{
	//	List->ptr_head = node;
	//}
	//else
	//{
	//	node->next = List->ptr_head;
	//	List->ptr_head = node;
	//}
	Lnode head;
	head.next = List->ptr_head;

	Lnode* p = &head;
	if (insertLnode(p, v))
	{
		List->ptr_head = head.next;	//更新指针
		++List->num;
	}
}
4.2任意位置插入
void insertPosList(LinkList* List, int pos, element_t v)
{
	if (pos < 0 || pos > List->num)
	{
		return;
	}
	Lnode head;
	head.next = List->ptr_head;

	Lnode* p = &head;
	int i = 0;				//找到pos-1的位置
	while (i < pos && p != NULL)
	{
		p = p->next;
		++i;
	}

	if (p && insertLnode(p, v))
	{
		++List->num;
	}
}
4.3尾插
void insertRearList(LinkList* List, element_t v)
{
	Lnode head;
	head.next = List->ptr_head;

	Lnode* p = &head;
	while (p->next)					//遍历到最后一个节点
	{
		p = p->next;
	}
	if (insertLnode(p, v))
	{
		List->ptr_head = head.next;
		++List->num;
	}
}
4.4删除指定节点
void deleteValueList(LinkList* List, element_t v)
{
	Lnode head;
	head.next = List->ptr_head;

	Lnode* p = &head;		//把头结点的地址,假设作为前置节点p
	// 站在p,往后看他下一个节点的data是否=v
	while (p && p->next)
	{
		if (p->next->data == v)
		{
			break;
		}
		p = p->next;
	}

	//找到v值前面的的节点首地址
	if (p && p->next)
	{
		deleteLnode(p);
		List->ptr_head = head.next;
		--List->num;
	}
}
4.5删除所有节点
void deleteAllLinkList(LinkList* List)
{
	Lnode head;
	head.next = List->ptr_head;

	Lnode* p = &head;
	while (p->next)
	{
		deleteLnode(p);
		--List->num;
	}
	printf("List->num = %d", List->num);
	List->ptr_head = NULL;
}
4.6展示节点内数据
void showLinkList(const LinkList* List)
{
	printf("List[%d] : ", List->num);
	Lnode* start = List->ptr_head;
	showLnode(start);
}

那么我们的头指针的链表也在此完结。

6.66、单向链表的小彩蛋

那么这一时刻又有人要问了,说y哥y哥,你的两种链表形式怎么都与leetcode里面的不一样,那么好,我告诉你:

我们都知道leetcode中,他只是给了首元节点,而没有头节点和头指针这种类型的链表。那么我们这个时候又该如何操作呢,此处只给出了插入的操作,其他操作因篇幅有限且lc也有,暂不提及。

Lnode* insertLink(Lnode *head, element_t v)
{
	Lnode dummy;
	dummy.next = head;

	Lnode* p = &dummy;
	if (insertLnode(p, v))
	{
		return dummy.next;
	}
	return NULL;
}

可以看到我们依旧为了方便将其转化为带头结点的链表。

7、总结

故遇到链表时,我们就往带头结点的链表上靠,当头结点出来以后,之后的所有操作都会好想很多。

三、单向循环链表

1、循环链表的图示

那么问题来了,下方的链表是循环链表吗,聪明的小伙伴已经发现了我的图中根本没标循环链表,

那就对了,因为这确实不是循环链表,这只是能提高尾插效率的一种双指针链表。

那么这种才是真正的循环链表,不过这也是其中的一种

为了方便,我们的代码实现将会采用下面这一种循环链表

2、循环链表的节点创建

typedef int element_t;

typedef struct Linknode
{
	element_t data;
	struct Linknode* next;
}Lnode;

typedef struct
{
	Lnode head;
	int num;
	Lnode* rear;
}LinkLoop;

3、循环链表的接口

// 静态初始化表头
void initLinkLoop(LinkLoop* link_loop);
// 插入,头插尾插
void insertLinkLoopHeader(LinkLoop* link_loop, element_t v);
void insertLinkLoopRear(LinkLoop* link_loop, element_t v);

// 删除
int deleteLinkLoop(LinkLoop *link_loop, element_t v);
void deleteAllLinkLoop(LinkLoop* link_loop);

// 遍历
void showLinkLoop(const LinkLoop *link_loop);
3.1循环链表的通用接口

同样,考虑到插入和删除的统一性,我们创建通用接口来提高效率

static void insertLinkLoop(LinkLoop* link_loop, Lnode* p, element_t v)
{
	Lnode* node = (Lnode*)malloc(sizeof(Lnode));
	if (node == NULL)
	{
		return;
	}
	node->data = v;
	
	node->next = p->next;
	p->next = node;
	if (link_loop->rear == p)//判断是否是第一个元素插入
	{
		link_loop->rear = node;
	}
	++link_loop->num;
}

static void delLinkLoop(LinkLoop* link_loop, Lnode* p)
{
	if (!p || p->next == &link_loop->head)
	{
		return;
	}
	Lnode* tmp = p->next;
	p->next = tmp->next;
	if (tmp == link_loop->rear)//若p下一个节点是尾指针则将尾指针交给p
	{
		link_loop->rear = p;
	}
	free(tmp);
	--link_loop->num;
}
3.2初始化链表
void initLinkLoop(LinkLoop* link_loop)
{
	link_loop->head.next = &link_loop->head;
	link_loop->rear = &link_loop->head;
	link_loop->num = 0;
}
3.3头插
void insertLinkLoopHeader(LinkLoop* link_loop, element_t v)
{
	Lnode* p = link_loop->rear->next;//&link_loop->head也可
	insertLinkLoop(link_loop, p, v);
}
3.4尾插
void insertLinkLoopRear(LinkLoop* link_loop, element_t v)
{
	Lnode* p = link_loop->rear;
	insertLinkLoop(link_loop, p, v);
}
3.5删除指定节点
int deleteLinkLoop(LinkLoop* link_loop, element_t v)
{
	Lnode* p = &link_loop->head;
	while (p->next != &link_loop->head)
	{
		if (p->next->data == v)
		{
			delLinkLoop(link_loop, p);
			return 1;		//找到
		}
		p = p->next;
	}
	return 0;		//没找到
}
3.6删除所有节点
void deleteAllLinkLoop(LinkLoop* link_loop)
{
	Lnode* p = &link_loop->head;
	while (p->next != &link_loop->head)
	{
		delLinkLoop(link_loop, p);
	}
	link_loop->rear = &link_loop->head;
	printf("list num = %d\n", link_loop->num);
}
3.7展现节点中元素
void showLinkLoop(const LinkLoop* link_loop)
{
	Lnode* p = link_loop->head.next;
	printf("loop_table[%d] : ", link_loop->num);
	while (p != &link_loop->head) //开头已经不是head了
	{
		printf("%d ", p->data);
		p = p->next;
	}
	printf("\n");
}

4.循环链表的应用:约瑟夫环问题

void init_JoseGame(JosephHead* game, int n)
{
	Node* node = NULL;
	for (int i = 1; i <= n; i++)
	{
		node = (Node*)malloc(sizeof(Node));
		node->data = i;
		if (game->head == NULL)//第一次插入
		{
			game->head = game->tail = node;
		}
		else
		{
			game->tail->next = node;//连接
			game->tail = node;
		}
		game->tail->next = game->head;//回头
	}
}

void showData(const JosephHead* game)
{
	Node* p = game->head;
	do {
		printf("%d\t", p->data);
		p = p->next;
	} while (p != game->head);
	printf("\n");
}

void start_JosephGame(JosephHead* game, int k)
{
//	Node* p = game->head;
//	while (1)
//	{
//		int i = 0;
//		for (i = 1; i < k - 1; i++)
//		{
//			p = p->next;
//		}
//		if (p->next->next == p)
//		{
//			printf("end : %d", p->data);
//			break;
//		}
//		Node* tmp = p->next;
//		p->next = tmp->next;
//		p = p->next;
//	}
	Node* fap = game->head; // 指向正在报数的人
	Node* slp = NULL;		// 
	while (fap->next != fap)
	{
		slp = fap;
		// 按照k值报数
		for (int i = 1; i < k; i++)
		{
			slp = fap;
			fap = fap->next;
		}
		// 使用slp删除后一个节点fap
		slp->next = fap->next;
		printf("%d ", fap->data);
		free(fap);
		fap = slp->next;
	}
	printf("\n the last Person : %d\n", fap->data);
	free(fap);
	game->head = game->head = NULL;
}

四、双向循环链表

此处我们的操作都将借用Linus Benedict Torvalds神也就是linux和git创作者及其团队写出来的代码来讲解,注意此处并非原创,早在linux内核里就有了,因为太NB,我将会借用大神的部分代码来讲。

废话不多说,我们首先先来看双向链表的初始化图

那么接下来大的就要来了

双向链表后面所有操作最核心的操作图就这么来了,即双向链表的插入逻辑

我们操作的顺序逆正都可,此处画和表达的是逆时针

接下来我们看看代码实现,领略大神的逻辑思想

1、双向链表的节点创建

typedef int element_t;
typedef struct dnode
{
	struct dnode* prev;
	element_t data;
	struct dnode* next;
} DNode;

// 带头结点的双向循环链表表头
typedef struct
{
	DNode head;
	int num;
} DLoopList;

2、双向链表的各个接口

void initDLoopList(DLoopList *loop_link);
void insertDLoopLinkHead(DLoopList* loop_link, element_t v);
void insertDLoopLinkRear(DLoopList* loop_link, element_t v);
void showDLoopLink(const DLoopList* loop_link);

void deleteValueDLoopList(DLoopList* loop_link, element_t v);
void deleteAllDLoopList(DLoopList* loop_link);

3、双向循环列表的接口

3.1双向循环链表的通用接口

依依旧旧考虑到插入和删除的统一性,依依旧旧考虑到效率,我们用static限制在此文件里

/* 定义通用的辅助函数接口 插入,删除 */
static void addDNode(DNode* new_node, DNode *prev, DNode *next)
{
	next->prev = new_node;
	new_node->next = next;
	new_node->prev = prev;
	prev->next = new_node;
}

static void delDNode(DNode* prev, DNode* next)
{
	next->prev = prev;
	prev->next = next;
}
3.2初始化链表
void initDLoopList(DLoopList* loop_link, element_t v)
{
	loop_link->head.next = &loop_link->head;
	loop_link->head.prev = &loop_link->head;
	loop_link->num = 0;
}
3.3头插
void insertDLoopLinkHead(DLoopList* loop_link, element_t v)
{
	DNode* new_node = malloc(sizeof(DNode));
	new_node->data = v;
	addDNode(new_node, &loop_link->head, loop_link->head.next);
	++loop_link->num;
}
3.4尾插
void insertDLoopLinkRear(DLoopList* loop_link, element_t v)
{
	DNode* new_node = malloc(sizeof(DNode));
	new_node->data = v;
	addDNode(new_node, loop_link->head.prev, &loop_link->head);
	++loop_link->num;
}
3.5删除指定节点
void deleteValueDLoopList(DLoopList* loop_link, element_t v)
{
	DNode* p = loop_link->head.next;
	while (p != &loop_link->head)
	{
		if (p->data == v)
		{
			delDNode(p->prev,p->next);
			free(p);
			--loop_link->num;
			return;
		}
		p = p->next;
	}
	printf("Not found\n");
}
3.6删除所有节点
void deleteAllDLoopList(DLoopList* loop_link)
{
	DNode* p = loop_link->head.next;
	while (p != &loop_link->head)
	{
		DNode* tmp = p->next;
		free(p);
		p = tmp;
		--loop_link->num;
	}
	loop_link->head.next = &loop_link->head;
	loop_link->head.prev = &loop_link->head;
	printf("table num : %d", loop_link->num);
}
3.7展现节点数据
void showDLoopLink(const DLoopList* loop_link)
{
	DNode* p = loop_link->head.next;
	printf("table[%d] : ", loop_link->num);
	while (p != &loop_link->head)
	{
		printf("%d ", p->data);
		p = p->next;
	}
	printf("\n");
}

五、结语

至此,链表集大成之作结束。

所有测试运行代码

接下来公布各接口测试代码

单向链表

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

//测试带头结点的链表
void test01()
{
	LinkTable table;
	table.num = 0;
	table.head.next = 0;

	for (int i = 0; i < 5; i++)
	{
		//insertHeadTable(&table, i + 100);
		insertRearTable(&table , i + 100);
	}
	showLinkTable(&table);
	insertPosTable(&table, 2, 500);
	showLinkTable(&table);
	deleteValueTable(&table, 500);
	showLinkTable(&table);
	deleteAllLinkTable(&table);
}

void test02()
{
	LinkList List;
	List.num = 0;
	List.ptr_head = NULL;

	for (int i = 0; i < 5; i++)
	{
		insertHeadList(&List, i + 200);
	}
	insertPosList(&List, 2, 500);
	showLinkList(&List);
	deleteValueList(&List, 204);
	showLinkList(&List);
	deleteAllLinkList(&List);
}

void test03()
{
	Lnode* head = NULL;
	for (int i = 0; i < 5; i++)
	{
		head = insertLink(head, 10 + i);
	}

	Lnode* p = head;
	while (p)
	{
		printf("%d\t", p->data);
		p = p->next;
	}
	printf("\n");
}

int main()
{
	test01();
	//test02();
	//test02();

	return 0;
}

循环链表

#include"LinkLoopList.h"
#include<stdio.h>

void test01()
{
	LinkLoop link_loop;
	initLinkLoop(&link_loop);

	for (int i = 0; i < 10; i++)
	{
		//insertLinkLoopHeader(&link_loop, i + 100);
		insertLinkLoopRear(&link_loop, i + 100);
	}
	showLinkLoop(&link_loop);
	deleteLinkLoop(&link_loop, 104);
	showLinkLoop(&link_loop);
	deleteAllLinkLoop(&link_loop);
}

void JosephGameTest()
{
	JosephHead game1 = { NULL,NULL };
	init_JoseGame(&game1, 10);
	showData(&game1);
	printf("start game : \n");
	start_JosephGame(&game1, 3);
}

int main()
{
	//test01();
	JosephGameTest();
	return 0;
}

双向循环链表

#include"DoubleLoopList.h"

DLoopList stu_table;
void test01()
{
	initDLoopList(&stu_table);
	for (int i = 0; i < 5; i++)
	{
		insertDLoopLinkHead(&stu_table, i+100);
	}
	showDLoopLink(&stu_table);
	deleteValueDLoopList(&stu_table, 103);
	showDLoopLink(&stu_table);
	deleteAllDLoopList(&stu_table); 
}

int main()
{
	test01();
	return 0;
}

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步我们下次再见。

上期内容:顺序表

下期内容:顺序栈和链式栈

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐