heap sort

转载:http://www.cnblogs.com/0zcl/p/6737944.html

 

其基本思想为(大顶堆)

  1. 将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区
  2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn)
  3. 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成

下图来张教材的图,是整个堆排序的过程: 整个过程的核心就是先初始化大顶堆,将最大数(堆顶)的放到堆的最后一个, 堆长度-1, 继续调整成大顶堆,直至有序序列为len(array_list)-1.

堆排序前42是在42后面,排序后42在42前面,因此堆排序是不稳定的。

 

下面举例说明:

给定一个列表array=[16,7,3,20,17,8],对其进行堆排序。

首先根据该数组元素构建一个完全二叉树,得到

然后需要构造初始堆,则从最后一个非叶节点开始调整,调整过程如下:

第一步: 初始化大顶堆(从最后一个有子节点开始往上调整最大堆)

20和16交换后导致16不满足堆的性质,因此需重新调整

这样就得到了初始堆。

第二步: 堆顶元素R[1]与最后一个元素R[n]交换,交换后堆长度减一

即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了。

第三步: 重新调整堆。此时3位于堆顶不满堆的性质,则需调整继续调整(从顶点开始往下调整)

重复上面的步骤:

注意了,现在你应该了解堆排序的思想了,给你一串列表,你也能写出&说出堆排序的过程。

在写算法的过程中,刚开始我是很懵比。后来终于看懂了。请特别特别注意: 初始化大顶堆时 是从最后一个有子节点开始往上调整最大堆。而堆顶元素(最大数)与堆最后一个数交换后,需再次调整成大顶堆,此时是从上往下调整的。

不管是初始大顶堆的从下往上调整,还是堆顶堆尾元素交换,每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换,交换之后都可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整。我在算法中是用一个while循环来解决的

 

开始写算法:

首先,我先初始化大顶堆:

复制代码

 1 def sift_down(array, start, end):
 2     """
 3     调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
 4     :param array: 列表的引用
 5     :param start: 父结点
 6     :param end: 结束的下标
 7     :return: 无
 8     """
 9     # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
10     # 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
11     left_child = 2*start + 1  # 左孩子的结点下标
12     # 当结点的右孩子存在,且大于结点的左孩子时
13     if left_child+1 <= end and array[left_child+1] > array[left_child]:
14         left_child += 1
15     if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换
16         temp = array[left_child]
17         array[left_child] = array[start]
18         array[start] = temp
19 
20     print(">>", array)
21 
22 
23 def heap_sort(array):  # 堆排序
24     # 先初始化大顶堆
25     first = len(array)//2 -1  # 最后一个有孩子的节点(//表示取整的意思)
26     # 第一个结点的下标为0,很多博客&课本教材是从下标1开始,无所谓吧,你随意
27     for i in range(first, -1, -1):  # 从最后一个有孩子的节点开始往上调整
28         print(array[i])
29         sift_down(array, i, len(array)-1)  # 初始化大顶堆
30 
31     print("初始化大顶堆结果:", array)
32 
33 if __name__ == "__main__":
34     array = [16, 7, 3, 20, 17, 8]
35     print(array)
36     heap_sort(array)
37     print(array)

复制代码

看下运行结果,发现有问题:

1

2

3

4

5

6

7

8

9

[167320178]

3

>> [167820173]

7

>> [162087173]

16

>> [201687173]

初始化大顶堆结果: [201687173]

[201687173]

上面代码的过程如下面4张图所示,但问题是初始化的大顶堆并不正确,当20与16交换后,算法并没有继续对以16为根结点的堆进行调整,导致17的右孩子,大于父结点16.

于是我改进了算法,每次子结点与父结点交换后,需将以子结点为根的完全二叉树调整为大顶堆,当然,如果父结点大与左右孩子,就不需交换,当然与无须再调整为大顶堆。改进后算法如下:

复制代码

 1 def sift_down(array, start, end):
 2     """
 3     调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
 4     :param array: 列表的引用
 5     :param start: 父结点
 6     :param end: 结束的下标
 7     :return: 无
 8     """
 9     while True:
10         # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
11         # 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
12         left_child = 2*start + 1  # 左孩子的结点下标
13         # 当结点的右孩子存在,且大于结点的左孩子时
14         if left_child+1 <= end and array[left_child+1] > array[left_child]:
15             left_child += 1
16         if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换
17             temp = array[left_child]
18             array[left_child] = array[start]
19             array[start] = temp
20 
21             start = left_child  # 交换之后以交换子结点为根的堆可能不是大顶堆,需重新调整
22         else:  # 若父结点大于左右孩子,则退出循环
23             break
24 
25         print(">>", array)
26 
27 
28 def heap_sort(array):  # 堆排序
29     # 先初始化大顶堆
30     first = len(array)//2 -1  # 最后一个有孩子的节点(//表示取整的意思)
31     # 第一个结点的下标为0,很多博客&课本教材是从下标1开始,无所谓吧,你随意
32     for i in range(first, -1, -1):  # 从最后一个有孩子的节点开始往上调整
33         print(array[i])
34         sift_down(array, i, len(array)-1)  # 初始化大顶堆
35 
36     print("初始化大顶堆结果:", array)
37 
38 if __name__ == "__main__":
39     array = [16, 7, 3, 20, 17, 8]
40     print(array)
41     heap_sort(array)
42     print(array)

