没有学习过数据结构算法之类专业毕业的,因为特地学习了下,收货挺多,记录下~
我们编写计算机程序的目的是解决我们实际的应用问题
首先
计算机科学研究的是什么
-
计算机科学不仅仅是对计算机的研究
-
计算机科学主要研究的是问题、问题解决过程以及问题的解决方案
为了更好地处理机器相关性或独立性,引入了抽象的概念
那么什么是抽象呢,我们举个例子 -
比如说汽车,从司机观点来看,汽车是一台可以带人去往目的地的代步工具
-
从抽象角度说,司机看到汽车的逻辑层次
-
这些操作机构(方向盘、油门、挡位)就称为“接口Interface”
抽象我们可以通过编程实现
而编程是通过一种程序设计语言,将抽象的算法实现为计算机可以执行的代码的过程,没有算法,编程无从谈起
图灵奖获得者Niklaus Wirth的著名公式:算法+数据结构=程序 -
程序设计语言需要为算法的实现提供实现过程和数据的机制,具体表现为控制结构和数据类型
-
程序设计语言均有语句对应控制结构,比如说顺序处理、分支选择、循环迭代
-
程序设计语言也提供最基本的数据类型来表示数据,比如整数、字符等等,对于复杂问题而言,直接使用这些基本数据类型不利于算法的表达
因此我们引发出来一种抽象数据类型ADT(Abstract Data Type),ADT是对数据进行处理的一种逻辑描述,并不涉及如何实现这些处理;并且它建立了一种对数据的封装,封装技术将可能的处理实现细节隐蔽起来,能有效的控制算法的复杂度
- 同一个ADT可以采用不同的数据结构来实现
- 采用程序设计语言的控制结构和基本数据类型来实现ADT所提供的逻辑接口
- 对数据实现“逻辑”层次和“物理”层次的分离,可以定义复杂的数据模型来解决问题,而不需要立即考虑次模型如何实现
刚提到了接口,我们也可以理解为接口的两端就是抽象与实现
- 由于对抽象数据类型可以有多种实现方案
- 独立于实现的数据模型
- 通过层层抽象,降低问题解决过程的复杂度~
我们为什么要学习和研究算法呢
- 首先,我们就是在学习各种不同问题的解决方案
- 其次,各种算法通常有较大的差异
- 在某些情况下,当我们遇到棘手的难题
- 某些问题解决需要一些折中的处理方式
什么是算法分析
- 算法是对问题解决的分步描述
- 程序则是采用某种编程语言实现的算法,同一个算法通过不同的程序员采用不同的编程语言,能产生很多程序
- 比较程序的好坏,有很多的因素,比如说代码风格、可读性等等,而我们主要感兴趣的是算法本身的特性
算法分析主要就是从计算资源消耗的角度来评判和比较算法——更高效利用计算资源,或者更少占用计算资源的算法就是好算法。
何为计算资源?
- 一种是算法解决问题过程中需要的存储空间或内存;但是存储空间受到问题自身数据规模的变化影响,要区分那些存储空间是问题本身描述所需,哪些是算法占用,不容易
- 另一种是算法的执行时间,我们可以对程序进行实际运行测试,获得真实的运行时间,在python中,我们可以通过time来打印出程序运行的开始结束时间
但是关于运行时间的检测实际上这样是不全面的,同一个算法,采用不同的编程语言编写,放在不同的机器上运行,得到的运行时间也会不一样,有时候会大不一样,因此我们需要更好的方法来衡量算法运行时间。
大O表示法
一个算法所实施的操作数量或步骤数可作为独立于具体程序/机器的度量指标
赋值语句是一个合适的选择
问题规模影响算法执行时间
- 问题规模:影响算法执行时间的主要因素
- 在前n个整数累计求和的算法中,需要累计的整数个数合适作为问题规模的指标
- 算法分析的目标是找出问题规模会怎么影响一个算法的执行时间
数量级函数 Order of Magnitude - 基本操作数量函数T(n)的精确值并不是特别重要,重要的是T(n)中起决定性因素的主导部分
- 数量级函数描述了T(n)中随着n增加而增加速度最快的主导部分,称作”大O“表示法,记作O(f(n)),其中f(n)表示T(n)中的主导部分
例如
T(n) = 1+n
当n增大时,常数1在最终结果中显得越来越无足轻重,所以可以去掉1,保留n作为主要部分,运行时间的数量级就是O(n)
再比如 T(n)=5n^2+27n+1005
当n很小时,常熟1005起决定性作用
但当n越来越大,n^2项就越来越重要了,其他两项对结果的影响越来越小
同样,n^2项中的系数5,对于其增长速度来说也影响不大
所以可以在数量级中去掉27n+1005,以及系数5的部分,确定为O(n^2)
影响算法运行时间的其他因素:
- 有时决定运行时间的不仅是问题规模
- 某些具体数据也会影响算法运行时间,分为最好、最差和平均情况,平均状况体现了算法的主流性能,对算法的分析要看主流,而不被某种特定的运行状况所迷惑
常见的大O数量级函数
通常当n很小的时候,难以确定其数量级
当n增长到较大时,容易看出其主要变化量级,比如说对数、常数、线性、平方、立方、指数
其他算法复杂度表示法
大O表示法,表示了所有上限中最小的那个上限
大Ω表示法,表示了所有下限中最大的那个下限
大𝚹表示法,如果上下限相同,那么就可以用大𝚹
变位词的判断问题
所谓”变位词“是指两个词之间存在组成字母的重新排列关系
如:heart和earth,python和typhon
为了简单起见,假设参与判断的两个词仅由小写字母构成,而且长度相等
解题目标:写一个bool函数,以两个词作为参数,返回这两个词是否变位词
解法一:逐字检查
问题规模:包含字符个数是n
且主要部分在于两重循环
所以总的执行次数是1+2+3+4+5…+n,所以数量级是0(n^2)
def anagramSolution1(s1, s2):
alist = list(s2)
pos1 = 0
stilloK = True
while pos1 < len(s1) and stilloK:
pos2 =0
found = False
while pos2 < len(alist) and not found:
if s1[pos1] == alist[pos2]:
found = True
else:
pos2=pos2+1
if found:
alist[pos2] = None
else:
stilloK = False
pos1= pos1+1
return stilloK
print(anagramSolution1('python','typhon'))
解法二
排序比较:将两个字符串都按照字母顺序排好序,再逐个字符对比是否相同,如果相同则是变位词,有任何的不同就不是变位词
def anagramSolution2(s1, s2):
alist1 = list(s1)
alist2 = list(s2)
#转为列表
alist1.sort()
alist2.sort()
pos = 0
#分别排序
matches = True
while pos < len(s1) and matches:
if alist1[pos] == alist2[pos]:
pos = pos+1
else:
matches = False
return matches
print(anagramSolution2('python','typhon'))
解法三
暴力法:穷尽一个所有可能的组合,将S1出现的字符进行全排列,再查看S2是否出现在全排列列表中,这里最大的困难是产生S1所有字符的全排列,对于越长的字符串,所需的时间和复杂度也越来越大,因此,暴力法恐怕不算是一个好的算法
解法四
计数比较:对比两个词中每个字符出现的次数,如果26个字母出现的次数都相同的话,这两个字符串就一定是变位词;
具体做法:为每个词设定一个26位的计数器,先检查每个词,在计数器中设定好每个字母出现的次数;计数完成后,进入比较阶段,看两个字符串的计数器是否相同,如果相同,则两个字符串是变位词
计数比较中有3个循环迭代,但不像解法1那样存在嵌套循环
前两个循环用于对字符串进行计数,操作次数等于字符长度n
第3个循环用于计数器比较,操作总次数是26次
所以总的操作次数T(n) = 2n+26,其数量级为O(n),这是一个线性数量级的算法,是4个变位词判断中性能最优的
def anagramSolution4(s1, s2):
C1= [0]* 26
C2= [0]* 26
for i in range(len(s1)):
pos = ord(s1[i])- ord('a')
C1[pos] = C1[pos] +1
for i in range(len(s2)):
pos = ord(s2[i]) - ord('a')
C2[pos] = C2[pos] + 1
stilloK = True
j = 0
while j < 26 and stilloK:
if C1[j] == C2[j]:
j = j+1
else:
stilloK = False
return stilloK
print(anagramSolution4('apple','pleap'))
值得注意的是,本算法依赖于两个长度为26的计数器列表,来保存字符计数,这相比前3个算法需要更多的存储空间,牺牲存储空间换取运行时间,或者相反,这种在时空之间的取舍和权衡,在选择问题解法的过程中经常会出现。