冒泡排序
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。
比如对一组数据 4,5,6,3,2,1从小到到大进行排序的,第一趟冒泡过程是:
经过一次冒泡操作之后,6 这个元素已经存储在正确的位置上。要想完成所有数据的排序,只要进行 6 次这样的冒泡操作就行了:
泡排序算法的原理比较容易理解,具体的代码看下面,可以结合着代码来看前面讲的原理。
def bubble_sort(arr):
"""冒泡排序,arr是列表"""
n = len(arr)
if n <= 1:
return
for i in range(n):
# 提前退出标志位
flag = False
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j] # 交换
flag = True # 此次冒泡有数据交换
if not flag:
break
if __name__ == "__main__":
nums = [4, 1, 33, 7, 5, 23, 56, 11, 10]
print("排序之前:", nums)
bubble_sort(nums)
print("排序之后:", nums)
运行效果:
排序之前: [4, 1, 33, 7, 5, 23, 56, 11, 10]
排序之后: [1, 4, 5, 7, 10, 11, 23, 33, 56]
插入排序(Insertion Sort)
一个有序的数组,只要遍历数组找到数据应该插入的位置将其插入,就能继续保持数组有序。
可以通过这种方法保持集合中的数据一直有序,插入排序算法就是采用这种思想来进行排序的:
将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
如下图,要排序的数据是 4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间
def insertion_sort(arr):
for i in range(1, len(arr)):
value = arr[i] # 待插入元素,arr[0:i-1]作为有序序列
j = i - 1 # j指向有序序列的末端
while j >= 0 and arr[j] > value:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = value # 插入数据
if __name__ == "__main__":
nums = [4, 1, 33, 7, 5, 23, 56, 11, 10]
print("排序之前:", nums)
insertion_sort(nums)
print("排序之后:", nums)
运行效果:
排序之前: [4, 1, 33, 7, 5, 23, 56, 11, 10]
排序之后: [1, 4, 5, 7, 10, 11, 23, 33, 56]
选择排序(Selection Sort)
选择排序算法也分已排序区间和未排序区间,每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
代码实现:
def selection_sort(arr):
for i in range(len(arr) - 1):
min_temp = i
for j in range(i + 1, len(arr)):
if arr[min_temp] > arr[j]:
min_temp = j
arr[i], arr[min_temp] = arr[min_temp], arr[i]
return arr
if __name__ == "__main__":
nums = [4, 1, 33, 7, 5, 23, 56, 11, 10]
print("排序之前:", nums)
selection_sort(nums)
print("排序之后:", nums)
运行效果:
排序之前: [4, 1, 33, 7, 5, 23, 56, 11, 10]
排序之后: [1, 4, 5, 7, 10, 11, 23, 33, 56]
希尔排序
希尔排序(以发明者的名字命名),是 插入排序 的一种更高效的改进版本。
实际思路:
先拆分成N个组的子数组
对每个子数组进行插入排序,让子数组有序
再拆分成 N 个子数组
对每个子数组进行插入排序,让子数组有序
.... 直到无法再拆分为止。
拆分原理:
第一次的跨度:用数组长度/2 第二次的跨度:上一次的跨度/2 直到跨度为0结束
前提:需要先对插入排序有较深的理解。
实现的思路:
代码实现:
def shell_sort(arr):
incr = len(arr) // 2
while incr > 0:
for i in range(incr, len(arr), incr):
val = arr[i] # 待插入元素,arr[0:i:incr]作为有序序列
j = i - incr # j指向有序序列的末端
while j >= 0 and val < arr[j]:
arr[j + incr] = arr[j]
j -= incr
arr[j + incr] = val
incr //= 2
if __name__ == "__main__":
nums = [4, 5, 6, 3, 2, 1]
print("排序之前:", nums)
shell_sort(nums)
print("排序之后:", nums)
运行效果:
排序之前: [4, 5, 6, 3, 2, 1]
排序之后: [1, 2, 3, 4, 5, 6]
归并排序
归并排序(Merge Sort)的核心思想,如果要排序一个数组,先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了
归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。
分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。
代码实现:
def merge(a, b):
temp_list = list() # 用来存放合并之后的数据
left = right = 0
while left < len(a) and right < len(b):
if a[left] < b[right]:
temp_list.append(a[left])
left += 1
else:
temp_list.append(b[right])
right += 1
if left == len(a):
for i in b[right:]:
temp_list.append(i)
else:
for i in a[left:]:
temp_list.append(i)
return temp_list
def merge_sort(lists):
if len(lists) <= 1:
return lists
middle = len(lists) // 2
left = merge_sort(lists[:middle]) # 左半部
right = merge_sort(lists[middle:]) # 右半部
return merge(left, right)
if __name__ == "__main__":
nums = [4, 5, 6, 3, 2, 1]
print("排序之前:", nums)
ret = merge_sort(nums)
print("排序之后:", ret)
运行效果:
排序之前: [4, 5, 6, 3, 2, 1]
排序之后: [1, 2, 3, 4, 5, 6]
快速排序
速排序算法(Quicksort),简称为“快排”。
快速排序的原理:
快速排序的核心思想是(如下图)
1.先确定一个基准数,让后按照比较规则,如本例是升序排列,则将比基数大的放到右边,比基数小的放到左边。
2.接下来各边重复步骤1,直到全部排序完毕。
代码实现
def partition(arr, left, right):
pivot_key = arr[left]
while left < right:
# 从右侧开始找一个小于 pivot_key的数据
while left < right and arr[right] >= pivot_key:
right -= 1
arr[left] = arr[right] # 将找到的右侧小于pivot_key的值,放入到左侧
# 从左侧开始找一个大于 pivot_key的数据
while left < right and arr[left] <= pivot_key:
left += 1
arr[right] = arr[left] # 将找到的数据放入到右侧
arr[left] = pivot_key # 将pivot_key数据放到中间位置
return left
def quick_sort(arr, left, right):
"""快速排序函数"""
# arr[] --> 排序数组
# left --> 起始索引
# right --> 结束索引
if left < right:
pi = partition(arr, left, right)
quick_sort(arr, left, pi - 1)
quick_sort(arr, pi + 1, right)
if __name__ == "__main__":
nums = [7, 10, 8, 9, 1, 5]
n = len(nums)
print("排序前的数组:", nums)
quick_sort(nums, 0, n - 1)
print("排序后的数组:", nums)
运行效果:
排序前的数组: [7, 10, 8, 9, 1, 5]
排序后的数组: [1, 5, 7, 8, 9, 10]
归并排序的处理过程是由下到上的,先处理子问题,然后再合并。快速排序的处理过程是由上到下的,先分区,然后再处理子问题。
桶排序(Bucket sort)
桶排序核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了
桶排序要求要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。其次要求数据在各个桶之间的分布是比较均匀的。
代码实现:
import math
# m表示桶的个数
def bucket_sort(arr, m):
if len(arr) < 2:
return
# 扫描最小值和最大值
min_temp, max_temp = arr[0], arr[0]
for i in range(0, len(arr)):
if arr[i] < min_temp:
min_temp = arr[i]
elif arr[i] > max_temp:
max_temp = arr[i]
bucket_size = math.ceil((max_temp - min_temp + 1) / m)
buckets = []
for i in range(m):
buckets.append([])
# 将数组中值分配到各个桶里
for data in arr:
bucket_index = (data - min_temp) // bucket_size
buckets[bucket_index].append(data)
# 对每个桶进行排序,时间复杂度小于O(nlogn)的排序算法都可以
k = 0
for bucket in buckets:
bucket.sort()
for data in bucket:
arr[k] = data
k += 1
if __name__ == "__main__":
nums = [7, 10, 8, 9, 1, 5]
print("排序前:", nums)
bucket_sort(nums, 4)
print("排序前:", nums)
桶排序应用
桶排序比较适合用在外部排序(数据存储在外部磁盘中)中:
比如有 10GB 的订单数据,希望按订单金额(假设金额都是正整数)进行排序,但是内存只有几百 MB。
可以先扫描一遍文件,分析订单金额所处的数据范围。假设经过扫描之后订单金额最小是 1 元,最大是 10 万元。将所有订单根据金额划分到 100 个桶里,第一个桶存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。
理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,就可以将这 100 个小文件依次放到内存中,用快排来排序。等所有文件都排好序之后,只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。
但订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 ,有可能某个金额区间的数据特别多,针对这些划分之后还是比较大的文件,可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。