常见的经典算法的实现
首先来一张图,这张图总结了常见的几种排序算法的时间复杂度和空间复杂度的对比。现对常见的几种算法进行实现,以备后需。图片来源:专知
1. 快速排序
- 思路
先寻找一个基准数,然后接下里的目的是要寻找一个位置,将这个基准数移动至该位置,要使得比该基准数小的所有数位于该基准的左侧,比该基准数大的所有数位于基准的右侧。该位置将整个要排序的数字划分为两段,然后分别对两段进行递归。 - 时间复杂度:平均复杂度O(nlog(n)), 最好O(nlog(n)), 最坏O(n^2), 不稳定
def QuickSort(self, l, left, right):
"""
:param left: 选中的基准位置
:param right: 列表的最后位置
"""
if len(l) == 0:
return
if left < right:
base = l[left] # 基准
i = left
j = right
# 1. 首先确定基准的位置
while i != j: # 当指针i和j不相遇的时候
while l[j] >= base and i < j:
j -= 1 # 首先从右向左遍历,寻找第一个比base小的数
while l[i] <= base and i < j:
i += 1 # 当j指针停止遍历时,i指针开始从左向右遍历,寻找第一个比base大的数
if i < j: # i和j 都停止遍历的时候,交换
tmp = l[i]
l[i] = l[j]
l[j] = tmp
l[left] = l[i] # i=j时,找到基准该呆的位置
l[i] = base
print(l)
# 2.基准将原始列表划分两部分,分别对两部分进行递归
self.QuickSort(l, left, i - 1)
self.QuickSort(l, i + 1, right)
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用快速排序的结果如下:
[3, 1, 2, 5, 4, 6, 9, 7, 10, 8]
[2, 1, 3, 5, 4, 6, 9, 7, 10, 8]
[1, 2, 3, 5, 4, 6, 9, 7, 10, 8]
[1, 2, 3, 4, 5, 6, 9, 7, 10, 8]
[1, 2, 3, 4, 5, 6, 8, 7, 9, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2. 归并排序
思路
归并排序的过程可以由下图一张图来说明。图片来源:dreamcatcher-cx
归并排序主要分为两个阶段:分和治。在分阶段,就是采用二分的形式,依次将数组划分为两段,直到每段的长度为1;治阶段就是讲划分的子数组两两合并,再合并的时候进行排序。
- 时间复杂度:平均、最好、最坏的时间复杂度都是O(nlog(n))。稳定。
def MergeSort(self, l):
"""
思路:先将数组依次2等分, 然后再合并
"""
left = 0
right = len(l) - 1
tmp = ['#']*len(l) # 辅助数组, 用特殊字符来初始化
self.sort(l, left, right, tmp)
# 分阶段:递归
def sort(self, l, left, right, tmp):
if left < right:
mid = (left + right) // 2
self.sort(l, left, mid, tmp) # 划分左边
self.sort(l, mid + 1, right, tmp) # 划分右边
self.merge(l, left, mid, right, tmp) # 合并两个子数组
# 治阶段:合并
def merge(self, l, left, mid, right, tmp):
i = left
j = mid + 1
t = 0 # 这里的临时指针的作用就是在每次递归合并的时候,重新指向辅助数组的0位置,防止数组保留每次递归的所有值
while i <= mid and j <= right:
if l[i] <= l[j]:
tmp[t] = l[i]
t += 1
i += 1
else:
tmp[t] = l[j]
t += 1
j += 1
while i <= mid: # 将左子数组剩下的直接插入
tmp[t] = l[i]
t += 1
i += 1
while j <= right: # 将右子数组剩下的直接插入
tmp[t] = l[j]
t += 1
j += 1
# 将已经合并好的复制到原始的数组中
t = 0
while left <= right:
l[left] = tmp[t]
t += 1
left += 1
print(l)
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用归并排序的结果如下:
[1, 6, 2, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
[1, 2, 6, 7, 9, 3, 4, 5, 8, 10]
[1, 2, 6, 7, 9, 3, 4, 5, 8, 10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3. 堆排序
- 思路
堆分最小堆和最大堆,最小堆的根节点是所有数中最小的,且每个父节点的值都小于其子节点的值;最大堆的根节点是所有数中最大的,且每个父节点的值都大于其子节点的值。堆排序的重点就是如何调整堆使其满足最小堆或者最大堆的要求。对于最大堆来说,对于当前节点,如果子节点的值比该节点大,那么就将子节点中的最大值和该节点交换,这样让最大的值慢慢向上浮,直到root节点。
堆的示意图如下。图片来源:dreamcatcher-cx
- 时间复杂度:平均、最好、最好时间复杂度都是O(nlog(n))
def HeapSort(self, l):
if not l:
return
for i in range(len(l) - 1, 0, -1):
print('第%s趟:%s' % (len(l) - i - 1, l[::-1]))
self.FitMaxHeap(l, 0, i)
l[i], l[0] = l[0], l[i]
# 采用非递归的方式来实现,并且是基于最大堆
def FitMaxHeap(self, arr, i, n):
j = 2 * i + 1 # 左孩子
curr = arr[i]
while j < n:
if j + 1 < n and arr[j + 1] < arr[j]:
j += 1
if curr <= arr[j]: # 没必要交换
break
else:
arr[i] = arr[j] # 交换
i = j
j = 2 * i + 1
arr[i] = curr
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用堆排序的结果如下:
[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
第1趟:[1, 10, 5, 4, 3, 9, 7, 2, 6, 8]
第2趟:[1, 2, 5, 4, 8, 9, 7, 3, 6, 10]
第3趟:[1, 2, 3, 10, 8, 9, 7, 4, 6, 5]
第4趟:[1, 2, 3, 4, 8, 9, 7, 5, 6, 10]
第5趟:[1, 2, 3, 4, 5, 9, 7, 10, 6, 8]
第6趟:[1, 2, 3, 4, 5, 6, 8, 10, 7, 9]
第7趟:[1, 2, 3, 4, 5, 6, 7, 10, 9, 8]
第8趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
4. 插入排序
- 思路
刚开始时以第1个元素作为已经排好序的,然后依次将后面的元素看做待插入的元素,依次移动到已排序的合适的位置进行插入;或者将待插入的元素与排好序的每个元素比较并交换,知道找到合适的位置插入。
下图展示了插入排序的思路。图片来源:带鱼兄
- 时间复杂度:平均复杂度O(n^2), 最好O(n), 最坏O(n^2),稳定
def InsertSort(self, l):
n = len(l)
print('第1趟:%s' % l)
for i in range(1, n):
tmp = l[i]
j = i
while tmp < l[j - 1] and j > 0: # [i, 0]逆序
l[j] = l[j - 1] # 依次后移
j -= 1
l[j] = tmp # 插入
print('第%s趟:%s' % (i + 1, l))
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用插入排序的结果如下:
[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
第1趟:[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
第2趟:[1, 6, 2, 7, 9, 3, 4, 5, 10, 8]
第3趟:[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
第4趟:[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
第5趟:[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
第6趟:[1, 2, 3, 6, 7, 9, 4, 5, 10, 8]
第7趟:[1, 2, 3, 4, 6, 7, 9, 5, 10, 8]
第8趟:[1, 2, 3, 4, 5, 6, 7, 9, 10, 8]
第9趟:[1, 2, 3, 4, 5, 6, 7, 9, 10, 8]
第10趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
5. 希尔排序
思路
希尔排序的思路是通过设置一个增量gap,然后基于数组元素的下表将元素分成很多小的组,然后在每个组内使用最简单的插入排序进行排序;紧接着设置gap = gap / 2,继续分组和排序,直到最后的gap=1。
下图展示了一个gap的排序过程,图片来源:skywang12345
- 时间复杂度:平均O(nlog(n)^2), 最好O(nlog(n)), 最坏O(nlog(n)^2)
# 基于直接交换的插入排序
def ShellSort(self, l):
gap = len(l) // 2
while gap > 0: # 对于每个增量
for i in range(gap, len(l)): # 按照元素下表增量划分成组
j = i
# 直接交换法
while j - gap >= 0 and l[j] < l[j - gap]: # 每一组内进行插入排序
l[j - gap], l[j] = l[j], l[j - gap]
j -= gap
print(gap, l)
gap //= 2
# 基于移动的插入排序
def ShellSort2(self, l):
gap = len(l) // 2
while gap > 0:
for i in range(gap, len(l)):
j = i
tmp = l[i]
# 移动法
while j - gap >= 0 and tmp < l[j - gap]:
l[j] = l[j - gap]
j -= gap
l[j] = tmp
print(l)
gap //= 2
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用希尔排序的结果如下:
[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
gap=5:[3, 1, 2, 7, 8, 6, 4, 5, 10, 9]
gap=2:[2, 1, 3, 5, 4, 6, 8, 7, 10, 9]
gap=1:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
6. 冒泡排序
思路:冒泡排序的思路很简单,就是通过两两比较,然后将最小的或者最大的元素往后移动,每趟会确定一个次小的元素。
下图展示了冒泡的思路。图片来源:郭威gowill
- 时间复杂度:平均的复杂度O(n^2),最好O(nlog(n)), 最坏O(n^2),稳定
def BubbleSort(self, l):
if l is None:
return None
for i in range(len(l)):
print('第%s趟' % (i + 1))
for j in range(len(l) - (i + 1)):
if l[j] > l[j + 1]:
l[j], l[j + 1] = l[j + 1], l[j] # 交换
print(l)
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用冒泡排序的结果如下:
[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
第1趟:[1, 2, 6, 7, 3, 4, 5, 9, 8, 10]
第2趟:[1, 2, 6, 3, 4, 5, 7, 8, 9, 10]
第3趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第4趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第5趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第6趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第7趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第8趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第9趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第10趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
7. 选择排序
- 思路:每一趟,选择数组中的最小的元素,然后依次交换位置,使其到达另一端。然后选择次小的。。。
- 时间复杂度: 平均时间复杂度O(n^2), 最好O(n), 最坏O(n^2),稳定
def SelectSort(self, l):
for i in range(len(l)):
index = i
for j in range(i, len(l)):
if l[j] < l[index]: # 选择最小的元素
index = j
if index != i: # 如果最小值的位置没有发生变化,则交换
l[i], l[index] = l[index], l[i]
print('第%s趟:%s' % (i+1, l))
对l = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
使用选择排序的结果如下:
[6, 1, 2, 7, 9, 3, 4, 5, 10, 8]
第1趟:[1, 6, 2, 7, 9, 3, 4, 5, 10, 8]
第2趟:[1, 2, 6, 7, 9, 3, 4, 5, 10, 8]
第3趟:[1, 2, 3, 7, 9, 6, 4, 5, 10, 8]
第4趟:[1, 2, 3, 4, 9, 6, 7, 5, 10, 8]
第5趟:[1, 2, 3, 4, 5, 6, 7, 9, 10, 8]
第6趟:[1, 2, 3, 4, 5, 6, 7, 9, 10, 8]
第7趟:[1, 2, 3, 4, 5, 6, 7, 9, 10, 8]
第8趟:[1, 2, 3, 4, 5, 6, 7, 8, 10, 9]
第9趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
第10趟:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]