数据结构(链表的增删改查)

链表

什么是链表

         ​链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

链表的连接的方式

        链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。这个比较抽象,直接上图就比较好理解一些。

首先创建一个结构体,里面有一个元素值,和一个指针

这个表示链表指针的指向

第一个phead表示链表第一个结构体的地址,用1当例子,1表示data的值,0x0012FF80表示指向下一个节点的地址,通过这样每一个指向节点的地址,链表就串联起来了,最后那个0x00000表示为NULL,意思为空他的后面没有指向的值。

链表的创建

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

上面创建了一个结构体,首先使用typedef进行对int类型进行改名,这样我们以后不想存int类型的元素时候直接在typedef上修改即可,然后进行使用typedef对结构体进行简化,方便使用,然后创建一个int类型的元素,和结构体的指针,因为他是指针所以并不会出现无限创建结构体。

 链表的打印

void SLTprint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d", cur->data);
		cur = cur->next;
		//cur++这种写法坚决错误,不同于顺序表
	}
	printf("\n");
}
//尾插的本质是原尾节点存新尾节点的地址

上面是接收指针来进行打印链表,因为有的时候会使用链表头,所以我们创建一个新指针来存放phead,然后通过while进行使用,判定cur是否为空,为空则停止打印,通过cur = cur->next;来指向下一个链表;cur为原节点的地址,cur->next为下一个节点的地址,将下一个赋值给原地址,就进行了迭代;

进行开辟空间

SLTNode* SLTBuyNode(SLTDataType x)
{
	//开辟空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
	}
	return newnode;
}

这个进行开辟空间,开辟空间的地址为newnode 

元素值为:newnode->data = x;
                  newnode->next = NULL;

然后返回是开辟节点的地址;

对链表进行尾插

void SLTPushBack(SLTNode** pphead, SLTDataType x)

{
	assert(pphead != NULL);
	SLTNode* newnode;
	newnode=SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾部
		SLTNode* cur = *pphead;
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

首先,这里用的是**pphead,为什么呢,形参是实参的临时拷贝,因为要对指针进行改变,就要传入指针的地址,直接传入指针是不对的,然后要进行断言,因为传入的指针是有可能为NULL(空)的但是指针的地址是“绝对不可能为空的”,所以必须断言然后依然是创建一个新指针来存放pphead进行使用,分两种情况,首先链表为空,则直接赋值插入,链表不为空则通过while来找到尾部“空”,进行插入;

 链表的头插

void SLTPushFrot(SLTNode** pphead, SLTDataType x)
{
    assert(pphead != NULL);
	SLTNode* newnode;
	newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

 头插就比较简单了,直接让原链表赋值于新开辟空间的next;在把newnde赋值会*pphead;就可以了,链表为空不为空都可以使用,同样的他也是需要对地址进行断言,判断链表地址是否为空;

 链表的尾删


void SLTPopBack(SLTNode** pphead)
{
    assert(pphead != NULL);
	assert(*pphead != NULL);
	if ((*pphead)->next==NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//找尾部
		SLTNode* ptr = NULL;
		SLTNode* cur = *pphead;
		while (cur->next != NULL)
		{
			ptr = cur;
			cur = cur->next;
		}
		free(cur);
		cur = NULL;
		ptr->next = NULL;
		/*while (cur->next->next != NULL)这样写也可以,指向下下一个的指针是否为空
		{
			ptr = cur;
			cur = cur->next;
		}
		free(cur);
		cur = NULL;*/

	}
}

 这个是链表的尾删;这个使用双断言   

assert(pphead != NULL);链表地址不能为空
assert(*pphead != NULL);链表也不能为空,链表都为空了还删他干嘛;

我们分为两种情况首先是链表只有一个元素,我们直接删除free(*pphead)即可

链表为多个元素,ok,我们先找到尾部,cur->next为空才是尾部,然后使用ptr存放尾部前一个元素的地址,然后对前一个地址上的元素cur->next置为空free,然后free(*pphead)即可;找尾部两种方法均可

cur->next->next表示下一个节点的下一个节点他属于是多走一步

链表的头删

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
    assert(*pphead != NULL);
	SLTNode* cur = *pphead;  // cur 指向头节点
	*pphead = cur->next;  // 更新头指针指向下一个节点
	free(cur);  // 释放原来的头节点
	cur = NULL;  // 将 cur 置为 NULL,虽然在这里 cur 已经没有用了
}

经过上面的一系列的,这个头删就非常简单了,首先双断言,然后储存原头节点,将头节点下一个赋值给头节点,然后释放原头节点;

链表的查找

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cut = phead;
	if (cut == NULL)
	{
		// 链表为空,直接返回NULL  

		return NULL;
	}

	while(cut != NULL)
	{
		if (cut->data == x)
		{
			return cut;
		}
		cut = cut->next;
	}
	
	return NULL;
}

