第一章:绪论
![](https://img-blog.csdnimg.cn/img_convert/941fc17b859d22ef65696a110511cd09.png)
1.1相关基本概念
范围大小:数据>数据对象>数据元素>数据项
数据:是描述客观事物的符号。是指能输入到计算机中并被计算机程序处理的符号总称。
例如:整数、实数和字符串都是数据。
数据对象:是具有相同性质的数据元素的集合,是数据的一个子集。
例如:大写字母就是一个数据对象,大写字母数据对象是集合{’A’,’B’……’Z’}。
数据元素:是数据的基本单位。
例如:一本书的书目信息为一个数据元素。
数据项:数据项是构成数据元素不可分割的最小单位。
例如:书目信息的每一项(如书名,作者名等)为一个数据项。
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
例如:某校学生年级信息及他们之间的关系(同学、学长)是数据结构。
1.2数据结构三要素
1.2.1逻辑结构
即数据元素之间的关系。
逻辑结构包括:集合、线性结构、树形结构、图结构
集合结构:结构中的数据元素之间除“同属一个集合”外,别无其它关系。
线性结构:结构中的数据元素之间只存在一对一的关系,除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后继。
树形结构:结构中数据元素之间存在一对多的关系。
图状结构:数据元素之间是多对多的关系。
1.2.2存储结构
即数据元素逻辑关系在计算机上的表示。
存储结构包括:顺序存储、非顺序存储(链式存储、索引存储、散列存储)
顺序存储:把 逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
链式存储: 逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
索引存储:在存储元素信息的同时,还建立附加的索引表,索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。
散列存储:根据元素的关键字直接计算出该元素的存储地址,又称 哈希(Hash)存储。
👉若采用顺序存储,则各个元素在物理上必须是连续的;若采用非顺序存储,则各个元素在物理上可以是离散的。
1.2.3数据的运算
数据的运算:施加在数据上的运算包括运算的定义何实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
例如:队列中 队头元素的出队是运算的定义。
1.3数据类型、抽象数据类型
数据类型:数据类型是一个值的集合和定义再此集合上的一组操作的总称。
例如:int数据类型描述:值的范围-2147483648~2147483647;可执行的操作:加、减、乘等.
数据类型还可分为以下两种:
原子类型:其值不可再分的数据类型。(int、short、bool等)
结构类型:其值可以再分解为若干成分(分量)的数据类型。(即struct结构体类型)
抽象数据类型:简称ADT,是抽象数据组织及与之相关的操作。是用数学化的语言定义数据的逻辑结构、定义运算。与具体的实现无关。
借助三要素理解ADT:
抽象数据类型只考虑逻辑结构和数据运算,不关心存储结构。
1.4算法
1.4.1算法基本概念
程序=算法+数据结构
算法:是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
简单点说算法就是处理信息的步骤
算法的五个特性:有穷性、确定性、可行性、输入、输出
有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
确定性:算法中每条指令必须有确定的含义,对于相同的输入只能得到相同的输出。
可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入:一个算法有 零个或多个输入,这些输入取自于某个特定的对象的集合。
输出:一个算法有 一个多个输出,这些输出是与输入有着某种特定关系的量。
好算法的特质:正确性、可读性、健壮性、高效率与低存储量需求
正确性:算法应能够正确的求接问题。
可读性:算法应具有良好的可读性,以帮助人们理解。
健壮性: 输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙地输出结果。
效率与低存储量需求:执行速度快, 时间复杂度低;不费内存, 空间复杂度低。
1.4.2时间复杂度
时间复杂度T(n):衡量程序运行时间的多少
用的并不是准确值(事实上也无法得出),而是根据合理方法得到的预估值。
语句频度:每条语句重复执行的次数
T(n):所有语句频度之和,其中n为问题的规模
预估一个算法所编程序的运行时间标准过程:
①得出整段代码频度 ②简化频度 ③用大O记法表示
以如下代码为例
void loveyou(int n){
int i=1;
while(i<=n){
i++;
printf("i love you %d\n",i);
}
printf("i love you more than %d\n",n);
}
int main(){
loveyou(n);
}
观察代码可知:
第2条代码语句频度:1次
第3条代码语句频度:n+1次
第4、5条代码语句频度:n次
第7条代码语句频度:1次
因此,整段代码中所有语句共执行了 3n+3 次,即整段代码的频度T(n)为3n+3。
👉思考一个问题,类似 3n+3这样的频度,还可以再简化吗?答案是肯定的。
以 3n+3 为例,当 n 无限大时,是否在 3n 的基础上再做 +3 操作,并无关紧要,因为 3n 和 3n+3 当 n 无限大时,它们的值是无限接近的。甚至于我们还可以认为,当 n 无限大时,是否给 n 乘 3,也是无关紧要的,因为 n 是无限大,3*n 也是无限大。
最终频度3n+3 可以简化为 n
得到最简频度的基础上,为了避免人们随意使用 a、b、c 等字符来表示运行时间,需要建立统一的规范。数据结构推出了大 O 记法来表示算法(程序)的运行时间。
大 O 记法格式如下:
O(频度)
其中,这里的频度为最简之后所得的频度。
快速计算时间复杂度步骤是:
①找到一个基本操作(最深层循环)
②分析该基本操作执行次数与问题规模n的关系
③将关于n的表达式简化就是时间复杂度
例如:
s = 0;
for(i = 0; i < n; i++)
for(j = 0; j < n; j++)
s += B[i][j];
sum = s;
①只看基本操作(最深层循环) s += B[i][j];
②该基本操作执行次数与问题规模n的关系是n*n
③简化后还是n*n,所以它的时间复杂度是n*n
简化频度表达式结论:
只保留最高阶的项,且系数变为1
例如: 2n^2+2n+1 简化为 n^2
常用时间复杂度大小关系:
O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 <O(nlogn)< O(n^2)平方阶 < O(n^3)(立方阶) < O(2^n) (指数阶)<O(n!)(阶乘阶)
记忆口诀:常对幂指阶
1.4.3空间复杂度
空间复杂度S(n):衡量程序运行所需内存空间的大小。
和时间复杂度类似,一个算法的空间复杂度,也常用大 O 记法表示。
例如
void loveyou(int n){
int i=1;
while(i<=n){
i++;
printf("i love you %d\n",i);
}
printf("i love you more than %d\n",n);
}
该代码无论问题规模如何变,算法运行所需的内存空间都是固定的常量,算法空间复杂度为:S(n)=O(1)
也称为算法原地工作。
如果随着输入值 n 的增大,程序申请的临时空间成线性增长,则程序的空间复杂度用 O(n) 表示;
如果随着输入值 n 的增大,程序申请的临时空间成 n^2 关系增长,则程序的空间复杂度用 O(n^2) 表示;
如果随着输入值 n 的增大,程序申请的临时空间成 n^3 关系增长,则程序的空间复杂度用 O(n^3) 表示;
计算空间复杂度步骤是:
①找到所占空间大小与问题规模相关的变量
②分析所占空间与问题规模n的关系
③将关于n的表达式简化就是空间复杂度
拓展(较难):
函数递归调用的空间复杂度
void loveyou(int n){
int flag[n]; //声明数组(该为影响空间复杂度的主要成分)
//省略数组初始化代码
if(n>1){
loveyou(n-1);
}
printf("i love you %d\n",n);
}
int main(){
loveyou(3);
}
分析这个代码运行过程:
调用loveyou(3)时,需要存储长度为3的数组flag[3]
递归调用loveyou(2),需要存储长度为2的数组flag[2]
递归调用loveyou(1),需要存储长度为1的数组flag[1]
它的内存开销是1+2+3=6
把它推广到调用n次内存开销是:1+2+3+……+n=1/2 n^2+1/2 n
空间复杂度就是S(n)=O(n^2)
第二章:线性表
![](https://img-blog.csdnimg.cn/img_convert/21f0c05f0033f0dd3d30ae99d4fc8faa.png)
2.1线性表的定义和基本操作
2.1.1线性表定义
线性表:是具有相同数据类型的n(n>0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为:L=(a1,a2,…ai,…,an)
直接前驱:某一元素的左侧相邻元素称为“直接前驱”
直接后继:某一元素的右侧相邻元素称为“直接后继”
a i是线性表中的“第i个”元素线性表中的 位序(位序是从1开始的,数组下标是从0开始的)
a1是 表头元素, an是 表尾元素
除第一个元素外,每一个元素有且只有一个直接前驱;除最后一个元素外,每一个元素有且只有一个直接后继。
线性表存储数据可分为:顺序存储结构(顺序表)和链式存储结构(链表)
2.1.2线性表基本操作
lnitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
Listlnsert(&L,i,e):插入操作。在表L中第i个位置上,插入元素e.
ListDelete(&L,i,&e):删除操作。删除表中第i个位置上的元素,并用e返回删除元素的值。
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置元素的值。
length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按先后顺序输出线性表L的所有元素的值。
Empty(L):判空操作。若L为空表,则返回True,否则返回False。
函数名和参数形式,命名都可发生改变,但是命名必须具有可读性。
&L使用的是引用传参,引用传参是直接把线性表的地址传过去进行操作,可直接修改线性表L的值(没&则修改不了)
为什么实现数据结构操作的基本操作 :
团队合作编程,我们定义的数据结构要做到别人也方便使用
将常用的操作/运算封装成函数,避免重复工作,降低出错的风险
2.2顺序表
2.2.1顺序表定义
顺序表:是用顺序存储的方式实现线性表
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
可以发现顺序表存储与数组存储相似,实际上顺序表就是由数组来存储。
2.2.2顺序表实现
顺序表实现有两种方法:静态分配、动态分配
静态分配实现顺序表:
#include<stdio.h>
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //用静态的"数组"存放数据元素
int length; //顺序表的当前长度
}SqList; //Sq是sequence(顺序)的缩写
//初始化顺序表
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++){
L.data[i]=0; //将所有数据元素设置为默认初始值(可省)
}
L.length=0; //一定不可省
}
int main(){
SqList L;//声明一个顺序表
InitList(L);//初始化顺序表
for(int i=0;i<MaxSize;i++){
printf("data[%d]=%d\n",i,L.data[i]);
}
return 0;
}
静态分配的弊端
1.“数组”存满后,无法增加表长
2.害怕”数组”存满,将表长定义的很长,浪费内存空间
动态分配实现顺序表:
malloc函数:作用是 动态分配内存,malloc的返回值是一个指针,指向一段可用内存的起始地址(在使用malloc函数前,需要调用头文件##include<stdlib.h>)
sizeof函数:用于确定变量的长度(单位为字节)
#include<stdio.h>
#include<stdlib.h>//malloc、free函数的头文件
#define InitSize 10 //默认的最大长度
typedef struct{
int *data; //指示动态分配数组的指针
int MaxSize;//顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
//初始化顺序表
void InitList(SeqList &L){
//用malloc 函数申请一片连续的存储空间
L.data =(int*)malloc(InitSize*sizeof(int)) ;
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
int *p=L.data;
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
free(p); //释放原来的内存空间
}
int main(void){
SeqList L; //声明一个顺序表
InitList(L);//初始化顺序表
IncreaseSize(L,5);
return 0;
}
第12行(int*)是将malloc函数返回的指针类型强转为int类型
顺序表特点:
①随机访问,即可在O(1)时间内找到第i个元素。
如找第6个元素,用数组a[5]访问就得出
②存储密度高,每个节点只存储数据元素本身
链式存储每个节点除了存储数据元素还要有额外的空间存储指针,相较而言,顺序表的存储密度较高。
③拓展容量不方便
静态分配无法拓展容量。即便动态分配的方式实现,拓展长度的时间复杂度也比较高。
④插入、删除操作不方便,需要移动大量元素
2.2.3顺序表的插入删除
顺序表的动态分配实现和静态分配实现插入删除操作雷同,以下就以静态实现的顺序表为例
顺序表的插入:
Listlnsert(&L,i,e):插入操作。在表L中第i个位置上,插入元素e.
bool ListInsert(SqList &L, int i, int e){
if(i<1||i>L.length+1) //用来判断i的范围是否有效
return false;
if(L.length>MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length; j>=i; j--){ //将第i个元素及其之后的元素后移
L.data[j]=L.data[j-1];
}
L.data[i-1]=e; //在位置i处放入e
L.length++; //长度加1
return true;
}
时间复杂度分析:
最好情况:新元素插入到表尾,不需要移动元素
i=n+1,循环0次;最好时间复杂度=O(1);
最坏情况:新元素插入到表头,需要将原有的n个元素全都向后移动
i=1,循环n次;最坏时间复杂度=O(n);
平均情况:假设新元素插入到任何一个位置的概率相同,即 i=1,2,3,…,length+1 的概率p都是 1/(n+1),
i=1,循环n次;i=2时,循环n-1次;i=3,循环n-2次…… i=n+1 时,循环0次
平均循环次数 =np+(n−1)p+(n−2)p+⋯⋯+1⋅p=n/2.
所以 平均时间复杂度=O(n)
顺序表的删除:
ListDelete(&L,i,&e):删除操作。删除表中第i个位置上的元素,并用e返回删除元素的值。
bool LisDelete(SqList &L, int i, int &e){ // e用引用型参数
if(i<1||i>L.length) //判断i的范围是否有效
return false;
e = L.data[i-1] //将被删除的元素赋值给e
for(int j=L.length; j>=i; j--){ //将第i个后的元素前移
L.data[j-1]=L.data[j];
}
L.length--; //长度减1
return true;
}
复杂度分析:为O(1)
2.2.4顺序表的查找
顺序表按位查找:
GetElem(L,i):按位查找操作。获取表L中第i个位置元素的值。
ElemType GetElem(SqList L, int i){
return L.data[i-1]; //注意是i-1,(因为数组下标从0开始,顺序表位序从1开始)
}//动态静态都是这样操作
复杂度分析:
分析同顺序表的插入,为O(n)
顺序表按值查找:
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList 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; //推出循环,说明查找失败
}
复杂度分析:
分析同顺序表的插入,为O(n)
2.3单链表
2.3.1单链表的定义
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。
链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
2.3.1.1顺序表与单链表优缺点:
顺序表优点:
可随机存取,时间复杂度为O(1),效率高。
(随机存取理解:假如想改变第6个元素值,此时用data[5]访问这个元素更改就行)
顺序表缺点:
在顺序表中间插入或删除元素时,时间复杂度为O(N),麻烦。
顺序表长度固定,改变容度不方便。
单链表优点:
不要求大片连续空间,改变容度方便
单链表缺点:
链表不可随机存取,查找元素效率低,存放指针要耗费一定的空间。
2.3.1.2单链表代码定义:
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
ElemType:在程序定义中代表某一不确定的类型(实际用时,把它改成需要的数据类型就好)
typedef 关键字,作用是为一种数据类型定义一个新名字,数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)
此代码的重难点在于:
第4行代码中LNode, *LinkList
执行了typedef struct LNode
typedef struct * LinkList这两个操作
此时要表示一个单链表,只需声明一个头指针L,指向单链表的第一个结点,那么此时有两种方式
LNode * L和LinkList L,但是一般用LinkList L表示一个单链表,虽然两者效果一样,但方便阅读,LNode *更强调表示结点。
2.3.1.3单链表两种实现方式:
带头结点的单链表:
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L){ //注意用引用 &
L = NULL; //空表,暂时还没有任何结点;防止脏数据
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针(并未创造结点)
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L == NULL)
}
不带头结点的单链表:
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L){
L = (LNode*) malloc(sizeof(LNode)); //头指针指向一个头结点(不存储数据)
if (L == NULL) //内存不足,分配失败
return false;
L -> next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针: 头指针
//初始化一个空表
InitList(L);
//...
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
if (L->next == NULL)//注意是L->next
return true;
else
return false;
}
带头结点操作比较方便
不带头结点操作比较麻烦(后续操作中能体现出来)
2.3.2单链表的插入删除
![](https://img-blog.csdnimg.cn/img_convert/081da275884e20b9f12315e26cee6a9b.png)
按位序插入(带头结点)
Listlnsert(&L,i,e):插入操作。在表L中第i个位置上,插入元素e.
typedef struct LNode{//定义单链表结点类型
int data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, int e){
if(i<1)
return false;//判断i的合法性
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
p = p->next; //p指向下一个结点
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p之后
return true;
}
敲重点:在单链表(带头结点)中找到第i个结点代码
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i){
p = p->next; //p指向下一个结点
j++; //当前p指向的是第几个结点
}
25 s->next = p->next;
26 p->next = s; 一定不要颠倒
按位序插入(带头结点)平均时间复杂度为O(n)
按位序插入(不带头结点)
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1)
return false;
//插入到第1个位置时的操作与其它结点操作不同
if(i==1){
LNode *s = (LNode *)malloc(size of(LNode));
s->data =e;
s->next =L;
L=s; //头指针指向新结点
return true;
}
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点
p = L; //L指向第一个结点,(不是头结点)!!
//循环找到第i-1个结点
while(p!=NULL && j<i-1){
p = p->next;
j++;
}
if (p==NULL) //i值不合法
return false;
//在第i-1个结点后插入新结点
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
可以发现插入第一个元素,需要更改头指针(麻烦,所以一般情况下不使用不带头结点单链表)
i>1的情况与带头结点唯一区别是j的初始值为1
不存在头结点,所以不存在第0个结点,初始指向的是第一个结点
平均时间复杂度O(n)
指定结点的后插操作
InsertNextNode(LNode *p, ElemType e): 给定一个结点p,在其之后插入元素e
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
bool InsertNextNode(LNode *p, ElemType e){
if(p==NULL){
return false;
}
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败,内存满了
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
平均时间复杂度 为 O(1)
指定结点的前插操作
InsertPriorNode(LNode *p, ElenType e):给定一个结点p,在其之后插入元素e
已知单链表A→B→D,分析如何在D前头插入C
由于单链表每个结点只存指向它下一个结点的指针,所以在D处是无法前插C的(只能联系后面的结点,就像你可以决定你生不生孩子,但不能决定当初你父亲生不生你,嘻嘻),只能是遍历查询D的前驱结点B,然后对B进行后插。时间复杂为O(n);
但还有一种简便方法:①对D后插,插入C,单链表变成A→B→D→C,②将D、C的数据域交换,单链表变成A→B→C→D。时间复杂度为常数阶,以下是代码实现
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
bool InsertPriorNode(LNode *p, ElenType e){
if(p==NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败,内存满了
return false;
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中元素复制到s
p->data = e; //p中元素覆盖为e
return true;
} //时间复杂度为O(1)
按位序删除节点
ListDelete(&L,i,&e):删除操作。删除表中第i个位置上的元素,并用e返回删除元素的值。
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElenType &e){
if(i<1) return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
//循环找到第i-1个结点
while(p!=NULL && j<i-1){ //如果i>lengh, p最后会等于NULL
p = p->next; //p指向下一个结点
j++;
}
if(p==NULL)
return false;
if(p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q = p->next; //令q指向被删除的结点
e = q->data; //用e返回被删除元素的值
p->next = q->next; //将*q结点从链中“断开”
free(q) //释放结点的存储空间
return true;
}
平均时间复杂度:O(n)
指定结点的删除
DeleteNode(LNode *p):删除结点p
分析类似 指定结点的前插操作
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data; //让p和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
free(q);
return true;
}
时间复杂度 : O(1)
重点:
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data; //让p和后继结点交换数据域
p->next = q->next; //将*q结点从链中“断开”
对于这段核心代码的理解:
o→p→x→y
①保存→x
②将p变成x(此时单链表是这个样子o→x→x→y)
③将→y给x(原是p的x)
此时单链表完成删除o→x→y
需要知道q->next,q是 p->next,所以q->next就是 p->next->next
2.3.3单链表的查找
按位查找
GetElem(L,i):按位查找操作。获取表L中第i个位置元素的值。
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
LNode * GetElem(LinkList L, int i){
if(i<0) return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i){ //循环找到第i个结点
p = p->next;
j++;
}
return p; //返回p指针指向的值
}
平均时间复杂度:O(n)
按值查找
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
LNode * LocateElem(LinkList L, ElemType e){
LNode *P = L->next; //p指向头结点的下一个结点(第一个结点)
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
平均时间复杂度:O(n)
求单链表长度
typedef struct LNode{//定义单链表结点类型
ElemType data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
int Length(LinkList L){
int len=0;
LNode *p = L;
while(p->next != NULL){
p = p->next;
len++;
}
return len;
}
2.3.4单链表的建立
头插法建立单链表
头插法将输入的数据插入到链表的表头
逆向建立单链表
数据插入到链表的表头,输出数据时的数据与读入的数据时相反的,如 输入1 2 3 ,输出的结果是 3 2 1
typedef struct LNode{//定义单链表结点类型
int data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点
L->next = NULL; //初始为空链表
scanf("%d", &x); //输入要插入的结点的值
while(x!=999){ //输入999表结束
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = x;
s->next = L->next; //将新结点插入表中
L->next = s; //将L设为头指针
scanf("%d", &x);
}
return L;
}
时间复杂度:O(n)
尾插法建立单链表
尾插法:将数据插入到链尾
正向建立单链表
输入数据的顺序与链表顺序的一致性,如 输入1 2 3 ,数据在链表同样以 1 2 3 保存
typedef struct LNode{//定义单链表结点类型
int data; //数据域,存放一个数据元素
struct LNode *next;//指针域,指针指向下一个结点
}LNode, *LinkList;
LinkList List_TailInsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode)); //建立头结点(初始化空表)
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x); //输入要插入的结点的值
while(x!=9999){ //输入9999表结束
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s //r指针指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
时间复杂度:O(n)
链表逆置
void listReverse(linkedList &L)
{
node *p,*s;
p = L->next; //p指针指向第一个结点
L->next = NULL; //头结点指向NULL
while(p)
{
// s记录正在处理的结点,p记录下一轮待处理的结点
s = p; //s承接上一轮记录的位置
p = p->next; //p为下一轮记录位置
//把s插入 已逆置的部分 中
s->next = L->next; // L->next代表已逆置的第一结点,s的指针域指向它
L->next = s; //将第一结点 设置为s
}
}