导言

堆排序是一种基于二叉堆数据结构的高效排序算法,具有 O (n log n) 的时间复杂度和原地排序的特性。使用模板实现堆排序可以使其灵活适用于各种数据类型,并支持自定义比较规则。

一、堆排序算法原理

堆排序主要分为两个阶段:

  1. 建堆阶段:将无序数组构建成一个二叉堆(最大堆或最小堆)
  2. 排序阶段:反复提取堆顶元素(最大值或最小值),并调整剩余元素维持堆特性

二叉堆是一种完全二叉树,对于最大堆,每个父节点的值大于或等于其子节点的值;对于最小堆,每个父节点的值小于或等于其子节点的值。

二、模板类实现

下面是完整的HeapSort模板类实现,基于提供的框架结构:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <vector>
#include <functional>
#include <iostream>
#include <algorithm>

// 模板类声明,默认使用std::less<T>作为比较器(构建最大堆)
template <typename T, typename Compare = std::less<T>>
class HeapSort
{
public:
// 构造函数:接收数组和大小,初始化容器和比较器
HeapSort(T *arr, size_t size);

// 堆调整函数:维护堆的性质
void heapAdjust(size_t parent, size_t heapSize);

// 排序函数:执行堆排序
void sort();

// 打印排序结果
void print() const;

private:
std::vector<T> _vec; // 存储待排序元素的容器
Compare _cmp; // 比较器对象
};

// 构造函数实现
template <typename T, typename Compare>
HeapSort<T, Compare>::HeapSort(T *arr, size_t size) {
if (arr && size > 0) {
_vec.assign(arr, arr + size);
}
}

// 堆调整函数:确保以parent为根的子树满足堆的性质
template <typename T, typename Compare>
void HeapSort<T, Compare>::heapAdjust(size_t parent, size_t heapSize) {
size_t left = 2 * parent + 1; // 左子节点索引
size_t right = 2 * parent + 2; // 右子节点索引
size_t target = parent; // 记录父节点和子节点中符合堆性质的节点索引

// 比较左子节点与当前节点
if (left < heapSize && _cmp(_vec[target], _vec[left])) {
target = left;
}

// 比较右子节点与当前目标节点
if (right < heapSize && _cmp(_vec[target], _vec[right])) {
target = right;
}

// 如果目标节点不是父节点,则交换并继续调整
if (target != parent) {
std::swap(_vec[parent], _vec[target]);
heapAdjust(target, heapSize); // 递归调整受影响的子树
}
}

// 排序函数:执行堆排序的主要逻辑
template <typename T, typename Compare>
void HeapSort<T, Compare>::sort() {
size_t n = _vec.size();
if (n <= 1) return; // 空数组或单个元素无需排序

// 1. 构建堆:从最后一个非叶子节点开始向上调整
for (int i = n / 2 - 1; i >= 0; --i) {
heapAdjust(i, n);
}

// 2. 排序阶段:逐个提取堆顶元素并调整堆
for (size_t i = n - 1; i > 0; --i) {
// 将当前堆顶元素(最大/最小)交换到数组末尾
std::swap(_vec[0], _vec[i]);

// 调整剩余元素为堆,堆大小减1
heapAdjust(0, i);
}
}

// 打印排序结果
template <typename T, typename Compare>
void HeapSort<T, Compare>::print() const {
for (const auto& elem : _vec) {
std::cout << elem << " ";
}
std::cout << std::endl;
}

三、使用示例

下面展示如何使用HeapSort类对不同数据类型进行排序,包括默认排序(升序)和自定义比较器(降序):

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include "heap_sort.h"
#include <string>

