数据结构起到承上启下的作用,上承计算机基础和C语言等基础内容,下启算法设计与分析、操作系统、编译原理、数据库原理等核心内容。
数据结构的内容包括各种数据(线性表、栈、队列、串、数组、数、二叉树和图等)的逻辑结构描述、存储结构表示和运算算法设计,其主要是基本算法的设计,而算法设计与分析主要是通用算法的设计。
1. 什么是数据结构?
1.1 解决问题方法的效率跟数据的组织方式有关
例1:如何在书架上摆放图书?(这个问题不科学)
问题分析:图书的摆放影响图书的插入和查找操作!可以有如下三种摆放方式:
①. 随便放:插入方便,但查找累死;
②. 按照书名的拼音字母顺序摆放:二分查找,但插入累死;
③. 把书架划分成几块区域,每块区域指定摆放某种类别的图书,同类别内按书名的拼音字母顺序摆放;先定类别再二分查找,先定类别再二分查找确定位置后移出空位。
问题总结:图书的组织方式影响其操作效率;空间问题+类别划分问题:图书应该分为多少个类别,每个类别应该分配多少书架呢?
1.2 解决问题方法的效率跟空间的利用效率有关
例2:写程序实现PrintN函数,使得传入一个正整数为N的参数后,能顺序打印从1到N的全部正整数。
问题分析:循环实现、递归实现(代码简洁,但耗费内存空间,可对比N=1000000的情况)。
#include <stdio.h>
void printN1(int m);
void printN2(int n);
int main(void) {
int N;
scanf("%d",&N);
printN2(N);
system("pause");
return 0;
}
/* 循环实现 */
void printN1(int m) {
int i;
for(i=1;i<=m;i++) {
printf("%d\n",i);
}
return;
}
/* 递归实现:耗费内存资源 当n过大时可能导致系统崩溃 */
void printN2(int n) {
if(n) {
printN2(n-1);
printf("%d\n",n);
}
return;
}
1.3 解决问题方法的效率跟算法的巧妙程度有关
例3:计算给定多项式在给定点x处的值。
问题分析:多项式分解法求解速度较快,比直接求解法快一个数量级。
/* 直接求解法 */
double f1(int n, double a[], double x){
int i;
double sum = a[0];
for(i = 1; i <= n; i++) {
sum += (a[i]*pow(x, i));
}
return sum;
}
/* 多项式分解法:速度较快 */
double f2(int n, double a[], double x) {
int i;
double sum = a[n];
for(i = n; i > 0; i--) {
sum = a[n-1] + x*sum;
}
return sum;
}
1.4 什么是数据结构?
- 数据对象在计算机中的组织方式(逻辑结构和物理存储结构);
- 数据对象必定与一系列加在其上的操作相关联;
- 完成这些操作所用的方法就叫做算法;
1.5 抽象数据类型(Abstract Data Type)
- 数据类型
- 数据对象集
- 数据集合相关联的操作集
- 抽象:描述数据类型的方法不依赖于具体实现
- 与存放数据的机器无关
- 与数据存储的物理结构无关
- 与实现操作的算法和编程语言无关
- 注意:只描述数据对象集和操作集“是什么”,而不涉及“如何实现”的问题。
2. 什么是算法?
2.1 什么是算法(Algorithm)?
- 一个有限指令集
- 接受一些输入(有些情况下不需要输入)
- 产生输出
- 一定在有限步骤之后终止
- 每一条指令必须
- 有充分明确的目标,不可以有歧义
- 计算机能处理的范围之内
- 描述应不依赖于任何一种计算机语言以及具体的实现手段
2.2 什么是好的算法?
空间复杂度S(n)——根据算法写成的程序在执行时占用存储单元的长度,其与输入数据的规模有关;空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。
如例2所示,PrintN(int N)函数的递归实现(S(n)=C⋅N),在调用下一个函数时会保存当前函数的状态到存储单元,数据规模过大即会导致内存超限,致使程序非正常中断。
时间复杂度T(n)——根据算法写成的程序在执行时所耗费时间的长度,其与输入数据的规模有关;时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。
如例3所示,因计算机计算加减法的速度远大于乘除法,故可只考虑乘除法的计算次数;直接求解法T(n)=(N2+N)/2=C1N+C2N2,而多项式分解法T(n)=CN。
因此,在分析一般算法的效率时,我们通常关注最坏情况复杂度Tworst(n)和平均复杂度Tavg(n),其中Tavg(n)≤Tworst(n)。
2.3 复杂度的渐进表示法
表达形式 | 具体描述 |
---|---|
T(n)=O(f(n)) | 存在常数C>0,n0>0,使得当n≥n0时,有T(n)≤C⋅f(n) |
T(n)=Ω(g(n)) | 存在常数C>0且n0>0,使得当n≥n0时,有T(n)≥C⋅g(n) |
T(n)=Θ(h(n)) | 同时有T(n)=O(h(n))和T(n)=Ω(h(n)) |
2.4 复杂度分析小窍门
- 若两段算法分别有复杂度T1(n)=O(f1(n)和T2(n)=O(f2(n),则
- T1(n)+T2(n)=max{O(f1(n),O(f2(n)}
- T1(n)×T2(n)=O(f1(n)×f2(n))
- 若T(n)是关于n的
k 阶多项式,那么T(n)=Θ(nk) - 一个for循环的复杂度等于循环次数乘以循环体代码的复杂度
- if-else结构的复杂度取决于if条件判断的复杂度和两个分支部分的复杂度,总体复杂度取三者中最大
3. 应用实例:最大子列和问题
给定N个整数的序列
3.1 算法1
3.2 算法2
3.3 算法3:分而治之
int Max3( int A, int B, int C )
{ /* 返回3个整数中的最大值 */
return A > B ? A > C ? A : C : B > C ? B : C;
}
int DivideAndConquer( int List[], int left, int right )
{ /* 分治法求List[left]到List[right]的最大子列和 */
int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */
int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/
int LeftBorderSum, RightBorderSum;
int center, i;
if( left == right ) { /* 递归的终止条件,子列只有1个数字 */
if( List[left] > 0 ) return List[left];
else return 0;
}
/* 下面是"分"的过程 */
center = ( left + right ) / 2; /* 找到中分点 */
/* 递归求得两边子列的最大和 */
MaxLeftSum = DivideAndConquer( List, left, center );
MaxRightSum = DivideAndConquer( List, center+1, right );
/* 下面求跨分界线的最大子列和 */
MaxLeftBorderSum = 0; LeftBorderSum = 0;
for( i=center; i>=left; i-- ) { /* 从中线向左扫描 */
LeftBorderSum += List[i];
if( LeftBorderSum > MaxLeftBorderSum )
MaxLeftBorderSum = LeftBorderSum;
} /* 左边扫描结束 */
MaxRightBorderSum = 0; RightBorderSum = 0;
for( i=center+1; i<=right; i++ ) { /* 从中线向右扫描 */
RightBorderSum += List[i];
if( RightBorderSum > MaxRightBorderSum )
MaxRightBorderSum = RightBorderSum;
} /* 右边扫描结束 */
/* 下面返回"治"的结果 */
return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );
}
int MaxSubseqSum3( int List[], int N )
{ /* 保持与前2种算法相同的函数接口 */
return DivideAndConquer( List, 0, N-1 );
}
3.4 算法4:在线处理
“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。