返回

揭开堆排序的序幕:算法领域的又一力作

Android

在音视频开发中,排序算法扮演着至关重要的角色,它们直接影响着音视频数据的处理效率和最终呈现效果。继冒泡排序和快速排序之后,今天我们将一起探索另一种常用的排序算法——堆排序。堆排序,顾名思义,就是利用“堆”这种特殊的数据结构来实现排序。

什么是堆?

在理解堆排序之前,我们先来认识一下“堆”。堆本质上是一种特殊的二叉树,它需要满足两个条件:

  1. 完全二叉树: 除了最后一层,其他层的节点都必须是满的,最后一层的节点也尽量靠左排列。
  2. 父节点的值大于等于(或小于等于)子节点的值: 如果父节点的值都大于等于子节点的值,我们称之为“大顶堆”;反之,如果父节点的值都小于等于子节点的值,我们称之为“小顶堆”。

堆排序的原理

堆排序的基本思路可以概括为“建堆”和“排序”两个步骤:

1. 建堆: 首先,我们需要将待排序的数组构建成一个堆(通常是大顶堆)。构建堆的过程可以理解为将数组元素逐个插入到堆中,并不断调整堆的结构,使其满足堆的两个条件。

2. 排序: 堆建好之后,堆顶元素就是数组中的最大值(如果是大顶堆)。我们将堆顶元素与数组的最后一个元素交换位置,然后将堆的大小减一(相当于把最大值从堆中移除)。接下来,我们需要重新调整堆的结构,使其仍然满足堆的条件。重复这个过程,直到堆的大小为1,此时数组就已经排好序了。

代码实现

下面我们用 Python 代码来实现一个大顶堆排序:

def build_max_heap(arr):
  """构建大顶堆"""
  n = len(arr)
  for i in range(n // 2 - 1, -1, -1):
    heapify(arr, n, i)

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)
  build_max_heap(arr)  # 构建大顶堆

  # 逐个取出堆顶元素(最大值)放到数组末尾
  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)

堆排序的优缺点

优点:

  • 时间复杂度稳定: 堆排序的时间复杂度始终为 O(n log n),无论输入数据是什么样的。
  • 原地排序: 堆排序只需要少量额外的空间,可以认为是原地排序。

缺点:

  • 缓存不友好: 堆排序的访问方式不是顺序的,可能会导致缓存命中率降低,影响性能。
  • 不稳定排序: 堆排序可能会改变相同元素的相对位置,因此它是不稳定排序。

常见问题解答

1. 堆排序和快速排序哪个效率更高?

在平均情况下,快速排序的效率通常比堆排序略高。但在最坏情况下,快速排序的时间复杂度会退化到 O(n^2),而堆排序的时间复杂度始终为 O(n log n)。

2. 堆排序适合哪些应用场景?

堆排序适合对稳定性要求不高,但对时间复杂度有严格要求的场景,例如优先队列的实现、Top K 问题等。

3. 如何构建一个小顶堆?

构建小顶堆的思路与构建大顶堆类似,只需要将 heapify 函数中的比较条件反过来即可。

4. 堆排序的空间复杂度是多少?

堆排序的空间复杂度为 O(1),因为它只需要少量额外的空间用于存储临时变量。

5. 堆排序的稳定性如何?

堆排序是不稳定排序,因为它可能会改变相同元素的相对位置。