常见的排序算法
排序算法可以从不同角度,按不同方式分类,下面是一些常见的排序算法:
- 插入排序
- 选择排序
- 交换排序
- 分配排序
- 归并排序
- 外部排序
记录结构,后面讨论排序算法是,使用的示例数据结构就是一个表,假定表中元素是下面定义的record类的对象 :
class record: def __init__(self, key, datum): self.key = key self.datum = datum
排序只关心record对象的key成分,但为了完成排序,经常需要把整个对象搬来搬去。
插入排序:顾名思义其基本操作方式是插入,不断把一个个元素插入一个序列中,最终得到排序序列。为此只需要维持好所构造序列的序列性质,最终就能得到结果。
- 算法的实现:插入的过程中需要一个个地处理未排序元素,最简单的方法是按下标处理,处理了一个元素就能留下一个空位,如果该空位与已排序序列相连,就可以直接用作该序列的延伸位置。把这些考虑综合起来,就得到如下图所示的状态安排,其中在连续表的前段积累已排序序列,通过这个序列的不断生长,最终完成整个序列的排序工作。如图所示,连续表的右边一段是尚未处理的元素,每次考虑这段的最左元素,即途中i标识的元素。

- 插入排序算法就是找到第i个元素在前段的插入位置,并维持其余元素的序对顺序,就得到record序列的插入排序算法
def insert_sort(lst): for i in range(1, len(lst)): x = lst[i] j = i while j>0 and lst[j-1].key > x.key: lst[j] = lst[j-1] # 反序逐个后移元素,确定插入位置 j -= 1 lst[j] = x
- 算法分析:
- 空间复杂度:计算中只用了两个简单变量,用于辅助定位和完成序列元素的位置转移,因此算法的空间复杂度是O(1),与序列大小无关。
- 时间复杂度:最坏情况下时间复杂度是O(n**2),最好情况是O(n),所以算法时间复杂度仍然是O(n**2)
- 这个算法是是稳定的,因为在内层循环中检索插入位置的过程中,一旦发现前面元素与当前元素关键码相同,就不再移动元素了,这种做法保证不会交换位置,因此算法稳定。
- 插入排序算法的变形:采用二分法检索插入位置
选择排序:选择合适记录,严格按递增方式选出记录(每次选最小元素),简单的顺序排放就能完成排序工作
- 选择排序基本思想:
- 维护需要考虑的所有记录中最小的i个记录的已排序序列
- 每次从剩余未排序的记录中选取关键码最小的记录,将其放在已排序序列记录的后面,作为序列的第i+1个记录,使已排序序列增长
- 以空序列作为排序工作的开始,做到尚未排序的序列里只剩一个元素是(它必然为最大),只需要直接将其放在已排序的记录之后,整个排序就完成了
- 算法实现:最简单的选择方法是顺序扫描序列中的元素,记住遇到的最小元素,一次扫描完毕就找到了一个最小元素,反复扫描就能完成排序工作。另一方面,选出了一个元素,原来的序列中就出现了一个空位,可以把这些空位集中起来存放排序好的序列 。如图所示,在排序过程中的任何时刻,表的前段积累了一批递增的已经排好序的记录,而且他们都不大于任何 一个未排序记录,下一步从未排序中选出最小记录,将其存放在已排序记录的后面,这样只剩一个记录其关键码一定最大,工作即可完成。

