Python没有数组(没有直接提供这样的数据结构),搜索算法、排序算法通常针对的对象都是列表,不过换汤不换药,形式变了,算法思想是永恒的
我们分析一个算法,有三种方法(基准评价、统计指令、复杂度分析)最好采用复杂度分析(避免平台环境不同造成的误差和不切实际的统计代价)
复杂度通常指时间复杂度,分为最好情况、最坏情况、平均情况三种情况进行考量——应该特别关注最坏情况的时间复杂度和平均时间复杂度
本章若无特殊说明,都默认作升序排序
顺序搜索就是从头到尾遍历一遍列表,查找是否存在指定的元素项
最好时间复杂度为O(1),最坏情况为O(n),平均情况也为O(n)
二分搜索也叫二叉搜索
二分搜索比顺序搜索更为高效,然而选择何种搜索算法取决于列表中的数据的组织方式,对于二分搜索还有一个额外的整体性代价,就是必须保持列表是有序的
二分搜索的代码实现
def binarySearch(target, sortedLyst):
low = 0
high = len(sortedLyst)-1
while low <= high:
mid = (low+high) // 2
if target == sortedLyst[mid]:
return mid
elif target < sortedLyst[mid]:
high = mid - 1
else:
low = mid + 1
return -low-1
最坏情况下,二分搜索的时间复杂度为O(log2n)
最单纯的想法就是搜索整个列表,找到最小项的位置;如果该位置不是列表的第一个位置的话就交换着两个位置上的项;然后算法回到第二个位置并重复这个过程,如果有必要的话,将最小项和第二个位置的项交换……当算法到达整个过程的最后一个位置,列表就是排序好的了
def selectionSort(lyst):
i = 0
while i < len(lyst) - 1:
minIndex = 1
j = i + 1
while i < len(lyst):
if lyst[j] < lyst[mid]:
minIndex = j
j += 1
if minIndex != i:
swap(lyst, minIndex, i):
i += 1
对于大小为n的列表,外围的循环执行n-1次,在第1次通过外围的循环的时候,内部的循环会执行n-1次;在第2次通过外围循环的时候,内部循环执行n-2次……最后一次通过外围循环的时候,内部循环执行一次
因此,对于大小为n的列表,选择排序总的比较次数为:
(n-1)+(n-2)+···+1 =
n(n-1)/2 =
0.5n*n - 0.5n
对于较大的n,忽略常数系数,选择排序在最坏情况下的时间复杂度为O(n2)
冒泡排序的策略是从列表的开头处开始,比较每一对数据项,直到移动至列表的末尾;每当出现成对的两项顺序不正确时,就交换它们的位置
冒泡排序的效果时把最大项以冒泡的形式排到列表的末尾,然后算法从列表开头至倒数第二个列表项重复上述过程……依次类推,直到该算法从列表的最后一项开始执行,此时列表已经是有序的了
def bubbleSort(lyst):
n = len(lyst)
while n > 1:
i = 1
while i < n:
if lyst[i] < lyst[i-1]:
swap(lyst, i, i-1)
i += 1
n -= 1
类似选择排序的分析过程,在最坏情况下冒泡排序的时间复杂度为O(n2)
如下的一个修改可以把冒泡排序最好情况的时间复杂度提升至线形阶(最坏情况不变)
def bubbleSort(lyt):
n = len(lyst):
while n > 1:
swapped = False
i = 1
while i < n:
if lyst[i] < lyst[i-1]:
swap(lyst, i, i-1)
swaped = True
i += 1
if not swaped: return
n -= 1
插入排序的过程和类似给手中的扑克牌排序:你的手中已经有i-1张牌,在抓取了第i张牌后,需要与手中的这些牌进行比较,直到找到正确的位置进行插入
def insertionSort(lyst):
i = 1
while i < len(lyst):
itemToInsert = lyst[i]
j = i - 1
while j >= 0:
if itemToInsert < lyst[j]:
lyst[j+1] = lyst[j]
j -= 1
else:
break
lyst[j+1] = itemToInsert
i += 1
和选择排序、冒泡排序一样,插入排序在最坏情况下的时间复杂度为O(n2)
值得注意的是,插入排序是不需要swap函数的(不是交换排序)
快速排序是目前已知最快的排序算法(平均角度)
它的最坏情况时间复杂度和前面三个一样是O(n2)
它的平均时间复杂度为O(nlog2n)
快速排序的核心思想是『分而治之』(divide-and-conquer),并很好的利用了『递归』(recursion)的便利性
快速排序的关键是选取『基准点』(pivot)
基准点的选取方法
1)固定基准点(缺陷:不安全、不稳定,首数据的原始状态制约——如果数据在调用快速排序之前就已经有序的话,将退化为平方阶的时间复杂度)
1)随机数选取(缺陷:随机数的产生消耗时间复杂度,拖慢了整体的时间性能)
2)三数中值法(推荐采纳方案):三个数分别是数据集合的最左端、最右端、和中间值,我们对这三者排序,然后取它们的中间的那个值
例如(e.g.):2,5,4,3,6,8,1,0,9,7,中间的数下标为9/2=4,对应的数是6,最左边是2,最右边是7,排序下,得到pivot是6
在代码实现中,选取基准点的方法叫"partition(分割、分开)"
说明下,pivot指元素对应的值,pivotLocation指pivot对应的下标
快速排序的实现步骤(pivot选取采用三数中值法)
1)设置两个变量i、j,排序开始的时候,i=0,j=n-1
2)以中间下标对应元素作为关键数据,即:pivot=A[middle]
3)从i开始向后搜索(i++),如果发现某项大于pivot,则swap(A[i], A[j])
4)从j开始向前搜索(j–),如果发现某项小于pivot,则swap(A[i], A[j])
5)重复3、4步骤,直到i==j
Python代码实现快速排序
"""如下的脚本为客户定义了一个顶层的quicksort函数;一个递归的quickHelper函数,它隐藏了用于子列表终点的额外参数;还有一个partition函数,用来选取基准点pivot"""
def quickssort(lyst):
quickSort(lyst, 0, len(lyst)-1)
def quicksortHelper(lyst, low, high):
if low < high:
pivotLocation = partition(lyst, low, high)
quicksortHelper(lyst, low, pivotLocation-1)
quicksortHelper(lyst, pivotLocation+1, high)
def partition(lyst, low, high):
# Find the pivot and exchange it with the last item
middle = (low+high) // 2
pivot = lyst[middle]
swap(lyst, middle, high)
# Set the boundary point to first position
boundary = low
# Move items less than pivot to the left
for index in range(low, high):
if lyst[index] < pivot:
swap(lyst, index, boundary) # lyst[index], lyst[boundary] = lyst[boundary], lyst[index]
boundary += 1
# Exchange the pivot item and the boundary item
swap(lyst, high, boundary)
return boundary
快速排序的另一种实现
def partition(lyst, low, high):
int mid = (low+high) // 2
if lyst[low] > lyst[high]:
swap(lyst, low, high) # 保证左边较小
if lyst[mid] > lyst[high]:
swap(lyst, mid, high) # 保证中间较小
if lyst[mid] > lyst[low]:
swap(lyst, mid, low) # 保证左边较小
int pivot = lyst[low] # 此时左边位置为基准点
# 如果就此结束,则是固定基准点
# 随机基准点
# randomIndex = rand() % (high - low) + low; #取数组中随机下标
# swap(array, randomIndex, low); # 与第一个数交换
# pivot = lyst[low];
# 三数取中法
while i < j:
while lyst[j] >= pivot and i < j:
j -= 1
while lyst[i] <= pivot and i < j:
i += 1
if i < j:
swap(lyst, i, j) # lyst[i], lyst[j] = lyst[j], lyst[i]
swap(lyst, low, i) # 将基准点归位
return i
def quicksortHelper(lyst, low, high):
if low < high:
pivotLocation = partition(lyst, low, high)
quicksortHelper(lyst, low, pivotLocation-1)
quicksortHelper(lyst, pivotLocation+1, high)
def quicksort(lyst):
quicksortHelper(lyst, 0, len(lyst)-1)
归并排序(merge sort)也叫合并排序和快速排序一样利用了递归、分而治之的策略突破O(n2)的限制
归并排序的关键在于归并
有三个Python函数在这个顶层的设计策略中协作
1)mergeSort:用户调用的函数
2)mergeSortHelper:一个辅助函数,它隐藏了递归调用需要的额外参数
3)Merge:实现归并过程的一个函数
from arrays import Array
def mergeSort(lyst):
# lyst list being sorted
# copyBuffer temporary space during merge
copyBuffer = Array(len(lyst))
mergeSortHelper(lyst, copyBuffer, 0, len(lyst)-1)
def mergeSortHelper(lyst, copyBuffer, low, high):
# lyst list being sorted
# copyBuffer temp space needed during merge
# low, high bounds of sublist
# middle midpoint of sublist
if low < high:
middle < 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):
# lyst list being sorted
# copyBuffer temp space needed during merge process
# low begining of first sorted sublist
# middle end of first sorted sublist
# middle+1 begining of second sorted sublist
# high end of second sorted sublist
# Initialize i1 and i2 to the first items in each sublist
i1 = low
i2 = middle + 1
# Interleave items from the sublist into the
# copyBuffer i such a way that order is maintained
for i in range(low, high+1):
if i1 > middle:
copyBuffer[i] = lyst[i2] # First sublist exhausted
i2 += 1
elif i2 > high:
copyBuffer[i] = lyst[i1] # Second sublit exhausted
i1 += 1
elif lyst[i1] < lyst[i2]:
copyBuffer[i] = lyst[i1] # Item in first sublist
i1 += 1
else:
copyBuffer[i] = lyst[i2] # Item in second sublist
i2 += 1
for i in range(low, high+1): # Copy sorted item back to
lyst[i] = copyBuffer[i] # proper position in lyst