上一节当中我们讲了顺序表,但是顺序表也有一些缺陷 1、 空间不够了需要增容,增容是要付出代价 2、 避免频繁增容,空间满了一般要扩2倍,可能就会导致一些空间浪费 3、 顺序表要求数据从开始位置连续存储,那么我们在头部或者中间位置插入数据就需要挪动数据,效率不高

针对顺序表缺陷,就设计出了链表


首先,我们写一个结构体,存放链表节点的数据

typedef int SLTDateType;

typedef struct SListNode//一个存放数据,一个存放下一个节点地址
{
	SLTDateType data;
	struct SListNode* next;//一个数据加一个指针
}SLTNode;//将结构体重定义为SLTNode,之后可以引用

在单链表中,我们将第一个结点的地址存放在phead中,我们需要用一个指针cur来指向这个链表的表头,就像顺序表一样,我们需要用这个指针来找到表中某一个位置进行操作。这我们可以把phead赋给cur来指向这个表头,直到cur指向NULL结束。 捕获.PNG 这里这个指针是我们想象出来的,我们称之为逻辑图,实际上,cur的next存放的是下一个节点的地址,想要遍历下一个节点,只需要将cur->next赋给cur,cur地址改变。这里我们画一个物理图 捕获.PNG


懂得他的遍历机制,我们先写一个打印函数 在SList.h中声明

void SListPrint(SLTNode* phead);
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;//打印完cur数据,将next赋给cur,直到cur为NULL,跳出循环
	}
}

尾插 这里先用尾插举例子 想要尾插,先要找到尾,将头结点phead地址传给tail,如果tail为NULL,开辟空间newnode,将数据x传给newnode,newnode的next置为NULL,并将新节点的地址传给tail->next

void SListPushBack(SLTNode* phead, SLTDateType x)
{
	SLTNode* tail = phead;
	while (tail->next!=NULL)
	{
		tail = tail->next;
	}
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	tail->next = newnode;

}

我们在test.c中调试

void TestSList1()
{
	SLTNode* plist = NULL;
	SListPushBack(plist, 1);
	SListPushBack(plist, 2);
	SListPushBack(plist, 3);
	SListPushBack(plist, 4);
}

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

发现运行结果失败,调试发现,当我们将plist指针初始化为NULL,传给phead,再传给tail时都是NULL,NULL它找不到下一个节点,所以这里我们分类讨论,当这个链表开始就是没有节点,我们就添加节点。所以我们直接将开辟新节点写在前面,然后再对后面的结果讨论 如果phead为空,将新节点传给phead;如果phead不为空,再开始tail遍历操作

void SListPushBack(SLTNode* phead, SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		SLTNode* tail = phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

再次调试,虽然没有报错,但是什么结果都没有,调试发现 捕获.PNG newnode地址传入phead ,但是plist的地址没有改变。但是这时又有人问了,这里我传的就是指针呀,为什么phead的改变还是不会影响plist?这里问题就出在形参phead的改变并没有影响实参plist的值。 因为在我们传递地址的值的时候,它相当于一个值,我们需要将地址的数传过去。在一般形参中,我们给出值无法改变实参,解决办法就是传入他的地址。同理,在传入 ++*phead++ 地址时,我们传入的是地址的一个值,想要改变实参,必须传入地址的地址值,才能拿到我们的地址。所以我们需要找到 ++*phead++ 的地址,所以就是 ++phead++,这里运用到了一个二级指针 这里将phead的类型改为++SLTNode phead++

void SListPushBack(SLTNode** phead, SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SLTNode* tail = *phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

test.c中也做出相应的改变

void TestSList1()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
}

那么有人说需不需要改变SListPrint中phead的指针,这个没有必要,因为我们不需要改变地址,我们只需要调用地址


这里总结链表的优点:

  • 按需申请空间,不用了就释放空间(更合理的使用了空间)
  • 头部中间插入数据,不需要挪动数据
  • 不存在空间浪费 但是它也有缺点就是:每此访问一个数据,都要存一个指针去链接后面的数据节点,不支持随机访问 其实顺序表和链表各有其的优点与缺点,并不是有了链表,顺序表就不要了,顺序表的缺点我们在上面提到过,但是它支持随机访问,有些算法需要结构支持随机访问,比如:二分查找、优化的快排等等。 图片.png

前插

因为前插后插都需要扩容,所以我们将malloc开辟新空间newnode封装成一个函数

SLTNode* BuyListNode(SLTDateType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
}

则前插函数

void SListPushFront(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	
	newnode->next = *pphead;//将pphead存入的头结点地址传给新节点newnode
	*pphead = newnode;//将新地址newnode的地址传给pphead,放入新的头结点地址
}

图片.png 测试一下 test.c

SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);

	SListPrint(plist);

图片.png

后插

