这个系列的大部分代码都会用python来写,部分觉得c++写起来更好理解的话就会用c++来写。
排序就是重新排列列表中元素,使表中的元素满足按选定关键内容递增或递减排列的过程,排序算法的比较维度一般有3个,时间复杂度、空间复杂度、是否稳定。
时间复杂度和空间复杂度都比较好理解,排序算法稳定性:在排序过程中是否交换过值相等的成员,如果交换过则称为不稳定的,未交换过则是稳定的。文章会提供些示例代码,大部分会用python来写,偶尔会用c++来写。
先发结论:
算法种类 | 最好时间复杂度 | 最差时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
冒泡排序 | O(n) | O(n2) | O(n2) | O(1) | 是 |
插入排序 | O(n) | O(n2) | O(n2) | O(1) | 是 |
选择排序 | O(n) | O(n2) | O(n2) | O(1) | 否 |
快排 | O(nlog2n) | O(n2) | O(nlog2n) | O(log2n) | 否 |
堆排 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 否 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 是 |
桶排序 | O(n) | O(n) | O(n) | O(n+m) | 是 |
希尔排序 | 不确定 | 不确定 | 不确定 | O(1) | 否 |
1. 冒泡排序
作为各个大小公司的最基本程序员面试题,冒泡排序应该是程序员最熟悉的算法之一。原理:假设待排列表长度为n,从前向后(从后向前)两两比较相邻元素的值,若为逆序则交换,直到序列完成,称为一趟冒泡,完成将最小值交换到最前的位置,就像是水中气泡漂浮至水面;下一趟时,最前位置将不在参与交换,每趟交换都会把最小元素交换到当前序列中的最前位置。
def BubbleSort(f_a): a = copy.deepcopy(f_a) for i in range(len(a)-1): for j in range(i+1, len(a)): if a[i] > a[j]: a[i], a[j] = a[j], a[i] return a
优点就是代码简单,缺点就是慢。对于无序情况和反序情况要每趟都要进行多次交换。
快速排序可以认为是冒泡排序的升级版,快速排序原理:基于分治法。选择一个pivot,将待排序列分为两部分,大于pivot的将分在pivot左边,小于pivot的分在右边,pivot到达其最终位置,递归的对左右两边进行再次快排,直到数组长度为1或者0。是一种相对复杂但高效的交换排序。
def Partition(a, low, high): pivot = a[low] while (low < high): while( low < high and a[high] >= pivot): high -= 1 a[low] = a[high] while( low < high and a[low] <= pivot): low += 1 a[high] = a[low] a[low] = pivot return low def _QuickSort(a, low, high): if low < high: pivot = Partition(a, low, high) _QuickSort(a, pivot + 1, high) _QuickSort(a, low, pivot - 1) def QuickSort(a): _QuickSort(a, 0, len(a) - 1) return a
c语言stdlib.h中qsort使用了快排,快排是所有内部排序中平均效率最高的排序,C++的 algorithm 中的sort在数据量较大时使用快排,在中等数据量时使用堆排,在数据量较小时采用插入排序。这也说明了快排的一个局限性,对于较小的数据量由于pivot分割不均匀问题,使得整个快排的效率未必会特别高,表现上反倒不如复杂度比较稳定的排序方法。快排的最差效率发生在每次选pivot分出的数组一边为空,为避免这种问题,可选多个数取中间值作为pivot。 空间使用上,快排会进行递归,所以需要一个递归工作栈保存其递归内容,最好情况为O(log2(n+1))。
2. 插入排序
插入排序原理也很容易理解:从原始队列中选择元素插入前方有序子队列,直到全部完成。
def InsertSort(f_a): a = copy.deepcopy(f_a) for i in range(1, len(a)): if a[i] < a[i-1]: tmp = a[i] j = i - 1 while j > -1 and tmp < a[j]: a[j+1] = a[j] j -= 1 a[j+1] = tmp return a
由于每次插入前都会从后向前进行比较再移动,所以不会出现同元素位置发生变化的场景,所以是稳定的。最好情况为表中数据已有序,只需比较n-1次,为O(n)。
希尔排序可以认为是插入排序的升级版,也是基于分治法,选步长为k将待排数组a 分为 a/k个数组[a[i], a[i+k], a[i+2k], ...],对每个数组中的数据进行插入排序完成一次后。减小k,重复操作,直到k减小为1。一般会选择len(a) / 2 作为初始步长。
def ShellSort(a): step = len(a) // 2 while step >= 1: for index in range(step, len(a)): tmp = a[index] j = index - step while j > -1 and a[j] > tmp: a[j+step] = a[j] j -= step a[j + step] = tmp step //= 2 return a
希尔排序的时间复杂度并不固定,和数组规模相关,有时会特别大,在一般情况下为O(n2)以内。
3. 简单选择排序
选择排序的原理也很容易理解:每次从原始队列中选择一个最小的元素与当前队列最前端的元素交换,然后再向后移动,重复同一个过程。
def SelectSort(f_a): a = copy.deepcopy(f_a) for i in range(len(a) - 1): a_min = a[i] for j in range(i+1, len(a)): if a[j] < a_min: a_min = a[j] a[i], a[j] = a[j], a[i] return a
简单选择排序的交换操作不会超过n-1次,但是比较次数要进行n(n-1)/2次,其实冒泡的比较次数也要n(n-1)/2次,但是冒泡的交换次数可能远超过n-1次,所以一般情况下选择排序效率高于冒泡。
堆排可以认为是简单选择的升级版,将待排数组塞进一棵完全二叉树,利用二叉树特性进行排列,令子节点的值小于父节点的值,如果不满足这种就交换。每次选出最大的值放在最后,然后再对其他值进行重复。
def AdjustDown(a, index, length): smallest = index left = 2 * index + 1 right = 2 * (index + 1) if left < length and a[left] > a[index]: smallest = left if right < length and a[right] > a[smallest]: smallest = right if index != smallest: a[index], a[smallest] = a[smallest], a[index] AdjustDown(a, smallest, length) def HeapSort(a): index = len(a) // 2 while index > -1: AdjustDown(a, index, len(a)) index -= 1 index = len(a) - 1 while index > -1: a[index], a[0] = a[0], a[index] AdjustDown(a, 0, index) index -= 1
堆排的复杂度很稳定始终为O(log2n)。堆排也适合于查找最大(小)前n个数问题。
4. 归并排序
归并是将两个或两个以上有序表组合成一个新的有序表。
两路归并,先将待排数组a分成len(a)个子表,然后两两字表归并,然后得到 len(a)/2个有序子表,重复。
def merge(a, low, mid, high): b = [] for i in a[low:high+1]: b.append(i) m, n, l = low, mid + 1, 0 while m <= mid and n <= high: if b[n-low] > b[m-low]: a[low+l] = b[m-low] m += 1 else: a[low+l] = b[n-low] n += 1 l += 1 while(m<=mid): a[low+l] = b[m-low] l += 1 m += 1 while(n<=high): a[low+l] = b[n-low] l += 1 n += 1 def _mergeSort(a, low, high): if low < high: mid = (high - low) // 2 + low _mergeSort(a, low, mid) _mergeSort(a, mid + 1, high) merge(a, low, mid, high) def MergeSort(a): _mergeSort(a, 0, len(a) - 1)
归并排序需要一个O(n)的辅助空间,所以空间复杂度是O(n),每一次的时间复杂度是O(n),所以整体时间复杂度是O(nlog2n);merge过程不会交换已有序内容的顺序,所以这个是稳定的算法。
5. 桶排
桶排的思路也是基于分治法,先将待排数组依照某种映射分成n个桶,然后在桶中进行排序(可使用快排或堆排等,例子中使用了上面的希尔排序),然后排序完成后再将桶中元素有序插回原数组。桶的数量一般为数组a的sqrt(len(a))个,在空间足够的情况下越多越好。
def BucketSort(a): k = sqrt(len(a)) dict1 = {} for num in a: index = num // k tmpdict = dict1.get(index, []) tmpdict += [num,] dict1[index] = tmpdict for _, v in dict1.items(): ShellSort(v) cnt = 0 for i in range(k+1): for num in dict1.get(i, {}): a[cnt] = num cnt += 1
桶排效率的关注点在于分桶方式,因为求哈希每个桶中的数并不均匀,如果选择的分桶方式不好可能把所有的数都会落在一个桶中。python中sort在数据量大时使用sample sort样本排序(数据量小时常采用折半插入排序(BinaryInsertion)),是先选取一定的数作为样本,对样本中的数进行梳理,用来选取合适的分桶值,使落在每个桶中的数量尽量均匀。