Python 排序算法
插入排序
原理:
- 如果列表有多个元素,从列表的第二个元素开始,让它和前一个元素对比
- 如果这个元素比前一个元素小,就和前一个元素互换位置,并继续和前面的元素对比。如果当前元素比前面的元素大,就不做任何操作,转而处理后面的元素。
譬如:
- 现有列表:
[0, 2, 10, 3, 38, 6]
- 第一次排序,将索引 1 的
2
和前面的元素0
对比。 2 > 0,因此不做任何操作,处理后续的数据: - 处理索引 2 的数据 10,10 大于前一个元素: 2,因此不做处理,执行下一步
- 索引 3 的数据 3 < 10, 因此互换位置后得到:
0, 2, 3, 10, 38, 6
, 3 继续和前面的 2 对比, 3>2,不做处理,处理后续数据: - 索引 4 的数据 38 > 10, 不做处理,处理后续数据:
- 索引 5 的数据 6 < 38, 互换位置:
0, 2, 3, 10, 6, 38
后继续和前面的 10 对比,6 < 10, 继续互换位置:0, 2, 3, 6, 10, 38
, 6 继续和前面的 3 对比,6 > 3,因此不做处理
def insert_sort(ls):
length = len(ls)
for i in range(length):
if i+1 < length: # i+1 代表后一个元素的索引,它不能超出列表的索引范围
while i >= 0 and ls[i + 1] < ls[i]:
"""
i >= 0 为了防止超出索引范围,因为 i 的值会动态变化:i -= 1
ls[i + 1] < ls[i] 是对比前后两个元素的大小,后面的元素较小的话,就互换两者的位置
"""
ls[i], ls[i + 1] = ls[i + 1], ls[i]
i -= 1
return ls
ls = insert_sort([0, 2, 10, 3, 38, 6, 11])
print(ls)
希尔排序
希尔排序是更高级些的插入排序。它的做法是先初始化一个间隔 gap = len(list)//2
,将第 i
个元素和 i+gap
个元素进行对比,如果后者较小就互换位置(这里其实就是插入排序的算法)。之后缩小间隔为 gap = gap//2
,继续进行插入排序... 直到间隔为1(间隔最开始通常取值为列表长度的一半,以后每次递减一半,直到变成 1 为止)
它的原理就是比较相隔较远距离的两个数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。
譬如针对一个长度为 6 的列表,我们设置初始间隔为 3,因此索引为 0, 3
、 1, 4
、2, 5
构成三个分组。针对每个分组的列表,执行插入排序。然后将间隔设置为之前间隔的一半,再次对列表进行分组排序... 直到间隔为 1 .
示例:
- 现有列表:
[0, 29, 1, 2, 9, 3]
,设置初始间隔为列表长度的一半:gap = 3
- 对列表中,每隔
gap
个元素构成一个分组:索引0, 3
、1, 4
、2, 5
三个分组 - 针对每个分组,进行插入排序
- 设置新的
gap = gap//2
,此时 gap = 1,按照这个 gap ,重新分组并对它进行插入排序,这样就完成了最终的排序(gap 为 1)。
代码:
def shellSort(arr):
length = len(arr)
gap = length // 2
while gap:
# 下面是插入排序的代码
for i in range(length):
if i + gap < length:
while i >= 0 and arr[i + gap] < arr[i]: # 普通的插入排序每次是和后一个元素对比,这里是和后面第 gap 个元素对比
arr[i + gap], arr[i] = arr[i], arr[i + gap]
i -= gap
gap = gap // 2
return arr
ls = shellSort([0, 29, 1, 2, 9, 3])
print(ls)
快速排序
对于一个待排序的列表,先选一个基准值,让列表中的元素和这个基准值对比,分成两部分:大于这个值的部分、小于这个值的部分;再分别对这两个部分进行找基准值的操作,最后将这个最小部分、基准值、最大部分合并。
def quicksort(array):
if len(array)<2: # 长度小于2,不用排序
return array
else:
pivot = array[0] # 基准点
small = [i for i in array[1:] if i <= pivot] # 找到小于基准点的部分
big = [i for i in array[1:] if i > pivot] # 大于基准点的部分
return quicksort(small) + [pivot] + quicksort(big) # 合并
print(quicksort([10,2,6,12,39,10]))
冒泡排序
冒泡排序的逻辑很简单:对n个数,每次只比较2个数。1和2比较,谁大谁放在后面,2再和3比较,3和4比较...n-1和n比较,这样到最后,最大的数就被放在了最后一个位置。继续循环,1和2,2和3,n-2和n-1(第一次循环最大数已经放在最后一个位置了,不用再比了)...
def bubble(array):
n = len(array)
for i in range(n): # 第一层循环,只是为了循环次数,每循环一次,只能将一个最大数放到后面
for j in range(n-i-1): # 第二层循环,查找最大数,一直将它挪放到后面
if array[j] > array[j+1]:
array[j],array[j+1] = array[j+1],array[j]
return array
print(bubble([5,2,3,9,1,0]))
选择排序
选择排序和冒泡有些像,冒泡是每次挪动一个数,选择是每次比较一个数。
对于n个数,
loop1:将第一个数和后面全部的数比较,找到最小数,放到第一位
loop2:将第二个数和后面所有的数比较,找到最小数,放到第二位
....
所以,选择排序每次只挪动一次数,而不像冒泡每两个数比较一次就挪动一次。
def selectSort(arr):
for i in range(len(arr)):
min = i # 记录最小值的索引
for j in range(i+1,len(arr)):
if arr[j] < arr[min]:
min = j
if i != min: # 如果当前索引的值不是最小值
arr[i],arr[min] = arr[min],arr[i]
return arr
arr = selectSort([1,5,2,8,9,3])
归并排序
针对一个拥有多个元素的列表,将列表拆分成多个子列表(下文的例子中会拆分成只有一个元素的列表们),然后对这些列表进行合并。
算法步骤:
- 创建一个序列,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
import random
def merge_sort(ls):
"""归并排序。原理是将一个列表分割成多个部分,针对每个部分进行排序,然后再对已经排序完成的各个部分进行合并。
:param ls: 待排序的列表
:return:
"""
def merge(ls1: list, ls2: list):
"""合并两个各自已经排好序的列表
:param ls1: 已经排好序的列表
:param ls2: 已经排好序的列表
:return:
"""
result = []
# 因为两个列表已经排好序了,因此我们可以直接对比两个列表的元素,将较小的放入新列表中。
while ls1 and ls2:
if ls1[0] == ls2[0]:
result.append(ls1.pop(0))
result.append(ls2.pop(0))
elif ls1[0] < ls2[0]:
result.append(ls1.pop(0))
else:
result.append(ls2.pop(0))
# 如果某个列表还有剩余的内容,则直接放到列表的后面(因为它已经排好序了)
if ls1:
result.extend(ls1)
elif ls2:
result.extend(ls2)
return result
length = len(ls)
if length < 2: # 返回的列表只有一个元素。只有一个元素的列表,可以看作是排好序的列表。
return ls
ls1, ls2 = ls[:length//2], ls[length//2:]
return merge(merge_sort(ls1), merge_sort(ls2))
ls = [random.randint(0, 1000) for i in range(20)]
ls = merge_sort(ls)
print(ls)
堆排序
针对列表:arr = [0,9,2,1,7,3,6,5]
我们可以将其元素从上到下、从左往右排列,看作如下结构的堆:
0
/ \
9 2
/ \ / \
1 7 3 6
/
5
堆末尾的几个节点:
5, 6, 3, 7
,它们没有子节点,因此这几个节点可以被称为 叶子节点。公式len(arr) // 2 - 1
可以获取最后一个非叶子节点的索引,即上面例子中节点1
的索引。
我们可以对堆按照某个格式排列,得到两种类型的堆:
大顶堆:每个节点的值,都比其子节点大(常用大顶堆来进行升序排序)
针对大顶堆,它满足:arr[i] >= arr[2i+1] and arr[i] >= arr[2i+2]; arr[2i+1] 是其左子节点,arr[2i+2] 是其右子节点
10
/ \
9 8
/ \ / \
7 6 5 4
小顶堆:每个节点的值,都比其子节点小(常用小顶堆来进行降序排序)
针对小顶堆,它满足:arr[i] <= arr[2i+1] and arr[i] <= arr[2i+2]
1
/ \
2 3
/ \ / \
4 5 6 7
将一个列表按照得到一个大顶堆后,便可以对其进行排序了:
- 将堆的第一个元素和最后一个元素位置互换,这样最大值便放到列表的末尾了。
- 重新对前
len(arr)-1
个元素进行排列,再次将其成为一个大顶堆,将堆的第一个元素和倒数第二个元素进行互换,这样第二大的值便放到了列表的倒数第二个位置。 - 重复上述步骤
import random
def heap_sort(ls):
"""堆排序
:param ls:
:return:
"""
def shift_node(ls, index, length):
"""对 ls 列表 index 索引的节点,分析它的子节点,子节点的索引要 <= length,如果子节点比父节点大,就将最大的子节点和父节点互换位置。
:param ls: 列表
:param index: 要处理的父节点的索引
:return:
"""
left = 2 * index + 1 # 左子节点的索引
right = 2 * index + 2 # 右子节点的索引
bigger = index
if left <= length and ls[bigger] < ls[left]: # 判断左节点的索引是否在合法的范围,并判断两个元素,获取较大值的索引
bigger = left
if right <= length and ls[bigger] < ls[right]:
bigger = right
if bigger != index:
ls[bigger], ls[index] = ls[index], ls[bigger] # 子节点和父节点互换位置
shift_node(ls, bigger, length) # 对子节点递归处理
def make_max_heap(ls):
"""
创建大顶堆(大顶堆适合做升序排序,小顶堆适合降序排序)
:param ls: 待排序的列表
:return:
"""
length = len(ls)
idx = length // 2 - 1 # 这是最后一个非叶子节点的节点索引
for i in range(idx, -1, -1): # 从最后一个非叶子节点开始,依次处理倒数第二、三... 的非叶子节点,如果它们的子节点比父节点大,就将子节点和父节点互换。完成一轮 for 循环后,就得到一个 大顶堆
shift_node(ls, i, length-1)
make_max_heap(ls) # 生成 大顶堆 的结构:列表的第一个元素是列表的最大值
length = len(ls)
for j in range(length - 1, -1, -1):
ls[0], ls[j] = ls[j], ls[0] # 将大顶堆的第一个元素和末尾元素互换,这样就将最大值放到了末尾
shift_node(ls, 0, j - 1) # 重新对 ls 的前 j-1 个元素调整节点,使其也变成一个 大顶堆,然后将大顶堆的第一个元素和列表倒数第二个元素互换
return ls
ls = [random.randint(0, 100) for i in range(20)]
print(heap_sort(ls))