这个是链表的查找,由于并没有改变链表,所以不需要使用**pphead,* phead就可以查找,

这里分两种情况,找到和没找到,找到返回位置指针,没找到返回null;

这里说明一下

    if (cut == NULL)
    {
        // 链表为空,直接返回NULL  

        return NULL;
    }

这个是判断是否为空链表的,为空链表则直接返回null,空链表找不到也返回null

选择插入在pos之前插入

//选择插入在pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead != NULL);
	assert(pos != NULL); // 检查 pos 不为 NULL
	if (*pphead==pos)
	{
		SLTPushFrot(pphead, x);
	}
	SLTNode* prev = *pphead;
	while (prev->next!=pos)
	{
		assert(prev != NULL); // 确保 prev 不是 NULL  
		prev = prev->next;
	}
	SLTNode* newnode = SLTBuyNode(x);
	prev->next= newnode;
	newnode->next = pos;
}

首先进行双断言assert(pphead != NULL);
    assert(pos != NULL); // 检查 pos 不为 NULL,pos是要选择指向的元素的指针,SLTDataType x表示插入的值

        首先分情况,如果是头节点直接调用头插函数即可,若不是,则通过SLTFind找到要插入的元素的指针,然后通过while找到插入的元素的指针的前一个指针,然后进行重新赋值,连接链表

选择插入在pos之前删除

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead != NULL);
	assert(*pphead != NULL);
	assert(pos != NULL); // 检查 pos 不为 NULL
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

依然是先断言,判断是否为空,然后进行分情况判断,若为头节点直接,调用头删函数即可;若不是,用while找到pos前一个节点,让前一个节点的next指向pos的next,在free(pos)即可

选择插入在pos之后添加和删除

void SLTInsertAfter( SLTNode* pos, SLTDataType x)
{

	assert(pos != NULL); // 检查 pos 不为 NULL
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* tum = pos->next;
	pos->next = newnode;
	newnode->next = tum;
}
//pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{

	assert(pos->next != NULL); // 检查 pos后面 不为 NULL
	SLTNode* tum = pos->next;
	pos->next = tum->next;
	free(tum);
	tum = NULL;

}

 因为比较简单所以就一起写了,因为在pos之后,所以不需要找前一个节点,调用SLTFind函数得到pos的指针,但可以不用使用头指针;

        添加是直接在先保存pos->next,然后在用pos->next = newnode;用新节点指向pos的next,newnode->next = tum;原来的指向newnode的next即可

        删除是最方便的了,首先检测pos的next不能为空为空则不用删除,然后创建一个新指针,是pos的next赋值tum,然后把tum的next指向指向pos的next,这样就跳过了pos后面那个节点;在free(tum);就可以了

释放链表

void SLTDestroy(SLTNode** pphead)
{
	if (*pphead==NULL)
	{
		return;
	}
	SLTNode* cur = *pphead;
	SLTNode* cur2 = NULL;
	while (cur)
	{
		cur2 = cur->next;
		free(cur);
		cur = cur2;
	}
	*pphead = NULL;
}

 在这个函数中创建了两个指针,一个用于释放链表一个用于储存链表的下一个next,这样就可以成功释放链表了

结尾

总之,链表作为一种灵活而强大的数据结构,在编程中应用广泛。掌握链表的基本原理和操作,不仅有助于提升编程能力,也为解决实际问题提供了有效的工具和思路。希望本文能够帮助读者更好地理解和应用链表,欢迎大家继续深入学习和探索数据结构与算法的世界!

如果有任何其他建议或想法或者发现错误,我非常乐意听取。并努力改进。

  • 39
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值