排序算法总结


1. 一些概念

1.1稳定的排序

数据在经过排序后,两个相同键值的记录仍然保持原来的次序,也就是两个相同的值在排序后前后顺序不变;常见的稳定排序算法有:冒泡、插入、基数排序;不稳定的排序算法有:选择排序、快速排序、堆积排序、希尔排序。

1.2 原地排序

指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序;希尔排序、冒泡排序、插入排序、选择排序、堆排序、快速排序都是原地排序,合并排序不是原地排序


2. 选择排序

2.1 逻辑和程序实现

以 55 23 87 62 16 排序为例
step1:找到此数列中最小值和第一个元素交换,得: 16 23 87 62 55
step2:从该数列第二个元素开始找,找到数列中最小的值和第二个元素交换,得:16 23 87 62 55
step3: 从第三个元素开始找,如此重复下去…

程序实现如下:

data = [16,26,39,27,12,8,45,63]

# way1
# for i in range(0,len(data)-1):
# 	for j in range(i+1,len(data)):
# 		if data[i] <= data[j]:
# 			continue
# 		else:
# 			data[i],data[j] = data[j],data[i]
# print(data)

# way2:
for i in range(len(data)-1):
	minIndex = i
	for j in range(i+1, len(data)):
		if data[j] < data[minIndex]:
			minIndex = j
	if minIndex != i:
		data[minIndex], data[i] = data[i], data[minIndex]
print(data)

输出:

[8, 12, 16, 26, 27, 39, 45, 63]

2.2 算法分析

(1)无论是最坏情况,平均情况,还是最好情况都需要找到最小(大)值,比较次数为 ( n − 1) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 \text{(}n-\text{1)}+\left( n-2 \right) +...+1=\frac{n\left( n-1 \right)}{2} n1+(n2)+...+1=2n(n1),因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)

(2)选择排序是将最大(最小)值直接与前方数字进行交换,排列顺序可能改变,如 序列5 5 3;因此 不是稳定的排序算法

(3)只需要一个额外的空间用于交换,空间复杂度为 O ( 1 ) O(1) O(1)


3.插入排序

3.1 逻辑和程序实现

将待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

以 55 23 87 62 16 为例:
step1: 55
step2: 23 55
step3: 23 55 87
step4: 23 55 62 87
step5: 16 23 55 62 87

程序实现如下:

data = [16,26,39,27,12,8,45,63]

for i in range(1,len(data)):
	num = i-1
	temp = data[i]
	#将第i个数据与前面排好序的数据逐个比较
	while num>=0 and temp< data[num]:
		data[num+1] = data[num]
		num -= 1
	data[num+1] = temp	
print(data)

输出

[8, 12, 16, 26, 27, 39, 45, 63]

3.2 算法分析

(1)最坏情况下(逆序)需要比较 ( n − 1) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 \text{(}n-\text{1)}+\left( n-2 \right) +...+1=\frac{n\left( n-1 \right)}{2} n1+(n2)+...+1=2n(n1)次,时间复杂度为 O ( n 2 ) O(n^2) O(n2);最好情况下(已经排好的情况)时间复杂度为 O ( n ) O(n) O(n)

(2)如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面,所以插入排序是稳定的排序算法

(3)空间复杂度为 O ( 1 ) O(1) O(1)


4. 希尔排序

4.1 逻辑和程序实现

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

希尔排序的基本思想是:将数据区分成特定区间的几个小块,用插入排序法排完区块内的数据后,在渐渐减少间隔距离

下面使用【63、92、27、36、45、71、58、7】从小到大排序说明希尔排序的过程
(1)将所有数据分成 8 / 2 = 4 8/2=4 8/2=4块(划分数不一定要2,其他数也行,一般习惯上用2),所以一开始间隔为4,如下图所示,分成4个区块,分别是 ( 63 , 45 ) , ( 92 , 71 ) , ( 27 , 58 ) , ( 36 , 7 ) (63, 45), (92, 71), (27, 58), (36, 7) (63,45),(92,71),(27,58),(36,7)
在这里插入图片描述
在分别用插入排序成为 ( 45 , 63 ) , ( 71 , 92 ) , ( 27 , 58 ) , ( 7 , 36 ) (45, 63), (71, 92), (27, 58), (7, 36) (45,63),(71,92),(27,58),(7,36),第一轮后的结果为:【45,71,27,7,63,92,58,36】

