数据结构与算法 算法分析

一.算法的概念
1.概念:

"算法"(Algorithm)是计算机处理信息的本质:计算机程序是通过1个算法来告诉计算机执行指定任务的步骤.算法处理信息时,通常会从输入设备或
数据的存储地址读取数据,并把结果写入输出设备或存储地址算法是独立存在的解决问题的方法和思想,对于算法而言,实现的语言并不重要,重要的是
思想.总的来说,算法就是对用于解决特定问题的方案/求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示1/多个操作

2.算法的5大特性:

1.输入:算法有1/多个输入          2.输出:算法至少有1个输出 
3.有穷性:算法在有限的步骤后会自动结束而不会无限循环,并且每个步骤可在可接受的时间内完成 
4.确定性:算法中的每步都有确定的含义,不会出现二义性 
5.可行性:算法的每步都是可行的,也就是说每步都能通过执行有限次来完成

二.数学基础与模型
1.概念:

定义1:如果 ∃ 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)≤cf(N) T(N)cf(N),则记为 T ( N ) = O ( f ( N ) ) T(N)=O(f(N)) T(N)=O(f(N))
定义2:如果 ∃ 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)≥cg(N) T(N)cg(N),则记为 T ( N ) = Ω ( g ( N ) ) T(N)=Ω(g(N)) T(N)=Ω(g(N))
定义3:当且仅当 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))时, T ( N ) = Θ ( h ( N ) ) T(N)=Θ(h(N)) T(N)=Θ(h(N))
定义4:如果 T ( N ) = O ( p ( N ) ) T(N)=O(p(N)) T(N)=O(p(N)) T ( N ) ≠ Θ ( p ( N ) ) T(N)≠Θ(p(N)) T(N)=Θ(p(N)),则 T ( N ) = O ( p ( N ) ) T(N)=O(p(N)) T(N)=O(p(N))
在这里插入图片描述
在这里插入图片描述

2.一些重要结论:

法则1:如果 T 1 ( N ) = O ( f ( N ) ) T_1(N)=O(f(N)) T1(N)=O(f(N)) T 2 ( N ) = O ( g ( N ) ) T_2(N)=O(g(N)) T2(N)=O(g(N)),那么:
T 1 ( N ) + T 2 ( N ) = m a x { O ( f ( N ) ) , O ( G ( n ) ) } T_1(N)+T_2(N)=max\{O(f(N)),O(G(n))\} T1(N)+T2(N)=max{O(f(N)),O(G(n))}
T 1 ( N ) ∗ T 2 ( N ) = O ( f ( N ) ∗ g ( N ) ) T_1(N)*T_2(N)=O(f(N)*g(N)) T1(N)T2(N)=O(f(N)g(N))
法则2:如果 T ( N ) T(N) T(N)是1个 k k k次多项式,则 T ( N ) = Θ ( N k ) T(N)=Θ(N^k) T(N)=Θ(Nk)
法则3:对任意常数 k , l o g k N = O ( N ) k,log^kN=O(N) k,logkN=O(N),这说明对数增长得非常缓慢

3.模型
在这里插入图片描述
三.衡量算法的效率
1.执行花费的时间:

实现算法程序的执行时间可以反应出算法的效率,即算法的优劣

问题:程序的运行离不开计算机环境(包括硬件和OS),这些环境因素会影响程序的执行时间
因此,单纯依靠运行的时间来比较算法的优劣并不客观准确

2.时间复杂度
(1)时间复杂度:

假设存在函数g(n),使得算法A处理规模为n的问题所用的基本操作数/时间为T(n)=g(n),则称g(n)为A的"时间复杂度",记为T(n),通常使用"大O记法"
来表示.由于每个基本操作的用时固定,2种记法的T(n)之间至多只相差1个常系数

假定计算机执行算法的1个基本操作的用时是1个时间单位,那么有多少个基本操作就代表会花费多少个时间单位.在不同计算机环境中,确切的单位时间不
同,但算法需要进行多少个基本操作(即花费多少个时间单位)大体相同因此用算法所需的基本操作数衡量其时间效率,可以忽略计算机环境的影响而客观
地反映算法的优劣,通常使用这种

