数据结构
- 用程序代码把现实世界的问题信息化
- 用计算机高效处理这些信息,从而创造价值
数据:
数据是信息的载体,是描述客观事物属性的数,字符及所有能输出到计算机中不能被计算机程序识别和处理(0和1二进制)的符号集合
数据元素,数据项
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。
一个数据元素可以由若干数据项组成,数据项是构成数据元素不可分割的最小单位
例:
数据结构,数据对象
数据结构:相互之间存在一种或多种特定关系的数据元素的集合
数据对象:具有相同性质的数据元素的集合,是数据的一个子集
例:
数据结构:某个特定门店的排队顾客信息和它们之间的关系
数据对象:全国所有门店的排队顾客信息的集合
数据结构的三要素:
数据的逻辑结构–元素之间的逻辑关系是什么?
- 集合:各个元素同属于一个集合,无其他关系
- 线性结构:数据元素之间是一对一的关系,除了第一个元素,所有元素都有唯一前驱,除了最后一个元素,所有元素都有唯一后继
- 树形结构:数据元素之间是一对多的关系
- 图结构:数据元素之间是多对多的关系(微信好友)
数据的物理结构–如何用计算机表示数据元素的逻辑关系
为什么顺序存储查找快,链式存储查找慢?
因为顺序存储向排队,按照对应位置就是对应的元素,在物理存储中是一片连续的空间,而链式存储只有元素知道下一个元素所在的位置,要顺序访问每一个前驱
数据的运算–施加在数据上的运算,包括运算的定义和实现,运算的定义是针对逻辑结构的,运算的实现是针对存储结构,指出运算的具体操作步骤。
数据类型,抽象数据类型
数据类型是一个值得集合和定义在此集合上的一组操作的总称
- 原子类型:其值不可再分的数据类型
例;
boolean类型类型
值的范围: true,false
可进行操作:与或非 - 结构类型:其值可以在分解为若干成分(分量)的数据类型
例:
typedef struct Point
{
int x;
int y;
struct Point* next;
} Point;
抽象数据类型:抽象数据组织及与之相关的操作
ADT:用数学化的语言定义数据的逻辑结构,定义运算,与具体的实现无关
算法
算法定义:
如何处理数据,以解决实际问题
算法的特性
- 有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成
注:算法必须是有穷的,而程序可以是无穷的(排队系统就是一个程序,可以不停歇) - 确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出
- 可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
- 输入:一个算法有零个或多个输入,这些输入取自于摸个特定的对象的集合
- 输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量
好算法的特点
- 正确性:算法应能够正确解决求解问题
- 可读性:算法应具有良好的可读性,帮助人们理解
- 健壮性:输入非法数据时,算法能适当地作出反应或进行处理,不会产生莫名其妙的输出
- 高效率和低存储量的需求(高效率:执行速度快,时间复杂度低)(低存储量:空间复杂度低)
时间复杂度
常数时间的操作:
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫常数操作,时间复杂度为一个算法流程中常数操作数量一个指标
n个数的排序算法
- 先从0到n-1位找最小的数,需要查找n次(数组寻址),每次需要比较n-1次,1次交换操作,所以整个算法的常数操量可以写为an^2+bn+c (时间复杂度为O(n^2))
注意:数组的寻址操作是和数据量无关的,可以是常数项操作,集合是链表,其遍历操作是和数据量有关的,不是常数操作
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下实际运行的时间,叫“常数项时间”
额外空间复杂度:一个算法在运行过程中临时占用存储空间大小的量度,即,需要多少额外的磁盘空间来维持整个算法的运行
算法1:
void loveYou(int n){//n为问题的规模
int i=1;
while(i<=n){
i++;
printf("%d\n",i);
}
printf("%d",n);
}
单循环:时间开销与问题规模n的关系–>T(n)=3n+3=O(n)
结论1:顺序执行的代码只会影响常数项,可以忽略
结论2:只需找循环中的一个基本操作分析其只需次数与n的关系即可
算法2:
void loveYou(int n){//n为问题的规模
int i=1;
while(i<=n){//此处需要执行n+1次,因为最好还要比较i=n+1时的情况
i++;
printf("%d\n",i);
for(int j=1;j<=n;j++){
printf("%d",j);
}
}
printf("%d",n);
}
时间开销与问题规模n的关系:
T(n)=O(n)+O(n^2) = O(n^2)
结论3:如果有多层嵌套循环只需要关注最深层循环了几次即可。
算法3
void loveYou(int n){//n为问题的规模
int i=1;
while(i<=n){
i*=2;//每次翻倍 i=2,4,6
printf("%d\n",i);
}
printf("%d",n);
}
计算上述算法的时间复杂度T(n):
设最深层循环的语句频度(总循环的次数)为x,则有循环条件可知,循环结束时刚好满足2^x>n
x=log2^n+1
T(n)=O(x)=O(log2^n)
void loveYou(int flag[],int n){//n为问题的规模
for(int i=0;i<n;i++){//从第一个元素开始查找
if(flag[i]==n){//找到元素n
printf("%d\n",n);//flag数组中乱序存放1~n这些数
break;
}
}
}
计算上述算法的时间复杂度T(n)
最好情况:元素n在第一个位置 --时间复杂度为T(n)=O(1)
最坏情况:元素n在最后一个位置 --最坏时间复杂度T(n)=O(n)
平均情况:假设元素n在任意一个位置的概率相同为1/n --平均时间复杂度T(n)=O(n)
循环次数x=(1+2+3+4+…+n)1/n=(1+n)/2
空间复杂度
算法1
void loveYou(int n){//n为问题的规模
int i=1;
while(i<=n){
i++;
printf("%d\n",i);
}
printf("%d",n);
}
无论问题规模怎么变,算法运行所需的内存空间都是固定的常量,算法空间复杂度为
S(n)=O(1)
算法原地工作–算法所需内存空间为常量
算法2:
void test(int n){
int flag[n];//声明一个长度为n的数组
int i;
}
假设在该计算机中一个int变量占4b
则所需的内存空间为4+4n+4=4n+8
S(n)=O(n)
算法3
void test(int n){
int flag[n][n];//声明一个长度为n*n的数组
int i;
}
二维数组所需的空间是4n^2
S(n)=O(n^2)
算法4
void loveYou(int n){
int a,b,c;//声明一系列局部变量
if(n>1){
loveYou(n-1);
}
printf("%d\n",n);
}
空间复杂度=递归调用深度
S(n)=O(n)
函数的递归调用,空间复杂度为44n B
线性表
(在考虑数据的逻辑结构的时候不用考虑数据元素的类型,用圆圈代替每个元素,例如,线性表,一张表每一行都是一个数据元素,o-o-o-o-o-o-o-o这样的逻辑结构,而具体实现这种逻辑的物理结构可以是链表也可以是顺序表)
定义(逻辑结构):线性表是具有相同(每个数据元素所占空间一样大)数据类型的n(n>=0)个数据元素的有限序列(有次序),其中n为表长,的那个n=0时线性表是一个空表。若用L命名线性表,一般表示为L=(a1,a2,…,an)
ai是线性表中的“第i个”元素,线性表中的位序"位序是从1开始的,而数组下标是从0开始的"
基本操作(运算)
a1是表头元素,an是表尾元素
除第一个元素外,每个元素有且仅有一个直接前驱,除了最后一个元素外,每个元素有且仅有一个直接后继
什么时候需要传入引用类型的参数:当需要返回修改后的参数,则传入引用类型(类似java中的数组)
顺序表
顺序表–用顺序存储的方式实现线性表顺序存储,把逻辑上相邻的元素存储在物理空间上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现
假使线性表第一个元素存放位置是LOC(L),则第二个元素存放的位置是LOC(L)+数据元素的大小
如何知道一个数据元素大小:c语言中有一个方法sizeof(ElemeType)
顺序表的实现–静态分配
在内存中所占的大小为sizeof(int)*MaxSize,第i个元素位置为,第一个元素位置+sizeof(int)*i
#include <stdio.h>
#define MaxSize 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;
}
| 仅c++支持此种写法
顺序表实现–动态分配
顺序表的特点
- 随机访问,即:可以在O(1)时间内找到第i个元素。(代码实现:data[i-1];静态分配,动态分配都一样)
- 存储密度高,每个节点只存储数据元素(链式存储需要存放指针)
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量的元素
顺序表的插入操作
对插入数据进行合法性判断
插入操作的时间复杂度
顺序表的删除元素
顺序表删除数据元素的时间复杂度
顺序表的查找
静态分配方式:
typedef struct{
ElemType data[MaxSize];//用静态的“数组”存放数据元素
int length;//顺序表当前长度
}SqList;//顺序表的类型定义(静态分配方式)
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
动态分配的方式:
#define InitSize 10//顺序表的初始长度
typedef struct{
ElemeType *data;//指示动态分配数组的指针
int MaxSize;//顺序表的最大容量
int lengthl//顺序表的当前长度
}SqList;//顺序表的类型定义(动态分配方式)
ElemType GetElem(SqList L,int i){
return L.data[i-1];
}
时间复杂度为O(1)
按值查找位序
#define InitSize 10//顺序表的初始长度
typedef struct{
ElemeType *data;//指示动态分配数组的指针
int MaxSize;//顺序表的最大容量
int lengthl//顺序表的当前长度
}SqList;//顺序表的类型定义(动态分配方式)
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
ElemType LocalElem(SqList L,int i){
for (int i=0;i<L.length;i++)
if(L.data[i]=e)
return i+1;
return 0
}
最好情况:第一个元素就是查找的元素O(1)
最好情况:O(n)
平均情况:1/n*1+…