数据结构之线性表(二)——单向链表

链表的概念

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
在这里插入图片描述
如图所示,链表与顺序表不同的地方在于,链表的存储并不是在一块连续的空间中,而是通过动态分配的一个个内存块链接起来进行存储的。图中的指针就是用来存储下一个内存块的地址,通常用next表示。链表的结构在逻辑上看起来是一个接一个连接起来的,但是在内存中,却是散乱排布的,只能通过指针定位。

下面我会详细说明链表是如何实现的:

单向链表的实现

单向链表的结构体定义是这样的:

typedef struct List
{
	LDataType data;
	struct List* next;
}List;

其中有一个数据位来存储数据,还有一个指针位指向下一个节点的位置,通过指针将该节点和后面的节点链接起来。

动态申请节点

对于链表来说,想要存储一个数据,首先需要开辟一个节点,作为存放数据的容器。我们通过malloc函数来获得一个节点。

List* BuySListNode(LDataType x)
{
	//动态开辟一个节点
	List* Node = (List*)malloc(sizeof(List));
	if (Node == NULL)
	{
		printf("节点开辟失败\n");
		exit(-1);
	}
	Node->data = x;
	Node->next = NULL;
	return Node;
}

单链表的打印

我们在写单向链表的时候,如果想要测试的话,可以通过调试窗口来进行查看,但是通过将链表打印出来能够更加直观的表现,同时也能够检查链表中的连接是否出现错误。

// 单链表打印
void SListPrint(List* plist)
{
	assert(plist);
	List* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

通过cur指针来维护,最开始cur指向链表的头,从前往后进行遍历,每次对其中的数据进行打印,直到cur指向的为空指针,标志着走到尾部。

单向链表的尾插

单向链表的插入有很多种,首先介绍一下尾插,尾插也就是在单向链表的尾部进行插入。

// 单链表尾插
void SListPushBack(List** pplist, LDataType x)
{
	assert(pplist);
	List* newNode = BuySListNode(x);
	List* cur = *pplist;


	//第一种情况:链表为空,直接在头插入数据
	if (*pplist == NULL)
	{
		*pplist = newNode;
	}
	//第二种情况:链表不为空,在某个数据的后面插入
	else
	{
		//如果头节点不是空,就说明链表中有元素
		//要先找到尾部
		while (cur->next)
		{
			cur = cur->next;
		}
		newNode->next = cur->next;
		cur->next = newNode;
	}

每次进行尾插时要通过遍历找到最后一个节点,然后进行插入。但是这其中分为两种情况:第一种是如果链表是一个空链表的话,也就不存在尾部,所以要先在头节点中存入数据;第二种是链表为非空,此时是有尾节点的,这时我们通过遍历找到链表中的最后一个节点,然后进行插入。

需要注意的是,这里连接的顺序是非常有讲究的,要先将新节点的next指向尾节点的next,再让尾节点的next指向新节点。如果顺序颠倒的话,就无法找到尾节点的next了,但是非要这么做的话,可以定义一个变量保存尾节点的next。

注意:在进行尾插的时候我传的是二级指针,这是因为节点本身就是一个指针,如果想要修改的话,必须要传它的地址过去,因此需要用二级指针接收。后面功能的实现,有修改链表的地方我都会用二级指针来传递,就不再解释了

单向链表的尾删

有了尾插必定会有尾删的操作,尾删就是在尾部删除,那么应该如何做呢?

// 单链表的尾删
void SListPopBack(List** pplist)
{
	assert(pplist);
	assert(*pplist);
	List* cur = *pplist;
	List* prev = NULL;
	//尾删首先要找到最后的节点
	while (cur->next)
	{
		prev = cur;
		cur = cur->next;
	}
	//如果prev不为空,说明链表中至少还有两个元素
	if (prev)
	{
		prev->next = cur->next;
		free(cur);
		cur = NULL;
	}
	//如果prev为空,说明链表中只剩下一个元素,进行尾删的话要把pplist置为空指针
	else
	{
		free(*pplist);
		*pplist = NULL;
	}
}

想要进行尾删的话,首先需要遍历找到尾节点,但是这里有一点要注意,尾删需要让尾节点的前面的节点连接到尾节点的next,因此我们在遍历时,需要保存每一个节点的前驱prev,直到找到尾节点,此时,prev指向的是尾节点的前驱。最后让尾节点的前驱连接到尾节点的后继即可。

尾插也有两种情况需要注意:一个是链表中至少还有两个节点的时候,这种情况是存在尾节点的,因此我们按照上面的方法操作。还有一种情况比较特殊,就是链表中只剩下一个节点的时候,此时我们就不需要找到尾节点,直接删除这个节点即可。删除之后,链表就会变成空链表,所以最后我们需要将头节点置空。下一次进行插入的时候就按照空链表进行插入。

单向链表的头插

这是单向链表的另一种插入方式,头插的方式要比尾插要简单很多,因为尾插的时候,需要遍历来找到尾节点,而头插并不需要这样子,头插只要记录下来头节点的下一个节点,然后进行插入即可。当然还要注意一点,当链表为空的时候,此时插入是把新的节点给到头节点。

// 单链表的头插
void SListPushFront(List** pplist, LDataType x)
{
	assert(pplist);
	List* newNode = BuySListNode(x);
	List* head = *pplist;
	//第一种:链表为空
	if (*pplist == NULL)
	{
		*pplist = newNode;
	}
	//第二种:链表不为空
	else
	{
		newNode->next = head;
		*pplist = newNode;
	}
}

单向链表的头删

对于单向链表的头删,其实也很简单,只需要记录头节点的下一个节点,然后释放掉头节点,使下一个节点作为新的头节点。

// 单链表头删
void SListPopFront(List** pplist)
{
	assert(pplist);
	List* head = *pplist;
	*pplist = (*pplist)->next;
	free(head);
}

单向链表的查找

// 单链表查找
List* SListFind(List* plist, LDataType x)
{
	assert(plist);
	List* cur = plist;
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

单向链表在中间位置插入

在做这个操作的时候我们一般时在一个位置的后面进行插入,很多人会问这是为什么呢?因为单向链表中只有后继没有前驱,如果在前面插入的话就需要去遍历链表,效率比较低;而在后面插入可以通过next直接找到后继的节点进行插入。

void SListInsertAfter(List* pos, LDataType x)
{
	assert(pos);
	List* newNode = BuySListNode(x);
	//在pos位置之后插入一定要注意顺序
	//要先将新节点的next指向pos的next,在使pos的next指向新节点
	//如果顺序颠倒,除非再用一个变量记录pos的next,否则就找不到pos的下一个节点
	newNode->next = pos->next;
	pos->next = newNode;
}

单向链表删除中间位置的节点

在中间位置删除节点,我们一般删除某个位置之后的节点,和中间位置的插入相同,如果删除某个位置的节点,就要找到它的前驱和后继,这就非常浪费时间,没有必要去浪费效率。

void SListEraseAfter(List* pos)
{
	assert(pos);
	List* next = pos->next;
	pos->next = next->next;
	free(next);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值