#示例:
如果a+b+c=1000,且a^2+b^2=c^2(a,b,c均为自然数),如何求出所有a,b,c可能的组合?
#法1:
for a in range(0,1001):
    for b in range(0,1001):
        for c in range(0,1001):
            if a**2+b**2==c**2 and a+b+c==1000:#本行视为1步
                print("a,b,c:%d,%d,%d"%(a,b,c))#本行视为1步
T1(1000)=2*1000^3          T1(n)=2*n^3
#法2:
for a in range(0,1001):
    for b in range(0,1001-a):
        c=1000-a-b#本行视为1步
        if a**2+b**2==c**2:#本行视为1步
            print("a,b,c:%d,%d,%d"%(a,b,c))#本行视为1步
T(1000)=3*1000*1001/2          T(n)=3*n*(n+1)/2=(3*n^2)/2+3*n/2

(2)大O记法:

对单调的整数函数f(n),如果∃整数函数g(n)和实常数c>0,使对充分大的n总有f(n)<=c*g(n),就说g(n)是f(n)1个渐近函数(忽略常数),记为f(n)
=O(g(n))(见定义1).在极限意义下,f(n)的增长速度受到g(n)的约束,亦即f(n)与g(n)的特征相似.这样,称O(g(n))为算法A的"渐进时间复杂度",
简称"时间复杂度",即考察n→∞时的情况

对算法的时间/空间性质,最重要的是其数量级和趋势,这些是分析算法效率的主要部分,而计量算法基本操作数量的规模函数中的常系数和低阶项可以忽
略不计.:可以认为3*n^2100*n^2属于同1个量级,如果2个算法的时间复杂度分别为这2个函数,就认为它们的效率"差不多",都为n^2#示例:
如果a+b+c=1000,且a^2+b^2=c^2(a,b,c均为自然数),如何求出所有a,b,c可能的组合?
#法1:
for a in range(0,1001):
    for b in range(0,1001):
        for c in range(0,1001):
            if a**2+b**2==c**2 and a+b+c==1000:#本行视为1步
                print("a,b,c:%d,%d,%d"%(a,b,c))#本行视为1步
T1(1000)=2*1000^3=O(1000^3)     T1(n)=2*n^3=O(n^3)
#法2:
for a in range(0,1001):
    for b in range(0,1001-a):
        c=1000-a-b#本行视为1步
        if a**2+b**2==c**2:#本行视为1步
            print("a,b,c:%d,%d,%d"%(a,b,c))#本行视为1步
T(1000)=3*1001*1000/2=O(1000^2)     T(n)=3*(1+n)*n/2=(3*n^2)/2+3*n/2=O(n^2)

(3)注意事项:

①不要将常系数和低阶项放入大O
如不要写成 T ( N ) = O ( 2 N 2 ) , T ( N ) = O ( N 2 + N ) T(N)=O(2N^2),T(N)=O(N^2+N) T(N)=O(2N2),T(N)=O(N2+N),而均应写成 T ( N ) = O ( N 2 ) T(N)=O(N^2) T(N)=O(N2)
因为关注的只是增长率的级别,要求的精度很低

②总能通过 lim ⁡ n → ∞ f ( N ) g ( N ) \displaystyle\lim_{n\to\infty}\frac{f(N)}{g(N)} nlimg(N)f(N)来确定二者的相对增长率:
lim ⁡ n → ∞ f ( N ) g ( N ) = 0 ⇒ f ( N ) = o ( g ( N ) ) \displaystyle\lim_{n\to\infty}\frac{f(N)}{g(N)}=0⇒f(N)=o(g(N)) nlimg(N)f(N)=0f(N)=o(g(N))
lim ⁡ n → ∞ f ( N ) g ( N ) c ≠ 0 ⇒ f ( N ) = Θ ( g ( N ) ) \displaystyle\lim_{n\to\infty}\frac{f(N)}{g(N)}c≠0⇒f(N)=Θ(g(N)) nlimg(N)f(N)c=0f(N)=Θ(g(N))
lim ⁡ n → ∞ f ( N ) g ( N ) = ∞ ⇒ g ( N ) = o ( f ( N ) ) \displaystyle\lim_{n\to\infty}\frac{f(N)}{g(N)}=\infty⇒g(N)=o(f(N)) nlimg(N)f(N)=g(N)=o(f(N))
lim ⁡ n → ∞ f ( N ) g ( N ) \displaystyle\lim_{n\to\infty}\frac{f(N)}{g(N)} nlimg(N)f(N)不存在 ⇒ ⇒ 二者无关

