堆排序算法(附Python和C++代码)

什么是堆排序算法?

堆排序是最流行且最快的排序算法之一。它基于完全二叉树数据结构。我们将寻找最大元素并将其放在最大堆的顶部。我们将将其放置在二叉树的父节点上。

假设给出一个数组,data = [10,5, 7, 9, 4, 11, 45, 17, 60]

在数组中,如果 i 索引(i=0,1,2,3 …)是父节点,那么 (2i+1) 和 (2i+2) 将是左子节点和右子节点。用这个数组创建一个完全二叉树会像这样

Heap Sort Algorithm

我们将从数组的开头到结尾进行堆化过程。最初,如果我们把数组转换成一棵树,它看起来就像上面那样。我们可以看到它没有维护任何堆属性(最小堆或最大堆)。通过对所有节点进行堆化过程,我们将获得排序后的数组。

堆排序的应用

以下是堆排序算法的一些用法

  • 构建“优先队列”需要堆排序。因为堆排序在每次插入后都会保持元素有序。
  • 堆数据结构在查找给定数组中第 k 个最大元素方面非常高效。
  • Linux 内核将堆排序用作默认的排序算法,因为它具有 O(1) 的空间复杂度。

创建堆排序及示例

在这里,我们将从以下完全二叉树构建一个最大堆。

Create Heap Sort with Example

叶子节点是 17、60、4、11 和 45。它们没有任何子节点。这就是为什么它们是叶子节点。因此,我们将从它们的父节点开始堆化方法。以下是步骤

步骤 1) 选择最左边的子树。如果子节点更大,则将父节点与子节点交换。

这里的父节点是 9。子节点是 17 和 60。因为 60 是最大的,所以 60 和 9 将被交换以维护最大堆

Create Heap Sort with Example

步骤 2) 现在,最左边的子树已堆化。下一个父节点是 7。这个父节点有两个子节点,最大的节点是 45。因此,45 和 7 将被交换。

Create Heap Sort with Example

Create Heap Sort with Example

步骤 3) 节点 60 和 4 的父节点是 5。因为“5”小于子节点 60,所以它将被交换。

Create Heap Sort with Example

Create Heap Sort with Example

步骤 4) 现在,节点 5 有子节点 17,9。这不符合最大堆属性。所以,5 将被 17 替换。

Create Heap Sort with Example

步骤 5) 节点 10 将与 60 交换,然后与 17 交换。过程将如下所示。

Create Heap Sort with Example

Create Heap Sort with Example

步骤 6) 到步骤 5 为止,我们创建了最大堆。每个父节点都比其子节点大。根节点具有最大值(60)。

注意:要创建排序后的数组,我们需要将最大值节点替换为其后继节点。

这个过程称为“提取最大值”。因为 60 是最大节点,我们将把它的位置固定在第 0 个索引,并创建一个不包含节点 60 的堆。

Create Heap Sort with Example

Create Heap Sort with Example

步骤 7) 由于 60 被移除,下一个最大值是 45。我们将从节点 45 再次执行“提取最大值”过程。

这次我们将得到 45 并用其后继节点 17 替换根节点。

我们需要执行“提取最大值”直到所有元素都被排序。

执行这些步骤直到提取所有最大值后,我们将得到以下数组。

Create Heap Sort with Example

什么是二叉堆?

二叉堆是一种完全二叉树数据结构。在这种树结构中,父节点要么小于子节点,要么大于子节点。如果父节点较小,则堆称为“最小堆”;如果父节点较大,则堆称为“最大堆”。

以下是最小堆和最大堆的示例。

Min Heap and Max Heap
最小堆和最大堆

在上图中,如果您注意到“最小堆”,父节点总是小于其子节点。在树的顶部,我们可以找到最小值 10。

同样,对于“最大堆”,父节点总是大于子节点。最大元素存在于“最大堆”的根节点。

什么是“堆化”?

“堆化”是堆的原则,它确保了节点的位置。在堆化中,最大堆始终保持父子关系,即父节点大于子节点。

例如,如果添加了一个新节点,我们需要重新调整堆的形状。但是,我们可能需要更改或交换节点或重新排列数组。这个重新调整堆的过程称为“堆化”。

以下是一个堆化工作原理的示例

Adding a New Node and Heapify
添加新节点并进行堆化

以下是堆化的步骤

步骤 1) 将节点 65 添加为节点 60 的右子节点。

步骤 2) 检查新添加的节点是否大于其父节点。

步骤 3) 因为它大于父节点,所以我们将右子节点与其父节点交换。

如何构建堆

在构建堆或堆化树之前,我们需要知道如何存储它。由于堆是完全二叉树,因此最好使用数组来存储堆的数据。

假设一个数组总共包含 n 个元素。如果“i”索引是父节点,那么左子节点将在索引 **(2i+1)**,右子节点将在索引 **(2i+2)**。我们假设数组索引从 0 开始。

使用此,让我们将最大堆存储到类似下面的数组中

Array-Based Representation of the Max Heap
基于数组的最大堆表示

堆化算法维护堆属性。如果父节点不具有极端值(最小或最大),它将被与最极端的子节点交换。

以下是堆化最大堆的步骤

步骤 1) 从叶子节点开始。

步骤 2) 找到父节点和子节点之间的最大值。

步骤 3) 如果子节点的值大于父节点,则交换节点。

步骤 4) 向上移动一级。

步骤 5) 重复步骤 2、3、4,直到到达索引 0 或排序整个树。

