数据结构与算法
1. 逻辑结构
- 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。
- 线性结构:线性结构中的数据元素存在一对一的关系
- 树形结构:树形结构中的数据元素之间存在一种一对多的关系
- 图形结构:数据元素是多对多的关系
2. 物理结构
- 顺序存储:是把数据元素放在地址连续的存储单元里,其数据间的逻辑关系核物理关系是一致的
- 链式存储结构:是把数据元素放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。
算法特性
- 输入:算法具有零个或多个输入
- 输出:算法至少有一个或多个输出
- 有穷性:算法执行完后,自动结束而不会无限循环
- 确定性:每一步都具有确定的含义,不会出现二义性;算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果;算法的每个步骤都应该被精确定义而无歧义
- 可行性:每一步都必须是可行的,也就是每一步都能通过执行有限次数完成。
算法设计要求
- 正确性
- 算法的正确性至少是具有输入、输出和加工处理无歧义性、能反映问题的要求、能得到问题的正确答案。
- 大体分为以下四个层次:
- 算法程序没有语法错误
- 算法程序对于合法的输入能够产生满足要求的输出
- 算法程序对于非法输入能够产生满足规格的说明
- 算法程序对于故意刁难的测试输入都有满足要求的输出结果
- 可读性
- 算法设计零一目的是为了便于阅读、理解和交流。
- 健壮性
- 当输入数据不合法时,算法也能做出相关处理,而不是产生异常、崩溃或莫名其妙的结果
- 时间效率高和存储量低
算法效率的度量方法
影响因素
- 算法采用的策略,方案
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
定义
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。他比奥斯随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
说明
- 用O()来体现算法的时间复杂度的记法,称之为大O记法
- 一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法
推导O阶方法
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高项。
- 如果最高阶项存在且不是1,则出去与这个项相乘的数。
- 得到的最后结果就是大O阶。
常见阶数
- 常数阶:所有加法常数都是O(1)
- 线性阶:一般含有非嵌套循环,线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。
- 平方阶:循环时间复杂度等于循环体的复杂度乘以该循环运行的次数
- 对数阶: 由于每次i*2之后,就距离n更近一步,假设有x个2相乘后大于或等于n,则会退出循环
算法的空间复杂度
定义:算法的空间复杂度通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数
- 通常,我们都是用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求
- 当直接让我们求“复杂度”时,通常指的是时间复杂度。
线性表
定义
由零个或多个数据元素组成的有限序列
说明
- 首先是一个序列,也就是元素之间要求先来后到。
- 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继。
- 另外,线性表强调是有限的。
数据类型
定义
是一组性质相同的值的集合及定义在此集合上的一些操作的总称
分类
- 原子类型:不可以再分解的基本类型,如整型、浮点型、字符型等
- 结构类型:由若干个类型组合而成,是可以再分解的,例如整型数组是由若干整形数据组成的
抽象数据类型
-
定义:是指一个数学模型及定义在该模型上的一组操作
-
数据类型抽象:是指抽取出事物具有的普遍性的本质。他要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,隐藏了繁杂的细节。
-
说明
- 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
- 抽象的意义在于数据类型的数学抽象类型
-
格式:
ADT //抽象数据类型名 Data //数据元素之间逻辑关系的定义 Operation //操作 endADT
线性表的抽象数据类型
-
抽象数据类型定义
ADT 线性表 Data 数据对象(每个元素的类型均为DataType) Operation Initlist(*L):初始化操作,建立一个空的线性表L ListEmpty(L):判断线性表是否为空表,若线性表为空,返回true,否则返回false ClearList(*L):将线性表清空 GetElem(L,i,*e):将线性表中的第i个未知元素值返回给e LOcateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则返回0表示失败 ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e ListDelete(*L,i,*e):删除线性表中第i个位置元素,并用e返回其值 ListLength(L):返回线性表L的元素个数 endADT
-
对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合实现。
-
线性表实现集合的并集
void unionL(List *La, list Lb) int La_len,Lb_len,i ; ElemType e; La_len = ListLength(*La); Lb_len = ListLength(lb); for(i=1; i<=Lb_len; i++){ GetElem(Lb,i,&e); if( !LocateElem(La,++La_len,e); } }
线性表的顺序存储结构
顺序存储结构
-
物理上的存储方式实际上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把相同的数据类型的数据元素依次放在这块空地中
-
结构代码
#define MAXSIZE 20 typedef int ElemType; typedef struct { ElenmType data[MAXSIZE]; int length; //线性表长度 }SqList;
ElemType是通用数据类型这里是整型
-
顺序存储结构封装的三个属性
- 存储空间的起始位置,数组data,他的存储位置就是线性表存储空间的存储位置
- 线性表的最大存储容量:数组长度MaxSize
- 线性表的当前长度
注意:数组的长度与线性表的当前长度需要区分:数组长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会变化的。
-
地址计算方法:LOC(ai)=LOC(a1)+(i-1)*c
备注:可以随时计算出线性表中任意位置的地址,所以时间相同,存储时间性能都为O(1),我们通常称为随机存储结构
GetElem实现
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//Status是函数的类型,其值是函数结果状态代码,如OK等
//初始条件:顺序线性表L已存在,1 <=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(SqList L,int i, ElemType *e){
if(L.length==0 || i<1 || i>L.length){
return ERROR;
}
*e = L.data(i-1);
return OK;
}
注意:返回值类型Status是一个整型,约定返回1代表OK,返回0代表ERROR
插入操作
1. 插入算法的思路
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于数组长度,则抛出异常或动态增加数组容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 将要插入元素填入位置i处
- 线性表长+1
2. 代码
Status ListInsert(SqList *L,int i, ElemType e)
{
int k;
if(L->length == MAXSIZE)//顺序线性表已经满了
{
return ERROR;
}
if(i<1 || i>L->lengh+1)//当i不在范围内时
{
return ERROR;
}
if(i <= L->length)//若插入数据位置不在表尾
{
//将要插入位置后的数据元素向后移动一位
for( k=L->length-1; k>=i-1;k--)
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] =e;//将新元素插入
L->length++;
return OK;
}
删除操作
1.算法思想
- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除位置开始便利到最后一个元素位置,分别将它们都向前移动一个位置
- 表长-1
2. 实现代码
Status ListDelete(Sqlist *L,int i,ElemType *e)
{
int k;
if(L->length ==0){
return ERROR;
}
if( i<1 || i>L->length+1)//如果删除元素位置超出数组范围,返回ERROR
{
return ERROR;
}
*e = L->data[i-1];
if(i < L->length){
for(k=i ; k<L->length ; k++){
L->data[k-1]=L->data[k];
}
}
L->length--;
return OK;
}
线性表特点总结
- 读取时间复杂度为O(1);插入或删除时,时间复杂度为O(n)
- 适合元素个数比较稳定,不经常插入和删除元素,存取较多的应用
优点
- 无需为表示元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
缺点
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 容易造成存储空间的“碎片”
链式存储结构
定义
- 把存储数据元素信息的域称为数据域
- 存储直接后继位置的域称为指针域
- 指针域中存储的信息称为指针或链
- 两部分信息组成的数据元素称为存储映像,或称为结点(Node)
- n个节点链接成一个链表,即为线性表的链式存储结构
- 因为此链表的每个结点中只包含一个指针域,所以叫做单链表
- 链表中的第一个结点的存储位置叫做头指针,最后一个指针为空
头指针与头结点的异同
头指针
- 是指链表只想的第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
头结点
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以存放链表的长度)
- 有了头结点,对在第一元素的结点前插入结点和删除第一结点其操作与其他结点的操作就统一了
- 头结点不一定是链表的必须要素
线性表的单链表存储结构
C语言实现
typedef struct Node
{
ElemType data; //数据域
struct Node* Next; //指针域
}Node;
typedef struct Node* LinkList;
单链表的读取
-
思路
- 声明一个结点p指向链表第一个结点,初始化j从1开始
- 当j>i时,就遍历链表,让p的指针向后移动,不断只想下一个结点,j+1;
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,返回结点p的数据
-
代码实现
Status GetElem (LinkList L,int i,ElemType *e) { int j; LinkList p; p = L->next; j = 1; while(p && j<i) { return ERROR; } *e = p->data; return OK; }
单链表的插入
1.思路
- 声明一结点p指向链表头结点,初始化j从1开始
- 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 若则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给s->data
- 单链表的插入刚才两个标准语句
- 返回成功
2.代码实现
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while( p && j<i)//用于寻找i结点
{
p = p->next;
j++;
}
if( !p || j>i)
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;//不能调换顺序
p->next = s;
return OK;
}
单链表的删除
1. 算法思路
- 声明结点p指向链表第一个结点,初始化j=1
- 当 j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 若查找成功,将欲删除结点p->next赋值给q
- 单链表的删除标准语句p->next = q->next
- 将q结点中的数据赋值给e,作为返回
- 释放q结点
2.代码实现
Status ListDelete (LinkList *L,int i, ElemType *e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next && j<i)
{
p=p->next;
++j;
}
if(!(p->next) || j>i)
{
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
单链表的整表创建
- 与顺序存储结构不同,数据分散在内存的各个角落,增长是动态的
- 对于每个链表,占用空间的大小和位置是不需要预先分配划定的,可以即时生成
- 创建单链表是一个动态生成链表的过程,从“空表”的初始状态起,依次建立各元素结点并插入链表
算法思路
- 声明一结点p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个人带头结点的单链表
- 循环实现后继结点的赋值和插入
头插法
-
从一个空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束为止
-
简单说,就是把新加进的元素放在表头后的第一个位置:
- 先让新结点的next指向头结点之后
- 然后让表头的next指向新结点
-
简单说就是现实中插队的现象
void CreaatListHead(LinkList *L,int n) { LinkList p; int i; srand(time(0));//初始化随机种子 *L = (LinkList)malloc(sizeof(Node)); (*L)->next = NULL; for( i=0; i<n; i++) { p = (LinkList)malloc(sizeeof(Node)); p->data = rand()%100+1; p->next = (*L)->next; (*L)->next = p; } }
尾插法
-
把新结点插入到最后
void CreateListTail(LinkList *L,int n) { LinkList p,r; int i; srand(time(0)); *L = (LinkList)malloc(sizeof(Node)); r = *L; for( i=0;i<n;i++) { p=(Node *)malloc(sizeof(Node)); p->data = rand()%100+1; r->next = p; r = p; } r->next = NULL; }
单链表的整表删除
- 当不再使用时,需要整表销毁释放内存
算法思路
-
声明结点p和q
-
将第一个结点赋值给p,下一结点赋值给q
-
循环执行释放p和将q赋值给p的操作
Status ClearList(LinkList *L) { LinkList p,q; p = (*L)->next; while(p) { q = p->next; free(p); p=q; } (*L)->next = NULL; return OK; }
单链表结构与顺序存储结构优缺点
存储分配方式
- 顺序存储结构一般用一段连续的存储单元一次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
- 时间性能
- 查找
- 顺序结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
- 查找
- 空间性能
- 顺序存储结构需要分配存储空间,大了空间浪费,笑了,容易溢出
- 单链表不用分配存储空间,只要有就可以分配,元素个数也不受限制
线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构
需要频繁插入和删除时,宜采用单链表结构
静态链表
数组实现
- 数组实现的链表又叫做静态链表,这种描述方法叫做游标实现法
代码实现
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
静态链表的初始化
Status InitList(StatusLinkList space)
{
int i;
for( i=0;i<MAXSZIZE-1;i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0;
return OK;
}
总结
- 对数组的第一个和最后一个元素特殊处理,他们的data不存放数据
- 通常把为使用的数组元素称为备用链表
- 数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标
- 数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用