【C语言数据结构】之单链表

一级目录

二级目录

三级目录

一、基础知识

表中结构具有线性关系,我们称之为线性表,线性表有两大实现方式,一是上一篇文章讲解的顺序表,第二个就是这篇文章我们讲解的链表。链表又有很多种类,单链表,循环链表,双向链表等。
单链表在逻辑上是连续的一段存储结构,但是在物理上不是连续的,结点之间不是在一段连续的空间中,这些结点之间的联系是通过指针的不断指向来完成的。
在这里插入图片描述

  • 每个结点包含一个数据域data和指针域next,数据域存储数据,指针域指向下一个结点的首地址。
  • 一般来说,头结点的data域可以什么信息都不存储,这样方便对链表进行操作,也可以保存最后一个结点的首地址(这样可以方便找到最后一个结点),首元结点才是数据开始的第一个结点,就相当于数组的下标为0的意思。
  • 链表不存在说链表存满这种说法,和顺序表不同的是,我们不是一次性开辟一段地址连续的存储空间,而是需要一个结点来创建一个结点,并且这些结点在物理层面上不是连续的。
  • 操作单链表实际上就是操作指针进行不断的指向,在脑海中,一定需要明确指针这时候在哪里,在干什么。

二、单链表的实现

1. 结点结构体和需要的宏定义

# define True 1
# define False 0
# typedef struct SeqLink
{
	int data;//数据域
	struct SeqLink* next;//指针域
}Link;

2.创建一个头结点

Link* CreatLink()
{
	Link* p = (Link*)malloc(sizeof(Link));
	if( p == NULL)
	{
		exit(1);//结点创建失败	
	}
	memset(p,0,sizeof(Link));
	return p;
}

3.判断是否为空

在这里插入图片描述如果说头结点的next指向空,说明没有首元结点,说明这个链表是空的。

int IsEmpty(Link* p)
{
	if(p->next == NULL)
	{
		return True;
	}
	return False;
}

4. 打印单链表

定义一个指针指向首元结点,然后开始循环,当这个结点不为空的时候说明这个结点存在,所以打印并指针向下一个指向。

void PrintLink(Link* p)
{
    if(IsEmpty(p) == True)
    {
        printf("空的怎么打印你教教我\n");
        return;
	}
    Link* tmp = p->next;
   	while(tmp!=NULL)
    {
        printf("%d ",tmp->data);
        tmp = tmp->next;
    }
    putchar('\n');
}

5. 头插

在这里插入图片描述
一定要注意赋值的先后顺序,如果先让头结点指向了新结点node,那么就丢失了原本的首元结点的地址了。

void InsertHead(Link* p,int num)
{
	Link* node = (Link*)malloc(sizeof(Link));
	if(node == NULL)
	{
		return;
	}
	node->data = num;
	node->next = p->next;
	p->next = node;
}

6. 尾插


第一步,遍历到最后一个结点。
第二步,让最后一个结点连接上新的结点
第三步,让新结点指向空

void InsertBack(Link* p,int num)
{
	if(IsEmpty(p) == True)
	{
		return;
	}
	Link* node = (Link*)malloc(sizeof(Link));
	Link* tmp = p->next;
	while(tmp!=NULL)
	{
		tmp = tmp->next;
	}
	tmp->next = node;
	node->data = num;
	node->next = NULL;
}

创建了一个Link*类型的tmp的作用是操作的时候保留下来了头指针,要不然就会找不到这个链表了。

7. 指定位置插入

在这里插入图片描述
和尾插类似,只不过从遍历到最后一个元素变成遍历到指定位置这个元素。
我将插入的这个结点叫i,前面的结点叫i-1,后面的叫i+1
需要注意的是,需要先将i指向i+1,再让i-1指向i
如果这个顺序颠倒过来,就会丢失i+1的位置。

void InsertPosition(Link* p,int num)
{
	Link* tmp =p;
	Link* node = (Link*)malloc(sizeof(Link));
	int i = 0;
	while(tmp->next!=NULL)
	{
		if(i==index)
		{
			node->next = tmp->next;
			node->data = num;
			tmp->next = node;
			return;
		}
		i++;
		tmp = tmp->next;
	}
	return;
}

8. 头删

在这里插入图片描述