③不要使用类似 f ( N ) ≤ O ( g ( N ) ) f(N)≤O(g(N)) f(N)O(g(N))的说法,这是错误且无意义的

3.最坏时间复杂度:

最优时间复杂度:算法完成工作最少需要多少基本操作
最坏时间复杂度:算法完成工作最多需要多少基本操作
平均时间复杂度:算法完成工作平均需要多少基本操作

最优时间复杂度的价值不大,因为其反映的只是最乐观最理想的情况,没有参考价值
最坏时间复杂度提供了1种保证,表明算法在此量级的基本操作中一定能完成工作
平均时间复杂度,是对算法的1个全面评价,完整全面的反映了某个算法的性质
但这种衡量并没有提供保证,不是每个计算都能在这个量级内完成
而且,对于平均情况的计算,也可能因为应用算法的实例分布不均匀而难以进行
因此,我们主要关注最坏时间复杂度,没有额外说明时,下文的时间复杂度都指最坏时间复杂度

4计算时间复杂度
(1)时间复杂度的几条基本计算规则:

1.基本操作:只有常数项,时间复杂度为O(1)
  #在C语言中通常是语句(结尾有分号的命令)
2.顺序结构:时间复杂度按加法计算
3.循环结构:时间复杂度按乘法计算
4.分支结构:时间复杂度取所有分支中的最大值
5.常系数和低阶项通常可以忽略,而只需要关心最高次项
6.函数不属于基本操作,而是对基本操作的封装,时间复杂度由源码决定
7.没有特别说明时,时间复杂度都指的是最坏时间复杂度

(2)常见时间复杂度:

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^2logn) < O(n^3) < O(2^n) < O(n!) < O(n^n)

在这里插入图片描述
在这里插入图片描述
(3)一些测试和数据:

#这是Python

>>> from timeit import Timer
>>> def test1():
...     l = []
...     for i in range(1000):
...         l = l + [i]
...
>>> def test2():
...     l = []
...     for i in range(1000):
...         l.append(i)
...
>>> def test3():
...     l = [i for i in range(1000)]
...
>>> def test4():
...     l = list(range(1000))
...
>>> def test5():
...     l = []
...     for i in range(1000):
...         l.extend([i])
...
>>> def test6():
...     l = []
...     for i in range(1000):
...             l.insert(0,i)
...
>>> def test7():
...     l = []
...     for i in range(1000):
...             l.insert(i,i)
...
>>> t1 = Timer("test1()", "from __main__ import test1")
>>> print("concat ",t1.timeit(number=1000), "seconds")
concat  1.0837655000004816 seconds
>>> t2 = Timer("test2()", "from __main__ import test2")
>>> print("append ",t2.timeit(number=1000), "seconds")
append  0.058485100000325474 seconds
>>> t3 = Timer("test3()", "from __main__ import test3")
>>> print("comprehension ",t3.timeit(number=1000), "seconds")
comprehension  0.0286645000005592 seconds
>>> t4 = Timer("test4()", "from __main__ import test4")
>>> print("list range ",t4.timeit(number=1000), "seconds")
list range  0.013237400000434718 seconds
>>> t5 = Timer("test5()", "from __main__ import test5")
>>> print("extend ",t5.timeit(number=1000), "seconds")
extend  0.15172119999988354 seconds
>>> t6 = Timer("test6()", "from __main__ import test6")
>>> print("insert first ",t6.timeit(number=1000), "seconds")
insert first  0.2830551999995805 seconds
>>> t7 = Timer("test7()", "from __main__ import test7")
>>> print("insert last ",t7.timeit(number=1000), "seconds")
insert last  0.09288670000023558 seconds

效率:list(range())>[i for i in range()]>append()>insert() at last>extend()>insert() at first>concat

##############################################################################

>>> x = list(range(2000000))
>>> pop_zero = Timer("x.pop(0)","from __main__ import x")
>>> print("pop_zero ",pop_zero.timeit(number=1000), "seconds")
pop_zero  1.2205524000000878 seconds
>>> pop_end = Timer("x.pop()","from __main__ import x")
>>> print("pop_end ",pop_end.timeit(number=1000), "seconds")
pop_end  0.0001419000000169035 seconds

效率:pop() at last>pop() at first

在这里插入图片描述
5.空间复杂度:

