算法 | 平均情况 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) 已排好序 | O(n^2) 逆序 | O(1) | 稳定 |
插入排序 | O(n^2) | O(n) 已排好序 | O(n^2) 逆序 | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) 已排好序 | O(n^2) 逆序 | O(1) | 不稳定 |
快速排序 | O(n log n) | O(n log n) | O(n^2) 已顺序或逆序 | O(log n) 最坏情况 O(n) | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
希尔排序 | O(n log n) | O(n log n) 取决步长 | O(n^2) | O(1) | 不稳定 |
反复扫描序列,在扫描过程中顺次比较两个元素大小,
如果逆序交换位置(如果某一趟冒泡排序中,没有发现一个逆序,则可以直接结束整个排序)
最好情况:序列都是正序的,时间复杂度O(n),比较n-1次 交换0次
最坏的情况:序列完全逆序。时间复杂度是O(n2),空间复杂度O(1)
平均O(n2)
def bubble_sort(data):
length = len(data)
# i表示趟数,每一趟将当前区间[0,i)的最大值放到i位置
for i in range(length, -1, -1):
# 因为要比较j+1,为了当值越界,所以是i-1
# [0,1,2 ... i-1]
# 优化 判断剩余的元素 是否已经排好序
# is_sorted = True
for j in range(i - 1):
# 每次判断相邻两个元素是否逆序,如果逆序就调整
# 此处可以优化,当所有相邻的元素都顺序的时候,就不用再扫描剩下的区间
if data[j] > data[j + 1]:
data[j], data[j + 1] = data[j + 1], data[j]
# 优化 如果交换操作,就需要排序
# is_sorted = False
# 优化 如果剩余的元素已经是顺序的了,就无需再进行扫描
# if is_sorted:
# break
return data
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
从第一个元素开始,该元素可以认为已经被排序
取出下一个元素,在已经排序的元素序列中从后向前扫描
如果该元素(已排序)大于新元素,将该元素移到下一位置
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
将新元素插入到该位置后
重复步骤2~5
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。
该算法可以认为是插入排序的一个变种,称为二分查找插入排序。
最好情况是数据已经有序,O(N)
最坏情况是数据逆序,O(N2)
def insert_sort(data):
length = len(data)
# 从1 开始,默认第一个是有序的
for i in range(1, length):
# i表示 当前要排序的元素的位置
item = data[i]
# index 左边是已经排好序的
index = i
# 在区间[0,index)上找到一个合适的位置把item插进去
# 因为每次是index-1用来比较,所以index>0(而不是index>=0)
while index > 0 and data[index - 1] > item:
# 如果有序序列中的元素比item大,就往后挪一个位置
data[index] = data[index - 1]
# 为了更直观的看,此处应该有个置空的操作
# data[index - 1] = None
# print(data, item)
# index向左移一位,再进行下次比较
index -= 1
# 找到一个位置,index左边小于或等于item
data[index] = item
return data
选择排序过程:
1、首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,
2、然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3、以此类推,直到所有元素均排序完毕。
比较次数n(n-1)/2
时间复杂度O(n2)
def select_sort(data):
length = len(data)
for i in range(length):
# 选择第一个元素为记为最小值,min_index左边是排行顺序的
min_index = i
# 扫描剩下的元素,选取比min_index所指的值小的
for j in range(i + 1, length):
# 迭代找到最小的值的索引,赋给min_index
if data[j] < data[min_index]:
min_index = j
# 找到最小值后,排到顺序的后面
data[i], data[min_index] = data[min_index], data[i]
return data
快速排序是一种划分交换排序
基本思想是:
1.先从数列中取出一个数作为基准数,一般是第一个数。
2.将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第一、二步,直到各区间只有一个数。
最坏情况 序列已经有序,蜕变成冒泡,时间复杂度为O(n2)空间复杂的O(n)
最好情况 每次比较项刚好在中间位置, 时间复杂度O(nlogn)空间复杂度O(logn)
平均复杂度 实在大量样本下使用随机抽样快排计算出来得概率
不稳定
优势 代码简单常数项小
归并排序 时间复杂度O(nlogn) 空间复杂度 O(n) 常数项大
快速排序采用“分而治之、各个击破”的观念,此为原地(In-place)分割版本。
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:
从数列中挑出一个元素,称为“基准”(pivot),
重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分割结束之后,该基准就处于数列的中间位置。这个称为分割(partition)操作。
递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
[5, 2, 8, 5, 9, 3, 5]
基准值选为 5
小于等于基准值:[2, 3]
大于基准值:[8, 9]
基准值 5 有三个重复的元素在数组中,但它们被划分到了不同的部分。
这会导致每次分区只能减少一个元素的规模,从而导致性能降低。
即 基准值左边的值 [2, 3]
基准值 右边的值 [5, 9, 5, 8] 和基准值相同的值是分散再右边的
小于基准值:[2, 3]
大于等于基准值:[5, 5, 8, 9, 5]
将重复的元素集中再一起,能够更好地处理存在重复元素的情况
小于基准值:[2, 3]
等于基准值:[5, 5, 5]
大于基准值:[8, 9]
左指针找到小于基准值的元素,
右指针找到大于基准值的元素,
而中间指针用于处理等于基准值的元素。
遍历过程中,将小于基准值的元素交换到左边,将大于基准值的元素交换到右边,等于基准值的元素则保持在中间
less 指向小于基准值的区域的末尾
more 指向大于基准值的区域的起始
left 用于遍历整个序列
# def swift_1(data, left, right):
# '''
# 选左边为基准数
# '''
# item = data[left]
# index = left + 1
# mid = index
#
# while index <= right:
# # 当left所指的值 小于基准值时,移动到mid左边
# if data[index] < item:
# data[index], data[mid] = data[mid], data[index]
# mid += 1
# index += 1
# data[left], data[mid - 1] = data[mid - 1], data[left]
# return mid - 1
def swift_1(data, left, right):
"""
单路快速排序使用一个基准元素将数组分成两个部分:小于基准的部分和大于基准的部分。
"""
# 选右边值为基准数
item = data[right]
mid = left
while left < right:
# 从左往右遍历,当left所指的值小于基准值时,和mid所指的值交换,
# mid向右移动一位,保持mid左边的值小于基准值
if data[left] < item:
data[left], data[mid] = data[mid], data[left]
mid += 1
left += 1
# 当扫描完成后,将right所指的数放到mid的位置,此时mid左边的数于,右边数大于等于
data[mid], data[right] = data[right], data[mid]
return mid
def swift_2(data, left, right):
# 单路快排 可能会在存在大量重复元素时性能下降
# 二路快排 分别从数组的左端和右端开始,将数组分成两个部分:小于基准、等于或大于基准
# 是通过 左右两个指针互相交换来减少交换操作,可以在存在重复元素的情况下保持较好的性能
# 最左边的选为基准值,拿出来后,此时left的位置是空的
item = data[left]
# 应该有一个置空的操作
# data[left] = None
while left < right:
# 从右往左扫,直到扫到小于基准值的 停下来,
# 扫的时候要满足left < right 比如 [0,1,2,3,4] 这种数据,如果选第一个数为基准值,
# 从右往左扫描的时候,直到第一个数也找不到一个数小于基准值的,
# 不限制的话 right就会继续向左,从而越界,导致错误
while left < right and data[right] > item:
right -= 1
# 如果left和right未相遇,把找到的那个小值,交换到基准值的位置,
# 并将left向右移动,此时 right的位置为空了
if left < right:
data[left] = data[right]
# 应该有一个置空的操作
# data[right] = None
left += 1
# 然后从左往右扫,直到扫到大于或等于基准值的 停下来
# 同理 [4,3,2,1,0] 的情况,如果第一个数为基准,从左往右扫描的时候,
# 直到最后一个数也找不到一个大于等于基准数的值
# 如果不限制的话,left就会继续向右,导致越界
while left < right and data[left] < item:
left += 1
# 如果left和right未相遇,把找到的那个大值,交换到right的位置,并将right向左移动,此时left的位置为空
if left < right:
data[right] = data[left]
# 应该有一个置空的操作
# data[left] = None
right -= 1
# 当left和right相遇的时候,就是要好的mid的位置,即mid左边都是小于基准值,mid右边都是大于等于基准值
# 所以将基准值放到mid的位置
data[left] = item
# 返回调整后基点的位置
return left
def swift_3(data, left, right):
"""
三路快排 将数组分为三个区 小于区 等于区 大于区 每次等于区域拍好之后,不会再参与下次排序
[0, less) 小于 base
[less, left) 等于 base
[left, more] 未处理,所以要遍历的区域
(more, right) 大于base
"""
base = data[right]
less = left # less指向小于区域的下一位
more = right # more指向大于区域的前一位
# 遍历整个未处理的区域
# 小于区域 [0, less) 等于区域[less, left) 未处理[left, more] 大于区域(more, right)
while left <= more:
if data[left] < base:
# 未处理的区域中,当前值 小于基准值,则交换到小于区域 less 和 left向右移动
data[left], data[less] = data[less], data[left]
left += 1
less += 1
elif data[left] > base:
# 未处理的区域中,当前值,大于基准值,则交换的大于区域,more向左移动
data[left], data[more] = data[more], data[left]
more -= 1
else:
# 如果相等,跳过
left += 1
# 返回小于区域与等于区 和 等于区与大于区域的 边界
return less, more
def quick1(alist, left, right):
# 二分数列
if left < right:
# 索引 [0,1,2,3,4,5,6]
# mid=3时,左边的索引范围0-2 右边的索引范围4-6
# 因为每次分隔后,mid的位置就排好了,以后都不会变了,所以不参与以后的排序
# 左闭右开的原则,所以左边的索引范围是 left, mid-1 右边的索引范围是 mid+1, right
mid = swift_1(alist, left, right)
quick1(alist, left, mid - 1)
quick1(alist, mid + 1, right)
return alist
def quick2(alist, left, right):
# 二分数列
if left < right:
# 索引 [0,1,2,3,4,5,6]
# mid=3时,左边的索引范围0-2 右边的索引范围4-6
# 因为每次分隔后,mid的位置就排好了,以后都不会变了,所以不参与以后的排序
# 左闭右开的原则,所以左边的索引范围是 left, mid-1 右边的索引范围是 mid+1, right
mid = swift_2(alist, left, right)
quick2(alist, left, mid - 1)
quick2(alist, mid + 1, right)
return alist
def quick3(alist, left, right):
# 二分数列
if left < right:
# 每次中间的所有元素不参与下次的排序,提高效率
mid = swift_3(alist, left, right)
quick3(alist, left, mid[0] - 1)
quick3(alist, mid[1] + 1, right)
return alist
更python的快排
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[0] # 选择第一个元素作为基准
left = [x for x in arr[1:] if x <= pivot] # 比基准小的部分
right = [x for x in arr[1:] if x > pivot] # 比基准大的部分
return quick_sort(left) + [pivot] + quick_sort(right)
是将两个已经排序的序列合并成一个序列的操作
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
def merge_sort(data):
# 不断递归调用自己一直到拆分成成单个元素的时候就返回这个元素,不再拆分了
if len(data) == 1:
return data
# 取拆分的中间位置
# mid = len(data) // 2
mid = len(data) >> 1
# 拆分过后左右两侧子串
left = data[:mid]
right = data[mid:]
# 对拆分过后的左右再拆分 一直到只有一个元素为止
# 最后一次递归时候ll和lr都会接到一个元素的列表
# 最后一次递归之前的ll和rl会接收到排好序的子序列
ll = merge_sort(left)
rl = merge_sort(right)
# 我们对返回的两个拆分结果进行排序后合并再返回正确顺序的子列表
# 这里我们调用拎一个函数帮助我们按顺序合并ll和lr
return merge(ll, rl)
# 这里接收两个列表
def merge(left, right):
# 从两个有顺序的列表里边依次取数据比较后放入result
# 每次我们分别拿出两个列表中最小的数比较,把较小的放入result
result = []
left_length = len(left)
right_length = len(right)
while left_length > 0 and right_length > 0:
# 为了保持稳定性,当遇到相等的时候优先把左侧的数放进结果列表,因为left本来也是大数列中比较靠左的
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
# while循环出来之后 说明其中一个数组没有数据了,我们把另一个数组添加到结果数组后面
if left:
result += left
if right:
result += right
return result
数据结构中的堆
可以看做是一颗完全的二叉树结构,最后一层的子节点,都在最左边
大根堆 就是所有子节点必须小于等于根节点,用于顺序排序
小根堆 就是所有子节点必须大于等于根节点,用于逆序排序
(01) 索引为i的左孩子的索引是 (2i+1);
(02) 索引为i的左孩子的索引是 (2i+2);
(03) 索引为i的父结点的索引是 floor((i-1)/2);
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build Max Heap):将堆中的所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
# 将序列中索引为index的数据插入到已经建好的堆中
def heap_insert(data, index):
# 根据index计算出当前节点的 父节点
root = int((index - 1) / 2)
while data[index] > data[root]:
# 然后比较当前节点是否大于父节点root,如果比root大,就和root交换
data[index], data[root] = data[root], data[index]
# 交换后,再计算它的父节点,然后继续进行比较,直到不大于它的父节点,或者到达根节点(index=root=0的时候)
index = root
root = int((index - 1) / 2)
# 大根堆中一个数变小后,往下沉
def heapify(data, index, length):
# 调整index(一般是0,因为堆排序中,会将最末尾的位置放置堆顶,
# 然后进行调整,直到找到合适的位置停止)所指元素的位置,再[0, length)区间上
# 调整过程是 从堆顶位置开始,找到左右子节点中比较大的值,如果比堆顶元素大,就和堆顶交换,
# 交换后,再继续找此时的,左右子节点中较大的 再进行比较,然后再替换,
# 直到所处位置的左右子节点的值都小于等于当前节点,或者到达的末尾的位置,再停止
left = index * 2 + 1
while left < length:
right = left + 1
# 比较当前节点的左右子节点,找到最大的那个下标
larger = right if (right < length and data[right] > data[left]) else left
# 比较当前节点和子节点中最大的那个,找到大的那个的下标
larger = larger if data[larger] > data[index] else index
# 如果当前节点和最大的那个节点数相同,则不需要做任何操作
if larger == index:
break
# 当前节点和左右节点的最大的那个交换
data[larger], data[index] = data[index], data[larger]
# 当前节点指向最大那个节点,再继续判断
index = larger
left = index * 2 + 1
def heapsort(data):
size = len(data)
if not data or size < 2:
return data
# 创建大根堆
for i in range(size):
heap_insert(data, i)
# 堆创建完成后,因为是大根堆,所以堆顶的元素是序列中最大的元素
right = len(data) - 1 # 此时堆最末尾的索引
# 然后再调整堆为大根堆
while right > 0:
# 将堆顶的元素,弹出(放到堆末尾,最末尾的叶子节点)
# 将末尾的元素放到堆顶,进行调整
data[0], data[right] = data[right], data[0]
heapify(data, 0, right)
# 缩小末尾位置,继续进行 交换和调整,直到所有的元素都交换并且调整过
right -= 1
return data
缩小增量排序,首先取一个整数gap,将元素分为gap个子序列,所有距离为gap的元素放在一个子序列终,
然后在每个子序列终实现直接插入排序,然后缩小间隔gap,直到gap缩小到1
def shell_sort(data):
length = len(data)
if length <= 1:
return data
# 初始步长
gap = length // 2
while gap > 0:
for i in range(gap, length):
tmp = data[i]
j = i
# 子序列进行插入排序
while j >= gap and tmp < data[j - gap]:
data[j] = data[j - gap]
j -= gap
data[j] = tmp
# 减小步长
gap = gap // 2
return data