单链表介绍

1.前言

    为什么要学习了顺序表还要学习链表呢?顺序表中有这样一些问题:1. 中间/头部的插入删除,时间复杂度为O(N) 2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

2.链表

    概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。也就是说:一个数据存在一个内存块(结点)中,这些内存块用指针进行相应的链接。链表有很多种结构,下面来看一下单链表。

3.单链表

    3.1单链表的定义

        单链表要求一个数据存一个内存块,多个内存块之间用指针进行链接,最后一个内存块中的地址指向空指针。因此需要定义变量存所要存放的数据,还需要定义指针变量存下一个结点的地址。为了防止出现连续改变或者为了后期方便的情况,单链表定义如下:

typedef int SLTDataType;

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

    3.2理解单链表

        为了理解单链表,我们先写一个单链表的打印函数,单链表不像顺序表有size的定义,因此需要有个指针指向第一个结点,这里定义为phead。phead不断往后走,走到空指针就结束了,所以参数传头结点的地址。

        但是我们在写打印函数时,我们再定义一个cur来存phead的值,让cur不断向后走,这样做一方面比较文雅,可以清晰的看到每个变量的含义;另一方面有时候可能会再次使用头结点。下面先写出打印函数再依次解释。

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

    在顺序表中,写打印函数开始时需要断言一下,那在单链表中,写打印函数需要断言吗?答案是不需要。为什么顺序表中需要断言而单链表中不需要断言呢?

顺序表中:

void SLPrint(SL* s)
{
	assert(s);
	for (int i = 0; i < s->size; i++)
	{
		printf("%d ", s->a[i]);
	}
	printf("\n");
}

顺序表需要断言是因为参数传来的是一个指向结构体的指针,结构体中有个指针变量指向一块数组空间,通过size来判断是不是数组中的元素个数,如果参数中指针指向的结构体都是空的,再想通过结构体指针来访问size是做不到的,因此需要断言。

单链表中:参数传来一个空指针,phead指向空,就直接说明什么都没有,也不存在访问空指针的问题。

    为什么写cur = cur->next而不写cur++。这是因为链表逻辑上是连续的,但在实际的物理结构中不一定是连续的。那cur是如何遍历的呢?在逻辑结构上,链表一个连着一个,cur不断向后走,每次指向一个新节点就访问里面的内容,直到遇到空指针停下来。

在物理结构上,链表间没有箭头,就是一个一个内存块里面存着值,cur向后走时里面的值进行变化。

    3.3尾插

        写尾插时需要断言吗?答案是不需要。因为就算参数传给phead是空,也是可以尾插的。假设已经有一段链表了,那如何进行尾插呢?首先需要弄个新结点出来,将新结点初始化,也就是存好我们要插入的值。然后我们就需要找尾,定义一个tail,找到最后一个结点和新结点链接,也就是原尾结点中存新结点的地址

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
    assert(pphead);                     //补充

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	SLTNode* tail = phead;
	while (tail->next != NULL)
	{
		tail = tail->next;
	}
	tail->next = newnode;
}

那如果链表为空会是怎么样呢?首先phead是空的,然后tail是空的,tail->next就会出问题。所以应该分情况,如果本来是空的,让phead直接指向新结点就可以了。

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	assert(pphead);                     //补充

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	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;
	}
}

下面就来测试一下,单链表这里就一个值,所以不用专门写初始化函数了。

void TestSList()
{
	SLTNode* plist = NULL;
	SLTPushBack(plist, 1);
	SLTPushBack(plist, 2);
	SLTPushBack(plist, 3);
	SLTPushBack(plist, 4);
	SLTPrint(plist);
}

当打印时,并没有给出我们想要的结果,这是什么原因?

这是因为仅仅把plist的值传过去了,plist并没有发生变化,因此传plist的地址,为了和一级区分参数用pphead。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);                     //补充

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

这里涉及到改变plist的指向,用到了二级指针。那打印函数需要传二级指针吗?答案是不需要,因为没有涉及改变指针指向的需求。

    3.4头插

        写头插时需要断言吗?答案是不需要。因为就算参数传给phead是空,也是可以头插的。假设已经有一段链表了,那如何进行头插呢?首先需要弄个新结点出来,将新结点初始化,也就是存好我们要插入的值,然后直接将新结点变为第一个就好。

        如果链表为空会是怎么样呢?和上面的思维一模一样,所以不用分情况。

那头插用二级指针吗?用的,用为存在改变plist指向问题。因为要新结点,所以把之前的创建新结点代码写成一个函数方便以后使用。


SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);                     //补充

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

    3.5尾删

        尾删很容易想到定义一个tail找到尾部,直接删除就行了。那尾删需要二级指针吗?目前也不好判断,先给一个二级指针,因为二级指针肯定不会错。

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

测试一下看看这个代码对不对,发现出了错误。

这是tail指向的位置释放后,上一个结点的next仍指向的是tail指向的结点,所以会打印出随机值。因此需要有一个变量来记录上一个结点。这里有两种方法。

方法一:

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

	prev->next = NULL;
}

方法二:

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

这两种方法有没有什么问题,如果还剩一个结点的时候再删除会怎么样?

当只有一个结点的时候再次删除程序出了问题。哪里有问题呢?

标红的位置都出现了问题。因此要分一个结点和多个结点的情况,一个结点的时候直接释放掉就可以了,这里拿第二个代码举例。