复制代码

但是运行下,出错了,下标越界!

1

2

3

4

5

6

7

8

9

10

11

Traceback (most recent call last):

  File "C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py", line 42in <module>

    heap_sort(array)

  File "C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py", line 35in heap_sort

[167320178]

    sift_down(array, i, len(array)-1)  # 初始化大顶堆

3

  File "C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py", line 17in sift_down

    if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换

IndexError: list index out of range

>> [167820173]

通过Debug知道为啥越界了

为了解决越界的问题,加个下标判定,轻松解决,oh year:

1

2

if left_child > end:

    break

初始化大顶堆代码:

 View Code

输出:

 View Code

 

初始化大顶堆后,已经要接近成功了。

此时需要交换堆顶与堆尾,但是问题来了,堆顶肯定是array[0],但堆尾呢? 因为每次交换堆顶与堆尾后,堆尾下标是会变化的啊。

为了每次交换时都能找到堆尾,我用一个循环。

1

2

3

# 交换堆顶与堆尾

for head_end in range(len(array)-10-1):  # start stop step

    array[head_end], array[0= swap(array[head_end], array[0]) # 交换堆顶与堆尾

交换堆顶与堆尾后,堆长度减一,且需从上往下调整成大顶堆。

1

2

3

4

# 交换堆顶与堆尾

for head_end in range(len(array)-10-1):  # start stop step

    array[head_end], array[0= swap(array[head_end], array[0]) # 交换堆顶与堆尾

    sift_down(array, 0, head_end-1)  # 堆长度减一(head_end-1),再从上往下调整成大顶堆

 

自此,堆排序算法ending,你会了吗? or 你会装逼了吗?

 

堆排序代码:

复制代码

 1 def swap(a, b):  # 将a,b交换
 2     temp = a
 3     a = b
 4     b = temp
 5     return a,b
 6 
 7 def sift_down(array, start, end):
 8     """
 9     调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
10     :param array: 列表的引用
11     :param start: 父结点
12     :param end: 结束的下标
13     :return: 无
14     """
15     while True:
16 
17         # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
18         # 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
19         left_child = 2*start + 1  # 左孩子的结点下标
20         # 当结点的右孩子存在,且大于结点的左孩子时
21         if left_child > end:
22             break
23 
24         if left_child+1 <= end and array[left_child+1] > array[left_child]:
25             left_child += 1
26         if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换
27             array[left_child], array[start] = swap(array[left_child], array[start])
28 
29             start = left_child  # 交换之后以交换子结点为根的堆可能不是大顶堆,需重新调整
30         else:  # 若父结点大于左右孩子,则退出循环
31             break
32 
33         print(">>", array)
34 
35 
36 def heap_sort(array):  # 堆排序
37     # 先初始化大顶堆
38     first = len(array)//2 -1  # 最后一个有孩子的节点(//表示取整的意思)
39     # 第一个结点的下标为0,很多博客&课本教材是从下标1开始,无所谓吧,你随意
40     for i in range(first, -1, -1):  # 从最后一个有孩子的节点开始往上调整
41         print(array[i])
42         sift_down(array, i, len(array)-1)  # 初始化大顶堆
43 
44     print("初始化大顶堆结果:", array)
45     # 交换堆顶与堆尾
46     for head_end in range(len(array)-1, 0, -1):  # start stop step
47         array[head_end], array[0] = swap(array[head_end], array[0]) # 交换堆顶与堆尾
48         sift_down(array, 0, head_end-1)  # 堆长度减一(head_end-1),再从上往下调整成大顶堆
49 
50 
51 
52 if __name__ == "__main__":
53     array = [16, 7, 3, 20, 17, 8]
54     print(array)
55     heap_sort(array)
56     print("堆排序最终结果:", array)

复制代码

运行结果:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

[167320178]

3

>> [167820173]

7

>> [162087173]

16

>> [201687173]

>> [201787163]

初始化大顶堆结果: [201787163]

>> [173871620]

>> [171687320]

>> [163871720]

>> [167831720]

>> [873161720]

>> [738161720]

堆排序最终结果: [378161720]

 

时间复杂度:

空间复杂度:堆排序数据交换时需要一个辅助空间,故空间复杂度是O(1)

 

在构建堆(初始化大顶堆)的过程中,完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和必要的互换,对于每个非终端结点来说,其实最多进行两次比较和一次互换操作,因此整个构建堆的时间复杂度为: O(n)。大概需进行n/2 * 2 = n次比较和n/2次交换。

 

在正式排序时,n个结点的完全二叉树的深度为⌊log2n⌋+1,并且有n个数据则需要取n-1次调整成大顶堆的操作,每次调整成大顶堆的时间复杂度为O(log2n)。因此,重建堆的时间复杂度可近似看做: O(nlogn)。

 

 

堆排序效果图:

各种排序的稳定性,时间复杂度和空间复杂度总结:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值