目录
前言
1、顺序表的缺陷
缺陷一:空间会经常不够,导致需要多次请求扩容空间
在顺序表那一节,我们讲到了顺序表基本函数结构的实现,也讲到了顺序表其实和数组本质差不多,存放的数据都是连续的,但是通过一些头插、尾插和指定位置插入数据可以看出顺序表在有些情况下可能会出现空间不够,需要扩容的操作,而且对于扩容机制,也会有本地扩容和异地扩容的方式,尤其是对于异地扩容,需要先释放掉原先存放的地址,然后再去异地申请一块地址,这其实是需要一定代价的。对于扩容的大小来说,一扩就是原来的2倍,这些扩出来的2倍我们不一定都能使用得完,所以就存在一定的空间浪费。
缺陷二:进行头插和头删时,需要挪动大量的数据
当我们进行头插头删或者指定位置去删除数据的时候,我们都要挪动大量的数据,除了尾插尾删 的情况下,并且在这两种插入和删除中最坏的两种,时间复杂度接近O(N),因此我们可以看出对于顺序表它还具有一大缺陷就是对于数据的插入和删除需要挪动大量的数据,因为时间复杂度的过高,导致效率的低下。
2、优化方法
对于空间不要需要扩容的问题,我们可以使用动态申请一个空间或者说是一个结点,然后每个结点相对应的去连接起来,也就是本节要介绍的【链表】。
对于缺陷二来说增删需要挪动大量的数据情况下,下面会介绍链表的实现的方式,链表就是将前一个结点和后一个节点相互连接起来,这样我们要进行插入和删除数据的时候,只需要修改他们的连接点即可,不需要挪动大量数据进行存储。
一、链表的初步认识和实现方式
链表的声明和定义
我们首先看看链表的结构是什么样子的,链表有两个结构一个是数据域另一个是下一个地址的指针域,每一个结点因为要存放数据和下一个结点的地址,因此需要一个数据域、一个指针域,如下图所示:
了解了链表的结构体是怎么实现的之后,接下来我们用代码的形式将其展现出来。
可以看到,这里数据域的数据类型是单独typedef重定义的,这个在顺序表中提到过,因为每一个数据可能并不是整型的,可能是char、long或者是double类型。对于next指针域,你可以看到其为一个结构体指针类型,因为这个next指针,它指向的下一个结点又是一个封装好的结构体。
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;//数据域
struct SListNode* next;//下一个结点的指针域
}SListNode;
二、链表功能实现
1.链表尾插
进行尾插我们要注意pplist传入的指针,因为我们使用的是不带头的链表,当链表没有数据时,我们要添加数据的话是对链表的内容进行修改,要对指针内容进行修改就需要传入指针的地址,那我们接收指针的地址就是二级指针了,这里也是不带头节点链表的关键点,大多数敲写链表会出现传入一级指针进去,那在形参里只是实参的一个临时拷贝,是要注意的地方,代码如下:
void SListPushBack(SListNode** pplist, SLTDataType x) {
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL) {//
*pplist = newnode;
}
else {
SListNode* tail = *pplist;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode;
}
}
插入一个新节点,我们可以封装一个可以创建新节点的函数,代码如下:
SListNode* BuySListNode(SLTDataType x) {
SListNode* p = (SListNode*)malloc(sizeof(SListNode));
if (p == NULL) {
perror("p malloc");
exit(-1);
}
p->data = x;
p->next = NULL;
return p;
}
我们尾插是通过找到最后一个节点进行尾插插入新节点,那么就需要找到节点下一位为NULL为止,那链表尾插的时间复杂度就是O(N)了,相对顺序表来说是比较久的。
注意:千万不要找到NULL才停止,是找到节点下一位是NULL,因为找到NULL后,插入新节点是对NULL进行修改是不正确以及会报错的。
2.链表尾删
链表尾删其实跟尾插的逻辑是相似的,这边我们使用的是先找到尾节点时记录尾节点的前一个节点地址,然后对尾节点释放掉且对尾节点的前一个节点的下一个节点地址置空。但是也是要注意的是,假设只有一个节点进行尾删我们就又是对pplist链表内容进行修改,所以也是要使用一个二级指针对指针内容进行修改。
void SListPopBack(SListNode** pplist) {
assert(pplist);
assert(*pplist);
if ((*pplist)->next == NULL) {
free(*pplist);
*pplist = NULL;
}
else {
SListNode* tail = *pplist, * prev = NULL;
while (tail->next) {
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
这里尾删的复杂度也是跟尾插的复杂度是一样的,都是O(N)。
3.链表数据内容打印
链表打印是不需要对链表内容进行修改的,所以这边直接传入链表指针即可,不需要传入二级指针。以及打印也很简单,直接依次打印直至到NULL为止,就可以退出循环。
void SListPrint(SListNode* plist) {
SListNode* tail = plist;
while (tail) {
printf("%d->", tail->data);
tail = tail->next;
}
printf("NULL\n");
}
4.链表头插
头插,插入第一个节点也就是影响指针的内容,那我们也要传入二级指针进行添加。
void SListPushFront(SListNode** pplist, SLTDataType x) {
assert(pplist);
SListNode* tail = BuySListNode(x);
tail->next = *pplist;
*pplist = tail;
}
这里我们会发现,头插相比尾插是非常简单的,而且时间复杂度为O(1),效率快的不是一点半点。
5.链表头删
对链表头删我们要保存头节点,让链表内容指向下一个节点,再释放掉保存的内容即可。
void SListPopFront(SListNode** pplist) {
assert(pplist);
assert(*pplist);
SListNode* tail = *pplist;
*pplist = (*pplist)->next;
free(tail);
tail = NULL;
}
6.查找数据
查找数据这,其实非常容易,只要使用一个循环语句去查找即可,然后返回该结点,但是如果找不到就退出循环,并且返回一个NULL回去,方法多种按着自己的需求编写即可。
SListNode* SListFind(SListNode* plist, SLTDataType x){
/*assert(plist);*/
SListNode* tail = plist;
while (tail != NULL) {
if (tail->data == x)
return tail;
tail = tail->next;
}
return NULL;
}
接下来有查找的位置插入数据和删除数据,但是也会区分前后关系,插入前后不同实现的方式也不一样,首先我们先来点简单些的,指定位置之后插入数据和指定位置之后删除数据:
7.指定位置之后插入数据
我们这里用pos来表示指定的数据,我们只需要将新结点的下一个结点的指针域存储成pos的下一个结点即可,再将pos的下一个指针域指向新结点即可。
void SListInsertAfter(SListNode* pos, SLTDataType x) {
assert(pos);
SListNode* tail = pos;
SListNode* p = BuySListNode(x);
p->next = tail->next;
tail->next = p;
}
8.指定位置之后删除数据
在pos位置删除之后的数据也是相对来说很容易,可以用tail记录要删除的结点,然后只需要将pos的next,指向下下一个位置即可,在free掉删除的数据,但是要注意的是,如果pos后没有数据我们就没必要删除了,可以给个警告说明后面为NULL,删除失败的,代码如下:
void SListEraseAfter(SListNode* pos) {
assert(pos);
assert(pos->next);
SListNode* tail = pos->next;
pos->next = pos->next->next;
free(tail);
tail = NULL;
}
接下来到相对来说比较难的啦,在指定pos位置之前插入数据和删除pos的位置
9.在指定位置之前插入数据
在指定位置之前插入其实跟尾插的复杂性很相似,我们找到了位置直接插入,那我们该如何让上一个节点连接到新节点里,显然是会出现问题的,那我们需要新建一个指针变量去查找,只要节点的下一个节点跟pos节点相同,代表我们已经找到了pos的上一个节点了,我们就可以直接插入数据。
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x) {
assert(pos);
assert(pplist);
SListNode* into = BuySListNode(x);
SListNode* tail = *pplist;
if (tail == pos) {
into->next = *pplist;
*pplist = into;
return;
}
while (tail->next != pos ) {
tail = tail->next;
}
tail->next = into;
into->next = pos;
}
但是这里需要注意一个问题,假设我们是插入的第一个节点,就会出现问题,因为经过刚刚思考的步骤,我们要存储pos的上一个节点进行插入,可是第一个节点就没有上一个节点,所以我们就需要单独判断,如果pos指定的位置就是第一个位置,只需要用上头插的技巧即可。
10.删除指定位置
删除指定位置跟尾删差不多的实现原理,也是需要找到上一个节点才能进行删除,但是这实现原理跟指定位置前插入有同一样的问题,删除第一个节点怎么办?简单!单独判断如果是第一个节点直接头删即可,代码如下:
void SLTErase(SListNode** pplist, SListNode* pos) {
assert(pplist);
assert(pos);
SListNode* tail = *pplist;
if (tail == pos) {
*pplist = tail->next;
free(tail);
tail = NULL;
return;
}
while (tail->next != pos) {
tail = tail->next;
}
tail->next = tail->next->next;
free(pos);
pos = NULL;
}
11.销毁链表
我们对链表的销毁不可以像顺序表一样只free掉第一个节点就完事了,顺序表他是一个连续的内存,但链表不一样,他只是我们自己在脑子里抽象的连接了起来,实际在内存里各有各的一块空间,所以我们要一个一个进行释放!代码如下:
void SLTDestroy(SListNode** pplist) {
assert(pplist);
SListNode* tail = *pplist;
while (tail){
*pplist = (*pplist)->next;
free(tail);
tail = *pplist;
}
*pplist = NULL;
}
三、完整代码
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode* next;
}SListNode;
void SListPushBack(SListNode** pplist, SLTDataType x);//尾插
void SListPopBack(SListNode** pplist);//尾删
void SListPushFront(SListNode** pplist, SLTDataType x);//头插
void SListPopFront(SListNode** pplist);//头删
SListNode* SListFind(SListNode* plist, SLTDataType x);//查找数据
void SListInsertAfter(SListNode* pos, SLTDataType x);//pos之后位置插入数据
void SListEraseAfter(SListNode* pos);//pos之后的位置删除
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x);//pos位置前面插入数据
void SLTErase(SListNode** pplist, SListNode* pos);//删除pos位置
void SListPrint(SListNode* plist);//打印
void SLTDestroy(SListNode** pphead);//销毁
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
SListNode* BuySListNode(SLTDataType x) {
SListNode* p = (SListNode*)malloc(sizeof(SListNode));
if (p == NULL) {
perror("p malloc");
exit(-1);
}
p->data = x;
p->next = NULL;
return p;
}
void SListPushBack(SListNode** pplist, SLTDataType x) {
assert(pplist);
SListNode* newnode = BuySListNode(x);
if (*pplist == NULL) {//
*pplist = newnode;
}
else {
SListNode* tail = *pplist;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPopBack(SListNode** pplist) {
assert(pplist);
assert(*pplist);
if ((*pplist)->next == NULL) {
free(*pplist);
*pplist = NULL;
}
else {
SListNode* tail = *pplist, * prev = NULL;
while (tail->next) {
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
void SListPushFront(SListNode** pplist, SLTDataType x) {
assert(pplist);
SListNode* tail = BuySListNode(x);
tail->next = *pplist;
*pplist = tail;
}
void SListPopFront(SListNode** pplist) {
assert(pplist);
assert(*pplist);
SListNode* tail = *pplist;
*pplist = (*pplist)->next;
free(tail);
tail = NULL;
}
SListNode* SListFind(SListNode* plist, SLTDataType x){
/*assert(plist);*/
SListNode* tail = plist;
while (tail != NULL) {
if (tail->data == x)
return tail;
tail = tail->next;
}
return NULL;
}
void SListInsertAfter(SListNode* pos, SLTDataType x) {
assert(pos);
SListNode* tail = pos;
SListNode* p = BuySListNode(x);
p->next = tail->next;
tail->next = p;
}
void SListEraseAfter(SListNode* pos) {
assert(pos);
assert(pos->next);
SListNode* tail = pos->next;
pos->next = pos->next->next;
free(tail);
tail = NULL;
}
void SLTInsert(SListNode** pplist, SListNode* pos, SLTDataType x) {
assert(pos);
assert(pplist);
SListNode* into = BuySListNode(x);
SListNode* tail = *pplist;
if (tail == pos) {
into->next = *pplist;
*pplist = into;
return;
}
while (tail->next != pos ) {
tail = tail->next;
}
tail->next = into;
into->next = pos;
}
void SLTErase(SListNode** pplist, SListNode* pos) {
assert(pplist);
assert(pos);
SListNode* tail = *pplist;
if (tail == pos) {
*pplist = tail->next;
free(tail);
tail = NULL;
return;
}
while (tail->next != pos) {
tail = tail->next;
}
tail->next = tail->next->next;
free(pos);
pos = NULL;
}
void SListPrint(SListNode* plist) {
SListNode* tail = plist;
while (tail) {
printf("%d->", tail->data);
tail = tail->next;
}
printf("NULL\n");
}
void SLTDestroy(SListNode** pplist) {
assert(pplist);
SListNode* tail = *pplist;
while (tail){
*pplist = (*pplist)->next;
free(tail);
tail = *pplist;
}
*pplist = NULL;
}
总结
看完了上述有关单链表的所有操作,从这些代码中我们可以看到对于二级指针和断言的使用比较频繁,很多接口算法中都用到了这两个东西,但是对于二级指针和断言,并不是什么地方都要使用的。
仔细地分析一下就可以知道,对于头插、尾插、头删、尾删这些操作,以及在pos结点之前插入和删除结点、销毁结点,这些接口均需要使用到二级指针来接受链表的头指针,我们要改变一级头结点指针就要使用二级指针去修改,对于上面这些接口操作,均需要修改头结点指针,因此便需要使用到二级指针去进行一个控制。
但是呢,并不是所有的接口都需要使用到二级指针,像打印、查找、在pos结点之后插入和删除结点这些操作,都是不会修改头结点指针的,所以我们只需要以及指针就可以了。
但是也会发现单链表也有他自己的缺陷,像尾插和尾删我们就要遍历找到尾节点才能进行相应的操作时间复杂度也是O(n),后面我们会讲到带头双向循环链表来解决相应的问题。
以上就是本文所要讲解的全部内容,非常感谢您的观看。