前言
顺序表和链表的实现需要一定的c语言代码能力,需要掌握动态内存管理的相关知识,会使用malloc,realloc,free等函数
一、线性表是什么?
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表和链表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
三、顺序表的实现
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素。
2. 动态顺序表:使用动态开辟的数组存储。(使用malloc,realloc等函数)
本质上顺序表就是一个数组,在数组上完成数据的增删查改。
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间
大小,下面是动态顺序表的接口和实现。
typedef int SLDatatype; //方便以后更改数据的类型,记得改打印顺序表的占位符
#define INIT_CAPACITY 5 //初始的数组容量
typedef struct Seqlist{
SLDatatype* a;
int size; //有效数据个数
int capacity; //开辟的空间容量
}SL;//typedef把struct Seqlist类型重命名为SL,是为了方便敲代码
//初始化顺序表
void SLInit(SL* ps);
//销毁顺序表
void SLDestroy(SL* ps);
//打印顺序表
void SLPrint(SL* ps);
//检查容量大小
void SLCheck(SL* ps);
//顺序表的增删查改
void SLPushBack(SL* ps, SLDatatype x); //尾插
void SLPopBack(SL* ps); //尾删
void SLPushFront(SL* ps, SLDatatype x); //头插
void SLPopFront(SL* ps); //头删
//任意位置插入
void SLInsert(SL* ps, int pos, SLDatatype x);
//任意位置删除
void SLErase(SL* ps, int pos);
//查询顺序表
int SLFind(SL* ps, SLDatatype x);
//初始化顺序表
void SLInit(SL* ps);
//顺序表的初始化
void SLInit(SL* ps)
{
SLDatatype* tmp = (SLDatatype*)malloc(sizeof(SLDatatype) * INIT_CAPACITY);
assert(tmp);
ps->a = tmp;
ps->capacity = INIT_CAPACITY;
ps->size = 0;
}
注意:malloc开辟的空间是给ps->a开辟的,指针a是指向存放顺序表的空间的,不要把开辟的内存给ps.
//销毁顺序表
void SLDestroy(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
malloc申请的空间在不使用后要使用free销毁,否则就会出现内存泄露的问题。
//打印顺序表
void SLPrint(SL* ps);
//顺序表的打印
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//检查容量大小
void SLCheck(SL* ps);
//检查
void SLCheck(SL* ps)
{
assert(ps);//断言 为假就会在程序运行过程中报错来提醒你
if (ps->size == ps->capacity)//扩容
{
SLDatatype* tmp = (SLDatatype*)realloc(ps->a, sizeof(SLDatatype) * 2*ps->capacity);
assert(tmp);//检查是否扩容成功
ps->a = tmp;
ps->capacity *= 2;
}
}
在每次插入前都要进行检查size是否等于capacity,容量满后要进行扩容,预防异地扩容和扩容失败(很少出现),不要直接用ps->a = (SLDatatype*)realloc(ps->a, sizeof(SLDatatype) * 2*ps->capacity);。记得capacity要乘2。
//顺序表的增删查改
void SLPushBack(SL* ps, SLDatatype x); //尾插
//顺序表的增删查改
void SLPushBack(SL* ps, SLDatatype x) //尾插
{
SLCheck(ps);
ps->a[ps->size++] = x;
}
void SLPopBack(SL* ps); //尾删
void SLPopBack(SL* ps) //尾删
{
if(ps->size > 0)
{
ps->size--;
}
}
不要想着删一个释放一个空间,把实际大小size--就行了。
void SLPushFront(SL* ps, SLDatatype x); //头插
void SLPushFront(SL* ps, SLDatatype x) //头插
{
SLCheck(ps);
memmove(ps->a + 1,ps->a,sizeof(SLDatatype)*ps->size);
ps->a[0] = x;
ps->size++;
}
头插的时间复杂度比尾插的高,顺序表不擅长进行头插,头删。这里除了可以用memmove外还可以用循环一个一个的移动。
void SLPopFront(SL* ps); //头删
void SLPopFront(SL* ps) //头删
{
SLCheck(ps);
memmove(ps->a, ps->a+1, sizeof(SLDatatype) * ps->size-1);
ps->size--;
}
//任意位置插入
void SLInsert(SL* ps, int pos, SLDatatype x);
//任意位置插入
void SLInsert(SL* ps, int pos, SLDatatype x)
{
assert(pos >0 && pos <= ps->size+1);
SLCheck(ps);
memmove(ps->a+pos, ps->a+pos-1, sizeof(SLDatatype) * ps->size-pos+1);
ps->a[pos-1] = x;
ps->size++;
}
这里可以复用头插的代码,只需要注意指针的位置即可,可以给具体的例子找规律(加减乘除真的是数学中最难的部分),要注意size的边界问题,size+1是尾插的情况。
//任意位置删除
void SLErase(SL* ps, int pos);
//任意位置删除
void SLErase(SL* ps, int pos)
{
assert(pos > 0 && pos <= ps->size);
memmove(ps->a + pos-1, ps->a + pos , sizeof(SLDatatype) * ps->size - pos);
ps->size--;
}
//查询顺序表
int SLFind(SL* ps, SLDatatype x);
//查询顺序表
int SLFind(SL* ps, SLDatatype x)
{
int i = 0;
while (i < ps->size)
{
if (ps->a[i] == x)
{
return i + 1;
}
i++;
}
return -1;//没查到
}
顺序表总结
1.中间头部插入,删除数据,需要挪动数据,效率低下。
2,空间不够,扩容,扩容有一定损耗,其次可能有一定的空间浪费。
四、链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
![](https://img-blog.csdnimg.cn/direct/57d9b266e5de4c60aabcae5494820620.png)
2. 带头或者不带头(也叫哨兵位)
![](https://img-blog.csdnimg.cn/direct/8a16f8c621434a4ca45487de1ae5f7cf.png)
3. 循环或者非循环
我们实际中最常用还是两种结构:
1. 无头单向非循环链表
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。
五、链表实现
无头单向非循环链表的实现
typedef int SLTDatatype;
typedef struct SLTlist {
SLTDatatype x; //节点的内容
struct SLTlist* next; //指向下一个节点的指针
}SLTNode;
//单链表的打印
void SLTPrint(SLTNode* phead);
//单链表的尾插
void SLTPushback(SLTNode** pphead, SLTDatatype val);
//扩容一个节点
SLTNode* BuyNode(SLTDatatype val);
//单链表的尾删
void SLTPopback(SLTNode** pphead);
//单链表的头插
void SLTPushfront(SLTNode** pphead, SLTDatatype val);
//单链表的头删
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);
//单链表查找
void SLTDestroy(SLTNode** pphead);
//单链表的打印
void SLTPrint(SLTNode* phead);
//单链表的打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->x);
cur = cur->next;
}
printf("NULL\n");
}
打印单链表当cur为空时也可以打印,只是打印的链表也为空链表。
//扩容一个节点
SLTNode* BuyNode(SLTDatatype val);
//扩容一个节点
SLTNode* BuyNode(SLTDatatype val)
{
SLTNode* tmp = (SLTNode*)malloc(sizeof(SLTNode));
assert(tmp);
tmp->next = NULL;
tmp->x = val;
return tmp;
}
//单链表的尾插
void SLTPushback(SLTNode** pphead, SLTDatatype val);
//单链表的尾插
//这里用二级指针来接收是因为当单链表为空时尾插会改变phead指针
//但是一级指针只能改变结构体,没办法改变结构体的指针,
//是改变int需要int*,那改变SLTNode*就需要SLTNode**,
//当然如果你想要用一级指针来实现也是完全没问题的,
//只需要给这个函数添加一个返回值即可。
//SLTNode* SLTPushback(SLTNode* phead, SLTDatatype val)
void SLTPushback(SLTNode** pphead, SLTDatatype val)
{
//没有节点时
if (*pphead == NULL)
{
*pphead = BuyNode(val);
}
else//有节点时需要找到最后一个节点进行尾插
{
SLTNode* tail = *pphead;
//找尾
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = BuyNode(val);
}
}
这里用二级指针来接收是因为当单链表为空时尾插会改变phead指针但是一级指针只能改变结构体,没办法改变结构体的指针,就像是改变int需要int*,那改变SLTNode*就需要SLTNode**,当然如果你想要用一级指针来实现也是完全没问题的,只需要给这个函数添加一个返回值即可。//SLTNode* SLTPushback(SLTNode* phead, SLTDatatype val)。我后面用的都是二级指针,在实现过程中你都可以改成一级指针。
//单链表的尾删
void SLTPopback(SLTNode** pphead);
//单链表的尾删
void SLTPopback(SLTNode** pphead)
{
if(*pphead!=NULL)
{
SLTNode* tail = *pphead;
//找尾
while (tail->next->next != NULL)
{
tail = tail->next;
}
//自己开的内存,含着泪也要释放掉
free(tail->next);
tail->next = NULL;
}
}
尾删很简单只要记得释放自己malloc的内存就没什么大问题。跟顺序表不同,单链表删除一个节点就需要释放一个节点的空间。
//单链表的头插
void SLTPushfront(SLTNode** pphead, SLTDatatype val);
//头插
//这里用二级的原因跟尾插一样,链表为空时头插会改变phead,你可以改成带返回值的形式
//SLTNode* SLTPushfront(SLTNode* phead, SLTDatatype val)
void SLTPushfront(SLTNode** pphead, SLTDatatype val)
{
SLTNode* newnode = BuyNode(val);
newnode->next = *pphead;
*pphead = newnode;
}
这里用二级的原因跟尾插一样,链表为空时头插会改变phead,你可以改成带返回值的形式
SLTNode* SLTPushfront(SLTNode* phead, SLTDatatype val)。
//单链表的头删
void SLTPopfront(SLTNode** pphead);
//头删
void SLTPopfront(SLTNode** pphead)
{
if (*pphead != NULL)
{
//保存第二个节点的地址
SLTNode* newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
}
单链表的头插,头删,尾插,尾删都会改变第一个节点的地址所以都需要用二级指针。
//单链表查找
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x);
//单链表的查找
//这个函数是用来服务在任意位置插入,删除函数的
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->x == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
这个函数是用来服务在任意位置插入,删除函数的,没找到对应的节点就返回NULL。
// pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x);
// pos位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
assert(pos);//pos表示没有这个节点直接中断程序
//pphead保存的是一个指针的地址,不可能为NULL,
//因为pphead是二级指针,如果为NULL,就表示一级指针的地址是NULL,
//而指针的内容可以为空,地址是不可能为空的。所以需要断言。
assert(pphead);
//当pos为头结点时,等于头插
if (pos == *pphead)
{
SLTPushfront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuyNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
pphead保存的是一个指针的地址,不可能为NULL,因为pphead是二级指针,如果为NULL,就表示一级指针的地址是NULL,而指针的内容可以为空,地址是不可能为空的。所以需要断言。
这里的函数名是pos位置之前插入,是因为你在顺序表中任意位置插入时,实际上是把pos和pos之后的都往后挪动了一位,把数据插到了原本pos的前面,这里的情况类似。
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos);
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
assert(pphead);
//当pos为头结点时,等于头删
if (pos == *pphead)
{
SLTPopfront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
//释放内存
free(pos);
}
}
// pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDatatype x);
// pos位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDatatype x)
{
assert(pos);
SLTNode* newnode = BuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
单链表很适合在pos位置尾插,尾删。
// pos位置后面删除
void SLTEraseAfter(SLTNode* pos);
// pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//防止pos是最后一个节点
SLTNode* tmp = pos->next;
pos->next=pos->next->next;
free(tmp);
}
//单链表销毁
void SLTDestroy(SLTNode** pphead);
//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* del = cur->next;
free(cur);
cur = del;
}
*pphead = NULL;
}
带头双向循环链表的实现
typedef int LTDataType;
typedef struct ListNode {
struct ListNode* prev;
LTDataType val;
struct ListNode* next;
}LTNode;
//创建一个节点
LTNode* BuyLTNode(LTDataType x);
//链表的初始化
LTNode* LTInit();
//链表的删除
void LTDestroy(LTNode* phead);
//链表的打印
void LTPrint(LTNode* phead);
//判断链表是否为空
bool LTEmpty(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
// 查找
LTNode* LֵTFind(LTNode* phead, LTDataType x);
//在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置
void LTErase(LTNode* pos);
//创建一个节点
LTNode* BuyLTNode(LTDataType x);
LTNode* BuyLTNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
//开辟空间失败
if (newnode == NULL)
{
perror("BuyLTNode");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
exit(1); 为异常退出 ,只要括号内数字不为0都表示异常退出
exit(0); 为正常退出 注意:括号内的参数都将返回给操作系统
//链表的初始化
LTNode* LTInit();
//创建一个哨兵位
LTNode* LTInit()
{
LTNode* head = BuyLTNode(-1);
head->next = head;
head->prev = head;
return head;
}
哨兵位的下一位是存放数据的第一个节点,哨兵位的上一位是存放数据的最后一个节点。哨兵位不用来存放数据。
//链表的删除
void LTDestroy(LTNode* phead);
//链表的删除
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
//因为是双向循环链表,cur不可能为NULL
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
这里要考虑销毁链表遍历时的结束条件。
//链表的打印
void LTPrint(LTNode* phead);
void LTPrint(LTNode* phead)
{
assert(phead);
printf("head<=>");
//打印从哨兵位的下一位开始
LTNode* cur = phead->next;
while (cur!=phead)
{
printf("%d<=>",cur->val);
cur = cur->next;
}
printf("\n");
}
//判断链表是否为空
bool LTEmpty(LTNode* phead);
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next ==phead;
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
/*assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;*/
//复用LTInsert
LTInsert(phead, x);
}
这个函数复用了LTInsert函数,注释部分是单独实现的代码。
//尾删
void LTPopBack(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//链表为空
if (LTEmpty(phead))
{
return;
}
/*LTNode* del = phead->prev;
LTNode* tail = del->prev;
tail->next = phead;
phead->prev = tail;
free(del);
del = NULL;*/
LTErase(phead->prev);
}
这个函数复用了LTErase函数,注释部分是单独实现的代码。phead->prev指向的是最后一个节点。
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
/*assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* start = phead->next;
newnode->next = start;
start->prev = newnode;
newnode->prev = phead;
phead->next = newnode;*/
//复用LTInsert
LTInsert(phead->next, x);
}
//头删
void LTPopFront(LTNode* phead);
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//链表为空
if (LTEmpty(phead))
{
return;
}
/*LTNode* del = phead->next;
LTNode* start = del->next;
phead->next = start;
start->prev = phead;
free(del);
del = NULL;*/
LTErase(phead->next);
}
// 查找
LTNode* LֵTFind(LTNode* phead, LTDataType x);
// 查找
LTNode* LֵTFind(LTNode* phead, LTDataType x)
{
LTNode* cur = phead->next;
while (cur!= phead)
{
if (cur->val == x)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
//在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x);
//在pos位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyLTNode(x);
LTNode* prev= pos->prev;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
一共要改变4个指针。
//删除pos位置
void LTErase(LTNode* pos);
//删除pos位置
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
一共要改变2个指针。
六、链表和顺序表的区别
不同点 | 顺序表 | 链表 |
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定 连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
如果有错误请在评论区指出,我看到后会修改,非常感谢您的观看。