void PopHead(Link* p)
{
	if(IsEmpty(p) == True)
	{
		return;
	}
	Link* tmp = p->next;//首元结点
	p->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

9. 尾删

在这里插入图片描述
尾删需要两个指针来完成,一个指向最后一个结点(要删除的)
另一个指向它的前一个结点,我们让前一个结点指向空,并且释放掉最后一个结点就可以了。

void PopBack(Link* p)
{
	if(IsEmpty(p) == True)
	{
		return;
	}
	Link* tmp1 = p;
	Link* tmp2 = p->next;
	while(tmp2!=NULL)
	{
		tmp1 = tmp1->next;
		tmp2 = tmp2->next;
	}
	tmp1->next = NULL;
	free(tmp2);
	tmp2 = NULL;
}

10.指定位置删除

在这里插入图片描述
还是需要两个结点,和尾删理解差不多,找到这个元素之后,让前一个元素指向它的下一个元素,然后释放掉这个元素。
需要注意的是下标是否超出原本链表长度。

void PopPosition(Link* p,int index)
{
	if(TsEmpty(p) == True)
	{
		return;
	}
	int i;
	Link* tmp1 = p;
	Link* tmp2 = p->next;
	while(tmp2!=NULL)
	{
		i++;
		tmp2 = tmp2->next;
	}
	if(index>i||index<0)
	{
		return;//索引错误
	}
	i = 0;
	tmp2 = p->next;
	while(i<index)
	{
		i++;
		tmp2 = tmp2->next;
		tmp1 = tmp1->next;
	}
	tmp1->next = tmp2->next;
	free(tmp2);
	tmp2 = NULL;
}

11.指定元素删除

从头到尾遍历单链表,找到这个元素就删除这个结点,没找到继续往后遍历,知道单链表结束。

void PopNum(Link* p,int num)
{
	if (IsEmpty(p) == True)
	{
		printf("空的我删你妈你问问问\n");
		return;
	}
	Link* tmp1 = p;
	Link* tmp2 = p->next;
	while(tmp2!=NULL)
	{
		if(tmp2->data = num)
		{
			tmp1->next = tmp2->next;
			free(tmp2);
			tmp2 = NULL;
			return;
		}
		tmp1 = tmp1->next;
		tmp2 = tmp2->next;
	}
}

12.更改指定位置的数据

遍历到这个结点,将这个结点的data重新赋值即可

void ChangePosition_Num(Link* p,int index,int new)
{
	if (IsEmpty(p) == True)
	{
		return;
	}
	int i = 0;
	Link* tmp = p->next;
	while(i<index)
	{
		i++;
		tmp = tmp->next;
		if(tmp == NULL)
		{
			printf("下标超出\n");
			return;
		}
	}
	tmp->data = new;
}

13.查询元素位置

遍历单链表,找到这个元素了就返回这个下标。

int SearchNum_Position(Link*p,int num)
{
	Link* tmp = p->next;
	int i =0;
	while(tmp!=NULL)
	{
		if(tmp->data == num)
		{
			return i;
		}
		i++;
		tmp = tmp->next;
	}
	return False;
}

14.通过下标查询元素值

遍历单链表,找到下标所对应的结点,返回这个结点的数据域。

int SearchPosition_Num(Link* p,int index)
{
	int i =0;
	Link* tmp = p->next;
	while(tmp!=NULL)
	{
		if(i == index)
		{
			return tmp->data;
		}
		i++;
		tmp = tmp->next;
	}
	return False;
}

三、总结

  • 操作单链表实际上就是指针的不断指向,我们要在脑海中对这个指针目前指向哪里,他其中包含什么数据要清楚。
  • 单链表一个结点就包含两个数据,一个数据域,一个指向下一个结点的指针域。
  • 在进行某些删除和插入结点的操作的时候,一定要注意断开连接和建立新连接的顺序,否则会造成丢失后面数据的局面出现。
二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) 二.内核链表 内核链表是一种链表,Linux内核中的链表都是用这种形式实现的 1.特性 内核链表是一种双向循环链表,内核链表的节点节点结构中只有指针域 使用内核链表的时候,将内核链表作为一个成员放入到一个结构体中使用 我们在链表中找到内核链表结构的地址,通过这个地址就可以找到外部大结构体的地址,通过大结构体就可以访问其中的成员 优势: 内核链表突破了保存数据的限制,可以用内核链表来保存任何数据(使用一种链表表示各种类型的数据,通用性很强) 内核链表中只有指针域,维护起来更加方便,效率更高 2.使用 内核链表在内核中已经被实现,我们只需要调用其接口直接使用即可 内核链表的实现代码在内核源代码的list.h文件中 3.源代码分析 (1)节点结构: struct list_head { struct list_head *next, *prev;//前置指针 后置指针 }; (2)初始化 #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) (3)插入 //从头部插入 static inline void list_add(struct list_head *new, struct list_head *head)//传入要插入的节点和要插入的链表 { __list_add(new, head, head->next); } //从尾部插入 static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } (4)通过节点找到外部结构体的地址 //返回外部结构体的地址,第一个参数是节点地址,第二个参数是外部结构体的类型名,第三个参数是节点在外部结构体中的成员名 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) (5)遍历内核链表 //遍历内核链表 #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); \ pos = pos->next) //安全遍历内核链表 #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next) C语言下的单链表,可以增加,删除,查找,销毁节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LiuJWHHH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值