单链表的实现

链表属于线性表的一种,不同于顺序表,用链表存储的数据在逻辑结构上是连续的,但在物理结构(内存中)不是连续的

链表的结构:

链表的空间不是一开始就创建好的,而是按需开辟;需要插入数据时,就开辟一个数据的空间

怎么将每个数据连接起来,我们需要定义一个结构体,一个结构体中有数据和下一个数据的地址

typedef int SListDataType;

typedef struct SListNode
{
	SListDataType val;//数据
	struct SListNode* next;//下一个数据的地址
}SListNode;

可以看到,整个链表就像是用链条串起来了一样,但要注意,上图只是我们想象出来的结构,在内存中,数据之间不一定是连续的

链表的分类:

1.单向和双向

2. 带头和不带头

3.循环和不循环

在做OJ题时,我们用的最多的是不带头单链表

日常工作时,用的最多的是双向循环链表

因此就重点讲这两种链表的实现

今天讲不带头单链表


上面已经给出单链表的结构

下面是单链表具体要实现的接口

//销毁
void SListDestroy(SList* phead);

//尾插
void SListPushBack(SList* phead, SListDataType x);

//头插
void SListPushFront(SList* phead, SListDataType x);

//尾删
void SListPopBack(SList* phead);

//头删
void SListPopFront(SList* phead);

//查找
SList* SListFind(SList* phead, SListDataType x);

//在pos位置前插入
void SListInsert(SList* phead, SList* pos, SListDataType x);

//在pos位置删除
void SListErase(SList* phead, SList* pos);

//在pos位置后插入
void SListInsertAfter(SList* pos, SListDataType x);

//在pos位置后删除
void SListEraseAfter(SList* pos);

尾插:

首先需要开辟一个结点,在后面的插入数据都需要开辟结点,因此我们分装一个函数,用来开辟结点

SList* CreateNewNode(SListDataType x)
{
	SList* newNode = (SList*)malloc(sizeof(SList));
	if (newNode == NULL)
	{
		perror("CreateNewNode:malloc fail");
		exit(-1);
	}

	newNode->next = NULL;
	newNode->val = x;

	return newNode;
}

 在插入数据前,需要考虑两种情况

  1. 当链表为空时,插入数据的同时,需要改变头结点
  2. 当链表有一个以上的结点,需要找到尾结点,在尾结点后插入数据

代码:

//尾插
void SListPushBack(SList* phead, SListDataType x)
{
	SList* newNode = CreateNewNode(x);

	//1.没有结点
	if (phead == NULL)
	{
		phead = newNode;
	}
	else
	{
		//2.一个以上结点
		SList* tail = phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newNode;
	}
}

进行测试:

打印函数:

//打印
void SListPrint(SList* phead)
{
	SList* cur = phead;
	while (cur)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}
void TestSList()
{
	SList* sl = NULL;
	SListPushBack(sl, 1);
	SListPushBack(sl, 2);
	SListPushBack(sl, 3);
	SListPrint(sl);
}

我们发现代码出现了bug,经过调试,发现是因为尾插函数中phead指针是外面sl指针的一份临时拷贝,函数结束时,并没有将phead的值带回给外面的sl指针

要解决这个问题,需要用到二级指针,我们想改变函数外面一个指针的地址,要用二级指针传参

因此,正确的尾插函数

//尾插
void SListPushBack(SList** pphead, SListDataType x)
{
	assert(pphead);

	SList* newNode = CreateNewNode(x);

	//1.没有结点
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	else
	{
		//2.一个以上结点
		SList* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newNode;
	}
}

同样的,如果有需要改变头指针的函数,参数都应传二级指针

由于打印函数不需要改变头指针,因此参数传不传二级指针都无所谓,但是为了保证接口参数的一致性,最好也传二级指针

//打印
void SListPrint(SList** pphead);

//销毁
void SListDestroy(SList** pphead);

//尾插
void SListPushBack(SList** pphead, SListDataType x);

//头插
void SListPushFront(SList** pphead, SListDataType x);

//尾删
void SListPopBack(SList** pphead);

//头删
void SListPopFront(SList** pphead);

//查找
SList* SListFind(SList** pphead, SListDataType x);

