〇. 目录
一. 选择排序
二. 冒泡排序
三. 插入排序法
四. 合并排序法(分治排序算法)
五. 希尔排序算法
六. 快速排序算法
七. 堆排序算法
八. 计数排序算法
九. 基数排序算法
一. 选择排序
1.1 定义
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
1.1.1 算法步骤
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
1.1.2 动图解释
1.2 案例
- 有数组[56, 18, 49, 84, 72]
- 要求:采用选择排序法对其进行排列
list_1 = [56, 18, 49, 84, 72]
- 找到最小值,并将其与56交换
- 从第二个开始,找到剩下的值中的最小值,并将其与第一个值调换
- 以此类推,将所有数值进行排序
list_1 = [56, 18, 49, 84, 72]
print('Begin:', list_1)
for i in range(len(list_1)):
for j in range(i+1, len(list_1)):
if list_1[j] <= list_1[i]:
list_1[j], list_1[i] = list_1[i], list_1[j]
if i != len(list_1) - 1:
print(f'第{i+1}次循环结束后:{list_1}')
print('End:', list_1)
Begin: [56, 18, 49, 84, 72]
第1次循环结束后:[18, 56, 49, 84, 72]
第2次循环结束后:[18, 49, 56, 84, 72]
第3次循环结束后:[18, 49, 56, 84, 72]
第4次循环结束后:[18, 49, 56, 72, 84]
End: [18, 49, 56, 72, 84]
二. 冒泡排序
2.1 定义
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
2.1.1算法步骤
-
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
-
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2.1.2 动图解释
2.2 案例
- 有一个数列[56, 20, 84, 66, 13]
- 按照冒泡排序算法进行递增排序
list_2 = [56, 20, 84, 66, 13]
list_2 = [56, 20, 84, 66, 13]
print('Begin:', list_2)
for i in range(len(list_2) - 1): # 确定循环次数
for j in range(len(list_2) - 1): # 排序循环
if list_2[j] >= list_2[j+1]: # 前一项比后一项大,则交换顺序
list_2[j], list_2[j+1] = list_2[j+1], list_2[j]
print(f'第{i + 1}次循环结束后:', list_2)
print('End:', list_2)
Begin: [56, 20, 84, 66, 13]
第1次循环结束后: [20, 56, 66, 13, 84]
第2次循环结束后: [20, 56, 13, 66, 84]
第3次循环结束后: [20, 13, 56, 66, 84]
第4次循环结束后: [13, 20, 56, 66, 84]
End: [13, 20, 56, 66, 84]
三. 插入排序法
3.1 定义
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1.1 算法步骤
-
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
-
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
3.1.2 动图解释
3.2 案例
- 有一个数列[58, 29, 86, 69, 10]
- 采用插入算法使其递增排列
list_3 = [58, 29, 86, 69, 10]
print('Begin:', list_3)
for i in range(len(list_3)):
temp = list_3[i] # 将一个元素拿出
j = i - 1 # 前i-1个元素视为已经排好的数列
while j >= 0 and temp < list_3[j]: # 保证角标大于0;并且temp比前一个数要小,那么就将拍好的数列一次向后退1
list_3[j+1] = list_3[j] # 退1操作
j -= 1 # 检查前一个元素
list_3[j+1] = temp # 检查完毕后将数插入合适的位置
print(f'第{i+1}次排序:', list_3)
print('End:', list_3)
Begin: [58, 29, 86, 69, 10]
第1次排序: [58, 29, 86, 69, 10]
第2次排序: [29, 58, 86, 69, 10]
第3次排序: [29, 58, 86, 69, 10]
第4次排序: [29, 58, 69, 86, 10]
第5次排序: [10, 29, 58, 69, 86]
End: [10, 29, 58, 69, 86]
3.3 实例:跳绳比赛排名
-
将5名同学的成绩进行排序
-
输出冠亚季军
-
数据集:
data = [('Mark', 149), ('Lisa', 170), ('Tom', 156), ('Jerry', 144), ('Linda', 139), ]
for i in range(len(data)):
temp = data[i]
j = i - 1
while j >= 0 and temp[1] > data[j][1]:
data[j+1] = data[j]
j -= 1
data[j+1] = temp
print('总排名:', data)
name = ['冠军', '亚军', '季军']
for i in range(len(name)):
print(f'{name[i]}是:{data[i]}')
总排名: [('Lisa', 170), ('Tom', 156), ('Mark', 149), ('Jerry', 144), ('Linda', 139)]
冠军是:('Lisa', 170)
亚军是:('Tom', 156)
季军是:('Mark', 149)
四. 合并排序法(分治排序算法)
4.1 定义
- 将一个无序的数组自顶向下递归分解,每次二分,直到每份只有一个数,然后将它们逐层合并为有序数组,生成最终的排序结果。(分解与合并是对称的,即分解的过程限定了合并的路径)
- 归并排序(Merge sort) 是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer) 的一个非常典型的应用。
4.1.1 算法步骤
- 分解:将待排序的 n 个元素分成各包含 n/2 个元素的子序列
- 解决:使用归并排序递归排序两个子序列
- 合并:合并两个已经排序的子序列以产生已排序的答案
4.1.2 图片解释
- 分治算法原理
- 归并排序图片解释
4.2 案例
- 有一个数列[33, 10, 49, 78, 57, 96, 66, 21]
- 使用合并排序法对其进行递增排列
list_4 = [33, 10, 49, 78, 57, 96, 66, 21]
def func_1(list_disordered):
result = [] # 存储排好序后的数列
length = len(list_disordered) // 2 # 将数列对半分(奇数则为前少后多)
left = list_disordered[:length]
right = list_disordered[length:]
if length > 2: # 若没到最小单元,则递归计算
left = func_1(left)
right = func_1(right)
if len(left) == 2 and left[0] > left[1] : # 对最小单元中的数列进行排序:left数列有可能只有1个元素;
left[0], left[1] = left[1], left[0]
if len(right) == 2 and right[0] > right[1]:
right[0], right[1] = right[1], right[0]
# 先判断是否为空,再判断大小:避免数列中只有一个数时出错
while True: # 将排好序的最小单元按照元素的大小放至result中进行返回
if len(left) == 0: # 出现空列表时,代表一个数列已经排序完,只需将另一个最小单元直接加进去
result.extend(right)
break
elif len(right) == 0:
result.extend(left)
break
if left[0] < right[0]: # 比较第一个数,将小的有限放在result中
result.append(left.pop(0))
else:
result.append(right.pop(0))
return result # 返回result:排好序的列表
print(f'Before:{list_4}')
result = func_1(list_4)
print(f'After: {result}')
Before:[33, 10, 49, 78, 57, 96, 66, 21]
After: [10, 21, 33, 49, 57, 66, 78, 96]
4.3 实例:争夺十二生肖(分治排序算法)
- 先到的动物先成为12生肖;
- 越早来的动物排名越靠前:6.25代表6:25到达
- 使用分治排序法对其进行排序
time = [6.01, 7.55, 7.30, 6.55, 6.25, 6.13, 8.15, 8.30, 6.00, 7.15, 7.00, 7.20]
animals = ['牛', '鸡', '猴', '龙', '兔', '虎', '狗', '猪', '鼠', '马', '蛇', '羊']
def merge_sort(disordered_list, name_list):
result_time = []
result_name = []
length = len(disordered_list) // 2
# 切分
left = disordered_list[:length]
right = disordered_list[length:]
left_name = name_list[:length]
right_name = name_list[length:]
# 递归
if length > 2:
left, left_name = merge_sort(left, left_name)
right, right_name = merge_sort(right, right_name)
# 排序
if len(left) == 2 and left[0] > left[1]:
left[0], left[1] = left[1], left[0]
left_name[0], left_name[1] = left_name[1], left_name[0]
if len(right) == 2 and right[0] > right[1]:
right[0], right[1] = right[1], right[0]
right_name[0], right_name[1] = right_name[1], right_name[0]
# 整合
while True:
if left[0] < right[0]:
result_time.append(left.pop(0))
result_name.append(left_name.pop(0))
else:
result_time.append(right.pop(0))
result_name.append(right_name.pop(0))
if len(left) == 0:
result_time.extend(right)
result_name.extend(right_name)
break
elif len(right) == 0:
result_time.extend(left)
result_name.extend(left_name)
break
return result_time, result_name
result_time, result_name = merge_sort(time, animals)
print(result_time)
print(result_name)
[6.0, 6.01, 6.13, 6.25, 6.55, 7.0, 7.15, 7.2, 7.3, 7.55, 8.15, 8.3]
['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪']
五. 希尔排序算法
5.1 定义
-
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
-
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
-
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
5.1.1 算法步骤
-
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
-
按增量序列个数 k,对序列进行 k 趟排序;
-
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
5.1.2 算法图解
5.2 案例
- 有一个数列[33, 10, 49, 78, 57, 96, 66, 21]
- 使用希尔排序算法对其进行递增排列
list_5 = [33, 10, 49, 78, 57, 96, 66, 21]
def sorting_shell(li):
length = len(li) // 2 # 获取当前的列表长度,并选择1/2作为最大的数组数量
for i in range(length, 0, -1): # 依次获取要分的数组个数,从大到小
for j in range(i): # 分成i个数组,就要对i个数列进行排序
while j + i <= len(li) - 1: # 当加上一定的步长后,不可以越界
if li[j] > li[j + i]: # 只进行相邻两元素的比较
li[j], li[j + i] = li[j + i], li[j]
j += i # 步长的变换
return li
print(f'Before:{list_5}')
sorting_shell(list_5)
print(f'After: {list_5}')
Before:[33, 10, 49, 78, 57, 96, 66, 21]
After: [10, 21, 33, 49, 57, 66, 78, 96]
5.3 实例:新闻头条热度
- 每条新闻都对应一个点击次数
- 按照点击次数的高低,使用希尔排序算法对新闻进行降序排序
data = {'宝藏湖北有多美':406,'建议高铁票改签允许两次':470,'珠峰高程测量登山队登顶成功':421}
def sorting_shell_ex(dict_data):
print(dict_data)
length = len(dict_data) // 2
for i in range(length, 0, -1):
for j in range(i):
while j + i <= len(dict_data) - 1:
if dict_data[j + i][1] > dict_data[j][1]:
dict_data[j + i], dict_data[j] = dict_data[j], dict_data[j + i]
j += i
return dict_data
data = {'宝藏湖北有多美':406,'建议高铁票改签允许两次':470,'珠峰高程测量登山队登顶成功':421}
result = sorting_shell_ex(list(data.items()))
for i in result:
print(i)
[('宝藏湖北有多美', 406), ('建议高铁票改签允许两次', 470), ('珠峰高程测量登山队登顶成功', 421)]
('建议高铁票改签允许两次', 470)
('珠峰高程测量登山队登顶成功', 421)
('宝藏湖北有多美', 406)
六. 快速排序算法
6.1 定义
- 快速排序又是一种分治思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法
- 快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的
6.1.1 算法步骤
-
从数列中挑出一个元素,称为 “基准”(pivot);
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
6.1.2 算法图解
6.2 案例
- 有一组数列[43, 19, 47, 11, 47, 33, 39, 18, 37, 5, 1, 41, 49]
- 使用快速排列算法将其按照升序进行排列
list_6 = [43, 19, 47, 11, 33, 39, 18, 37, 5, 1, 41, 49]
def quick_sorting(li):
flag_left, flag_right = 0, 0 # 左半部分和右半部分数据的锁定索引
result = [] # 返回结果列表
length = len(li)
if length == 0: # 传入的列表为0时,直接返回空列表
return result
while True: # 将虚拟中间值放至合适位置
for i in range(1, length): # 从前往后遍历,锁定左侧>虚拟中间值的索引
if li[i] > li[0]:
flag_left = i
break
for i in range(length - 1, -1, -1): # 从后往前遍历,锁定右侧<虚拟中间值的索引
if li[i] < li[0]:
flag_right = i
break
if flag_left < flag_right: # 如果左侧的索引大于右侧,则两个列表值互换(保证左边小,右边大)
li[flag_right], li[flag_left] = li[flag_left], li[flag_right]
elif flag_left >= flag_right: # 如果左侧的索引小于右侧,则说明已经将所有小于虚拟中间值的值移到了左边,这时将虚拟中间值放在中间,循环结束
li[flag_right], li[0] = li[0], li[flag_right]
break
if length > 2: # 列表长度大于2,可以使用递归继续迭代,把最小单位的数列排好序
left = quick_sorting(li[0:flag_left])
right = quick_sorting(li[flag_left:])
result.extend(left) # 左侧值永远相对右侧值最小,故从左到右依次加入result数列
result.extend(right)
elif length == 2: # 列表中有2个值,则简单排好序后加入result数列
if li[0] > li[1]:
li[0], li[1] = li[1], li[0]
result.extend(li)
else: # 只有1个值时直接加入result数列
result.extend(li)
return result
print(f'Before:', list_6)
result = quick_sorting(list_6)
print(f'After: ', result)
Before: [43, 19, 47, 11, 33, 39, 18, 37, 5, 1, 41, 49]
After: [1, 5, 11, 18, 19, 33, 37, 39, 41, 43, 47, 49]
6.3 实例:入职年限
- 某公司的6名员工的入职年限分别是[1, 3, 15, 20, 5, 4]
- 使用快速排序算法给这些员工年限从高到低进行排序
data = [1, 3, 15, 20, 5, 4]
def func_6(li):
result = []
if len(li) == 0:
return result
if len(li) > 2:
left_li = [i for i in li[1::] if i >= li[0]] # 用更直接的方式选取了比虚拟中间值大/小的数据
right_li = [i for i in li[1::] if i < li[0]] # 用更直接的方式选取了比虚拟中间值大/小的数据
left_li.append(li[0])
left = func_6(left_li)
right = func_6(right_li)
result.extend(left)
result.extend(right)
elif len(li) == 2:
if li[0] < li[1]:
li[0], li[1] = li[1], li[0]
result.extend(li)
else:
result.extend(li)
return result
re = func_6(data)
print(re)
[20, 15, 5, 4, 3, 1]
七. 堆排序算法
7.1 定义
-
堆是一个近似完全二叉树的结构,同时满足堆积的性质,即子结点的键值或索引总是小于/大于它的父结点。从这句话来看,堆必须满足以下两个条件:
- 是一个完全二叉树。
- 子结点的键值或索引总是小于(或者大于)它的父结点。
-
首先来介绍一下什么是完全二叉树,完全二叉树的每个结点都只有两个叉,从上到下、从左到右依次生成。如图1 所示就是一个完全二叉树。其中,a是b和c的父结点,b是d和e的父结点。反过来说,b和c是a的子结点,d和e是b的子结点。如果要为其添加一个结点,添加到q的位置就是错的,不满足完全二叉树的特点:只有添加w的位置才满足完全二叉树的条件,如图2 所示。
-
接下来看堆的第二个条件:子结点的键值或索引总是小于(或者大于)它的父结点。例如,有如图3 这样一棵完全二叉树。从图中可知,父结点10比子结点5、8大,父结点5比子结点3、4大,父结点8比子结点6大。因此,它不但是一个完全二叉树,还满足子结点小于父结点的要求,这样的结构就称为堆。
-
堆结构中,我们可以通过公式确定某个结点的父结点和子结点的位置。假设该结点的位置为,则其父结点位置(i-1)/2,左子结点位置=2i+1,右子结点位置=2i+2。下面来验证一下,为图4.58中的堆结构编号,如图4 所示。
-
递增排序:每个结点的值都大于或等于其子结点的值。
-
递减排序:每个结点的值都小于或等于其子结点的值。
7.1.1 算法步骤
-
创建一个堆 H[0……n-1];
-
把堆首(最大值)和堆尾互换;
-
把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
-
重复步骤 2,直到堆的尺寸为 1。
7.1.2 算法图解
7.2 案例
- 现有一数组:[91, 60, 96, 13, 35, 65, 46, 65, 10, 30, 20, 31, 77, 81, 22]
- 使用堆排序将其进行降序排序
list_7 = [91, 60, 96, 13, 35, 65, 46, 65, 10, 30, 20, 31, 77, 81, 22]
def heap_sorting(li):
result = []
flag = len(li)-1
while flag >= 1:
for i in range(flag, -1, -1):
if li[i] > li[(i-1)//2] and i != 0:
li[i], li[(i-1)//2] = li[(i-1)//2], li[i]
elif i == 0:
result.append(li[0])
li[0], li[flag] = li[flag], li[0]
flag -= 1
return result
result = heap_sorting(list_7)
print(result)
[96, 91, 81, 77, 65, 65, 60, 46, 35, 31, 30, 22, 20, 13]
八. 计数排序算法
8.1 定义
- 计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
- 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
- 基本思想:将待排序数据值转换为键,存储在额外开辟的数组空间中。计数排序要求输入的数据必须是有确定范围的整数,因此计数排序法适用于量大、范围小的数据,如员工入职年限问题、年龄问题、高考排名问题等。
8.1.1 算法步骤
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
8.1.2 算法图解
8.2 案例
- 现有一数组:[1, 2, 4, 1, 3, 5, 2, 2, 7, 3, 4]
- 使用计数排序将其进行升序排序
list_8 = [1, 2, 4, 1, 3, 5, 2, 2, 7, 3, 4]
def counting_sorting(li):
result = [] # 最终返回的数列
min_, max_ = li[0], li[0] # 初始化最大值和最小值
for i in range(len(li)): # 找到最大值和最小值
if li[i] < min_:
min_ = li[i]
if li[i] > max_:
max_ = li[i]
key_list = [i for i in range(min_, max_ + 1)] # 创建键列表
count_list = [0 for i in range(len(key_list))] # 计数列表
for one in li: # 计数操作:键出现一次,计数一次
for i in range(len(key_list)):
if one == key_list[i]:
count_list[i] += 1
for i in range(len(key_list)): # 根据每个键出现的次数,一次往结果列表里加入
for j in range(count_list[i]):
result.append(key_list[i])
return result
print(f'Before:{list_8}')
re = counting_sorting(list_8)
print(f'After: {re}')
Before:[1, 2, 4, 1, 3, 5, 2, 2, 7, 3, 4]
After: [1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 7]
九. 基数排序算法
9.1 定义
- 基数排序法和计数排序法一样,都是非交换排序算法。
- 主要思想:设置若干个桶,将关键字为k的记录放入第k个桶,然后按序号将非空的数据连接。关键字k就是将每个数据按个位、十位、百位……进行分割而产生的。
- 基数排序不仅可以应用于数字之间的排序,还可以应用于字符串排序(按26个字母顺序)等
9.1.1 算法步骤
- 其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
9.1.2 算法图解
9.2 案例
- 有一数列[410, 265, 52, 530, 116, 789, 110]
- 用基数排序法按升序排列
list_9 = [410, 265, 52, 530, 116, 789, 110]
def radix_sorting(li):
max_, standard = 0, 0
for i in range(len(li)): # 寻找最大宽度的数字,并以此确定后期循环次数
flag = 0
while True:
result = li[i] * (10 ** flag)
if result - int(result) > 1e-6:
flag += 1
else:
if flag >= max_:
max_ = flag
if len(str(li[i]).replace('.', '')) > standard:
standard = li[i]
break
flag_ = len(str(int(standard * 10 ** max_))) # 确定最宽数值
for f in range(0, flag_):
data_bucket = [[] for i in range(10)]
for i in range(len(li)):
num = int(li[i] * 10 ** (max_ - f)) % 10 # 依次获取最小位的数字
data_bucket[num].append(li[i])
li = []
for one_bucket in data_bucket: # 将桶中的数字拿出
li.extend(one_bucket)
return li
print(f'Before:{list_9}')
re = radix_sorting(list_9)
print(f'After: {re}')
Before:[410, 265, 52, 530, 116, 789, 110]
After: [52, 110, 116, 265, 410, 530, 789]