本系列为《Python数据结构与算法分析》第二版学习笔记,作者:布拉德利.米勒;戴维.拉努姆。译:吕能, 刁寿钧
排序是指将集合中的元素按某种顺序排列的过程。与搜索算法类似,排序算法的效率与待处理元素的数目相关。
1、冒泡排序
冒泡排序多次遍历列表,它比较相邻的元素,将不和顺序的交换,每一轮遍历都将下一个最大值放在正确的位置上。
特别的,如果在一轮遍历中没有发生元素交换,就可以确定列表已经有序,此时可以终止遍历。
冒泡排序函数
def shortBubbleSort(alist):
exchanges = True
passnum = len(alist) - 1
while passnum > 0 and exchanges:
exchanges = False
for i in range(passnum):
if alist[i] > alist[i+1]:
exchanges = True
alist[i], alist[i + 1] = alist[i + 1], alist[i]
passnum -= 1
return alist
给定有n个元素的列表排序总共需要遍历n-1遍,则总的比较次数为:
所以,冒泡排序的时间复杂度为O(n2)。
2、选择排序
选择排序在冒泡排序的基础上做了改进,每次遍历列表只做一次交换。每次遍历时寻找最大值,并在遍历完之后将它放在正确的位置上。和冒泡排序一样,第一次遍历后,最大的元素就位,第二次遍历后,第二大的元素就位,依此类推。
选择排序函数:
def selectionSort(alist):
for fillslot in range(len(alist)-1, 0, -1):
positionOfMax = 0
for location in range(1, fillslot+1):
if alist[location] > alist[positionOfMax]:
positionOfMax = location
alist[fillslot], alist[positionOfMax] = alist[positionOfMax], alist[fillslot]
return alist
选择排序的时间复杂度为O(n2)。
3、插入排序
插入排序在列表较低的一端维护一个有序的子列表,并逐个将每个新元素插入这个子列表。
插入排序函数:
def insertionSort(alist):
for index in range(1, len(alist)):
currentvalue = alist[index]
position = index
while position > 0 and alist[position-1] > currentvalue:
alist[position] = alist[position-1]
position -= 1
alist[position] = currentvalue
return alist
在最坏的情况下,插入排序算法的比较次数是前n-1个整数之和,对应的时间复杂度是O(n2)。在基准测试中,插入排序的性能很不错。
4、希尔排序
希尔排序也称“递减增量排序”,它对插入排序做了改进,将列表分成数个子列表,并对每一个子列表插入排序。
希尔排序函数
def shellSort(alist):
sublistcount = len(alist) // 2
while sublistcount > 0:
for startpositon in range(sublistcount):
gapInsertionSort(alist, startpositon, sublistcount)
print("After increments of size", sublistcount, "The list is", alist)
sublistcount = sublistcount // 2
return alist
希尔排序的时间复杂度大概介于O(n)和O(n2)之间,通过改变增量,比如采用2k-1,希尔排序的时间复杂度可以达到O(n3/2)。
5、归并排序
归并排序是一种递归算法,每次将一个列表一分为二。如果列表为空或只有一个元素,那么从定以上来说它就是有序的。如果列表不止一个元素,就将列表一分为二,并对两部分都递归调用归并排序,当两部分都有序后,就进行归并这一基本操作。归并是指将两个较小的有序列表归并为一个有序列表的过程
归并排序函数
def mergeSort(alist):
if len(alist) > 1:
mid = len(alist) // 2
lefthalf = alist[:mid]
righthalf = alist[mid:]
mergeSort(lefthalf)
mergeSort(righthalf)
i = 0
j = 0
k = 0
while i < len(lefthalf) and j < len(righthalf):
if lefthalf[i] < righthalf[j]:
alist[k] = lefthalf[i]
i += 1
else:
alist[k] = righthalf[j]
j += 1
k += 1
while i < len(lefthalf):
alist[k] = lefthalf[i]
i += 1
k += 1
while j < len(righthalf):
alist[k] = righthalf[j]
j += 1
k += 1
return alist
归并排序的时间复杂度是O(nlogn),mergeSort函数需要额外的空间来存储切片操作得到的两半部分。当列表较大时,使用额外的空间可能会使排序出现问题。
6、快速排序
和归并排序一样,快速排序也采用分治策略,但不适用额外的存储空间。不过,代价是列表可能不会被一分为二,这时,算法的效率会有所降低。
def quickSort(alist):
quickSortHelper(alist, 0, len(alist)-1)
def quickSortHelper(alist, first, last):
if first < last:
splitpoint = partition(alist, first, last)
quickSortHelper(alist, first, splitpoint-1)
quickSortHelper(alist, splitpoint+1, last)
def partition(alist, first, last):
pivotvalue = alist[first]
leftmark = first + 1
rightmark = last
done = False
while not done:
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark += 1
while alist[rightmark] >= pivotvalue and rightmark >= leftmark:
rightmark -= 1
if rightmark < leftmark:
done = True
else:
alist[leftmark], alist[rightmark] = alist[rightmark], alist[leftmark]
alist[first], alist[rightmark] = alist[rightmark], alist[first]
return rightmark
对于长度为n的列表,如果划分操作总是在列表的中部,就会切分logn次。为了找到分割点,n个元素都要与基准值比较。所以,时间复杂度是O(nlogn)。
另外,最坏情况下,分割点不在列表的中部,而是偏向某一端,这会导致切分不均匀。这种情况下,含有n个元素的列表可能被分成一个不含元素的列表与一个含有n-1个元素的列表,然后,含有n-1个元素的列表可能被分成不含元素的列表与一个含有n-2个元素的列表,依次类推,这会导致时间复杂度变为O(n2),因为还要加上递归的开销。