一、单链表前言
上篇文章我们讲述了顺序表,认真学习我们会发现顺序表优缺点。
缺点1:头部和中部的插入删除效率都不行,时间和空间复杂度都为O(N);
缺点2:空间不够了扩容有一定的消耗(尤其是realloc的异地扩容);
缺点3:扩容逻辑,可能还存在空间,就像我只需要插入170个数据,但是要扩容200个数据大小,就会造成浪费。
优点1:尾插尾删足够快;
优点2:下标的随机访问和修改很快很方便;
基于顺序表的缺点我们通过今天的单链表来解决;
二、链表
2.1链表的基本概念和结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
结构图:
链表和顺序表一样也是通过结构体实现的,不一样的是链表中的结构体只含有有效数据和指向下个结构体的结构体指针
让我们先通过代码实现一个简单的链表吧!
#include<stdio.h>
#include<stdlib.h>
typedef struct SListNode{int data;struct SListNode* next;
}SLTNode;
int main()
{SLTNode*n1=(SLTNode*)malloc(sizeof(SLTNode));n1->data=1;SLTNode*n2=(SLTNode*)malloc(sizeof(SLTNode));n2->data=2;SLTNode*n3=(SLTNode*)malloc(sizeof(SLTNode));n3->data=3;n1->next=n2;n2->next=n3;n3->next=NULL;SLTNode*plist=n1;while(plist){printf("%d->",plist->data);plist=plist->next;}printf("NULL\n");return 0;}
这里我们开辟了三个结构体的空间用三个结构体指针指向每个结构体,然后将每个结构体里的结构体指针指向下一个结构体,再将每个结构体存入有效数据,一个简单的3长度的链表。
注意:
1.看上面的图逻辑上我们可能会认为是连续的,但是在物理地址内存上可能是连续的也可能是不连续的;
2.现实中的每个结点都是从堆上开辟的
3.从堆上开辟空间是按照一定策略的,两次连续的开辟空间可能是连续的,也可能是不连续的
2.2链表的分类
2.2.1单向或者双向
2.2.2带头或者不带头
2.2.3循环或者非循环
虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。
三、无头单向非循环链表增删查改链表的实现
3.1链表的创建
typedef int SLTDatatype;
typedef struct SListNode
{SLTDatatype data;struct SListNode* next;
}SLTNode;
3.2填充数据及开辟新的结点
SLTNode* BuySLT(SLTDatatype x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc failed");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
链表的实现靠的是结构体里的结构体指针指向下一个结构体链接,所以我们要将新的结点返回,
返回的是结构体指针,定义函数是也要结构体指针接受。
3.3打印链表
void SLprint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
我们是靠头结构体指针找到链表的,但是打印有效数据要移动指针所以我们就要在一开始创建一个指针,利用这个指针的移动打印数据,当这个指针为空时并不是指向空时停止打印。
3.4查找有效数据
SLTNode* SLFind(SLTNode* phead, SLTDatatype x)
{SLTNode* cur = phead;while (cur){if (cur->data = x){return cur;}cur = cur->next;}return NULL;
}
和打印有效数据相似通过另一个指针的移动查找我们需要的有效数据,当指针为空时我们找不到有效数据返回NULL,当找到有效数据时返回指向含有这个有效数据的结构体;
3.5单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = newnode;}
}
首先我们需要一个空结构体指针指向开辟好的返回值为结构体指针,当无链表时也相当于头插我们只需将开辟好返回值为结构体指针赋值给空指针就行,此时这个链表只有一个结构体;当链表已经形成时此时才是我们的尾插,我们需要另一个指针从头开始找到链表末尾的空指针,再将这个空指针指向以开辟好的返回值为结构体指针就可以从尾部将其串联起来;
为什么要使用二级指针呢?
我们是有一个指针指向空或者链表的头,在尾插时我们要通过头指针的移动增添数据,但是这个指针我们不可以移动,移动的话会造成内存泄漏,所以我们又创建一个指针将这个指针指向我们的空或者链表的头,通过指针的操作将我们的链表串联起来,我们也可以将这个指针想象成我们的头指针,这个指针会变化相当于头指针变化,但是我们是在一个函数域中操作,除了这个作用域一切都会销毁,相当于什么都没敢。通俗的说我们是在操作头指针,在另个作用域中改变我们原有的值只能通过二级指针实现,但是头指针已经是个指针了,所以我们需要一个二级指针。
3.6尾删
void SLTPopBack(SLTNode** pphead)
{if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
首先我们要判断这个链表是否只有一个结构体,如果只有一个结构体直接将这个结构体释放销毁就行了;如果有多个结构体链接只需要找到倒数第二个结构体中的指向最后一个结构体的指针,释放这个指向最后一个结构体的指针就是可以实现尾删;
3.7头插
void SLPushFront(SLTNode** pphead, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);if ((*pphead) == NULL){*pphead = newnode;}else{SLTNode* newnode = BuySLT(x);newnode->next = *pphead;*pphead = newnode;}
}
如果这个头指针为空时及没有链表,直接将这个指针指向开辟好的结构体的就行,如果不为空,将开辟好的结构体里指向下一个结构体的结构体指针指向我们的头指针,再将头指针指向开辟好的结构体就行;
3.8头删
void SLPopFront(SLTNode** pphead)
{if ((*pphead) == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;}
}
首先我们还是先判断这个链表是否只含有一个结构体,如果只含有一个结构体和尾删的方法一样
如果含有多个结构体头删,我们只需要新创建一个指针保存第一个结构体里储存着第二个结构体的指针,然后释放掉我们的头指针,再将新创建的指针赋值给头指针就可以完成尾删。
到这里无头单向非循环链表增删查改的重点内容和注意事项就讲解完毕了,下面我们实现一下链表中间随机的增删查改,就不一一讲解了,大家自行了解;
四、无头单向非循环链表中间随机增删查改
4.1在pos位置后插入x
void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);newnode->next = pos->next;pos->next = newnode;
}
4.2在pos位置后插入x
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{if (pos == *pphead){SLPushFront(pphead, x);}else{SLTNode* newnode = BuySLT(x);SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;}
}
4.3删除pos位置的值
void SLTErase(SLTNode** pphead, SLTNode* pos)
{if ((*pphead) == pos){SLPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}
4.4删除pos位置后面的值
void SLEraseAfter(SLTNode* pos)
{assert(pos->next);SLTNode* newnode = pos->next->next;free(pos->next);pos->next = newnode;
}
五、无头单向非循环链表增删查改完整代码
#define _CRT_SECURE_NO_WARNINGS 67
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDatatype;
typedef struct SListNode
{SLTDatatype data;struct SListNode* next;
}SLTNode;
//打印
void SLprint(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
//开辟新的空间
SLTNode* BuySLT(SLTDatatype x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc failed");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
//查找
SLTNode* SLFind(SLTNode* phead, SLTDatatype x)
{SLTNode* cur = phead;while (cur){if (cur->data = x){return cur;}cur = cur->next;}return NULL;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = newnode;}
}
//尾删
void SLTPopBack(SLTNode** pphead)
{if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
//头插
void SLPushFront(SLTNode** pphead, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);if ((*pphead) == NULL){*pphead = newnode;}else{SLTNode* newnode = BuySLT(x);newnode->next = *pphead;*pphead = newnode;}
}
//头删
void SLPopFront(SLTNode** pphead)
{if ((*pphead) == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* newhead = (*pphead)->next;free(*pphead);*pphead = newhead;}
}
//在pos位置后插入x
void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{SLTNode* newnode = BuySLT(x);newnode->next = pos->next;pos->next = newnode;
}
//在pos位置前插入x
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{if (pos == *pphead){SLPushFront(pphead, x);}else{SLTNode* newnode = BuySLT(x);SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;}
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{if ((*pphead) == pos){SLPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}void SLEraseAfter(SLTNode* pos)
{assert(pos->next);SLTNode* newnode = pos->next->next;free(pos->next);pos->next = newnode;
}
int main()
{SLTNode* plist = NULL;//尾插SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushBack(&plist, 5);SLTPushBack(&plist, 10);printf("尾插:\n");SLprint(plist);//尾删printf("尾删:\n");SLTPopBack(&plist);SLprint(plist);//头插printf("头插:\n");SLPushFront(&plist, 10);SLprint(plist);//头删printf("头删:\n");SLPopFront(&plist);SLprint(plist);//在pos位置后插入xSLTNode* pos1 = SLFind(plist, 1);SLInsertAfter(pos1, 10);printf("在pos位置后插入x\n");SLprint(plist);//在pos位置前插入xSLTNode* pos2 = SLFind(plist, 1);printf("在pos位置前插入x\n");SLInsert(&plist, pos2, 20);SLprint(plist);//删除pos位置的值SLTNode* pos3 = SLFind(plist, 10);SLTErase(&plist, pos3);printf("删除pos位置的值:\n");SLprint(plist);//删除pos后面的值SLTNode* pos4 = SLFind(plist, 1);SLEraseAfter(pos4);printf("删除pos位置后面的值:\n");SLprint(pos4);return 0;
}
到这里我们的无头单向非循环链表增删查改的所有内容就讲解完了,有什么问题或者需要指正的可以在评论区留下您宝贵的意见。