堆排序
1.概念
堆一般指的是二叉堆,顾名思义,二叉堆是完全二叉树或者近似完全二叉树。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
1.1. 堆的性质
- 是一颗完全二叉树
- 每一个节点的值都大于或等于其子节点的值, 为最大堆。反之为最小堆
1.2. 堆的存储
一般用数组来表示堆(顺序存储)。
下标为 i 的结点的父结点下标为(i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2)
1.3. 堆的操作
在堆的数据结构中,堆中的最大值总是位于根节点(最大堆)。堆中定义以下几种操作:
- 最大堆调整:将堆的末端子节点做调整,使得子节点永远小于父节点
- 创建最大堆:将堆中所有的数据重新排列
- 堆排序:移除位于堆的根节点(与堆中待排序列最后一元素进行交换),然后对最大堆进行调整(递归运算)
1.4. 排序思想
利用大顶堆(小顶堆)堆顶记录的是最大值(最小值)这一特性,使得每次从无序堆中生成一个最大值或最小值。
① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点
② 依次将根节点与待排序序列的最后一个元素交换
③ 再维护从根节点到该待排序序列元素的前一个节点为最大堆,如此往复,最终得到一个递归序列
2.算法步骤
① 先将初始的R[0…n-1]建立成最大堆,此时是无序堆,而堆顶是最大元素。
② 再将堆顶R[0]和无序区的最后一个记录R[n-1]交换,由此得到新的无序区R[0…n-2]和有序区R[n-1],且满足R[0…n-2].keys ≤ R[n-1].key
③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1…n-1]调整为堆。然后再次将R[1…n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1…n-2]和有序区R[n-1…n],且仍满足关系R[1…n-2].keys≤R[n-1…n].keys,同样要将R[1…n-2]调整为堆。
④ 直到无序区只有一个元素为止。
3.动图演示
4.代码实现
4.1. 最大堆排序
4.1.1 算法实现
void swap(int* a, int* b) {
int temp = *b;
*b = *a;
*a = temp;
}
void max_heapify(int arr[], int start, int end) {
//建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { //若子节点指标在范围内才做比较
if (son + 1 <= end && arr[son] < arr[son + 1]) //先比较两个子节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
return;
else { //否则交换父子内容再继续子节点和孙节点比较
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len) {
int i;
for (i = len / 2 - 1; i >= 0; i--)//堆初始化。从最后一个父节点开始
max_heapify(arr, i, len - 1);
//先将第一个元素和已排好元素前一位做交换,再从新调整,直到排序完毕
for (i = len - 1; i > 0; i--) {//长度为len的数组,需要排序len-1次
swap(&arr[0], &arr[i]); //并且是要从后往前遍历
max_heapify(arr, 0, i - 1);
}
}
4.1.2 测试程序
void main()
{
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 9, 70, 35,89};//定义一个待排序的数组
int len = sizeof(arr) / sizeof(*arr);//获取数组长度
printf("\nsort before\n");
for (int i = 0; i < len; i++)//打印排序前数组元素
{
printf("%d\t", arr[i]);
}
heap_sort(arr,len);//大堆法
printf("\nheap_sort_big after\n");//打印排序后数组元素
for (int i = 0; i < len; i++)
{
printf("%d\t", arr[i]);
}
system("pause");
}
4.2. 最小堆排序
4.2.1 算法实现
void swap(int* a, int* b) {
int temp = *b;
*b = *a;
*a = temp;
}
void min_heapify(int arr[], int start, int end) {
//建立父节点指标和子节点指标
int dad = start;
int len = start+1;//start标号是未排序元素中的最大值
int son = 2*dad -len ;//left child 递推关系后文有描述
while (son >=end) { //若子节点指标在范围内才做比较
if (son - 1 >= end && arr[son] > arr[son - 1]) //先比较两个子节点大小,选择最小的
son--;
if (arr[dad] < arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
return;
else { //否则交换父子内容再继续子节点和孙节点比较
swap(&arr[dad], &arr[son]);
dad = son;
son = 2 * dad - len;
}
}
}
void heap_sort(int arr[], int len) {
int i;
for (i = len / 2 - 1; i <len; i++)
min_heapify(arr,i,0);
//先将第一个元素和已排好元素前一位做交换,再从新调整,直到排序完毕
for (i = 0; i <len-1; i++) {
swap(&arr[i], &arr[len-1]);
min_heapify(arr,len-1,i+1);
}
}
4.2.2 测试程序
void main()
{
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 9, 70, 35,89};//定义一个待排序的数组
int len = sizeof(arr) / sizeof(*arr);
printf("\nsort before\n");
for (int i = 0; i < len; i++)
{
printf("%d\t", arr[i]);
}
heap_sort(arr,len);//小堆法
printf("\nheap_sort_small after\n");
for (int i = 0; i < len; i++)
{
printf("%d\t", arr[i]);
}
system("pause");
}
5.代码解释
大堆排序法非常的通俗易懂,这个我就不过多介绍,主要还是说明一下小堆法。
小堆法中最难的一个地方就是父节点与子节点之间的关系。
我的解决办法是:利用大堆法中根据父节点与子节点之间的关系,计算出在小堆法如何根据父节点的位置计算出子节点。
- 假设现有条件是一个长度为len的数组,父节点索引号为Parent(相对于数组首地址偏移地址)
- 左右子节点的索引号为leftChild、rightChild。
对于大端法而言:
l
e
f
t
C
h
i
l
d
=
2
∗
P
a
r
e
n
t
+
1
leftChild = 2*Parent + 1
leftChild=2∗Parent+1
r i g h t C h i l d = 2 ∗ P a r e n t + 2 rightChild = 2*Parent+2 rightChild=2∗Parent+2
对于小端法而言:
l
e
f
t
C
h
i
l
d
=
2
∗
P
a
r
e
n
t
−
l
e
n
leftChild = 2*Parent-len
leftChild=2∗Parent−len
r i g h t C h i l d = 2 ∗ P a r e n t − l e n − 1 rightChild = 2*Parent-len-1 rightChild=2∗Parent−len−1
小端法父子节点的推导过程:
小端法中的父节点与子节点之间的距离是和大端法中父节点和子节点之间的距离是一样的!
大端法中父节点与小端法中的父节点之间的关系为:
P
a
r
e
n
t
B
i
g
E
n
d
+
P
a
r
e
n
t
S
m
a
l
l
E
n
d
=
l
e
n
−
1
①
ParentBigEnd + ParentSmallEnd = len-1 ①
ParentBigEnd+ParentSmallEnd=len−1①
小端法中父节点与左右子节点之间的距离:
L
e
f
t
D
i
s
t
a
n
c
e
S
m
a
l
l
=
l
e
f
t
C
h
i
l
d
S
m
a
l
l
E
n
d
−
P
a
r
e
n
t
S
m
a
l
l
E
n
d
②
LeftDistanceSmall = leftChildSmallEnd-ParentSmallEnd②
LeftDistanceSmall=leftChildSmallEnd−ParentSmallEnd②
R i g h t D i s t a n c e S m a l l = r i g h t C h i l d S m a l l E n d − P a r e n t S m a l l E n d ③ RightDistanceSmall=rightChildSmallEnd-ParentSmallEnd③ RightDistanceSmall=rightChildSmallEnd−ParentSmallEnd③
大端法中父节点与左右子节点之间的距离:
L
e
f
t
D
i
s
t
a
n
c
e
B
i
g
=
l
e
f
t
C
h
i
l
d
B
i
g
E
n
d
−
P
a
r
e
n
t
B
i
g
E
n
d
LeftDistanceBig = leftChildBigEnd-ParentBigEnd
LeftDistanceBig=leftChildBigEnd−ParentBigEnd
R i g h t D i s t a n c e B i g = r i g h t C h i l d B i g E n d − P a r e n t B i g E n d ③ RightDistanceBig=rightChildBigEnd-ParentBigEnd③ RightDistanceBig=rightChildBigEnd−ParentBigEnd③
联立上述①、②,①、③方程得
l
e
f
t
C
h
i
l
d
=
2
∗
P
a
r
e
n
t
−
l
e
n
\textcolor{red}{leftChild = 2*Parent-len}
leftChild=2∗Parent−len
r i g h t C h i l d = 2 ∗ P a r e n t − l e n − 1 \textcolor{red}{rightChild = 2*Parent-len-1} rightChild=2∗Parent−len−1