算法描述了最终能解决一个问题的计算过程。可读性和易维护性是重要的质量指标。
在计算机上运行算法会消耗两种资源:处理时间和空间或内存。当解决相同的问题或处理相同的数据集的时候,消耗这两种资源较少的算法会比消耗资源更多的算法具有更高的质量,因此,它也是更加合适的算法。
3.1.1度量算法的运行时间
# -*- coding:utf-8 -*- import time problemSize = 10000000 print('%12s%16s' % ('ProblemSize', 'second')) for count in range(5): # 定义起始时间 start = time.time() work = 1 # 循环遍历问题大小 for x in range(problemSize): # 使用work增减统计次数 work += 1 work -= 1 # 再次调用时间减去开始时间得到运行时间 elapsed = time.time() - start print('%12s%16.3f' % (problemSize, elapsed)) # 问题大小翻倍 problemSize *= 2
ProblemSize second 10000000 1.553 20000000 2.749 40000000 5.514 80000000 11.129 160000000 29.064
不同硬件平台的处理速度不同,因此,一个算法的运行时间,在不同机器上是不同的。
对于很大的数据集合来说,确定某些算法的运行时间是不切实际的。
3.1.2统计指令
#-*- coding:utf-8 -*- problemSize = 1000 print('%12s%15s' % ('ProblemSize', 'Iterations')) for count in range(5): #定义次数 number = 0 work = 1 #循环嵌套遍历问题大小的平方 for j in range(problemSize): for k in range(problemSize): #统计次数 number += 1 work += 1 work -= 1 print('%12d%15d' % (problemSize, number)) #循环一次后问题大小翻倍 problemSize *= 2
ProblemSize Iterations 1000 1000000 2000 4000000 4000 16000000 8000 64000000 16000 256000000
估算算法性能的另一种技术,是统计对不同的问题规模所要执行的指令的数目。
#-*- coding:utf-8 -*- from counter import Counter #递归实现Fibonacci函数 def fib(n, counter): #递增一次,并记数 counter.incremnet() if n < 3: return 1 else: return fib(n-1, counter) + fib(n-2, counter) problemSize = 2 print('%12s%15s' % ('ProblemSize', 'Calls')) for count in range(5): counter = Counter() fib(problemSize, counter) print('%12d%15d' % (problemSize, counter)) problemSize *= 2
计算斐波那契数列对不同问题规模的调用次数。
统计指令方法的问题在于当问题规模过大时,计算机还是无法运行的足够快。统计指令是正确的思路。
3.1.3度量算法所使用的内存
对于算法所使用的资源的完整分析,还包括所需的内存数量。一些算法对于任何问题都需要相同大小的内存,另一些算法则会随着问题规模越来越大,从而需要更多的内存。
3.2复杂度分析
对于大多数问题规模n来说,线性行为所做的工作比二次阶的行为所做的工作要少很多。
如果一个算法对于任何的问题规模,都需要相同的操作次数,那么它具有常数阶的性能。
另一种复杂度的阶要比线性阶好一些,但是比常数阶差一些,这就是对数阶。
比多项式阶更差一些的复杂度阶,叫做指数阶。对于较大的问题规模来说,运行指数阶算法是不切实际的。
3.3搜索算法
3.3.1搜索最小值
def fMin(lyst): cIndex = 1 minIndex = 0 while cIndex < len(lyst): if lyst[cIndex] < lyst[minIndex]: minIndex = cIndex cIndex += 1 return minIndex print(fMin([1, 3, 4, 5, 1, 5, 6]))
这个算法遍历整个数列,所以时间复杂度是O(n)
3.3.2顺序搜索一个列表
#-*- coding:utf-8 -*- def sList(target, lyst):#查找目标的值是否在列表中 position = 0 while position < len(lyst): if target == lyst[position]: return position position += 1 #没找到则返回-1 return -1 print(sList(3, [1, 3, 4, 5, 1, 5, 6]))
3.3.3最好情况,最坏情况和平均情况的性能
有些算法的性能取决于所处理数据的放置方式。
顺序搜索的分析考虑三种情况:
1.最坏情况,目标在列表末尾,或者根本不在列表中,复杂度为O(n)
2.最好情况一次就找到目标,复杂度为O(1)
3.平均情况要把在每一个可能的位置找到目标项所需迭代次数相加,并除以n(列表的值数)。复杂度约为O(n)。
顺序搜索的最好情况的性能是很少见的,而平均情况和最坏情况的性能则基本相同。
3.3.4有序列表的二叉搜索
当搜索排序的数据的时候,可以使用二叉搜索。
def binarySearch(target, soredLyst): left = 0 right = len(soredLyst) - 1 while left <= right: midpoint = (left + right) // 2 if target == soredLyst[midpoint]: return midpoint elif target < soredLyst[midpoint]: right = midpoint - 1 else: left = midpoint + 1 return -1 print(binarySearch(3, [1, 2, 3, 5, 6]))
二叉搜索可能比顺序搜索更为高效,但必须保证列表是有序的。
3.3.5比较数据项
class SavingAccount(object): def __init__(self, name, pin, balance = 0.0): self._name = name self._pin = pin self._balance = balance def __lt__(self, other): return self._name < other._name
__lt__方法对两个账户的对象_name字段调用了<运算符。
为了允许算法对一个新对象的类使用比较运算符==,<,>,在该类中应定义__eq__,__lt__,__gt__方法。
3.4.1选择排序
#-*- coding:utf-8 -*- def selectionSort(lyst): #起始为第一个位置 i = 0 #当i未超出列表范围时循环 while i < len(lyst) - 1: #默认最小值索引为i mIndex = i #j为最小值后一位数 j = i + 1 #循环嵌套判断j是否为最小值 while j < len(lyst): if lyst[j] < lyst[mIndex]: mIndex = j #循环最后j+1判断下一个值 j += 1 #如果最小值索引不等于i,则交换两者的值 if mIndex != i: swap(lyst, mIndex, i) #一次大循环后i自增1,排列第二个位置 i += 1 return lyst def swap(lyst, i, j): temp = lyst[i] lyst[i] = lyst[j] lyst[j] = temp print(selectionSort([5, 3, 1, 2, 4]))
选择排序在各种情况下的复杂度为O(n2)。
3.4.2冒泡排序
#-*- coding:utf-8 -*- def bubbleSortWithTweak(lyst): #定义n为列表的长度 n = len(lyst) #列表不为0时循环 while n > 1: #未转换为False swapped = False #定义起始位置为1 i = 1 #当n大于起始位置时循环 while i < n: #如果前一位数大于后一位数,交换它们的值 if lyst[i] < lyst[i-1]: swap(lyst, i, i-1) swapped = True #循环最后i自增1 i += 1 #如果是已经排好的列表则直接返回 if not swapped:return #大循环最后n自减1,表示开始进行下一次比较 n -= 1 return lyst def swap(lyst, i, j): temp = lyst[i] lyst[i] = lyst[j] lyst[j] = temp print(bubbleSortWithTweak([5,4,2,1,3]))
冒泡排序的复杂度也是O(n2)
3.4.3插入排序
和其他排序算法一样,插入排序包含两个循环。外围的循环遍历从1到n-1的位置。对于这个循环中的每一个位置i,我们都保存该项并且从位置i-1开始内部循环。对于这个循环的每一个位置j,我们都将项移动到位置j+1,直到找到了给保存的项(第i项)的插入位置。
#-*- coding:utf-8 -*- def insertionSort(lyst): #定义i为1 i = 1 #当i小于列表长度时循环 while i < len(lyst): #默认插入点为第二个列表值 itemToInsert = lyst[i] #定义j为i前一位数 j = i - 1 #当j大于等于0时开始循环 while j >= 0: #如果插入点的值小于前一位的值,则将j后一项和j调换 if itemToInsert < lyst[j]: lyst[j+1] = lyst[j] #调换一次后j自减1 j -= 1 else: break #内循环结束后将j后一项值向插入点 lyst[j + 1] = itemToInsert #大循环结束后i自增1,插入点向后移 i += 1 return lyst print(insertionSort([2, 5, 1, 4, 3]))
关键在与内循环,使小的值不断上移,直到合适位置。
插入排序的最坏情况的复杂度是O(n2)。
3.4.4再谈最好情况,最坏情况和平均情况性能
最好情况:在该条件下。算法做的工作最少。
最坏情况:算法做的事最多。
平均情况:算法做的工作最典型。
3.5.1快速排序
#-*- coding:utf-8 -*- #将列表的起始位置和结束位置和列表传给Helper函数 def quicksort(lyst): quicksortHelper(lyst, 0, len(lyst) - 1) def quicksortHelper(lyst, left, right): #如果列表不为0 if left < right: pivotL = partition(lyst, left, right) #左值排序 quicksortHelper(lyst, left, pivotL - 1) #右值排序 quicksortHelper(lyst, pivotL + 1, right) #旨在将小于中值的数向左,大于中值的数在右 def partition(lyst, left, right): #中点位置为起点与终点和的一半 middle = (left + right) // 2 #找到中点值并与最后一个值交换 pivot = lyst[middle] lyst[middle] = lyst[right] lyst[right] = pivot #指向第一个位置 boundary = left #在列表中循环 for index in range(left, right): #如果最后的值大于前面的值,则将比pivot小的值往左移 if lyst[index] < pivot: swap(lyst, index, boundary) boundary += 1 #交换pivot和boundary swap(lyst, right, boundary) return boundary def swap(lyst, i, j): temp = lyst[i] lyst[i] = lyst[j] lyst[j] = temp import random def main(size = 20, sort = quicksort): lyst = [] for count in range(size): lyst.append(random.randint(1, size + 1)) print(lyst) sort(lyst) print(lyst) if __name__ == '__main__': main()
思路:分割左边的子列表,在中值左右产生两个子列表,对子列表重复分割过程,直到子列表的长度最大为1.
3.5.2合并排序
#-*- coding:utf-8 -*- from arrays import Array def mergeSort(lyst): copyBuffer = array(len(lyst)) mergeSortHelper(lyst, copyBuffer, 0, len(lyst) - 1) def mergeSortHelper(lyst, copyBuffer, low, high): if low < high: middle = (low + high) // 2 mergeSortHelper(lyst, copyBuffer, low, middle) mergeSortHelper(lyst, copyBuffer, middle + 1, high) merge(lyst, copyBuffer, low, middle, high) def merge(lyst, copyBuffer, low, middle, high): i1 = low i2 = middle + 1 for i in range(low, high + 1): if i1 > middle: copyBuffer[i] = lyst[i2] elif i2 > high: copyBuffer[i] = lyst[i1] elif lyst[i1] <lyst[i2]: copyBuffer[i] = lyst[i1] i1 += 1 else: copyBuffer[i] = lyst[i2] i2 += 1 for i in range(low, high + 1): lyst[i] = copyBuffer[i] import random def main(size = 20, sort = mergeSort): lyst = [] for count in range(size): lyst.append(random.randint(1, size + 1)) print(lyst) sort(lyst) print(lyst) if __name__ == '__main__': main()
计算一个列表的中间位置,并且递归地排序其左边和右边的子列表。
将俩个排好序的子列表重新合并成单个排好序的列表。
当子列表不再能够划分的时候停止这个过程。