1 冒泡排序 Bubble Sort
1.1 算法原理
(1)比较相邻的元素,如果第一个元素比第二个元素大,则将两者交换;
(2)从开始的第一对元素到结尾的最后一对元素,对每一对相邻的元素进行步骤(1)的操作。
(3)针对所有的元素重复(1)(2)步骤,除了最后一个;
(4)持续每次对越来越少的元素重复上面的步骤,直至没有任何一对数字需要比较;
备注:设置一个flag,如果出现一趟排序过程中没有出现交换的操作,则认为整个数组已排好序。
1.2 算法实现
def bubble_sort(nums, reverse=False):
for i in range(len(nums)):
flag = True
for j in range(0, len(nums)-i-1):
if reverse:
if nums[j] < nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
flag = False
else:
if nums[j] > nums[j+1]:
nums[j], nums[j+1] = nums[j+1], nums[j]
flag = False
if flag:
return nums
return nums
1.3 算法复杂度
稳定性:冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法;
空间复杂度:由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1);
时间复杂度:
(1)最坏的情况:每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²);
(2)最佳的情况:内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n);
(3)平均的情况:时间复杂度为O(n²)。
2 选择排序
2.1 算法原理
(1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
(2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
(3)重复(2),直到所有元素均排序完毕。
2.2 算法实现
def selection_sort(nums, reverse=False):
for i in range(len(nums)-1):
# 找出未排序序列中最小值的索引
min_index = i # 记录最小值的索引
for j in range(i+1, len(nums)):
if nums[j] < nums[min_index]:
min_index = j
# 如果当前元素不是最小值,则将当前元素与最小值进行交换
if i != min_index:
nums[i], nums[min_index] = nums[min_index], nums[i]
return nums[::-1] if reverse else nums
2.3 算法复杂度
稳定性:每次从未排序的数中选择最小的数与未排序的第一个数交换,破坏了相对顺序(比如未排序的第一个是3,第二个也是3,第三个是2也是未排序中最小的,此时交换第三个和第一个,会破坏第一个和第二个之间的相对顺序),所以不是稳定的排序;
空间复杂度:由于选择排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1);
时间复杂度:无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍,时间复杂度为O(n²)。
3 插入排序
3.1 算法原理
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入到有序序列的适当位置。
(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
3.2 算法实现
def insert_sort(nums, reverse=False):
for i in range(1, len(nums)):
for j in range(i, 0, -1):
if nums[j] < nums[j-1]:
nums[j], nums[j-1] = nums[j-1], nums[j]
else:
break
return nums[::-1] if reverse else nums
3.3 算法复杂度
稳定性:每次交换不改变其他元素间的相对顺序,因此是稳定的排序算法
空间复杂度:空间复杂度为常量O(1)
时间复杂度:
最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²);
最好的情况是已经排序好,只要遍历一次,不需要交换,时间复杂度为O(n)。
4 希尔排序
4.1 算法原理
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
<1> 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
<2> 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
其算法流程如下:
(1)给定一个增量inc,将整个数组分成N/inc组,对每一组中的元素分别进行插入排序;(这里N为数组的长度)
(2)将当前增量缩减一半,若结果大于0,则进入步骤(3),否则结束循环完成排序;
(3)采用当前增量的值重复步骤(1)、(2)。
4.2 算法实现
def shell_sort(nums, inc=3, reverse=False):
import math
gap = math.floor(len(nums) / inc)
while gap > 0:
groups = math.floor(len(nums) / gap) # 根据增量进行分组
for i in range(0, len(nums), groups):
# 对每一组分别进行插入排序
nums[i: i+groups] = insert_sort(nums[i: i+groups])
# 缩减增量
gap = math.floor(gap / 2)
return nums[::-1] if reverse else nums
4.3 算法复杂度
稳定性:相同的元素会分到不同的子序列,可能会破坏相对顺序,故为不稳定的排序算法
空间复杂度:空间复杂度为常量O(1)(内部是插入排序)
时间复杂度:最坏情况和平均时间复杂度都是O(nlog2 n),最好情况不确定
5 归并排序
5.1 算法原理
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
其算法流程如下:
(1)申请空间,使其大小为两个已经排序序列之和,该空间用于存放合并后的序列;
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置;
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一个位置;
(4)重复步骤(3)知道某一指针达到序列尾部;
(5)将另一序列剩下的所有元素直接复制到合并序列的尾部。
5.2 算法实现
import math
def recursion_merge(left, right):
# 合并子数组
result = []
while left and right:
num = left.pop(0) if left[0] <= right[0] else right.pop(0)
result.append(num)
return result + left[:] + right[:]
# 递归版本
def recursion_merge_sort(nums, reverse=False):
if len(nums) < 2:
return nums
middle = math.floor(len(nums)/2)
# 递归划分数组,对子数组分别进行排序并合并结果
nums = recursion_merge(recursion_merge_sort(nums[:middle]), recursion_merge_sort(nums[middle:]))
return nums[::-1] if reverse else nums
# 非递归版本
def merge_sort(nums, reverse=False):
merge_time, interval = 0, 2
while merge_time < math.ceil(math.log(len(nums), 2)):
for i in range(0, len(nums), interval):
nums[i: i+interval] = recursion_merge(nums[i: i+interval//2], nums[i+interval//2: i+interval])
interval *= 2
merge_time += 1 # 归并次数加1
return nums[::-1] if reverse else nums
5.3 算法复杂度
稳定性:分割和归并的时候都不会改变相对顺序,故是稳定的排序算法
空间复杂度:需要一个辅助数组,空间复杂度是O(n),递归需要保存的空间为O(log2 n),所以加起来仍然是O(n)
时间复杂度:假设数组长度为n,那么拆分数组共需log2 n,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlog2 n)
通过自上而下的递归实现的归并排序, 将存在堆栈溢出的风险。
6 快速排序
6.1 算法原理
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法流程:
(1)从数列中挑出一个元素,称为 “基准”(pivot);
(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
(3)递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
理解:
快排主要包含3步:
(1)第一步是确定好partition的最左边的索引以及最右边的索引;
(2)第二步根据确定好的范围将所有比left索引对应元素小的元素均移至左边,比其大的元素移到右边;【三要素:pivot,index和i:pivot初始值为left,用于记录初始的left位置;index初始值设置为left+1,用于记录比pivot值小(大)的元素索引,每遇到一个满足条件的就将当前元素与arr[index]进行交换,并将index加1更新;i值的初始值为left+1,用于按顺序依次遍历left+1到right之间的元素。遍历结束后,index保存了当前元素比pivot值小的最后一个索引,我们只需将其与pivot值进行交互,就能使左边的元素比pivot值小,右边的元素比pivot值大,最后再返回当前基准线所对应的索引即可】
(3)根据返回的基准索引pivot,将其左右两边按同样的步骤继续划分:左半部分为left:pivot-1;右半部分为pivot+1:right;重复(1)(2)步骤;【对于递归可直接根据索引值反复调用函数;对于非递归写法可使用栈来存储每次操作需要的left和right索引值;需要操作时按顺序依次弹出栈顶顺序即可,只需注意入栈和出栈的先后顺序 】
6.2 算法实现
def partition(nums, left, right):
# pivot记录原始位置,i用于遍历从下一个位置开始到right之间的元素,index记录比pivot值小的元素下标
pivot, index, i = left, left+1, left+1
while i <= right:
# 如果遇到比基准值小的元素,则依次将它们排列到pivot的右边
if nums[i] < nums[pivot]:
nums[i], nums[index] = nums[index], nums[i]
index += 1
i += 1
# 遍历完成后,将pivot值与最后一个比pivot值小元素的下标
# 这样一来pivot左边的值均比pivot小,右边的值均大于等于pivot值
nums[pivot], nums[index-1] = nums[index-1], nums[pivot]
return index - 1
# 递归版本
def recursion_quick_sort(nums, left=None, right=None, reverse=False):
# left和right的初始值为第一个下标和最后一个下标
left = left if left is not None else 0
right = right if right is not None else len(nums)-1
if left < right:
# 找到基准线,即令基线左半部分的元素都比pivot值元素小,基线右半部分元素均比pivot值大
partition_index = partition(nums, left, right)
# 分别对pivot左右两边的数组进行递归,再次进行排序,直至排序完毕
recursion_quick_sort(nums, left, partition_index-1)
recursion_quick_sort(nums, partition_index+1, right)
return nums[::-1] if reverse else nums
# 非递归版本
def quick_sort(nums, reverse=False):
# 将需要操作的左右界限的索引按顺序依次存入到栈中;
# 每次处理一个partition时提出相应的left和right,直至所有的区域均处理完毕。
stack = [len(nums) - 1, 0]
while stack:
# pop函数默认输出最后一个
left = stack.pop(-1)
right = stack.pop(-1)
index = partition(nums, left, right)
if left < index - 1:
stack.append(index - 1)
stack.append(left)
if right > index + 1:
stack.append(right)
stack.append(index + 1)
return nums[::-1] if reverse else nums
6.3 算法复杂度
稳定性:同选择排序相似, 快速排序每次交换的元素都有可能不是相邻的, 因此它有可能打破原来值为相同的元素之间的顺序. 因此, 快速排序并不稳定;
空间复杂度:首先就地快速排序使用的空间是O(1);而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;最优的情况下空间复杂度为O(log2 n),每一次都平分数组;最差的情况下空间复杂度为O( n ),退化为冒泡排序的情况
时间复杂度:平均时间复杂为O(nlog2 n),最糟糕时将达到O(n²)的时间复杂度。
7 堆排序
7.1 算法原理
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
算法步骤:
(1)创建一个堆 H[0……n-1];
(2)把堆首(最大值)和堆尾互换;
(3)输出最后一个元素,并调用 heapify(0),目的是把新的数组顶端数据调整到相应位置;
(4)重复步骤 (2)(3),直到堆的尺寸为 1。
7.2 算法实现
# 构建大根堆
class MaxHeapSort(object):
def __init__(self, nums, reverse=False):
self.nums = nums
self.reverse = reverse
self.length = len(nums)
def build_max_heap(self):
# 从中间节点开始,从下往上调整从左到右调整
for i in range(math.floor(self.length/2)-1, -1, -1):
self.heapify(i)
def heapify(self, i):
# i为当前节点,left, right分别为其左子节点和右子节点
left, right, largest = 2*i+1, 2*i+2, i
# 找出节点中最大值元素的索引
if left < self.length and self.nums[left] > self.nums[largest]:
largest = left
if right < self.length and self.nums[right] > self.nums[largest]:
largest = right
# 将父节点的值更新为最大值
if largest != i:
self.nums[i], self.nums[largest] = self.nums[largest], self.nums[i]
# 如果调整后其子孙节点的值大于其子节点的值则继续自顶向下调整,直至满足堆的条件
self.heapify(largest)
def heap_sort(self):
# 大根堆顺序输出,因为每次都将堆顶元素放置到末尾,小根堆输出逆序输出
self.build_max_heap() # 构建大根堆
# 调整堆,每次都将堆顶元素与最后一个元素交换,交换后调整前面的n-1个元素;
# 重复以上步骤,直至调整到最后一个元素
for i in range(len(self.nums), 0, -1):
self.nums[0], self.nums[i] = self.nums[i], self.nums[0]
self.length -= 1
self.heapify(0) # 调整后堆顶元素即为最大值
return self.nums[::-1] if self.reverse else self.nums
# 小根堆
class MinHeapSort(object):
def __init__(self, nums, reverse=False):
self.nums = nums
self.reverse = reverse
self.length = len(nums)
def heapify(self, i):
left, right, smallest = 2*i+1, 2*i+2, i
if left < self.length and self.nums[left] < self.nums[smallest]:
smallest = left
if right < self.length and self.nums[right] < self.nums[smallest]:
smallest = right
if smallest != i:
self.nums[smallest], self.nums[i] = self.nums[i], self.nums[smallest]
self.heapify(smallest)
def built_min_heap(self):
for i in range(math.floor(self.length/2)-1, -1, -1):
self.heapify(i)
def heap_sort(self):
self.built_min_heap()
for i in range(len(self.nums)-1, 0, -1):
self.nums[0], self.nums[i] = self.nums[i], self.nums[0]
self.length -= 1
self.heapify(0)
return self.nums[::-1] if self.reverse else self.nums
7.3 算法复杂度
稳定性:并不是邻近交换,故为不稳定排序
空间复杂度:不需要辅助数组,空间复杂度为O(1)
时间复杂度:建立堆的过程, 从n/2 一直处理到0, 时间复杂度为O(n);
调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度log2n,一共要执行n次,加起来时间复杂度为O(nlog2 n)
8 计数排序
8.1 算法原理
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法流程:
(1)找出待排序的数组中最大和最小的元素
(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
8.2 算法实现
# 计数排序
def counting_sort(nums, range_value, reverse=False):
# 计数排序的范围为:最大值减去最小值加1
cnt, cache = 0, [0] * range_value
for num in nums:
cache[num] += 1
for i in range(len(cache)):
while cache[i] > 0:
nums[cnt] = i
cache[i] -= 1
cnt += 1
return nums[::-1] if reverse else nums
8.3 算法复杂度
稳定性:是稳定排序
空间复杂度:需要辅助数组,空间复杂度为O(max-min+1)
时间复杂度:当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。
9 桶排序
9.1 算法原理
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
(1)在额外空间充足的情况下,尽量增大桶的数量
(2)使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
9.2 算法实现
# 桶排序
def bucket_sort(nums, bucket_size=5, reverse=False):
if len(nums) == 0 or len(nums) == 1:
return nums
buckets, min_value, max_value = [[] for _ in range(10)], min(nums), max(nums)
bucket_count = math.floor(((max_value - min_value) / bucket_size) + 1) # 计算每个桶存放的容量
# 利用映射函数将数据分配到各个桶中
for num in nums:
buckets[math.floor((num - min_value) / bucket_count)].append(num)
nums.clear()
for i in range(bucket_size):
# 对每个桶内的元素选择合适的排序算法进行排序并按顺序依次存入到数组中
nums += merge_sort(buckets[i])
return nums[::-1] if reverse else nums
10 桶排序
10.1 算法原理
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
10.2 算法实现
# 基数排序
def radix_sort(nums, reverse=False):
# 对于负数可以先找到最小值,将所有的数都加上这个负数,最后处理完之后再重新减去该值
min_value = min(nums)
flag = True if min_value < 0 else False
if flag:
nums = [i+abs(min_value) for i in nums]
times, max_value, buckets = 0, max(nums), [[] for _ in range(10)]
count = len(str(max_value))
pad = count
while count:
# 按基数进行排序
for num in nums:
buckets[int((str(num).zfill(pad)[count-1]))].append(num)
count -= 1
nums.clear()
for i in range(10):
nums += buckets[i]
if count:
buckets = [[] for _ in range(10)]
if flag:
nums = [i+abs(min_value) for i in nums]
return nums[::-1] if reverse else nums