参考书目:《大话数据结构》python
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操做。排序算法,就是如何使得记录按照要求排列的方法。算法
排序的稳定性:
通过某种排序后,若是两个记录序号同等,且二者在原无序记录中的前后秩序依然保持不变,则称所使用的排序方法是稳定的,反之是不稳定的。sql
内排序和外排序
内排序:排序过程当中,待排序的全部记录所有放在内存中
外排序:排序过程当中,使用到了外部存储。
一般讨论的都是内排序。shell
影响内排序算法性能的三个因素:django
根据排序过程当中借助的主要操做,可把内排序分为:数组
按照算法复杂度可分为两类:缓存
如下的七种排序算法只是全部排序算法中最经典的几种,不表明所有。数据结构
冒泡排序(Bubble sort):时间复杂度O(n^2)
交换排序的一种。其核心思想是:两两比较相邻记录的关键字,若是反序则交换,直到没有反序记录为止。ide
其实现细节能够不一样,好比下面3种:函数
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 冒泡排序算法 class SQList: def __init__(self, lis=None): self.r = lis def swap(self, i, j): """定义一个交换元素的方法,方便后面调用。""" temp = self.r[i] self.r[i] = self.r[j] self.r[j] = temp def bubble_sort_simple(self): """ 最简单的交换排序,时间复杂度O(n^2) """ lis = self.r length = len(self.r) for i in range(length): for j in range(i+1, length): if lis[i] > lis[j]: self.swap(i, j) def bubble_sort(self): """ 冒泡排序,时间复杂度O(n^2) """ lis = self.r length = len(self.r) for i in range(length): j = length-2 while j >= i: if lis[j] > lis[j+1]: self.swap(j, j+1) j -= 1 def bubble_sort_advance(self): """ 冒泡排序改进算法,时间复杂度O(n^2) 设置flag,当一轮比较中未发生交换动做,则说明后面的元素其实已经有序排列了。 对于比较规整的元素集合,可提升必定的排序效率。 """ lis = self.r length = len(self.r) flag = True i = 0 while i < length and flag: flag = False j = length - 2 while j >= i: if lis[j] > lis[j + 1]: self.swap(j, j + 1) flag = True j -= 1 i += 1 def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4,1,7,3,8,5,9,2,6]) # sqlist.bubble_sort_simple() # sqlist.bubble_sort() sqlist.bubble_sort_advance() print(sqlist)
简单选择排序(simple selection sort):时间复杂度O(n^2)
经过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录进行交换。
通俗的说就是,对还没有完成排序的全部元素,从头至尾比一遍,记录下最小的那个元素的下标,也就是该元素的位置。再把该元素交换到当前遍历的最前面。其效率之处在于,每一轮中比较了不少次,但只交换一次。所以虽然它的时间复杂度也是O(n^2),但比冒泡算法仍是要好一点。
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 简单选择排序 class SQList: def __init__(self, lis=None): self.r = lis def swap(self, i, j): """定义一个交换元素的方法,方便后面调用。""" temp = self.r[i] self.r[i] = self.r[j] self.r[j] = temp def select_sort(self): """ 简单选择排序,时间复杂度O(n^2) """ lis = self.r length = len(self.r) for i in range(length): minimum = i for j in range(i+1, length): if lis[minimum] > lis[j]: minimum = j if i != minimum: self.swap(i, minimum) def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0]) sqlist.select_sort() print(sqlist)
直接插入排序(Straight Insertion Sort):时间复杂度O(n^2)
基本操做是将一个记录插入到已经排好序的有序表中,从而获得一个新的、记录数增1的有序表。
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 直接插入排序 class SQList: def __init__(self, lis=None): self.r = lis def insert_sort(self): lis = self.r length = len(self.r) # 下标从1开始 for i in range(1, length): if lis[i] < lis[i-1]: temp = lis[i] j = i-1 while lis[j] > temp and j >= 0: lis[j+1] = lis[j] j -= 1 lis[j+1] = temp def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0]) sqlist.insert_sort() print(sqlist)
该算法须要一个记录的辅助空间。最好状况下,当原始数据就是有序的时候,只须要一轮对比,不须要移动记录,此时时间复杂度为O(n)。然而,这基本是幻想。
希尔排序(Shell Sort)是插入排序的改进版本,其核心思想是将原数据集合分割成若干个子序列,而后再对子序列分别进行直接插入排序,使子序列基本有序,最后再对全体记录进行一次直接插入排序。
这里最关键的是跳跃和分割的策略,也就是咱们要怎么分割数据,间隔多大的问题。一般将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后获得的结果是基本有序而不是局部有序。下面的例子中经过:increment = int(increment/3)+1来肯定“增量”的值。
希尔排序的时间复杂度为:O(n^(3/2))
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 希尔排序 class SQList: def __init__(self, lis=None): self.r = lis def shell_sort(self): """希尔排序""" lis = self.r length = len(lis) increment = len(lis) while increment > 1: increment = int(increment/3)+1 for i in range(increment+1, length): if lis[i] < lis[i - increment]: temp = lis[i] j = i - increment while j >= 0 and temp < lis[j]: lis[j+increment] = lis[j] j -= increment lis[j+increment] = temp def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0,123,22]) sqlist.shell_sort() print(sqlist)
堆是具备下列性质的彻底二叉树:
每一个分支节点的值都大于或等于其左右孩子的值,称为大顶堆;
每一个分支节点的值都小于或等于其作右孩子的值,称为小顶堆;
所以,其根节点必定是全部节点中最大(最小)的值。
若是按照层序遍历的方式(广度优先)给节点从1开始编号,则节点之间知足以下关系:
堆排序(Heap Sort)就是利用大顶堆或小顶堆的性质进行排序的方法。堆排序的整体时间复杂度为O(nlogn)。(下面采用大顶堆的方式)
其核心思想是:将待排序的序列构形成一个大顶堆。此时,整个序列的最大值就是堆的根节点。将它与堆数组的末尾元素交换,而后将剩余的n-1个序列从新构形成一个大顶堆。反复执行前面的操做,最后得到一个有序序列。
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 堆排序 class SQList: def __init__(self, lis=None): self.r = lis def swap(self, i, j): """定义一个交换元素的方法,方便后面调用。""" temp = self.r[i] self.r[i] = self.r[j] self.r[j] = temp def heap_sort(self): length = len(self.r) i = int(length/2) # 将原始序列构形成一个大顶堆 # 遍历从中间开始,到0结束,其实这些是堆的分支节点。 while i >= 0: self.heap_adjust(i, length-1) i -= 1 # 逆序遍历整个序列,不断取出根节点的值,完成实际的排序。 j = length-1 while j > 0: # 将当前根节点,也就是列表最开头,下标为0的值,交换到最后面j处 self.swap(0, j) # 将发生变化的序列从新构形成大顶堆 self.heap_adjust(0, j-1) j -= 1 def heap_adjust(self, s, m): """核心的大顶堆构造方法,维持序列的堆结构。""" lis = self.r temp = lis[s] i = 2*s while i <= m: if i < m and lis[i] < lis[i+1]: i += 1 if temp >= lis[i]: break lis[s] = lis[i] s = i i *= 2 lis[s] = temp def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22]) sqlist.heap_sort() print(sqlist)
堆排序的运行时间主要消耗在初始构建堆和重建堆的反复筛选上。
其初始构建堆时间复杂度为O(n)。
正式排序时,重建堆的时间复杂度为O(nlogn)。
因此堆排序的整体时间复杂度为O(nlogn)。
堆排序对原始记录的排序状态不敏感,所以它不管最好、最坏和平均时间复杂度都是O(nlogn)。在性能上要好于冒泡、简单选择和直接插入算法。
空间复杂度上,只须要一个用于交换的暂存单元。可是因为记录的比较和交换是跳跃式的,所以,堆排序也是一种不稳定的排序方法。
此外,因为初始构建堆的比较次数较多,堆排序不适合序列个数较少的排序工做。
归并排序(Merging Sort):创建在归并操做上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个很是典型的应用。将已有序的子序列合并,获得彻底有序的序列;即先使每一个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 归并排序 class SQList: def __init__(self, lis=None): self.r = lis def swap(self, i, j): """定义一个交换元素的方法,方便后面调用。""" temp = self.r[i] self.r[i] = self.r[j] self.r[j] = temp def merge_sort(self): self.msort(self.r, self.r, 0, len(self.r)-1) def msort(self, list_sr, list_tr, s, t): temp = [None for i in range(0, len(list_sr))] if s == t: list_tr[s] = list_sr[s] else: m = int((s+t)/2) self.msort(list_sr, temp, s, m) self.msort(list_sr, temp, m+1, t) self.merge(temp, list_tr, s, m, t) def merge(self, list_sr, list_tr, i, m, n): j = m+1 k = i while i <= m and j <= n: if list_sr[i] < list_sr[j]: list_tr[k] = list_sr[i] i += 1 else: list_tr[k] = list_sr[j] j += 1 k += 1 if i <= m: for l in range(0, m-i+1): list_tr[k+l] = list_sr[i+l] if j <= n: for l in range(0, n-j+1): list_tr[k+l] = list_sr[j+l] def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 12, 77, 34, 23]) sqlist.merge_sort() print(sqlist)
另一个版本:
def merge(lfrom, lto, low, mid, high): """ 两段须要归并的序列从左往右遍历,逐一比较,小的就放到 lto里去,lfrom下标+1,lto下标+1,而后再取,再比,再放, 最后lfrom里的两段比完了,lto里留下的就是从小到大排好的一段。 :param lfrom: 原来的列表 :param lto: 缓存的列表 :param low: 左边一段的开头下标 :param mid: 左右两段的中间相隔的下标 :param high: 右边一段的最右下标 :return: """ i, j, k = low, mid, low while i < mid and j < high: if lfrom[i] <= lfrom[j]: 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 def merge_pass(lfrom, lto, llen, slen): """ 用来处理全部须要合并的段,这须要每段的长度,以及列表的总长。 最后的if语句处理表最后部分不规则的状况。 :param lfrom: 原来的列表 :param lto: 缓存的列表 :param llen: 列表总长 :param slen: 每段的长度 :return: """ 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] def merge_sort(lst): """ 主函数。 先安排一个一样大小的列表,做为辅助空间。 而后在两个列表直接作往复的归并,每归并一次slen的长度增长一倍, 逐渐向llen靠拢,当slen==llen时说明归并结束了。 归并完成后最终结果可能刚好保存在templist里,所以代码里作两次归并, 保证最后的结果体如今原始的lst列表里。 :param lst: 要排序的原始列表 :return: """ slen, llen = 1, len(lst) templist = [None]*llen while slen < llen: merge_pass(lst, templist, llen, slen) slen *= 2 merge_pass(templist, lst, llen, slen) slen *= 2
归并排序在计算过程当中须要使用必定的辅助空间,用于递归和存放结果,所以其空间复杂度为O(n+logn)。
归并排序中不存在跳跃,只有两两比较,所以是一种稳定排序。
总之,归并排序是一种比较占用内存,但效率高,而且稳定的算法。
快速排序(Quick Sort)由图灵奖得到者Tony Hoare发明,被列为20世纪十大算法之一。冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为O(nlog(n))。
快速排序算法的核心思想:经过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另外一部分记录的关键字小,而后分别对这两部分继续进行排序,以达到整个记录集合的排序目的。
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author: Liu Jiang # Python 3.5 # 快速排序 class SQList: def __init__(self, lis=None): self.r = lis def swap(self, i, j): """定义一个交换元素的方法,方便后面调用。""" temp = self.r[i] self.r[i] = self.r[j] self.r[j] = temp def quick_sort(self): """调用入口""" self.qsort(0, len(self.r)-1) def qsort(self, low, high): """递归调用""" if low < high: pivot = self.partition(low, high) self.qsort(low, pivot-1) self.qsort(pivot+1, high) def partition(self, low, high): """ 快速排序的核心代码。 其实就是将选取的pivot_key不断交换,将比它小的换到左边,将比它大的换到右边。 它本身也在交换中不断变换本身的位置,直到完成全部的交换为止。 但在函数调用的过程当中,pivot_key的值始终不变。 :param low:左边界下标 :param high:右边界下标 :return:分完左右区后pivot_key所在位置的下标 """ lis = self.r pivot_key = lis[low] while low < high: while low < high and lis[high] >= pivot_key: high -= 1 self.swap(low, high) while low < high and lis[low] <= pivot_key: low += 1 self.swap(low, high) return low def __str__(self): ret = "" for i in self.r: ret += " %s" % i return ret if __name__ == '__main__': sqlist = SQList([4, 1, 7, 3, 8, 5, 9, 2, 6, 0, 123, 22]) sqlist.quick_sort() print(sqlist)
另一个版本:
def quick_sort(nums): # 封装一层的目的是方便用户调用 def qsort(lst, begin, end): if begin >= end: return i = begin key = lst[begin] for j in range(begin+1, end+1): if lst[j] < key: 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(nums, 0, len(nums)-1)
下面是一个实例测试数据:
测试数据量(个) | 100 | 1000 | 10000 | 100000 | 1000000 |
---|---|---|---|---|---|
冒泡 | 0.001 | 0.11 | 11s | - | - |
反序冒泡 | 0.001 | 0.14 | 14s | - | - |
快速排序 | 0.001 | 0.003~0.004 | 0.040~0.046 | 0.51~0.53 | 6.36~6.56s |
从数据中可见:
基本的快速排序还有能够优化的地方:
前面咱们每次选取pivot_key的都是子序列的第一个元素,也就是lis[low],这就比较看运气。运气好时,该值处于整个序列的靠近中间值,则构造的树比较平衡,运气比较差,处于最大或最小位置附近则构造的树接近斜树。
为了保证pivot_key选取的尽量适中,采起选取序列左中右三个特殊位置的值中,处于中间值的那个数为pivot_key,一般会比直接用lis[low]要好一点。在代码中,在原来的pivot_key = lis[low]这一行前面增长下面的代码:
m = low + int((high-low)/2) if lis[low] > lis[high]: self.swap(low, high) if lis[m] > lis[high]: self.swap(high, m) if lis[m] > lis[low]: self.swap(m, low)
若是以为这样还不够好,还能够将整个序列先划分为3部分,每一部分求出个pivot_key,再对3个pivot_key再作一次上面的比较得出最终的pivot_key。这时的pivot_key应该很大几率是一个比较靠谱的值。
原来的代码中pivot_key这个记录老是再不断的交换中,其实这是不必的,彻底能够将它暂存在某个临时变量中,以下所示:
def partition(self, low, high): lis = self.r m = low + int((high-low)/2) if lis[low] > lis[high]: self.swap(low, high) if lis[m] > lis[high]: self.swap(high, m) if lis[m] > lis[low]: self.swap(m, low) pivot_key = lis[low] # temp暂存pivot_key的值 temp = pivot_key while low < high: while low < high and lis[high] >= pivot_key: high -= 1 # 直接替换,而不交换了 lis[low] = lis[high] while low < high and lis[low] <= pivot_key: low += 1 lis[high] = lis[low] lis[low] = temp return low
快速排序算法的递归操做在进行大量数据排序时,其开销能被接受,速度较快。但进行小数组排序时则不如直接插入排序来得快,也就是杀鸡用牛刀,未必就比菜刀来得快。
所以,一种很朴素的作法就是根据数据的多少,作个使用哪一种算法的选择而已,以下改写qsort方法:
def qsort(self, low, high): """根据序列长短,选择使用快速排序仍是简单插入排序""" # 7是一个经验值,可根据实际状况自行决定该数值。 MAX_LENGTH = 7 if high-low < MAX_LENGTH: if low < high: pivot = self.partition(low, high) self.qsort(low, pivot - 1) self.qsort(pivot + 1, high) else: # insert_sort方法是咱们前面写过的简单插入排序算法 self.insert_sort()
能够采用尾递归的方式对整个算法的递归操做进行优化,改写qsort方法以下:
def qsort(self, low, high): """根据序列长短,选择使用快速排序仍是简单插入排序""" # 7是一个经验值,可根据实际状况自行决定该数值。 MAX_LENGTH = 7 if high-low < MAX_LENGTH: # 改用while循环 while low < high: pivot = self.partition(low, high) self.qsort(low, pivot - 1) # 采用了尾递归的方式 low = pivot + 1 else: # insert_sort方法是咱们前面写过的简单插入排序算法 self.insert_sort()
排序算法的分类:
没有十全十美的算法,有有点就会有缺点,即便是快速排序算法,也只是总体性能上的优越,也存在排序不稳定,须要大量辅助空间,不适于少许数据排序等缺点。
七种排序算法性能对比