存在与时间复杂度相对应的"空间复杂度"来衡量程序占用的内存空间的大小,计算公式为S(N)=O(f(N))(N为问题的规模,f(N)为程序占用的内存空
间的大小,S(N)为程序的空间复杂度).不过通常来说,复杂度指的都是时间复杂度

6.其他渐进符号:

Θ(西塔):紧确界,相当于"="
O(大欧):上界,相当于"<="
o(小欧):非紧的上界,相当于"<"
Ω(大欧米伽):下界,相当于">="
ω(小欧米伽):非紧的下界,相当于">"

四.实例(连续子序列的最大和)

:如果所有元素均小于0,则规定最大和为0

//

1.T(N)=O(N^3)
#include <stdio.h>

int MaxSubSum(int N,int A[N]) {
	int ThisSum,MaxSum=0,i,j,k;

	for(i=0;i<N;i++) {
		for(j=i;j<N;j++) {
			ThisSum=0;

			for(k=i;k<=j;k++) {
				ThisSum+=A[k];
			}

			if(ThisSum>MaxSum) {
				MaxSum=ThisSum;
			}
		}
	}
	return MaxSum;
}

int main(void) {
	int n=5;
	int a[n];
	a[0]=2,a[1]=-3,a[2]=0,a[3]=7,a[4]=-2;
	printf("%d\n",MaxSubSum(n,a));//结果:7
	return 0;
}

//

2.T(N)=O(N^2)
#include <stdio.h>

int MaxSubSum(int N,int A[N]) {
	int ThisSum,MaxSum=0,i,j;
	for(i=0;i<N;i++) {
		ThisSum=0;

		for(j=i;j<N;j++) {
			ThisSum+=A[j];

			if(ThisSum>MaxSum) {
				MaxSum=ThisSum;
			}
		}
	}
	return MaxSum;
}

int main(void) {
	int n=5;
	int a[n];
	a[0]=2,a[1]=-3,a[2]=0,a[3]=7,a[4]=-2;
	printf("%d\n",MaxSubSum(n,a));//结果:7
	return 0;
}

//

3.T(N)=O(NlogN)
该算法属于"分治算法"(Divide-And-Conquer)
想法是先把问题分成2个大致相等的子问题,然后递归求解
再将2个子问题的解合并并进行少量的附加工作,然后得到最终的解

#include <stdio.h>

int max(int a,int b) {
	if(a>b) {
		return a;
	} else {
		return b;
	}
}

int MaxSubSum(int Left,int Right,int A[Right+1]) {
	int MaxLeftSum,MaxRightSum,MaxLeftBorderSum,MaxRightBorderSum,LeftBorderSum,RightBorderSum,Center,i;
	
	if(Left==Right) {
		if(A[Left]>0) {
			return A[Left];
		} else {
			return 0;
		}
	}
	
	Center=(Left+Right)/2;
	MaxLeftSum=MaxSubSum(Left,Center,A);
	MaxRightSum=MaxSubSum(Center+1,Right,A);
	
	MaxLeftBorderSum=0,LeftBorderSum=0;
	for(i=Center;i>=Left;i--) {
		LeftBorderSum+=A[i];
		if(LeftBorderSum>MaxLeftBorderSum) {
			MaxLeftBorderSum=LeftBorderSum;
		}
	}
	
	MaxRightBorderSum=0,RightBorderSum=0;
	for(i=Center+1;i<=Right;i++) {
		RightBorderSum+=A[i];
		if(RightBorderSum>MaxRightBorderSum) {
			MaxRightBorderSum=RightBorderSum;
		}
	}
	
	int Max=max(MaxLeftSum,MaxRightSum);
	Max=max(Max,MaxLeftBorderSum+MaxRightBorderSum);
	
	return Max;
}

int MaxSubseqSum(int N,int A[N]) {
	return MaxSubSum(0,N-1,A);
}

int main(void) {
	int n=5;
	int a[n];
	a[0]=2,a[1]=-3,a[2]=0,a[3]=7,a[4]=-2;
	printf("%d\n",MaxSubseqSum(n,a));//结果:7
	return 0;
}

//

4.T(N)=O(N)
#include <stdio.h>