(2)缩小间隔 ( 8 / 2 ) / 2 (8/2)/2 (8/2)/2,如下图所示,分成 ( 45 , 27 , 63 , 58 ) , ( 71 , 7 , 92 , 36 ) (45, 27, 63, 58), (71, 7, 92, 36) (45,27,63,58),(71,7,92,36)两个区块
在这里插入图片描述
再分别对两个区块进行插入排序,最后得到的结果是【27,7,45,36,58,71,63,92】

(3)最后用 ( 8 / 2 ) / 2 / 2 (8/2)/2/2 (8/2)/2/2的间距进行插入排序,也就是对每个元素都进行排序,最后得到的结果为【7,27,36,45,58,63,71,92】

程序实现如下

data = [16,26,39,27,12,8,45,63]
jump = len(data)//2
while jump != 0:
	# 插入排序
	for i in range(jump, len(data)):
		temp = data[i]
		j = i-jump
		while temp < data[j] and j >= 0:
			data[j+jump] = data[j]
			j = j-jump
		data[j+jump] = temp
	# 排序完调整间隔
	jump = jump//2
print(data)

输出

[8, 12, 16, 26, 27, 39, 45, 63]

4.2 算法分析

(1)时间复杂度:希尔排序的复杂度和增量序列是相关的,{1,2,4,8,…}这种序列并不是很好的增量序列,最坏的情况下还是 O ( n 2 ) O(n^2) O(n2), Hibbard提出了另一个增量序列{ 1 , 3 , 7 , . . . , 2 k − 1 1,3,7,...,2^k-1 1,3,7...,2k1},这种序列的时间复杂度(最坏情形)为 O ( n 3 / 2 ) O(n^{3/2}) O(n3/2)

(2) 稳定性:不是稳定的,虽然插入排序是稳定的,但是希尔排序在插入的时候是跳跃性插入的,有可能破坏稳定性,如序列【7,5,5,8】
在这里插入图片描述
得到结果如下,破坏了稳定性
在这里插入图片描述
(3)空间复杂度: O ( 1 ) O(1) O(1)


5.合并排序

5.1 逻辑和程序实现

step1:将N个长度为1的键值合并成N/2个长度为2的键值组
step2:将N/2个长度为2的键值组合并成N/4个长度为4的键值组
step3:将键值组不断的合并,直到合并成一组长度为N的键值组为止
下面使用【38,16,41,72,52,98,63,25】从大到小排序为例,说明归并排序过程
在这里插入图片描述
程序实现如下

def merge_sort(arr):
	if len(arr) < 2:
		return arr
	else:
		mid = len(arr)//2
		left, right = arr[:mid], arr[mid:]
		return merge(merge_sort(left), merge_sort(right))

def merge(left, right):
	result = []
	while left and right:
		if left[0] <= right[0]:
			result.append(left.pop(0))
		else:
			result.append(right.pop(0))
	if left:
		result.extend(left)
	if right:
		result.extend(right)
	return result

if __name__ == "__main__":
	data = [16,26,39,27,12,8,45,63]
	result = merge_sort(data)
	print(result)

输出

[8, 12, 16, 26, 27, 39, 45, 63]

5.2 算法分析

(1)时间复杂度, n项数据需要 l o g 2 n log_2n log2n次处理,每次处理的时间复杂度 O ( n ) O(n) O(n),所以归并排序的最差、最好、平均情况复杂度都为 O ( n l o g n ) O(nlogn) O(nlogn)
(2)空间复杂度, O ( n ) O(n) O(n),是非原地算法
(3)是一个稳定的排序方式


