堆及其应用之堆排序

堆排序是一种高效的排序算法,其时间复杂度为 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;
}

这两个程序的主要区别在于heapifyminHeapify函数中的比较操作符:一个是> (最大堆),另一个是< (最小堆)。 这直接导致了排序结果的顺序不同。 记住编译时需要一个C编译器 (例如GCC)。 编译并运行即可看到结果。

请注意,minHeapSort的结果是从大到小排序的。 为了得到从小到大的排序,需要在排序后反转数组。 但是这里我选择直接输出最小堆排序的结果,展现从大到小的排序效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值