建堆的時間複雜度為什麼是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/zh-hant/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

發表回復

登錄後才能評論