def select_sort(lst): for i in range(len(lst)-1): k = i for j in range(i, len(lst)): if lst[j].key < lst[k].key: k = j if i != k: lst[i], lst[k] = lst[k], lst[i]
- 算法分析
- 空间复杂度:算法中只用到几个变量,空间复杂度是O(1)
- 时间复杂度:因为有两次循环,所以时间复杂度是O(n^2)
- 算法稳定性:由于存在可能造成关键码相同元素交换次序,所以不稳定
- 效率:低于插入排序
交换排序:一个序列中的记录没排好序,那么其中一定有逆序存在,如果交换所发现的逆序记录对,得到的序列将更接近排序序列,通过不断减少序列中的逆序,最终可以得到排序序列,采用不同的确定逆序方法和交换方法,可以得到不同的交换排序序列。
- 起泡排序:起泡排序是一种典型的(也是最简单的)通过交换元素消除逆序实现排序的方法,其中的基本操作是比较相邻记录,发现相邻的逆序对时就交换它们,通过反复比较和交换,最终完成整个序列的排序工作
- 算法实现:
def bubble_sort(lst): for i in range(len(lst)):
found = False for j in range(1, len(lst)-1): if lst[j-1].key > lst[j].key: lst[j-1], lst[j] = lst[j], lst[j-1]
found = True
if not found:
break
- 算法分析:
- 时间复杂度:平均时间复杂度是O(n^2)
- 空间复杂度:O(1)
- 效率:比较低
快速排序:快速排序是实践中平均速度最快的算法之一,算法中也采用了逆序和交换位置的方法,但算法中最基本的思想是划分,即按照某种标准把考虑的记录划分为“小记录 ”和“大记录”,并通过递归不断划分,最终得到一个排序的序列
- 算法基本过程:
- 选择一种标准,把被排序序列中的记录按照这种标准分为大小两组,显然,从整体的角度,这两组记录的顺序已定,较小一组的记录应该排在前面。
- 采用同样方式,递归的分别划分得到的这两组记录,并继续递归的划分下去
- 划分总是得到越来越小的分组,如此工作下去直到每个记录组中最多包含一个记录时,整个序列的排序完成
- 算法实现:
def quick_sort(lst): qsort_rec(lst, 0, len(lst)-1) def qsort_rec(lst, l, r): if l>=r: # 分段无记录或只有一个记录 return i = l j = r pivot = lst[i] # lst[i] 是初始空位 while i < j: # 找到pivot的最终位置 while i < j and lst[j].key >= pivot.key: # 用j向左扫描找到小于pivot的记录 j -= 1 if i < j: lst[i] = lst[j] i += 1 # 小记录移到左边 while i < j and lst[i].key <= pivot.key: # 用i向右扫描找到大于pivot的记录 i += 1 if i < j: lst[j] = lst[i] j -= 1 # 大记录移到右边 lst[i] = pivot # 将pivot存入其最终位置 qsort_rec(lst, 1,i-1) # 递归处理左半区域 qsort_rec(lst, i+1, r) # 递归处理右半区域
- 另一种实现
def quick_sort(lst): def qsort(lst, begin, end): if begin >= end: return pivot = lst[begin].key i = begin for j in range(begin+1, end+1): if lst[j].key < pivot: i += 1 lst[i], lst[j] = lst[j], lst[i] lst[begin], lst[i] = lst[i], lst[begin] qsort(lst, begin, i-1) qsort(lst, i+1, end) qsort(lst, 0, len(lst)-1)
- 算法分析:
- 时间复杂度:O(nlogn)
- 空间复杂度:O(logn)
- 稳定性:不稳定
归并排序:归并算法是一种典型的序列操作,其工作是把两个或更多有序序列合并为一个有序序列,基于归并 的思想也可以 实现排序,称为归并排序,其基本方法如下:
- 初始时,把待排序序列中的n个记录看成n个有序子序列(因为一个记录的序列总是排好序的),每个子序列的长度均为1
- 把当时序列组里的有序子序列两两归并,完成一边后序列组里的排序序列个数 减半,每个子序列长度加倍
- 对加长的有序子序列重复上面操作,最终得到一个长度为n的有序序列
这种归并方法称为简单的二路归并排序,其中每次操作都是把两个有序序列合并为一个有序序列,也可以考虑三路归并 或者跟多 路归并。
- 算法实现:归并算法分三层实现
- 最下层:实现表中相邻的一对有序序列的归并工作,将归并的结果存入 另一个顺序表里的相同位置。
- 中间层:基于操作1(一对序列的归并操作),实现对整个表里顺序各对有序序列的归并,完成一边归并,对 各序列的归并结果存入另一顺序表里的同位置分段
- 最高层:在两个顺序表之间往复执行操作2,完成一遍归并后交换两个表的地位,然后 在重复操作2的工作,直至整个表里 只有 一个有序序列时排序完成
- 最下面一层函数merge,它完成了表中连续排放的两个有序序列的归并工作,lfrom:被归并的有序段,lto:归并结果
def merge(lfrom, lto, low, mid, high): i, j, k = low, mid, low while i < mid and j < high: if lfrom[i].key <= lfrom[j].key: lto[k] = lfrom[i] i += 1 else: lto[k] = lfrom[j] j += 1 k += 1 while i < mid: lto[k] = lfrom[i] i += 1 k += 1 while j < high: lto[k] = lfrom[j] j += 1 k += 1
函数的第一个循环处理两个分段都未归并元素的情况,其每次迭代取出两个分段中当时的最小记录(它一定是这两个分段之一的最小记录),把它移到lto表里的下一个位置,在循环体里还要正确更新几个下标变量,当某个分段不再有更多记录时本循环结束,后随的两个循环把另一分段中剩下的元素逐一复制到表lto中,这两个循环只有一个真正执行。
- 函数merge_pass实现一对对分段的一遍归并,它需要知道表长度和分段长度,参数llen和slen分别表示这两个长度,第一个循环处理一对对长度都为slen的分段,随后的if语句处理表最后剩下 的两个或一个分段。
def merge_pass(lfrom, lto, llen, slen): i = 0 while i + 2*slen < llen: merge(lfrom, lto, i, i+slen, i + 2*slen) i += 2*slen if i + slen < llen: merge(lfrom, lto, i, i + slen, llen) else: for j in range(i, llen): lto[j] = lfrom[j]
- 最后时主函数merge_sort,它先安排 一个同样长度的表,而后在两个表之间往复的做一遍遍归并,直至完成工作
def merge_sort(lst): slen, llen = 1, len(lst) templst = [None]*llen while slen < llen: merge_pass(lst, templst, llen, slen) slen *= 2 merge_pass(templst, lst, llen, slen) slen *= 2
整个序列的实际排序完成时,得到的结果有可能正好放在templst里,这是再执行一次merge_pass(循环体里的第二个merge_pass调用)就能把结果复制会原来的表lst.
- 算法分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
算法比较

