数据结构——链表

一.链表与顺序表

1.顺序表基本原理

 

#define Max 100

typedef int SeqDateType

int main()
{
//方法一:动态开辟内存来制作顺序表
 SeqDateType* SeqList = (SeqDateType*)malloc(sizeof(SeqDateType));
//方法二:静态数组创建顺序表
 SeqDateType SeqList[Max];
}

顺序表通常选择方法一来实现,因为静态数组无法扩容,容量上限锁死了不利于存储数据;顺序表每个元素都只有一个前驱元素和一个后驱元素,并且数据在内存中的存储是连续的。

2.链表基本原理

 

typedef int LDateType

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

链表通常依靠结构体来实现,如上述代码简单地定义了一个单链表,由存储数据的val和指向下一节点的指针组成;链表是依靠指针去访问不同的节点(空间),使这些空间产生逻辑上的链接,但实际是空间是不连续的。

3.链表与顺序表的优缺点对比

顺序表优点:可以通过下标随机访问元素,存取方便;因为在内存中的存储是连续的,所以缓存利用率高。(缓存每次都是读取一片空间,顺序表的特性使其缓存利用率高

顺序表缺点:因为空间上要求连续,插入和删除时往往需要挪动大量数据,导致效率低下;动态开辟空间大概率会出现空间浪费(无法释放部分空间,最优情况就是刚刚好满),如果是静态则无法是扩容;其次动态开辟空间是有损耗的,性能会有所下降。

链表的优点:内存分配更加灵活,不会出现空间浪费,需要一个节点创建一个节点,也可以只释放(free)一个节点的空间;插入删除仅仅需要改变几个节点的指针指向,不需要挪动数据,插入删除效率高。

链表的缺点:无法随机访问数据;空间不连续导致缓存的利用率低;

二.链表的增删查改

单链表完整代码:slist/slist · swi/c语言 - 码云 - 开源中国 (gitee.com)

链表的增删查改对于指针的赋值顺序很讲究,不然容易出现要使用的指针被覆盖找不到的情况。当然也可以选择将要用的指针提前存储起来(推荐,省事也浪费不了多少内存,还能提升代码可读性)

注意:以下图片均为逻辑图,请不要当作真实空间情况

1.插入数据

头插

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);//该函数用于创建节点并放入数据x,指针默认为NULL
	newNode->next = *pplist;
	*pplist = newNode;
}

1.头插第一步先创建一个新的节点: 

 2.然后让新的节点的指针指向头节点

3.最后将头指针指向新创建的节点就完成了

 

尾插 

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* cur = *pplist;
	SListNode* newNode = BuySListNode(x);
	if (cur == NULL)
		*pplist = newNode;
	else
	{
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = newNode;
	}
}

 由代码可见,尾插分为两种情况,如果链表为空直接将新节点地址赋值给头指针就完成了尾插。

这里主要讲一下不为空的情况:

1.既然是尾插那么就要找尾,结束条件:cur->next != NULL,只要下一个不为空就进行向下找。

2.当循环执行完毕,cur就到达尾节点了,接下来完成链接就好了。

 

2.删除数据

头删

void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	//assert(*pplist);为了便于测试所以使用if语句判断
	if (*pplist == NULL)
	{
		printf("链表为空,无法删除\n");
		return;
	}

	SListNode* next = (*pplist)->next;
	free(*pplist);
	*pplist = next;
}

1.头删第一步不是删除,因为删除后你要考虑新的头指针应该指向哪里,不然头指针就变成野指针了。同时操作顺序很重要,如果先释放了空间再进行 *pplist = *pplist->next 也一样不行。因为空间已经释放 *pplist->next 这样的操作是不合法的。所以释放前就要存储好*pplist->next 避免丢失。

 

 2.提前存储好下一个节点地址后,释放原头指针地址的空间,最后让头指针指向新的第一个节点

 尾删

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	//assert(*pplist);为了便于测试所以使用if语句判断
	if (*pplist == NULL)
	{
		printf("链表为空,无法删除\n");
		return;
	}

	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

 尾删同样是两种情况,只有一个节点时直接释放头指针空间,然后赋值为NULL。

当有多个节点时:

注意事项:看到cur->next->next != NULL这个判断条件可能有人会有点懵,为什么是下一个的下一个不为空。我只能说,这是单链表的无奈。当你释放掉最后一个节点的空间时,你会发现原来的倒数第二个节点的next无法赋值为NULL。因为单链表无法回头啊。所以这个判断条件使得cur停在了倒数第二个节点处,这样就方便了。

1.循环过后,cur停在倒数第二个节点。

2.之后就简单了,释放最后一个节点的空间,并将cur->next赋值为NULL就完成尾删了

 

 

3.查找数据

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

 查找就相对简单,只要从头到尾遍历一次,其中有与查找目标x相同的数据就返回其地址,循环结束都找不到就返回NULL。

三.双向循环带头链表

完整代码:双向带头链表 · 59ded72 · swi/c语言 - Gitee.com

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

双向 ,循环 ,带头分别是链表的结构特点,可以随意组合。

  1. 双向:节点含有指向上一个和下一个的指针。
  2. 带头:带有一个头节点,一定不为空,该头节点也可以称作哨兵位,但哨兵位不存储有效数据。
  3. 循环:链表头尾相接,通俗点说就是,头节点的上一个是尾节点,尾节点的下一个是头节点

如果为空时:

 不为空时:

 这样的结构解决了很多问题:例如找尾的问题,哨兵位的上一个就是尾节点。同时也可以访问上一个节点。因为哨兵位的存在,也不用特意判断头节点为不为空,然后分情况讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值