数据的逻辑结构
- 集合
- 线性结构
- 树形结构
- 图结构
数据的物理结构(存储结构)
(以下3种为非顺序存储)
把逻辑上相邻的元素存储在物理位置上也想来的存储单元中,元素之间的关系由存储单元的邻接关系来体现
逻辑上相邻的元素在物理上可以不相邻,而是借助元素存储地址的指针来表示元素之间的逻辑关系
做存储元素信息的同时,还建立附加的索引表。索引表中的每项成为索引项,索引项的一般形式是(关键字,地址)
根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储
故:
- 若采用顺序存储,则哥哥元素在物理上必须是连续的;若采用非顺序存储,则各个数据元素在物理上可以是离散的
- 数据的存储结构会影响存储空间分配的方便程度
- 数据的存储结构会影响对数据运算的速度
数据的运算
施加在数据上的运算包括运算的定义和实现。
运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤
线性结构的常用运算:
- 对头元素出队
- 新元素入队
- 输出队列长度等
数据类型和抽象数据类型
数据类型是一个值的集合和定义在此集合上的一组操作的总称
- 原子类型:其值不可再分的数据类型
- 结构类型:其值可以再分解为若干成分的数据类型
抽象数据类型(ADT)是抽象数据组织及与之相关的操作
数据结构的三要素:
- 逻辑结构
- 物理结构(存储结构)
- 数据的运算
算法
算法的描述
- 自然语言
- 流程图、NS流程图
- 伪代码:类语言:类C语言
- 程序的代码:C语言程序,Java语言程序
算法与程序
-
算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法
-
程序是用某种程序设计语言对算法的具体实现
程序=数据结构+算法
- 数据结构通过算法实现操作
- 算法根据数据结构设计程序
算法的特性
必须具备以下五个特性
-
有穷性。一个算法必须在执行有穷步之后结束,且每一步都可在有穷时间内完成。算法必须是有穷的,而程序可以是无穷的
-
确定性。算法中每条指令必须有确切的含义,没有二义性,只有唯一的一条执行路径,对于相同的输入只能得出相同的输出
-
可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现
-
输入。一个算法有零或多个输入,这些输入取自某个特定的对象的集合
-
输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量
好算法的特质
- 正确性。应能够正确地解决求解问题
- 可读性。算法应具有良好的可读性,以帮助人们理解。算法可以用伪代码描述,甚至用文字描述,最重要的是无歧义地描述出解决问题的步骤
- 健壮性。当输入非法输入时,算法能做出反应或进行处理,而不会产生莫名其妙的输出结果
- 高效率和低存储量需求,即执行速度快,时间复杂度低,不费内存,空间复杂度低
在以上几个访民啊都满足的情况下,主要考虑算法的效率,通过算法的效率高低来评判不同算法的优劣。
算法效率分为两方面:
- 时间效率:指的是算法所耗费的时间
- 空间效率:指的是算法执行过程中所耗费的存储空间
但是又是时间效率和空间效率是矛盾的
算法的时间复杂度
算法时间效率的度量
算法时间效率可以用该算法编制的程序在计算机上所耗费的时间来度量
两种度量方法:
事后统计法
存在的问题:
- 和机器性能有关
- 和编程语言有关,越高级的语言执行效率越低,例如java的效率低于C
- 和编译程序产生的机器指令质量有关
- 有些算法不能事后统计
所以需要排除与算法本身无关的外界因素
事前分析法
一个算法的运行时间是指一个算法在计算机上运行所耗费的时间大致等于计算机执行一种简单的操作(如赋值,比较,移动等)所需的时间与算法中进行的简单操作次数乘积
- 算法的运行时间=一个简单擦欧总所需的时间*简单操作次数
- 即算法中每条语句的执行时间之和
- 算法运行时间=∑每条语句的执行次数(语句频度)*该语句执行一次所需的时间
- 每条语句执行一次的时间因机器而异。取决于指令性能、速度以及编译的代码质量,是由机器本身硬件环境决定与算法无关
- 所以我们可以假设执行每条语句所需的时间均为单位时间,这样对算法的运行时间的讨论就可以转化为讨论该算法中所有语句的执行次数,即频度之和,这样可以独立于不同机器的软硬件环境来分析算法的时间性能了
所以采用事前预估算法时间开销T(n)与问题规模n的关系(T=time)
例如一个n*n矩阵相乘的算法:
算法所耗费的时间定义为该该算法种每条语句的频度之和,则T(n)为:
但是这样逐个计算太麻烦,为了便于比较不同算法的时间效率,我们仅比较他们的数量级
- 数量级越小越好
若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)为算法的渐进时间复杂度(O是数量级的符号),简称时间复杂度。
算法中基本语句重复执行的次数是问题规模n的某个函数f(n),算法的时间量度记作:
T(n)=O(f(n))
基本语句:
- 算法中重复执行次数和算法的执行次数成正比的语句
- 对算法运行时间贡献最大
- 执行次数最多
求渐进时间复杂度
当问题规模足够大时,可以只考虑阶数高的部分,然后去掉系数化为1,再在前面用O(n)括起来用来表示时间复杂度
一般情况下,不必计算所有操作的执行次数,而只考虑算法中基本操作执行的次数,他是问题规模n的某个函数,用T(n)表示
只保留最高次项,忽略所有低次幂项和最高次幂系数,体现增长率的含义
分析时间复杂度的基本方法:
- 找出语句频度最大的那条语句作为基本语句
- 计算基本语句的频度得到问题规模 n 的某个函数 f(n)
- 取其数量级用符号"O"表示
时间复杂度是由嵌套最深层语句的频度决定的
对于复杂的算法,可以将他分成几个容易估算的部分,然后利用大O加法法则和乘法法则,计算算法的时间复杂度:
- 加法规则
多项相加,只保留最高阶的项,且系数变为1
- 乘法规则
多项相乘,都保留
算法时间效率的比较
- 当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊
阶数排序
哪个阶数高就保留谁
-------常对幂指阶-------
时间复杂度越低的算法越优秀
例如:
由于O(n)>O(log2n),所以保留O(n3),所以时间复杂度为:
T3(n)=n3
当代码较长时,若前面有1000行顺序执行(没有循环,直接执行下来)的代码,不需要一行一行的数,因为:
顺序实行的代码只会影响常数项,可以忽略
只需要挑循环中的一个基本操作分析它的执行次数与n的关系即可
嵌套循环的时间复杂度分析
外层执行n次,内层执行n的平方次,所以:
如果有多层嵌套循环,只需要关注最深处循环循环了几次
有的情况下,算法中基本操作重复执行的次数还随问题的输入数据集不同而不同:
最坏时间复杂度:最坏情况下算法的时间复杂度
平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
一般而言不会去算最好时间复杂度,因为这意义不大
效率的度量
空间复杂度 S(n)
s=space
若空间复杂度是常数阶的话,就称这种算法可以原地工作,即算法所需内存空间为常量
以后当分析一个算法的空间复杂度时,只需要关注它所需要的存储空间大小与问题规模相关的变量
当发生函数递归调用时,空间复杂度就等于函数递归调用的深度
算法要占据的空间:
- 算法本身要占据的空间,输入/输出,指令,常数,变量等
- 算法要使用的辅助空间
线性表
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0是线性表是一个空表。若用L命名线性表,则其一般表示为
L=(a1,a2,…,ai,ai+1,…,an);
脚标从1开始
几个概念:
- ai是线性表中的“第i个”元素线性表中的位序
- a1是表头元素,an是表尾元素
- 除第一个元素外,每个元素有且仅有一个直接前驱元素;除最后一个元素外,每个元素有且仅有一个直接后继元素
线性表的基本操作
-
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
-
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
-
Listlnsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
List Insert( ElementType X, int i, List PtrL )
List p, s;
if(i==1){
/*新结点插入在表头*/
s = (List)malloc(sizeof(struct LNode)); /*申请、填装结点*/
s->Data=X;
s->Next = PtrL;
return s;/*返回新表头指针*/
}
p = FindKth( i-1, PtrL );/*查找第i-1个结点*/
if(p == NULL ){
“第i-1个不存在,不能插入*/
printf( "参数i错 " );
return NULL;
}else{
s = (List)malloc(sizeof(struct LNode)); /*申请、填装结点*/
s->Data=X;
s->Next = p->Next; /*新结点插入在第i-1个结点的后面*/
p->Next = s;
return PtrL;
- ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
(1)先找到链表的第i-1个结点,用p指向;
(2)再用指针s指向要被删除的结点(p的下一个结点);
(3)然后修改指针,删除s所指结点;
(4)最后释放s所指结点的空间。
List Delete( int i, List PtrL )
{
List p, s;
if(i==1){
/*若要删除的是表的第一个结点*/
s = PtrL;/*s指向第1个结点*/
if (PtrL!=NULL) PtrL = PtrL->Next;/*从链表中删除*/
else return NULL;
free(s);/*释放被删除结点*/
return PtrL;
}
p = FindKth( i-1, PtrL );/*查找第i-1个结点*/
if(p == NULL ){
printf(“第%d个结点不存在”,i-1);
return NULL;
} else if( p->Next == NULL )t
printf(“第%d个结点不存在”, i);
return NULL;
}else {
s = p->Next;/*s指向第i个结点*/
p->Next = s->Next;
/*从链表中删除*1
free(s); /释放被删除结点*/
return PtrL;
-
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
-
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
其他常用操作:
-
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
-
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
-
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
对数据的操作-----创建、销毁、增删改查
C语言函数的定义----返回值类型 函数名(参数1类型 参数1名,参数2类型 参数2名,…)
在实际开发中,可根据实际需求定义其他基本操作
函数名和参数名的形式、命名都可改变
什么时候要传入引用"&":对参数的修改结果需要带回来
广义表
- 广义表时线性表的推广
- 对于线性表而言,n个元素都是基本的单元素
- 广义表中,这些元素不仅可以是单元素也可以是另一个广义表
线性表的链式存储实现
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存的任意位置上的
- 链表中元素的逻辑次序和物理次序不一定相同
- 每个结点由两个部分组成:数据域和指针域,这样若干个结点构成的就是链表
- 只要找到第一个元素就能一次找到后面的元素
- 第一个元素由头指针head指向
- 单链表由头指针唯一确定,因此单链表可以用头指针的名字来命名
- 最后一个结点的指针域为NULL
相关术语
- 结点:数据元素的存储映像,由数据域和指针域构成
- 链表:n个结点由指针链组成一个链表。他是线性表的链式存储映像,称为线性表的链式存储结构
单链表、双链表、循环链表
- 结点只有一个指针域的链表,称为单链表或线性链表
- 结点有两个指针域的链表,称为双链表
一个指针域用来存储前驱元素的地址,另一个元素用来存储后继元素的地址,所以双链表就有3个域:1个数据域+2个指针域
最后一个结点的地址域为NULL
- 首位相接的链表称为循环链表
最后一个元素的指针域存储头结点的地址
头指针、头结点和首元结点
- 头指针:是指向链表中第一个结点的指针。通常在首元结点的的前面加一个头结点
- 首元结点:是指链表中存储第一个数据元素的结点
- 头结点:是在链表的首元结点之前附设的一个结点;
链表存储结构示意图的两种形式
- 不带头结点
Head指针里存放第一个元素的地址
- 带头结点
head指向头结点,头结点的指针域里面存放首元结点的地址
空表的表示
- 无头结点时,头指针为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
在链表中设置头结点的好处
- 便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理
-
便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
头结点的数据域内装的是什么
头结点的数据域可以为空,也可以放线性表长度等附加信息,但此节点不能计入链表长度值
链表(链式存储结构)的特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。这种存储元素的方法被称为顺序存取法
顺序表→随机存储 链表→顺序存取
单链表
- 带头结点的单链表
单链表由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表表示成表L
单链表的存储结构
在高级程序语言中,节点用结构体来表达
data类型根据实际要求决定
next部分存的是下一个节点的地址,所以是指针型
单链表存储结构的定义:
typedef struct Lnode{
//声明结点的类型和指向结点的指针类型
int/*所存数据类型*/ data;//结点的数据域
struct Lnode *next;//结点的指针域
}Lnode, *LinkList;
//Lnode表示的是节点,*LinkList为指向结构体Lnode的指针类型
//定义链表:(指向头结点的指针代表链表)
LinkList L;
//定义结点指针p
Lnode *p 或 LinkList p(少用);
单链表存储结构的使用:
Lnode a;
a.data=5;
a.next=...;
//由于在定义的时候,LinkList前面加了*,所以在定义的时候就可以简化省略
Lnode *L//定义指向变量的地址,等价于:
LinkList L
案例
例如,存储学生学号、姓名、成绩的单链表结点类型定义如下:
void main() {
typedef struct student {
char num[8];//数据域
char num[8];//数据域
int score;//数据域
struct student* next;//指针域,指针指向struct student类型
}Londe,*LinkList;
}
但是,为了方便操作,下面的这种操作方式更常用
typedef struct {
char num[8];//数据域
char num[8];//数据域
int score;//数据域
}ElemType;
typedef struct Lnode {
ElemType data;//数据域
struct Lnode* next;//指针域
}Lnode,*LinkList;
单链表基本操作的实现
单链表的初始化 (带头结点的链表)
头结点存在,但为空,后面没有结点
算法步骤:
(1)生成新结点作头结点,用头指针L指向头结点。
(2)将头结点的指针域置空。
算法代码描述
typedef struct Lnode {
ElemType data;
struct Lnode* next;
}Lnode,*LinkList;
//(1)
status InitList(LinkList &L){
L=new LNode;//new出来的是地址,将该地址赋值给L作为指针的head,对于C来讲,需要使用malloc来申请内存空间:
//L=(LinkList)malloc(sizeof(LNode))
//(2)
L->next=NULL;
}
补充算法:
1.判断链表是否为空
链表中无元素,但是头指针和头结点仍然存在,称为空链表,所以,判断头结点指针域为空即可
int ListEmpty(LinkList L){
//若L为空表,则返回1,否则返回0
if(L->next)//非空
return 0;
else
return 1;
2.单链表的销毁:链表销毁以后将不在存在
从头指针开始,依次释放所有结点
Status Destroy_L(LinkList &L){
Lnode *p;
while(L!=NULL){
//L==NULL时结束循环
P=L;
//此时不能直接删除该结点,因为直接删除以后指针域也没了,所以要先让L往后移一位,然后再删,删完以后再将L的值赋给P,再重复这样的操作
L=L->next;
delete(P)或free(p);
}
}
3.清空链表
链表仍然存在,但链表中无元素,成为空链表(头指针和头结点仍然存在)
依次释放所有节点,并将头结点指针域设置为空
Status ClearList(LinkList &L){
//将链表L置为空表
Lnode *p,*q;
p=L->next;
while(p!=NULL){
//还有内容没到表尾
q=p->next;
delete p;
p=q;
}
L->next=NULL;//将头节点的指针域设置为空
return OK;
}
4.求单链表的表长
给定带头结点的单链表,数出来链表中由多少个元素
从第一个结点首元结点开始,每数一个计数器就+1,依次统计所有结点
int ListLength_L(LinkList L){
//返回L中数据元素
LinkList
P=L->next;//p指向第一个结点
int count=0;
while(p){
//遍历单链表,统计结点数
count++;
p=p->next;
}
return count;
}
几个重要操作:
-
p=L;//p指向头结点
-
s=L->next;//s指向首元结点
-
p=p->next;//p指向下一结点
5.链表取值 取单链表中第i个元素的内容
取出下表中第3个元素和第15个元素
从首元结点开始一个一个往后数
从链表的头指针出发,顺着链域next诸葛结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构
i=3
Status FetElem_L(LinkList L,int i,ElemType &e){
//获取线性表中的某个数据元素的内容,通过变量e返回
p=L->next;j=1;//初始化
while(p&&j<i){
//只有当j==i才表示找到了,j>i表示要找的位置不正确,例如0或者-1或超过了元素的个数,p若为空也无法找到,所以循环若要继续进行,要满足这2个条件
p=p->next;++j;//指针往后移,计数器+1
}
if(!p||j>i){
//超过元素个数||个数小于1(第i个元素不存在)
return ERROR;
}else{
e=p->data;//取第i个元素
return OK;
}
}
6.按值查找
- 按值查找:根据指定数据获取该数据所在的位置(该数据的地址)
- 按值查找:根据指定数据获取该数据所在的位置序号(是第几个数据元素)
根据指定数据获取该数据所在的位置,若存在则输出地址
步骤:
1.从第一个结点起,依次和e相比较
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或"NULL" 。
Lnode *LocateElem_L(LinkList L,Elemtype e){
int e=30;
p=L->next;
while(p!=NULL&&p->data!=e){
//p不为空且p的data域的值不为e
p=p->next; //p向后移动一位
}else{
return p;
//不管找到还是没找到都返回指针变量,找到了就是这个结点,没找到就返回NULL
}
}
根据指定数据获取该数据所在的位置,若存在则输出该数据的位置序号
int *LocateElem_L(LinkList L,Elemtype e){
int e=30;
//返回L中值为e的数据元素的位置序号,查找失败返回0
p=L->next;
i=1;
while(p!=NULL&&p->data!=e){
j++;
}
if(p!=NULL){
return j;
} else{
return 0;
}
}
7.插入操作 在第i个结点之前插入值为e的新结点
1、首先找到 ai-1的存储位置 p。
2、生成一个数据域为e的新结点s。
3、插入新结点: ① 新结点的指针域指向结点 ai
②结点 ai-1的指针域指向新结点
关键步骤:①s->next=p->next;②p->next=s;
要插入第i个结点,就需要找第i-1个结点
//在L中第i个元素之前插入数据元素e
Status Listlnsert_L(LinkList &L,int i,ElemType e){
p=L;
int j=0;//在找第i-1个元素时,j用作索引计数
while(p&&j<i-1){
//p存在且在链表长度之内
p=p->next;++j//寻找第i-1个结点,p指向i-1结点
}
if(!p||j>i-1){
//i大于表长+1或小于1,则插入位置非法
return ERROR;
}
s=new LNode;
s->data=e;//生成新结点,将结点s的数据域置为e
s->next=p->next;
p->next=s;//将结点插入L中
return OK;
}
8.删除操作 删除第i个结点
1.找到ai-1的存储位置p,若需要可以保存要删除的ai的值
2.令p->next指向ai+1
3.释放结点ai的空间
//q结点是需要删除的结点,p结点是q结点的前驱结点
Status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;p,i;
while(p->next&&j<i-1){
p=p->next; ++j; }//循环条件为:p指向的结点存在且序号在需要删除的第i-1个结点的前面
//寻找第i个结点,并令p指向其前驱
if(!(p->next)l|j>i-1) //删除位置不合理的两种情况
{
return ERROR;
}
q=p->next;
//找到p以后通过p找到q,临时保存被删结点的地址以备释放
※ p->next=q->next;(或者用p->next=p->next0->next;)
//改变删除结点前驱结点的指针域
e=q->data;
//若需要数据可保存删除结点的数据域
delete q;
//释放删除结点的空间
return OK;
}//ListDelete L
9.单链表的建立 头插法-元素插入在链表头部,也叫前插法 时间复杂度O(n)
建立单链表的时候,每次把新元素插入到链表的头部(前面)的位置:
步骤:
- 从一个空表开始,重复读入数据;
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将各结点插入到链表的前端
例如,建立链表L(a,b,c,d,e)
先插入的是最后一个元素,最后插入的是第一个元素,每次插入都是插入到链表的前面
void Create_H(LinkList &L,int n){
//首先需要建立空的头结点,就需要申请空间,并将头结点的指针域置空
L=new LNode;
L.next=NULL;//至此就建立了一个带头结点的单链表
for(i=n;i>0;i--){
//插入n个结点,循环n次
//然后创建一个新节点,继续申请空间存储新结点,将数据存入此节点的数据域,并将头结点的指针域指向该节点,然后将该结点的指针域置为NULL
p=new LNode;
cin>>p->data;//输入元素值scanf(&p->data)
p->next=L->next//完成新结点到后面一个结点的指针
L.next=p;//完成头结点到新结点的指针
}
}
单链表的建立 尾插法-元素插入在链表尾部,也叫后插法 时间复杂度O(n)
1.从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。尾指针始终指向最后一个结点
//正位序输入n个元素的值,建立带头结点的单链表L
void Create_R(LinkList &L,int n){
L=new LNode;
L->next=NULL;
r=L;//尾指针指向头结点
for(i=0;i<n;++i){
p=new LNode;
cin>>p->data;
p->next=NULL;
r->next=p;
r=p;//让尾指针指向新的尾结点
}
}
单链表的查找、插入、删除算法时间效率分析
1.查找
Lnode *LocateELem_L(LinkListL, Elemtype e) {
//在线性表L中查找值为e的数据元素
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
p=L->next;
while(p &&p->data!=e)
※ p=p->next;
return p;
}
- 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)
2.插入和删除
- 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)
- 但是,如果要在单链表中进行前插或删除操作,若不知道插入或删除的位置,就要查找到前驱结点,所耗时间复杂度为 O(n)
单循环链表
是一种头尾相接的链表,表中最后一个结点的指针域指向头节点,整个链表形成一个环
优点:从表中任一结点出发均可找到表中其他结点
- 头指针表示:
找 a1的时间复杂度:O(1),找an的时间复杂度: O(n),这样就不是很方便,所以,我们对循环单链表的操作常常是在表的首位位置上进行
- 尾指针表示:
a1的存储位置是:R->next->next,an的存储位置是:R,时间复杂度都是O(1)
循环空表的指针域里存的是头指针,即存头结点的地址的指针
怎样知道到了最后一个结点:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件
就不再像非循环链表那样判断 p 或p->next 是否为空,而是判断它们是否等于头指针。即:
案例
带尾指针循环链表的合并(将Tb合并在Ta之后)
算法代码描述 时间复杂度O(1)
LinkList Connect(LinkList Ta,LinkList Tb){
p=Ta->next;//p存表Ta的头结点
Ta->next=Tb->next->next;//Tb表头链接到Ta表尾
delete Tb->next;//释放Tb表头结点
Tb->next=p; //Tb的表尾指向Ta的表头
return Tb;//返回Tb指针即返回合并后的链表
}
双向链表
单链表的每个结点只能指向后继节点,要找前驱结点就非常麻烦,只能通过头结点一个一个往后找
单链表的结点→有指示后继的指针域→找后继结点方便;
即:查找某结点的后继结点的执行时间为O(1)
BUT:无指示前驱的指针域→找前驱结点难: 从表头出发查找。
即:查找某结点的前驱结点的执行时间为O(n)
用双向链表可以克服单链表的这种缺点
双向链表
- 双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
定义
typedef struct DulNode {
Elemtype data;
struct DulNode* prior, * next;
//2个指针域,一个指向前驱元素,一个指向后继元素
}DuLNode,*DuLinkList;
双向循环链表
和单链的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点。
双向链表结构的对称性(设置指针p指向某一结点)
p -> prior -> next = p = p -> next -> prior
- 在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为 O(n)。
双向链表的插入
算法:双向链表的插入
void ListInser_Dul(DuLinkList &L,int i,ElemType e){
//给一个指向双向链表的头结点的指针,在带头结点的双向循环链表L中第i个位置之前插入元素e
if(!p=GetElemP_DuL(L,i))//在链表L上找到第i个元素,并赋值给p,让p指向第i个结点,若i的位置不合理非法,则得到false,然后通过前面的!来执行后面的return
{
return ERROR;
}else{
s=new DuLNode;
s->date=e;//创建新结点并为之赋值
s->prior=p->prior;//完成以上步骤①
p->prior->next=s;//完成以上步骤②
s->next=p;//完成以上步骤③
p->prior=s;//完成以上步骤④
return OK;
}
}
双向链表的删除 时间复杂度O(n)
void ListDelete_DuL(DuLink &L,int i,ElemType &e){
//删除带头结点的双向循环链表L的第i个元素,并将其值用e返回
if(!(p=GetElemP_DuL(L,i))){
return ERROR;
}else{
e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return OK;
}
}
单链表、循环链表和双向链表的时间效率比较
查找表头结点(首元结点) | 查找表位结点 | 查找结点*p的前驱结点 | |
---|---|---|---|
带头结点的单链表L | L->next 时间复杂度O(1) |
从L->next依次向后遍历 时间复杂度O(n) | 通过p->next无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next 时间复杂度O(1) |
从L->next依次向后遍历 时间复杂度O(n) | 通过p-> next可以找到其前驱时间复杂度O(n) |
带头结点仅设尾指针R的循环单链表 | R->next 时间复杂度O(1) |
R时间复杂度O(1) | 通过p->next可以找到其前驱时间复杂度O(n) |
带头结点的双向循环链表L | L->next 时间复杂度O(1) |
L->prior时间复杂度O(1) | p->prior 时间复杂度O(1) |
顺序表和链表的比较
-
链式存储结构的优点:
- 结点空间可以动态申请和释放,需要再申请,不需要就释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
-
链式存储结构的缺点:
-
存储密度小,每个结点的指针域需额外占用存储空间,当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
- 存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即:
例如:
存储密度为=8/12=67%
一般的,存储密度越大,存储空间的利用率就越高,显然,顺序表的存储密度为1(100%),而链表的存储密度小于1
- 链式存储结构是非随机存储结构。对任一结点的操作都要从头指针依指针链找到该结点,这增加了算法的复杂度
-
比较表格:
线性表的应用
线性表的合并
问题描述:
假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A=AUB
若有重复元素则出现一次即可
La=(7,5,3,11) Lb=(2,6,3)→La=(7,5,3,11,2,6)
算法步骤:
依次取出Lb中的每个元素,执行以下操作:
1.在La中查找元素
2.如果找不到,则将其插入La的最后
代码实现:
void union(List &La, List Lb){
La_len=ListLength(La);
Lb_len=ListLength(Lb);//求出这两个链表的长度
for(i=1;i<=Lb_len;i++){
GetElem(Lb,i,e);//在Lb中依次取出每个元素的值
if(!LocatedElem(La,e)){
//将值在La中比较,如果不存在:
ListInsert(&La,++La_len,e);//插入到La中
}
有序表的合并
问题描述:
已知线性表La和Lb中的数据元素按值非递减有序排列,现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列。
若有重复相等的元素都会出现在新的有序表中
La=(1,7,8) Lb=(2,4,6,8,10,11) →
Lc=(1,2,4,6,7,8,8,10,11)
算法步骤:
(1)创建一个空表Lc
(2)依次从 La 或Lb中“摘取”元素值较小的结点插入到 Lc 表的最后,直至其中一个表变空为止
(3)继续将 La 或 Lb其中一个表的剩余结点插入在 Lc 表的最后
用顺序表实现
![image-20220711193527364](https://i-blog.csdnimg.cn/blog_migrate/558e74ec0040dd584e1c9a34b491cbb9.png)
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
pa=LA.elem;
pb=LB.elem;
//指针pa和pb的初值分别指向两个表的第一个元素
LC.length=LA.length+LB.length;
//新表长度为待合并两表的长度之和
LC.elem=new ElemType[LC.length];
//为合并后的新表分配一个数组空间
pc=LC.elem;
//指针pc指向新表的第一个元素
pa_last=LA.elem+LA.length-1;
//指针pa_last指向LA表的最后一个元素
pb_last=LB.elem+LB.length-1;
//指针pb_last指向LB表的最后一个元素
while(pa<=pa_last && pb<=pb_last){
//两个表都非空才能进行比较
if(*pa<=*pb){
*pc++=*pa++;
} else{
*pc++=*pb++;
}
//依次取两表中值较小的结点,如果pa中的较小,那就把pa中的该元素加入到pc中,然后这定位这两个顺序表的指针都要往后面移一位,否则就是pb和pc的指针往后移一位
}
//当某一个表空掉以后,另一个表还会有剩余元素,此时需要对剩余的元素进行比较将其加到pc里面去
while(pa<=pa_last){
*pc++=*pa++;
}//LB表已到达表尾,将LA中剩余元素加入LC
while{
(pb<=pb_last)*pc++=*pb++;
}//LA表已到达表尾,将LB中剩余元素加入LC
}
算法的时间复杂度是: O(ListLength(La)+ListLength(Lb))
算法的空间复杂度是: O(ListLength(La)+ListLength(Lb))
用链表实现
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
pa=La->next; pb=Lb->next;
pc=Lc=La;
//用La的头结点作为Lc的头结点,pc指针指向该头结点
while( pa && pb){
//都不为空
if(pa->data<=pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}else {
pc->next=pb;
pc=pb;
pb=pb->next;}
}
pc->next=pa?pa:pb;//插入剩余段
delete Lb;
//释放Lb的头结点
算法的时间复杂度:O(ListLength(La)+ListLength(Lb))
算法的空间复杂度:O(1)
案例:
一元多项式的运算:实现两个多项式相减乘运算
稀疏多项式的运算
稀疏多项式非零项的数组表示:
实现:
线性表A =((7,0),(3,1),(9,8),(5,17))
线性表B =((8,1), (22,7),(-9,8))
-
创建一个新数组c
-
分别从头遍历比较a和b的每一项
-
指数相同,对应系数相加,若其和不为零,则在c中增加—个新项
-
指数不同,则将指数较小的项复制到c中
-
-
一个多项式已遍历完毕时,将另一个剩余项依次复制到c中即可
但是数组c多大合适?这就是顺序存储结构存在的问题:
- 存储空间分配不灵活
- 运算的空间复杂度高
所以用链式存储结构:
typedef struct PNode{
float coef;//系数
int expn;//指数
struct PNode *next;//指针域
}PMode,*Polynomial;
首先分配头结点,然后再插入,可以用头插法也可以用尾插法,此处尾插法更适合,因为会顺序插入
思路
1.创建一个只有头结点的空链表。
2.根据多项式的项的个数n,循环n次执行以下操作:
①生成一个新结点 * s;
② 输入多项式当前项的系数和指数赋给新结点* s的数据域;
③设置一前驱指针pre,用于指向待找到的第一个大于输入项指数的结点的前驱,pre初值指向头结点;
④指针q初始化,指向首元结点;
⑤循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点* q;
⑥将输入项结点* s插入到结点* q之前。
算法步骤:
①指针p1和p2初始化,分别指向Pa和Pb的首元结点
② p3指向和多项式的当前结点,初值为Pa的头结点
③当指针p1和p2均未到达相应表尾时,则循环比较p1和p2所指结点对应的指数值
(p1->expn与p2->expn),有下列3种情况:
当p1->expn==p2->expn时,则将两个结点中的系数相加
若和不为零,则修改p1所指结点的系数值,同时删除p2所指结点
若和为零,则删除p1和p2所指结点;
当p1->expn< p2->expn时,则应摘取p1所指结点插入到“和多项式”链表中去;
当p1->expn>p2->expn时,则应摘取p2所指结点插入到“和多项式”链表中去。
④将非空多项式的剩余段插入到p3所指结点之后。
⑤释放Pb的头结点。
代码实现
void MergeList_Sq(SqList LA, SqList LB, SqList& LC) {
pa = LA.elem;
pb = LB.elem;
//指针pa和pb的初值分别指向两个表的第一个元素
LC.length = LA.length + LB.length;
//新表长度为待合并两表的长度之和
LC.elem = new ElemType[LC.length];
//为合并后的新表分配一个数组空间
pc = LC.elem;
//指针pc指向新表的第一个元素
pa_last = LA.elem + LA.length - 1;
//指针pa_last指向LA表的最后一个元素
pb_last = LB.elem + LB.length - 1;
//指针pb_last指向LB表的最后一个元素