堆排序是一种高效的排序算法,其时间复杂度为 O(n log n),它利用了堆这种数据结构。堆是一种特殊的树形数据结构,满足堆性质:
- 最大堆 (Max-Heap): 父节点的值总是大于或等于其子节点的值。
- 最小堆 (Min-Heap): 父节点的值总是小于或等于其子节点的值。
堆排序的过程可以分为两个阶段:
1. 建堆 (Heapify): 将输入数组构建成一个最大堆(对于从小到大排序)或最小堆(对于从大到小排序)。
2. 堆排序 (Heap Sort): 重复执行以下步骤,直到堆为空:
- 将堆顶元素(最大或最小元素)与堆的最后一个元素交换。
- 将堆的大小减 1。
- 重新调整堆,使其满足堆性质。
从小到大排序 (使用最大堆):
例子: 对数组 [5, 1, 4, 2, 8, 7, 3, 6]
进行从小到大排序。
1. 建堆:
我们从数组的最后一个非叶子节点开始,自底向上调整堆。数组的索引从 0 开始。
- 非叶子节点: 叶子节点的父节点索引为
(i-1)/2
。 非叶子节点的索引范围是[n/2 -1, 0]
,其中n
是数组长度。 - 调整堆: 对于每个非叶子节点,如果它的值小于其子节点中的最大值,则交换它们,然后递归地调整受影响的子树。
(1) 初始数组: [5, 1, 4, 2, 8, 7, 3, 6]
(2) 从索引 (8/2 -1) = 3
开始,调整堆:
- 检查节点 8 (值 3): 没有子节点,无需调整。
- 检查节点 7 (值 6): 子节点 14(值 7) > 6,交换。数组变为 [5, 1, 4, 2, 8, 7, 6, 3]。
- 检查节点 6 (值 7): 子节点 13(值 6), 14(值 3)。7>6, 7>3. 无需调整。
- 检查节点 5 (值 8): 子节点 11(值 7), 12(值 6)。8>7, 8>6. 无需调整。
- 检查节点 4 (值 2): 子节点 9(值 3), 10(值 6)。6 > 2, 交换。数组变为 [5, 1, 4, 6, 8, 7, 2, 3]。然后6>3,交换。数组变为 [5, 1, 4, 6, 8, 7, 3, 2]。
- 检查节点 3 (值 4): 子节点 7(值 3), 8(值 2)。4>3, 4>2。无需调整。
- 检查节点 2 (值 1): 子节点 5(值 6), 6(值 7)。7>1,交换.数组变为[5, 7, 4, 6, 8, 1, 3, 2]。然后7>3,交换。数组变为[5, 7, 4, 6, 8, 3, 1, 2]。
- 检查节点 1 (值 5): 子节点 3(值 7), 4(值 6)。7>5,交换。数组变为[7, 5, 4, 6, 8, 3, 1, 2]。然后7>6,无需交换.
- 检查节点 0 (值 7): 子节点 1(值 5), 2(值 4)。7>5, 7>4。无需调整。
(3) 建堆完成后的数组: [8, 7, 6, 4, 2, 3, 1, 5]
2. 堆排序:
(1) 将堆顶元素 (8) 与堆的最后一个元素 (5) 交换。数组变为 [5, 7, 6, 4, 2, 3, 1, 8]。堆的大小减 1 (现在是 7)。重新调整堆。
(2) 重复步骤 (1),直到堆为空。 这个过程需要反复将堆顶元素与最后一个元素交换,然后调整堆。
最终排序后的数组为 [1, 2, 3, 4, 5, 6, 7, 8]
需要注意的是,手动模拟建堆过程比较繁琐,通常使用编程语言实现。 以下是一个Python代码示例(从小到大排序):
import heapq
def heap_sort(arr):
heapq.heapify(arr) # 建立最小堆,需要修改成最大堆才能从小到大排序
return [heapq.heappop(arr) for _ in range(len(arr))]
arr = [5, 1, 4, 2, 8, 7, 3, 6]
sorted_arr = heap_sort(arr) #最小堆排序,结果为[1,2,3,4,5,6,7,8]
print(sorted_arr)
def heap_sort_max(arr):
n = len(arr)
#构建最大堆
for i in range(n//2 - 1, -1, -1):
heapify_max(arr, n, i)
#堆排序
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] #交换堆顶和最后一个元素
heapify_max(arr, i, 0) #重新调整堆
def heapify_max(arr, n, i):
largest = i
l = 2 * i + 1
r = 2 * i + 2
if l < n and arr[l] > arr[largest]:
largest = l
if r < n and arr[r] > arr[largest]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify_max(arr, n, largest)
arr = [5, 1, 4, 2, 8, 7, 3, 6]
heap_sort_max(arr)
print(arr) #最大堆排序,结果为[8,7,6,5,4,3,2,1]
从大到小排序 (使用最小堆):
让我们用同样的例子 [5, 1, 4, 2, 8, 7, 3, 6]
来演示从大到小排序的过程。
1. 建堆 (构建最小堆):
与构建最大堆类似,我们从数组的最后一个非叶子节点开始,自底向上调整堆,确保每个父节点的值都小于等于其子节点的值。 关键的区别在于比较操作:我们总是选择较小的值作为父节点。
(1) 初始数组: [5, 1, 4, 2, 8, 7, 3, 6]
(2) 从索引 (8/2 - 1) = 3
开始,自底向上调整,确保每个父节点小于等于其子节点。
- 检查节点 3 (值 2): 没有子节点,无需调整。
- 检查节点 2 (值 7): 子节点 5 (值 3), 6 (值 6)。 7 > 3, 7 > 6,需要交换。但这里我们用最小堆,所以无需交换。
- 检查节点 1 (值 1): 子节点 3 (值 2), 4 (值 7)。 1 < 2, 1 < 7, 无需交换。
- 检查节点 0 (值 5): 子节点 1 (值 1), 2 (值 7)。 5 > 1, 需要交换 [1,5,4,2,8,7,3,6]。5<7,无需交换。
… 依次向上调整。 整个建堆过程需要仔细地进行比较和交换,确保每个父节点都小于等于其子节点。
(3) 建堆完成后的数组 (最小堆): (最终结果可能与最大堆不同,因为最小堆的结构不同) 例如,一个可能的最小堆是 [1, 2, 3, 4, 5, 6, 7, 8]
2. 堆排序:
(1) 将堆顶元素 (最小值) 与堆的最后一个元素交换。例如,如果最小堆是 [1, 2, 3, 4, 5, 6, 7, 8]
,交换 1 和 8,得到 [8, 2, 3, 4, 5, 6, 7, 1]
。堆的大小减 1。
(2) 重新调整堆,使其仍然满足最小堆性质。 注意,这次调整堆的目标是保持最小堆,因此比较和交换操作与建堆时一样,选择较小的值作为父节点。
(3) 重复步骤 (1) 和 (2),直到堆为空。 每次堆顶元素(当前最小值)都会被放到数组的末尾。
最终结果应该是从大到小排序的数组 [8, 7, 6, 5, 4, 3, 2, 1]
。
关键区别:
- 建堆: 从大到小排序用最小堆,从小到大排序用最大堆。 比较和交换操作的逻辑相反。
- 堆排序: 从大到小排序每次将堆顶最小元素与末尾元素交换,从小到大排序则将堆顶最大元素与末尾元素交换。 其余步骤类似。
Python 代码 (从大到小排序,使用最小堆):
import heapq
def heap_sort_descending(arr):
heapq.heapify(arr) # 建立最小堆
result = []
for _ in range(len(arr)):
result.append(heapq.heappop(arr))
return result[::-1] # 反转结果
arr = [5, 1, 4, 2, 8, 7, 3, 6]
sorted_arr_descending = heap_sort_descending(arr)
print(sorted_arr_descending) # 输出 [8, 7, 6, 5, 4, 3, 2, 1]
这段代码利用了heapq
模块,它默认构建最小堆。 我们先用heapq.heapify
建立最小堆,然后通过循环弹出最小元素并反转列表来实现从大到小排序。 这是利用最小堆简洁地实现从大到小排序的方法。
如果要避免使用[::-1]
反转列表,可以用自定义函数来构建最小堆并进行排序,类似于之前最大堆的例子,只是比较操作需要调整为小于等于。 这需要更复杂的代码,但更能体现堆排序的原理。
以下分别给出C语言代码实现最大堆排序(从小到大)和最小堆排序(从大到小)。 为了清晰起见,代码中包含了较为详细的注释。
1. 最大堆排序 (从小到大):
#include <stdio.h>
// 函数声明
void heapify(int arr[], int n, int i);
void heapSort(int arr[], int n);
// 建立最大堆
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化最大值为当前节点
int l = 2 * i + 1; // 左子节点索引
int r = 2 * i + 2; // 右子节点索引
// 如果左子节点比当前节点大
if (l < n && arr[l] > arr[largest])
largest = l;
// 如果右子节点比当前节点大
if (r < n && arr[r] > arr[largest])
largest = r;
// 如果最大值不是当前节点,则交换并递归调用heapify
if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
heapify(arr, n, largest);
}
}
// 堆排序
void heapSort(int arr[], int n) {
// 建立最大堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 一个一个地取出堆顶元素
for (int i = n - 1; i > 0; i--) {
// 将堆顶元素(最大值)与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 重新调整堆
heapify(arr, i, 0);
}
}
int main() {
int arr[] = {5, 1, 4, 2, 8, 7, 3, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("未排序数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
heapSort(arr, n);
printf("排序后数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
2. 最小堆排序 (从大到小):
#include <stdio.h>
// 函数声明
void minHeapify(int arr[], int n, int i);
void minHeapSort(int arr[], int n);
// 建立最小堆
void minHeapify(int arr[], int n, int i) {
int smallest = i; // 初始化最小值为当前节点
int l = 2 * i + 1; // 左子节点索引
int r = 2 * i + 2; // 右子节点索引
// 如果左子节点比当前节点小
if (l < n && arr[l] < arr[smallest])
smallest = l;
// 如果右子节点比当前节点小
if (r < n && arr[r] < arr[smallest])
smallest = r;
// 如果最小值不是当前节点,则交换并递归调用minHeapify
if (smallest != i) {
int temp = arr[i];
arr[i] = arr[smallest];
arr[smallest] = temp;
minHeapify(arr, n, smallest);
}
}
// 最小堆排序
void minHeapSort(int arr[], int n) {
// 建立最小堆
for (int i = n / 2 - 1; i >= 0; i--)
minHeapify(arr, n, i);
// 一个一个地取出堆顶元素
for (int i = n - 1; i > 0; i--) {
// 将堆顶元素(最小值)与最后一个元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 重新调整堆
minHeapify(arr, i, 0);
}
}
int main() {
int arr[] = {5, 1, 4, 2, 8, 7, 3, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("未排序数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
minHeapSort(arr, n);
printf("排序后数组:\n");
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
这两个程序的主要区别在于heapify
和minHeapify
函数中的比较操作符:一个是>
(最大堆),另一个是<
(最小堆)。 这直接导致了排序结果的顺序不同。 记住编译时需要一个C编译器 (例如GCC)。 编译并运行即可看到结果。
请注意,minHeapSort
的结果是从大到小排序的。 为了得到从小到大的排序,需要在排序后反转数组。 但是这里我选择直接输出最小堆排序的结果,展现从大到小的排序效果。