以下内容来自网络,如侵权请联系我
堆排序|快排|归并排序
-
三种排序算法的时间复杂度:O(nlogn)
-
三种排序算法的空间复杂度:
- 快排:平均O(logn), 最坏O(n)
- 归并:O(n)
- 堆排:O(1),topK问题可能O(k)
-
运行时间:快排 < 归并排序 < 堆排序
-
算法特点:
- 快排:不稳定,极端情况排序效率低,最坏会退化成O(n^2)
- 归并排序:稳定,需要额外的内存空间开销
- 堆排序:不稳定,在效率较高的排序算法中,相对较慢
1.快速排序
python
# 快速排序
def partition(nums: [int], left: int, right: int):
"""分区函数,左边比中间小,右边比中间大"""
if left > right:
return nums
pivot = left
i, j = left, right
while i < j: # while循环的操作在于寻找pivot位置并交换元素,最终使得pivot为基准,分割左右半区元素,继续递归
while i < j and nums[j] >= nums[pivot]: # 大于pivot对应元素时, j--,实现右指针左移,直到 j <= i(往往相遇时相等) 或者 nums[j] < nums[pivot]
j -= 1
while i < j and nums[i] < nums[pivot]: # 小于等于pivot对应元素时,i++,实现左指针右移,直到 i >= j(往往相遇时相等) 或者 num[i] > nums[pivot]
i += 1
nums[i], nums[j] = nums[j], nums[i] # 当 i=j 时,已经跳出上面两个while循环,即左右指针相遇,或者(j的元素小于等于pivot && i的元素大于pivot),肯定需要交换不同位置元素,实现pivot分割数组,最终使得左右指针相遇
nums[pivot], nums[j] = nums[j], nums[pivot] # 交换基准与左右指针相遇位置的元素,此次分区结束,新排序的数组一定是pivot元素大于左区,小于右区
partition(nums, left, j - 1) # 递归左半区
partition(nums, j + 1, right) # 递归右半区
return nums
def quickSort(nums: [int]) -> [int]:
"""快速排序"""
length = len(nums)
return partition(nums, 0, length-1)
if __name__ == "__main__":
import random
nums = list(range(50))
random.shuffle(nums)
randomNums = nums[:10]
print(randomNums)
print(quickSort(randomNums))
golang
func quickSort(nums []int) []int {
// 快速排序
length := len(nums)
return partition(nums, 0, length-1)
}
func partition(nums []int, left, right int) []int {
// 分区及排序,快排的具体实现
// 递归终止条件
if left > right {return nil}
// 设置基准-哨兵
pivot := left
i, j := left, right
// 循环遍历,直到 i==j
for i < j {
for i < j && nums[j] >= nums[pivot] {
j--
}
for i < j && nums[i] <= nums[pivot] {
i++
}
// 交换元素
nums[i], nums[j] = nums[j], nums[i]
}
// 交换基准与此轮相遇位置元素
nums[pivot], nums[j] = nums[j], nums[pivot]
// 递归左区
partition(nums, left, i-1)
// 递归右区
partition(nums, i+1, right)
return nums
}
2.归并排序
Python内部的sort()方法是基于归并排序实现的。
# 归并排序
def merge(arr: [int], start: int, mid: int, end: int):
i = start
j = mid + 1
tmpArr = [] # 开辟临时容器
while i <= mid and j <= end: # 当左右两个分区都有数据时
if arr[i] < arr[j]: # 左边小,加入tmpArr
tmpArr.append(arr[i])
i += 1
else:
tmpArr.append(arr[j]) # 右边小,加入tmpArr
j += 1
# 上面while完成后,左右两区有一区没数据,另外一区直接添加到尾部
while i <= mid:
tmpArr.append(arr[i])
i += 1
while j <= end:
tmpArr.append(arr[j])
j += 1
arr[start:end+1] = tmpArr # tmpArr按arr原位置写入
def mergeSort(arr: [int], start: int, end: int) -> [int]:
if start < end:
mid = (start+end) >> 1
# 递归左边
mergeSort(arr, start, mid)
# 递归右边
mergeSort(arr, mid+1, end)
# 合并数据
merge(arr, start, mid, end)
return arr
if __name__ == "__main__":
import random
nums = list(range(100))
random.shuffle(nums)
nums = nums[:20]
randomNums = [60, 54, 62, 49, 24, 13, 88, 59, 66, 57, 42, 23, 0, 15, 8, 67, 21, 9, 12, 78]
print(mergeSort(randomNums, 0, 19))
golang实现
// mergeSort
func mergeSort(nums []int) []int {
// 递归终止
if len(nums) <= 1 {
return nums
}
// 拆分与合并
mid := len(nums) >> 1
left := mergeSort(nums[:mid])
right := mergeSort(nums[mid:])
return merge(left, right)
}
// 合并func
func merge(left, right []int) []int {
res := make([]int, len(left)+len(right))
l, r, i := 0, 0, 0
for l < len(left) && r < len(right) {
if left[l] <= right[r] {
res[i] = left[l]
l++
} else {
res[i] = right[r]
r++
}
i++
}
// 左右区,哪个还未遍历完就把对应区的元素加入res
for l < len(left) {
res[i] = left[l]
i++
l++
}
for r < len(right) {
res[i] = right[r]
i++
r++
}
/* 加入剩余区的元素的另一种写法
copy(res[i:], left[l:]) // 如果右区遍历完,执行左区copy
copy(res[i+len(left)-l:], right[r:]) // 如果左区空,执行这个copy, l=len(left)
*/
return res
}
3.堆排序
堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。
堆的结构是一棵完全二叉树的结构,并且满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。
关于二叉树和完全二叉树的介绍可以参考:二叉树简介
堆排序先按从上到下、从左到右的顺序将待排序列表中的元素构造成一棵完全二叉树,然后对完全二叉树进行调整,使其满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。构建出堆后,将堆顶与堆尾进行交换,然后将堆尾从堆中取出来,取出来的数据就是最大(或最小)的数据。重复构建堆并将堆顶和堆尾进行交换,取出堆尾的数据,直到堆中的数据全部被取出,列表排序完成。
- 1.堆排序
- 2.优先级队列
- 2.1 合并小文件
- 2.2 高性能定时器
- 3.取top k元素
- 4.取中位数
- 4.1 99%的响应时间
重点
列表排序的算法思路:
- 1.重复构建堆,并交换堆顶和队尾元素
- 2.取出队尾数据
- 3.直到堆中数据全部取出
堆结构分为大顶堆和小顶堆:
-
- 大顶堆:每个节点(叶节点除外)的值都大于等于其子节点的值,根节点的值是所有节点中最大的,所以叫大顶堆,在堆排序算法中用于升序排列。
-
- 小顶堆:每个节点(叶节点除外)的值都小于等于其子节点的值,根节点的值是所有节点中最小的,所以叫小顶堆,在堆排序算法中用于降序排列。
堆排序的原理如下:
-
- 将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
-
- 将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
-
- 将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
-
- 将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
-
- 重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。
3.1 大顶堆
代码中,先实现一个bigHeapify(array, start, end)
函数,用于比较节点与其子节点中的较大者,如果值小于子节点的值则进行交换。代码中不需要真正将数据都添加到完全二叉树中,而是根据待排序列表中的数据索引来得到节点与子节点的位置关系。完全二叉树中,节点的索引为i
,则它的左子节点的索引为2*i+1
,右子节点的索引为2*i+2
,有n个节点的完全二叉树中,非叶子节点有n//2
个,列表的索引从0
开始,所以索引为0~n//2-1
的数据为非叶子节点。
实现堆排序函数heapSort(array)
时,先调用bigHeapify(array, start, end)
函数循环对非叶子节点进行调整,构造大顶堆,然后将堆顶和堆尾交换,将堆尾从堆中取出,添加到已排序序列中,完成一轮堆排序。然后循环构建大顶堆,每次将最大的元素取出,直到堆中的数据全部被取出。
堆排序的时间复杂度和稳定性
-
- 时间复杂度
在堆排序中,构建一次大顶堆可以取出一个元素,完成一轮堆排序,一共需要进行n轮堆排序。每次构建大顶堆时,需要进行的比较和交换次数平均为logn(第一轮构建堆时步骤多,后面重建堆时步骤会少很多)。时间复杂度为 T(n)=nlogn ,再乘每次操作的步骤数(常数,不影响大O记法),所以堆排序的时间复杂度为 O(nlogn) 。
- 时间复杂度
-
- 稳定性
在堆排序中,会交换节点与子节点,如果有相等的数据,可能会改变相等数据的相对次序。所以堆排序是一种不稳定的排序算法。
python
扩展阅读:
- 稳定性
# 堆排序-大顶堆-升序排列
def maxHeapify(arr: [int], start: int, end: int) -> None:
"""向下调整,堆化"""
root = start
# left child index
child = root*2 + 1
while child <= end:
# 比较左右子节点的大值
if child+1 <= end and arr[child] < arr[child+1]:
child += 1
if arr[root] < arr[child]:
# 子节点更大,交换节点与子节点的值
arr[root], arr[child] = arr[child], arr[root]
# 更新节点到子节点位置,继续下一轮交换
root = child
child = root*2 + 1
else:
break
# 递归法
def maxHeapify_(arr, start, end):
pos = start
child = pos*2 + 1
if child < end:
right = child + 1
if right < end and arr[right] > arr[child]:
child = right
if arr[child] > arr[pos]:
arr[pos], arr[child] = arr[child], arr[pos]
maxHeapify_(arr, pos, end)
def maxheapSort(arr: [int]) -> [int]:
"""大顶堆排序"""
# 初始化非叶子节点
first = (len(arr) >> 1) - 1
for start in range(first, -1, -1):
# 从下往上,从右至左对每个非叶子节点进行向下调整,循环构成大顶堆
maxHeapify(arr, start, len(arr)-1)
# 交换堆顶尾数据,堆数量--,重新堆化
for end in range(len(arr)-1, 0, -1):
# 交换堆顶堆尾数据, 构造大顶堆
arr[0],arr[end] = arr[end], arr[0]
# 重新调整堆,构造大顶堆
maxHeapify(arr, 0, end-1)
return arr
if __name__ == "__main__":
import random
nums = list(range(20))
random.shuffle(nums)
nums = [19, 1, 11, 4, 12, 8]
orderNums = maxheapSort(nums)
print(orderNums)
golang
func maxHeapSort(nums []int) []int {
// max heap sort
end := len(nums)-1
// 从最后一个非叶子节点开始堆化
for i:=end>>1;i>=0;i-- {
maxHeapify(nums, i, end)
}
// 依次弹出堆顶元素,然后堆化,相当于依次把最大的放尾部
for i:=end;i>=0;i-- {
nums[0], nums[i] = nums[i], nums[0]
end--
maxHeapify(nums, 0, end)
}
return nums
}
func maxHeapify(nums []int, root, end int) {
// 大顶堆堆化,堆顶值小就一直下沉
for {
// 获取做左孩子
child := root*2 + 1
// 越界跳出
if child > end {
return
}
// 比较左右孩子节点,如果右孩子大则取右孩子与root比较,否则左右孩子左孩子大, child不用++
if child < end && nums[child] <= nums[child+1] {
child++
}
// 如果父节点值大于左右孩子的大值,说明,已经堆化
if nums[root] > nums[child] {
return
}
// 交换父节点与较大的子节点
nums[root], nums[child] = nums[child], nums[root]
// 更新父节点为孩子节点,继续与孙节点比较,不断往下沉
root = child
}
}
3.2 小顶堆
以小顶堆应用的topK问题为例。
TopK
现有n个数,得到前k大的数,显然这需要降序排列,取前k个数即可(k<n)。
使用小顶堆的解决思路:
- 1.取数组的前k个元素建立一个小顶堆,堆顶就是第k大的元素
- 2.依次向后遍历数组,对于数组中的元素,如果小于堆顶,忽略,大于堆顶元素,将堆顶替换为该元素,并且调整堆
- 3.遍历数组的所有元素,倒序弹出堆顶
时间:
- 1.排序后的切片: O(nlogn)
- 2.排序的三人组: O(kn)
- 3.堆排序思路: O(nlogk)
python
# 小顶堆-降序排列
def minHeapify(arr: [int], start: int, end: int):
"""大的节点往下沉,小的往上冒-小顶堆化"""
root = start
child = root*2 + 1
tmpVal = arr[start]
while child <= end:
if child + 1 <= end and arr[child+1] < arr[child]:
child += 1
if arr[child] < tmpVal:
arr[root] = arr[child]
root = child
child = 2*root + 1
else:
arr[root] = tmpVal
break
else:
arr[root] = tmpVal
def minHeapSort(arr: [int], k: int) -> [int]:
"""小顶堆,升序排列, topK"""
# 以arr切片作为heap的初始序列
heap = arr[:k]
# 初始化topK的小顶堆的非叶子节点
first = (k-2) >> 1
# 小顶堆,堆化
for i in range(first, -1, -1):
minHeapify(heap, i, k-1)
# 依次遍历arr的第k+1及以后的元素,入小顶堆顶,小顶堆化
for i in range(k, len(arr)-1):
if arr[i] > heap[0]:
heap[0] = arr[i]
minHeapify(heap, 0, k-1)
# 排序heap, 小顶堆数量--
for i in range(k-1, -1, -1):
heap[0], heap[i] = heap[i], heap[0]
minHeapify(heap, 0, i-1)
return heap
def minHeapSort_(arr: [int]) -> [int]:
"""小顶堆,降序输出"""
length = len(arr)
# 初始化非叶子结点
first = (length-2) >> 1
# 遍历非叶子节点,小堆化
for i in range(first, -1, -1):
minHeapify(arr, i, length-1)
# 排序数组, 小顶堆数量--
for i in range(length-1, -1, -1):
arr[0], arr[i] = arr[i], arr[0]
minHeapify(arr, 0, i-1)
return arr
if __name__ == "__main__":
nums = [0, 8, 9, 12, 13, 15, 21, 23]
# print(minHeapSort(nums, 5)) # top5, 前5的数
# print(minHeapSort(nums, 8)) # k == len(nums), topK即为原数组的降序排列, copy
print(minHeapSort_(nums))
golang
func minHeapify(arr []int, root, end int) {
// 小顶堆,堆化,非叶子节点大的下沉
for {
child := root*2 + 1
// 越界跳出
if child > end {
return
}
// root的值和子节点的小值比较
if child < end && arr[child] >= arr[child+1] {
child++
}
// root节点已经小于子节点,跳出
if arr[root] < arr[child] {
return
}
// root节点大于等于子节点,交换
arr[root], arr[child] = arr[child], arr[root]
root = child
}
}
func minHeapSort(arr []int) []int {
// 小顶堆,降序输出
length := len(arr)
// 非叶子节点初始化, int(n/2)
first := (length-2) >> 1
// 遍历所有非叶子节点,大的下沉
for i:=first;i>=0;i-- {
minHeapify(arr, i, length-1)
}
// 依次弹出堆尾节点,交换后继续堆化
for i:=length-1;i>=0;i-- {
arr[0], arr[i] = arr[i], arr[0]
minHeapify(arr, 0, i-1)
}
return arr
}
func minHeapSort_(arr []int, k int) []int {
// 小顶堆, top K
heap := arr[:k]
// 初始化非叶子节点
first := (k-2) >> 1
// 形成一个数量为k的小顶堆
for i:=first;i>=0;i-- {
minHeapify(heap, i, k-1)
}
// 遍历后续节点,只有大于堆顶的元素,放入堆顶,堆化
for i:=k;i<len(arr)-1;i++ {
if arr[i] > heap[0] {
heap[0] = arr[i]
minHeapify(heap, 0, k-1)
}
}
// 依次弹出heap堆中的堆顶元素,弹出的元素就是topK
for i:=k-1;i>=0;i-- {
heap[0], heap[i] = heap[i], heap[0]
minHeapify(heap, 0, i-1)
}
return heap
}
补充
4.插入排序 vs 希尔排序
希尔排序可以看做插入排序的升级版。
两种排序算法的时空复杂度:
- 时间复杂度: 都是O(n^2)
- 空间复杂度: 都是O(1)
插入排序
python实现
# 插入排序
def insertSort(nums: [int]) -> [int]:
length = len(nums)
for i in range(1, length):
while i > 0 and nums[i-1] > nums[i]:
nums[i-1], nums[i] = nums[i], nums[i-1]
i -= 1
return nums
if __name__ == "__main__":
import random
nums = list(range(100))
random.shuffle(nums)
randomNums = nums[:10]
print(randomNums)
print(insertSort(randomNums))
golang
func insertSort(nums []int) []int {
length := len(nums)
for i:=0;i<length;i++ {
for j:=i;j>0;j-- {
if nums[j-1] > nums[j] {
nums[j-1], nums[j] = nums[j], nums[j-1]
}
}
}
return nums
}
希尔排序
python
# 希尔排序
def shellSort(nums: [int]) -> [int]:
length = len(nums)
gap = length >> 1
while gap:
for i in range(gap, length):
while i-gap >= 0 and nums[i-gap] > nums[i]:
nums[i-gap], nums[i] = nums[i], nums[i-gap]
i -= gap
gap >>= 1
return nums
if __name__ == "__main__":
import random
nums = list(range(100))
random.shuffle(nums)
randomNums = nums[:10]
print(randomNums)
print(shellSort(randomNums))
golang
func shellSort(nums []int) []int {
length := len(nums)
gap := length >> 1
for gap > 0 {
for i:=gap;i<length;i++ {
j := i
for j-gap >= 0 && nums[j-gap] > nums[j] {
nums[j-gap], nums[j] = nums[j], nums[j-gap]
j -= gap
}
}
gap >>= 1
}
return nums
}