1.1 什么是数据结构
第一讲主要讲了什么是算法,什么是数据结构,通过具体的问题(多项式求值和最大子列和),让人直观的感受到算法对解决问题的影响(当然也讲了衡量算法好坏的算法复杂度分析)。
1、影响解决问题的方法和效率的因素:
- 数据的组织方式
- 空间的利用效率
- 算法的巧妙程度
例:给定一个N,输出从1到N的所有整数,用循环和递归分别实现。
//循环
void printCycle(int N) {
for (int i = 1; i <= N; i++)
printf("%d ", i);
}
//递归,N足够大的时候会爆内存,递归这个东西看起来代码比较简洁就是特别占地方。
void printRecursion(int N) {
printf("%d ", N);
if (N == 1) return;
printRecursion(N - 1);
}
例:给定多项式,计算多项式在X处的值。
f
(
x
)
=
a
0
+
a
1
x
+
a
2
x
2
+
⋅
⋅
⋅
+
a
n
x
n
f(x)=a_{0}+a_{1}x+a_{2}x^{2}+\cdot \cdot \cdot + a_{n}x^{n}
f(x)=a0+a1x+a2x2+⋅⋅⋅+anxn
用f1(一项算完再算另一项,会反复计算x的高次方)和f2(把能提取的x都提取出来)两种方式分别实现,由以下代码可以测得f1比f2慢一个数量级!
//计算函数的值----方法1,逐项计算
void f1(int n, double a[], double x) {
double sum = 0;
for (int i = 0; i <= n; i++)
{
sum += a[i] * pow(x, i);
}
printf("%f", sum);
}
//计算函数的值----方法2,先提取再计算
void f2(int n, double a[], double x) {
double tem = a[n];
for (int i = n-1; i >= 0; i--)
{
tem = a[i] + tem * x;
}
printf("%f", tem);
}
描述算法的时候用抽象数据类型(类似于面向对象的编程语言中的类)来描述数据结构。
1.2 什么是算法
1、算法:
- 有限的指令集
- 有一些输入(有时没有)
- 有输出
- 在有限步之后终止
- 算法的每一步必须精确可执行,但是也要抽象,不依赖于某一个语言
2、算法好坏的指标:
- 空间复杂度S(n):占用存储空间的大小。
- 时间复杂度T(n):耗费时间的长度。
空间复杂度: 上一节中的递归输出会在每一次调用的时候把当前的状态存储在堆栈中,当数据规模足够大的时候,各个状态会把可用内存占满。
时间复杂度: 上一节中的f1和f2比较,f1逐项计算算,每次循环做多次乘法,一共做n平方级次,f2每次循环做1次,只需要做n常数级次乘法。f1比f2高一阶,所以数据规模大的时候f1要比f2慢很多。
分析算法好坏的时候需要考虑最坏情况复杂度和平均复杂度。一般考虑最坏情况即可。
3、算法复杂度的渐进表示:
O(n)表示复杂度上界,还有拟合值和下界,一般就考虑上界。
1.3 应用实例:最大子列和问题
给定一个整数序列,有N个值,A1、A2、、、An,一个整数序列有很多连续的子列,最大子列和就是求这些子列的和的最大值,如果最大值为负数,就返回0。
//最大子列和----方法1,计算所有的子列和
void maxSubSum1(int a[], int N) {
int max = 0;
int sum = 0;
for (int i = 0; i < N; i++)
for (int j = i + 1; j <= N; j++)
{
sum = 0;
for (int k = i; i < j; k++)
sum += a[k];
if (sum > max) max = sum;
}
printf("%d", max);
}
//时间复杂度是n的三次方级,此算法时间复杂度高,因为大量的重复计算,优化得到算法2。
//最大子列和----方法2,计算所有的子列和,但是计算的同时避免一部分重复求和
void maxSubSum2(int a[], int N) {
int max = 0;
int sum = 0;
for (int i = 0; i < N; i++) {
sum = 0;
for (int j = i; j < N; j++)
{
sum += a[j];
if (sum > max) max = sum;
}
}
printf("%d", max);
}
//显然,次算法的复杂度为n平方级,比算法1好了很多。然而平方级的复杂度并不好,需要nlog n的算法。下面附上PTA算法题的运行结果。
//最大子列和----方法3,分而治之,整数序列从中间分成两半,分别考虑各自的最大子列和
//然后再从中间开始依次扫描跨越两边的整数,得到第三个最大子列和。
//整体时间复杂度为Nlog N。
int maxSubSum3(int a[], int left, int right) {
//退出递归
if (left == right) return a[left] > 0 ? a[left] : 0;
//计算中点
int center = (left + right) / 2;
//计算左、右最大子列和
int leftMax = maxSubSum3(a, left, center);
int rightMax = maxSubSum3(a, center + 1, right);
//计算跨中点最大子列和
int sum = 0;
int centerLeftMax = 0;
for (int i = center; i >= left; i--)
{
sum += a[i];
if (sum > centerLeftMax) centerLeftMax = sum;
}
sum = 0;
int centerRightMax = 0;
for (int i = center + 1; i <= right; i++)
{
sum += a[i];
if (sum > centerRightMax) centerRightMax = sum;
}
int centerMax = centerLeftMax + centerRightMax;
//比较三者大小
return centerMax > leftMax ? (centerMax > rightMax ? centerMax : rightMax) : (leftMax > rightMax ? leftMax : rightMax);
}
可以直观的看到算法3比算法2快了很多。
//最大子列和----方法4:在线处理,即时处理,任何时刻停下来都能得到当前输入的正确输出。复杂度O(N)。
void maxSubSum4(int a[], int N) {
int max = 0;
int sum = 0;
for (int i = 0; i < N; i++) {
sum += a[i];
if (sum > max)
max = sum;
else
sum = sum < 0 ? 0 : sum;
}
printf("%d", max);
}
这个算法核心在于最大子列和是连续的,如果前面连续的子列和为负值,那么它对结果就没有贡献可以抛弃,只考虑除它以外的最大子列和。
算法4相比与算法3的递归占用的空间是较小的,数据规模更大时应该会反映出这一点。