【数据结构】堆排序

一、堆排序的概念

堆排序(Heapsort):利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据,需要注意的是 排升序要建大堆,排降序建小堆。

二、堆排序的实现

我们先创建一个堆排序的函数:

cpp 复制代码
void HeapSort(int arr[], int n);

假设我们要对下列数组来使用堆排序(升序):

cpp 复制代码
int arr[] = {70, 56, 30, 25, 15, 10, 75};

根据我们之前学到的知识,数组是可以直接看为完全二叉树的,所以我们可以把它化为堆。此时我们就可以 "选数" (堆排序本质上是一种选择排序)。

第一步:构建堆

第一步就是要想办法把 arr 数组构建成堆(这里我们先构建成小堆)。我们介绍两种方法,分别为向上调整算法和向下调整算法:

方法1:向上调整

cpp 复制代码
void Swap(HPDataType* px, HPDataType* py) {
    HPDataType tmp = *px;
    *px = *py;
    *py = tmp;
}
/* 小堆的向上调整 */
void AdjustUp(int* arr, int child) {
    assert(arr);
    // 首先根据公式计算算出父亲的下标
    int parent = (child - 1) / 2;
    // 最坏情况:调到根,child=parent 当child为根节点时结束(根节点永远是0)
    while(child > 0) {
        if(arr[child] < arr[parent]) {  // 如果孩子小于父亲(不符合小堆的性质)
            // 交换他们的值
            Swap(&arr[child],&arr[parent]); // 传地址
            // 往上走
            child = parent;
            parent = (child - 1) / 2;
        } else {  // 如果孩子大于父亲(符合小堆的性质)
            // 跳出循环
            break;  
        }
    }
}

方法1:

cpp 复制代码
/* 升序 */
void HeapSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        AdjustUp(arr, i);   // 传入数组 和 child的下标
    }
}

方法2:向下调整

cpp 复制代码
void SmallAjustDown(int* arr, int n, int parent) {
    int child = parent * 2 + 1; // 默认为左孩子
    while(child < n) { // 叶子内
        // 选出左右孩子中小的那一个
        if(child + 1 < n && arr[child + 1] < arr[child]) {
            child = child + 1;
        }
        // 如果孩子小于父亲(不符合小堆的性质)
        if(arr[child] < arr[parent]) {  
            // 交换它们的值
            Swap(&arr[child], &arr[parent]);
            // 往下走
            parent = child;
            child = parent * 2 + 1;
        } else { // 如果孩子大于父亲(符合小堆的性质)
            // 跳出循环
            break;
        }
    }
}

方法2:

cpp 复制代码
/* 升序 */
void HeapSort(int arr[], int n) {
    for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(arr, n, i);
    }
}

也可以这样写:

cpp 复制代码
/* 升序 */
void HeapSort(int arr[], int sz) {
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
}

测试一下:

cpp 复制代码
#include <stdio.h>
 
/* 交换函数 */
void Swap(int* px, int* py) {
    int tmp = *px;
    *px = *py;
    *py = tmp;
}
 
/* 小堆下调 */
void AdjustDown(int arr[], int sz, int father_idx) {
    int child_idx = father_idx * 2 + 1;                                        // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) {                                                   // 最坏情況:调到叶子(child >= 数组范围时必然已经调到叶子)
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] < arr[child_idx])) {   // 如果右孩子存在且右孩子比左孩子小
            child_idx = child_idx + 1;                                         // 让其代表右孩子
        }
        if (arr[child_idx] < arr[father_idx]) {                                // 如果孩子的值小于父亲的值(大符合小堆的性質)
            Swap(&arr[child_idx], &arr[father_idx]);                           // 交换它们的值
            /* 往下走 */
            father_idx = child_idx;                                            // 更新下标
            child_idx = father_idx * 2 + 1;                                    // 计算出该节点路线的新父亲
        } else {                                                               // 如果孩子的值大于父亲的值(符合小堆的性质)
            break;                                                             // 终止循环
        }
    }
}
 
/* 升序 */
void HeapSort(int arr[], int sz) {
	/* 创建大堆,选出最大的数  O(N)  */
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
}
 
