绪论及线性表
1. 绪论(非重点)
1.1 概念
数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。
一个数据元素可由多个数据项组成,数据项是构成数据元素的不可分割的最小单位。
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
逻辑结构 数据元素之间的逻辑关系是什么?
集合: 各个元素同属于一个集合,别无其它关系。
线性结构: 数据元素之间是一对一的关系。除了第一个元素,所有元素都有唯一前驱;除了最后一个元素,所有元素都有唯一后驱。
树形结构: 数据之间是一对多的关系。
图结构: 数据元素之间是多对多的关系
数据的物理结构(存储结构) 如何用计算机表示数据元素的逻辑关系?
顺序存储: 把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
链式存储: 逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
索引存储: 在存储元素信息的同时,还建立附加的索引表。索引表当中每项称为索引项,索引项的一般形式为(关键词,地址)
散列存储: 根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。
总结:
1、若采用顺序存储,则各个元素在物理上必须是连续的;若采用非顺序,则各个数据元素在物理上可以是离散的。
2、数据的存储结构会影响存储空间分配的方便程度。
3、数据的存储结构会影响对数据运算的速度。
数据的运算: 施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
数据类型 是一个值的集合和定义在此集合上的一组操作的总称。
1)原子类型。其值不可再分的数据类型
2)结构类型。其值可以再分解为若干成分(分量)的数据类型
抽象数据类型(Abstract Data Type , ADT)是抽象数据组织及与之相关的操作。
1.2 算法
什么是算法? 程序 = 数据结构 + 算法
算法的特性:
有穷性: 一个算法必须总在执行有穷步之后结束,且每一步都在有穷时间内完成。
注:算法必须是有穷的,而程序可以是无穷的
确定性: 算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
可行性: 算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入: 一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
输出: 一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。
“好”算法的特质:
1)正确性。算法应能够正确的解决求解问题。
2)可读性。算法应具有良好的可读性,以帮助人们理解。
3)健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名奇妙的输出结构。
4)高效率与低存储量的需求
算法效率的度量:
时间复杂度: 事先预估算法时间开销T(n)与问题规模n的关系(T表示“time”)
空间复杂度:
2. 线性表
2.1. 线性表的基本概念
线性表 是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。若用L命
名线性表,则其一般表示为L = (a1, a2, … , ai, ai+1, … , an)
几个概念:
ai是线性表中的“第i个”元素线性表中的位序,a1是表头元素;an是表尾元素。位序从1开始数组下标从0开始
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继
线性表的基本操作:
InitList(&L): 初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L): 销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
ListInsert(&L,i,e): 插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i,&e): 删除操作。删除表L中的第i个位置的元素,并使用e返回元素的值。
LocateElem(L,e): 按值查找操作。在表L中查找具有给定关键词值的元素。
GetElem(L,i): 按位查找。获取表L中第i个位置的元素的值。
其他常用操作:
Length(L): 求表长。返回线性表的长度,即L中的数据元素的个数。
PrintList(L): 输出操作。求前后顺序输出线性表L的所有元素值。
Empty(L): 判空操作。若L为空表,则返回true,否则返回false。
Tips
1)对数据的操作(记忆思路)–创销、增删改查
2)C语言函数的定义 --<返回值类型> 函数名(<参数1类型> 参数1,<参数2类型> 参数2,…)
3)实际开发中,可根据实际需求定义其他的基本操作
4)函数名和参数的形式、命名都可改变
5)什么时候要传入引用“&” – 对参数的修改结果需要“带回来”
2.2 线性表的实现
2.2.1 线性表的顺序表示
顺序表:用顺序存储的方式实现线性表顺序存储。 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来实现。
静态分配
动态分配
顺序表的特点
1)随机访问,即可以在O(1)时间内找到第i个元素。
2)存储密度高,每个节点只存储数据元素。
3)拓展容量不方便(即使采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
4)插入、删除操作不方便,需要移动大量元素
插入
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
时间复杂度:
删除
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
时间复杂度:
最好情况: 删除表尾元素,不需要移动其他元素。i = n,循环0次;最好时间复杂度= O(1)
最坏情况: 删除表头元素,需要将后续的n-1个元素全都向前移动。i = 1,循环n-1次;最坏时间复杂度= O(n);
顺序表的查找
按值查找:
按位查找:
时间复杂度:O(1)
课后代码题:
1.从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空则显示错误信息并退出运行。
bool Del_min(sqList &L,Elemtype &value){
if(L.length == 0)
return false;
value = L.data[0];
int pos = 0;
for(int i = 1;i<L.length;i++)
if(value > L.data(i)){
value = L.data[i];
pos = i;
}
L.data[pos] = L.data[L.length-1];
L.length--;
return true;
}
2.设置一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1).
void reverse(sqList &L){
ElemType temp;
for(int i =0;i< L.length/2;i++){
temp=L.data[i];
L.data[i]=L.data[L.length-i-1];
L.data[L.length-i-1]=temp;
}
}
2.2.2 线性表的链式表示
单链表
单链表的定义
要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个节点。
LNode *L; // 声明一个指向单链表第一个结点的指针
或:LinkList L; // 声明一个指向单链表第一个结点的指针 代码可读性更高
单链表的插入与删除
ListInsert(&L,i,e):插入操作。在表L中的第i个位置插入指定元素e
不带头节点
指定结点的后插操作
时间复杂度:O(1)
指定结点的前插操作
时间复杂度:O(n)
时间复杂度:O(1)
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。最坏、平均时间复杂度:O(n);最好时间复杂度:O(1)
*bool DeleteNode(LNode p): 指定节点的删除。 时间复杂度:O(1)
单链表的查找
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。平均时间复杂度:O(n)
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。平均时间复杂度:O(n)
int Length(LinkList L):求表的长度。时间复杂度:O(n)
单链表的建立
尾插法建立单链表:时间复杂度:O(n)
LinkList List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设ElemType为整型
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)
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode)); //创建头结点
L->next=NULL; //初始为空链表
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode*)malloc(sizeof(LNode)); //创建新结点
s->data=x;
s->next=L->next;
L->next=s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
双链表
双链表的初始化
定义:
双链表的插入
双链表的删除
双链表的遍历
循环链表
循环单链表
从头结点找到尾部,时间复杂度为O(n);从尾部找到头部,时间复杂度为O(1)
循环双链表
静态链表
静态链表:用数组的方式实现的链表
优点:增、删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变
适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
插入位序为i的结点:
①找到一个空的结点,存入数据元素
②从头结点出发找到位序为i-1的结点
③修改新结点的next
④修改i-1号结点的next
删除某个结点:
①从头结点出发找到前驱结点
②修改前驱结点的游标
③被删除结点next设为-2
2.3顺序结构与链表对比
逻辑结构
顺序表和链表的逻辑结构都是线性结构,都属于线性表。
存储结构
顺序表采用顺序存储:
优点:支持随机存取、存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表采用链式存储:
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
基本操作
由于采用不同的存储方式实现,因此基本操作的实现效率也不同。
当初始化时…;
顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展。
当销毁时:
顺序表:修改Length = 0;静态分配:静态数组 系统自动回收空间;动态分配:动态数组(malloc、free)需要手动free
链表:依次删除各个结点(free)
当插入或删除一个数据元素时…;
顺序表:插入/删除元素要将后续元素都后移/前移;时间复杂度O(n),时间开销主要来自移动元素,若数据元素很大,则
移动的时间代价很高。
链表:插入/删除元素只需修改指针即可,时间复杂度O(n),时间开销主要来自查找目标元素。查找元素的时间代价更低。
当查找一个数据元素时…
顺序表:按位查找:O(1),按值查找:O(n),若表内元素有序,可在O(log2n)时间内找到。
链表:按位查找:O(n),按值查找:O(n)
表长难以预估、经常要增加/删除元素——链表
表长可预估、查询(搜索)操作较多——顺序表