6.快速排序

6.1 逻辑和程序实现

快速排序法又称为分割交换排序法,是目前公认的最佳排序法,整体思想为:先在数据中找到一个虚拟的中间值(一般直接将第一个元素设置为中间值),并按此中间值将所有打算排序的数据分为两部分,其中小于中间值的数据放在左边,大于中间值的数据放在右边,再以同样的方式分别处理左右两边的数据,直到排完序为止。具体分割步骤如下:

step1:假设 K K K的值为第一个键值
step2:从左向右找出键值 K i K_i Ki,使得 K i > K K_i>K Ki>K
step3:从右向左找出键值 K j K_j Kj,使得 K j < K K_j<K Kj<K
step4:如果 i < j i<j i<j,那么 K i K_i Ki K j K_j Kj互换,并回到step2
step5:若 i > = j i>=j i>=j,则将 K K K K j K_j Kj互换,并以 j j j为基准点分割成左右部分,然后针对左右两边进行步骤step1-5

下面使用【26、3、38、1、67、8、55、14、43、18】为例讲解快速排序的过程
在这里插入图片描述
因为 i < j i<j i<j,故交换 K i K_i Ki K j K_j Kj,然后继续比较
在这里插入图片描述
因为 i < j i<j i<j,故交换 K i K_i Ki K j K_j Kj,然后继续比较
在这里插入图片描述
因为 i > = j i>=j i>=j,故交换 K K K K j K_j Kj,并以 j j j为基准点分割成左右两半:
在这里插入图片描述
经过上面几个步骤后,小于键值K的数据都放在其左边,大于键值K的数据都放在了其右边,再按照上面的步骤,对左半边和右半边的数据再分别排序,如此重复下去,最后的结果为
在这里插入图片描述
程序实现如下

def quick(data, left, right):
	size = len(data)
	if left < right:

		# 从左到右找一个大于K的值
		leftIdx = left + 1
		while leftIdx < size and data[leftIdx] < data[left]:
			leftIdx += 1

		# 从右到左找一个小于K的值
		rightIdx = right
		while rightIdx >= 0 and data[rightIdx] > data[left]:
			rightIdx -= 1

		# 如果left < right, 则交换
		while rightIdx > leftIdx:
			data[leftIdx], data[rightIdx] = data[rightIdx], data[leftIdx]

			leftIdx += 1
			while leftIdx < size and data[leftIdx] < data[left]:
				leftIdx += 1
			
			rightIdx -= 1
			while rightIdx >= 0 and data[rightIdx] > data[left]:
				rightIdx -= 1

		data[left], data[rightIdx] = data[rightIdx], data[left]

		# 将左右两半各自再次进行快速排序
		quick(data, left, rightIdx-1)
		quick(data, rightIdx+1, right)

if __name__ == '__main__':
	data = [10, 7, 8, 9, 1, 5]
	quick(data, 0, len(data)-1)
	print(data)

输出

[1, 5, 7, 8, 9, 10]

6.2 算法分析

(1)在最好和平均情况下,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),最坏情况下就是每次挑中的中间值不是最大就是最小,因而最坏情况下时间复杂度为 O ( n 2 ) O(n^2) O(n2)

(2)快速排序法不是稳定的排序算法

(3)最差情况下空间复杂度为 O ( n ) O(n) O(n),最好情况下为 O ( l o g 2 n ) O(log_2n) O(log2n)

(4)快速排序法是平均运行时间最快的排序法


7.堆积排序

7.1 逻辑和程序实现

堆积排序法用到了二叉树的技巧,他是利用堆来完成排序的,堆是一种特殊的二叉树,可分为最大堆和最小堆,最大堆满足如下条件:

  • 他是一个完全二叉树
  • 所有节点的值都大于等于他左右子节点的值
  • 树根是堆积树中最大的