分配排序和基数排序
- 为每个关键码值设定一个桶(即是能够容纳任一多个记录的容器,例如用一个连续表或链接表)
- 排序是简单的根据关键码把记录放入相应桶中
- 存入所有记录后,顺序收集各个桶里的记录,就得到排序的序列
算法实现
- 需要排序的任然是record类型的顺序表,list
- 记录中的关键码是十进制数字的元组,包含r个元素
- 排序算法的参数是表lst和关键码码元素的长度r
def radix_sort(lst, d): rlists = [[] for i in range(10)] llen = len(lst) for m in range(-1, -d-1, -1): for j in range(llen): rlists[lst[j]].append(lst[j]) j = 0 for i in range(10): tmp = rlists[i] for k in range(len(tmp)): lst[j] = tmp[k] j += 1 rlists[i].clear()
python系统的list排序
python系统有一个内置的排序函数sort,可以对任何迭代对象排序,得到一个排序的表,另外,表list类的对象也有一个sort方法,其实两者共享一个排序算法,这是一种混成式排序算法,称为Timsort,可译为蒂姆排序。
蒂姆排序是一种基于归并技术的稳定排序算法,其中结合使用归并排序和插入排序技术,最坏的时间复杂度是O(nlogn),该算法具有适应性,在被排序的数组元素接近排好序的情况下,它的时间复杂度可能远小于O(nlogn),有可能达到线性时间。蒂姆排序算法再最坏情况下需要n/2工作空间,引起其工作空间复杂度是O(n),但另一方面,如果情况比较有利,它只需要很少零食存储空间。
浙公网安备 33010602011771号