1.1 什么是数据结构
1.1.1 关于数据组织
“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相关的函数来给出。”
——Sartaj Sahni,《数据结构、算法与应用》
“数据结构是ADT(抽象数据类型Abstract Data Type)的物理实现。”
——Clifford A.Shaffer,《数据结构与算法分析》
“数据结构(Data StructStructure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。”
——中文维基百科
- 如何在书架上摆放图书?
相关操作 | 随便放 | 按照书名的拼音字母顺序排放 | 先分类,在每个类别内按拼音字母顺序排放 |
---|---|---|---|
新书怎么插入? | 哪里有空放哪里 | 若新进一本《阿Q正传》… | 先定类别,二分查找确定位置,移除空位 |
怎么找到某本指定的书? | …累死 | 二分查找 | 先定类别,再二分查找 |
- 空间如何分配?
- 类别应该分多细?
说明——解决问题方法的效率,跟数据的组织方式有关。
1.1.2 关于空间使用
- 写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,能顺序打印从1到N的全部正整数
void PrintN (int N)
{
int i;
for (i=1; i<=N; i++){
printf("%d\n",i);} //循环实现
return;
}
void PrintN (int N)
{
if (N){
printN(N-1); //递归函数
printf("%d\n",N);}
return;
}
输入N=100000时,第二种实现方式罢工了!!
说明——解决问题方法的效率,跟空间的利用效率有关。
1.1.3 关于算法效率
- 写程序计算给定多项式在给定点x处的值
f = a 0 + a 1 x + . . . + a n − 1 x n − 1 + a n x n \operatorname{f} = a_0 + a_1x +... +a_{n-1}x^{n-1}+a_nx^n f=a0+a1x+...+an−1xn−1+anxn
double f(int n, double a[], double x)
{
int i;
double p = a[0];
for ( i=1; i<=n; i++)
p += (a[i]*pow(x,i));
return p;
}
秦九韶算法:
f
=
a
0
+
x
(
a
1
+
x
(
.
.
.
(
a
n
−
1
+
x
(
a
n
)
)
.
.
.
)
)
\operatorname{f} = a_0 +x(a_1+x(...(a_{n-1} +x(a_n))...))
f=a0+x(a1+x(...(an−1+x(an))...))
double f(int n, double a[], double x)
{
int i;
double p = a[n];
for ( i=n; i>0; i--)
p = a[i-1] +x*p;
return p;
}
注:clock():捕捉从程序开始运行到clock()被调用时所耗费的时间。这个时间单位是clock tick,即“时钟打点”。
常数CLK_TCK:机器时钟每秒所走的时钟打点数。
模板:
#include <stdio.h>
#include <time.h>
clock_t start, stop;
/* clock_t是clock()函数返回的变量类型 */
double duration;
/* 记录北侧函数运行时间,以秒为单位 */
int main()
{ /*不在测试范围内的准备工作写在clock()调用之前 */
start = clock(); /* 开始计时 */
MyFunction(); /* 把被测函数放在这里 */
stop = clock();
duration = ((double)(stop - start))/CLK_TCK;
/* 其他不在测试范围的处理写在后面,例如输出duration的值 */
return 0;
}
用如上内容计算
∑
i
=
0
9
i
⋅
x
i
\sum_{i=0}^9i\cdot x^i
i=0∑9i⋅xi
在给定点 x=1.1处的值f(1.1)
前者后者运行得到的duration都是0,是由于程序运行时间太快,还不到一个tick。那么我们让被测函数重复运行多次,使得测出的总的时钟打点间隔充分长,最后计算被测函数平均每次运行的时间即可!
最终测出后者的运行时间比前者少了一个数量级。
说明——解决问题方法的效率,跟算法的巧妙程度有关。
1.1.4 抽象数据类型
所以到底什么是数据结构???
- 数据对象在计算机中的组织方式
- 逻辑结构(线,树,图)
- 物理存储结构(数组,链表…)
- 数据对象必定与一系列加在其上的操作相关联
- 完成这些操作所用的方法就是算法
抽象数据类型(Abstract Data Type)
- 数据类型
- 数据对象集
- 数据集合相关联的操作集
(C语言中两者是分开的,但是对于面向对象的语言比如C++、Java等,能把数据集和相关的操作集封装在一个类里面。)
- 抽象:描述数据类型的方法不依赖于具体实现
- 与存放数据的机器无关
- 与数据存储的物理结构无关
- 与实现操作的算法和编程语言均无关
只描述数据对象集和相关操作集“是什么”,并不涉及“如何做到”的问题
1.2 什么是算法
1.2.1 算法的定义
- 算法(Algorithm)
- 一个有限指令集
- 接受一些输入(有些情况下不需要输入)
- 产生输出
- 一定在有限步骤之后终止
- 每一条指令必须
- 有充分明确的目标,不可以有歧义
- 计算机能处理的范围之内
- 描述应不依赖于任何一种计算机语言以及具体的实现手段
1.2.2 什么是好的算法?
- 空间复杂度S(n)——根据算法写成的程序在执行时占用存储单元的长度。这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。
- 时间复杂度T(n)——根据算法写成的程序在执行时耗费时间的长度。这个长度往往也与输入数据的规模有关。时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。
在分析一般算法的效率时,我们经常关注下面两种复杂度
- 最坏情况复杂度 T w o r s t ( n ) T_{worst}(n) Tworst(n)
- 平均复杂度 T a v g ( n ) T_{avg}(n) Tavg(n)
分析最坏情况复杂度相对来说更简单。
1.2.3 复杂度的渐进表示
不需要对算法做一个非常精细的分析,只粗略的知道它的一个增长趋势就可以了。
- T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))表示f(n)是T(n)的某种上界。
- T ( n ) = Ω ( g ( n ) ) T(n) = \varOmega(g(n)) T(n)=Ω(g(n)) 表示g(n)是T(n)的某种下界
- T ( n ) = θ ( h ( n ) ) T(n) = \theta(h(n)) T(n)=θ(h(n)) 表示上界和下界等价。
复杂度分析小窍门
-
若
两
段
算
法
分
别
有
复
杂
度
Y
1
(
n
)
=
O
(
f
1
(
n
)
)
和
T
2
(
n
)
=
O
(
f
2
(
n
)
)
若两段算法分别有复杂度 Y_1(n) = O(f_1(n))和T_2(n) = O(f_2(n))
若两段算法分别有复杂度Y1(n)=O(f1(n))和T2(n)=O(f2(n))
- 则 T 1 ( n ) + T 2 ( n ) = m a x ( O ( f 1 ( n ) ) , O ( f 2 ( n ) ) ) 则T_1(n) +T_2(n) = max(O(f_1(n)),O(f_2(n))) 则T1(n)+T2(n)=max(O(f1(n)),O(f2(n)))
- T 1 ( n ) × T 2 ( n ) = O ( f 1 ( n ) × f 2 ( n ) ) T_1(n) \times T_2(n) = O(f_1(n) \times f_2(n)) T1(n)×T2(n)=O(f1(n)×f2(n))
- 若 T ( n ) 是 关 于 n 的 k 阶 多 项 式 , 那 么 T ( n ) = θ ( n k ) 若T(n)是关于n的k阶多项式,那么T(n) = \theta (n^k) 若T(n)是关于n的k阶多项式,那么T(n)=θ(nk)
- 一个for循环的时间复杂度等于循环次数乘以循环体代码的复杂度
- if-else 结构的复杂度取决于if的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大
1.3 应用实例:最大子列和问题
1.3.1 应用实例-算法1&2
给
定
N
个
整
数
的
序
列
{
A
1
,
A
2
,
.
.
.
,
A
N
}
,
求
函
数
f
(
i
,
j
)
=
m
a
x
{
0
,
∑
k
=
i
j
A
k
}
的
最
大
值
。
给定N个整数的序列\lbrace A_1,A_2,...,A_N\rbrace ,求函数f(i,j)= max\lbrace0,\sum_{k=i}^jA_k\rbrace 的最大值。
给定N个整数的序列{A1,A2,...,AN},求函数f(i,j)=max{0,k=i∑jAk}的最大值。
算法1
int MaxSubseqSum1(int A[], int N)
{int ThisSUm, Maxsum = 0;
int i,j,k;
for(i=0; i<N; i++){
for(j=i; j<N; j++){
ThisSum = 0;
for(k=i; k<=j; k++)
ThisSum += A[k];
if(ThisSum >MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
T(N)=O(N^3)
int MaxSubseqSum2(int A[], int N)
{int ThisSUm, Maxsum = 0;
int i,j,k;
for(i=0; i<N; i++){
ThisSum = 0;
for(j=i; j<N; j++){
ThisSum += A[j];/*对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可*/
if(ThisSum >MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
T(N)=O(N^2)
1.3.2 应用实例-算法3 分而治之
先把数组一分为二,分成全在左侧的子列、跨越中线的子列和全在右侧的子列三种情况,再分别递归地去解决问题。
T(N)=O(NlogN)
1.3.3应用实例-算法4 在线处理
int MaxSubseqSum4(int A[], int N)
{int ThisSUm, Maxsum = 0;
int i;
for(i=0; i<N; i++){
ThisSum +=A[i];
if(ThisSum>MaxSum)
MaxSum = ThisSum;
else if(ThisSum<0)/*如果当前子列和为负*/
ThisSum = 0;/*则不可能使后面的部分和增大,抛弃之 */
}
return MaxSum;
}
“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。
T(N)=O(N)