void HeapPrint(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
 
int main()
{
    int arr[] = {70, 56, 30, 25, 15, 10, 75, 33, 50, 69};
    int sz = sizeof(arr) / sizeof(arr[0]);
 
    HeapSort(arr, sz);
    HeapPrint(arr, sz);
 
    return 0; 
}

运行结果如下:

第二步:排序

刚才介绍了两种方法来构建堆,现在堆已经构建完毕了,我们可以开始设计排序部分的算法了。

如果排升序,建小堆......

① 选出最小的数,放到第一个位置,这很简单,直接取顶部就可以得到最小的数。

② 但问题来了,如何选出次小的数呢?

建小堆来排升序是完全可以的,但是效率太低!

所以使用大堆来排升序。

我们刚才已经实现好小堆了,根据上一节学到的知识,小堆要变成大堆,直接把刚才的代码的 "<" 改成 ">" 即可:

cpp 复制代码
#include <stdio.h>
 
/* 交换函数 */
void Swap(int* px, int* py) {
    int tmp = *px;
    *px = *py;
    *py = tmp;
}
 
/* 大堆下调 */
void AdjustDown(int arr[], int sz, int father_idx) {
    int child_idx = father_idx * 2 + 1;                                        // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) {                                                   // 最坏情況:调到叶子(child >= 数组范围时必然已经调到叶子)
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] > arr[child_idx])) {   // 如果右孩子存在且右孩子比左孩子大
            child_idx = child_idx + 1;                                         // 让其代表右孩子
        }
        if (arr[child_idx] > arr[father_idx]) {                                // 如果孩子的值大于父亲的值(不符合大堆的性質)
            Swap(&arr[child_idx], &arr[father_idx]);                           // 交换它们的值
            /* 往下走 */
            father_idx = child_idx;                                            // 更新下标
            child_idx = father_idx * 2 + 1;                                    // 计算出该节点路线的新父亲
        } else {                                                               // 如果孩子的值小于父亲的值(符合大堆的性质)
            break;                                                             // 终止循环
        }
    }
}
 
/* 升序 */
void HeapSort(int arr[], int sz) {
	/* 创建大堆,选出最大的数  O(N)  */
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
 
}
 