//在pos位置前插入
void SListInsert(SList** pphead, SList* pos, SListDataType x);

//在pos位置删除
void SListErase(SList** pphead, SList* pos);

//在pos位置后插入
void SListInsertAfter(SList* pos, SListDataType x);

//在pos位置后删除
void SListEraseAfter(SList* pos);

这时再测试:


头插:

同样需要判断没有节点和一个以上结点的情况

//头插
void SListPushFront(SList** pphead, SListDataType x)
{
	assert(pphead);

	SList* newNode = CreateNewNode(x);

	//1.没有结点
	if (*pphead == NULL)
	{
		*pphead = newNode;
	}
	else
	{
		//2.一个以上结点
		newNode->next = *pphead;
		*pphead = newNode;
	}
}


尾删:

  1. 当链表为空时,是不能进行删除操作的,要断言一下
  2. 当只有一个结点时,释放掉头指针指向的空间,并将头指针置空
  3. 当有一个以上结点时,找到尾,释放尾结点,将尾结点的上一个结点中的next置空
//尾删
void SListPopBack(SList** pphead)
{
	assert(pphead);
	assert(*pphead);

	//只有一个结点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SList* tailPrev = NULL;

		SList* tail = *pphead;
		while (tail->next != NULL)
		{
			tailPrev = tail;
			tail = tail->next;
		}

		free(tail);
		tailPrev->next = NULL;
	}
}

头删:

  1. 链表不能为空
  2. 当只有一个结点,释放掉头结点,头指针置空
  3. 当有一个以上的结点,释放掉头结点,头指针指向下一个结点
//头删
void SListPopFront(SList** pphead)
{
	assert(pphead);
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SList* cur = *pphead;
		*pphead = (*pphead)->next;
		free(cur);
	}
}

查找:

  1. 遍历链表,如果找到了,返回结点的地址;如果找不到,返回NULL
//查找
SList* SListFind(SList** pphead, SListDataType x)
{
	assert(pphead);

	SList* cur = *pphead;
	while (cur)
	{
		if (cur->val == x)
			return cur;
		cur = cur->next;
	}

	return NULL;
}

在pos位置前插入:

该函数通常跟查找函数一起用,用Find函数找到结点pos,再在pos前插入新结点,需要注意:

  1. pos不为空才能在pos前插入
  2. 头指针不能为空,因为必须存在结点,才能在某个结点之前插入数据
  3. 满足上面的条件时,分两种情况:
    1) 要在头指针前面插入,此时就相当于头插
    2)  在其他位置插入,需要找到pos的上一个结点posPrev才能进行插入
//在pos位置前插入
void SListInsert(SList** pphead, SList* pos, SListDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	SList* newNode = CreateNewNode(x);

	if (*pphead == pos)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SList* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}

		newNode->next = pos;
		posPrev->next = newNode;
	}
}

在pos位置删除:

  1. 如果pos为头指针,此时相当于头删
  2. 如果pos为其他位置,找到pos上一个结点posPrev,再进行删除操作
//在pos位置删除
void SListErase(SList** pphead, SList* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	else
	{
		SList* posPrev = *pphead;
		while(posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
	}
}

在pos位置后插入:

//在pos位置后插入
void SListInsertAfter(SList* pos, SListDataType x)
{
	assert(pos);

	SList* newNode = CreateNewNode(x);

	newNode->next = pos->next;
	pos->next = newNode;
}

在pos位置后删除:

  1. pos位置的下一个结点必须存在才能删除
//在pos位置后删除
void SListEraseAfter(SList* pos)
{
	assert(pos);
	assert(pos->next);

	SList* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
}

销毁:

//销毁
void SListDestroy(SList** pphead)
{
	assert(*pphead);

	SList* cur = *pphead;
	while (cur)
	{
		SList* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

单链表的结构分析:

  • 单链表在任意位置后面插入数据还是比较方便的,但要在任意位置之前插入数据还需要遍历链表,找到posPrev,这就比较麻烦了
  • 另外,每种插入删除都需要单独判断头指针的情况

下次我将会讲双向循环链表,可以解决上面的问题,优化我们的链表


需要源码的可以进入我的Gitee主页自行查看

https://gitee.com/baiyahua/leet-code/tree/master/SList/SList

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值