建堆的时间复杂度为什么是O(nlogn)

一、证明建堆的时间复杂度为O(nlogn)

对于堆的构建,我们需要经过heapify调整操作。我们把构建一个大小为N的堆中所有非终端节点调整的时间称为一次heapify的执行时间,则对于一次heapify的操作,时间复杂度为log(n)。

假设我们已经完成了对整个堆的所有非叶子节点的heapify操作,我们仍然需要对底层叶子节点进行排列。每个叶子节点heapify的时间复杂度是0,由于有N个叶子节点,所以对于所有堆操作的时间复杂度为O(NlogN)。

二、建堆时间复杂度

建堆的时间复杂度也就是上面所说的所有堆操作的时间复杂度,即O(NlogN)。

def heapify(arr, n, i):
  largest = i
  l = 2 * i + 1
  r = 2 * i + 2
  if l < n and arr[i] < arr[l]:
    largest = l
  if r < n and arr[largest] < arr[r]:
    largest = r
  if largest != i:
    arr[i], arr[largest] = arr[largest], arr[i]
    heapify(arr, n, largest)

def build_heap(arr):
  n = len(arr)
  for i in range(n, -1, -1):
    heapify(arr, n, i)

三、初始建堆的时间复杂度

初始建堆的时间复杂度同样是O(NlogN)。因为我们需要对堆中的每个元素进行调整heapify操作,对每个元素而言,最坏情况下需要调整的层数是logN。

我们可以利用heapify操作和完全二叉树的性质加速堆的构建过程。直接将一个无序序列构建成堆的时间复杂度是O(NlogN),但是如果我们从第N/2个节点(该节点的父节点为N/4,满足所有父节点大于其子节点)开始heapify调整,由于完全二叉树节点大部分都是叶子节点,所以这些节点heapify操作后不会影响其他的节点。因此,如果我们从下往上调整,整个维护堆时间复杂度就降为了O(N)。

def heapify(arr, n, i):
  largest = i
  l = 2 * i + 1
  r = 2 * i + 2
  if l < n and arr[i] < arr[l]:
    largest = l
  if r < n and arr[largest] < arr[r]:
    largest = r
  if largest != i:
    arr[i], arr[largest] = arr[largest], arr[i]
    heapify(arr, n, largest)

def build_heap(arr):
  n = len(arr)
  for i in range(n // 2 - 1, -1, -1):
    heapify(arr, n, i)

四、归并排序的时间复杂度

归并排序时间复杂度为O(nlogn)。它使用了分治思想:将一个大问题分为一个或多个更小的子问题,直到子问题可以使用直接计算的方式求解,然后将所有子问题的解合并起来得到大问题的解。

归并排序的核心思想是将一组未排序的元素划分为越来越小的子集,再将各个子集排序并合并成较大的有序集合,直到最后只剩下一个排序完成的集合。

def merge_sort(arr):
  if len(arr) > 1:
    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]
    merge_sort(left)
    merge_sort(right)
    i = j = k = 0
    while i < len(left) and j < len(right):
      if left[i] < right[j]:
        arr[k] = left[i]
        i += 1
      else:
        arr[k] = right[j]
        j += 1
      k += 1
    while i < len(left):
      arr[k] = left[i]
      i += 1
      k += 1
    while j < len(right):
      arr[k] = right[j]
      j += 1
      k += 1

五、快速排序的时间复杂度

快速排序时间复杂度在最坏情况下为O(n^2);在平均情况下表现良好,为O(nlogn)。

快速排序的核心思想是选择一个元素作为pivot,将整个数组划分成左右两个子数组,使得左边的元素都小于pivot,右边的元素都大于pivot,然后对左右子数组分别进行排序。

在最坏情况下,pivot每次都是数组中最大或最小的元素,并且每次都只划分出一个子数组,快速排序就会变成冒泡排序,时间复杂度为O(n^2)。

def quick_sort(arr, left, right):
  if left < right:
    pivot_index = partition(arr, left, right)
    quick_sort(arr, left, pivot_index - 1)
    quick_sort(arr, pivot_index + 1, right)

def partition(arr, left, right):
  pivot = arr[right]
  i = left - 1
  for j in range(left, right):
    if arr[j] < pivot:
      i += 1
      arr[i], arr[j] = arr[j], arr[i]
  arr[i + 1], arr[right] = arr[right], arr[i + 1]
  return i + 1

六、堆的复杂度

堆数据结构的操作复杂度如下:

  • 建堆的时间复杂度为O(n),这是通过下面提到的bottom-up heap construction实现的。
  • 添加元素的时间复杂度为O(logn),因为我们只需要添加到堆的最后,然后逐步向上调整元素。
  • 获取最大(小)值并删除的时间复杂度为O(logn),由于我们需要从堆顶获取最大/小值,然后将堆的最后一个元素移动到堆顶并进行heapify操作。
  • 获取最大(小)值但是不删除的时间复杂度为O(1),我们只需要返回堆的根节点。

七、堆排序的时间复杂度

堆排序时间复杂度为O(nlogn)。

使用堆排序的基本思路是通过堆数据结构构建一个最大堆/最小堆,然后将堆的根节点与堆的最后一个节点交换,取出堆的最后一个节点,对新的堆进行heapify操作,直到堆为空,所有节点都已经进行了排序。

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[0], arr[i] = arr[i], arr[0]
    heapify(arr, i, 0)

