算法(Algorithm)
本文为基于浙江大学陈越老师的数据结构课程1的学习笔记
1. 定义
- 一个有限指令集
- 接受一些输入(有些情况下不需要输入)
- 产生输出
- 一定在有限步骤之后终止
- 每一条指令必须
- 有充分明确的目标,不可以有歧义
- 计算机能处理的范围之内
- 描述应不依赖于任何一种计算机语言以及具体的实现手段
(1) 例:选择排序算法的伪码描述
void SelectionSort (int List[], int N)
{/*将N个整数List[0]…List[N-1]进行非递减排序*/
for(i=0; i<N; i++)
{
MinPosition = ScanForMin(List, i, N-1);
/*从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition*/
Swap(List[i], List[MinPosition]);
/*将从未排序部分的最小元换到有序部分的最后位置*/
}
}
2. 算法效率
(1) 什么是算法效率
除了正确性,算法的另外一个重要的特点就是效率Efficiency了。有两种算法效率:时间效率Time Efficiency和空间效率Space Effiency。时间效率也称为时间复杂度Time Complexity,指出算法运行有多快;空间效率有称为空间复杂度Space Complexity,指出算法需要多少额外的空间。2
(2) printN(空间复杂度)
a. 循环
#include<stdio.h>
void printN(int N)
{
int i;
for(i=1;i<=N;i++)
{
printf("%d\n",i);
}
return;
}
int main()
{
void printN(int N);
int N;
scanf("%d",&N);
printN(N);
return 0;
}
运行结果:
b. 递归
#include<stdio.h>
void printN(int N)
{
if(N)
{
printN(N-1);
printf("%d\n",N);
}
return;
}
int main()
{
int N;
scanf("%d",&N);
printN(N);
return 0;
}
运行结果:
注意:
当输入100000时:
循环:
递归:
原因:
当一个非常简单的程序使用递归实现时,系统会分配大量的内存。
这是因为,每一次递归的实现中,系统都会重新为变量分配空间而不是覆盖原来的空间。
因此,当问题没有特别复杂,并不一定需要使用到递归程序时,应当避免使用递归程序,
尤其是递归次数多的程序,可能会造成内存分配的崩溃。
结论:
解决问题方法的效率,跟空间的利用率有关。
(3) 计算多项式值(时间复杂度)
a. 直接算法
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;
}
b. 秦九韶算法3
double f2(double a[],int n,double x)
{
double result=a[n];
int i;
for(i=n;i>=1;i++){
result=a[i-1]+x*result;
}
return result;
}
注意:
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;
}
#include<stdio.h>
#include<time.h>
#include<math.h>
clock_t start,stop;
double duration;
#define MAXN 10//多项式最大项数,即多项式阶数+1
double f1(int n,double a[],double x);
double f2(int n,double a[],double x);
int main()
{
int i;
double a[MAXN];//储存多项式的系数
for(i=0;i<MAXN;i++)
a[i] = (double)i;
start = clock();
f1(MAXN-1,a,1.1);
stop = clock;
duration = ((double)(stop-start))/CLK+TCK;
printf("ticks1 = %f\n",(double)(stop-start));
printf("duration1 = %6.2e\n",duration);
return 0;
}
让被测函数重复运行充分多次,使得测出的总的时钟打点间隔充分长,最后计算被测函数平均每次运行的时间即可!
原因:
秦九韶算法最大的优点在于将求n次多项式的值转化为求n个一次多项式的值。 在人工计算时,利用秦九韶算法和其中的系数表可以大幅简化运算;对于计算机程序算法而言,加法比乘法的计算效率要高很多,因此该算法仍有极大的意义,用于减少CPU运算时间。
结论:
解决问题方法的效率,还跟算法的巧妙程度有关。
3. 算法复杂度进阶
(1) 复杂度的渐进表示
- T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n)) 表示存在常数 C > 0 , n 0 > 0 C>0,n_0>0 C>0,n0>0 使得当 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 ) = Ω ( g ( n ) ) T(n)=Ω(g(n)) T(n)=Ω(g(n)) 表示存在常数 C > 0 , n 0 > 0 C>0,n_0>0 C>0,n0>0 使得当 n ≥ n 0 n≥n_0 n≥n0 时有 T ( n ) ≥ C ⋅ g ( n ) T(n)≥C·g(n) T(n)≥C⋅g(n)
- T ( n ) = Θ ( h ( n ) ) T(n)=Θ(h(n)) T(n)=Θ(h(n)) 表示同时有 T ( n ) = O ( h ( n ) ) T(n)=O(h(n)) T(n)=O(h(n)) 和 T ( n ) = Ω ( h ( n ) ) T(n)=Ω(h(n)) T(n)=Ω(h(n))
复杂度图表:
复杂度函数图:
每秒10亿指令计算机的运行时间表:
(2) 复杂度分析便捷方法
- 若两段算法分别有复杂度
T
1
(
n
)
=
O
(
f
1
(
n
)
)
T_1(n)=O(f_1(n))
T1(n)=O(f1(n)) 和
T
2
(
n
)
=
O
(
f
2
(
n
)
)
T_2(n)=O(f_2(n))
T2(n)=O(f2(n)),则
- T 2 ( n ) + T 2 ( n ) = m a x ( O ( f 1 ( n ) ) , O ( f 2 ( n ) ) ) T_2(n)+T_2(n)=max(O(f_1(n)),O(f_2(n))) T2(n)+T2(n)=max(O(f1(n)),O(f2(n)))
- T 2 ( n ) + T 2 ( n ) = O ( f 1 ( n ) × f 2 ( n ) ) T_2(n)+T_2(n)=O(f_1(n)×f_2(n)) T2(n)+T2(n)=O(f1(n)×f2(n))
- 若 T 1 ( n ) T_{1}(n) T1(n) 是关于n的k阶多项式,那么 T ( n ) = Θ ( n k ) T(n)=Θ(n^k) T(n)=Θ(nk)
- 一个
for
循环的时间复杂度等于循环次数乘以循环体代码的复杂度 if-else
结构的复杂度取决于if
的条件判断复杂度和两个分枝部分的复杂度,总体复杂度取三者中最大
4.应用实例
最大子列和问题
给定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 A k f(i,j)=max{0,\sum_{k=i}^jA_k} f(i,j)=max0,∑k=ijAk 的最大值(1≤i≤j≤K)
算法1:
int MaxSubseqSum1(int A[], int N)
{
int ThisSum, MaxSum = 0;
int i, j, k;
for(i=0; i<N; i++)
{/*i是子列左端位置*/
for(j=i; j<N; j++)
{/*j是子列右端位置*/
ThisSum = 0;/*ThisSum是从A[i]到A[j]的子列和*/
for(k=i; k<=j; k++)
ThisSum += A[k];
if(ThisSum > MaxSum)/*如果刚得到的这个子列和更大*/
MaxSum = ThisSum;/*则更新结果*/
}/*j循环结束*/
}/*i循环结束*/
return MaxSum;
}
T ( N ) = O ( N 3 ) T(N)=O(N^3) T(N)=O(N3)
直观原因:三层for循环嵌套
算法2:
int MaxSubseqSum2(int A[], int N)
{
int ThisSum, MaxSum = 0;
int i,j;
for(i=0; i<N; i++)
{/*i是子列左端位置*/
ThisSum = 0;/*ThisSum = 0;*/
for(j=i; j<N; j++)
{/*j是子列右端位置*/
ThisSum += A[j];
/*对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可*/
if(ThisSum > MaxSum)/*如果刚得到这个子列和更大*/
MaxSum = ThisSum;/*则更新结果*/
}/*j循环结束*/
}/*i循环结束*/
return MaxSum;
}
T ( N ) = O ( N 2 ) T(N)=O(N^2) T(N)=O(N2)
直观原因:两层for循环嵌套
算法3:分而治之4
int Max3(int A, int B, int C)
{/*返回三个整数的最大值*/
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)
{
return DivideAndConquer(List, 0, N-1);
}
T ( N ) = O ( N l o g N ) T(N)=O(NlogN) T(N)=O(NlogN)
直观原因:
T
(
N
)
=
2
T
(
N
/
2
)
+
c
N
=
2
[
2
T
(
N
/
2
2
)
+
c
N
/
2
]
+
c
N
=
2
k
O
(
1
)
+
c
k
N
=
O
(
N
l
o
g
N
)
\begin{aligned} T(N)&=2T(N/2)+cN \\ &=2[2T(N/2^2)+cN/2]+cN \\ &=2^kO(1)+ckN \\ &=O(NlogN) \\ \end{aligned}
T(N)=2T(N/2)+cN=2[2T(N/22)+cN/2]+cN=2kO(1)+ckN=O(NlogN)
T(1)=O(1),其中N/2^k=1
算法4:在线处理
int MaxSubseqSum4(int A[], int N)
{
int ThisSum, 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;
/*则不可能使后面的部分和增大,抛弃之*/
}
return MaxSum;
}
T ( N ) = O ( N ) T(N)=O(N) T(N)=O(N)
直观原因:一层for循环
“在线
”的意思是指每输入一个数据就进行即时处理
,在任何一个地方中止输入,算法都能正确给出当前的解。