单链表
1.链表的概念与结构
前言:为什么要存在链表呢?在很多情况下,我们用数组就能很好的完成工作,而且不会产生太多的差异,那么链表存在的意义是什么?链表相比于数组有什么优势或者不足吗?
首先我们要先知道顺序表的问题
顺序表的问题:
1.中间/头部的插入删除,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。
因此需要有一种数据类型与其进行互补!下面我来介绍一下链表:
首先是链表的概念:
链表是一种物理存储结构上非连续、非顺序的存储结构 ,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
单链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
与顺序表存储的区别:
从本质上来讲,链表与数组的确有相似之处,他们的相同点是都是线性数据结构,这与树和图不同,而它们的不同之处在于数组是一块连续的内存,而链表可以不是连续内存,链表的节点与节点之间通过指针来联系。
由于其非连续内存的特性导致链表非常适用于频繁插入、删除的场景,而不见长于读取场景,这跟数组的特性恰好形成互补,链表的特性与数组互补,各有所长,而且链表由于指针的存在可以形成环形链表,在特定场景也非常有用,因此链表的存在是很有必要的。
需要注意的是:
1.链式结构在逻辑结构是连续的,但是在物理结构上不一定连续,也就是说实际上链表是没有那个箭头连接的!!!只是结点结构体的next指向下一个结点的地址。
2.结点,即是一个结构体变量,包含数据域和指针域。一般实在堆上面申请空间的,使用malloc进行内存分配,开辟的空间有可能连续有可能不连续(一般开辟空间很大的时候就不连续)也就是所谓的原地扩容和异地扩容。
3.链表最后的指针会指向NULL;
4.链表新节点的时候不可以使用realloc进行一个个的增容空间,因为使用realloc进行增容的时候,不管是原地还是异地增容都会在原来的空间后面进行增容的,即增容后的整个空间的物理结构是连续的,但是,链表的物理结构是不连续的。
链表的优点:
1.在链表中,我们只需要指导第一个节点的位置,就可以通过指针把整个链表进行访问,不存在扩容的代价,不存在空间的浪费,因为这是按需索取空间。
2.链表再头插或者尾插又或中间插入数据的时候,都不需要数据进行挪动,通过改变指针就可以完成。
2.链表的分类
实际中要实现的链表结构非常多样,以下情况结合起来就有8中链表结构:
1.链接方向:单向、双向
2.带不带头节点(哨兵卫)
3..循环、非循环
今天我们要介绍的是最简单的一种类型:无头结点的单链表。
3.链表的接口(实现操作)
//创造新节点
LTNode* BuynewNode(LTDatatype x);
//尾插
void LTPushBack(LTNode** pplist, LTDatatype x);
//头插
void LTPushFront(LTNode** pplist, LTDatatype x);
//尾删
void LTPopBack(LTNode** pplist);
//头删
void LTPopFront(LTNode** pplist);
//打印链表
void LTPrint(LTNode** pplist);
//查找附加修改
LTNode* LTFind(LTNode** pplist, LTDatatype x);
//再pos结点位置之后插入x
void LTInsertAfter(LTNode* pos, LTDatatype x);
//再pos位置之前插入x
void LTInsertBefore(LTNode** pplist, LTNode* pos, LTDatatype x);
//删除pos所在位置之后的结点
void LTEraseAfter(LTNode* pos);
//删除pos位置的结点
void LTErase(LTNode** pplist, LTNode* pos);
//销毁链表
void LTDestory(LTNode** pplist);
3.1单链表的定义
typedef int LTDatatype;
typedef struct LinkList
{
LTDatatype val; //数据域
struct LinkList* next;//指针域
}LTNode;
3.2动态申请一个节点
//创造新节点
LTNode* BuynewNode(LTDatatype x)
{
LTNode* tmp = (LTNode*)malloc(sizeof(LTNode));
if (tmp == NULL)
{
perror("malloc fail\n");
exit(-1);
}
else
{
tmp->val = x;
tmp->next = NULL;
}
return tmp;
}
3.3初始化单链表
//单链表初始化十分简单
void TestLinkList()
{
LTNode* plist = NULL;
}
3.4打印单链表
//打印单链表
void LTPrint(LTNode** pplist)
{
//在此处不可以直接断言plist,因为,链表可能为空链表,当链表为空链表的时候,打印出来就是空链表,只有NULL
//若在此处对plist进行断言,如果链表为空链表的话,在此就会直接报错,而我们想要的是,若链表为空链表时,就打印出空链表,而不是直接报错、
LTNode* cur = *pplist;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
3.5头插法
void LTPushFront(LTNode** pplist, LTDatatype x)
{
LTNode* newNode = BuynewNode(x);
if (*pplist == NULL)
{
*pplist = newNode;
}
else
{
newNode->next = *pplist;
*pplist = newNode;
}
}
3.6尾插法
void LTPushBack(LTNode** pplist, LTDatatype x)
{
LTNode* tmp = BuynewNode(x);
//如果*ppplist是空的话直接将开辟的新节点作为头结点
if (*pplist == NULL)
{
*pplist = tmp;
}
else
{
//非空链表、
//先遍历链表进行找尾、
LTNode* ptail = *pplist;
while (ptail->next)//注意这是不带哨兵卫的需要判断的是ptail->next是否为空
{
ptail = ptail->next;
}
ptail->next = tmp;
}
}
3.8头删法
void LTPopFront(LTNode** pplist)
{
assert(pplist);
assert(*pplist);
//if (*pplist == NULL)
//{
// return;
//}
//只有一个结点
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
LTNode* cur = *pplist;
*pplist = (*pplist)->next;
free(cur);
cur = NULL;
}
}
3.9尾删法
void LTPopBack(LTNode** pplist)
{
//重点!!!
// 注意点一:
//即使链表为空链表, 即结构体指针变量pplist为空指针NULL,但是结构体指针变量pplist的 地址 一定不是空指针NULL, 并且该结构体指针变量
//pplist的地址一定不能是空指针NULL,若为空指针NULL,下面对其解引用,即*ppplist相当于对空指针NULL进行解引用,会出现错误、
assert(pplist);
//注意点二:
//一定要分链表只有一个结点和多个结点如果只有一个结点就不能用 ptail->next->next 去判断了 因为ptail->next为NULL NULL->next err
if (*pplist == NULL)
{
return;
}
//只有一个结点
else if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
//多个结点
else
{
LTNode* ptail = *pplist;
while (ptail->next->next)
{
ptail = ptail->next;
}
free(ptail->next);
ptail->next = NULL;
}
}
3.10查找附加修改
LTNode* LTFind(LTNode** pplist, LTDatatype x)
{
//当链表为空时,plist等于NULL,此时查找的话想得到的结果就是找不到即可,并不需要直接断言从而报错,太暴力、
//assert(plist);
LTNode* cur = *pplist;
while (cur)
{
if (cur->val == x)
{
cur->val *= 10;//修改成10倍
return cur;
}
cur = cur->next;
}
return cur;
}
3.11在pos结点位置之后插入x
void LTInsertAfter(LTNode* pos, LTDatatype x)
{
assert(pos);
LTNode* newNode = BuynewNode(x);
LTNode* next = pos->next;
pos->next = newNode;
newNode->next = next;
}
3.12在pos位置之前插入x
void LTInsertBefore(LTNode** pplist, LTNode* pos, LTDatatype x)
{
assert(pplist);
assert(pos);
//LTNode* newNode = BuynewNode(x);
//LTNode* cur = *pplist;
//注意!! 插入时需要判断是否只有一个结点
//方法一:
//if ((*pplist)->next == NULL)
//{
// newNode->next = *pplist;
// *pplist = newNode;
//}
//else
//{
// while (cur->next != pos)
// {
// cur = cur->next;
// }
// newNode->next = pos;
// cur->next = newNode;
//}
//方法二
//如果pos的位置恰好为第一个节点的位置时就相当于是头删、
//判断pos所在的位置是否为第一个节点、
if ((*pplist) == pos)
{
LTPushFront(pplist, x);
}
else
{
LTNode* cur = *pplist;
//pos所在位置不是第一个节点的位置
while (cur->next != pos)
{
cur = cur->next;
}
LTNode* newNode = BuynewNode(x);
newNode->next = pos;
cur->next = newNode;
}
}
3.12删除pos位置之后的结点
void LTEraseAfter(LTNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
else
{
LTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
}```
### 3.13删除pos所在位置的结点
```c
void LTErase(LTNode** pplist, LTNode* pos)
{
assert(pos);
if (*pplist == pos)
{
LTNode* next = (*pplist)->next;
free(*pplist);
*pplist = next;
}
else
{
LTNode* prev = *pplist;
LTNode* next = pos->next;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = next;
free(pos);
pos = NULL;
}
}
3.13销毁链表
void LTDestory(LTNode** pplist)
{
assert(pplist);
//逐一销毁
LTNode* cur = *pplist;
while (cur)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
*pplist = NULL;
}