八、数组建堆时间复杂度

由于数组不像二叉堆有左右孩子节点的关系,所以它需要其他方法来实现建堆,使用数组实现堆的时间复杂度为O(N)。

数组建堆的思路是对于任意结点i,如果它有左右孩子节点,那么孩子节点比结点i的编号都要大,这样我们就可以通过一个线性数据结构解决类似二叉树结构的问题。

def heapify(arr, n, i):
  largest = i
  l = 2 * i + 1
  r = 2 * i + 2
  if l < n and arr[i] < arr[l]:
    largest = l
  if r < n and arr[largest] < arr[r]:
    largest = r
  if largest != i:
    arr[i], arr[largest] = arr[largest], arr[i]
    heapify(arr, n, largest)

def build_heap(arr):
  n = len(arr)
  for i in range(n // 2 - 1, -1, -1):
    heapify(arr, n, i)

九、最小堆构建时间复杂度

和最大堆一样,最小堆的构建时间复杂度同样为O(NlogN)。

最小堆是指根节点的键值最小。由于我们需要排序所有元素并构建二叉堆,所以时间复杂度与最大堆构建相同。

十、最大堆时间复杂度

最大堆的构建时间复杂度为O(NlogN)。

和最小堆类似,最大堆的构建时间复杂度也与元素总数相关。在堆的构建过程中,维护堆的时间复杂度为O(NlogN)。

结束语

建堆的时间复杂度很大程度上决定了堆排序的时间复杂度,堆排序是一种基于比较的排序算法。由于它使用了堆数据结构,可以实现O(nlogn)的时间复杂度,堆排序的空间复杂度是O(1)。

除了堆排序,堆数据结构还有许多常见的应用。例如,求最大k个数、实现优先队列等。

原创文章,作者:小蓝,如若转载,请注明出处:https://www.506064.com/n/286521.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
小蓝小蓝
上一篇 2024-12-23 03:47
下一篇 2024-12-23 03:48

相关推荐

  • 解决docker-compose 容器时间和服务器时间不同步问题

    docker-compose是一种工具,能够让您使用YAML文件来定义和运行多个容器。然而,有时候容器的时间与服务器时间不同步,导致一些不必要的错误和麻烦。以下是解决方法的详细介绍…

    编程 2025-04-29
  • 想把你和时间藏起来

    如果你觉得时间过得太快,每天都过得太匆忙,那么你是否曾经想过想把时间藏起来,慢慢享受每一个瞬间?在这篇文章中,我们将会从多个方面,详细地阐述如何想把你和时间藏起来。 一、一些时间管…

    编程 2025-04-28
  • 计算斐波那契数列的时间复杂度解析

    斐波那契数列是一个数列,其中每个数都是前两个数的和,第一个数和第二个数都是1。斐波那契数列的前几项为:1,1,2,3,5,8,13,21,34,…。计算斐波那契数列常用…

    编程 2025-04-28
  • 时间戳秒级可以用int吗

    时间戳是指从某个固定的时间点开始计算的已经过去的时间。在计算机领域,时间戳通常使用秒级或毫秒级来表示。在实际使用中,我们经常会遇到需要将时间戳转换为整数类型的情况。那么,时间戳秒级…

    编程 2025-04-28
  • 如何在ACM竞赛中优化开发时间

    ACM竞赛旨在提高程序员的算法能力和解决问题的实力,然而在比赛中优化开发时间同样至关重要。 一、规划赛前准备 1、提前熟悉比赛规则和题目类型,了解常见算法、数据结构和快速编写代码的…

    编程 2025-04-28
  • 使用JavaScript日期函数掌握时间

    在本文中,我们将深入探讨JavaScript日期函数,并且从多个视角介绍其应用方法和重要性。 一、日期的基本表示与获取 在JavaScript中,使用Date对象来表示日期和时间,…

    编程 2025-04-28
  • Java Date时间大小比较

    本文将从多个角度详细阐述Java中Date时间大小的比较,包含了时间字符串转换、日期相减、使用Calendar比较、使用compareTo方法比较等多个方面。相信这篇文章能够对你解…

    编程 2025-04-27
  • 从时间复杂度角度看循环赛日程表

    循环赛日程表是指在一个比赛中,每个参赛者都需要与其他所有参赛者逐一比赛一次,而且每个参赛者可以在同一场比赛中和其他参赛者比赛多次,比如足球、篮球等。循环赛日程表的设计需要考虑时间复…

    编程 2025-04-27
  • 二分查找时间复杂度为什么是logN – 知乎

    二分查找是一种常用的查找算法。它通过将目标值与数组的中间元素进行比较,从而将查找范围缩小一半,直到找到目标值。这种方法的时间复杂度为O(logN)。下面我们将从多个方面探讨为什么二…

    编程 2025-04-27
  • One change 时间:简化项目开发的最佳实践

    本文将介绍 One change 时间 (OCT) 的定义和实现方法,并探讨它如何简化项目开发。OCT 是一种项目开发和管理的策略,通过将更改限制在固定的时间间隔(通常为一周)内,…

    编程 2025-04-27

发表回复

登录后才能评论