堆排序的基本思路是:首先将数据建成堆积树,然后循环的将堆积树的树根放到堆的末尾;这里建立堆积树使用数组,而不用链表,用数组构建树,一维数组中的索引有如下关系:

  • 左子树索引值是父节点索引值*2+1
  • 右子树索引值是父节点索引值*2+2

接下来用【[0, 5, 6, 4, 8, 3, 2, 7, 1]】建立一棵堆积树来理解堆积树构建过程:

(1)原始的二叉树如下所示
在这里插入图片描述
(2)从最后的子树开始,即索引值为【3, 7, 8】构成的子树,如上图 arr[3]=4, arr[7]=7, arr[8]=1;arr[3]为树根,若arr[7]大于根节点,则必须交换,然后判断arr[8]是否大于arr[3],若大于也必须交换,此处arr[7]>arr[3],需要交换,交换后arr[8]=1 < arr[3]=7,不需要再次交换,最后结果如下图
在这里插入图片描述
(3)再用同样的方式判断由索引【2,5,6】构成的子树中是否需要交换数值,由于arr[5]<arr[2],arr[6]<arr[2]故该子树中不需要交换数值,结果仍然如上图

(4)再判断由索引【1,3,4】构成的子树,由于arr[3]>arr[1],索引需要将arr[3]和arr[1]交换,然后arr[4]=8 > arr[1]=7,还要继续交换,最后结果如下:
在这里插入图片描述
(5)最后判断索引【0,1,2】构成的子树,只需交换arr[1]和arr[0],得到结果如下
在这里插入图片描述
在arr[1]和arr[0]交换后,还需要检查索引【0】为树根的子树是否需要交换数值,可以发现首先arr[3]要和arr[1]交换,然后arr[4]要和arr[1]交换,同时又要检查索引【3】为树根的子树是否需要交换数值,最后结果为
在这里插入图片描述
其实,如果有个子树里面需要交换数值,在交换完后还要检查叶子节点为根的子树是否需要交换数值,如在步骤(2)中,arr[7]和arr[3]交换后需要检查以arr[7]为树根的子树是否需要交换数值,(2)中是因为arr[7]后面没有节点,因此未检查;再如步骤(5)中所示,会依次向下检查。

上面就是堆积树建立过程,上图就是一棵堆积树。在建立好堆积树后,我们需要用堆积树来排序,具体步骤如下:
(1)将堆积树的树根与最后的节点元素交换,结果如下图;
在这里插入图片描述
同时,我们在不考虑索引【8】的元素下(即只考虑索引【0-7】的元素)按上面构建堆积树的步骤,将索引【0-7】重新构建成一棵堆积树,如下图所示
在这里插入图片描述
(2)再又将树根元素与最后节点元素相交换(由于不考虑索引【8】,因此最后节点元素是索引【7】对应的元素),结果如下图
在这里插入图片描述
同时,在舍弃最后的元素不考虑的情况下,将其他元素重新构建成堆积树,如下图所示
在这里插入图片描述
(3)按照步骤(1)(2)所示,重复下去,最后结果如下图所示,即为排序结果
在这里插入图片描述
堆排序程序实现如下:

# 从最后一棵子树开始向上建立堆积树
def buildMaxHeap(data):
	for i in range(len(data)//2, -1, -1):
		heapify(data, i)

# 构建堆积树
def heapify(data, i):
	left = 2*i + 1 
	right = 2*i + 2
	largest = i
	# 判断左子节点是否需要交换
	if left < length and data[left] > data[largest]:
		largest = left
	# 判断右子节点是否需要交换
	if right < length and data[right] > data[largest]:
		largest = right
	if largest != i:
		data[i], data[largest] = data[largest], data[i]
		# 如果交换了,要继续检查子节点为根的子树是否需要交换
		heapify(data, largest)

# 使用堆积树排序
def heapSort(data):
	global length
	length = len(data)
	buildMaxHeap(data)
	# 将堆积树的树根和最后一个子节点交换,此处用length来控制不考虑最后一个子节点
	for i in range(len(data)-1, 0, -1):
		data[0], data[i] = data[i], data[0]
		length -= 1
		# 首尾交换后需要重新构建堆积树
		heapify(data, 0)
	return data

输出

[0, 1, 2, 3, 4, 5, 6, 7, 8]

7.2 算法分析

(1)时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
(2)空间复杂度: O ( 1 ) O(1) O(1)
(3)不是稳定排序算法


8.基数排序

8.1 逻辑和程序实现

基数排序与之前的排序方法不太一样,它并不需要进行元素间的比较操作,是属于一种分配模式排序方式;基数排序法按比较的方向可分为最高位优先(MSD) 和最低位优先(LSD)两种。MSD是从最左边的的位数开始比较,而LSD是从最右边的低位开始比较

下面用LSD对三位数排序为例,讲解工作原理
在这里插入图片描述
(1)把每个数字按其个位数字放到列表中,如下
在这里插入图片描述
合并后成为
在这里插入图片描述
(2)再将其按十位数字按序放入列表中
在这里插入图片描述
合并后成为
在这里插入图片描述
(3)再按百位数字按序放入列表当中
在这里插入图片描述
合并后成为如下所示,即为排序结果
在这里插入图片描述
基数排序程序实现如下

def dadix(data, maxnum):
	n = 1 # n为基数,从个位数开始排序;参数maxnum表示最大数字是几位数,如果是三位数将其设置成100
	while n<=maxnum:
		# 将各个数字按个位(十位/百位)放入表格中
		size = len(data)
		temp = [[0]*size for i in range(10)]
		for i in range(size):
			m = (data[i]//n)%10
			temp[m][i] = data[i]

		# 从表格按照顺序取出数字放入一维数组中
		k = 0
		for i in range(10):
			for j in range(size):
				if temp[i][j] != 0:
					data[k] = temp[i][j]
					k += 1
		n = n*10 #将个位调整为十位/百位

if __name__ == '__main__':
	data = [59,95,7,34,60, 168, 171, 259, 372, 45, 88, 133]
	dadix(data, 100)
	print(data)

输出

[7, 34, 45, 59, 60, 88, 95, 133, 168, 171, 259, 372]

程序中的表格格式设置成如下形式,按个位放入后,然后按行的顺序取出
在这里插入图片描述

8.2 算法分析

(1)时间复杂度: O ( n ) O(n) O(n)
(2)空间复杂度: O ( n ) O(n) O(n)
(3)是稳定的排序算法


9.计数排序

9.1 计数排序基本逻辑

计数排序不是比较排序,排序的速度快于任何比较排序算法。用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
下面直接说使用对【9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9】排序的例子说明计数排序的逻辑思路

(1)由于最大值为10,最小值为1,即所有数字都在0-10之间,建立一个长度为11的数组,数组下标从0到10,元素初始值全为0,如下所示:
在这里插入图片描述
(2)遍历这个数组,每一个整数按照其值对号入座,对应数组下标的元素进行加1操作:比如第一个整数是9,那么数组下标为9的元素加1
在这里插入图片描述
第二个整数是3,那么数组下标为3的元素加1:
在这里插入图片描述
一直遍历完整个无序数组,最后得到如下结果:
在这里插入图片描述
(3)数组中的每一个值,代表了数列中对应整数的出现次数。有了这个统计结果,排序就很简单了,直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次,最后得到排序结果为 【0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10】

9.2 计数排序的改进

(1)上面的计数排序存在一些缺陷,比如【95, 94, 91, 98, 99, 90, 99, 93, 91, 92】该数列最大值是99,但最小值是90,如果我们只以数列的最大值来决定统计数组的长度的话,就要创建长度为100的数组,那么就会浪费前面90个空间。

改进1
数列最大值和最小值的差+1作为统计数组的长度。同时,数列的最小值作为一个偏移量,用于统计数组的对号入座。以刚才的数列为例,统计数组的长度为 99-90+1=10,偏移量等于数列最小值90。对于第一个整数95,对应的统计数组下标为95-90=5,如图所示:
在这里插入图片描述
(2)现实业务里,比如给学生的考试分数排序,如果遇到相同的分数就会分不清谁是谁。看看下面这个例子:小辉:90;大黄:99;小红:95;小白:94;小绿:95;当我们填充统计数组之后,我们只知道有两个成绩并列95分的学生,却不知道谁是小红,谁是小绿
改进2
在填充完统计数组之后,对统计数组做一下变形。我们仍然以学生的成绩表为例,把之前的统计数组进行变形,统计数组从第二个元素开始,每一个元素都加上前面所有元素之和

在这里插入图片描述
相加的目的就是为了让统计数组存储的元素值等于相应整数的最终排序位置。比如下标是9的元素值是5,代表原始数列的整数99最终的排序是在第5位;接下来,我们创建输出数组sortedArray,长度和输入数列一致,然后从后向前遍历输入数列

step1:遍历成绩表最后一行的小绿:小绿是95分,找到countArray下标为5的元素,值是4,代表小绿的成绩排名是在第4位,同时给countArray下标是5的元素值减1,从4变成3,代表着下次再遇到95分时,最终排名是第3位

step2:历成绩表倒数第二行的小白:小白是94分,找到countArray下标是4的元素,值是2,代表小白的成绩排名在第2位。同时,给countArray下标是4的元素值减1,从2变成1,代表下次再遇到94分的成绩时(实际上已经遇不到了),最终排名是第1位

step3:遍历成绩表倒数第三行的小红:小红是95分,找到countArray下标是5的元素,值是3(最初是4,减1变成了3),代表小白的成绩排名在第3位。同时,给countArray下标是5的元素值减1,从3变成2,代表下次再遇到95分的成绩时(实际上已经遇不到了),最终排名是第2位。
在这里插入图片描述
同样是95分的小红和小绿就能清楚地排出顺序,所以优化版的计数排序属于稳定排序;后面的遍历过程依此类推

9.3 计数排序步骤

由上面分析可知,计数排序步骤为:
(1)找到给定数据的最大最小值
(2)创建统计数组并计算统计对应元素个数
(3)统计数组变形
(4)倒序遍历原始数组,从统计数组中找到正确位置,输出到结果数组

9.4 程序实现

计数排序程序实现如下

def countSort(data):
	# 求最大值最小值
	minnum, maxnum = data[0], data[0]
	for i in range(len(data)):
		if data[i]<minnum:
			minnum = data[i]
		if data[i]>maxnum:
			maxnum = data[i]
	d = maxnum-minnum

	# 创建统计数组并计算统计对应元素个数
	countArray = [0]*(d+1)
	for num in data:
		countArray[num-minnum] += 1

	# 统计数组变形
	sumnum = 0
	for i in range(len(countArray)):
		sumnum += countArray[i]
		countArray[i] = sumnum

	# 倒序遍历原始数组,从统计数组中找到正确位置,输出到结果数组
	sortedArray = [0]*len(data)
	for i in range(len(data)-1, -1, -1):
		sortedArray[countArray[data[i]-minnum]-1] = data[i]
		countArray[data[i]-minnum] -= 1
	return sortedArray

if __name__ == '__main__':
	data = [90, 99, 95, 94, 95]
	result = countSort(data)
	print(result)

输出

[90, 94, 95, 95, 99]

9.5 算法分析

(1)时间复杂度:如果原始数列的规模是N,最大最小整数的差值是M,由于代码中第1、2、4步都涉及到遍历原始数列,运算量都是N,第3步遍历统计数列,运算量是M,所以总体运算量是3N+M,去掉系数,时间复杂度是 O ( N + M ) O(N+M) O(N+M)

(2)空间复杂度:如果不考虑结果数组,只考虑统计数组的话,空间复杂度是 O ( M ) O(M) O(M)

(3)改进后是稳定的排序算法

(4)缺陷:
【A】当数列最大最小值差距过大时,并不适用于计数排序;比如给定20个随机整数,范围在0到1亿之间,此时如果使用计数排序的话,就需要创建长度为1亿的数组,不但严重浪费了空间,而且时间复杂度也随之升高
【B】当数列元素不是整数时,并不适用于计数排序;如果数列中的元素都是小数,比如3.1415,或是0.00000001这样子,则无法创建对应的统计数组,无法进行计数排序

9.6 参考

什么是计数排序?


10.桶排序

10.1 基本思想

桶排序的基本思想是将一个数据表分割成许多buckets(使用映射函数,将数据分入不同的容器中),然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。它是一个分布式的排序,介于MSD基数排序和LSD基数排序之间。

10.2性能分析

(1)时间复杂度
初始化桶,连接排好序的桶,其时间复杂度为 O ( n ) O(n) O(n) 一般有m<n (m个桶);

对于每个桶采用其他的排序算法:m个桶,每个桶中的元素平均假设有n/m个,在上面进行基于比较的排序,复杂度不会低于 n ∗ O ( n / m ∗ l g ( n / m ) ) n*O(n/m*lg(n/m)) nO(n/mlg(n/m)),平均意义下每个桶中的元素有n/m个, O ( m ∗ n / m ∗ l g ( n / m ) = O ( n ∗ l g ( n / m ) ) O(m * n/m *lg(n/m) = O(n*lg(n/m)) O(mn/mlg(n/m)=O(nlg(n/m)),所以总的时间复杂度为 T ( n ) = O ( n + n ∗ l g ( n / m ) ) T(n)=O(n+n*lg(n/m)) T(n)=O(n+nlg(n/m))

当m=n时时间复杂度为O(n),此时和计数排序一样,桶数量越多,时间效率越高,然而桶数量越多占用空间也就越大

(2)桶排序是稳定排序算法


11.冒泡排序

11.1 冒泡排序基本逻辑

从第一个元素开始,比较相邻元素大小,若大小顺序有误,则对调后再进行下一个元素的比较,这样扫描一次后,就可以使得最后一个元素是最大值(或最小值),接着再扫描第二次,直到完成所有元素的排序。

下面以【55, 23, 87, 62, 16】从小到大排序为例演示冒泡排序的过程
在这里插入图片描述
(1)第一次扫描先用第一个元素55和第二个元素23比较, 55>23,故交换;然后55<87,不叫换;87>62,交换;87>16,交换。最后得到结果如下

在这里插入图片描述
(2)第二次扫描,首先23<55,不交换;55<62,不交换;62>16,交换,得到如下结果

在这里插入图片描述
(3)如此循环扫描下去,最后得到结果为
在这里插入图片描述
从上面的排序过程可知,5个元素的冒泡排序必须执行5-1次扫描,第一次扫描比较5-1次,一共比较4+3+2+1=10次

11.2 程序实现

data=[16,25,39,27,12,8,45,63]

for i in range(1,len(data)):
	#当某次扫描时所有数都没有发生交换,则直接退出,已经排好序
	flag = 0
	for j in range(len(data)-i):
		if data[j]>data[j+1]:
			data[j], data[j+1] = data[j+1], data[j]
			flag = 1
	#判读是否要提前退出
	if flag == 0:
		break
print(data)

输出

[8, 12, 16, 25, 27, 39, 45, 63]

11.3 算法分析

(1)时间复杂度:最坏和平均情况下都需比较 ( n − 1 ) + ( n − 2 ) + . . . + 2 + 1 = n ( n − 1 ) / 2 (n-1)+(n-2)+...+2+1=n(n-1)/2 (n1)+(n2)+...+2+1=n(n1)/2次,时间复杂度为 O ( n 2 ) O(n^2) O(n2),最好情况下只需扫描一次,发现没有交换操作,就表示已经排序完成,时间复杂度为 O ( n ) O(n) O(n)

(2)空间复杂度: O ( 1 ) O(1) O(1)

(3)是稳定的排序算法

12. 总结

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值