void SLTPopBack(SLTNode** pphead)
{
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

从这里也发现需要用二级指针,存在plist指向内容改变的情况。那如果链表都空了还能继续删吗?肯定不可以,因此需要检查一下。

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);                     //补充

	assert(*pphead);

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

    3.6头删

        有多个结点时,定义first变量指向第一个结点。这里肯定用二级指针,因为plist的指向的内容会发生变化。

        有一个结点的时候,也是同样的处理方式。

void SLTPopFront(SLTNode** pphead)
{
	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

这里也能发现,链表空了不能继续删除,因此检查一下。

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);                     //补充

	assert(*pphead);

	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

    3.7查找

        查找就是遍历一遍找我们想要找的值在哪个结点中,找到了返回结点的地址,没有找到就返回NULL。这里不需要断言,在空链表中也是可以查找的。这里也不需要二级指针,因为没有涉及到实参指针的改变。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

其实这里的查找也从侧面表示了修改,比如一个链表是1->2->3->4,现在想把2改成10,我们就可以找到2,然后直接修改。

    3.8pos之前插入

        new一个新结点出来,pos如果是第一个位置,相当于头插。pos如果不是第一个位置,能不能随便插入呢?

不能,因为要修改pos前一个结点next的值,就需要有前一个结点的地址,要找这个结点的地址则需要从头开始找。这里需要传二级指针,因为实参指针可能会被修改。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

这里有需要断言的地方吗?pphead永远不可能为空,因为它是二级指针,是地址的地址,所以这里一定要断言,防止有人传参时直接传空指针过来(之前的函数也补充)。pos按道理来说是一个合理的值,因为使用者肯定先是找到了正确的pos,才到pos之前插入,这里不用为别人的错误买单,但自己检查一下有没有pos这个值,pos起码不能为空。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

    3.9pos位置删除

        pos位置是第一个相当于头删,如果不考虑第一个的情况可能会跳过第一个永远找不到。pos在其他位置,能不能直接链接删除呢?

不能,还是要找到pos前一个结点的位置,找到后链接并删除。这里也传二级指针,因为实参的值可能会变。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

这里有需要断言的地方吗?pphead永远不可能为空,因为它是二级指针,是地址的地址,所以这里一定要断言,防止有人传参时直接传空指针过来(之前的函数也补充)。pos按道理来说是一个合理的值,因为使用者肯定先是找到了正确的pos,才到pos之前插入,这里不用为别人的错误买单,但自己检查一下有没有pos这个值,pos起码不能为空。空链表也不能插入,检查pos也间接检查了这个问题。

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

 思考:单链表不给头指针,能不能在pos前插入?

在pos的后面插入,交换值。

    3.10pos后面插入

        pos后面插入很容易,new一个结点后直接插入就好,注意顺序不要覆盖值。断言也只断言pos,原有和上面一样。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

    3.11pos后面删除

        将pos位置下一个位置的结点保存起来,再链接删除。这里除了断言pos还需要断言pos下一个位置有没有结点。

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

    3.12销毁

        链表的销毁就是将结点一个一个销毁,那问题在哪?在于把当前位置结点销毁后就不能找到下一个结点了,所以要保存下一个。参数可以是一级指针,这样就就要人为在外部置空,类似于free那样;参数也可以是二级指针,直接在内部置空即可。

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

4.完整代码

//SList.h

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;

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

void SLTPrint(SLTNode* phead);
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
void SLTEraseAfter(SLTNode* pos);
//SList.c


#include "SList.h"

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	/*SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;*/
	assert(pphead);                     //补充

	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}


void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);                     //补充

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}


//void SLTPopBack(SLTNode** pphead)
//{
//	SLTNode* prev = NULL;
//	SLTNode* tail = *pphead;
//	while (tail->next != NULL)
//	{
//		prev = tail;
//		tail = tail->next;
//	}
//	free(tail);
//	tail = NULL;
//
//	prev->next = NULL;
//}

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);                     //补充

	assert(*pphead);

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

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);                     //补充

	assert(*pphead);

	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = BuySLTNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}


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


void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}
//Test.c



#include "SList.h"

//void TestSList()
//{
//	SLTNode* plist = NULL;
//	SLTPushBack(&plist, 1);
//	SLTPushBack(&plist, 2);
//	SLTPushBack(&plist, 3);
//	SLTPushBack(&plist, 4);
//	SLTPrint(plist);
//	SLTPushFront(&plist, 6);
//	SLTPushFront(&plist, 7);
//	SLTPushFront(&plist, 8);
//	SLTPrint(plist);
//	SLTPopBack(&plist);
//	SLTPopBack(&plist);
//	SLTPopBack(&plist);
//	SLTPrint(plist);
//	SLTPopBack(&plist);
//	SLTPrint(plist);
//	SLTPopFront(&plist);
//	SLTPrint(plist);
//
//}

void TestSList()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	SLTNode* ret = SLTFind(plist, 2);
	ret->data = 10;
	SLTPrint(plist);
	SLTInsert(&plist, ret, 20);
	SLTPrint(plist);
	SLTErase(&plist, ret);
	SLTPrint(plist);
	SLTNode* retf = SLTFind(plist, 20);
	SLTInsertAfter(retf, 33);
	SLTPrint(plist);
	SLTEraseAfter(retf);
	SLTPrint(plist);


}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值