一、概述
基本的排序算法在经过前人呕心沥血的研究下基本可以分为以下十种,当然除此之外,还有结合多种算法思想基于他们的改进变种。
在插入、选择、交换这三大类基于比较的排序算法中,时间复杂度会随着优化程度在O(n^2)~O(nlogn)之间变化,希尔排序、快速排序、堆排序分别代表着杰出的优化策略。
基于分治递归思想的归并排序将待排数据像二叉树一样分化至最简单的一个数排序问题,子问题合并时间复杂度可控制在O(n),不难想到整体时间复杂度取决于树的深度,即达到O(nlogn)。
计数排序、桶排序、基数排序三种线性时间排序算法本质上运用了相同的思想:先将数据按一定映射关系分组(桶),然后桶内排序,顺序输出。三种姑且称为‘桶’排序算法在分组函数使用上不同,导致分组粒度不同,带来的额外空间开销出现差异。这三种排序算法适用于数据满足一定的条件,否则额外的空间开销将无法承受。
n:数据规模 ;k:‘桶’个数
稳定性定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
二、算法简介及代码展示
1.冒泡排序
算法的运作如下:
1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
#1 冒泡排序
'''
def bubble_sort(arr):
n = len(arr)
if n <= 1:
return arr
for i in range(0,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]
return arr
#优化1:
#某一趟遍历如果没有数据交换,说明已经排好序了,因此不用再进行迭代了。用一个标记记录这个状态即可。
def bubble_sort2(ary):
n = len(ary)
for i in range(n):
flag = True # 标记
for j in range(1, n - i):
if ary[j] < ary[j - 1]:
ary[j], ary[j - 1] = ary[j - 1], ary[j]
flag = False
# 某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了
if flag:
break
return ary
#优化2:
#记录某次遍历时最后发生数据交换的位置,这个位置之后的数据显然已经有序,不用再排序了。
# 因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。
def bubble_sort3(ary):
n = len(ary)
k = n # k为循环的范围,初始值n
for i in range(n):
flag = True
for j in range(1, k): # 只遍历到最后交换的位置即可
if ary[j - 1] > ary[j]:
ary[j - 1], ary[j] = ary[j], ary[j - 1]
k = j # 记录最后交换的位置
flag = False
if flag:
break
return ary
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = bubble_sort(srcArr)
print('冒泡排序结果:',resArr)
2,简单插入排序:类似扑克游戏中整理牌的过程
插入排序具体算法描述如下:
1. 从第一个元素开始,该元素可以认为已经被排序
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5. 将新元素插入到该位置后
6. 重复步骤2~5
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
#2 插入排序
'''
def insert_sort(arr):
n = len(arr)
if n <= 1:
return arr
for i in range(1,n): #默认第一个是有序的,从第二个数开始排序比较
key = i - 1
mark = arr[i] # 注: 必须将ary[i]赋值为mark,不能直接用ary[i]
while key >= 0 and arr[key] > mark:
arr[key + 1] = arr[key]
key -= 1
arr[key + 1] = mark
return arr
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = insert_sort(srcArr)
print('插入排序结果:',resArr)
3,简单选择排序:
选择排序具体算法描述如下:
1,在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3,以此类推,直到所有元素均排序完毕。
特点如下:
1. 运行时间和输入无关
为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供任何实质性帮助的信息。因此使用这种排序的我们会惊讶的发现,一个已经有序的数组或者数组内元素全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!而其他算法会更善于利用输入的初始状态,选择排序则不然。
2. 数据移动是最少的
选择排序的交换次数和数组大小关系是线性关系,选择排序无疑是最简单直观的排序。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
#3 选择排序
'''
def select_sort(arr):
n = len(arr)
if n <= 1:
return arr
for i in range(0,n):
min = i #记录最小元素的下标
for j in range(i+1,n):
if arr[j] < arr[min]:
min = j
if min != i: #找到最小元素进行交换
arr[min],arr[i] = arr[i],arr[min]
return arr
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = select_sort(srcArr)
print('选择排序结果:',resArr)
4,快速排序
快速排序基于选择划分,是简单选择排序的优化。
快速排序使用分治法(Divide and conquer)策略来把一个序列分为两个子序列。
快速排序算法过程为:
1. 从数列中挑出一个元素,称为”基准”(pivot),
2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
三数取中
在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。
根据枢纽值进行分割
每次划分将数据选到基准值两边,循环对两边的数据进行划分,类似于二分法。
算法的整体性能取决于划分的平均程度,即基准值的选择,此处衍生出快速排序的许多优化方案,甚至可以划分为多块。
基准值若能把数据分为平均的两块,划分次数O(logn),每次划分遍历比较一遍O(n),时间复杂度O(nlogn)。
额外空间开销出在暂存基准值,O(logn)次划分需要O(logn)个,空间复杂度O(logn)
快排每次排序问题规模缩小一半, 在大多数场景下都是最快的,但是也有最坏的情况,那就数据是逆序的情况,比如9,8,7,…2,1 这时的时间复杂度变为O(n2)O(n2)。为了避免这种情况,可以选择不取第一个元素,而是随机取。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
#4 快速排序
'''
def quick_sort(arr):
n = len(arr)
if n <= 1:
return arr
quicksort(arr, 0, n-1)
return arr
def quicksort(arr, left, right): # 递归调用
if left >= right:
return
mid = partition(arr, left, right)
quicksort(arr, left, mid - 1)
quicksort(arr, mid + 1, right)
def partition(arr, left, right):
key = left # 划分参考数索引,默认为第一个数,可优化
while left < right:
while left < right and arr[right] >= arr[key]:
right -= 1
while left < right and arr[left] <= arr[key]:
left += 1
arr[left], arr[right] = arr[right], arr[left]
arr[left], arr[key] = arr[key], arr[left]
return left
'''
另外一种实现方法
先从待排序的数组中找出一个数作为基准数(取第一个数即可),然后将原来的数组划分成两部分:
小于基准数的左子数组和大于等于基准数的右子数组
。然后对这两个子数组再递归重复上述过程,直到两个子数组的所有数都分别有序。
最后返回“左子数组” + “基准数” + “右子数组”,即是最终排序好的数组。
'''
# 实现快排
def quicksort2(nums):
if len(nums) <= 1:
return nums
less = []# 左子数组
greater = [] # 右子数组
base = nums.pop() # 基准数
for x in nums:# 对原数组进行划分
if x < base:
less.append(x)
else:
greater.append(x)
# 递归调用
return quicksort2(less) + [base] + quicksort2(greater)
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = quick_sort(srcArr)
print('快速排序结果:',resArr)
print('快速排序2结果:', quicksort2(srcArr))
5,归并排序
在上面基于分治思路,我们研究归并排序。归并排序是建立在归并操作上的一种有效的排序算法。
1. 把 n 个记录看成 n 个长度为 l 的有序子表
2. 进行两两归并使记录关键字有序,得到 n/2 个长度为 2 的有序子表
3. 重复第 2 步直到所有记录归并成一个长度为 n 的有序表为止。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
原理
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
合并方法:
设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为m-i +1、n-m。
1、j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标
2、若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束
3、//选取r[i]和r[j]较小的存入辅助数组rf
如果r[i]<r[j],rf[k]=r[i]; i++; k++; 转⑵
否则,rf[k]=r[j]; j++; k++; 转⑵
4、//将尚未处理完的子表中元素存入rf
如果i<=m,将r[i…m]存入rf[k…n] //前一子表非空
如果j<=n , 将r[j…n] 存入rf[k…n] //后一子表非空
5、合并结束。
归并排序划分子问题采用二分法,共需O(logn)次划分,当然需要相当次合并;每次合并遍历比较O(n)。时间复杂度O(nlogn)。
额外空间开销出在合并过程中的一个暂存数组,空间复杂度O(n)。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
#5 归并排序
'''
def merge_sort(arr):
n = len(arr)
if n <= 1:
return arr
mergeSort(arr, 0, n-1)
return arr
#递归调用归并排序
def mergeSort(arr, left, right):
if left >= right:
return
mid = (left + right)//2
mergeSort(arr,left,mid)
mergeSort(arr,mid+1,right)
Merge(arr,left,mid,right)
#合并左右子序列函数
def Merge(arr, left, mid, right):
temp = [] # 中间数组
i = left # 左段子序列起始
j = mid + 1 # 右段子序列起始
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
while i <= mid:
temp.append(arr[i])
i += 1
while j <= right:
temp.append(arr[j])
j += 1
for i in range(left, right + 1): # !注意这里,不能直接arr=temp,他俩大小都不一定一样
arr[i] = temp[i - left]
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = merge_sort(srcArr)
print('归并排序结果:',resArr)
6,堆排序 HeapSort
堆排序基于交换,是冒泡排序的优化。堆排序在 top K 问题中使用比较频繁。堆排序是一种树形选择排序,是对直接选择排序的有效改进。具体涉及大(小)顶堆的建立与调整。
堆排序与快速排序,归并排序一样都是时间复杂度为O(N∗logN) 的几种常见排序方法。
堆排序是采用二叉堆的数据结构来实现的,虽然实质上还是一维数组。二叉堆是一个近似完全二叉树 。
学习堆排序前,先讲解下什么是数据结构中的二叉堆。
二叉堆定义及性质:
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足二个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
由于其它几种堆(二项式堆,斐波纳契堆等)用的较少,一般将二叉堆就简称为堆。
堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i +1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
堆的操作——插入删除
下面先给出《数据结构C++语言描述》中最小堆的建立插入删除的图解,再给出本人的实现代码,最好是先看明白图后再去看代码。
堆化数组
有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图:
很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 19都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:
堆排序算法过程:
1,构造最大堆(Build_Max_Heap):若数组下标范围为0~n,考虑到单独一个元素是大根堆,则从下标n/2开始的元素均为大根堆。于是只要从n/2-1开始,向前依次构造大根堆,这样就能保证,构造到某个节点时,它的左右子树都已经是大根堆。
2,堆排序(HeapSort):由于堆是用数组模拟的。得到一个大根堆后,数组内部并不是有序的。因此需要将堆化数组有序化。思想是移除根节点,并做最大堆调整的递归运算。第一次将heap[0]与heap[n-1]交换,再对heap[0…n-2]做最大堆调整。第二次将heap[0]与heap[n-2]交换,再对heap[0…n-3]做最大堆调整。重复该操作直至heap[0]和heap[1]交换。由于每次都是将最大的数并入到后面的有序区间,故操作完后整个数组就是有序的了。移除位在第一个数据的根节点,并做最大堆调整的递归运算
3,最大堆调整(Max_Heapify):该方法是提供给上述两个过程调用的。目的是将堆的末端子节点作调整,使得子节点永远小于父节点 。
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下
2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
排序演示:
堆排序的初始建堆过程时间复杂度O(n),对O(n)级别个非叶子节点进行堆调整操作O(logn),时间复杂度O(nlogn);之后每一次堆调整操作确定一个数的次序,时间复杂度O(nlogn)。合起来时间复杂度O(nlogn)
额外空间开销出在调整堆过程,根节点下移交换时一个暂存空间,空间复杂度O(1)
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# 构造堆
class Heap_array:
def __init__(self, array, size):
self.array = array
self.size = size
# 2 调整大顶堆
def Max_Heapify(Heap, i):
left = 2 * i
right = 2 * i + 1
largest = i
if left < Heap.size and Heap.array[largest] < Heap.array[left]:
largest = left
if right < Heap.size and Heap.array[largest] < Heap.array[right]:
largest = right
if largest != i:
Heap.array[largest], Heap.array[i] = Heap.array[i], Heap.array[largest]
Max_Heapify(Heap, largest)
def Build_max_Heap(Heap):
for i in range((Heap.size // 2) - 1, -1, -1):
Max_Heapify(Heap, i)
def Heap_sort(array):
if not array:
return None
if len(array) == 1:
return array
Heap = Heap_array(array, len(array))
# print(Heap.array)
# 1 构造大顶堆
Build_max_Heap(Heap)
for i in range(len(array) - 1, 0, -1):
Heap.array[0], Heap.array[i] = Heap.array[i], Heap.array[0]
Heap.size -= 1
Max_Heapify(Heap, 0)
return Heap.array
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = Heap_sort(srcArr)
print('堆排序结果:', resArr)
效率是O(N*logN)但是看起来很线性。
7,希尔排序 ShellSort
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。因DL.Shell于1959年提出而得名。希尔排序是非稳定排序算法。同时该算法是冲破O(n2)的第一批算法之一。
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
若数据不是偶数个,可以考虑加一个无穷大,辅助排序,最后删除即可。
下面给出严格按照定义来写的希尔排序
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# 希尔排序
# 双杠用于整除(向下取整),在python直接用 “/” 得到的永远是浮点数,
# 用round()得到四舍五入值
def shell_sort(nums):
size = len(nums)
gap = size >> 1
while gap > 0:
for i in range(gap, size):
j = i
while j >= gap and nums[j - gap] > nums[j]:
nums[j - gap], nums[j] = nums[j], nums[j - gap]
j -= gap
gap = gap >> 1
if __name__ == '__main__':
nums = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(nums)
print('希尔排序:', nums)
8,外排序
9,计数排序,基数排序,桶排序
上面的几种算法,最好的都是n*logn,接下来介绍几种线性时间内的排序算法。
9.1计数排序
计数排序的核心思想,是用空间换取时间,本质是建立了基于元素的Hash表。是一种稳定排序。
假设n个输入元素中每一个都是介于0到k之间的整数,此处k为某个整数。当k=O(n)时,计数排序的运行时间为Θ(n)。
对每一个数的元素x,确定出小于x的元素个数。有了这一信息就可以把x直接放到最终输出数组中的位置上。
计数数组的大小取决于待排数据取值范围,所以对数据有一定要求,否则空间开销无法承受。
计数排序只需遍历一次数据,在计数数组中记录,输出计数数组中有记录的下标,时间复杂度为O(n+k)。
额外空间开销即指计数数组,实际上按数据值分为k类(大小取决于数据取值),空间复杂度O(k)。
图示
对于数据2 5 3 0 2 3 0 3程序执行的过程如下图所示:
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
#计数排序
import random
def countingSort(alist,k):
n=len(alist)
b=[0 for i in range(n)]
c=[0 for i in range(k+1)]
for i in alist:
c[i]+=1
for i in range(1,len(c)):
c[i]=c[i-1]+c[i]
for i in alist:
b[c[i]-1]=i
c[i]-=1
return b
if __name__=='__main__':
a=[random.randint(0,100) for i in range(100)]
print(countingSort(a,100))
9.2基数排序
基数排序进行多轮按位比较排序,轮次取决于最大数据值的位数。基数排序一般用于长度相同的元素组成的数组。
先按照个位比较排序,然后十位百位以此类推,优先级由低到高,这样后面的移动就不会影响前面的。
基数排序按位比较排序实质上也是一种划分,一种另类的‘桶’罢了。比如,第一轮按各个位比较,按个位大小排序分别装入10个‘桶’中,‘桶’中个位相同的数据视作相等,桶是有序的,按序输出,后面轮次接力完成排序。
基数排序‘桶’内数据在划分桶时便已排序O(n),k个桶,时间复杂度为O(n*k)。
额外空间开销出在数据划分入桶过程,桶大小O(n+k),空间复杂度O(n+k)。
常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位0~9数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,每一轮排序都基于上轮排序后的结果;最后一轮,最左边那位也作为关键字并排序,整个数组就达到有序状态。一定要注意每一轮排序中排序方法必须是稳定的。否则基数排序不能得到正确的结果。
基数排序也适用于字符串,若字符串使用的是8位的ASCII扩展字符集,则基的大小是256。
- 对于元素的每一位(关键字),计数排序都可以统计其频率,然后直接将整个元素按照该关键字进行分类、排序,实现起来简单。(想想插入排序、归并排序等稳定排序算法要如何按照某一位来将整个元素排序,是不是更复杂?)
- 因为数据范围确定且都不大(基的大小),因此不会占用多少空间;
- 而且计数排序不是基于比较,比通常的比较排序方法效率更高;
- 计数排序是稳定排序,这一点至关重要。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
import math
def RadixSort(ls):
def getbit(x, i): # 返回x的第i位(从右向左,个位为0)数值
y = x // pow(10, i)
z = y % 10
return z
def CountSort(ls):
n = len(ls)
num = max(ls)
count = [0] * (num + 1)
for i in range(0, n):
count[ls[i]] += 1
arr = []
for i in range(0, num + 1):
for j in range(0, count[i]):
arr.append(i)
return arr
Max = max(ls)
for k in range(0, int(math.log10(Max)) + 1): # 对k位数排k次,每次按某一位来排
arr = [[] for i in range(0, 10)]
for i in ls: # 将ls(待排数列)中每个数按某一位分类(0-9共10类)存到arr[][]二维数组(列表)中
arr[getbit(i, k)].append(i)
for i in range(0, 10): # 对arr[]中每一类(一个列表) 按计数排序排好
if len(arr[i]) > 0:
arr[i] = CountSort(arr[i])
j = 9
n = len(ls)
for i in range(0, n): # 顺序输出arr[][]中数到ls中,即按第k位排好
while len(arr[j]) == 0:
j -= 1
else:
ls[n - 1 - i] = arr[j].pop()
return ls
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = RadixSort(srcArr)
for i in resArr:
print(i, end=' ')
9.3桶排序
桶排序实际上是计数排序的推广,但实现上要复杂许多。桶排序是稳定的排序
桶排序先用一定的函数关系将数据划分到不同有序的区域(桶)内,然后子数据分别在桶内排序,之后顺次输出。
当每一个不同数据分配一个桶时,也就相当于计数排序。
假设n个数据,划分为k个桶,桶内采用快速排序,时间复杂度为O(n)+O(k * n/k*log(n/k))=O(n)+O(n*(log(n)-log(k))),
显然,k越大,时间复杂度越接近O(n),,每个桶只有一个数据,时间复杂度降低为O(N).当然空间复杂度O(n+k)会越大,这是空间与时间的平衡。
桶排序的思想:
- 根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;
- 将落在特定范围内的所有元素放入对应的桶中;
- 对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;
- 按照划分的范围顺序,将桶中的元素依次取出。排序完成。
举个例子,假如被排序的元素在0~99之间,我们可以建立10个桶,每个桶按范围顺序依次是[0, 10)、[10, 20]......[90, 99),注意是左闭右开区间。对于待排序数组[0, 3, 2, 80, 70, 75, 72, 88],[0, 3, 2]会被放到[0, 10)这个桶中,[70 ,75, 72]会被放到[70, 80)这个桶中,[80, 88]会被放到[80, 90)这个桶中,对这三个桶中的元素分别排序。得到
- [0, 10)桶中的元素: [0, 2, 3]
- [70, 80)桶中的元素: [70, 72, 75]
- [80, 90)桶中的元素: [80, 88]
依次取出三个桶中元素,得到序列[0, 2, 3, 70, 72, 75, 80, 88]已经排序完成。
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
def BucketSort(ls):
#桶内使用快速排序
def QuickSort(ls):
def partition(arr, left, right):
key = left # 划分参考数索引,默认为第一个数,可优化
while left < right:
while left < right and arr[right] >= arr[key]:
right -= 1
while left < right and arr[left] <= arr[key]:
left += 1
(arr[left], arr[right]) = (arr[right], arr[left])
(arr[left], arr[key]) = (arr[key], arr[left])
return left
def quicksort(arr, left, right): # 递归调用
if left >= right:
return
mid = partition(arr, left, right)
quicksort(arr, left, mid - 1)
quicksort(arr, mid + 1, right)
# 主函数
n = len(ls)
if n <= 1:
return ls
quicksort(ls, 0, n - 1)
return ls
######################
n = len(ls)
big = max(ls)
num = big // 10 + 1
bucket = []
buckets = [[] for i in range(0, num)]
for i in ls:
buckets[i // 10].append(i) # 划分桶
for i in buckets: # 桶内排序
bucket = QuickSort(i)
arr = []
for i in buckets:
if isinstance(i, list):
for j in i:
arr.append(j)
else:
arr.append(i)
for i in range(0, n):
ls[i] = arr[i]
return ls
if __name__ == '__main__':
srcArr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
resArr = BucketSort(srcArr)
for i in resArr:
print(i, end=' ')
所有排序算法中最快的应该是桶排序(很多人误以为是快速排序,实际上不是.不过实际应用中快速排序用的多)但桶排序一般用的不多,因为有几个比较大的缺陷.
1.待排序的元素不能是负数,小数.
2.空间复杂度不确定,要看待排序元素中最大值是多少.
所需要的辅助数组大小即为最大元素的值.
排序总结
1,其余一些如竞标赛排序,每次比较最后得到一个最大或者最小,这种基于比较的可以达到o(N*logN)
2.三种O(n^2)平均时间复杂度的排序算法 插入 选择 冒泡在空间复杂度、稳定性方面表现较好,甚至在特定情况下即便考虑时间复杂度也是最佳选择。
3.堆排序初始建堆过程较复杂,仅建堆时间复杂度就达到O(nlogn),但之后的排序开销稳定且较小,所以适合大量数据排序。
4.希尔排序性能看似很好,但实际上他的整体性能受步长选取影响较大,插入排序本质也使他受数据影响较大。
5.归并排序在平均和最坏情况下时间复杂度都表现良好O(nlogn),但昂贵的空间开销大O(n)。
6.快速排序大名鼎鼎,又有个好名字,但最坏情况下时间复杂度直逼O(n^2),远不如堆排序和归并排序。
7.基于比较排序的算法(如前七种)时间复杂度O(nlogn)已是下限。
8.三种线性时间复杂度排序算法虽然在速度上有决定性的优势,但也付出了沉重的空间代价,有时数据的特点让这种空间代价变得无法承受。所以他们的应用对数据本身有着特定的要求。
9.关于稳定性,希尔排序、快速排序和堆排序这三种排序算法无法保障。三种算法因为划分(子序列、大小端、左右孩子)后各自处理无法保证等值数据的原次序。
三、排序相关问题
3.1 逆序数问题
方法一:利用归并排序求解
归并排序的主要思想是将整个序列分成两部分,分别递归将这两部分排好序之后,再合并为一个有序的序列。
在合并的过程中是将两个相邻并且有序的序列合并成一个有序序列,如以下两个有序序列
Seq1:3 4 5
Seq2:2 6 8 9
合并成一个有序序:
Seq:2 3 4 5 6 8 9
对于序列seq1中的某个数a[i],序列seq2中的某个数a[j],如果a[i]<a[j],没有逆序数,如果a[i]>a[j],那么逆序数为seq1中a[i]后边元素的个数(包括a[i]),即len1-i+1,
这样累加每次递归过程的逆序数,在完成整个递归过程之后,最后的累加和就是逆序的总数
图中 low-mid 及mid-high都是升序,若a[i] > a[j],则a[i...mid]都大于a[j]
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
"""
一、逆序数:
给定一个数组array[0,...,n-1], 若对于某两个元素array[i],array[j],若i<j and array[i]>array[j],
则认为array[i],array[j]是逆序对。一个数组中包含的逆序对的数目称为该数组的逆序数。设计一个算法求一个数组的逆序数
二、利用 归并排序 的思想
在归并排序中,会将两个升序的数组进行合并,利用升序数组的特性,可以快速求得逆序数
"""
temp = [0] * 100
count = [0]
pairs = []
def Merge(nums, low, mid, high):
i = low
j = mid + 1
size = 0
while i <= mid and j <= high:
if nums[i] < nums[j]:
temp[size] = nums[i]
i += 1
else:
# 除了以下三行代码,其余代码与归并排序一模一样
count[0] += (mid - i + 1)
for h in range(i, mid + 1):
pairs.append((nums[h], nums[j]))
temp[size] = nums[j]
j += 1
size += 1
while i <= mid:
temp[size] = nums[i]
size += 1
i += 1
while j <= high:
temp[size] = nums[j]
size += 1
j += 1
for i in range(size):
nums[low + i] = temp[i]
def Merge_sort(nums, low, high):
if low >= high:
return
mid = (low + high) >> 1
Merge_sort(nums, low, mid)
Merge_sort(nums, mid + 1, high)
Merge(nums, low, mid, high)
if __name__ == '__main__':
nums = [3, 56, 2, 7, 45, 8, 1]
Merge_sort(nums, 0, len(nums) - 1)
print(pairs)
方法二:树状数组求解
树状数组实际上还是一个数组,只不过它的每个元素保存了跟原来数组的一些元素相关的结合值。
若A为原数组,定义数组C为树状数组。C数组中元素C[ i ]表示A[ i –lowbit( i ) + 1]至A[ i ]的结合值。
lowbit(i)是i的二进制中最后一个不为零的位数的2次方,可以这样计算
lowbit(i)=x&(-x)
lowbit(i)=x&(x^(x-1))
当想要查询一个sum(n)时,可以依据如下算法即可:
step1: 令sum = 0,转第二步;
step2: 假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;
step3: 令n = n – lowbit(n),转第二步。
n = n – lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。而n的二进制里最多有log(n)个1,所以查询效率是log(n)的。
修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。所以修改算法如下(给某个结点i加上x):
step1: 当i > n时,算法结束,否则转第二步;
step2: Ci = Ci + x, i = i + lowbit(i)转第一步。
i = i +lowbit(i)这个过程实际上也只是一个把末尾1补为0的过程。
求逆序的思路:
可以把数一个个插入到树状数组中, 每插入一个数, 统计比他小的数的个数,对应的逆序为 i- getsum( data[i] ),其中 i 为当前已经插入的数的个数, getsum( data[i] )为比 data[i] 小的数的个数,i- getsum( data[i] ) 即比 data[i] 大的个数, 即逆序的个数。最后需要把所有逆序数求和,就是在插入的过程中边插入边求和。
方法二这里就不赘述了
3.2查找前k个最大数
#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
# n个整数,找出其中最大的k个数
构建一个k的小顶堆T,从k+1开始,若小于T[0],则更新该小顶堆
或者构建n的小顶堆返回末尾的k个数 及构建n的大顶堆返回前k个数
后一种时间复杂度更优,但消耗空间较大
'''
def heap_sort(ary, num):
def siftdown(ary, e, begin, end):
i, j = begin, begin * 2 + 1
while j < end:
if j + 1 < end and ary[j + 1] < ary[j]:
j += 1
if e < ary[j]:
break
ary[i] = ary[j]
i, j = j, j * 2 + 1
ary[i] = e
end = len(ary)
for i in range(end // 2 - 1, -1, -1):
siftdown(ary, ary[i], i, end)
# 方法1
for i in range(end - 1, -1, -1):
e = ary[i]
ary[i] = ary[0]
siftdown(ary, e, 0, i)
return ary[:-num - 1:-1]
# 方法2
'''
li = []
for i in range(num):
if len(ary) > i:
li.append(ary[0])
e = ary[end-1-i]
siftdown(ary, e, 0, end-1-i)
else:
break
return li
'''
if __name__ == '__main__':
a = [4, 5, 1, 6, 2, 7, 3, 8]
num = int(input("最小的k个数:"))
print(heap_sort(a, num))
from math import ceil
import copy
'''
算法: BFPRT
语言: python
作用: 给定无序浮点list,选取最大n个数
输入: ctr_scores,N个数浮点无序数组
输出: largest_n_id,n个最大数的index
时间复杂度: O(N)
空间复杂度: O(N)
example1:
array = [6,2,3,4,1]
print(topN(array,3))
print(topN(array,1))
print(topN(array,0))
print(topN(array,5))
[0, 2, 3]
[0]
[]
[0, 1, 2, 3, 4]
example2:
array = [2,2,3,3]
print(topN(array,3))
print(topN(array,1))
print(topN(array,0))
print(topN(array,6))
print(topN(array,2))
[2, 3, 0]
[2]
[]
n should <length of ctr_scores!
None
[2, 3]
example3:
array = [3,3,3,3]
print(topN(array,3))
print(topN(array,1))
print(topN(array,2))
[0, 1, 2]
[0]
[0, 1]
'''
# 将输入的数组划分为5个一组,不足5的单独一组,并在每个组内插入排序
def split_insertSort(array,group_num):
for k in range(group_num):
start = k * 5 # 需排序的子数组起点
end = min((k+1)*5,len(array)) # 需排序的子数组终点
for i in range(start+1, end):
if array[i - 1] > array[i]:
temp = array[i] # 当前需要排序的元素
index = i # 用来记录排序元素需要插入的位置
while index > start and array[index - 1] > temp:
array[index] = array[index - 1] # 把已经排序好的元素后移一位,留下需要插入的位置
index -= 1
array[index] = temp # 把需要排序的元素,插入到指定位置
return array
# 返回中位数数组
def getMedian(array,group_num):
mArray = []
for k in range(group_num):
start = k * 5 # 子数组起点
end = min((k+1)*5,len(array)) # 子数组终点
if (end - start)%2 == 1: # 如果子数组长度奇数
mArray.append(array[int((start+end-1)/2)])
else: # 偶数,取下中位数
mArray.append(array[int(((start+end-1)-1)/2)])
return mArray
# 大于等于中位数的放在右边,小于中位数的放在左边
def partition(ctr_scores,median):
left_median = []
right_median = []
isFirstShow = 0
for num in ctr_scores:
if num < median: # 左边,小于
left_median.append(num)
elif num == median:# 第一次出现忽略过去
if isFirstShow == 0:
isFirstShow = 1
else:
right_median.append(num)
else:# 右边,大于等于
right_median.append(num)
return left_median,right_median
def topN(ctr_scores, n):
def BFPRT_findMedian(array):
# 得到有多少组
group_num = ceil(len(array)/5)
# 按照5组一个划分,并且组内插排
array = split_insertSort(array,group_num)
array = getMedian(array,group_num)# O(N)
if len(array) == 1: # 如果已经找到,就退出递归
return array[0]
else: # 递归调用求中位数
return BFPRT_findMedian(array)
def BFPRT_Main(ctr_scores,n):
median = BFPRT_findMedian(ctr_scores)
# 根据求得的中位数遍历一次,划分,小的在左边,大的在左边
left_median,right_median = partition(ctr_scores,median)
if len(right_median) == n:
return median
elif len(right_median) < n :
left_median.append(median)
return BFPRT_Main(left_median,n - len(right_median))
else:
return BFPRT_Main(right_median,n)
ctr_scores_run = copy.copy(ctr_scores)
if n>len(ctr_scores):
print('n should <length of ctr_scores!')
return
if n == len(ctr_scores):
median = -1
elif n == 0:
median = 9999999999
else:
median = BFPRT_Main(ctr_scores_run,n)
largest_n_index = []
isFirstShow = 0
for i in range(len(ctr_scores)):# O(N)
if ctr_scores[i] <= median: # 左边,小于
continue
else:# 右边,大于等于
largest_n_index.append(i)
n-=len(largest_n_index)
if 0 < n:# O(N)
for i in range(len(ctr_scores)):
if ctr_scores[i] == median: # 左边,小于
largest_n_index.append(i)
n-=1
if n == 0:
break
return largest_n_index
在一堆数中求其前k大或前k小的问题,简称TOP-K问题。而目前解决TOP-K问题最有效的算法即是BFPRT算法,又称为中位数的中位数算法,该算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最坏时间复杂度为O(n)。
在首次接触TOP-K问题时,我们的第一反应就是可以先对所有数据进行一次排序,然后取其前k即可,但是这么做有两个问题:
- 快速排序的平均复杂度为O(nlogn),但最坏时间复杂度为O(n2),不能始终保证较好的复杂度;
- 我们只需要前k大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。
除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为k的堆,时间复杂度为O(nlogk)。
那是否还存在更有效的方法呢?我们来看下BFPRT算法的做法。
在快速排序的基础上,首先通过判断主元位置与k的大小使递归的规模变小,其次通过修改快速排序中主元的选取方法来降低快速排序在最坏情况下的时间复杂度。
下面先来简单回顾下快速排序的过程,以升序为例:
- 选取主元;
- 以选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
- 分别对左边和右边进行递归,重复上述过程。
二:算法过程及代码
BFPRT算法步骤如下:
- 选取主元;
1.1. 将n个元素按顺序分为⌊n5⌋个组,每组5个元素,若有剩余,舍去;
1.2. 对于这⌊n5⌋个组中的每一组使用插入排序找到它们各自的中位数;
1.3. 对于 1.2 中找到的所有中位数,调用BFPRT算法求出它们的中位数,作为主元; - 以 1.3 选取的主元为分界点,把小于主元的放在左边,大于主元的放在右边;
- 判断主元的位置与k的大小,有选择的对左边或右边递归。
3.3 链表排序