算法入门——part2.排序(中)
前文:(插入排序、冒泡排序、选择排序)
https://blog.csdn.net/nanashi_F/article/details/88902072
4.堆排序(Heap Sort)
介绍
堆
(二叉)堆数据结构是一种数组对象,如图所示,它可以被视为一棵完全二叉树。树中每个结点与数组中存放该结点值的那个元素对应。树的每一层都是填满的,最后一层可能除外(最后一层从一个结点的左子树开始填)。
树的根为A[1],给定了某个结点的下标i,其父结点PARENT(i)、左儿子LEFT(i)和右儿子RIGHT(i)的下标可以简单地计算出来:
PARENT(i)
return[i/2」
LEFT(i)
return 2i
RIGHT(i)
return 2i+1
几个重要过程
●MAX HEAPIFY
其运行时间为O(Ign),是保持最大堆性质的关键。
在算法的每一步里, 从元素A[i],A[LEFT(i)]和A[RIGHT(i)]中找出最大的,并将其下标存在largest中。如果A[i]是最大的,则以i为根的子树已是最大堆,程序结束。否则,i的某个子结点中有最大元素,则交换A[i]和A[largest],从而使i及其子女满足堆性质。
●BUILD MAX-HEAP
以线性时间运行,对每个节点运行一次MAX HEAPIFY过程。可以在无序的输人数组基础上构造出最大堆。
●HEAPSORT
对一个数组原地用堆进行排序
时间复杂度及稳定性
堆可以被看成是一棵树,结点在堆中的高度定义为从本结点到叶子的最长简单下降路径上边的数目:定义堆的高度为树根的高度。因为具有n个元素的堆是基于一棵完全二叉树的,因而其高度为O(Ign)。我们将看到,堆结构上的一些基本操作的运行时间至多与树的高度成正比,为O(lgn)。
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
代码实现
环境python3,下同
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import random
Range = 100
Length = 10
list = random.sample(range(Range),Length) #随机获取指定长度片段
print("start:",list)
def big_endian(arr, start, end):
root = start
while True:
child = root * 2 + 1 # 左孩子
if child > end: # 孩子比最后一个节点还大 也就意味着最后一个叶子节点了 就得跳出去一次循环已经调整完毕
break
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
else: # 父子节点顺序正常 直接过
break
def Heap_Sort(arr):
# 无序区大根堆排序
first = len(arr) // 2 - 1 #//取整除
for start in range(first, -1, -1): # 从下到上,从右到左对每个节点进调整 循环得到非叶子节点
big_endian(arr, start, len(arr) - 1) # 去调整所有的节点
for end in range(len(arr) - 1, 0, -1):
arr[0], arr[end] = arr[end], arr[0] # 顶部尾部互换位置
big_endian(arr, 0, end - 1) # 重新调整子节点的顺序 从顶开始调整
return arr
def main():
print("final:",Heap_Sort(list))
if __name__ == "__main__":
main()
5.快速排序(Quick Sort)
介绍
快速排序是对冒泡排序的一种改进。
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index](其中center_index是中枢元素的数组下标,一般取为数组第0个元素)而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。
时间复杂度及稳定性
排序算法的最坏时间复杂度为O(n^2),且最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候。
快速排序的平均时间复杂度为O(nlogn)。
在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
代码实现
def Quick_Sort(nums):
mid = nums[len(nums)//2] # 选取基准值,也可以选取第一个或最后一个元素
left, right = [], [] # 定义基准值左右两侧的列表
nums.remove(mid) # 从原始数组中移除基准值
for num in nums:
if num >= mid:
right.append(num)
else:
left.append(num)
return Quick_Sort(left) + [mid] + Quick_Sort(right)
排序实例
1为左列,2为右列
start: [2, 41, 91, 56, 75, 57, 25, 21, 1, 90]
1 [2, 41, 56, 25, 21, 1]
2 [91, 75, 90]
1.2.1 [2, 21, 1]
1.2.2 [41, 56]
1.2.1.1 [2, 1]
1.2.1.2 []
1.2.1.1.1[]
1.2.1.1.2[2]
1.2.2.1[41]
1.2.2.2[]
2.1[]
2.2[91, 90]
2.2.1 []
2.2.2 [91]
final: [1, 2, 21, 25, 41, 56, 57, 75, 90, 91]