C语言实现单链表

引入:

对于顺序表来说,开辟的空间是连续的,可以利用下标随机访问,而且尾插尾删时,时间复杂度是O(1),但是对于头插头删,以及中间插入和删除时,因为要挪动元素,所以时间复杂度是O(n)。而且因为空间是连续的,所以malloc开辟时,一旦空间不够,那么就要在内存空间中另找一块大的空间来存放。而且容量不够时要增容,所以会有一定的系统消耗。

而链表的特点:

1、空间按需索取

2、空间利用率提高

3、不要求物理空间连续,头部和中间的插入不需要挪动数据。

链表头插头删和中间插入和删除时间复杂度都是O(1)。

但缺点是不能随机访问,每次都只能从头开始一个个往后找,所以链表的尾插尾删的时间复杂度是O(n)。

下面是一个链表的代码(注释很详细就不再赘述了):

test.c(测试链表的接口用)

#include"SList.h"
void TestSList2()
{
	SLTNode* plist = NULL;//创建一个结构体指针
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	SLTNode* pos = SLTFind(plist, 3);
	if (pos)
	{
		SLTEraseAfter(plist, pos);
	}
	
	SLTPrint(plist);
	SLTDestory(&plist);
	SLTPrint(plist);
	
	

}

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

SList.h(各种函数声明和一些#define定义)

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define SLTDataType int//将数据类型进行重命名,方便将来存储不同类型的数据
typedef struct SListNode
{ 
	SLTDataType data;//数据域
	struct SListNode* next;//指针域

}SLTNode;

void SLTPrint(SLTNode* phead);//打印

//打印链表不会改动头指针,所以传一级指针,函数内部形参的改变不会影响到外部的实参。


void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插

//链表尾插,有可能链表为空,此时就会改变头指针有,将动态开辟的新结点赋给头结点,此时函数内部的变化就需要
//传递到外部,所以用二级指针。


void SLTPopBack(SLTNode** pphead);//尾删
//尾删时可能只有头结点,所以可能会删掉头结点,所以函数内部的变化就需要
//传递到外部,所以用二级指针。


void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插
//头插会改变头指针,所以要传二级指针。



void SLTPopFront(SLTNode** pphead);//头删
//头删会改变头指针,所以要传二级指针。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查改
//查改不会改变头指针,所以传一级指针。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在pos位置之前插入
//之前插入会有改变头结点的可能,所以传二级指针,而pos是查找,不修改,所以传一级指针。


void SLTEraseAfter(SLTNode* phead, SLTNode* pos);//删除pos位置之后的元素
//因为是之后的位置,所以不会改变头结点所以是一级指针。

void SLTDestory(SLTNode** pphead);//销毁链表
//会删除头结点,所以传二级指针

SList.c(函数的实现)

#include "SList.h"

//新结点
SLTNode* BuyNode(SLTDataType x)
{
	//动态开辟一个新结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));

	//判断新结点返回值是否为空,即开辟是否成功
	assert(newnode);

	//新开辟的结点的数据域改为传进来的值
	newnode->data = x;

	//新开辟的结点的指针域指向NULL。
	newnode->next = NULL;

	//返回该结点
	return newnode;
}

//打印链表
void SLTPrint(SLTNode* phead) 
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		//当当前指针不为空,就打印当前结构体内部的数据。
		printf("%d->", cur->data);
		//指针向后走
		cur = cur->next;
	}
	//为空就打印NULL。
	printf("NULL\n");

}

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//断言二级指针是否为空,因为理论上来说,不管头指针是否为空,头指针的地址一定不为空
	SLTNode* newnode = BuyNode(x);//申请一个新结点,因为后面各种插入都会申请新结点,所以就封装了一个函数
	if (*pphead == NULL)
	{
		//如果头结点为空,就将申请的结点给头节点
		(*pphead) = newnode;
	}
	else
	{
		//否则用一个tail指针找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//找到尾之后,将新结点链接到尾上。
		tail->next = newnode;
	}
}

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);//断言二级指针是否为空,因为理论上来说,不管头指针是否为空,头指针的地址一定不为空

	assert(*pphead);//断言头指针是否为空

	//定义两个指针,tail找尾,tailPrev是记录尾的前一个结点,方便最后的释放。
	SLTNode* tail = *pphead;
	SLTNode* tailPrev = NULL;
	if (tail->next == NULL)
	{
		//处理一上来就是头结点的情况
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (tail->next != NULL)
		{
			//若tail->next != NULL,说明不是最后一个结点,更新tailPrev
			tailPrev = tail;
			tail = tail->next;
		}
		free(tail);//释放尾结点
		tail = NULL;//置空
		tailPrev->next = NULL;//原本指向tail的tailPrev的next指向NULL
	}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);//断言二级指针是否为空,因为理论上来说,不管头指针是否为空,头指针的地址一定不为空

	assert(*pphead);//断言头指针是否为空

	SLTNode* next = (*pphead)->next;//用一个next指针先记录头指针的下一个

	free(*pphead);//释放头指针

	*pphead = next;//头指针指向前面记录的next。
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyNode(x);//申请一个结点
	
	newnode->next = *pphead;//新结点的next指向头结点

	*pphead = newnode;//头结点改为新结点
	
}

//查找,返回结点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	
	while (cur)//用cur指针遍历链表
	{
		if (cur->data == x)
		{
			//如果cur指针的数据域的值和所要查找的值相同,就返回该指针。
			return cur;
		}
		else
		{
			//cur指针往后走
			cur = cur->next;
		}
	}
	//遍历之后发现没有符合条件的,就返回NULL。
	return NULL;
}

//任意位置插入(在位置前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);//断言二级指针是否为空,因为理论上来说,不管头指针是否为空,头指针的地址一定不为空
	assert(pos);//断言地址是否为空
	if (pos == *pphead)//如果pos是头指针,那么就是头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuyNode(x);//申请一个新结点
		SLTNode* preve = *pphead;
		while (preve->next != pos)
		{
			//如果preve的next不为pos就继续向后走
			preve = preve->next;
		}
		//将newnode链接到preve的next
		preve->next = newnode;
		//newnode的next链接到pos
		newnode->next = pos;
		
	}

}

//删除某个结点后的位置
void SLTEraseAfter(SLTNode* phead, SLTNode* pos)
{
	assert(pos);//断言pos是否为空
	SLTNode* tail = phead;
	while (tail->next != pos)
	{
		tail = tail->next;
	}
	if (tail->next == NULL)
	{
		//处理pos是最后一个结点的情况
		return;
	}
	//用delPrev来记录要删除的前一个结点
	SLTNode* delPrev = tail->next;
	//释放要删除的结点
	free(delPrev->next);
	//置空
	delPrev->next = NULL;

	
}

//删除链表
void SLTDestory(SLTNode** pphead)
{
	assert(pphead);//断言二级指针是否为空,因为理论上来说,不管头指针是否为空,头指针的地址一定不为空
	while (*pphead)
	{
		SLTPopBack(pphead);//调用尾删即可
	}
}

水平有限,欢迎指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
二.内核链表 内核链表是一种链表,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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值