线性表
1️⃣ 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表: 2️⃣ 顺序表、 3️⃣ 链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
注:
1️⃣线性的意思是数据以连线的方式存储
2️⃣顺序表本质上就是数组。但是在此之上,顺序表要求存储的数据必须是从头开始、连续紧挨着的,不能跳跃间隔
3️⃣链表是用指针把一块一块内存链接起来
图示:
![](https://i-blog.csdnimg.cn/blog_migrate/71283d96f2b89d44e1d3c66f24bad9f0.jpeg)
顺序表在逻辑结构上是连续的,物理结构上也是连续的;
链表在逻辑结构上是连续的,但是在物理结构上不是连续的。
顺序表
概念及结构
顺序表是 用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。 顺序表的物理结构和逻辑结构是一致的。
插:创建程序的时候,命名尽量用英文。因为日后在工作中各个源文件链接的时候,中文会出现转换问题,引来不必要的麻烦
顺序表一般可以分为:
1. 静态顺序表:使用 定长数组存储。 特点是如果满了就不让插入。 缺点是空间给多大合适呢?很难确定。N给小了不够用,给大了浪费
2. 动态顺序表:使用 动态开辟的数组存储。
静态顺序表:
//静态的顺序表,并不实用
#define N 100//一发,动全身
struct SeqList//SeqList:顺序表
{
int a[N];
int size;//表示数组中存储了多少个数据
};
注:
用结构体的好处:不仅找到开辟的空间大小,还知道存储的数据个数
以后凡是涉及多个数据的,尽量考虑用结构体
缺点:
因为结构体中定义了数据类型为int,故后续想再插入其它类型的数据就不行了
改进一:可以修改存储的数据类型,插入其它类型的数据
#define N 100
typedef int SLDataType;//int重命名为SLDataType,即顺序表数据类型。以后想插入什么类型就把int改成什么类型
//例如插入double类型,即:typedef double SLDataType;
struct SeqList
{
SLDataType a[N];
int size;
};
void SeqListPushBack(struct SeqList* ps, int x);//SeqListPushBack,即顺序表尾部插入
缺点:
struct SeqList太长,不方便
改进二:将struct SeqList重命名为SL,方便使用
#define N 100
typedef int SLDataType;//int重命名为SLDataType,即顺序表数据类型
typedef struct SeqList
{
SLDataType a[N];
int size;
}SL;
void SeqListPushBack(SL* ps, int x);
![](https://i-blog.csdnimg.cn/blog_migrate/3a218d2a5ebeaee06e150c66e2cf16bc.jpeg)
动态顺序表:
//顺序表的动态存储
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* array; //把数组改成指针,意味着我们不再开辟一块固定大小的空间,而是指向一块空间
int size; //表示数组中存储了多少个数据
int capacity; //表示array指向的空间实际能够存数据的空间容量是多大
}SL;
![](https://i-blog.csdnimg.cn/blog_migrate/6b7c9fe79e9eb6e6bc46662a9ac69ea9.jpeg)
接口函数:
//尾插
void SeqListPushBack(SL* ps, SeqDataType x);
//头插
void SeqListPushFront(SL* ps, SeqDataType x);
//头删
void SeqListPopFront(SL* ps);
//尾删
void SeqListPopBack(SL* ps);
//查找
int SeqListFind(SL* ps, SeqDataType x);
//修改
void SeqListModify(SL* ps, int pos, SeqDataType x);
//中间位置插入
void SeqListInsert(SL* ps, int pos, SeqDataType x);
//中间位置删除
void SeqListErase(SL* ps, int pos);
//初始化
void SeqListInit(SL *ps);
//销毁
void SeqListDestory(SL *ps);
//打印
void SeqListPrint(SL* ps);
//扩容
void SeqCheckCapacity(SL* ps);
注:这里接口函数取名是根据STL里的vector来取的,方便日后学习STL。
用处:
在通讯录中,我们输入了某个人的信息。那我们就可以调用所需要的接口函数,把这个信息插入到顺序表当中。顺便提一句,假如插入的信息数据类型是多样的,即结构体类型。那么我们就用 typedef 结构体名 Type;
接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开辟多了浪费,开辟少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
示例一:尾插
Seqlist.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SL, SEQ;
//初始化
void SeqListInit(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SeqDataType x);
//扩容
void SeqCheckCapacity(SeqList* ps);
//打印
void SeqListPrint(SL* ps);
//销毁
void SeqListDestory(SL* ps);
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);//看解释2
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
SL.c
#include "SeqList.h"//
//对顺序表的初始化
void SeqListInit(SL *ps)//看解释1
{
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;//看解释3
}
//扩容
void SeqCheckCapacity(SL* ps)
{
//如果没有空间或空间不足,则扩容
if (ps->size == ps->capacity)//有两种情况:1.刚初始化,指针为空,size=capacity=0 2.size=capacity≠0
{
//int newcapacity = ps->capacity * 2; //这样会存在问题,当一开始插入数据时,capacity为0,乘2后还是为0
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; //如果capacity等于0了,则开辟4个空间
SeqDataType* newA = realloc(ps->a, sizeof(SeqDataType*) * newcapacity);//看解释8
if (newA == NULL)//看解释9
{
printf("realloc fail\n");
exit(-1);//看解释10
}
//程序能运行到这儿,说明realloc开辟空间成功
ps->a = newA;//把刚开辟的空间给它
ps->capacity = newcapacity;//把刚扩容的空间给它
}
}
//尾插
void SeqListPushBack(SL* ps, SeqDataType x)
{
assert(ps);
//扩容函数,检查空间是否足够,如果满了则扩容,反之直接插入数据:
SeqCheckCapacity(ps);//看解释6
//插入数据:
ps->a[ps->size] = x;//在有效个数后面放入要插入的数据x
ps->size++;//插入一个数据后有效个数加1
}
//打印
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
//销毁
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;//防止a成为野指针
ps->capacity = ps->size = 0;
}
解释:
1️⃣如果void SeqListInit(SL *ps)初始化成功,则调用监视时,显示结果是这样的:
![](https://i-blog.csdnimg.cn/blog_migrate/4da3df9a5772b3b5448eefd9a51bc2bb.png)
2️⃣ 对SeqListInit(&s1),为何用&s1而不用s1?
s1是我们定义的一个结构体类型变量,如果用s1,那么对应的void SeqListInit(SL ps)中的ps,属于传值调用。然而函数调用后的形参ps不是实参s1,而是s1的临时拷贝,形参的改变无法影响实参。那我们后续搞的尾插,头插,头删...毫无意义
![](https://i-blog.csdnimg.cn/blog_migrate/806314bab87d407f0e24b82f2db57737.png)
所以我们要改成传址调用,传址调用是形参通过实参传递的地址访问实参的空间,并在其空间进行修改
3️⃣为什么该项目中对结构体成员的访问都是用的->而不是.?
因为我们函数调用采用的是传址调用,所以要用->
4️⃣动态顺序表的尾插是怎么实现的?
下图举例,插入数据5
![](https://i-blog.csdnimg.cn/blog_migrate/d4f2d7aec62f271505a606b0f9d85175.jpeg)
5️⃣动态顺序表尾插的三种情况:
整个顺序表都没有空间,处于刚初始化,空指针和无空间状态
空间不够,先扩容
空间足够,直接插入数据即可
6️⃣ 为什么会有SeqCheckCapacity(ps)?
因为接下来我们要在顺序表中插入数据,插入前就必须检查空间是否充足。如果充足,就数据就插入,否则要扩容再插入
7️⃣扩容时,我们不是扩一个插一个,效率低。而是一次扩现有容量空间的2倍
8️⃣这里用realloc的原因:
实际上,第一次进来的时候,指针变量为空指针、size=capacity=0。这时我们应该先开辟一块空间,再考虑之后的扩容。开辟空间,我们就需要用到malloc函数,而realloc是对已有空间扩容。但是,如果realloc指向的那块空间是空指针,那么realloc函数的功能等价于malloc函数,即开辟空间
9️⃣newA==NULL则表明上面的realloc分申请空间失败,返回NULL
🔟exit()详解:
头文件:stdlib.h
功能:为退出程序的函数
用法:
exit(1); 为异常退出 //只要括号内数字不为0都表示异常退出
exit(0); 为正常退出
注意:括号内的参数都将返回给操作系统;
return() 是返回到上一级主调函数,不一定会退出程序;
示例二:尾删
Seqlist.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SL, SEQ;
//初始化
void SeqListInit(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SeqDataType x);
//扩容
void SeqCheckCapacity(SeqList* ps);
//打印
void SeqListPrint(SL* ps);
//销毁
void SeqListDestory(SL* ps);
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
SeqListPopBack(&s1);
SeqListPopback(&s1);
SeqListPrint(&s1);
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
SL.c
#include "SeqList.h"//
//对顺序表的初始化
void SeqListInit(SL *ps)
{
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
//扩容
void SeqCheckCapacity(SL* ps)
{
//如果没有空间或空间不足,则扩容
if (ps->size == ps->capacity)//有两种情况:1.刚初始化,指针为空,size=capacity=0 2.size=capacity≠0
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SeqDataType* newA = realloc(ps->a, sizeof(SeqDataType*) * newcapacity);
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//程序能运行到这儿,说明realloc开辟空间成功
ps->a = newA;
ps->capacity = newcapacity;
}
}
//尾删
void SeqListPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0); //看解释4
ps->size--;
//注意,不能把pq->size置0,这不叫删除,且万一该数据就是0呢
}
//打印
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
//销毁
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;//防止a成为野指针
ps->capacity = ps->size = 0;
}
解释:
1️⃣尾删的错误写法:
//尾删
void SeqListPopBack(SL* ps)
{
//尾删思路:
//第一步:把size空间里最后那个位置上的数字赋为0
//第二步:把size减一,相当于有效空间减一
ps->a[ps->size-1]=0;//看解释2
ps->size--;看解释3
}
/*经过验证,我们发现:即使把第7行代码删掉,程序运行结果也不变
表明:给size的最后一个有效数字赋值为0没啥意义,它要是本身为0相当于啥都没做。而且删的对象是size的空间,跟你这个数是啥没啥关系
*/
2️⃣a[ps->size-1]什么意思?
ps->size-1的作用是找到size空间的最后位置上的数的下标,又因为数组排序是从0开始,所以实际size大小要减一才能对应到数组中去
a[p->size-1]是定位到size空间的最后位置,即等效最后那个数。对它进行赋值就是对那个末位数进行赋值
3️⃣ps->size--的注意事项:
size不能想减多少就减多少,减到1是它的极限。因为一旦小于1,比如size=-2,那么之后再对a进行尾插时会发生非法访问,销毁时free作用下系统会检测出来并报警告
改进:
//尾删
void SeqListPopBack(SL* ps)
{
for(ps->size > 0)
{
ps->size--;
}//大于0时减一,小于等于0时,啥都不干
}
4️⃣assert是比for更粗暴的方法,因为你要是用for条件判断的话,你不符合条件我只是不执行。但是assert是你一旦不满足条件,我就弹出警告,让你后面的尝试中断。
⏩for是温柔处理方式,assert是暴力处理方式
示例三:头插
Seqlist.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SL, SEQ;
//初始化
void SeqListInit(SL* ps);
//尾插
void SeqListPushBack(SL* ps, SeqDataType x);
//扩容
void SeqCheckCapacity(SeqList* ps);
//打印
void SeqListPrint(SL* ps);
//销毁
void SeqListDestory(SL* ps);
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
SeqListPushFront(&s1,10);
SeqListPushFront(&s1,20);
SeqListPrint(&s1);
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
SL.c
#include "SeqList.h"//
//对顺序表的初始化
void SeqListInit(SL *ps)
{
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
//扩容
void SeqCheckCapacity(SL* ps)
{
//如果没有空间或空间不足,则扩容
if (ps->size == ps->capacity)//有两种情况:1.刚初始化,指针为空,size=capacity=0 2.size=capacity≠0
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SeqDataType* newA = realloc(ps->a, sizeof(SeqDataType*) * newcapacity);
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//程序能运行到这儿,说明realloc开辟空间成功
ps->a = newA;
ps->capacity = newcapacity;
}
}
//头插
void SeqListPushFront(SL* ps,SLDataType x)
{
//检查空间是否充足,看解释1,2
SeqListCheckCapacity(ps);
//挪动数据,看解释1
int end = ps->size-1;
while(end>=0)
{
ps->a[end+1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
//打印
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
//销毁
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;//防止a成为野指针
ps->capacity = ps->size = 0;
}
1️⃣思路:
![](https://i-blog.csdnimg.cn/blog_migrate/0a7e9fc9a0ab94fc3805dbfbf59f0907.jpeg)
但是以上这个思路有个问题没有考虑到,那就是我把数据往后挪的时候,我压根没有考虑是否后面还有没有空间。如果没有空间又没有扩容就把数据往后移,是非法访问地址,销毁时free会查出这个错误并警告
2️⃣扩容时,分为两种扩容方式:原地扩容和异地扩容。
原地扩容:该空间后面还有足够的剩余空间,那就把原空间往后扩容
异地扩容:该空间后面没有足够的剩余空间了,那就在内存中重新开辟一块满足需求的新空间,并把原数据拷贝下来,然后再释放旧空间返回新空间地址。
所以,你知道我们为什么要用relloc开辟空间了吧?
示例四:头删
Seqlist.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SeqDataType; //如果要修改则只需要修改这一处
typedef struct SeqList
{
SeqDataType* a;
int size; //表示a中有多少个有效数据
int capacity; //表示a的空间到底有多大
}SL, SEQ;
//初始化
void SeqListInit(SL* ps);
//头删
void SeqListPopFront(SL* ps);
//打印
void SeqListPrint(SL* ps);
//销毁
void SeqListDestory(SL* ps);
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
SeqListPopFront(&s1);
SeqListPrint(&s1);
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
SL.c
#include "SeqList.h"//
//对顺序表的初始化
void SeqListInit(SL *ps)
{
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
//头删
void SeqListPopFront(SL* ps)
{
assert(ps->size > 0);//还可以警告空间为空的情况
int begin=1;
//挪动数据
while(begin < ps->size)
{
ps->a[begin-1] = a[begin];
++begin;
}//看解释1
ps->size--;
}
//打印
void SeqListPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
//销毁
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;//防止a成为野指针
ps->capacity = ps->size = 0;
}
1️⃣思路:
![](https://i-blog.csdnimg.cn/blog_migrate/63ad241af855c7e7a0c835ef6ae2ed84.jpeg)
示例五:查找
int SeqListFind(SL* ps, SLDataType x)
{
for(int i=0; i<ps->size; i++)
{
if(ps->a[i]==x)
{
return i;
}
}
return -1;
}//找到了返回x位置下标,找不到返回-1
示例六:插入
void SeqListInsert(SL* ps, int pos, SLDataType x)
{
/*温柔处理方式:
for(pos < 0 || pos > ps-> size)
{
printf("pos invalid\n");
return;
}*/
//暴力处理方式:
assert(pos >= 0 && pos < ps->size);//看解释2
SeqListCheckCapacity(ps);
int end=ps->size-1;
while (end >= pos)
{
ps->a[end + 1] = ps->[end];
end--;
}//看解释3
ps->a[pos] = x;
ps->size++;//看解释4
}//指定pos位置数据插入
1️⃣思路:
限定插入范围
检查容量空间是否有剩余(只要capacity比size多1就行,反正只插入一个数),不足则扩容
把pos位置上及之后的元素往后挪1
最后用x覆盖pos位置
2️⃣限定插入的范围:必须在数组有效空间内
![](https://i-blog.csdnimg.cn/blog_migrate/93115182ad9c84bb965dbf7bd5304eba.jpeg)
3️⃣
![](https://i-blog.csdnimg.cn/blog_migrate/6b7282a2d641e6ec53dbadd78a2772b4.jpeg)
4️⃣ps->size++有什么意义?
插入数据x后,空间a的有效数据size就增加了1,所以最后要把它补上
示例七:插入的高级用法--任意元素替换
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);//看解释2
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
int pos=SeqListFind(&s1,4);
if(pos !=-1)
{
SeqListInsert(&s1,pos,40);
}//看解释1
SeqListPrint(&s1);
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
1️⃣找到元素4,并用40替换它
示例八:改进头插--复用插入
void SeqListPushFront(SL* ps,SLDataType x)
{
SeqListInsert(ps,0,x);
}
示例九:改进尾插--复用插入
//尾插
void SeqListPushBack(SL* ps, SeqDataType x)
{
SeqListInsert(ps,ps->size,x);
}
示例十:删除
指定pos位置数据删除
void SeqListErase(SL* ps, int pos)
{
assert(pos >= 0 && pos < ps->size);//看解释1
int begin = pos+1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}//看解释2
ps->size--;
}
1️⃣为什么pos < ps->size,而不是?
因为pos代表数组a的下标,从0开始到size-1。当pos=ps->size时,不再属于有效空间的范围
![](https://i-blog.csdnimg.cn/blog_migrate/697fd6e812a83877511afc51f032dc65.jpeg)
2️⃣思路:类似于插入,不同的是它是往前覆盖
![](https://i-blog.csdnimg.cn/blog_migrate/1abd93062aa55a664153abf857dc0f1b.jpeg)
示例十一:改进头删--复用删除
void seqListPopFront(SL* ps)
{
assert(ps->size > 0);
SeqListErase(ps,0);
}
示例十二:改进尾删--复用删除
void SeqListPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
SeqListErase(ps,ps->size);
}
示例十三:删除的高级用法--任意元素的删除
test.c:
#include "SeqList.h"
//测试尾插
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);//看解释2
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPrint(&s1); //1 2 3 4 5
int pos=SeqListFind(&s1,5);//找到5的下标
if(pos !=-1)
{
SeqListErase(&s1,pos);
}//看解释1
SeqListPrint(&s1);
SeqListDestory(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
1️⃣找到数字5并删除它
示例十四:合并上述功能,写一个通讯录
Seqlist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 要求:存储的数据从0开始,依次连续存储
// 静态的顺序表
// 问题:开小了,不够用。开大了,存在浪费。
//#define N 10000
//struct SeqList
//{
// int a[N];
// int size; // 记录存储了多少个数据
//};
typedef int SLDataType;
// 动态的顺序表
typedef struct SeqList
{
SLDataType* a;
int size; // 存储数据个数
int capacity; // 存储空间大小
}SL, SeqList;
void SeqListPrint(SeqList* psl);
//void SLInit(SeqList* psl);
void SeqListInit(SeqList* psl);
void SeqListDestroy(SeqList* psl);
void SeqListCheckCapacity(SeqList* psl);
// 时间复杂度是O(1)
void SeqListPushBack(SeqList* psl, SLDataType x);
void SeqListPopBack(SeqList* psl);
// 时间复杂度是O(N)
void SeqListPushFront(SeqList* psl, SLDataType x);
void SeqListPopFront(SeqList* psl);
// 在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 删除pos位置的数据
void SeqListErase(SeqList* psl, size_t pos);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
test.c
#include "SeqList.h"
void TestSeqList1()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushBack(&s, 0);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPushBack(&s, 10);
SeqListPushBack(&s, 20);
SeqListPrint(&s);
//SeqListPrint(NULL);
// 越界不一定能检查出来,越界是抽查
// 就像查酒驾一样。不一定会被查到,但是我们一定不能酒驾
//int a[10];
//a[10] = 1;
//a[11] = 1;
//a[12] = 1;
//a[100] = 1;
}
void TestSeqList2()
{
/*int* p = (int*)malloc(sizeof(int) * 10);
printf("%p\n", p);
int* p1 = (int*)realloc(p, sizeof(int) * 100);
printf("%p\n", p1);*/
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, -1);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, -1);
SeqListPrint(&s);
}
void TestSeqList3()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPrint(&s);
SeqListInsert(&s, 10, 100);
SeqListInsert(&s, 1, 20);
SeqListInsert(&s, 5, 50);
SeqListInsert(&s, 0, 50);
SeqListPrint(&s);
SeqListDestroy(&s);
}
void TestSeqList4()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPrint(&s);
SeqListErase(&s, 4);
SeqListPrint(&s);
SeqListErase(&s, 0);
SeqListPrint(&s);
SeqListErase(&s, 1);
SeqListPrint(&s);
}
void TestSeqList5()
{
SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushFront(&s, -1);
SeqListPushFront(&s, -2);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
}
void menu()
{
printf("***********************************************\n");
printf("请选择你的操作:>\n");
printf("1.头插数据 2.头删数据\n");
printf("3.尾插数据 4.尾删数据\n");
printf("5.打印数据 6.查找数据\n");
printf("-1.退出 \n");
printf("***********************************************\n");
}
void MenuTest()
{
SL s;
SeqListInit(&s);
int input = 0;
int x = 0;
while (input != -1)
{
Menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入你要头插的数据,以-1结束:\n");//缺点:为了设置一个标志,头插不了-1
sacnf("%d", &x);
while (x != -1)
{
SeqListPushFront (&s,x);
scanf("%d", &x);
}
break;
case 2:
printf("请输入你要头删的数据,以-1结束:\n");//缺点:为了设置一个标志,头删不了-1
sacnf("%d", &x);
while (x != -1)
{
SeqListPopFront(&s, x);
scanf("%d", &x);
}
break;
case 3:
printf("请输入你要尾插的数据,以-1结束:\n");//缺点:为了设置一个标志,尾插不了-1
sacnf("%d", &x);
while (x != -1)
{
SeqListPushBack(&s, x);
scanf("%d", &x);
}
break;
case 4:
SeqListPopBack(&s);
break;
case 5:
SeqListPrint(&s);
break;
case 6:
printf("请输入你要查找的数据,以-1结束:\n");//缺点:为了设置一个标志,尾插不了-1
sacnf("%d", &x);
while (x != -1)
{
SeqListFind(&s, x);
scanf("%d", &x);
}
break;
default:
printf("无此选项,请重新输入\n");
break;
}
}
SeqListDestory(&s);
}
int main()
{
MenuTest();
return 0;
}
SL.c
#include "SeqList.h"
void SeqListPrint(SeqList* psl)
{
assert(psl);
for (int i = 0; i < psl->size; ++i)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
void SeqListInit(SeqList* psl)
{
assert(psl);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
void SeqListDestroy(SeqList* psl)
{
assert(psl);
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
void SeqListCheckCapacity(SeqList* psl)
{
assert(psl);
// 如果满了,我们要扩容
if (psl->size == psl->capacity)
{
size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = realloc(psl->a, sizeof(SLDataType)*newCapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
psl->a = tmp;
psl->capacity = newCapacity;
}
}
}
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl);
/*SeqListCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;*/
SeqListInsert(psl, psl->size, x);
}
void SeqListPopBack(SeqList* psl)
{
assert(psl);
//psl->a[psl->size - 1] = 0;
/*if (psl->size > 0)
{
psl->size--;
}*/
SeqListErase(psl, psl->size-1);
}
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl);
//SeqListCheckCapacity(psl);
挪动数据,腾出头部空间
//int end = psl->size - 1;
//while (end >= 0)
//{
// psl->a[end + 1] = psl->a[end];
// --end;
//}
//psl->a[0] = x;
//psl->size++;
SeqListInsert(psl, 0, x);
}
void SeqListPopFront(SeqList* psl)
{
assert(psl);
// 挪动数据覆盖删除
/*if (psl->size > 0)
{
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
--psl->size;
}*/
SeqListErase(psl, 0);
}
// 在pos位置插入x
//void SeqListInsert(SeqList* psl, int pos, SLDataType x)
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
// 暴力检查
assert(psl);
// 温和检查
if (pos > psl->size)
{
printf("pos 越界:%d\n", pos);
return;
//exit(-1);
}
// 暴力检查
//assert(pos <= psl->size);
SeqListCheckCapacity(psl);
//int end = psl->size - 1;
//while (end >= (int)pos)
//{
// psl->a[end + 1] = psl->a[end];
// --end;
//}
size_t end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end-1];
--end;
}
psl->a[pos] = x;
psl->size++;
}
// 删除pos位置的数据
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t begin = pos + 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
psl->size--;
}
int SeqListFind(SeqList* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; ++i)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
数组相关面试题:
面试题1:移除元素
题目描述:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
思路一:从数组下标为0开始挨个检查,一旦发现这个数是等于val,那就让这个数后面的数依次往前覆盖挪1 (借鉴头删的思路)。挪完后,朝下一个下标重复上述操作
时间复杂度:O()
最坏的情况: 数组中的值全是等于val
运算: 覆盖第一个数,运行n-1次;运行第二个数,运行n-2次......即:
(n-1)+(n-2)+(n-3)+...+2+1=![]()
思路二: 一次遍历数组,把不是val的值,放到新开辟的数组tmp中,再把tmp数组的值拷贝回去
时间复杂度:O(2N)
运算: 遍历一遍N,拷贝回原数组时再遍历一遍N
空间复杂度:O(N) (以空间换取时间)
![](https://i-blog.csdnimg.cn/blog_migrate/c2f2b02128ec87aa695fc054554196df.png)
思路三:src挨个依次去找nums数组中不等于val的值,放到dst指向的位置去,再++src,++dst
时间复杂度:O(N)
空间复杂度:O(1)
运算: 在大段空间中只给了常数个变量,故可认为是O(1)
![](https://i-blog.csdnimg.cn/blog_migrate/5dcc3440025299f54566ef4434bb8383.jpeg)
int removeElement(int* nums, int numsSize, int val)
{
int src=0,dst=0;//定义指向数组两侧的变量
while(src<numsSize)//限定src的范围
{
if(nums[src]!=val)
{
nums[dst]=nums[src];
src++;
dst++;
}
else
{
src++;
}
}
return dst;//dst就是输出的数据个数
}
面试题2:删除有序数组中的重复项
题目描述:
给你一个升序排列的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。将最终结果插入 nums 的前 k 个位置后返回 k 。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
原理讲解:对数组设两个指针,分列两侧。当ii+1时,改变j指向的数组元素,并j++进一位。反之则不发生变化。无论是否相等,都要i++进一位。当i+1指向非法空间时,必须单独处理
![](https://i-blog.csdnimg.cn/blog_migrate/362df1ece8909aeab2e40a4506453212.jpeg)
int removeDuplicates(int* nums, int numsSize){
if(numsSize==0)
{
return 0;
}
int i=0,j=0;
while(i<numsSize-1)
{
if(nums[i+1]!=nums[i])
{
nums[j]=nums[i];
i++;
j++;
}
else
{
i++;
}
}
nums[j]=nums[i];
j++;
return j;
}
思路:如何用双指针考虑?
int removeDuplicates(int* nums, int numsSize)
{
//如果是空数组,直接返回0
if(numsSize== 0)
return 0;
int cur = 0;
int dst = 0;
int next = 1;
while(next < numsSize)
{
if(nums[cur] != nums[next])//前后不相等,直接把值赋给dst
{
nums[dst++] = nums[cur++];
next++;
}
else
{
//前后相等时,next继续往后找,直到两者不相等为止
//在该循环也需要限制next范围,否则容易出现越界
while(next < numsSize && nums[cur] == nums[next])
{
next++;
}
nums[dst] = nums[cur];
dst++;
cur = next; //cur跳过已经判断出相等的部分
next++;
}
}
if(cur < numsSize)//如果不进行判断,可能会出现越界访问
{
nums[dst] = nums[cur]; //把最后一个值赋给dst
dst++;
}
return dst;
}
面试题3: 合并两个有序数组
题目描述:
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
时间复杂度:O(m+n)
空间复杂度:0(1) (因为我没有开辟额外的空间)
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int end1 = m - 1, end2 = n - 1;
int end = m + n - 1;//看解释1
while (end1 >= 0 && end2 >= 0)//看解释2
{
if (nums1[end1] > nums2[end2])
{
nums1[end--] = nums1[end1--];
}
else
{
nums1[end--] = nums2[end2--];
}
}//看解释3
while (end2 >= 0)
{
nums1[end--] = nums2[end2--];
}//看解释4
}
1️⃣
![](https://i-blog.csdnimg.cn/blog_migrate/3ce8d2078901765d0491c266b718dc30.jpeg)
2️⃣为什么要把循环条件设为&&而不是| |?
这个就属于数学中的逻辑问题了,我们这样想:是不是end1或end2其中一个<0,咱这个程序就不用比较大小,就循环结束收尾了?那反过来,我们要这个循环还在运行,那我们就得让这俩货都0,
3️⃣
![](https://i-blog.csdnimg.cn/blog_migrate/d2156d322952b6526ca94dfddbeab11d.jpeg)
4️⃣
![](https://i-blog.csdnimg.cn/blog_migrate/deb7421958a86bdd8e8d757e0dbe1212.jpeg)
此时只有end2还没有<0,就把end2剩余的全赋给end对应的空框框
顺序表的缺陷
①内存不够需要扩容,扩容分为原地扩容和异地扩容,扩容需要付出代价(增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗)
原地扩容:
![](https://i-blog.csdnimg.cn/blog_migrate/553c8a9d8c58c21f46d86ab26b62eb7b.png)
异地扩容:
![](https://i-blog.csdnimg.cn/blog_migrate/278cd8787cb3be2384b8b04649ab7312.png)
②避免频繁扩容,我们满了基本都是扩2倍,可能就会导致一定的空间浪费(例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。)
③顺序表要求数据从开始位置连续存储,那么我们在头部或者中间位置插入删除数据就需要挪动数据,时间效率不高,时间复杂度为O(N)
注:针对顺序表的缺陷,就设计出了链表
顺序表的优点:
支持随机访问
cpu高速缓存命中率更高
链表
链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
链表的优点:
按需申请空间,不用了就释放空间。(空间的使用更合理)
任意位置插入删除数据效率高,时间复杂度O(1),不需要挪动数据
不存在空间浪费
链表的缺点:
每存一个数据,都要存一个指针去链接后面数据的节点
不支持随机访问(用下标直接访问第i个)
链表存储一个值,同时要存储连接指针,也有一定的消耗
cpu高速缓存命中率更低
注:有些算法需要结构支持随机访问,比如二分查找、优化的快排等等
链表的结构
![](https://i-blog.csdnimg.cn/blog_migrate/37edcd8219823057bf2b107d1c7cb31c.png)
链表的结构非常多样,大概可以分为八种结构:
带头还是不带头
![](https://i-blog.csdnimg.cn/blog_migrate/447e446d2a88be41a87096a0f0f50ad4.png)
注:当遇到尾删把链表删完的情况,plist依旧指向哨兵位head,不需要改变plist(带头)
而plist需要改变(不带头)
循环还是不循环
![](https://i-blog.csdnimg.cn/blog_migrate/ab61759e35d7211777ffb76624756ba7.png)
单向还是双向
![](https://i-blog.csdnimg.cn/blog_migrate/c1c6add1084f2104e142a0389d8088ee.png)
虽然单链表的结构众多,但大部分常用的还是两种结构。无头单向不循环链表和带头双向循环链表
![](https://i-blog.csdnimg.cn/blog_migrate/91aac0f9830e7f20b722cb95ecb48f72.png)
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
无头+单向+非循环链表接口实现
![](https://i-blog.csdnimg.cn/blog_migrate/0ff4f76ffa7b9e8461b727a8e0f6afee.jpeg)
逻辑图/结构:想象出来的,形象方式表示
![](https://i-blog.csdnimg.cn/blog_migrate/61738d91971df97af93570813873286c.jpeg)
物理图/结构:在内存中实实在在如何存储的
定义链表的基本结构 ,节点(在头文件中定义)
typedef int SLTDataType;
typedef struct SLTNode
{
SLTDataType data;
struct SLTNode* next;//下一个节点的地址
}SLTNode;
打印链表:
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;//看解释1
while (cur != NULL)
{
printf("%d->", cur->data);//看解释2
cur = cur->next;//看解释3
}
}
1️⃣phead是个指针变量,存放的是第一个数据d1的地址,SLTNode* cur = phead就相当于把d1的地址给了cur
![](https://i-blog.csdnimg.cn/blog_migrate/4d507b5408b74978f470e80c3fdb3fdd.jpeg)
2️⃣cur->data是啥意思?
cur:存储了节点的地址
->:具有解引用的功能
data:对应节点的存储数据
=>cur存着节点的地址,访问节点的data,再解引用得到data存储的数据
3️⃣cur->next <=> cur访问next,并解引用next,得到d2的地址
cur=cur->next<=>cur接收右侧传过来的地址。此时cur存放的是d2的地址
![](https://i-blog.csdnimg.cn/blog_migrate/664a3a5beabc07bc35bfc2539341ca3a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/7638c83c33879f98f22bc607fb3ea464.jpeg)
①SLTNode* cur = phead
②cur = cur->next
链表的尾插:
常见的错误写法:看解释5
void SeqListPushBack (SLTNode* phead,SLTDataType x)//看后面的完整尾插解释1
{
//找到尾节点
SLTNode* tail = phead;//看解释4
while (tail->next != NULL)//看解释3
{
tail = tail->next;
}//看解释4
//创建新节点,并给字点补充data和next
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//看解释2
newnode->data=x;
newnode->next=NULL;
//让尾节点的next包含的地址由NULL改成新节点的地址
tail->next=newnode;
}
1️⃣思路:我现在要在现有的链表末尾插入一个节点newnode,就要先用malloc创建出这个节点并得到它的地址,然后给这个节点data赋值x和next赋空指针,给前一个节点即尾节点tail的next赋newnode的地址
![](https://i-blog.csdnimg.cn/blog_migrate/20fec4f7f1fa0e1aea45e1338cc909b0.jpeg)
2️⃣SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)) 是啥意思?
malloc(sizeof(SLTNode)):给新节点开辟空间,开辟成功返回开辟空间地址,失败则返回NULL
(SLTNode*):新节点是SLTNode*类型,那么右边想把地址传过去,也必须强制类型转换成一样的类型
3️⃣循环结束的条件是tail->next = NULL,那么反之循环继续的条件便是tail->next != NULL
4️⃣找节点跟这个 SLTNode* tail = phead 有什么关系?
我们要找到尾节点tail,能想到的办法就是从第一个节点(phead有它的地址,∴SLTNode* tail = phead)开始,依次判断(用到while循环)它的next是否为0x0000000(即NULL),如果是则该节点一定是尾节点,反之则进入下一次循环继续判断。
5️⃣ 对SLTNode* tail = phead的理解:
![](https://i-blog.csdnimg.cn/blog_migrate/b89e1c863fd5409f0cfe410e5c9774f8.png)
同理,tail=phead和上述等价
5️⃣因为如果phead为空指针,那么tail->相当于解引用NULL空指针
空指针解引用:
C语言空指针的值为NULL。一般NULL指针指向进程的最小地址,通常这个值为0。试图通过空指针对数据进行访问,会导致运行时错误。当程序试图解引用一个期望非空但是实际为空的指针时,会发生空指针解引用错误。对空指针的解引用会导致未定义的行为。在很多平台上,解引用空指针可能会导致程序异常终止或拒绝服务。如:在 Linux 系统中访问空指针会产生 Segmentation fault 的错误。详细请参见CWEID 476: NULL Pointer Dereference ( http://cwe.mitre.org/data/definitions/476.html)。
正确的写法:
void SeqListPushBack (SLTNode** phead,SLTDataType x)//看解释2
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
if(*phead==NULL)
{
*phead=newnode;
}//看解释3
else
{
//找到尾节点
SLTNode* tail = phead;//看解释4
while (tail->next != NULL)//看解释3
{
tail = tail->next;
}//看解释4
//把新节点的位置放入之前的尾节点的next中
tail->next = newnode;
}
}
1️⃣思路:和上面的错误写法基本一致,唯一不同的是对phead=NULL这一情况的处理。当phead=NULL时,没有链表,也就不存在什么尾节点,更没有去找尾节点这一行为。故直接把新节点newnode的地址传给phead。
![](https://i-blog.csdnimg.cn/blog_migrate/15d6417f27ae1646fa370cfa9985c4c9.jpeg)
注:如果不想处理空链表的情况,那么只需要用assert(phead)即可。遇空链表警告
phead和plist的区别:都是指向第一个节点的地址,不过一个是形参一个是实参
2️⃣为什么会采用二级指针 SLTNode** phead ?
因为当链表不存在,只有一个头指针plist时,不存在找尾节点的行为。只需要把头指针接到新节点即可。接这个过程的本质是改变头指针内存储的地址NULL,把它变为新节点的地址。这属于形参的改变影响实参
![](https://i-blog.csdnimg.cn/blog_migrate/b6626abf8ae983a73520db4e720adc6a.jpeg)
尾插的完整写法:
SList.h
#include <stdio.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//下面是函数的声明,不可省略
void SListPrint(SLTNode* phead);
void SeqListPushBack(SLTNode** pphead, SLTDataType x);
Test.c
#include "SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);//看解释1、2
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist);
}
int main()
{
TestSList1();
return 0;
}
1️⃣phead和plist的区别:都是指向第一个节点的地址,不过一个是形参一个是实参
2️⃣为什么传参时要用&plist,plist本身就等于地址,为什么不直接用plist?
![](https://i-blog.csdnimg.cn/blog_migrate/3ee3e45d5a3d1d82dd882694e70751c2.jpeg)
传的都不是plist本身地址,而是内容。phead的修改不会引起plist的改变,相当于就是传值调用
SList.c
#include <stdio.h>
#include "SList.h"
#include <stdlib.h>
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
}
void SListPushBack(SLTNode** pphead, SLTDataType x)//看解释1
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
//把新节点的位置放入之前的尾节点的next中
tail->next = newnode;
}
}
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/4230dbaa69cebafcbf7d5d301c0b53e7.jpeg)
1️⃣回顾一下指针传参:
![](https://i-blog.csdnimg.cn/blog_migrate/334d25f5d853947a4520d219c523cd94.jpeg)
链表的头插:
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = *pphead;//pphead是它本身的地址,*pphead是它的内容:第一个节点的地址
*pphead = newnode;
}
思路:
![](https://i-blog.csdnimg.cn/blog_migrate/0dc419c7f8eb4d60431f6a7dab306857.jpeg)
完整的头插:
SList.h
#pragma once
#include <stdio.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void SListPrint(SLTNode* phead);
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode** pphead, SLTDataType x);
Test.c
#include "SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushFront(&plist, 0);
SListPrint(plist);
}
int main()
{
TestSList1();
return 0;
}
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "SList.h"
#include <stdlib.h>
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;//看解释1
while (cur != NULL)
{
printf("%d->", cur->data);//看解释2
cur = cur->next;//看解释3
}
printf("NULL\n");//为了把链表最终指向NULL这一情况表现出来。没有它输出就没有NULL
}
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
if (*pphead == NULL)
{
*pphead = newnode;
}//看解释1
else
{
//找到尾节点
SLTNode* tail = *pphead;//看解释4
while (tail->next != NULL)//看解释3
{
tail = tail->next;
}//看解释4
//把新节点的位置放入之前的尾节点的next中
tail->next = newnode;
}
}
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = *pphead;
*pphead = newnode;
}
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/f5fe13e30626762a9bad659a1eed2b7b.jpeg)
因为每次插的时候,都要创建一次新节点。所以干脆写一个创建新节点的函数:
//创建节点
SLTNode* BuyListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)//空间开辟失败
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}//看解释1
else
{
//找到尾节点
SLTNode* tail = *pphead;//看解释4
while (tail->next != NULL)//看解释3
{
tail = tail->next;
}//看解释4
//把新节点的位置放入之前的尾节点的next中
tail->next = newnode;
}
}
链表的尾删:
错误写法:
void SListPopBack(SLTNode** pphead)//看解释1
{
SLTNode* tail = *pphead;
while (tail->next)//看解释2
{
tail = tail->next;
}
free(tail);//看解释3
tail = NULL;//看解释4
}
1️⃣为什么尾删也需要用到二级指针,难道要该plist的内容吗?
对滴,当尾删到极限,即把链表全删完时。确实要把plist的next改为NULL
2️⃣while(tail->next)是个啥玩意?
while(tail->next)等价于while(tail->next != NULL)。NULl就等价于0,非0就是真。
指针,整型这些都可以作为循环判定依据
3️⃣free(tail)是什么意思?
free函数的作用是让系统回收括号内地址对应的空间,tail现在等于尾节点的空间地址,所以free(tail)就是系统回收尾节点空间或者说释放尾节点的空间
4️⃣tail = NULL是什么意思?
把尾节点置空
5️⃣为什么这个代码错了?
尾节点被释放置空了,然而尾节点前一个节点的next还指向已经被释放空间的尾节点,这就是一个经典的指向野指针问题。正确的做法是补充一个:把尾节点的前一个节点的next置空
改进写法--找尾节点的前一个节点:
方法一:
void SListPopBack(SLTNode** pphead)//看解释1
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
//找尾节点及其前一个节点
while (tail->next)
{
prev = tail;//把判定不是尾节点的地址将prev覆盖,确保prev总是在tail前一个节点
tail = tail->next;
}
//找到了,接下来把尾节点删掉,前一个节点prev的next接NULL
free(tail);
tail = NULL;
prev->next = NULL;//把尾节点的前一个节点置空变为尾节点
}
1️⃣思路:
![](https://i-blog.csdnimg.cn/blog_migrate/2b35b1952af376a732a79c5ee652fe1a.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/4b03980b80145dbe4780f6da3f18dfcc.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/6e6f7e24a423604461b834dc7d520571.jpeg)
经while循环判断,第三步的tail指向尾节点,故此时的prev指向尾节点的前一个节点
方法二:
void SListPopBack(SLTNode** pphead)//看解释1
{
SLTNode* tail = *pphead;
while (tail->next->next)//此时的tail是倒数第三个节点
{
tail = tail->next;//此时的tail是倒数第二个节点
}
free(tail->next);//tail->next对应尾节点地址
tail->next = NULL;
}
但是,上述两种写法依然有问题:当链表只有5个节点而我们要删除6个节点的时候,程序运行就会崩溃。因为发生了上述访问野指针的问题。
怎么发生的?分析一波:
当把最后一个节点回收置空后,plist依然存的是已经回收释放的第一个节点的地址。后续打印时,plist会访问该节点,造成访问野指针。故当出现尾删全部节点时,我们要把plist存放的地址置空。
void SListPopBack(SLTNode** pphead)//看解释1
{
/*//温柔一点
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)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
思路:
当链表有两个及以上的节点时,走else--while依次删尾节点
当链表仅一个节点时,走if释放该节点,并把pphead置空
尾删的完整写法:
SList.h
#pragma once
#include <stdio.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
void SListPrint(SLTNode* phead);
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode** pphead, SLTDataType x);
void SListPopBack(SLTNode** pphead);
Test.c
#include "SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushFront(&plist, 0);
SListPrint(plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPopBack(&plist);
SListPrint(plist);
}
int main()
{
TestSList1();
return 0;
}
SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "SList.h"
#include <stdlib.h>
#include <assert.h>
//打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//创建节点
SLTNode* BuyListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
//把新节点的位置放入之前的尾节点的next中
tail->next = newnode;
}
}
//头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SListPopBack(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)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
链表的头删:
错误的代码:
void SListPopFront(SLTNode** pphead)
{
assert(*pphead);//假如链表为空,则删个屁!看解释1
SLTNode* FirstNode = *pphead->next;//看解释2
free(*pphead);//free(第一个节点地址),即把第一个节点空间释放
*pphead = FirstNode;
}
1️⃣必须有它的原因:
该代码等价于assert(*pphead!=NULL),即假如pphead存储的是空指针,那么FirstNode->next会发生解引用空指针,引发警告。有两种情况pphead存储的是空指针而不是第一个节点的地址:
我多次头删,把链表都删没了,只剩下一个NULL覆盖pphead
本来链表就没有,pphead存的就是NULL
2️⃣该行代码等价于:
SLTNode* FirstNode = *pphead; //FirstNode是新的第一个节点
=>引入指针变量FirstNode,把第一个节点的地址传给它
*pphead = FirstNode->next;
=>用第二个节点的地址覆盖pphead的第一个节点地址
链表的查找:
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)//遍历链表
{
if (cur->data == x)
{
return cur;
}//找到了,返回该节点地址
else
{
cur = cur->next;
}//该节点不是我们要找的节点,往下一个节点判断
}
return NULL;//没找到,返回空指针
}
这份代码有个缺陷:万一我要找的数字与多个节点的data都相等怎么办?
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 2);
SListPushBack(&plist, 4);
SLTNode* pos1 = SListFind(plist, 2);//找到data为2的节点的地址
SLTNode* pos2 = NULL;
if (pos1 != NULL)
{
pos2 = SListFind(pos1->next, 2);//看解释1
}
}
1️⃣找第二个对应相等的节点的地址肯定不能从头开始,不然又要得到pos1的地址。所以我们从pos1的下一个地址开始查找
这份代码也有个缺陷,那就是我要已知链表中有多少个对应相等的节点才能这么写。那有没有一种方法,就是我不知道链表中有多少个对应相等的节点,但是可以全部找出来?
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 2);
SListPushBack(&plist, 4);
SListPushBack(&plist, 2);
SListPushBack(&plist, 2);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 2);
int i = 1;
while (pos)
{
printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
pos = SListFind(pos->next, 2);
}
}
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/47924433e618c6211cd9cd089999aeb1.png)
链表的修改:
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 2);
SListPushBack(&plist, 4);
SListPushBack(&plist, 2);
SListPushBack(&plist, 2);
SListPushBack(&plist, 4);
SLTNode* pos = SListFind(plist, 2);
int i = 1;
while (pos)
{
pos->data=30;//把2改为30
pos=SListFind(pos->next,2);
}//把链表中data为2的节点都改为data为30
}
链表的插入--前插:
错误的写法:
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//创建新节点
SLTNode* newnode = BuyListNode(x);
//找到pos的前一个节点
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;//用插入的节点的地址把前一个节点内pos的地址覆盖了
newnode->next = pos;//把pos的地址传给插入的节点的next
}
SLTNode* pos = SListFind(plist, 3);
if (pos)
{
SListInsert(&plist, pos, 30);//在data为3的节点前插入data为30的新节点
}
SListPrint(plist);
该代码错误的原因是:当我们要在第一个节点前插入一个新节点时,我们会发现popPrev没有next。即头插失败
正确的写法:
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//创建新节点
SLTNode* newnode = BuyListNode(x);
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}//头插写法
else
{
//找到pos的前一个节点
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
SLTNode* pos = SListFind(plist, 3);
if (pos)
{
SlistInsert(&plist, pos, 30);
}
SListPrint(plist);
前插的改进:让每个符合条件的节点前都插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//创建新节点
SLTNode* newnode = BuyListNode(x);
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}//头插写法
else
{
//找到pos的前一个节点
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
SLTNode* pos = SListFind(plist, 3);
while (pos)
{
SListInsert(&plist, pos, 30);
pos = SListFind(pos->next, 2);
}
SListPrint(plist);
运行结果:每个2前面都插入了30
![](https://i-blog.csdnimg.cn/blog_migrate/a14c98ea4ff32f531225ddd737a610ab.png)
单链表的缺陷还是很多的,单纯的单链表增删查找意义不大。之所以要学,主要是以下几点:
很多OJ题考察的都是单链表
单链表更多的是作更复杂数据结构的子结构。比如哈希桶、邻接桶
链表的插入--后插
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
链表的删除:
void SListErase(SLTNode** pphead, SLTNode* pos)
{
//头删
if (*pphead == pos)
{
/*
*pphead = pos->next;
free(pos);
*/
SListPopFront(pphead);
}
//中间删
else
{
SLTNode* prev = NULL;
//找到pos前一个节点prev
while (prev->next != pos)
{
prev = prev->next;
}
//pos节点的两侧节点相连
prev->next = pos->next;
//删除节点
free(pos);
pos = NULL;//有没有这步无所谓
}
}
链表的删除--删除pos后一个节点
void SListEraseAfter(SLTNode* pos)
{
assert(pos->next);
SLTNode* Next = pos->next;
pos->next = Next->next;
free(Next);
Next = NULL;//有没有这步无所谓
}
注:以后写函数,凡是不为空的,都可以尝试加个assert
链表的销毁
错误的代码:
void SListDestory(SLTNode* pphead)
{
assert(pphead);
free(pphead);
}
错误的原因:链表不是顺序表,顺序表是数组,申请一块地址,后续操作是对那一块地址的空间增减。释放该地址就释放了整个空间。而链表是内存中一块块四处分别的空间的组成,要想销毁链表,就得把每块空间都一一销毁了
正确的代码:
void SListDestory(SLTNode* pphead)
{
assert(pphead);
SLTNode* cur=*pphead;
while(cur)
{
SLTNode* Next=cur->next;
free(cur);
cur=Next;
}
*pphead=NULL;
}
链表面试题:
面试题一:移除链表元素
题目描述:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* prev=NULL,* cur=head;//看解释1
while(cur)//看解释2
{
if(cur->val==val)
{
//头删,看case2
if(cur==head)
{
head=cur->next;
free(cur);
cur=head;
}
//中间删除,看case1
else
{
prev->next=cur->next;
free(cur);
cur=prev->next;//把cur移向下一个节点,继续查找删除
}//删除
}
else
{
prev=cur;
cur=cur->next;
}//往后挪
}
return head;//看解释3
}
思路:
case 1:头节点不符合删除条件
![](https://i-blog.csdnimg.cn/blog_migrate/d6f8d0307e671123d6ab7acb35d453ef.png)
case 2:头节点符合删除条件,按case的删除处理不可以。因为最终我们要return head。head都销毁了,就是野指针了
解决方法:我们不销毁head,我们把head后的下一个节点地址传给head,再把存有head地址的cur给销毁了。最后把head内存储的第二个节点地址传给cur。如此一来,返回head时,链表就是从第二个节点开始,也就达到了删除的目的
1️⃣
创建两个指针,cur找和val相等的节点,prev找cur的前一个节点。
cur从head开始找,故首先存储head头节点的地址。prev要比head慢一步,故先存储NULL,避免成为野指针。
2️⃣循环结束意味着把链表中所有符合cur->val==val这一条件的节点都删除了,故肯定要遍历完链表,故结束标志是cur==NULL。故写成while(cur->next)就错了,因为当尾节点符合删除要求时,一下子while循环就结束了,根本来不及处理
3️⃣为什么是返回head?
因为函数类型是struct ListNode*,对应返回地址。而head就是头节点的地址
面试题二:反转链表
题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路1:创建一个空的新链表,遍历原链表,将原链表的节点依次头插进新链表。
由于此题思路并不复杂,所以这里没有选择作图。
struct ListNode* reverseList(struct ListNode* head){
struct ListNode* newHead = NULL;
struct ListNode* cur = head;
while (cur)
{
//先存一份next避免当前节点头插进新链表后无法继续遍历
struct ListNode* next = cur->next;
cur->next = newHead;
newHead = cur;
cur = next;
}
return newHead;
}
思路2:翻转指针方向
![](https://i-blog.csdnimg.cn/blog_migrate/70c6f871a75d70af99f486a09f37bf96.png)
逻辑过程:
![](https://i-blog.csdnimg.cn/blog_migrate/ddf0b8d35650c39bf349d049e1422cc9.png)
即:
![](https://i-blog.csdnimg.cn/blog_migrate/91c45068a15703884a57fb2e1fddfd16.png)
疑问1:为什么不是n3==NULL停止循环了???
![](https://i-blog.csdnimg.cn/blog_migrate/d4a8a33f75bd20614794318a8202d4ff.png)
疑问2:要是链表为空了
答:要是链表为空了,直接返回NULL
注:重复的过程一般是用循环解决
循环三要素:初始值,迭代过程,结束条件
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL)
return NULL;
//初始化
struct ListNode* n1 = NULL,*n2 = head,*n3 = n2->next;
//结束条件
while(n2)
{
//翻转
n2->next = n1;
//迭代过程(重复过程),即迭代往后走
n1 = n2;
n2 = n3;
if(n3)//最后一步时,n3为空
n3 = n3->next;
}
return n1;
}
思路3:头插法,取原链表中的节点,头插到newhead中
![](https://i-blog.csdnimg.cn/blog_migrate/97fff876a2c52d3d7ea92a78bd2c9c0a.png)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL)
{
return NULL;
}
struct ListNode *cur = head,*next = cur->next;
struct ListNode* newhead = NULL;
while(cur)
{
//头插
cur->next = newhead;
newhead = cur;
cur = next;
if(next)
next = next->next;
}
return newhead;
}
面试题三:链表的中间结点
题目描述:
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路1:
先遍历数组,得到链表节点数count
利用(count/2)+1得到再次遍历数组时,遍历的第几个对应中间节点
再次遍历链表,直到遍历至(count/2)+1
时间复杂度:O(N)
分析:先遍历链表,得到count,时间复杂度为O(N)。你再次遍历,得到中间节点,时间复杂度为O(N/2),总的时间复杂度为O(3N/2),去掉系数后,为O(N)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head)
{
//遍历链表,得到节点总数
struct ListNode* cur=head;
int count=0;
while(cur->next)
{
cur=cur->next;
count++;
}
//遍历得到的节点数会比实际数少1
count++;
//节点总数得到了,现在得到再一次需要遍历的个数
int times=(count/2)+1;
//重置cur的指向,让它从头开始
cur=head;
//接下来遍历
int i=0;
while(i<times-1)
{
cur=cur->next;
i++;
}
//此时得到的cur便是中间节点的地址
return cur;
}
思路2:快慢指针法
![](https://i-blog.csdnimg.cn/blog_migrate/327a289ea2f0d9ed5b553a654dd9090c.png)
大家从上面的思路中也可以看出它也是迭代的过程,初始条件是slow,fast都指向第一个结点,迭代过程,slow走一步,fast走两步,结束条件fast->next,fast只要有一个为空就结束。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head){
struct ListNode *slow = head,*fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
面试题四:链表中倒数第k个结点
题目描述:输入一个链表,输出该链表中倒数第k个结点。
思路:快慢指针法
fast先走k步
slow再和fast一起走,fast==NULL时,slow就是倒数第k个
![](https://i-blog.csdnimg.cn/blog_migrate/933a2aed9841df3ec2cced0939dd91d8.png)
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode* fast,*slow;
fast=slow=pListHead;
//while(--k)// k-1步
while(k--)//k步
{
//1.传过来的是空链表
//2.k>=结点数
if(fast==NULL)
{
return NULL;
}
fast=fast->next;
}
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
思路2:
先遍历数组,得到链表节点数count
利用count-k+1得到再次遍历数组时,遍历的第几个对应倒数第k个节点
再次遍历链表,直到遍历至count-k+1,对应倒数第k个节点的地址
不完善的写法:
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
/**
*
* @param pListHead ListNode类
* @param k int整型
* @return ListNode类
*/
#include <assert.h>
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
//遍历链表,得到节点总数
struct ListNode* cur=pListHead;
int count=0;
while(cur->next)
{
cur=cur->next;
count++;
}
//遍历得到的节点数会比实际数少1
count++;
assert(k<=count);//看解释1
//倒数第k个节点对应的times为count-k+1
int times=count-k+1;
//重置cur的指向,让它从头开始
cur=pListHead;
//接下来遍历
int i=0;
while(i<times-1)
{
cur=cur->next;
i++;
}
return cur;
}
运行结果:
![](https://i-blog.csdnimg.cn/blog_migrate/5c2cfb1064eca892fb3a6c00aa396e00.jpeg)
1️⃣提交后,我们会发现就是当k大于节点数时,我们这个代码就处理不了了。于是就想到用assert。但是:
注:OJ题不要用断言!!!😩
因为这样做是把问题抛给Leetcode,它才不会帮你解决
注:如果遇到运行失败时,可以在代码中加入printf()函数,在自测输入那里输入特例观察
2️⃣错误的原因是不完善,对k大于节点数已经链表为空的情况没有补充到解决方案
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
/**
*
* @param pListHead ListNode类
* @param k int整型
* @return ListNode类
*/
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
//遍历链表,得到节点总数
struct ListNode* cur=pListHead;
int count=0;
if(cur==NULL)//针对链表为空的情况
{
return NULL;
}
else
{
while(cur->next)
{
cur=cur->next;
count++;
}
//遍历得到的节点数会比实际数少1
count++;
if(k>count)//针对k>count,输出为空的情况
{
return NULL;
}
else
{
//倒数第k个节点对应的times为count-k+1
int times=count-k+1;
//重置cur的指向,让它从头开始
cur=pListHead;
//接下来遍历
int i=0;
while(i<times-1)
{
cur=cur->next;
i++;
}
return cur;
}
}
}
面试题五: 合并两个有序链表
题目描述:
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:类似于合并两个有序数组,创建两个指针分别指向两个链表,将两个指针指向的val分别进行比较,取小的尾插到另一个大的节点后面,并且创建一个尾指针来维护新链表的末尾
考虑链表为空的情况:可能链表一为空链表二不为空,也可能链表二为空链表一不为空,也可能两者都为空
以示例1为例分析:
第一步:先取一个较小的节点做新链表的第一个节点,方便后面尾插(前提是两指针不为空)
假设当cur2指向的元素大于cur1指向的元素时,将cur1所指元素尾插至新链表后,否则将cur2所指元素尾插至新链表后
cur1指向红色链表,cur2指向紫色链表,此时cur1指向的1等于cur2指向的1,所以将cur2指向的1作为头部将cur1指向的1尾插至cur2指向的1之后
同时让新链表的头部phead指向紫色链表的节点1,方便之后返回结果
![](https://i-blog.csdnimg.cn/blog_migrate/3c1bf1a4fbbef21aba7450e1619c8a62.png)
分离得到:
![](https://i-blog.csdnimg.cn/blog_migrate/d167e6d3d47a106b84fcfc3720108c1e.png)
要设置指针保存指向紫色链表3的位置,才能再尾插,也就是使 cur2 = cur2->next
我们发现实际上我们并没有创建新链表,而是将其中一个作为新链表
第二步:比较cur1指向1与cur2指向的3,1<3,所以将节点1尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点1
![](https://i-blog.csdnimg.cn/blog_migrate/502f17402fc75ec055a5f547c455f01b.png)
得到结果:
![](https://i-blog.csdnimg.cn/blog_migrate/a7784dbb612293478497ab51a1d5fde6.png)
第三步:比较cur1指向2与cur2指向的3,2<3,所以将节点2尾插至新链表的后面,并且使cu1 = cur1->next,tail此时也指向红色节点2
![](https://i-blog.csdnimg.cn/blog_migrate/040e897f41ff49310453002cf6b9e060.png)
得到结果:
![](https://i-blog.csdnimg.cn/blog_migrate/75d5d310238c32cc4addcea9cd086872.png)
第四步:比较cur1指向4与cur2指向的3,3<4,所以将节点3尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点3
![](https://i-blog.csdnimg.cn/blog_migrate/ace3db260abd08b2c9df328d3977e654.png)
得到结果:
![](https://i-blog.csdnimg.cn/blog_migrate/96f6b95f06a3f1ce119af75257cb6d24.png)
第五步:比较cur1指向4与cur2指向的4,cur2指向的4不大于cur1指向的4,所以将节点4尾插至新链表的后面,并且使cu2 = cur2->next,tail此时也指向紫色节点4
![](https://i-blog.csdnimg.cn/blog_migrate/6df2c86b07124ebbfb4746b10ad0e08b.png)
得到结果:
![](https://i-blog.csdnimg.cn/blog_migrate/82a469b123e505df18b9f7a9b45e9d23.png)
第六步:cur2已经指向空了,再将另一个链表剩下的节点插至新链表后即可,注意,这里只需要连接一次,因为圣血的节点本就是相连的
代码(只实现函数):
上述分析中的cur1,cur2在代码中是l1,l2
![](https://i-blog.csdnimg.cn/blog_migrate/d14dbeb1535bc4ce9b8b0e2635903b7f.png)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
if(list1==NULL)
{
return list2;
}
if(list2==NULL)
{
return list1;
}
struct ListNode *head = NULL,*tail = NULL;
while(list1 != NULL && list2 != NULL)
{
if(list1->val < list2->val)
{
if(tail == NULL)
{
head = tail = list1;
}
else
{
tail->next = list1;
tail = tail->next;
}
list1=list1->next;
}
else
{
if(tail == NULL)
{
head = tail = list2;
}
else
{
tail->next = list2;
tail=tail->next;
}
list2=list2->next;
}
}
//list1还没结束
if(list1)
{
tail->next = list1;
}
//list2还没结束
if(list2)
{
tail->next = list2;
}
return head;
}
还有一种哨兵解法,就是创建一个新节点,同样也是取小的尾插至新节点之后,省去了判断两个链表是否为空的情况,但返回前需要free掉该空间
![](https://i-blog.csdnimg.cn/blog_migrate/be8d414479cc56baeb83a66012d17f72.png)
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {
//创建哨兵
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
//维护新空间的尾部
struct ListNode* tail = guard;
while (l1 != NULL && l2 != NULL)
{
if (l2->val > l1->val)
{
tail->next = l1;
tail = tail->next;
l1 = l1->next;
}
else
{
tail->next = l2;
tail = tail->next;
l2 = l2->next;
}
}
//拷贝剩下的节点
if (l2 == NULL)
{
tail->next = l1;
}
else
{
tail->next = l2;
}
//释放哨兵空间
struct ListNode* tmp = guard->next;//记得是存放guard->next
free(guard);
guard = NULL;
return tmp;
}
面试题六:链表分割
题目描述:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:
创建两条链表,小于x的放入小链表,大于等于x的放入大链表,然后通过尾结点指针将两条链表合并起来。
1.判断极端条件,如果链表为空,既给定值也无法分割,直接返回空。
2.创建大链表bigger,存放所有大于等于x的结点。创建小链表samller,存放所有小于x的结点。但二者的指向都只是一个假结点是存放了一个无效值的头结点,从它们的next结点才开始存放旧链表中的值。因为这样可以更好的使尾指针进行运作。
3.因为需要搬运结点以及链接两链表,所以各自建立一个尾指针,作为搬运工,并初始化为各自头指针的指向。
4.创建当前结点cur,为循环判断条件,优先级最高,并初始化为链表首结点。
![](https://i-blog.csdnimg.cn/blog_migrate/b18618b180af9a9b2b3bddd38d4a7d02.png)
5.通过判断当前指针的有效性来决定是否继续执行循环逻辑。如果不为空,进行判定逻辑:
若当前结点值小于给定值,将它尾插入小链表,并更新小链表尾结点及当前指针的指向。
若当前结点值大于等于给定值,将它尾插入大链表,并更新大链表尾结点及当前指针的指向。
![](https://i-blog.csdnimg.cn/blog_migrate/8922fe75f7a2c93229bafa1c4a472613.png)
![](https://i-blog.csdnimg.cn/blog_migrate/acd33d9279e1f28824a87cd64d2e6969.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e5e9c0ebf16b26cc620ec44ce537935c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e268256ac7ac67a7398da2446e13c7cd.png)
6.最后执行链接逻辑,手动将小链表的尾指针smalltail的next指向大链表的bigger的next结点。将大链表的尾结点置为NULL,保证链表的有限性。
![](https://i-blog.csdnimg.cn/blog_migrate/bad17ba304010a8971c3cd861f0e408c.png)
7.最终忽略假结点,返回小链表的smaller的next结点即可。
struct ListNode* partition(ListNode* pHead, int x)
{
//搞两组链表,一组存储比x小的节点,一组存储比x大或等于的节点
struct ListNode* lessHead, *lessTail, *greaterHead, *greaterTail;
//开个哨兵位头节点,方便尾插
lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lessTail->next = NULL; //相当于lessTail->next=lessHead->next=NULL;
//同理:
greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
greaterTail->next = NULL;
//新建指针cur,让它去遍历链表
struct ListNode* cur = pHead;
//遍历链表
while (cur)
{
//存储比x小的的节点的尾插
if (cur->val < x)
{
lessTail->next = cur; //cur对应的节点接在lessTail后面
lessTail = cur; //lessTail往后挪1
}
//存储>=x的节点的尾插
else
{
//同理
greaterHead->next = cur;
greaterHead = cur;
}
//无论遇到的节点大于小于还是等于x,cur都要往后挪1
cur = cur->next;
}
//把两个新链表连接起来,小于x的链表的尾节点接到>=x的链表的头节点前
lessTail->next = greaterHead->next;
//此时greatertail还连着lessTail,如果不解决,就会陷入死循环
greaterTail->next=NULL;
//回收销毁两个新链表前,要把连起来的两个链表的头节点位置存储起来
struct ListNode* newHead = lessHead->next;
//回收链表
free(lessHead);
free(greaterHead);
//置不置空无所谓,反正也没有访问它
return newHead;
}
注:以后遇到代码运行失败,找不到原因。可以使用屏蔽法排除错误
面试题七:链表的回文结构
题目描述:
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
解决方案:
1、采用头插法将链表逆置,将整个链表反转过来,判断是否与原链表一样即可
2、升级版:既然它要是回文结构,我们只需要将链表的一半逆置即可,采用双指针遍历的方式,一个从前往后,一个从后往前,同时遍历并比较链表元素是否相等即可。
在这里我来说一下第二种方法的具体思路
(1) 第一步,先求出链表的总长度
(2) 第二步,找到中间节点
(3) 第三步,逆置链表后半部分节点
(4) 第四步,判断链表是否是回文结构
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
//得到中间节点地址的函数
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;//返回中间节点
}
//反转链表
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* newHead = NULL;
struct ListNode* cur = head;
while (cur) {
//先存一份next避免当前节点头插进新链表后无法继续遍历
struct ListNode* next = cur->next;
cur->next = newHead;
newHead = cur;
cur = next;
}
return newHead;
}
class PalindromeList {
public:
bool chkPalindrome(ListNode* A)
{
// write code here
struct ListNode* mid = middleNode(A); //得到中间节点
struct ListNode* rHead =reverseList(mid);//将mid之后的链表反转
//创建两个指针,分别指向链表两部分的头节点,为遍历做准备
struct ListNode*curA=A;
struct ListNode*curR=rHead;
//遍历
while(curA&&curR)//结束条件是curA或curR其中之一走到next为NULL
{
if(curA->val!=curR->val)
{
return false;
}
else
{
curA=curA->next;
curR=curR->next;
}
}
return true;
}
};
注:牛客可以在规定函数外写函数。
面试题八:相交链表
题目描述:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
![](https://i-blog.csdnimg.cn/blog_migrate/06df23f2608e2e49eb9d515440f6ff97.png)
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
思路1:暴力求解/穷举法(把所有可能都走一遍)
依次取A链表中的每个节点跟B链表中的所有节点比较
如果有地址相同的节点,则相交
时间复杂度:O()
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
//新建两个指针,用来分别遍历两个链表
struct ListNode* curA=headA;
struct ListNode* curB=headB;
//遍历前判断一下两链表是否为空:如果为空,肯定不相交。反正,可能相交,参与遍历
if(curA==NULL||curB==NULL)
{
return NULL;
}
//遍历
while(curA&&curB)
{
//A上的节点,按顺序依次分别比较B的所有节点
//如果curA=curB,则找到了。否则将curB往后挪,直到比较完所有的curB
while(curA!=curB)
{
curB=curB->next;
//如果curB从尾节点挪动到NULL,按照判断条件,还有继续参与循环,直到遇到解引用空指针警告,所有我们要剔除这一可能
if(curB==NULL)
{
goto flag;
}
}
flag:
//出了循环要么没找到,要么找到了
if(curA==curB)
{
//找到了
return curA;
}
//没找到,A往后挪1
curA=curA->next;
//如果不把curB的存储内容重新置换为headB的话,本次循环一结束,curB依旧存储的是NULL
curB=headB;
}
//没找到
return NULL;
}
思路2:
尾节点相同就相交,否则不相交
求交点:长的链表先走长度差步,再同时走,第一个相同就是交点
时间复杂度:O(N)
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* tailA, *tailB;
tailA = headA;
tailB = headB;
int lenA = 1; // 存放A链表的长度
int lenB = 1; // 存放B链表的长度
// A链表找尾节点及链表长度
while (tailA->next)
{
tailA = tailA->next;
++lenA;
}
// B链表找尾节点及链表长度
while (tailB->next)
{
tailB = tailB->next;
++lenB;
}
// 如果相交,它们的尾节点一定相等。如果不相等就返回NULL
if (tailA != tailB)
{
return NULL;
}
// 相交,求交点,长的先走"差距步", 然后一起走
struct ListNode* shortList = headA; // 假设A短
struct ListNode* longList = headB; // 假设B长
// 如果A的长度大于B
if (lenA > lenB)
{
// 那么就交换一下
longList = headA;
shortList = headB;
}
// 求出差距步
int gap = abs(lenA - lenB); // abs是求绝对值的函数
// 长的先走差距步
while (gap--)
{
longList = longList->next;
}
// 然后同时走
while (shortList && longList)
{
if (shortList == longList)
{
return shortList;
}
shortList = shortList->next;
longList = longList->next;
}
// 要加这个,不然会显示[编译出错]
return NULL;
}
面试题九:环形链表
题目描述:
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
面试题十:复制带随机指针的链表
题目描述:
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
示例 1:
![](https://i-blog.csdnimg.cn/blog_migrate/b57fe6314867b0534627e0902867e068.png)
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
![](https://i-blog.csdnimg.cn/blog_migrate/021baf3ed51ed4a767eb7b2fe52febe6.png)
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
![](https://i-blog.csdnimg.cn/blog_migrate/4eac9c02088302bc1e744d1a68882702.png)
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
提示:
0 <= n <= 1000
-104 <= Node.val <= 104
Node.random 为 null 或指向链表中的节点。
图片解释:
![](https://i-blog.csdnimg.cn/blog_migrate/bdc2135be16aafae17bd054ee60051ce.png)
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
struct Node* copyRandomList(struct Node* head)
{
//将copy插入每一个原节点的后边
struct Node* cur = head;
while(cur)
{
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
//拷贝原节点中random的指向位置
cur = head;
while(cur)
{
struct Node* copy = cur->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = copy->next;
}
//将copy从原节点上取出并连接 再将原节点进行连接
struct Node*copyhead = NULL,*copytail = NULL;
cur = head;
while(cur)
{
struct Node*copy = cur->next;
struct Node*next = copy->next;
if(copytail == NULL)
{
copyhead = copytail = copy;
}
else
{
copytail->next = copy;
copytail = copy;
}
cur->next = next;
cur = next;
}
return copyhead;
}
带头+双向+循环链表:
![](https://i-blog.csdnimg.cn/blog_migrate/4d6d2e74dbf2bffef5e8eb9139f038d8.png)
结构体创建:
![](https://i-blog.csdnimg.cn/blog_migrate/3d581aaeba6c087cc510ffd6c707e5ac.png)
typedef int LTDataType;
typedef struct ListNode
{
//值域
LTDataType data;
//前址域
struct ListNode* prev;
//后址域
struct ListNode* next;
}LTNode;
这里的节点成员包含三个:值域,前址域,后址域
初始化:
要想得到并带出动态开辟的链表空间地址有两个方案:
1.设计传入参数为链表指针的地址,只有传入指针的地址,才能修改指针的内容,即改成动态开辟的链表地址将之带出函数
2.设计返回类型为节点指针,返回动态开辟的链表节点指针,使之在函数外就能够接受到改地址
![](https://i-blog.csdnimg.cn/blog_migrate/f663d24df16e92626e0b94d987e8b8fe.png)
过程:
由于刚开始只有一个节点,也就是头结点,又要构成双向循环,所以头结点的prev和next都指向的是头结点自己
这里初始化没有传二级指针,而是采取了返回头结点的方式。所以需要先malloc申请一个头结点phead
然后将phead的prev和next都指向phead自己,初始化就大功告成了。
方法一:
void ListInint(LTNode** pphead)
{
*pphead = (LTNode*)malloc(sizeof(LTNode));
(*pphead)->next = (*pphead);
(*pphead)->prev = (*pphead);
}
void TestList1()
{
//头指针指向哨兵位
LTNode* plist ;
ListInit(&plist);
}
方法二:
LTNode* ListInit()
{
//哨兵位头节点
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->next = phead;
phead->prev = phead;
//返回哨兵位的地址
return phead;
}
void TestList1()
{
//头指针指向哨兵位
LTNode* plist = ListInit();//看解释1
}
1️⃣这种写法给我们打开了一个思路:
任何传二级指针的调用都可以写成这种地址接收的形式
创建新节点:
LTNode* BuyListNode(LTDataType x)
{
//开辟哨兵位头节点
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//初始化哨兵卫头结点值域和址域
newnode->prev = NULL;
newnode->next = NULL;
newnode->data = x;
return newnode;
}
尾插:
![](https://i-blog.csdnimg.cn/blog_migrate/6708816841a1903503dcd66bed0b5553.png)
尾插首先要找到尾节点,这里的尾节点很容易找到,就是头节点的prev指向的节点。如下:
![](https://i-blog.csdnimg.cn/blog_migrate/cbbcc70f8245fde32a2d3ce90b743c7f.gif)
这里的尾插也满足空表情况下进行尾插。所以该代码没问题
过程:
比起单链表的尾插,带头双向循环链表进行尾插就十分容易,因为它不用遍历寻找尾结点,头结点phead的prev指向的就是尾结点
先断言防止链表为初始化而为空链表
再使用BuyListNode函数申请一个新节点NewNode,通过头节点的prev找到尾结点
将尾结点tail的next链接上NewNode,再将NewNode的prev指向tail
此时NewNode为新的尾结点,将其next指向头结点phead,再将phead的prev指向新的尾结点NewNode,尾插就完成了
当然之后完成ListInsert以后就可以直接调用它完成尾插
void ListPushBack(ListNode* phead,LTDataType x)//看解释1
{
assert(phead);//看解释2
ListNode* newnode = BuyListNode(x);
ListNode* tail = phead->prev;//看解释3
tail->next = newnode;//新节点尾节点连接,看解释4-①
newnode->prev = tail;//尾节点跟新节点连接,看解释4-②
newnode->next = phead;
phead->prev = newnode;
}
1️⃣为什么这里phead传的不是二级指针?
因为该链表有哨兵位,插入是从哨兵位的后面开始的,而不是头指针。故不需要对头指针的内容做出修改,因而不需要传二级指针
2️⃣assert(phead)等价于assert(phead !=NULL),phead哨兵位绝不可能为空,因为你就算把链表删完了也不会删掉哨兵位,只剩哨兵位时,哨兵位会自己连自己
3️⃣图解:
![](https://i-blog.csdnimg.cn/blog_migrate/6bec501618cabd030c51850ea1d89407.png)
4️⃣图解:
![](https://i-blog.csdnimg.cn/blog_migrate/1968a3d4ff102bef9bbdd15c796a2492.png)
打印:
1.循环遍历链表打印数据
2.以哨兵卫头节点地址作为遍历结束标记
首先先断言链表不为空,为空就不用打印了
然后就是利用cur指针从头节点的下一个节点遍历链表(头结点不存储有效数据)节点打印数据域data,问题是终止条件是什么
单链表时在cur为为NULL时就到尾了,可是带头双向链表没有指向NULL的节点
其实也很简单,当cur再次为头结点phead时说明已经遍历链表一遍了,这就是终止条件
// 双向链表打印
void ListPrint(LTNode* phead)
{
//断言传入指针不为NULL
assert(phead);
//创建寻址指针
LTNode* cur = phead->next;
//循环遍历链表
while (cur != phead)
{
//打印数据
printf("%d->", cur->data);
//找到下一个节点
cur = cur->next;
}
return;
}
尾删:
经典的错误:
void ListPopBack(LTNode* phead)
{
LTNode* tail = phead->prev;
free(phead->prev);//看解释1
tail->prev->next = phead;//看解释1
phead->prev=tail->prev;
}
1️⃣phead->prev和tail指向的是同一块地址,用free把phead->prev释放以后,你再用tail->就是访问野指针了
改进的写法:
void ListPopBack(LTNode* phead)
{
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
tailPrev->next = phead;
phead->prev=tailPrev;
}
这种写法依然有问题,因为这种尾删可以删掉哨兵位,这是非常危险的
正确的写法:
1.考虑节点只剩哨兵卫的情况
2.尾删前记录前一节点的地址
3.注意构建节点关系的逻辑
![](https://i-blog.csdnimg.cn/blog_migrate/3302b4e05e78cae803130f946a9ecd47.gif)
过程:
进行尾删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
之后通过phead的prev找到尾结点tail,创建变量tailprev指向tail的上一个节点
因为tail节点要被释放,所以tailprev是尾删后链表的新尾节点
将tailprev的next指向头结点phead,phead的prev指向tailprev
最后释放tail节点,就完成了尾删
完成ListErase后可以直接调用完成尾删
// 双向链表尾删
void ListPopBack(LTNode* phead)
{
//断言传入指针不为NULL
assert(phead);
//只剩哨兵卫头结点的情况
if (phead->next == phead)//看解释1
return;
//记录尾节点及其前一节点
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
//释放尾节点
free(tail);
//构建尾节点前一节点与哨兵卫头结点的关系
tailprev->next = phead;
phead->prev = tailprev;
return;
}
1️⃣phead->next==phead表示链表为空,不能再删了,这里写成暴力的形式assert(phead->next!=phead)更方便
那我要是不用tailPrev,怎么办?
void ListPopBack(LTNode* phead)
{
//断言传入指针不为NULL
assert(phead);
//只剩哨兵卫头结点的情况
assert(phead->next!=phead);
//记录尾节点及其前一节点
LTNode* tail = phead->prev;
//构建尾节点与哨兵卫头结点的关系
phead->prev=tail->prev;
tail->prev->next=phead;
//释放尾节点
free(tail);
return;
}
头插:
头插的实现在这里也很简单,先找到有效节点的头节点(即PHead的next指向的第一个节点),然后将新节点的next指向该节点,该节点的prev指向新节点,再让PHead的next指向新节点,新节点的prev指向PHead即可。(看起来可能有些乱,但是画图就特别容易理解)
![](https://i-blog.csdnimg.cn/blog_migrate/ee109e0d06d664ee283bb1c74be790e3.png)
1.头插前记录哨兵卫头节点的下一节点
2.注意构建节点关系的逻辑
过程:
这里说是头插但并不是在头结点phead前面插入,无论如何phead都是头节点,它就是个哨兵,起的站岗作用,所以头插其实是在头结点phead后面进行插入
创建变量next指向第二个节点就是头结点的下一个节点,之后在next前面进行插入新节点
之后的工作就是将phead的next指向NewNode,NewNode的prev指向phead
NewNode的next指向next,next的prev指向NewNode完成双向链接
完成ListInsert以后就可以直接调用它完成头插
void ListPushFront(LTNode* phead, LTDataType x)
{
//断言传入指针不为NULL
assert(phead);
//创建节点
LTNode* newnode = BuyListNode(x);
//记录哨兵卫头结点的下一节点
LTNode* next = phead->next;
//构建各节点之间的关系
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
头删
头删的实现与尾删异曲同工,找到有效节点的头节点,保存下一个节点,将PHead与之连接,然后再释放有效节点的头节点即可。
![](https://i-blog.csdnimg.cn/blog_migrate/cb83c0a81b51c6bb74372c687097d8fc.png)
这里依然要注意,空表情况不能进行删除
1.考虑只剩哨兵卫头结点的情况
2.头删前保存节点地址
3.注意节点关系的构建
过程:
进行头删时我们不仅要断言链表不为空,并且要断言链表不能只有头节点,因为我们不希望将头结点也删除
并且头删删除的也并不是头节点,是头节点的下一个节点
之后通过phead的next找到要删除的结点next,创建变量Nextnext指向next的下一个节点
然后将Nextnext和头节点phead进行链接,具体实现不赘述了
最后释放next节点,就完成了头删
完成ListErase后可以直接调用完成头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
//记录哨兵卫头结点下一节点及其的下一节点
LTNode* next = phead->next;
LTNode* nextNext = next->next;
//构建关系以及释放节点
phead->next = nextNext;
nextNext->prev = phead;
free(next);
}
查找
1.循环遍历链表,以哨兵卫头节点地址作为结束遍历
2.没找到则返回NULL,找到就返回其地址
过程:
这个函数很少单独使用,更多的是为了辅助ListInsert,ListErase这两个函数,通过ListFind找到要插入删除的节点,然后将其地址传给ListInsert,ListErase进行插入或删除
实现也很简单,使用cur指针遍历链表,对比每一个节点data的值和x的值,x代表使用者想要查找的值
如果相等就返回节点地址,如果找不到就返回NULL
通过ListFind还可以实现链表元素的修改
LTNode* ListFind(LTNode* phead, LTDataType x)
{
//断言传入指针不为NULL
assert(phead);
//创建寻址指针
LTNode* cur = phead->next;
while (cur != phead)
{
//比较数据
if (cur->data == x)
return cur;
//找到下一个节点
cur = cur->next;
}
//没找到则返回NULL
return NULL;
}
链表pos位置前插
这里与头插的操作相比,两者其实也没啥区别。头插是找有效节点的头节点,在这里我们把pos看作该节点,把pos的prev指向的节点看作是PHead节点,这样的话,原理就与头插相同了。
![](https://i-blog.csdnimg.cn/blog_migrate/0b33a8515a63fd544bf2942f0328ad7f.png)
1.前插前保存前一节点地址
2.注意构建节点关系的逻辑
过程:
pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
之后创建变量posprev指向pos的上一个节点,申请新节点将其prev指向posprev,将其next指向pos
posprev的next指向新节点NewNode,pos的prev指向NewNode
这样pos位置之前的插入就完成了,并且可以替代头插尾插
// 双向链表在pos的前面进行插入
void ListInsert(LTNode* pos, LTDataType x)
{
//断言传入指针不为NULL
assert(pos);
//创建新节点
LTNode* newnode = BuyListNode(x);
//记录pos节点的前一节点
LTNode* posPrev = pos->prev;
//构建节点之间的关系
//紫色区:
posPrev->next = newnode;
newnode->prev = posPrev;
//橙色区:
newnode->next = pos;
pos->prev = newnode;
return;
}
改进尾插--复用插入
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->prev,x);
}
改进头插--复用插入
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next,x);
}
pos删除:
1.删除前保存前节点和后节点地址
2.注意节点关系的构建
过程:
pos参数一般是基于ListFind查找而来,故这两个函数经常搭配使用
之后创建变量posprev指向pos的上一个节点,创建变量posnext指向pos的下一个节点
将posprev与posnext链接,释放pos,就完成pos位置的删除了,并且可以替代掉尾删头删
// 双向链表删除pos位置的节点
void ListErase(LTNode* pos)
{
//断言传入指针不为NULL
assert(pos);
//记录pos的前一节点和后一节点
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
//释放节点并构建关系
free(pos);
pos=NULL;
posPrev->next = posNext;
posNext->prev = posPrev;
return;
}
void TestList1()
{
//头指针指向哨兵位
LTNode* plist = ListInit();
//尾插
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
LTNode* pos=ListFind(plist,2);
if(pos)
{
ListErase(pos);
}
ListPrintf(plist);
}
改进尾删--复用删除
void ListPopBack(LTNode* phead)
{
//断言传入指针不为NULL
assert(phead);
//只剩哨兵卫头结点的情况
assert(phead->next!=phead);
ListErase(phead->prev);
}
改进头删--复用删除
void ListPopFront(LTNode* phead)
{
//断言传入指针不为NULL
assert(phead);
//只剩哨兵卫头结点的情况
assert(phead->next!=phead);
ListErase(phead->next);
}
销毁:
过程:
最后是销毁链表,这里有个很容易犯的错误就是直接free(phead),这样只是释放了头节点,链表的其它节点并未释放,会造成内存泄露。
所以我们需要通过cur遍历链表所有节点一个一个进行释放,并且防止释放了cur找不到cur的下一个节点,需要先创建临时变量进行保存
最后将phead置为空指针代表链表为空,即销毁完成。
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur=phead->next;
while(cur !=phead)
{
LTNode* next=cur->next;
free(cur);
cur=next;
}
free(phead);
phead=NULL;
}
void TestList1()
{
//头指针指向哨兵位
LTNode* plist = ListInit();
//尾插
ListPushBack(plist, 1);
ListPushBack(plist, 2);
ListPushBack(plist, 3);
ListPushBack(plist, 4);
ListPrint(plist);
ListDestroy(plist);
plist=NULL;
}
总结
该链表用起来真的很爽,能直接找到头尾节点,并且因为有哨兵位的存在,就不需要考虑是否为空表的情况下的插入,就不用改变phead,所以就不用像之前的单链表一样,得传二级指针。真的是链表中的完美存在,不过在进行删除操作时,一定要考虑空表情况下不可进行删除。因此要加个assert进行断言。
顺序表和链表的区别
![](https://i-blog.csdnimg.cn/blog_migrate/a724b42caccb0629e27a6b7494f5d7bd.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c95e51379c87bd0b2e0d556ede15c120.png)
![](https://i-blog.csdnimg.cn/blog_migrate/7432a9eac82f9fbde54ffa0f319fd5de.png)
假设写了一个程序,实现分别对顺序表和链表上的每个数据+1,那么这个程序会编译成指令,然后CPU再执行
CPU运算的速度很快,那内存的速度跟不上,CPU一般就不会直接访问内存,而是把要访问的数据先加载到缓存体系,如果是 ≤ 8byte的数据 (寄存器一般是8byte),会直接到寄存器;而大的数据,会到三级缓存,CPU直接跟缓存交互。
CPU执行指令运算要访问内存,先要取0x10 (假设) 的数据,拿0x10去缓存中找,发现没有 (不命中) ,这时会把主存中这个地址 开始的一段空间都读进来 (缓存)。
假设不命中,一次加载20byte空间到缓存 (具体加载多大byte,取决于硬件体系)
如果是整型数组,下一次访问0x14、0x18… 就会命中
如果是链表,先取第1个节点0x60,不命中;再取第2个0x90,不命中…。会不断把周围的空间加载到缓存,这样会造成一定的缓存污染