第一章 引入
1.1 什么是数据结构
- 解决问题方法的效率,和数据的组织方式有关。
- 递归函数在处理大量数据时,会大量占用空间。这体现了解决问题方法的效率,跟空间的利用效率有关。
- 解决问题方法的效率,与算法的巧妙程度有关。
一、数据结构是数据对象在计算机中的组织方式
1. 数据对象的逻辑结构
- 线形结构(一对一)
- 树(一对多)
- 图(多对多)
2. 数据对象在计算机中的物理存储结构
- 数组
- 链表 等
3. 描述数据对象的方法 —— 抽象数据类型
(1)数据类型
-
数据对象集
-
数据集合相关联的操作集
注意:在C语言中,这两个分开处理;但在面向对象的编程语言中,这两个会在同一个类(class)中出现
(2) 抽象:描述数据类型的方法不依赖于具体实现
对数据类型的描述:
-
与存放数据的机器无关
-
与数据存储的物理结构无关
-
与是实现操作的算法和编程语言无关
注意:我们只描述数据对象集和相关操作集 “是什么” ,并不涉及 “如何做到” 的问题
二、数据对象必定与一系列加在其上的操作相关联
三、完成这些操作所用的方法是算法
1.2 什么是算法
一、算法
-
是一个有限指令集
-
接收一些输入,有时候不需要输入
-
一定产生输出
-
一定在有限步骤后终止
-
其中的每一条指令必须:
- 有明确的目标,不可以有歧义
- 在计算机能处理的范围之内
- 算法描述应该抽象,不依赖与任何一种计算机语言,以及具体的实现手段
二、什么是好的算法
1. 空间复杂度 S(n)
:程序在执行时占用存储单元的长度
- 占用存储单元的长度,与输入数据的规模有关
- 空间复杂度过高的算法可能导致使用的内存超限,从而造成程序的中断
2. 时间复杂度 T(n)
:程序在执行时耗费时间的长度
- 耗费时间的长度与输入数据的规模有关
- 时间复杂度过高的低效算法可能导致在规定时间内等不到运行结果
3. 在分析一般算法的效率时,一般关注下面两种复杂度
- 最坏情况复杂度
Tworst(n)
:一般通过该标准比较算法间的优劣 - 平均复杂度
Tavg(n)
Tavg(n) <= Tworst(n)
三、复杂度的渐进表示法
1. 复杂度的渐进表示
- 在上图中,我们可以看出,
f(n)
是T(n)
的某种上界,g(n)
是T(n)
的某种下界 - 为了保证能够准确地、有意义地分析算法,一般选取上界中最大的、下界中最小的
2. 复杂度分析小窍门
-
若两段算法分别有复杂度
T1(n) = O(f1(n))
和T2(n) = O(f2(n))
,则T1(n) + T2(n) = max(O(f1(n)), o(f2(n)))
T1(n) x T2(n) = O(f1(n) x f2(n))
-
若
T(n)
是关于n
的k
阶多项式,则T(n) = θ(n^k)
- 考虑最高阶的一项即可,可以忽略其他的项
-
一个 for 循环的时间复杂度 = 循环次数 x 循环体代码的复杂度
-
if-else 结构的复杂度 = 在各种分情况下的复杂度中最大的复杂度
1.3 应用实例:求连续最大子列和问题
-
题目:给定
N
个整数的序列 { A 1 , A 2 , . . . , A N } \{ A_1, A_2, ... , A_N \} {A1,A2,...,AN},求函数 f ( i , j ) = m a x { 0 , ∑ k = i j } f(i, j) = max\{0, \sum_{k=i}^j\} f(i,j)=max{0,∑k=ij} 的最大值 -
注意:再计算子列和的过程中,参与计算的元素必须前后相邻。
算法1:
/*
* 输入:int A[] 整数序列
* 输出:int N 整数序列中元素的个数
*/
int MaxSubseqSum1 (int A[], int N) {
int ThisSum, MaxSum = 0;
int i, j, k;
/* 遍历所有子列和 */
for (i=0; i<N; i++) { /* i是子列左端位置。此行选定i */
for (j=i; j<N; j++) { /* j是子列右端位置。此行选定j */
ThisSum = 0; /* ThisSum是从A[i]到A[j]的子列和 */
for (k=i; k<=j; k++) { /* A[k]的起点是A[i], 终点是A[j] */
ThisSum += A[k]; /* 前面选定了i和j, 接下来计算出这一段子列和 */
}
if (ThisSum > MaxSum) { /* 如果刚得到的这个子列和更大 */
MaxSum = ThisSum; /* 则更新结果 */
}
} /* j循环结束 */
} /* i循环结束 */
return MaxSum;
}
- 算法1复杂度:
T
(
N
)
=
O
(
N
3
)
T(N)=O(N^3)
T(N)=O(N3)
有三层循环,每一层循环的循环变量都会取到最大值N
算法2:
int MaxSubseqSum2 (int A[], int N) {
int ThisSum, MaxSum = 0;
int i, j;
for (i=0; i<N; i++) { /* i是子列左端位置。此行选定i */
ThisSum = 0; /* ThisSum是从A[i]到A[j]的子列和 */
for (j=i; j<N; j++) { /* j是子列右端位置。此行选定j */
ThisSum += A[j]; /* i相同,j不同时,只要在j-1次循环的基础上累加一项即可 */
if (ThisSum > MaxSum) { /* 如果刚得到的这个子列和更大 */
MaxSum = ThisSum; /* 则更新结果 */
}
} /* j循环结束 */
} /* i循环结束 */
return MaxSum;
}
- 算法2复杂度:
T
(
N
)
=
O
(
N
2
)
T(N) = O(N^2)
T(N)=O(N2)
只有两层循环,每层循环的循环变量最大取N
算法3:运用 “分而治之” 思想,采取递归的方法
static int MaxSubSum (const int A[], int Left, int Right) {
int MaxLeftSum, MaxRightSum;
int MaxLeftBorderSum, MaxRightBorderSum;
int LeftBorderSum, RightBorderSum;
int Center, i;
if (Left == Right) {
if (A[Left] > 0) {
return A[Left];
} else {
return 0;
}
}
}
- 算法3复杂度:
整体复杂度为 T ( N ) = 2 T ( N / 2 ) + c N T(N)=2T(N/2)+cN T(N)=2T(N/2)+cN,且 T ( 1 ) = O ( 1 ) T(1)=O(1) T(1)=O(1)
递推式子(展开等号右边的 T ( . ) T(.) T(.)项),最终, T ( N ) = 2 k O ( 1 ) + c k N T(N)=2^kO(1)+ckN T(N)=2kO(1)+ckN,其中, N / 2 k = 1 N/2^k=1 N/2k=1
进一步化简得, T ( N ) = O ( N ⋅ l o g N ) T(N)=O(N·logN) T(N)=O(N⋅logN) - 算法3算法抽象化
表1 左部分1 左部分2 右部分1 右部分2 4 -3 5 -2 -1 2 6 -2
在左部分1中,从右往左,第一步取-3,第二步取4-3=1,取最大值得1。左部分2,从左往右,第一步取5,第二步取5-2=3,取最大值得5;同理,右部分1从右往左、右部分2从左往右执行相应操作。最终得到如下表所示的第一轮子列和。
| |||||||
|
|
|
| ||||
|
|
|
|
左部分在1,5,1+5=6中取最大值,得6;右部分在2,6,2+6=8中取最大值,得8,得到如下表所示的第二轮子列和
| |||||||
|
| ||||||
|
|
如下表所示
| |||||||
|
| ||||||
|
|
|
|
|
|
|
|
左部分从右往左依次相加,最大值为-2+5+(-3)+4=4。右部分从左往右依次相加,最大值为-1+2+6=7。这就是第三轮子列和;在4,7,4+7=11中,得到第四轮子列和
四轮子列和中,11最大,所以,最终结果为11
算法4:在线处理算法
- “在线”指,每输入一个数据就进行即时的处理,在任何地方种植输入,算法都能正确地给出当前的解。
int MaxSubseqSum4(int A[], int N) {
int ThisNum, MaxSum;
int i;
ThisSum = MaxSum = 0;
for (i = 0; i < N; i++) {
ThisSum += A[i]; // 正常向右累加
if (ThisSum > MaxSum) {
MaxSUm = ThisSUm; // 再累加过程中,若发现更大子列和,则更新当前结果
} else if (ThisSum < 0) { // 若当前子列和为负数
ThisSum = 0; // 则此时不可能使后面的部分和增大,抛弃现在的子列和,从0再开始计算子列和
}
}
return MaxSum;
}
- 算法4复杂度:
T
(
N
)
=
O
(
N
)
T(N)=O(N)
T(N)=O(N)
整个函数只有一个for
循环,每次循环中的if
结构都是常数数量级的复杂度,故该算法的复杂度是线性的。