算法(数据结构)学习笔记

算法(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;
}

运行结果:

循环和递归1

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;
}

运行结果:

循环和递归2

注意:

当输入100000时:

循环:
循环和递归3

递归:
循环和递归4

原因:

当一个非常简单的程序使用递归实现时,系统会分配大量的内存。
这是因为,每一次递归的实现中,系统都会重新为变量分配空间而不是覆盖原来的空间。
因此,当问题没有特别复杂,并不一定需要使用到递归程序时,应当避免使用递归程序,
尤其是递归次数多的程序,可能会造成内存分配的崩溃。

结论:

解决问题方法的效率,跟空间的利用率有关。

(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 nn0 时有 T ( n ) ≤ C ⋅ f ( n ) T(n)≤C·f(n) T(n)Cf(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 nn0 时有 T ( n ) ≥ C ⋅ g ( n ) T(n)≥C·g(n) T(n)Cg(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循环

在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。

运行时间比较:

运行时间比较


参考资料:


  1. 数据结构:什么是算法 ↩︎

  2. 算法效率分析——夜雨风云的博客 ↩︎

  3. 秦九韶算法的百度百科 ↩︎

  4. 最大子列和问题——YelloJesse的博客 ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值