void PrintArray(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
 
int main()
{
    int arr[] = {70, 56, 30, 25, 15, 10, 75, 33, 50, 69};
    int sz = sizeof(arr) / sizeof(arr[0]);
 
    HeapSort(arr, sz);
    PrintArray(arr, sz);
 
    return 0; 
}

运行结果如下:

现在改成了大堆,我们要排升序,我们可以让堆顶数和最后的数进行交换:

这并不会带来堆结构的破坏!我们把75不看作堆的一部分即可。再进行向下调整,就可以找到次小的数了。

步骤总结:

① 建大堆,选出最大的数。

② 最大的数跟最后一个数交换。

③ 如何选出次大的数呢?把最后一个数不看作堆里面,进行向下调整。

代码实现:

第一种

cpp 复制代码
/* 堆排序 - 升序 */
void HeapSort(int arr[], int sz) {
	/* 创建大堆,选出最大的数  O(N)  */
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
 
	/* 依次选数,调堆   O(N * logN)  */
	int end = sz - 1;
	while (end > 0) {
		Swap(&arr[0], &arr[end]);      // 最大的数跟最后一个数交换
		AdjustDown(arr, end, 0);       // 调堆,选出次大的数
		end--;
	}
}

第二种:

cpp 复制代码
void HeapSort(int arr[], int sz) {
	/* 建堆 */
	for (int father = (sz - 1 - 1) / 2; father >= 0; father--) {
		AdjustDown(arr, sz, father);
	}
	/* 排序 */
	for (int end = sz - 1; end > 0; end--) {
		Swap(&arr[0], &arr[end]);   // 最大的数跟最后一个数交换
		AdjustDown(arr, end, 0);    // 调堆,选出次大的数
	}
}

三、完整代码

升序:使用大堆

cpp 复制代码
#include <stdio.h>
 
void Swap(int* pa, int* pb) {
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
 
void AdjustDown(int arr[], int sz, int father) {
	int child = father * 2 + 1;
	while (child < sz) {
		if (child + 1 < sz && arr[child + 1] > arr[child]) {
			child += 1;
		}
		if (arr[child] > arr[father]) {
			Swap(&arr[child], &arr[father]);
			father = child;
			child = father * 2 + 1;
		}
		else {
			break;
		}
	}
}
 
/* 堆排序 - 升序 */
void HeapSort(int arr[], int sz) {
	/* 创建大堆,选出最大的数  O(N)  */
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
 
	/* 依次选数,调堆   O(N * logN)  */
	int end = sz - 1;
	while (end > 0) {
		Swap(&arr[0], &arr[end]);      // 最大的数跟最后一个数交换
		AdjustDown(arr, end, 0);       // 调堆,选出次大的数
		end--;
	}
}
 
void HeapPrint(int arr[], int sz) {
	int i = 0;
	for (i = 0; i < sz; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");
}
 
int main()
{
	int arr[] = { 70, 56, 30, 25, 15, 10, 75, 33, 50, 69 };
	int sz = sizeof(arr) / sizeof(arr[0]);
 
	printf("排序前: ");
	HeapPrint(arr, sz);
 
	HeapSort(arr, sz);
 
	printf("排序后: ");
	HeapPrint(arr, sz);
 
	return 0;
}

运行结果如下:

降序:使用小堆

cpp 复制代码
#include <stdio.h>
 
/* 交换函数 */
void Swap(int* px, int* py) {
    int tmp = *px;
    *px = *py;
    *py = tmp;
}
 
/* 小堆下调 */
void AdjustDown(int arr[], int sz, int father_idx) {
    int child_idx = father_idx * 2 + 1;                                        // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) {                                                   // 最坏情況:调到叶子(child >= 数组范围时必然已经调到叶子)
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] < arr[child_idx])) {   // 如果右孩子存在且右孩子比左孩子小
            child_idx = child_idx + 1;                                         // 让其代表右孩子
        }
        if (arr[child_idx] < arr[father_idx]) {                                // 如果孩子的值小于父亲的值(不符合小堆的性質)
            Swap(&arr[child_idx], &arr[father_idx]);                           // 交换它们的值
            /* 往下走 */
            father_idx = child_idx;                                            // 更新下标
            child_idx = father_idx * 2 + 1;                                    // 计算出该节点路线的新父亲
        }
        else {                                                               // 如果孩子的值大于父亲的值(符合小堆的性质)
            break;                                                             // 终止循环
        }
    }
}
 
/* 堆排序 - 降序 */
void HeapSort(int arr[], int sz) {
	/* 创建大堆,选出最大的数  O(N)  */
	int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
	while (father >= 0) {
		AdjustDown(arr, sz, father);
		father--;
	}
 
	/* 依次选数,调堆   O(N * logN)  */
	int end = sz - 1;
	while (end > 0) {
		Swap(&arr[0], &arr[end]);      // 最大的数跟最后一个数交换
		AdjustDown(arr, end, 0);   // 调堆,选出次小的数
		end--;
	}
}
void PrintArray(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
 
int main()
{
    int arr[] = { 70, 56, 30, 25, 15, 10, 75, 33, 50, 69 };
    int sz = sizeof(arr) / sizeof(arr[0]);
 
    printf("排序前: ");
    PrintArray(arr, sz);
 
    HeapSort(arr, sz);
 
    printf("排序后: ");
    PrintArray(arr, sz);
 
    return 0;
}

运行结果如下:

相关推荐
sp422 小时前
如何绘制一棵树
数据结构
carver w2 小时前
c++,数据结构,unordermap哈希表基本操作
数据结构·c++·散列表
带鱼吃猫7 小时前
高并发内存池(三):手把手从零搭建ThreadCache线程缓存
数据结构·c++·链表·visual studio
yongui478347 小时前
INTLAB区间工具箱在区间分析算法中的应用与实现
数据结构·算法
wewe_daisy8 小时前
python、数据结构
开发语言·数据结构·python
洲覆14 小时前
C++ constexpr 修饰符与函数
开发语言·数据结构·c++
Tiny番茄17 小时前
146. LRU缓存
数据结构·leetcode·缓存
spiderwiner18 小时前
Part03 数据结构
数据结构·c++·csp
小欣加油20 小时前
leetcode 206 反转链表
数据结构·c++·算法·leetcode·链表·职场和发展