堆分为最大堆和最小堆,其实就是完全二叉树。最大堆要求节点的元素都要不小于其孩子,最小堆要求节点元素都不大于其左右孩子,两者对左右孩子的大小关系不做任何要求,其实很好理解。有了上面的定义,我们可以得知,处于最大堆的根节点的元素一定是这个堆中的最大值。
其实我们的堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。
其基本思想为(大顶堆)
- 将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn)
- 由于交换后新的堆顶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
|
[
16
,
7
,
3
,
20
,
17
,
8
]
3
>> [
16
,
7
,
8
,
20
,
17
,
3
]
7
>> [
16
,
20
,
8
,
7
,
17
,
3
]
16
>> [
20
,
16
,
8
,
7
,
17
,
3
]
初始化大顶堆结果: [
20
,
16
,
8
,
7
,
17
,
3
]
[
20
,
16
,
8
,
7
,
17
,
3
]
|
上面代码的过程如下面4张图所示,但问题是初始化的大顶堆并不正确,当20与16交换后,算法并没有继续对以16为根结点的堆进行调整,导致17的右孩子,大于父结点16.
于是我改进了算法,每次子结点与父结点交换后,需将以子结点为根的完全二叉树调整为大顶堆,当然,如果父结点大与左右孩子,就不需交换,当然与无须再调整为大顶堆。改进后算法如下:

但是运行下,出错了,下标越界!
1
2
3
4
5
6
7
8
9
10
11
|
Traceback (most recent call last):
File
"C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py"
, line
42
,
in
<module>
heap_sort(array)
File
"C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py"
, line
35
,
in
heap_sort
[
16
,
7
,
3
,
20
,
17
,
8
]
sift_down(array, i,
len
(array)
-
1
)
# 初始化大顶堆
3
File
"C:/Users/Administrator/PycharmProjects/laonanhai/编程/我的堆排序.py"
, line
17
,
in
sift_down
if
array[left_child] > array[start]:
# 当左右孩子的最大值大于父结点时,则交换
IndexError:
list
index out of
range
>> [
16
,
7
,
8
,
20
,
17
,
3
]
|
通过Debug知道为啥越界了
为了解决越界的问题,加个下标判定,轻松解决,oh year:
1
2
|
if
left_child > end:
break
|
初始化大顶堆代码:

输出:

初始化大顶堆后,已经要接近成功了。
此时需要交换堆顶与堆尾,但是问题来了,堆顶肯定是array[0],但堆尾呢? 因为每次交换堆顶与堆尾后,堆尾下标是会变化的啊。
为了每次交换时都能找到堆尾,我用一个循环。
1
2
3
|
# 交换堆顶与堆尾
for
head_end
in
range
(
len
(array)
-
1
,
0
,
-
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)
-
1
,
0
,
-
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[
16
,
7
,
3
,
20
,
17
,
8
]
3
>> [
16
,
7
,
8
,
20
,
17
,
3
]
7
>> [
16
,
20
,
8
,
7
,
17
,
3
]
16
>> [
20
,
16
,
8
,
7
,
17
,
3
]
>> [
20
,
17
,
8
,
7
,
16
,
3
]
初始化大顶堆结果: [
20
,
17
,
8
,
7
,
16
,
3
]
>> [
17
,
3
,
8
,
7
,
16
,
20
]
>> [
17
,
16
,
8
,
7
,
3
,
20
]
>> [
16
,
3
,
8
,
7
,
17
,
20
]
>> [
16
,
7
,
8
,
3
,
17
,
20
]
>> [
8
,
7
,
3
,
16
,
17
,
20
]
>> [
7
,
3
,
8
,
16
,
17
,
20
]
堆排序最终结果: [
3
,
7
,
8
,
16
,
17
,
20
]
|
时间复杂度:


上图来自:http://blog.youkuaiyun.com/hguisu/article/details/7776068
参考博客:http://www.cnblogs.com/dolphin0520/archive/2011/10/06/2199741.html
上文转发出处: http://www.cnblogs.com/0zcl/p/6737944.html
2)图解堆排序









代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public
class
HeapSort {
private
static
void
heapSort(
int
[] arr) {
int
len = arr.length -
1
;
for
(
int
i = len/
2
-
1
; i >=
0
; i --){
//堆构造
heapAdjust(arr,i,len);
}
while
(len >=
0
){
swap(arr,
0
,len--);
//将堆顶元素与尾节点交换后,长度减1,尾元素最大
heapAdjust(arr,
0
,len);
//再次对堆进行调整
}
}
public
static
void
heapAdjust(
int
[] arr,
int
i,
int
len){
int
left,right,j ;
while
((left =
2
*i+
1
) <= len){
//判断当前父节点有无左节点(即有无孩子节点,left为左节点)
right = left +
1
;
//右节点
j = left;
//j"指针指向左节点"
if
(j < len && arr[left] < arr[right])
//右节点大于左节点
j ++;
//当前把"指针"指向右节点
if
(arr[i] < arr[j])
//将父节点与孩子节点交换(如果上面if为真,则arr[j]为右节点,如果为假arr[j]则为左节点)
swap(arr,i,j);
else
//说明比孩子节点都大,直接跳出循环语句
break
;
i = j;
}
}
public
static
void
swap(
int
[] arr,
int
i,
int
len){
int
temp = arr[i];
arr[i] = arr[len];
arr[len] = temp;
}
public
static
void
main(String[] args) {
int
array[] = {
20
,
50
,
20
,
40
,
70
,
10
,
80
,
30
,
60
};
System.out.println(
"排序之前:"
);
for
(
int
element : array){
System.out.print(element+
" "
);
}
heapSort(array);
System.out.println(
"\n排序之后:"
);
for
(
int
element : array){
System.out.print(element+
" "
);
}
}
}
|
输出:
排序之前: 20 50 20 40 70 10 80 30 60 排序之后: 10 20 20 30 40 50 60 70 80