// 测试整数排序
void test_int_sort() {
std::cout << "=== 整数排序测试 ===" << std::endl;
int arr[] = {5, 2, 9, 1, 5, 6};
size_t size = sizeof(arr) / sizeof(arr[0]);

// 默认使用std::less<T>,构建最大堆,最终得到升序结果
HeapSort<int> heap_asc(arr, size);
heap_asc.sort();
std::cout << "升序排序结果: ";
heap_asc.print();

// 使用std::greater<T>,构建最小堆,最终得到降序结果
HeapSort<int, std::greater<int>> heap_desc(arr, size);
heap_desc.sort();
std::cout << "降序排序结果: ";
heap_desc.print();
std::cout << std::endl;
}

// 测试浮点数排序
void test_double_sort() {
std::cout << "=== 浮点数排序测试 ===" << std::endl;
double arr[] = {3.14, 1.41, 2.71, 0.577, 1.618};
size_t size = sizeof(arr) / sizeof(arr[0]);

HeapSort<double> heap(arr, size);
heap.sort();
std::cout << "浮点数排序结果: ";
heap.print();
std::cout << std::endl;
}

// 定义一个结构体用于测试
struct Employee {
std::string name;
int salary;

Employee(std::string n, int s) : name(std::move(n)), salary(s) {}

// 为了打印方便,重载输出运算符
friend std::ostream& operator<<(std::ostream& os, const Employee& e) {
os << "{" << e.name << ", " << e.salary << "}";
return os;
}
};

// 测试结构体排序
void test_struct_sort() {
std::cout << "=== 结构体排序测试 ===" << std::endl;
Employee employees[] = {
{"Alice", 5000},
{"Bob", 3000},
{"Charlie", 7000},
{"David", 4000}
};
size_t size = sizeof(employees) / sizeof(employees[0]);

// 自定义比较器:按工资比较
auto cmp = [](const Employee& a, const Employee& b) {
return a.salary < b.salary;
};

HeapSort<Employee, decltype(cmp)> heap(employees, size);
heap.sort();
std::cout << "按工资升序排序结果: ";
heap.print();
}

int main() {
test_int_sort();
test_double_sort();
test_struct_sort();
return 0;
}

四、代码解析

4.1 模板参数说明

  • typename T:表示要排序的数据类型,可以是基本类型(int、double 等)或自定义类型
  • typename Compare = std::less:比较器类型,默认使用std::less,构建最大堆,最终得到升序结果

4.2 核心函数解析

  1. 构造函数
    • 接收原始数组和大小
    • 使用 vector 存储元素,方便管理和随机访问
  2. heapAdjust 函数
    • 功能:维护堆的性质,确保以指定节点为根的子树是一个有效的堆
    • 实现:比较父节点与左右子节点,找到符合比较器规则的节点作为目标节点,必要时交换并递归调整
  3. sort 函数
    • 建堆阶段:从最后一个非叶子节点开始向上调整,将整个数组构建成一个堆
    • 排序阶段:反复将堆顶元素与当前堆的最后一个元素交换,然后调整剩余元素为新的堆

五、编译与运行

运行结果

1
2
3
4
5
6
7
8
9
=== 整数排序测试 ===
升序排序结果: 1 2 5 5 6 9
降序排序结果: 9 6 5 5 2 1

=== 浮点数排序测试 ===
浮点数排序结果: 0.577 1.41 1.618 2.71 3.14

=== 结构体排序测试 ===
按工资升序排序结果: {Bob, 3000} {David, 4000} {Alice, 5000} {Charlie, 7000}

六、性能分析

  • 时间复杂度
    • 建堆阶段:O (n)
    • 排序阶段:O (n log n)
    • 总体时间复杂度:O (n log n)
  • 空间复杂度:O (1),堆排序是原地排序算法,仅需要常数级的额外空间
  • 稳定性:堆排序是不稳定的排序算法,相等元素的相对位置可能改变

七、优缺点总结

优点

  1. 时间复杂度稳定为 O (n log n),不受输入数据分布影响
  2. 空间复杂度低,是原地排序算法
  3. 适合处理大量数据

缺点

  1. 不稳定排序,不保留相等元素的相对顺序
  2. 缓存友好性较差,相比快速排序局部性更差
  3. 实际应用中通常比快速排序慢