排序算法总结(基于Python)
比较排序
(基于比较的排序)给定一个包含个对象的待排序序列。假如已知如何比较任意两个对象的大小关系,以及如何对这一序列排序。对于自定义对象,比较规则应当满足两个性质:传递性和全序性。
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
特点:调整相邻两个对象的位置,每进行一次内循环,将最大值调整到最后。
时间复杂度:
def bubble_sort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
for j in range(0, n - i - 1):
# 比较大小
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
梳排序
梳排序是改良自冒泡排序,其要旨在于消除乌龟,亦即在阵列尾部的小数值,这些数值是造成冒泡排序缓慢的主因。相对地,兔子,亦即在阵列前端的大数值,不影响冒泡排序的效能。
冒泡排序总是比较相邻的值。所以一次交换中只能删除一个逆序计数。而梳式排序通过使用大于1的间隔来改进冒泡排序。间隔从一个较大的值开始,并在每次迭代中缩小1.3倍,直到达到值1。因此,梳式排序在一次交换中删除了多个逆序计数,比冒泡排序性能上更好。
收缩系数的设定影响着梳排序的效率。根据经验,收缩系数是1.3 (通过在超过20万个随机列表上测试梳排序)。
虽然平均情况下,它比冒泡排序性能更好,但在最坏的情况下仍然是。
def comb_sort(arr):
the_len = len(arr)
if the_len < 2: # 0和1
return arr
else:
i = int(the_len / 1.3)
while i >= 1:
for j in range(the_len):
if i + j >= the_len:
i = int(i / 1.3)
break
else:
if arr[j] >= arr[j + i]:
arr[j], arr[j + i] = arr[j + i], arr[j]
return arr
堆排序
堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。堆的结构是一棵完全二叉树的结构,并且满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。
堆排序先按从上到下、从左到右的顺序将待排序列表中的元素构造成一棵完全二叉树,然后对完全二叉树进行调整,使其满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。构建出堆后,将堆顶与堆尾进行交换,然后将堆尾从堆中取出来,取出来的数据就是最大(或最小)的数据。重复构建堆并将堆顶和堆尾进行交换,取出堆尾的数据,直到堆中的数据全部被取出,列表排序完成。总的时间复杂度:
堆结构分为大顶堆和小顶堆:
- 大顶堆:每个节点(叶节点除外)的值都大于等于其子节点的值,根节点的值是所有节点中最大的,所以叫大顶堆,在堆排序算法中用于升序排列。
-小顶堆:每个节点(叶节点除外)的值都小于等于其子节点的值,根节点的值是所有节点中最小的,所以叫小顶堆,在堆排序算法中用于降序排列。
堆排序的原理如下:
- 将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
- 将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
- 将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
- 将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
- 重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。
def heap_sort(array):
first = len(array) // 2 - 1
for start in range(first, -1, -1):
# 从下到上,从右到左对每个非叶节点进行调整,循环构建成大顶堆
big_heap(array, start, len(array) - 1)
for end in range(len(array) - 1, 0, -1):
# 交换堆顶和堆尾的数据
array[0], array[end] = array[end], array[0]
# 重新调整完全二叉树,构造成大顶堆
big_heap(array, 0, end - 1)
return array
def big_heap(array, start, end):
root = start
# 左孩子的索引
child = root * 2 + 1
while child <= end:
# 节点有右子节点,并且右子节点的值大于左子节点,则将child变为右子节点的索引
if child + 1 <= end and array[child] < array[child + 1]:
child += 1
if array[root] < array[child]:
# 交换节点与子节点中较大者的值
array[root], array[child] = array[child], array[root]
# 交换值后,如果存在孙节点,则将root设置为子节点,继续与孙节点进行比较
root = child
child = root * 2 + 1
else:
break
归并排序
归并排序(Merge Sort)是一种非常高效的排序方式,它用了分治的思想,基本排序思想是:先将整个序列两两分开,然后每组中的两个元素排好序。接着就是组与组和合并,只需将两组所有的元素遍历一遍,即可按顺序合并。以此类推,最终所有组合并为一组时,整个数列完成排序。
时间复杂度:
def merge(s1,s2,s):
i = j = 0
while i+j<len(s):
# j==len(s2)时说明s2走完了,或者s1没走完并且s1中该位置是最小的
if j==len(s2) or (i<len(s1) and s1[i]<s2[j]):
s[i+j] = s1[i]
i += 1
else:
s[i+j] = s2[j]
j += 1
def merge_sort(s):
n = len(s)
# 剩一个或没有直接返回
if n < 2:
return
# 拆分
mid = n // 2
s1 = s[0:mid]
s2 = s[mid:n]
# 子序列递归调用排序
merge_sort(s1)
merge_sort(s2)
# 合并
merge(s1,s2,s)
快速排序
快速排序(Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
时间复杂度:
稳定性:不稳定
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot)。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
基准点的选择对快速排序的性能有极大的影响。基准点若选成区间的最大/最小值会导致退化(遍历区间时pivot始终大于或小于其他所有元素,这会导致第6和7行的循环把区间遍历两遍);最好的情况应选择区间内元素的中位数(每次恰好将区间内元素分割成大于、小于基准点的两部分,只需遍历一遍),但这通常无法实现,会导致时间常数大幅增高。
从区间最左侧,区间中央,区间最右侧取中位数做基准点(三数取中法),这种方法只需少量比较即可获得(不那么准确的)中位数,在序列已有序时获得良好的效果。
快速排序的分治算法在大多数情况下内存访问效率极高,虽然其他算法都是 ,但是快排比其他的快。
def quick_sort(alist, start, end):
if start >= end: # 递归的退出条件
return
mid = alist[start] # 设定起始的基准元素
low = start # low为序列左边在开始位置的由左向右移动的游标
high = end # high为序列右边末尾位置的由右向左移动的游标
while low < high:
# 如果low与high未重合,high(右边)指向的元素大于等于基准元素,则high向左移动
while low < high and alist[high] >= mid:
high -= 1
alist[low] = alist[high] # 走到此位置时high指向一个比基准元素小的元素,将high指向的元素放到low的位置上,此时high指向的位置空着,接下来移动low找到符合条件的元素放在此处
# 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
while low < high and alist[low] < mid:
low += 1
alist[high] = alist[low] # 此时low指向一个比基准元素大的元素,将low指向的元素放到high空着的位置上,此时low指向的位置空着,之后进行下一次循环,将high找到符合条件的元素填到此处
# 退出循环后,low与high重合,此时所指位置为基准元素的正确位置,左边的元素都比基准元素小,右边的元素都比基准元素大
alist[low] = mid # 将基准元素放到该位置,
# 对基准元素左边的子序列进行快速排序
quick_sort(alist, start, low - 1) # start :0 low -1 原基准元素靠左边一位
# 对基准元素右边的子序列进行快速排序
quick_sort(alist, low + 1, end) # low+1 : 原基准元素靠右一位 end: 最后
内省排序
内省排序(Introsort)是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为 。
内省排序将快速排序的**最大递归深度限制为 **,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为。
heap排序在平均时间复杂度是 ,最坏情况也是 ,看起来要比快排要快。但是实际上,快排是要比heap排序要快,第一个原因是:heap排序虽然和快排在平均情况下的时间复杂度是 ,但是heap排序的时间常数要比快排的时间常数要大。第二个原因是:据统计,快排的最坏情况在是很少发生的。第三个原因是:快排能够比较好的吻合程序的空间局部性原理,因为它操作的基本都是相邻的元素(虚拟存储器的设计理论基础中就有程序的时间局部性和空间局部性),能够减少内存缺页中断的发生次数。
import random
from math import log2
def percolateDown(heap, idx, maxIdx=None, *, reverse=False):
if maxIdx is None:
maxIdx = len(heap) - 1
while idx < maxIdx:
largestIdx = idx
if 2 * idx + 1 <= maxIdx and (reverse ^ (heap[2 * idx + 1] > heap[largestIdx])):
largestIdx = 2 * idx + 1
if 2 * idx + 2 <= maxIdx and (reverse ^ (heap[2 * idx + 2] > heap[largestIdx])):
largestIdx = 2 * idx + 2
if largestIdx != idx:
heap[idx], heap[largestIdx] = heap[largestIdx], heap[idx]
idx = largestIdx
else:
break
def heapify(heap, maxIdx=None, *, reverse=False):
if maxIdx is None:
maxIdx = len(heap) - 1
for idx in range(maxIdx // 2, -1, -1):
percolateDown(heap, idx, reverse=reverse)
def heapSort(heap, reverse=False):
heapify(heap, reverse=reverse)
for idx in range(len(heap) - 1, 0, -1):
heap[0], heap[idx] = heap[idx], heap[0]
percolateDown(heap, 0, idx - 1, reverse=reverse)
return heap
def findPivot(begin, end):
return random.randint(begin, end)
def partition(array, begin, end, *, reverse=False):
pivotIdx = findPivot(begin, end)
pivot = array[pivotIdx]
array[end], array[pivotIdx] = array[pivotIdx], array[end]
firstLarger = begin
for idx in range(begin, end):
if reverse ^ (array[idx] <= pivot):
array[idx], array[firstLarger] = array[firstLarger], array[idx]
firstLarger += 1
array[end], array[firstLarger] = array[firstLarger], array[end]
return firstLarger
def introSort(array, begin=0, end=None, depth=0, *, reverse=False):
if end is None:
end = len(array) - 1
if depth < log2(len(array)):
if begin < end:
mid = partition(array, begin, end, reverse=reverse)
introSort(array, begin, mid - 1, depth + 1, reverse=reverse)
introSort(array, mid + 1, end, depth + 1, reverse=reverse)
else:
array[begin:end + 1] = heapSort(array[begin:end + 1], reverse=reverse)
Timesort
Timsort是一种混合、稳定高效的排序算法,源自合并排序和插入排序,旨在很好地处理多种真实数据。它由Tim Peters于2002年实施使用在Python编程语言中。该算法查找已经排序的数据的子序列,并使用该知识更有效地对其余部分进行排序。这是通过将已识别的子序列(称为运行)与现有运行合并直到满足某些条件来完成的。从版本2.3开始,Timsort一直是Python的标准排序算法。如今,Timsort 已是是 Python、 Java、 Android平台 和 GNU Octave 的默认排序算法。
针对现实中需要排序的数据分析看,大多数据通常是有部分已经排好序的数据块,Timsort 就利用了这一特点。Timsort 称这些已经排好序的数据块为 “run”,我们可以将其视为一个一个的“分区”。在排序时,Timsort迭代数据元素,将其放到不同的 run 里,同时针对这些 run ,按规则进行合并至只剩一个,则这个仅剩的 run 即为排好序的结果。
换句话说,就是分析待排序数据,根据其本身的特点,将排序好的(不管是顺序还是逆序)子序列的分为一个个run分区,当然,这个分区run也存在一定的约束,即根据序列会产生一个minrun,如果原始的run小于minrun的长度,用插入排序扩充run,直到达到条件,之后使用归并排序来合并多个run。
def binary_search(arr, left, right, value):
if left >= right:
if arr[left] <= value:
return left + 1
else:
return left
elif left < right:
mid = (left + right) // 2
if arr[mid] < value:
return binary_search(arr, mid + 1, right, value)
else:
return binary_search(arr, left, mid - 1, value)
def insertion_sort(arr):
length = len(arr)
for index in range(1, length):
value = arr[index]
pos = binary_search(arr, 0, index - 1, value)
arr = arr[:pos] + [value] + arr[pos:index] + arr[index + 1:]
return arr
def merge(l1, l2):
if not l1:
return l2
if not l2:
return l1
if l1[0] < l2[0]:
return [l1[0]] + merge(l1[1:], l2)
else:
return [l2[0]] + merge(l1, l2[1:])
def timsort(arr):
if not arr: # 空列表
return
runs, sorted_runs = [], []
new_run = [arr[0]]
length = len(arr)
# 划分run区,并存储到runs里,这里简单的按照升序划分,没有考虑降序的run
for index in range(1, length):
if arr[index] < arr[index - 1]:
runs.append(new_run)
new_run = [arr[index]]
else:
new_run.append(arr[index])
if length - 1 == index:
runs.append(new_run)
break
for run in runs:
insertion_sort(run)
# 合并runs
sorted_arr = []
for run in runs:
sorted_arr = merge(sorted_arr, run)
return sorted_arr
非比较排序
桶排序
桶排序(Bucket Sort)是计数排序的推广,其主要思想是:将待排序集合中处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。
算法实现步骤:
- 根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
- 遍历排序序列,将每个元素放到对应的桶里去;
- 对不是空的桶进行排序;
- 按顺序访问桶,将桶中的元素依次放回到原序列中对应的位置,完成排序。
def bucket_sort(array):
min_num, max_num = min(array), max(array)
# 桶的大小
bucket_num = (max_num - min_num) // 3 + 1
# 桶数组
buckets = [[] for _ in range(int(bucket_num))]
# 向桶数组填数
for num in array:
buckets[int((num - min_num) // 3)].append(num)
new_array = list()
for i in buckets:
for j in sorted(i):
new_array.append(j)
return new_array
基数排序
基数排序(Radix Sort)是一种非比较型整数排序算法,是桶排序的扩展。基本思想是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。按照低位先排序,分别放入10个队列中,然后采用先进先出的原则进行收集;再按照高位排序,然后再收集;依次类推,直到最高位,最终得到排好序的数列。对于数值偏小的一组序列,其速度是非常快的,时间复杂度达到了线性,而且思想也非常的巧妙。
算法实现步骤
- 取得数组中的最大数,并取得位数;
- 对数位较短的数前面补零;
- 分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中;
- 收集,再将放置在0~9号桶中的数据按顺序放到数组中;
- 重复3~4过程,直到最高位,即可完成排序。
def radix_sort(arr):
n = len(str(max(arr))) # 记录最大值的位数
for k in range(n): # n轮排序
# 每一轮生成10个列表
bucket_list = [[] for i in range(10)] # 因为每一位数字都是0~9,故建立10个桶
for i in arr:
# 按第k位放入到桶中
bucket_list[i // (10 ** k) % 10].append(i)
# 按当前桶的顺序重排列表
arr = [j for i in bucket_list for j in i]
return arr