前言
参考用书:王道考研《2024年 数据结构考研复习指导》
配套视频:2.1_线性表的定义和基本操作_哔哩哔哩_bilibili
参考博文:通俗易懂讲解 链表 - 知乎
这是一篇孤独而又灿烂的博文(→_→),趁没人看偷偷更新一下~
考研笔记整理:内容包含线性顺序表、单链表、双链表、循环链表、静态链表的基本定义与代码~
考研一起加油~ψ(._. )>!
目前为第2版内容:
第1版:查资料、测试代码、增加注释~
第2版:合并博文线性链表的内容,调整措辞与排版,删减歧义内容,增加思维导图~
截图来源:《孤单而又灿烂的神-鬼怪》
线性表的定义和基本操作
线性表的定义
具有相同数据类型n(n≥0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。
线性表的特点
- 表中元素的个数有限;
- 表中元素具有逻辑上的顺序性,表中元素有其先后顺序;//元素是具有前后驱关系序列;
- 表中元素都是数据元素,每个元素都是单个元素;
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间;//可以是基本数据类型,也可以是其他类型C支持的数据类型:C 数据类型 | 菜鸟教程 (runoob.com)
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
线性表的基本操作
基本可以分为:(1)创建与销毁、(2)查看表内容、(3)查找表元素、(4)插入与删除 这四类操作~
- (1-1)Initialist(&L):初始化表。构建一个空的线性表;
- (1-2)DestoryList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
- (2-1)Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
- (2-2)Empty(L):判空操作。若L为空表,则返回true,否则返回false。
- (2-3)PrintList(L):输出操作。按前后顺序输出线性表L中的所有元素值。
- (3-1)LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字的元素。
- (3-2)GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
- (4-1)ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
- (4-2)ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
注意:
- 符号“&”表示C++语言中的引用调用,在C语言中采用指针也可达到同样的效果。
- 基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同。
线性表的顺序表示
顺序表的定义
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
截图来源:线性表的顺序储存结构(向量 )(一)Coco的博客-CSDN博客
关于顺序表的备注说明:
- 起始位置为LOC(A),线性表元素的起始位序为1,而数组元素的起始下标为0;
- 每个元素所占用存储空间的大小为Sizeof(ElemType)。
顺序表的特点
- 顺序表最主要的特点是随机访问,即通过首地址和元素序号可在时间O(1)内找到指定的元素;
- 顺序表上的存储密度高,每个结点只存储数据元素;
- 顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素;
- 顺序表拓展容量不方便。
关于随机访问的备注说明:
- 每个数据元素的存储位置都和线性表的起始位置相差一个和该数据元素的位序成正比的常数,因此,顺序表中的任意一个数据元素都可以随机存取。
关于容量拓展的备注说明:
- 一维数组可以是静态分配的,也可以是动态分配的~
- 静态分配,一旦表空间占满,不能拓展容量,整个表没救了~想要避免这个问题,可以在刚开始存储空间要得十分充足,但空闲的存储空间没办法回收,会浪费内存空间~
- 动态分配,一旦表空间占满,可以拓展容量,但不是很方便~首先在内存中开辟更大的连续存储空间(有时一大片儿的存储空间并不是很好找),然后迁移数据~
顺序表的基本操作
(1)初始化 Initialist(&L)
(1-1)静态分配
以下是静态分配实现初始化的关键语句~
#define Maxsize 10 //定义线性表的最大长度,在内存中占用的连续空间大小为:Maxsize*sizeof(Elemtype)
typedef struct{
ElemType data[Maxsize]; //顺序表的元素,此处假设元素类型名称为Elemtype(可替换为需要的类型,例如int),并以静态数组的方式实现此表,最多存放的数据个数为Maxsize
int length; //顺序表的当前长度
}SqList; //以上为顺序表的类型定义,并用SqList命名
以下是静态分配实现的步骤~
注意:“&”的引用调用功能可以在C++中实现,在C中会报错的~
#include <stdio.h>
#define Maxsize 10 //定义最大长度为10
typedef struct{
int data[Maxsize]; //使用静态的“数组”存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
void InitList(SqList &L){ //初始化顺序表
for(int i=0; i<Maxsize; i++)
L.data[i]=0; //将所有元素设置为默认初始值
L.length=0; //顺序表初始长度为0
}
int main(){
SqList L; //声明一个顺序表
InitList(L);//初始化顺序表
return 0;
}
整个程序中其实分为两个部分:
- typedef struct{}以数组的方式定义顺序表的长度、数据类型、初始长度、序列名称;
- void InitList(SqList &L)为数组所有的元素赋初始值,非必要语句,但可以避免内存已经存在的脏数据污染序列~
(1-2)动态分配
以下是动态分配实现初始化的关键语句~
#define Initsize 10 //定义线性表的初始长度,在内存中占用的连续空间大小为:Initsize*sizeof(Elemtype)
typedef struct{
ElemType data[Maxsize]; //顺序表的元素,此处假设元素类型名称为Elemtype(可替换为需要的类型,例如int),并以静态数组的方式实现此表,最多存放的数据个数为Maxsize
int length; //顺序表的当前长度
}SeqList; //以上为顺序表的类型定义,并用SeqList命名
以下是C实现动态申请内存空间的关键语句~
- malloc():在内存空间中开辟一整片存储空间,并返回存储空间的初始地址的指针data;malloc申请的空间大小=每个数据元素占用的存储长度sizeof(Elemtype)x顺序表初始容纳的数据元素数量(InitSize)~
- (Elemtype*):将返回的指针data的数据类型 强行转为 定义的数据类型(可替换为Int等)~
L.data=(Elemtype*)malloc(sizeof(Elemtype)*InitSize);
以下是C++实现动态申请内存空间的关键语句~
- c++ new 作用相当于c# malloc,申请内存空间~
- c++ delete 作用相当于c# free,释放内存空间~
L.data=new Elemtype[InitSize];
综上,以下动态分配实现的步骤~
#include <stdlib.h> //malloc、free的函数头文件
#define Initsize 10 //定义初始长度(首次设置的最大长度)为10
typedef struct{
int *data; //使用动态分配数组的指针
int Maxsize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义
void InitList(SeqList &L){
//用malloc函数申请一片连续的函数空间,将空间初始位置指针变为int类型并赋给了*L.data
L.data=(int*)malloc(Initsize*sizeof(int));
L.length=0; //顺序表的当前长度为0
L.Maxsize=Initsize; //顺序表的当前最大容量为10
}
void IncreaseSize(SeqList &L,int len){
//增加动态数组的长度
int *p=L.data; //将data的值赋予P,即*P现在指向原内存空间的头地址
L.data=(int*)malloc((L.Maxsize+len)*sizeof(int));
for(int i=0; i<L.length; i++){
L.data[i]=p[i]; //将数据复制到新区域
}
L.Maxsize=L.Maxsize+len; //顺序表的最大长度增加len(5)
free(p); //释放原来的内存空间
}
int main(){
SeqList L; //声明顺序表
InitList(L); //初始化顺序表
IncreaseSize(L,5); //在顺序表中增加5个元素的空间
return 0;
}
(2)按位查找操作GetElem(L,i) 与 按值查找操作LocateElem(L,e)
(2-1)按位查找操作GetElem(L,i)
以下是静态分配按位查找的关键代码,在实际运用中需要增加"i"的输入越界判断以保证程序的健壮性~
typedef struct{
ElemType data[Maxsize]; //使用静态的“数组”存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
ElemType GetElem(SqList L, int i) {
if (i < 1 || i > L.length) {
// 处理越界情况,例如返回一个特定的错误值或抛出异常
// 这里假设返回一个默认的错误值 -1
return -1;
}
return L.data[i - 1];
}
以下是动态分配按位查找的关键代码~
#define Initsize 10 //定义初始长度(首次设置的最大长度)为10
typedef struct{
ElemType *data; //使用动态分配数组的指针
int Maxsize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义
ElemType GetElem(SeqList L, int i) {
if (i < 1 || i > L.length) {
// 处理越界情况,例如返回一个特定的错误值或抛出异常
// 这里假设返回一个默认的错误值 -1
return -1;
}
return L.data[i - 1];
}
注意:数据类型会影响数据的字节,此处的指针类型Elemtype需要与所查询数据类型保持一致,才能保证在内存中所读取的字节符合要求,可参考C 数据类型 | 菜鸟教程 (runoob.com)~
按位查找的最差与平均时间复杂度为O(1),可以立即找到数据,不用执行循环语句~
(2-2)按值查找操作LocateElem(L,e)
以下是动态分配按值查找的关键代码~
#define Initsize 10 //定义初始长度(首次设置的最大长度)为10
typedef struct{
ElemType *data; //使用动态分配数组的指针
int Maxsize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找失败
}
上述代码需要注意:
- 基本数据类型:int、char、double、float等都可以替换Elemtype直接用运算符“==”比较~
- 但是两个完全一样的结构体不能直接比较——"struct a == struct b"这种不能通过编译;如果想比较,可以逐个比较结构体中的分量,例如 “a.data1 == b.data1 && a.data2 == b.data2”~
按值查找的最差与平均时间复杂度为O(n),取决于最深层的循环L.data[i]==e执行次数,即找到的元素在最末的位置(n)或中间的位置((n+1)/2)~
(3)插入ListInsert(&L,i,e) 与删除ListDelete(&L,i,&e)
(3-1)插入ListInsert(&L,i,e)
注意:(1)以下程序同样在C++中实现~(2)以静态分配举例~
#include <stdio.h>
#define Maxsize 10 //定义最大长度为10
typedef struct{
int data[Maxsize]; //使用静态的“数组”存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
void InitList(SqList &L){ //初始化顺序表
for(int i=0; i<Maxsize; i++)
L.data[i]=0; //将所有元素设置为默认初始值
L.length=0; //顺序表初始长度为0
}
bool ListInsert(SqList &L,int i,int e){
if(i<1||i>L.length+1) //判断i的取值范围:i<1或i>现有表长+1,都不是合法输入
return false;
if(L.length>=Maxsize) //判断现有表长是否超过储存空间,满足条件则不能插入
return false;
for(int j=L.length; j>=i; j++)
L.data[j]=L.data[j-1]; //将第i=3个位置之后的数据后移一位
L.data[i-1]=e; //将第i=3个位置(data[2])赋予数据e=3
L.length++; //顺序表长度+1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
ListInsert(L,3,3); //在表L的第i=3个位置上插入数据e=3
return 0;
}
整个程序分为三个部分:
- typedef struct{}以数组的方式定义顺序表的长度、数据类型、初始长度、序列名称;
- void InitList(SqList &L)为数组所有的元素赋初始值,非必要语句,但可以避免内存已经存在的脏数据污染序列~
- Bool ListInsert()实现数据的插入功能,目标位序i及其后数据顺序后移(元素循环从后向前移动),在位置i中放入e;且在插入以前会判断输入数据是否合法,是否满足线性表的定义(逻辑相邻且物理相邻),且表中是否有可插入的空位~
插入操作的最差与平均时间复杂度为O(n),取决于最深层的循环L.data[j]=L.data[j-1]执行次数,即原有的n个元素(最差)或n/2个元素(平均)全部后移~
(3-2)删除ListDelete(&L,i,&e)
#include <stdio.h>
#define Maxsize 10 //定义最大长度为10
typedef struct{
int data[Maxsize]; //使用静态的“数组”存放数据
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
void InitList(SqList &L){ //初始化顺序表
for(int i=0; i<Maxsize; i++)
L.data[i]=0; //将所有元素设置为默认初始值
L.length=0; //顺序表初始长度为0
}
bool ListDelete(SqList &L,int i,int &e){
if(i<1||i>L.length) //判断i的取值范围:i<1或i>现有表长,都不是合法输入
return false;
e=L.data[i-1]; //将被删除的元素第i=3个位置(data[2])赋予数据e
for(int j=i.length; j<L; j++)
L.data[j-1]=L.data[j]; //将第i=3个位置之后的数据前移一位
L.length--; //顺序表长度-1
return true;
}
int main(){
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//省略代码:表中插入元素
int e=-1; //引用型参数,返回被删除的值
if(ListDelete(L,3,e)) //删除表L的第i=3个位置的元素,并返回该元素值
print("已删除第3个元素,删除元素值为=%d\n",e);
else
print("位序不合法,删除失败\n");
return 0;
}
整个程序分为四个部分:
- typedef struct{}以数组的方式定义顺序表的长度、数据类型、初始长度、序列名称;
- void InitList(SqList &L)为数组所有的元素赋初始值,非必要语句,但可以避免内存已经存在的脏数据污染序列~
- Bool ListDelete()实现数据的删除功能,将位置i的数据放入e,目标位序i后的数据顺序前移(元素循环从前向后移动);且在插入以前会判断输入数据是否合法,是否满足线性表的定义(逻辑相邻且物理相邻)~
- ListDelete(&L,i,&e)记得增加引用符号'&',表示将值赋给了本代码中的e,而非内存中其他的同名变量e~
删除操作的最差与平均时间复杂度为O(n),取决于最深层的循环L.data[j-1]=L.data[j]执行次数,即原有的n个元素(最差)或n/2个元素(平均)全部前移~
单链表
单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表节点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。
单链表的特点
(1)解决顺序表需要大量连续存储单元的缺点,但单链表附加指针域,也存在浪费空间的缺点;
(2)由于单链表的元素离散地分布在存储单元中,所以单链表是非随机存取地存储结构,即不能直接找到表中某个特定的节点。查找某节点时,需要从表头开始遍历,依次查找。
- 可能因为单链表不是连续存储,所以按位查找不太方便的意思~
(3)通常可以用头指针来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。此外,为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头节点。
- 带头结点单链表判定为空:head→next==null ;
- 带头结点循环单链表判定为空:head→next==head ;
- 无头节点单链表判定为空:head==null 。
单链表的基本操作
(1)初始化 InitList(&L)
(1-1)单链表结点类型描述
1 前4行定义结点结构:数据域+指针域,后2行typedef是为简化变量名称,增加程序的可读性~
struct LNode{ //定义单链表中结点的类型
ElemType data; //数据域:每个结点存放一个数据元素
struct LNode *next; //指针域:指针指向下一个结点
};
typedef struct LNode LNode; //typedef类型定义,将结点命名为LNode
typedef struct LNode *Linklist; //typedef类型定义,指针命名为*LinkList
等价于下面的代码~
typedef struct LNode{ //定义单链表中结点的类型
ElemType data; //数据域:每个结点存放一个数据元素
struct LNode *next; //指针域:指针指向下一个结点
}LNode,*LinkList; //typedef类型定义,将结点命名为LNode 指针命名为*LinkList
备注:在此,LNode *L 或者 LinkList L 皆可表示创建指向单链表头节点的指针,作用相同;前者强调为结点,后者强调为链表~
2 内存申请空间,增加新结点的操作~
#include <stdlib.h>
LNode *p = (LNode *)malloc(sizeof(LNode));
- malloc():在头文件<stdlib.h> 中,用途为在内存空间中开辟一整片存储空间,并返回存储空间的初始地址的指针LNode *p;malloc申请的空间大小=单个结点占用的存储长度sizeof(LNode)~
- (LNode *):将返回的指针data的数据类型 强行转为 定义的数据类型~
(1-2)初始化无头节点的单链表
基础思路:建立一个没有数据元素的单链表,清空内容,下同~
#include <stdio.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = NULL; //空表,暂时没有结点,清空脏数据;需要头文件<stdio.h>
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针(不创建结点)
InitList(L); //初始化一个空表
}
(1-3)初始化带头节点的单链表
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配头节点;需要头文件<stdlib.h>
if(L==NULL) //内存不足,分配失败;需要头文件<stdio.h>
return false;
L->next = NULL; //空表,仅有头节点,暂时没有数据结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针(不创建结点)
InitList(L); //初始化一个空表,带头节点
}
(2)插入ListInsert(&L,i,e) 与删除ListDelete(&L,i,&e)
(2-1)插入ListInsert(&L,i,e)
(2-1-1)带头节点指针,按位序插入
基础思路:从头遍历到所需位置,然后执行插入操作,需要修改插入结点的前、后2条链子~
截图来源: 通俗易懂讲解 链表 - 知乎 (zhihu.com)
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配头节点;需要头文件<stdlib.h>
if(L==NULL) //内存不足,分配失败;需要头文件<stdio.h>
return false;
L->next = NULL; //空表,仅有头节点,暂时没有数据结点
return true;
}
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1) //输入的位置如果<1,为非法输入
return false;
LNode *p; //创建指针p,任务为寻找插入的位置
int j=0;
p = L; //首先将p的位置定到为头节点L的位置
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请空间,创建结点s
s->data = e; //将e存到空间内,结点为s
s->next = p->next; //结点s的next指针与结点p的next指针指向同一个位置(也就是i+1的位置)
p->next = s; //结点p的next指针指向s
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针(不创建结点)
InitList(L); //初始化一个空表(带头节点)
ListInsert(L,i,e); //在第i个位置插入元素e(带头节点)
}
嗯,讲起来实现的功能如下图,把e插入到指定的位置i(图示为1)的位置中~
截图来源:王道考研配套课件2.3.2
注意:
- 插入操作,首先需要s指向下一个数据(图中a1),其次p指向s(同时断开与a1的指针),如上图~
- 如果颠倒顺序,先让p指向s(同时断开a1的指针),那么e就会因为找不到内存中a1的位置而傻傻指向自己,如下图~
截图来源:王道考研配套课件2.3.2
最坏时间复杂度/平均时间复杂度O(n),例如,插入数据在表尾,遍历到最后一个结点的复杂度为O(n),插入本身的复杂度为O(1)~
(2-1-2)无头节点指针,按位序插入
基础思路:相比带头节点类似,差异是需要对第1个结点的操作进行特殊处理(因为找不到第1个数据元素的前向结点~)
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//
bool InitList(LinkList &L){
L = NULL; //空表,暂时没有结点,清空脏数据;需要头文件<stdio.h>
return true;
}
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1) //输入的位置如果<1,为非法输入
return false;
if(i==1){ //输入的位置如果=1,因为找不到前向结点需要特殊处理
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请空间,创建结点s
s->data = e; //将e存到空间内,结点为s
s->next = L; //结点s的next指针与原头结点L的指针指向同一个位置(也就是原表的第1个数据的位置)
L = s; //将头节点L指向结点s
return true;
}
LNode *p; //创建指针p,任务为寻找插入的位置
int j=0;
p = L; //首先将p的位置定到为头节点L的位置
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请空间,创建结点s
s->data = e; //将e存到空间内,结点为s
s->next = p->next; //结点s的next指针与结点p的next指针指向同一个位置(也就是i+1的位置)
p->next = s; //结点p的next指针指向s
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针(不创建结点)
InitList(L); //初始化一个空表(无头节点)
ListInsert(L,i,e); //在第i个位置插入元素e(无头节点)
}
对于首个结点的处理比较麻烦,因此之后的链表,若无特殊说明,均视为带头节点指针的链表~
(2-1-3)指定结点后插
#include <stdio.h>
#include <stdlib.h>
bool InsertNextNode(LNode *p,ElemType e){
if(p==NULL) //p值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请空间,创建结点s
if(s==NULL) //内存分配失败
return false;
s->data = e; //将数据e存到结点s中
s->next = p->next; //结点s的next指针与结点p的next指针指向同一个位置(也就是i+1的位置)
p->next = s; //结点p的next指针指向s
return true;
}
此处与按位操作的步骤有重复:按位操作可以在找寻到结点后,直接执行封装的后插操作“return InsertNextNode(p,e);”~
最坏时间复杂度/平均时间复杂度O(1)
(2-1-4)指定结点前插
基础思路:先执行结点p后插结点s的操作,然后互换p、s结点的数据(具体来说,就是把p的数据搬运到p的后向结点s,然后把e的输入到s的前向结点p)这么一个暗度陈仓的大动作~
#include <stdio.h>
#include <stdlib.h>
bool InsertPriorNode(LNode *p,ElemType e){
if(p==NULL) //p值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请空间,创建结点s
if(s==NULL) //内存分配失败
return false;
s->next = p->next; //结点s的next指针与结点p的next指针指向同一个位置(也就是i+1的位置)
p->next = s; //结点p的next指针指向结点s
s->data = p->data; //结点p的data搬运到结点s
p->data = e; //将数据e存到结点p中
return true;
}
最坏时间复杂度/平均时间复杂度O(1)
(2-2)删除ListDelete(&L,i,&e)
(2-2-1)按位序删除
基础思路:先把p的前向结点指针指向p的后向结点,然后删除p~
截图来源: 通俗易懂讲解 链表 - 知乎 (zhihu.com)
#include <stdio.h>
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1) //输入的位置如果<1,为非法输入
return false;
LNode *p; //创建指针p,任务为寻找插入的位置
int j=0;
p = L; //首先将p的位置定到为头节点L的位置
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) //i值不合法
return false;
LNode *q=p->next; //令q指向被删除结点
e = q->data; //用e返回被删除元素的值
p->next = q->next; //将*q结点从链中“断开”
free(q); //释放结点存储空间
return true;
}
最坏时间复杂度/平均时间复杂度O(n),例如插入数据在表尾,遍历到最后一个结点的复杂度为O(n),插入本身的复杂度为O(1)。
(2-2-2)删除指定结点
基础思路:执行结点p后插结点q的操作,交换p、q数据,p指向q的下一个结点,最后删掉结点q,又是这么一个暗度陈仓的大动作~
#include <stdio.h>
bool DeleteNote(LNode *p){
if(p==NULL) //p值不合法
return false;
LNode *q=p->next; //令*q指向*p的后向结点
p->data = p->next->data; //和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
free(q); //释放后继结点存储空间
return true;
}
但是这个思路并不能解决删除最后一个结点的问题(q会指向null,出现空指针的错误),最保险的办法还是需要从头开始遍历q的上一个指针~
(3)按位查找操作GetElem(L,i) 与 按值查找操作LocateElem(L,e)
(3-1)按位查找操作GetElem(L,i)
基础思路:从头遍历到需要查找的位置~
#include <stdio.h>
bool GetElem(LinkList L,int i){
if(i<1) //输入的位置如果<1,为非法输入
return false;
LNode *p; //创建指针p,任务为寻找插入的位置
int j=0;
p = L; //首先将p的位置定到为头节点L的位置
while(p!=NULL && j<i){ //循环找到第i个结点
p=p->next;
j++;
}
return p;
}
这段代码也可以封装起来,在插入的操作中直接调用 "LNode *p = GetElem(L,i-1);"~
最坏时间复杂度/平均时间复杂度O(n)
(3-2)按值查找操作LocateElem(L,e)
基础思路:从头遍历到需要查找的位置~
#include <stdio.h>
bool LocateElem(LinkList L,ElemType e){
LNode *p = L->next; //创建指针p,任务为寻找插入的位置,起始位置在第1个结点后
while(p!=NULL && p->data !=e) //循环找到数据域为e的结点
p=p->next;
return p;
}
说明:
- 基本数据类型:int、char、double、float等都可以替换Elemtype直接用运算符“==”比较~
- 但是两个完全一样的结构体不能直接比较——"struct a == struct b"这种不能通过编译;如果想比较,可以逐个比较结构体中的分量,例如 “a.data1 == b.data1 && a.data2 == b.data2”~
最坏时间复杂度/平均时间复杂度O(n)
以下是查找的衍生操作:求表长~
(3-3)求表长Length(L)
#include <stdio.h>
int Length(LinkList L){
int len = 0; //统计表长
LNode *p = L; //创建指针p,任务为寻找插入的位置,起始位置在第1个结点后
while(p->next !=NULL){ //循环遍历结点
p=p->next;
len++;
}
return len;
}
最坏时间复杂度/平均时间复杂度O(n)
(4)建立单链表
(4-1)尾插法List_TailInsert(L)
基础思路:初始化→建立结点指针、尾指针→在尾指针处执行后插数据操作→后移尾指针~
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
LinkList List_TailInsert(LinkList &L){
int x; //设置链表的数据类型(ElemType)为整型
L = (LNode *)malloc(sizeof(LNode)); //建立头节点;需要头文件<stdlib.h>
LNode *s,*r=L; //声明两个指针s、r均指向头节点,r为表尾指针
scanf("%d",&x); //用户键入结点的值(整型)
while(x!=9999){ //用户键入结点的值为9999时,退出插入操作
s = (LNode *)malloc(sizeof(LNode)); //建立s结点
s->data=x; //s结点的数据域填入x
r->next=s; //r指针指向s结点(建立s结点与前结点的链)
r=s; //r指针后移到s结点的位置(更新尾节点位置)
scanf("%d",&x);
}
r->next=NULL; //尾结点指针置空
return L;
}
最坏时间复杂度/平均时间复杂度O(n)
(4-2)头插法List_HeadInsert(L)
基础思路:初始化→建立头指针、尾指针→在头指针处执行后插数据操作~
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
LinkList List_HeadInsert(LinkList &L){
LNode *s,*r=L; //声明指针s
int x; //设置链表的数据类型(ElemType)为整型
L = (LNode *)malloc(sizeof(LNode)); //建立头节点;需要头文件<stdlib.h>
L->next=NULL; //尾结点数据置空
scanf("%d",&x); //用户键入结点的值(整型)
while(x!=9999){ //用户键入结点的值为9999时,退出插入操作
s = (LNode *)malloc(sizeof(LNode)); //建立s结点
s->data=x; //s结点的数据域填入x
s->next=L->next; //s结点的指针指向L结点的指针(同时指向原表第1个数据)
L->next=s; //L结点指向s结点(建立单链)
scanf("%d",&x);
}
return L;
}
最坏时间复杂度/平均时间复杂度O(n)
注意:头插法存入的数据与实际输入顺序是相反的,会实现逆置效果,是考点~
双链表
双链表的定义
双链表结点中有两个指针prior和next(在单链表的结点中增加了一个指向其前驱的prior指针),分别指向其前驱结点和后继结点。
截图来源: 在java项目中怎么设计双链表 - 编程语言 - 亿速云
双链表的特点
1.增加了前驱结点,方便逆向检索;插入、删除操作的时间复杂度仅为O(1);
2.按值查找与按位查找与单链表相同。
双链表的基本操作
(1)初始化 InitDLinkList(&L)
基础思路:建立一个没有数据元素的单链表,带头节点~命名的D是double的缩写~
#include <stdio.h>
#include <stdlib.h>
typedef struct DNode{ //定义单个结点的结构
ElemType data;
struct DNode *prior;
struct DNode *next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode)); //分配头节点;需要头文件<stdlib.h>
if(L==NULL) //内存不足,分配失败;需要头文件<stdio.h>
return false;
L->prior= NULL; //头节点的prior指向null
L->next = NULL; //头节点之后暂时没有数据结点
return true;
}
void test(){
DLinkList L; //声明一个指向双链表的指针(不创建结点)
InitDLinkList(L); //初始化一个空表,带头节点
}
(2)插入ListInsert(&L,i,e) 与删除ListDelete(&L,i,&e)
(2-1)指定结点后插InsertNextNode
基础思路:结点s需要与前后2个元素的前后2个链子相连,共修改4条链子~
截图来源:linux 内核链表 : 双向循环链表 | 码农家园
截图本身为循环双链表,插入与删除的操作在非表尾时与非循环双链表相同(下列代码第9行,判断结点p是否有后继节点,循环双链表不需要考虑)~
注意:
- 插入时修改指针的顺序;每个方向先写后向的链,再断掉原表的链~
- 判断p结点是否有后继结点(是否为表尾数据元素);如果是的话,就不需要NULL指向前结点啦~
bool InsertNextNode(DNode *p,DNode *s){
if(p==NULL) //p值不合法
return false;
DNode *s = (DNode *)malloc(sizeof(DNode)); //申请空间,创建结点s
if(s==NULL) //内存分配失败
return false;
s->data = e; //将数据e存到结点s中
s->next = p->next; //结点s的next指针与结点p的next指针指向同一个位置(也就是i+1的位置)
if(p->next != NULL) //判断p结点的下一个结点不为空
p->next->prior = s; //结点p的下一个结点的prior指针指向s
s->prior = p; //结点s的prior指针指向p
p->next = s; //结点p的next指针指向s
return true;
}
(2-2)指定结点删除DeleteNextNode
基础思路:删除结点s需要修改2条链子~
注意:需要分别判断结点s本身与其前、后的结点是否为空~
截图来源:linux 内核链表 : 双向循环链表 | 码农家园
截图本身为循环双链表,插入与删除的操作在非表尾时与非循环双链表相同(下列代码第8行,判断结点p是否有后继节点,循环双链表不需要考虑)~
bool DeleteNextNode(DNode *p){
if(p==NULL) //p值不合法
return false;
DNode *q=p->next; //令*q指向*p的后向结点
if(q==NULL) //q没有后继
return false;
p->next = q->next; //p的next指针指向后向结点
if(q->next!=NULL)
q->next->prior = p; //将*q结点从链中“断开”
free(q); //释放后继结点存储空间
return true;
}
以下是删除的衍生操作:销毁表~
(2-3)销毁双链表DestoryList
基础思路:封装删除结点的操作,循环使用→释放头节点、清空头指针~
void DestoryList(DLinklist &L){
while(L->next != NULL)
DeleteNextNode(L);
free(L); //释放头节点
L=null; //头指针指向null
}
循环链表
循环链表的定义
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头节点,从而整个链表形成一个环。
截图来源:图解:链式存储结构之循环链表(修订版)_吴师兄学算法的博客-CSDN博客
循环双链表中,头节点的prior指针指向尾结点,尾结点的next指针指向头结点。
循环链表的特点
1 单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意一个结点开始遍历整个链表。有时对循环单链表不设头指针而只设尾指针,以使得操作效率更高。
- 许多操作是在链表的头部或尾部。若设的是头指针,对在表尾插入元素需要O(n)的复杂度;若设的是尾指针r,r->next即为头指针。对在表头或表尾插入元素都只需要O(1)的复杂度。
2 在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头节点的指针是否为空,而是它是否等于头指针。
- 循环链表判空条件:L->next==L;
- 循环链表判断表尾结点条件: p->next==L
循环链表的基本操作
(1)初始化
(1-1)循环单链表 InitList(LinkList &L)
基础思路:建立一个没有数据元素的循环单链表,带头节点~
注意:循环链表没有空元素,数据元素为空时头节点的指针指向自身~
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{ //定义单个结点的结构
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配头节点;需要头文件<stdlib.h>
if(L==NULL) //内存不足,分配失败;需要头文件<stdio.h>
return false;
L->next = L; //头节点之后暂时没有数据结点,注意循环链表此处不是null
return true;
}
(1-2)循环双链表 InitDLinkList(DLinklist &L)
基础思路:建立一个没有数据元素的循环双链表,带头节点~
注意:循环链表没有空元素,数据元素为空时头节点的两个结点都指向自身...实际效果有点类似于...
截图来源:Keep 自我关爱式肩部拉伸
#include <stdio.h>
#include <stdlib.h>
typedef struct DNode{ //定义单个结点的结构
ElemType data;
struct DNode *prior;
struct DNode *next;
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L = (DNode *)malloc(sizeof(DNode)); //分配头节点;需要头文件<stdlib.h>
if(L==NULL) //内存不足,分配失败;需要头文件<stdio.h>
return false;
L->prior= L; //头节点的prior指向自己,构成循环
L->next = L; //头节点之后暂时没有数据结点
return true;
}
void test(){
DLinkList L; //声明一个指向双链表的指针(不创建结点)
InitDLinkList(L); //初始化一个空表,带头节点
}
静态链表
静态链表的定义
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。
和顺序表一样,静态链表也要预先分配一块连续的空间。
静态链表以next==-1作为其结束的标志;空闲游标可标记为-2以区分内存的脏数据~
截图来源:静态链表 - 搜狗百科
截图来源:https://www.cnblogs.com/dongry/p/10210609.html
静态链表的特点
1 静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素;
2 不能随机存取,只能从头结点依次向后查找;
3 容量固定不可变;
4 没有单链表方便,实际用于早期不支持指针的低级语言,或数据元素数量几乎固定不变的场景[操作系统的文件分配表FAT]。// 因此也不怎么常考~
静态链表的基本操作
(1)初始化 InitList(&L)
(1-1)静态链表结点类型描述SLinkList
基础思路:3-6行定义结点结构:数据域+指针域,8-10行是为静态链表分配连续存储空间~
#include <stdio.h>
#define MaxSize 10 //预定义静态链表的最大长度
struct Node{ //定义静态链表中结点的类型
ElemType data; //数据域:每个结点存放一个数据元素
int next; //指针域:下一个元素的数组下标
};
void test(){
struct Node a[MaxSize]; //数组a作为静态链表
}
顺序表与链表的比较
项目 | 线性顺序表 | 线性链表 | 备注 |
逻辑结构 | 线性表 | 线性表 | 相同~ |
存储结构 | 顺序存储 | 链式存储 | |
1 连续存储,支持随机存取 2 存储密度高 3 不方便分配空间、改变容量 | 1 离散存储,不支持随机存取 2 存储密度低 3 方便分配离散空间、改变容量 | ||
基本操作 | 创建: 1 需要预分配大片连续空间; 2 不方便拓展(静态不能更改,动态可更改但不太方便)。 | 创建: 1 声明头指针,增加头节点(头节点非必要),即可创建; 2 方便拓展。 | 链表灵活性较强~ |
销毁: 1 length=0 逻辑标记为空表; 2 静态分配:系统自动回收空间;动态分配:free(L.data);。 | 销毁: 1 循环删除结点。 | 顺序表回收快一些~(虽然一般没人比这个...) | |
插入与删除(按位): 1 按位查找元素O(1); 2 所有元素后移、前移O(n); 3 总体复杂度O(n)。 | 插入与删除(按位): 1 遍历查找元素O(n); 2 插入、删除元素O(1); 3 总体复杂度O(n)。 | 链表快一些~ //虽然复杂度相等,但查找元素比移动元素消耗的时间少~ | |
查找: 1 按位查找元素O(1); 2 按值查找元素O(n),如果为有序表,则时间为O(log2n)~ | 查找: 1 按位查找元素O(n); 2 按值查找元素O(n)~ | 顺序表速度快一些~ |
结尾
博文写得模糊或者有误之处,欢迎留言讨论与批评~
码字不易,若有所帮助,可以点赞支持一下博主嘛?感谢~(●'◡'●)