用C++实现堆排序,并说明原理
68 2025-01-08 10:08
堆排序是一种利用堆这种数据结构所设计的排序算法,它是基于选择排序的一种改进。堆排序分为两个步骤:建堆和排序。
### 堆排序原理
- **建堆**:首先将待排序的序列构建成一个大顶堆(或小顶堆),此时堆顶元素(数组的第一个元素)是整个序列的最大值(或最小值)。
- **排序**:将堆顶元素与末尾元素交换,然后将剩余的序列重新调整为堆,重复此过程,直到整个序列有序。
### C++实现
下面是一个使用C++实现的堆排序算法的示例代码:
```cpp
#include <iostream>
using namespace std;
// 调整堆,使其满足堆的性质
void adjustHeap(int arr[], int i, int size) {
int temp = arr[i]; // 保存当前元素的值
for (int k = 2 * i + 1; k < size; k = 2 * k + 1) { // 从i节点的左子节点开始
if (k + 1 < size && arr[k] < arr[k + 1]) { // 如果左子节点小于右子节点,k指向右子节点
k++;
}
if (arr[k] > temp) { // 如果子节点大于父节点,将子节点值赋给父节点
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp; // 将temp值放到最终的位置
}
// 建立堆
void buildHeap(int arr[], int size) {
for (int i = size / 2 - 1; i >= 0; i--) { // 从最后一个非叶子节点开始调整
adjustHeap(arr, i, size);
}
}
// 堆排序
void heapSort(int arr[], int size) {
buildHeap(arr, size); // 建立大顶堆
for (int i = size - 1; i > 0; i--) {
swap(arr[i], arr[0]); // 将堆顶元素与最后一个元素交换
adjustHeap(arr, 0, i); // 重新调整堆
}
}
int main() {
int arr[] = {4, 6, 8, 5, 9};
int size = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
heapSort(arr, size);
cout << "Sorted array: ";
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
```
### 代码说明
- `adjustHeap` 函数用于调整堆,使其满足堆的性质。它从指定的节点开始,向下调整,直到满足堆的性质。
- `buildHeap` 函数用于建立初始的堆。它从最后一个非叶子节点开始,依次向上调整每个节点。
- `heapSort` 函数是堆排序的主函数,它首先建立初始堆,然后通过交换堆顶元素和最后一个元素,并重新调整堆,直到整个数组有序。
### 时间复杂度
- 建堆的时间复杂度是 \(O(n)\)。
- 排序的时间复杂度是 \(O(n \log n)\)。
- 因此,堆排序的总时间复杂度是 \(O(n \log n)\)。
堆排序是一种不稳定的排序算法,但它具有较好的时间复杂度和空间复杂度(空间复杂度为 \(O(1)\)),因此在很多实际应用中被广泛使用.
将 `adjustHeap` 函数从迭代形式改写为递归形式,主要思路是将循环逻辑转换为递归调用。递归版本的核心在于每次处理一个节点后,递归地调整其子节点。以下是递归版本的实现:
```cpp
void adjustHeapRecursive(int arr[], int i, int size) {
int left = 2 * i + 1; // 左子节点的索引
int right = 2 * i + 2; // 右子节点的索引
int largest = i; // 假设当前节点是最大的
// 如果左子节点存在且大于当前节点,则更新 largest
if (left < size && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点存在且大于当前最大节点,则更新 largest
if (right < size && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大的节点不是当前节点,交换它们并递归调整
if (largest != i) {
swap(arr[i], arr[largest]);
adjustHeapRecursive(arr, largest, size); // 递归调整交换后的子树
}
}
```
### 代码解释
- **参数说明**:
- `arr[]`:待调整的数组。
- `i`:当前需要调整的节点的索引。
- `size`:数组的大小。
- **逻辑流程**:
1. **计算子节点索引**:计算当前节点的左子节点和右子节点的索引。
2. **确定最大节点**:比较当前节点、左子节点和右子节点,确定最大值的索引。
3. **交换节点**:如果最大值的索引不是当前节点的索引,说明需要调整,将当前节点与最大值节点交换。
4. **递归调整**:交换后,递归地调整交换后的子树,以确保子树仍然满足堆的性质.
### 使用递归的优势
- **代码简洁**:递归版本的代码通常比迭代版本更简洁,逻辑更直观。
- **易于理解**:对于熟悉递归的程序员来说,递归版本的代码更容易理解和调试。
### 注意事项
- **递归深度**:对于非常大的数组,递归可能会导致栈溢出。因此,在实际应用中,对于大规模数据,迭代版本可能更安全.
- **性能开销**:递归调用会增加函数调用的开销,因此在性能要求较高的场景中,迭代版本可能更优.
递归版本的 `adjustHeap` 函数在逻辑上与迭代版本等价,但在实现方式上更加直观和简洁.
给出堆排序算法的python实现
堆排序是一种利用堆这种数据结构所设计的排序算法。它是一种选择排序,分为两个步骤:建立堆和堆调整。
以下是堆排序算法的Python实现:
```python
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
# 建立最大堆
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# 一个一个从堆顶取出元素
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(arr, i, 0)
# 示例
arr = [12, 11, 13, 5, 6, 7]
heap_sort(arr)
print("排序后的数组是:", arr)
```
在这个实现中,`heapify` 函数用于维护堆的性质,`heap_sort` 函数则用于执行整个堆排序过程。首先,通过从最后一个非叶子节点开始向上调整,建立一个最大堆。然后,将堆顶元素(最大值)与最后一个元素交换,并调整剩余的堆,直到整个数组被排序。
在堆排序算法中,`k = 2 * k + 1` 这个表达式用于在调整堆的过程中找到当前节点的左子节点。具体来说,这个表达式的作用是遍历当前节点的所有子节点,以确保堆的性质得到满足。以下是详细的解释:
### 堆的结构
- **堆**是一种完全二叉树,通常用数组来表示。对于数组中的任意节点 `i`:
- 左子节点的索引为 `2 * i + 1`。
- 右子节点的索引为 `2 * i + 2`。
### 调整堆的目的
- 在调整堆的过程中,我们需要确保当前节点(父节点)大于其子节点(对于大顶堆)。如果当前节点小于其子节点,我们需要将当前节点与子节点中最大的一个交换,然后继续调整交换后的子节点,以确保整个子树满足堆的性质。
### `k = 2 * k + 1` 的作用
- **初始设置**:在循环开始时,`k` 被设置为 `2 * i + 1`,即当前节点的左子节点的索引。
- **循环条件**:只要 `k < size`,说明当前节点还有子节点需要检查。
- **更新 `k`**:在循环中,如果当前节点的左子节点小于右子节点,`k` 会被更新为右子节点的索引。如果需要交换当前节点和子节点,`k` 会被更新为 `2 * k + 1`,即交换后的新节点的左子节点的索引。这样可以继续检查和调整新的子节点,确保整个子树满足堆的性质。
### 代码逻辑
- 在 `adjustHeap` 函数中,`k = 2 * k + 1` 是用来在交换节点后,继续向下调整堆的。如果当前节点与子节点交换后,新的子节点可能仍然不满足堆的性质,因此需要继续向下调整,直到整个子树都满足堆的性质。
通过这种方式,`adjustHeap` 函数能够有效地将当前节点下沉到合适的位置,确保堆的性质在整个数组中得到满足。
为什么build堆的时候要从最后一个非叶子节点开始调整
在构建堆的过程中,从最后一个非叶子节点开始调整的原因主要有以下几点:
### 1. **叶子节点的性质**
- **叶子节点的定义**:在完全二叉树中,叶子节点是没有子节点的节点。对于一个数组表示的完全二叉树,叶子节点自然满足堆的性质,因为它们没有子节点可以违反堆的性质。
- **无需调整**:由于叶子节点没有子节点,它们天然满足堆的性质(即每个节点都大于或等于其子节点)。因此,不需要对叶子节点进行任何调整。
### 2. **从下到上的调整策略**
- **自下而上的调整**:从最后一个非叶子节点开始调整,可以确保在调整每个节点时,其子树已经满足堆的性质。这样可以避免重复调整子树。
- **逐步修复**:从最后一个非叶子节点开始,逐步向上调整,可以看作是一个“从下往上”的修复过程。这种方法确保了在调整当前节点时,其子树已经是一个合法的堆。
### 3. **效率和时间复杂度**
- **时间复杂度优化**:虽然每次调整操作的时间复杂度为 \(O(\log n)\),但由于从最后一个非叶子节点开始调整,整体的时间复杂度为 \(O(n)\)。这是因为每个节点的调整次数与其在树中的深度有关,而树的深度逐层递减。
- **避免重复工作**:如果从根节点开始调整,可能会导致对已经满足堆性质的子树进行不必要的调整,从而增加计算量。
### 4. **逻辑上的合理性**
- **确保子树合法性**:在调整当前节点时,确保其子树已经是一个合法的堆,可以简化调整过程。如果子树不满足堆的性质,调整过程会变得复杂。
- **逐步构建合法堆**:从最后一个非叶子节点开始,逐步向上构建合法的堆,可以确保在每一步中,当前节点及其子树都满足堆的性质。
综上所述,从最后一个非叶子节点开始调整堆是一种高效且合理的策略,能够确保在构建过程中逐步修复和构建合法的堆结构,从而提高整个建堆过程的效率和正确性.
全部评论