目录
顺序表的弊端
开始说单链表之前呢我们先谈谈为什么要有链表,上一篇文章讲的顺序表有什么弊端吗?还别说顺序表的问题还是有的,主要为如下几点:
1.中间或头部插入删除,时间复杂度为O(N);
2.增容需要需要申请空间,拷贝数据,释放旧空间,会有不小的损耗;
3.增容一般是2倍的增长,势必会有一些空间的浪费。例如当前容量为100,增容后为200,如果此时我们再继续插入5个数据就结束了,就会浪费剩下的95个数据空间。
由此,我们引出了今天的大哥:链表
链表的概念和结构
概念:链表是一种物理存储结构上不连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
结构(如图):
单链表
我们先来看看代码中,单链表的格式:
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SLTNode;
这里的结构体指针是访问下一个结点的关键,我们简单的写一个打印函数来感受一下next的作用:
打印函数
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur=cur->next;
}
}
我们要理解next是一个指针,它所指向的是下一个结点的地址,所以在打印数据的时候如何找到下一个结点就需要用当前的指针指向next,这样就可以连续找到下一个结点并将数据打印出来。那什么时候结束呢?我们知道最后一个结点的next是一个空指针,所以在遍历的时候我们只要保证cur不为NULL就可以,为NULL时说明已经到了最后一个结点。
尾插函数
接下来我们写尾插函数,首先需要申请一个新的结点,然后对这个结点进行初始化,然后找到原来链表的结尾处,最后链接起来。这就是尾插的基本思路,下面我们来实现一下。
void SLPushBack(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
//找尾
SLTNode* tail = phead;
while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;
尾插的问题1
我们仔细观察上面的函数实现,看看有什么问题,找尾这部分代码是我们经常犯的错误。
我们认为tail从头部pead开始找尾,尾的标志是next为NULL,找到之后我们把新的结点地址赋给tail就ok了,于是就写出了上边的代码,但真的正确么?
仔细分析,我们可以知道,tail找到尾之后,tail作为指针记录的是next的地址即NULL(NULL是tail此时的内容),然后我们接下来的操作是将newnode的值赋给tail,而newnode是一个指针,它指向的内容是新结点的地址,所以此时tail被赋值成新结点的地址,但我们需要注意的是无论是tail还是newnode都是这个函数的局部变量,出了函数都会销毁,因此实际上,这个新的结点并没有与原来的尾链接起来。
所以如何才能链接起来呢?我们可以对找尾部分做如下修改:
//找尾
SLTNode* tail = phead;
/*while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;*/
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
分析一下这样修改的意义:当我们找到next是NULL的结点时,我们让tail所指向的结构体中的next赋值成新结点的地址,注意这里并没有用tail这个局部变量来赋值,绝妙的地方是我们用结构体中的next接收新结点的地址,因为结构体不会因为函数的结束而销毁
尾插的本质是原尾结点中要存储新的尾结点的地址
你以为到这尾插就结束了么?仔细看还有什么问题。
尾插的问题2
如果我们传过来的链表为空呢,我们找尾部分还有什么问题,我们的while就出现问题了,下一句就会出现野指针,所以我们需要分情况:
//找尾
SLTNode* tail = phead;
if (phead == NULL)
{
phead = newnode;
}
else
{
/*while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;*/
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
到这呢我们就可以进行在test.c文件进行初步的测试,打印出来看看结果:
咦?为什么是NULL呢?我明明插入了1、2、3、4了,好的到这呢我们的又一个坑它来了
尾插的问题3
这里一个关键点就是我们老生常谈的问题:形参是实参的临时拷贝
我们都知道面对这个问题我们只需要传参时把参数换成指针,指向实参所在地址就行了,那我们这里传参的时候形参的类型也是指针呀,为什么还是无法改变数据呢?
这里我们来一个小插曲来理解一下
void Func1(int y)
{
y=1;
}
void Func2(int* p)
{
*p=1;
}
int main()
{
int x=0;
Func1(x);
Func2(&x);
return 0;
}
对比Func1和Func2我们很清楚要想改变x的值,必须用Func2,Func1无法改变,这就是经典的形参是实参的临时拷贝的例子,那我们再看一个例子:
void Func(int* ptr)
{
ptr = (int*)malloc(sizeof(int));
}
int main()
{
int* px = NULL;
Func(px);
return 0;
}
在这个例子中ptr的值会不会改变px呢?我们简单的画一个图演示一下:
从图上可以看出ptr不会影响px,出了Func函数ptr销毁,malloc申请的空间就无法找到,造成内存泄漏。所以类比上边的例子,这个例子犯的错误同样是形参是实参的临时拷贝问题,只是这次把函数的参数换成了指针类型,但本质依然没变,这个做一个修改如下:
void Func(int** pptr)
{
*pptr = (int*)malloc(sizeof(int));
}
int main()
{
int* px = NULL;
Func(&px);
free(px);
return 0;
}
我们再画个图理解一下:
此时我们可以看出即使Func函数销毁,px依旧指向malloc申请的空间,也可以free申请的空间,
这里我们传的参数是二级指针。
总结上面两个例子我们发现:修改int类型的数据,我们传的是int*一级指针;我们要修改int*类型的数据,就需要传int**二级指针。
ok到这我们再回到我们的尾插函数看看问题,我们依然是看图解决:
如图,指针指向关系就是这样,如果SLTPushBack函数结束,那么phead和newnode这两个局部变量就会跟着销毁,这样一来malloc申请d空间找不到,内存泄漏,plist也没有任何改变。所以这里我们需要传递的参数应该是二级指针,plist这个指针的地址,这样它们的关系图就变成了这样;
对应的代码就修改成了这样:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
//找尾
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
/*while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;*/
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
这个时候我们再测试一下就会发现得到了我们想要的结果:
那么到这尾插函数就结束了,相对应的我们来写头插函数
头插函数
由于我们在写头插函数时依然需要得到一个新的结点,因此我们可以把得到结点的那部分封装成一个函数如下:
获取节点的函数
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
头插函数
然后头插就可以写成这样:
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
那么我们浅浅测试一下,看看结果:
尾删函数
尾删的问题1
很多人会写成这样,我们一起看一下对不对:
void SLTPopBack(SLTNode** pphead)
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
free(tail);
tail = NULL;
}
我们试着调用一下这个函数看看效果:
经测试我们发现,产生一个随机值,大概率是野指针,我们来分析一下
我们找到尾之后呢,直接将其释放,这就导致它的前一项的next成了野指针,虽然把tail置空了,但是tail是一个局部变量,不会影响前一个结点的next的值,依然是一个野指针。
如何修改呢,如下:
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;
}
我们用一个prev指针记录tail位置,它时刻指向tail上一个位置,所以找到尾之后,通过prev这个结构体指针来让next置空,看一下测试结果:说明修改正确
但是呢如果我们多测试几组呢
尾删的问题2
看,当我们删到只剩一个数据时,继续删除就会出现问题,那我们回头看看当只有一个数据时为什么会出现问题。
问题出在最后一步,只有一个结点时,while循环不会进去,tail被free置空,prev始终是个空指针,也就不存在next了,所以prev->next这步代码出现问题了。
所以这里我们需要特殊处理,把一个结点的情况单独写出来
void SLTPopBack(SLTNode** pphead)
{
//链表本身就是空的
// 温柔的检查
/*if (*pphead == NULL)
return;*/
//暴力检查
assert(*pphead!=NULL);
//只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//多个结点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
此时再看看测试结果:正确
头删函数
接下来我们写头删函数
void SLTPopFront(SLTNode** pphead)
{
//链表本身就是空的
// 温柔的检查
/*if (*pphead == NULL)
return;*/
//暴力检查
assert(*pphead != NULL);
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;
}
还是,我们测试一下,比如将值为2的结点*2如何实现
查找函数的主要作用是为了下面的函数做铺垫
下面我们来实现某一位置前插入一个结点的函数
某一位置前插入函数
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
//先找到pos前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
这里我们需要注意一点是pos不能为NULL,因为如果pos是NULL,就会产生空指针问题,pphead也不可能为空,因为它是plist的地址,plist指向的内容可以为空,但plist的地址不可能为空,所以我们这里有必要加断言,对pphead和pos加断言确保传过来的值不为空。
我们再来浅浅测试一下:
接下来是我们的某一位置删除函数的实现:
某一位置前删除函数
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead==pos)
{
SLTPopFront(pphead);
}
else
{
//先找到pos前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
思考一下如果我们只给一个pos,不给头指针,我们能不能实现某一位置前的插入和删除?
答案是可以的,我们可以换一种思路,比如要在data为2的结点前插入20,我们可以在2的后面插入20,然后交换就实现了。同理,删除时,要删除data为2的结点,我们可以把下一个结点的data赋给data为2的结点,然后把这个结点删除,就间接的把2删除了,但是这种方式不能删尾,因为尾之后没有可以代替的值。
ok,我们继续进行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;
}
那么到这里我们单链表的介绍就基本结束了,下面附上所有代码:
完整代码
SList.h
#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);//查找
//pos之前插入
void SLTInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x);
//pos位置删除
void SLTErase(SLTNode** pphead,SLTNode* pos);
//pos之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//pos位置之后删除
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 fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
//找尾
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
/*while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;*/
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopBack(SLTNode** pphead)
{
//链表本身就是空的
// 温柔的检查
/*if (*pphead == NULL)
return;*/
//暴力检查
assert(pphead);
assert(*pphead!=NULL);
//只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//多个结点
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
void SLTPopFront(SLTNode** pphead)
{
//链表本身就是空的
// 温柔的检查
/*if (*pphead == NULL)
return;*/
//暴力检查
assert(pphead);
assert(*pphead != NULL);
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(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
//先找到pos前一个位置
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (*pphead==pos)
{
SLTPopFront(pphead);
}
else
{
//先找到pos前一个位置
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 TestSList1()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
}
void TestSList4()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
SLTNode* ret = SLTFind(plist, 2);
ret->data *= 2;
SLTPrint(plist);
}
void TestSList5()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
SLTNode* ret = SLTFind(plist, 2);
SLTInsert(&plist, ret, 20);
SLTPrint(plist);
}
int main()
{
//TestSList1();
//TestSList2();
//TestSList3();
//TestSList4();
TestSList5();
return 0;
}
码字不易,看到这的老铁们相信收获还是有的,三连一下,给博主点鼓励呗!