int MaxSubSum(int N,int A[N]) {
	int ThisSum,MaxSum,j;
	ThisSum=MaxSum=0;
	for(j=0;j<N;j++) {
		ThisSum+=A[j];
		if(ThisSum>MaxSum) {
			MaxSum=ThisSum;
		} else if(ThisSum<0) {
			ThisSum=0;
		}
	}
	return MaxSum;
}

int main(void) {
	int n=5;
	int a[n];
	a[0]=2,a[1]=-3,a[2]=0,a[3]=7,a[4]=-2;
	printf("%d\n",MaxSubSum(n,a));//结果:7
	return 0;
}

该算法还有几个优点:
1.任何数据都只读取1(且为顺序读取),一旦完成对A[i]的读取和处理,就不再需要它了
因此在内存中不必存储数组的元素,故本算法仅需要用于存储ThisSum/MaxSum/j的3块内存空间
2.在任何时刻,该算法都能给出已读入的数据中的最大连续子列和

这种仅需要常量内存空间并以线性时间运行的算法称为"联机算法"(On-Line Algorithm)
这种算法几乎是完美的

五.时间复杂度中的对数

对数常常是算法分析中最困难,最混乱的方面
对数出现规律可概括为:如果1个算法用常数时间(O(1))将问题的规模削减为其一部分(通常是1/2)
那么该算法就是O(logN);而如果使用常数时间只是把问题的规模减少1个常数,则是O(N)的
显然,只有一些特殊问题才可能是O(logN)型的,如若输入N个数,仅读入这些数就会花费Ω(N)的时间
因此,通常说的O(logN)算法的前提都是数据已经提前读入了

1.对分查找(Binary Search;二分查找,折半查找):

给定整数 X , A 0 , A 1 . . . A N − 1 X,A_0,A_1...A_{N-1} X,A0,A1...AN1,其中 A i   ( i = 1 , 2... N − 1 ) A_i\,(i=1,2...N-1) Ai(i=1,2...N1)已经预先排序并已经在内存中了,求使得 A i = X A_i=X Ai=X的下标 i i i(若 X X X不在数据中,则返回 i = − 1 i=-1 i=1)

#include <stdio.h>

int BSearch(int N,int A[N],int X) {
	int Low=0,Mid,High=N-1;
	while (Low<=High) {
		Mid=(Low+High)/2;
		if (A[Mid]<X) {
			Low=Mid+1;
		} else if (A[Mid]>X) {
			High=Mid-1;
		} else {
			return Mid;
		}
	}
	return -1;
}

int main(void) {
	int a[5]={1,2,3,4,5};
	int n=5,x=33;
	int r=BSearch(n,a,x);
	printf("%d\n",r);//结果:-1
	return 0;
}

//对分查找可以看作第1个数据结构实现方法,提供了O(logN)的查找操作
//而所有其他操作均需要O(N)时间(尤其是插入)
//这在数据稳定(即不允许插入/删除)的应用中非常有用
//因为一旦数据输入并排序完成,访问会很快

2.欧几里得算法(辗转相除法):

//使用迭代次数作为运行时间的度量
#include <stdio.h>

unsigned int Gcd(int m,int n) {
    unsigned int r;
    while (n>0) {
    	if (n>m) {//如果n>m,则该次迭代用于交换两数
    		r=m;
    		m=n;
    		n=r;
		} else {
	        r=m%n;
	        m=n;
	        n=r;
	    }
    }
    return m;
}

int main(void) {
	int a=33,b=11;
	int r=Gcd(a,b);
	printf("%d\n",r);//结果:11
	return 0;
}

//可以证明该算法的迭代次数不超过2logN=O(logN)(见下述定理)
//事实上,迭代次数的上限还可以改进为1.44logN
//而平均迭代次数约为(12ln2lnN)/Π^2+1.47

定理:如果 M > N M>N M>N,则 M   m o d   N < M 2 M\,mod\,N<\frac{M}{2} MmodN<2M
在这里插入图片描述

3.幂运算:

//使用乘法次数作为运行时间的度量
#include <stdio.h>

long p(long x,unsigned n) {
	if (n==0) {
		return 1;
	} else if (n==1) {//这2行非必需 
		return x;
	} else if (n%2==0) {
		return p(x*x,n/2);
	} else {
		return p(x*x,n/2)*x;
		//return pow(x,n-1)*x;也可以 
	}
}

int main(void) {
	int a=9,b=3;
	int r=p(a,b);
	printf("%d\n",r);//结果:729
	return 0;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值