如果链表为空,将新节点地址传给pphead,如果不是空,那么先找到尾部位置tail,找到了,将新节点的地址传给tail->next

void SListPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

尾删

我们需要判断一下这个链表是否为空,如果是一个节点,直接将pphead的地址释放掉,并置空。如果有两个及以上节点,我们需要有两个指针,一个是tail指针指向要删除的节点,另一个prev指针指向释放节点的前一个,这样我们可以将tail的地址释放,并将前一个指针prev->next置为NULL。

void SListPopBack(SLTNode** pphead)
{
	////温柔一点
	//if (*pphead == NULL)
	//{
	//	return;//如果链表为空,直接返回
	//}
	//粗暴一点
	assert(*pphead != NULL);

	//一个节点
	//两个及以上节点
	if ((*pphead)->next == NULL)//* 和->的优先级是一样的,需要一个括号
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		SLTNode* prev = NULL;
		//while (tail->next != NULL)
		while (tail->next)//while条件非0即真,除了0意外都为真,遇到0跳出循环
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next=NULL;
	}
}

上面的else语句也可写成

SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;

这样可以少定义一个节点

头删

同样的步骤,首先判断链表是否为空,将头结点赋给指针begin遍历,如果begin->next为NULL,直接释放头指针,如果不为NULL,先将begin->next赋给pphead,存入新的节点地址,然后释放第一个节点,达到头删的效果。

void SListPopFront(SLTNode** pphead)
{
	//温柔的方式
	/*if (*pphead == NULL)
	{
		return;
	}*/
	assert(*pphead != NULL);
	SLTNode* begin = *pphead;
	if (begin->next)
	{
		*pphead = begin->next;
		free(begin);
	}
	else
	{
		free(*pphead);
	}
}

找到节点位置

SList.h声明

SLTNode* SListFind(SLTNode* phead,SLTDateType x);//找到某个位置,并返回位置

因为不需要改变地址,所以传一级指针就可以,将头结点地址传给指针cur,写一个while循环开始遍历,如果没有找到数字x,将下一个地址传给cur.找到了返回cur。如果这个链表找不到想要的数字,直接返回NULL。

SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data != x)
		{
			cur = cur->next;
		}
		else
		{
			return cur;
		}
	}

	return NULL;
}

在test.c中测试一下 写完查找函数,既可以找到相应的数字,也可以改变该位置的数字

void TestSList3()
{
	SLTNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 4);

	SLTNode* pos = SListFind(plist, 2);
	int i = 0;
	while (pos)
	{
		printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
		pos = SListFind(pos->next, 2);
	}
	//修改3->30
	pos = SListFind(plist, 3); 
	if (pos)
	{
		pos->data = 30;
	}
	SListPrint(plist);
}

在指定位置插入

在指定为之前插入

同样,只要在表中加入数据,就需要扩容.通过BuyListNode增加一个新节点newnode,因为需要前一个节点来记录增加后新节点的位置,所以定义一个posPrev,这样可以记录增加的新节点。但是如果想要在头结点增加,前一个节点为NULL,NULL是没有下一个节点的,不存在posPrev->next.所以分类讨论。 声明SList.h

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
	SLTNode* newnode = BuyListNode(x);
	
	if (*pphead == pos)//新节点newnode的next直接指向头结点,再把newnode的地址赋给pphead,形成新的头结点。
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		newnode->next=pos;
		posPrev->next = newnode;
	}
}

在指定位置后插入

这个就比前插写法简单多了,也比较推荐,因为逻辑越简单越好。开辟空间后,就可以插入。

void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
	assert(pos);

	SLTNode* newnode = BuyListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

在指定位置删除

这里的归类跟上边有一些区别,一个是找到想要删除的数字了,一个是没有找到,而前面的一个是头结点情况,一个是不为头结点情况。这里如果没找到pos,则向下继续找。如果找到了,一种情况是头结点,直接删除,头结点指向pos的next,不是头结点,用定义的posPrev->next记录pos->next,然后将pos释放掉。 SList.h声明

void SListErase(SLTNode** pphead, SLTNode* pos);
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	SLTNode* posPrev = NULL,*cur=*pphead;
	while (cur)
	{
		if (cur == pos)
		{
			if (pos = *pphead)
			{
				*pphead = pos->next;
				free(pos);
			}
			else
			{
				posPrev->next = cur->next;
				free(pos);
			}
		}
		else
		{
			posPrev = cur;
			cur = cur->next;
		}
	}
	
}

销毁数据

最后,我们前面用到了malloc,所以写一个销毁函数 这里我们不能直接释放cur节点,这样只能释放一个,我们应该逐个遍历并释放,最后将头结点指针置为空,销毁完成。 SList.h声明

void SListDestory(SLTNode** pphead);
void SListDestory(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

全部代码附上gitee链接 单链表