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;
}