以下是递归堆化的伪代码(最大堆)

def heapify():
  input→ array, size, i
  largest = i
  left = 2*i + 1
  right = 2*i + 2
if left<n and array[largest ] < array[left]:
  largest = left
if right<n and array[largest ] < array[right]:
  largest = right
If largest not equals i:
  swap(array[i],array[largest])
  heapify(array,n,largest)

堆排序伪代码

以下是堆排序算法的伪代码

Heapify(numbers as an array, n as integer, i as integer):
  largest = i
  left = 2i+1
  right= 2i+2
if(left<=n) and (numbers[i]<numbers[left])
  largest=left
if(right<=n) and (numbers[i]<numbers[right])
  largest=right
if(largest  != i)
  swap(numbers[i], numbers[largest])
  Heapify(numbers,n,largest)
HeapSort(numbers as an array):
  n= numbers.size()
for i in range n/2 to 1
  Heapify(numbers,n,i)
for i in range n to 2
  Swap numbers[i] with numbers[1]
  Heapify(numbers,i,0)

C++ 堆排序代码示例

#include <iostream>
using namespace std;
void display(int arr[], int n)
{
    for (int i = 0; i < n; i++)
    {
        cout << arr[i] << "\t";
    }
    cout << endl;
}
void heapify(int numbers[], int n, int i)
{
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    if (left < n && numbers[left] < numbers[largest])
    {
        largest = left;
    }
    if (right < n && numbers[right] < numbers[largest])
    {
        largest = right;
    }
    if (largest != i)
    {
	//uncomment the following line to see details in output
        //cout<<"Swapping "<< numbers[i]<< " and "<<numbers[largest]<<endl;
        swap(numbers[i], numbers[largest]);
        heapify(numbers, n, largest);
    }
}
void heapSort(int numbers[], int n)
{
    for (int i = n/2 - 1; i >= 0; i--)
    {
        heapify(numbers, n, i);
//uncomment the following line to see details in output
 //cout<<"Heapify:\t";
  //display(numbers,n);
    }
    for (int i = n - 1; i >= 0; i--)
    {
        swap(numbers[0], numbers[i]);
        heapify(numbers, i, 0);
    }
}
int main()
{
    int numbers[] = { 10,5, 7, 9, 4, 11, 45, 17, 60};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    cout<<"Initial Array:\t";
    display(numbers,size);
    heapSort(numbers, size);
    cout<<"Sorted Array (descending order):\t";
    display(numbers, size);
}

输出

Initial Array:  10      5       7       9       4       11      45      17      60
Sorted Array (descending order):  60      45      17      11      10      9       7       5       4

Python 堆排序代码示例

def display(arr):
    for i in range(len(arr)):
    print(arr[i], end = "\t")
print()
def heapify(numbers, n, i):
    largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and numbers[left] < numbers[largest]:
    largest = left
if right < n and numbers[right] < numbers[largest]:
    largest = right
if largest != i:
    numbers[i], numbers[largest] = numbers[largest], numbers[i]
heapify(numbers, n, largest)
def heapSort(items, n):
    for i in range(n //2,-1,-1):
        heapify(items, n, i) for i in range(n - 1, -1, -1):
        items[0], items[i] = items[i], items[0] heapify(items, i, 0) numbers = [10, 5, 7, 9, 4, 11, 45, 17, 60] print("Initial List:\t", end = "") display(numbers) print("After HeapSort:\t", end = "") heapSort(numbers, len(numbers)) display(numbers)

输出

Initial List:   10      5       7       9       4       11      45      17      60
After HeapSort: 60      45      17      11      10      9       7       5       4

堆排序的时间和空间复杂度分析

对于堆排序,我们可以分析时间复杂度和空间复杂度。对于时间复杂度,我们有以下情况

  1. 最佳情况
  2. 平均情况
  3. 最坏情况

堆是在完全二叉树上实现的。因此,在二叉树的底层将有最多的节点。如果底层有 n 个节点,则上一层将有 n/2 个节点。

Time and Space Complexity Analysis

在这个例子中,第 3 层有四个项目,第 2 层有两个项目,第 1 层有一个项目。如果总共有 n 个项目,则高度或总层数将是 **Log2(n)**。因此,插入单个元素最多可能需要 Log(n) 次迭代。

当我们想从堆中获取最大值时,只需获取根节点。然后再次运行堆化。每次堆化需要 **Log2(n)** 时间。提取最大值需要 O(1) 时间。

堆排序算法的最佳情况时间复杂度

当数组中的所有元素都已排序时,构建堆需要 O(n) 时间。因为如果列表已排序,则插入一个项将花费常量时间 O(1)。

因此,在最佳情况下,创建最大堆或最小堆需要 O(n) 时间。

堆排序算法的平均情况时间复杂度

插入一个项或提取最大值需要 O(log(n)) 时间。因此,堆排序算法的平均情况时间复杂度为 **O(n log(n))**。

堆排序算法的最坏情况时间复杂度

与平均情况类似,在最坏的情况下,我们可能需要执行 n 次堆化。每次堆化需要 O(log(n)) 时间。因此,最坏情况时间复杂度为 **O(n log(n))**。

堆排序算法的空间复杂度

堆排序是一种原地设计算法。这意味着执行任务不需要额外的或临时的内存。如果我们查看实现,我们会注意到我们使用了 swap() 来执行节点交换。没有需要其他的列表或数组。因此,空间复杂度为 O(1)。