算法-常见10大排序算法
转载-10 大经典排序算法(Python版)
排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:
关于时间复杂度:
- 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
- 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
- O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。希尔排序
- 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性:
排序后 2 个相等键值的顺序和排序之前它们的顺序相同
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
名词解释:
n:数据规模
k:“桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
1、冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
(1) 算法步骤
1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3. 针对所有的元素重复以上的步骤,除了最后一个。
4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
(2) 动图演示
(3) python 代码
def bubbleSort(arr):
# 需要多少次
for i in range(1, len(arr)):
# 内层的比较过程; n 个数字,比较 n-1次;
for j in range(0, len(arr) - i):
if arr[j] > arr[j+1]: # 如果 left > rihgt: 则发生交换
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
def bubbleSort(arr):
# 比较推荐这种写法, 外部是冒泡循环的次数,内部是 冒泡的范围
for i in range(len(arr)): 或者len(arr) -1
# 内层比较: 比较n-1次
for j in range(len(arr)-i-1):
if arr[j] > arr[j+1]: # 稳定性体现
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
(4) 自己总结
1. 冒泡含义: 每次选择最大的数字放在最后一个位置, 选择过程为: 从头开始比较,维持一个 最大尾, 直到一轮结束;
2. 因为两两比较交换过程中 只有 大于时候才交换所以稳定;
3. 平均时间复杂度: O(n^2)
4. 最好情况: O(n) 数组有序: flag未false, 不用做下一次冒泡
5. 最差情况: O(n^2) 数组逆序:需要每次都进行 两两比较, 冒泡
2、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
没有移动过程, 每次遍历也仅仅是交换一次, 冒泡一次遍历可能交换多次(相当于排序)
(1)算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
(2)动图演示
(3)Python 代码
def selectionSort(arr):
for i in range(len(arr) - 1):
# 记录最小数的索引
minIndex = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[minIndex]:
minIndex = j
# i 不是最小数时,将 i 和最小数进行交换
if i != minIndex:
arr[i], arr[minIndex] = arr[minIndex], arr[i]
return arr
def selectionSort(arr):
# 推荐这种写法,外部就是 选择的次数, len(arr), len(arr)-1都可以,
# 内部i的循环会自动终止
for i in range(len(arr)): # or len(arr)-1
# 记录最小数的索引
minIndex = i
for j in range(i+1, len(arr)):
if arr[j] < arr[minIndex]:
minIndex = j
# 一次选择结束, i 不是最小数时候, 将i 和最小数交换
if i != minIndex:
arr[i], arr[minIndex] = arr[minIndex], arr[i]
return arr
arr = [6,2,22,45,1,6,8,200,56,111]
selectionSort(arr)
(4) 自己总结
1. 快速: 每次从列表中选择一个最小 或者 最大 的数字。 一轮进行一次交换;
2. 平均时间复杂度: O(N^2)
3. 最差时间复杂度:O(N^2)
4. 最好时间复杂度:O(N^2)
5. 不稳定
3、插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
(1)算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
(2) 动图演示
插入: 将有序序列后面的元素,依次 插入到前面的有序序列中,插入完成,排序完成;
(3)Python 代码
def insertionSort(arr):
for i in range(len(arr)):
preIndex = i -1 # 记录有序序列末尾
current = arr[i] # 待插入元素
# 当前元素可以插入到 有序序列中;
while preIndex >= 0 and arr[preIndex] > current:
arr[preIndex+1] = arr[preIndex]
preIndex -= 1
# 找到 当前元素应该插入位置并插入
arr[preIndex+1] = current
return arr
(4).自己总结
1. 最坏时间复杂度 : O(N^2)
2. 最好时间复杂度:O(N)
3. 平均时间复杂度:O(N^2)
4. 空间复杂度:O(1)
5. 稳定
4、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
(1)算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
(2)Python 代码
def shellSort(arr):
import math
gap=1
while(gap < len(arr)/3):
gap = gap*3+1
while gap > 0:
for i in range(gap,len(arr)):
temp = arr[i]
j = i-gap
while j >=0 and arr[j] > temp:
arr[j+gap]=arr[j]
j-=gap
arr[j+gap] = temp
gap = math.floor(gap/3)
return arr
5、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)
- 自下而上的迭代
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
希尔排序是插入排序的一种更高效率的实现。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的
作者:夏海峰
链接:https://www.jianshu.com/p/1af509b2be08
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(1)算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
(2)动图演示
(3)Python 代码
class Solution:
def reversePairs(self, nums: List[int]) -> int:
# 归并排序
def merge(left, right):
'''
合并操作: 2 个序列: left、right的合并操作;
'''
result = []
l, r = 0 ,0
while l < len(left) and r < len(right):
# 比较;
if left[l] <= right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
# 左、右分别添加;
result += left[l:]
result += right[r:]
# 返回合并数组;
return result
def mergeSort(arr):
'''
分治操作: 递归进行分治;
'''
# 长度为1, 直接返回;
if len(arr) <= 1:
return arr
mid = len(arr) // 2
# 框架: 左边;
left = mergeSort(arr[:mid])
# 框架:右边;
right = mergeSort(arr[mid:])
# 返回左右合并结果;
return merge(left, right)
res = mergeSort(nums)
return res
分析总结
1. 平均时间复杂度: O(N log N)
2. 最差时间复杂度: O(N log N)
3. 最好时间复杂度: O(N log N)
4. 空间复杂度: O(1)
5. 稳定
6、快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
冒泡: ----> 递归分治: 快速排序
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
(1)算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
(2)动图演示
快速排序(三种算法实现和非递归实现)
- 左右指针法:
- 挖坑法
- 前后指针法
(3).python实现
def quickSort(arr, left=None, right=None):
left = 0 if not isinstance(left,(int, float)) else left
right = len(arr)-1 if not isinstance(right,(int, float)) else right
if left < right:
partitionIndex = partition(arr, left, right)
quickSort(arr, left, partitionIndex-1)
quickSort(arr, partitionIndex+1, right)
return arr
def partition(arr, left, right):
pivot = left
index = pivot+1
i = index
while i <= right:
if arr[i] < arr[pivot]:
swap(arr, i, index)
index+=1
i+=1
swap(arr,pivot,index-1)
return index-1
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def quickSort(lists,left,right):
if left >= right:
return lists
key = lists[left]
low = left
high = right
while left < right:
while left < right and lists[right] >= key:
right -= 1
lists[left] = lists[right]
while left < right and lists[left] <= key:
left += 1
lists[right] = lists[left]
lists[left] = key
quickSort(lists,left+1,high)
quickSort(lists,low,left-1)
return lists
lists = [1,2,3,8,6,4,0,3,5]
print(quickSort(lists,0,len(lists)-1))
自己总结
1. 思想大概懂了,但是具体的实现过程,三种方法未知
7、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
(1)算法步骤 --不整理了,后面没看
常见使用排序题目
164. 最大间距
剑指 Offer 51. 数组中的逆序对
- 归并排序思路: 只改一行;
class Solution:
def reversePairs(self, nums: List[int]) -> int:
# 暴力: O(n^2);
# 归并排序思路: 合并过程中统计 逆序对 数目;
# 归并排序改变一行代码;
# 全局变量cnt:统计 逆序对 数目;
self.cnt = 0
def merge(left, right):
'''
合并操作: 2 个序列: left、right的合并操作;
'''
result = []
l, r = 0 ,0
while l < len(left) and r < len(right):
# 比较;
if left[l] <= right[r]:
result.append(left[l])
l += 1
else:
# 计算当前left、right 数组之间的逆序对数; 一行代码;
self.cnt += len(left) - l
result.append(right[r])
r += 1
# 左、右分别添加;
result += left[l:]
result += right[r:]
# 返回合并数组;
return result
def mergeSort(arr):
'''
分治操作: 递归进行分治;
'''
# 长度为1, 直接返回;
if len(arr) <= 1:
return arr
mid = len(arr) // 2
# 框架: 左边;
left = mergeSort(arr[:mid])
# 框架:右边;
right = mergeSort(arr[mid:])
# 返回左右合并结果;
return merge(left, right)
mergeSort(nums)
return self.cnt
topK问题:
剑指 Offer 40. 最小的k个数
4种解法秒杀TopK(快排变形/堆/二叉搜索树/计数排序)
Top k 系列题目
- python自带的包
- 方法一: 最大堆实现:
- 时间复杂度: N logK;
- 空间复杂度:logK
# 堆排序
class Solution:
def getLeastNumbers(self, arr, k):
if not arr or k == 0:
return []
if len(arr) <= k:
return arr
heap = arr[:k]
def buildMaxHeap(pos):
'''
构建、调整最大堆;
'''
while pos*2+1 < k:
# 寻找最大元素下标;
max_pos = pos*2+1
if pos*2+2 < k and heap[pos*2+2] > heap[pos*2+1]:
max_pos = pos*2+2
# 最大元素和当前元素交换;
if heap[pos] < heap[max_pos]:
heap[pos],heap[max_pos] = heap[max_pos],heap[pos]
pos = max_pos
else:
break
for i in range(k//2, -1, -1):
buildMaxHeap(i)
# buildMaxHeap(0)
for i in range(k, len(arr)):
if arr[i] < heap[0]:
heap[0] = arr[i]
buildMaxHeap(0)
else:
continue
return heap
arr = [5, 6, 7, 3, 1, 2]
k = 5
s = Solution()
res = s.getLeastNumbers(arr, k)
print(res)
- python调用堆的包:
return heapq.nsmallest(k, arr)
- 方法2: 快排改进
- 时间复杂度: 最好: O(N); 最差: O(N^2)
- 空间复杂度: logN
class Solution:
def getLeastNumbers(self, arr, k):
# 快排思路; 计算key值的下标, 选取一边;
def partition(arr, left, right):
'''
分治算法
'''
key = arr[left]
while left < right:
# 右边找小于key;
while left < right and arr[right] >= key:
right -= 1
# 交换
arr[left] = arr[right]
# 左边找大于key;
while left < right and arr[left] <= key:
left += 1
# 交换
arr[right] = arr[left]
# 位置插入
arr[left] = key
return left
if not arr or not k:
return []
low, high = 0, len(arr)-1
index = partition(arr, low, high)
while index != k-1:
if index < k-1:
low = index+1
index = partition(arr, index+1, high)
elif index > k-1:
high = index-1
index = partition(arr, low, index-1)
return arr[:k]
arr = [3,2,1]
k = 2
s = Solution()
res = s.getLeastNumbers(arr, k)
print(res)