持续更新中……
前言:由于课程安排,得用搁了一年没用过的C语言开始完成数据结构作业~希望将自己摸索路上的思考记载下来并分享给大家,也希望各位大佬能批评指正,后续所有代码也会传到GitHub上
绪论
- 数据结构的基本概念,研究对象
- 抽象数据型(ADT)
- 算法复杂性
- 逐步求精方法
简要介绍历史:
数据结构创始人——Donald.E.Knuth
31岁开始出版历史性经典巨著:The Art of Computer Programming
计划共写7卷,然而出版三卷之后便已震惊世界,获得图灵奖(36岁)
简要介绍数据结构重点:
如果将学习程序设计语言比作识字的过程,那么学习数据结构则是相当于写作文的过程,再进一步的算法设计与分析就相当于写文章了。
1.1数据结构的兴起和发展
数据结构随着程序设计的发展而发展
- 无结构阶段:在简单数据上作复杂计算
- 结构化结构:数据结构+算法=程序
- 面向对象阶段:(对象+行为)= 程序
- ……
1.2研究对象与基本概念
数据结构有如下基本概念:
- 数据项:构成数据元素的不可分割的最小单位
- 数据对象:具有相同性质的数据元素的集合
- 结点:数据元素在计算机内的威串表示
- 域(字段):数据元素中数据项在计算机内的表示
姓名 | 语文 | 数学 |
---|---|---|
张三 | 85 | 54 |
李四 | 92 | 87 |
数据元素:单个学生的成绩(表中的一行)
数据项:某门成绩(表中的一格)
数据结构:根据各种不同的数据集合和数据元素之间的关系,研究如何表示、存储和操作(查找、插入、删除、修改、排序)这些数据
通常可以用一个二元组<D,R>来表示或写成DS=<D,R>
其中D是数据集合(数据对象),R是D中数据元素之间所存在的关系的有限集合
数据结构按照视角的不同,可以分为逻辑结构和存储结构
数据的逻辑结构是从具体问题抽象出来的人为定义的数据模型
数据的存储结构是计算机分配内存,存储结点和结点关系
逻辑结构包含如下四类(以后重点学习的方向):
- 集合::数据元素之间就是“属于同一个集合”
- 数据线性结构:数据元素之间存在着一对一的线性关系
- 树型结构:数据元素之间存在着一对多的层次关系
- 图型结构:数据元素之间存在着多对多的任意关系
存储结构包含如下两种基本存储结构
- 顺序存储结构(逻辑关系用存储位置(例如数组连续存储))
- 链接存储结构(逻辑关系用指针)
简单而言,数据的逻辑结构属于用户视图,存储结构属于计算机视图
1.3抽象数据类型
抽象数据类型(A bstract D ata T ype)
定义:一个数学模型和在该模型上定义的操作集合的总称
- ADT是程序设计语言中数据类型概念的进一步推广和进一步抽象(不指定具体编程语言,不指定参数具体类型,一切都抽象)
- 同一数学模型上定义不同的操作集,则表示代表不同的ADT
- 例如: A D T i n t = ( { x ∣ x ∈ Z } , { + , − , ∗ , / , % , ⩽ , = = } ) ADT\,int =(\{x|x \in Z\},\{+,-,*,/,\%,\leqslant,==\}) ADTint=({x∣x∈Z},{+,−,∗,/,%,⩽,==})
区别数据类型、数据结构和ADT
不同含义:
- 数据类型是一组值的集合
- 数据结构则是数据元素之间的抽象关系
- 抽象数据型是一个数学模型及在该模型上定义的操作集的总称
相互关系:
- 数据结构是抽象数据型(ADT)中数学模型的表示
- ADT是数据类型的进一步推广和进一步抽象
1.4算法及算法分析
算法(Algorithm):是对特定问题 求解步骤的一种描述,是指令的有限序列
算法的五大特性:
- 输入:一个算法有零个或多个输入
- 输出:一个算法有一个或多个输出
- 有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成
- 确定性:算法中的每一条指令必须有确切的含义,对于相同的输入只能得到相同的输出
- 可行性:算法描述的操作可以通过已经实现的基本操作执行有限次来实现
算法分析::对算法所需计算机资源—时间和空间进行估算
- 时间复杂性(Time Complexity)
- 空间复杂性(Space Complexity)
算法分析——时间复杂度分析
算法的执行时间是基本(操作)语言重复操作执行的次数,它是问题规模的一个函数。将这个函数的渐近阶称为该算法的时间复杂度
for (int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
x++;
}
}
- 问题规模(输入量的多少):n
- 基本语句(执行次数与整个算法的执行时间 成正比的操作):x++
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
算法分析----大O记号
定义:设T(n)和f(n)是定义在正整数集合上的两个函数,若存在两个正的常数 c c c和 n 0 n_0 n0,对于任意 n ≥ n 0 n≥n_0 n≥n0(当问题规模充分大时在渐近意义下的阶),都有 T ( n ) ≤ c × f ( n ) T(n)≤c×f(n) T(n)≤c×f(n),则称 T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
定理:若
A
(
n
)
=
a
m
n
m
+
a
m
−
1
n
m
−
1
+
…
…
+
a
1
n
+
a
0
A(n)=a_mn^m+a_{m-1}n^{m-1}+……+a_1n+a_0
A(n)=amnm+am−1nm−1+……+a1n+a0是一个m次多项式,则
A
(
n
)
=
O
(
n
m
)
A(n)=O(n^m)
A(n)=O(nm)
说明:在计算算法时间复杂度时,可以忽略所有低次幂(低阶)项和最高次幂(最高阶)项的系数
for (i=1; i<=n; ++i)
for (j=1; j<=n; ++j)
{
c[i][j]=0;
for (k=1; k<=n; ++k)
c[i][j]+=a[i][k]*b[k][j];
}
- 问题规模:n
- 基本语句:*
- 复杂度: O ( n 3 ) O(n^3) O(n3)
算法分析----时间复杂性分析的基本方法
时间复杂性的运算法则
设
T
1
(
n
)
=
O
(
f
(
n
)
)
,
T
2
(
n
)
=
O
(
g
(
n
)
)
T_1(n)=O(f(n)),T_2(n)=O(g(n))
T1(n)=O(f(n)),T2(n)=O(g(n)),则
①加法规则:
T
1
(
n
)
+
T
2
(
n
)
=
O
(
m
a
x
{
f
(
n
)
,
g
(
n
)
}
)
T_1(n)+T_2(n)= O(max\{f(n),g(n)\})
T1(n)+T2(n)=O(max{f(n),g(n)})
②乘法规则:
T
1
(
n
)
∗
T
2
(
n
)
=
O
(
f
(
n
)
⋅
g
(
n
)
)
T_1(n)*T_2(n) = O(f(n)·g(n))
T1(n)∗T2(n)=O(f(n)⋅g(n))
时间复杂性的分析方法
- 求程序中各语句、各模块的运行时间
- 求整个程序的运行时间。
- 各种语句和模块分析应遵循的规则是:
(1)赋值语句或读/写语句:
运行时间通常取O(1) .有函数调用的除外,此时要考虑函数的执行时间
(2)语句序列:
运行时间由加法规则确定,即该序列中耗时最多的语句的运行时间
(3)分支语句:
运行时间由条件测试(通常为O(1) )加上分支中运行时间最长的语句的运行时间
(4)循环语句:
运行时间是对输入数据重复执行n次循环体所耗时间的总和
每次重复所耗时间包括两部分:一是循环体本身的运行时间;二是计算循环参数、测试循环终止条件和跳回循环头所耗时间。后一部分通常为O(1)
通常,将常数因子忽略不计,可以认为上述时间是循环重复次数n和m的乘积,其中m是n次执行循环体当中时间消耗最多 的那一次的运行时间(乘法规则)
当遇到多重循环时,要由内层循环向外层逐层分析。因此,当分析外层循环的运行时间时,内层循环的运行时间应该是已知的。此时,可以把内层循环看成是外层循环的循环体的一部分
(5)函数调用语句:
①若程序中只有非递归调用,则从没有函数调用的被调函数开始,计算所有这种函数的运行时间。然后考虑有函数调用的任意一个函数P,在P调用的全部函数的运行时间都计算完之后,即可开始计算P的运行时间
②若程序中有递归调用,则令每个递归函数对应于一个未知的时间开销函数T(n),其中n是该函数参数的大小,之后列出关于T的递归方程并求解
例:求解n!递归算法的时间复杂度
long fact(int n)
{
if ((n == 0) || (n == 1))
return (1);
else
return (n * fact(n – 1));
}
时间复杂度的递归方程
T
(
n
)
=
{
C
当n=0,n=1时
G
+
T
(
n
−
1
)
当n>1时
T(n)=\begin{cases} C& \text{当n=0,n=1时} \\ G+T(n-1)& \text{当n>1时} \end{cases}
T(n)={CG+T(n−1)当n=0,n=1时当n>1时
解递归方程:
T
(
n
)
=
G
+
T
(
n
−
1
)
T
(
n
−
1
)
=
G
+
T
(
n
−
2
)
…
…
T
(
2
)
=
G
+
T
(
1
)
T
(
1
)
=
C
→
T
(
n
)
=
G
∗
(
n
−
1
)
+
C
取
f
(
n
)
=
n
∴
T
(
n
)
=
O
(
f
(
n
)
)
=
O
(
n
)
\begin{aligned} T(n) &=G+T(n-1)\\ T(n-1) &=G+T(n-2)\\ &……\\ T(2) &=G+T(1)\\ T(1) &=C\\ \rightarrow T(n) = G*(n-1)+C\\ 取f(n)=n\ \therefore T(n)=O(f(n))=O(n) \end{aligned}
T(n)T(n−1)T(2)T(1)→T(n)=G∗(n−1)+C取f(n)=n ∴T(n)=O(f(n))=O(n)=G+T(n−1)=G+T(n−2)……=G+T(1)=C
空间资源开销
- 对于空间开销,也可以实行类似的渐进分析方法
- 很多算法使用的数据结构是静态的存储结构,即存储空间在算法执行过程中并不发生变化
- 使用动态数据结构算法的存储空间是变化的,在算法运行过程中有时会有数量级的增大或缩小。对于这种情况,空间开销的分析和估计是十分必要的
时空资源折中原理
同一个问题求解,一般会存在多种算法,这些算法在时空开销上的优劣往往表现出“时空折中”(trade-off)的性质
- 为了改善一个算法的时间开销,往往以增大空间开销为代价
- 有时,为了缩小算法的空间开销,也可以牺牲计算机的运行时间,通过增大时间开销来换取存储空间的节省
1.5 逐步求精的程序设计方法(Cont.)
- 模型化(建模)
- 确定算法
- 逐步求精(根据程序中使用的数据形式,定义若干ADT)
- ADT的实现(选择适当的数据结构表示数学模型,并用相应的函数实现每个操作)
例:交叉路口的交通安全管理问题
- 问题描述
一个具有多条通路的交叉路口,当允许某些通路上的车辆在交叉路口“拐弯”时,必须对其他一些通路上的车辆加以限制,不允许同时在交叉路口“拐弯”,以免发生碰撞。所有这些可能的“拐弯”组成一个集合 - 基本要求
把这个集合分成尽可能少的组,使得每组中所有的“拐弯”都能同时进行而不发生碰撞。这样,每组对应一个指挥灯,因而实现了用尽可能少的指挥灯完成交叉路口的管理。
根据上述步骤
- 模型化:
转换成“着色问题”- 用图作为交叉路口的数学模型;
- 每个“拐弯”对应图中的一个顶点;
- 若两个“拐弯”不能同时进行,则用一条边把这两个“拐弯”所对应的两个结点连接起来,并且说这两个顶点是相邻的
- 确定算法:
可以考虑“贪心”算法和“穷举”算法
“贪心”算法的思想:- 首先用第一种颜色对图中尽可能多的
顶点着色(尽可能多表现出“贪心”) - 然后用第二种颜色对余下的顶点中尽
可能多的顶点着色 - 如此直至所有顶点都着色完成
- 首先用第一种颜色对图中尽可能多的
- 逐步求精:
考虑用伪代码定义出ADT,再对ADT实现进行优化 - 对ADT中的抽象数据型结合题目进行具体定义