數據結構:創建堆(或者叫“堆化”,Heapify)

目錄

最直觀的思路

更優化的思路(自底向上的構建)

第一步:重新審視問題

第二步:找到規律,形成策略

用一個實例來推演

第三步:編寫代碼

總結與分析


我們來深入探討“創建堆”(或者叫“堆化”,Heapify)的過程。這是堆結構中一個非常核心且巧妙的操作。

我們的任務:給定一個包含任意數據的普通數組,如何最高效地將它原地轉換成一個合法的堆?

例如,給定數組 arr = [3, 5, 80, 100, 70, 60],我們需要將它重新排列,使其滿足大頂堆的屬性。

同樣,我們從第一性原理出發,探索解決之道。


最直觀的思路

我們已經掌握了向堆中 insert 一個新元素的方法。一個非常自然的想法是:

  1. 假設我們有一個空的堆。

  2. 然后我們遍歷給定的數組,把數組中的每一個元素,依次 insert 到這個新堆中。

  3. 當所有元素都插入完畢后,我們不就得到了一個合法的堆嗎?

?這個思路是完全正確的,它利用了我們已有的“工具”(insert 函數)。

數據結構:在堆中插入元素(Inserting In a Heap)-CSDN博客

我們知道,每次 insert 操作的核心是“上浮”(Sift Up),其時間復雜度是 O(logk),其中 k 是堆中當時的元素數量。

推演過程:

  • 插入第1個元素:代價是 O(log1)

  • 插入第2個元素:代價是 O(log2)

  • ...

  • 插入第 N 個元素:代價是 O(logN)

把所有這些代價加起來,總的時間復雜度近似為 O(NlogN)。

📍小結一下方法一:

  • 優點:思路簡單,容易理解,直接復用了 insert 操作。

  • 缺點:效率不是最優的。O(NlogN) 雖然不錯,但我們不禁要問:有沒有可能更快?

這個問題,促使我們去尋找一個更根本、更高效的方法。


更優化的思路(自底向上的構建)

第一步:重新審視問題

我們手里的數組,本身就可以被看作一棵“完全二叉樹”,只不過它的節點值不滿足“堆序屬性”。 我們的目標就是修復這個屬性。

讓我們從一個新的角度來觀察這棵樹,思考一個問題:

在這棵“亂序”的樹中,哪些部分已經天然滿足堆的屬性了?

答案是:所有的葉子節點 (leaf nodes)。

一個葉子節點,因為它沒有孩子,所以它自己就構成了一個合法的、大小為1的堆。 “父節點 >= 子節點” 這個條件因為沒有子節點而天然成立。

這是一個至關重要的突破口!


第二步:找到規律,形成策略

在一個用數組表示的、大小為 n 的完全二叉樹中,哪些是葉子節點呢?

  • 對于下標為 i 的節點,它的左孩子是 2*i + 1。如果 2*i + 1 >= n,那么它就沒有左孩子,也就必然沒有右孩子,它就是葉子節點。

  • 我們可以推斷出,從下標 n/2n-1 的所有節點,都是葉子節點。

既然所有的葉子節點都已經是“合法”的堆了,我們就不需要對它們做任何操作。我們需要修復的,僅僅是那些非葉子節點

最后一個非葉子節點在哪里?它就在第一個葉子節點的前面,也就是下標為 (n/2) - 1 的位置。

這啟發了我們一個自底向上的修復策略:

1??我們從最后一個非葉子節點開始。

2??對它調用我們之前學過的 siftDown (下沉) 操作。

siftDown 的作用是,假設一個節點的左右子樹都已經是合法的堆,它可以讓這個節點“下沉”到正確位置,從而使這個節點和它的子樹共同構成一個更大的合法堆。

當我們從最后一個非葉子節點開始時,它的孩子必然是葉子節點(也就是合法的堆),所以 siftDown 的前提條件滿足了!

3??然后,我們向前移動到倒數第二個非葉子節點,對它也執行 siftDown

因為我們是自底向上處理的,所以當處理這個節點時,它的子樹也必然已經被我們調整成合法的堆了。

4??我們不斷地向前重復這個過程,直到處理完根節點(下標為0)。當根節點也 siftDown 完畢后,整個數組就變成了一個合法的堆。


用一個實例來推演

我們用 arr = [3, 5, 80, 100, 70, 60] (n=6) 來走一遍這個流程。

  • 非葉子節點的索引范圍是 0(6/2) - 1 = 2。所以我們只需要處理索引 2, 1, 0

  • 初始狀態:

        3/ \5   80/ \   /100 70 60

1. 從 i = 2 (值為80) 開始:

  • 對節點 80 執行 siftDown

  • 它的孩子是 6080 >= 60,滿足堆序,無需交換。

  • 數組狀態: [3, 5, 80, 100, 70, 60]

  • 樹狀態 (局部):

      80/60  (這個小堆是OK的)

2. 處理 i = 1 (值為5) :

  • 對節點 5 執行 siftDown

  • 它的孩子是 10070

  • 5 小于它的更大的孩子 100

  • 交換 5100

  • 數組狀態: [3, 100, 80, 5, 70, 60]

  • 樹狀態 (局部):

      100/  \5    70 (這個中等大小的堆也OK了)

3. 處理 i = 0 (值為3,根節點) :

  • 對節點 3 執行 siftDown

  • 它的孩子是 10080

  • 3 小于它更大的孩子 100

  • 交換 3100

  • 數組狀態: [100, 3, 80, 5, 70, 60]

  • 3 下沉到了原先 100 的位置(索引1)。現在要繼續對它進行 siftDown

  • 節點 3 (在索引1) 的新孩子是 5 (索引3) 和 70 (索引4)。

  • 3 小于它更大的孩子 70

  • 交換 370

  • 數組狀態: [100, 70, 80, 5, 3, 60]

  • 3 下沉到了索引4,成為了葉子節點,siftDown 結束。

  • 最終樹狀態:

    100/   \70    80/ \   /
5   3  60

至此,整個數組已經是一個合法的大頂堆了。


第三步:編寫代碼

這個過程的代碼實現非常簡潔。它只需要一個 for 循環,從最后一個非葉子節點開始,調用我們已經寫好的 siftDown 函數。

// 假設我們有一個數組 arr 和它的長度 n
// 我們可以復用之前的 siftDown, 但需要稍微修改一下,讓它接受數組和范圍
void siftDownForSort(int arr[], int n, int index) {int currentIndex = index;while (leftChild(currentIndex) < n) { // 注意這里的邊界是 nint largerChildIndex = leftChild(currentIndex);int rightChildIndex = rightChild(currentIndex);if (rightChildIndex < n && arr[rightChildIndex] > arr[largerChildIndex]) {largerChildIndex = rightChildIndex;}if (arr[currentIndex] >= arr[largerChildIndex]) {break;}swap(&arr[currentIndex], &arr[largerChildIndex]);currentIndex = largerChildIndex;}
}
// 我們需要 siftDown, swap, leftChild, rightChild 這些我們已經寫好的輔助函數
// ... (此處省略這些函數的代碼)// buildHeap 函數:將一個任意數組轉換成一個堆
// arr: 指向數組的指針
// n: 數組中元素的數量
void buildHeap(int arr[], int n) {// 找到最后一個非葉子節點的索引int lastNonLeafNode = (n / 2) - 1;// 從最后一個非葉子節點開始,自底向上,對每個節點執行 siftDownfor (int i = lastNonLeafNode; i >= 0; i--) {// siftDownForSort 函數就是我們之前為堆排序寫的 siftDown// 它接受數組、數組大小和要操作的索引作為參數siftDownForSort(arr, n, i); }
}

總結與分析

我們通過兩種不同的思路來解決“建堆”問題:

  1. 方法一(自頂向下):通過 N 次 insert (sift up),時間復雜度為 O(NlogN)。

  2. 方法二(自底向上):通過約 N/2 次 siftDown,時間復雜度為 O(N)。

雖然看起來都是循環N次,每次調用一個看似 O(logN) 的操作,但對于方法二,可以進行更嚴謹的數學分析:

大部分節點的 siftDown 操作都發生在樹的底層,路徑很短,只有根節點附近的少數節點才需要較長的 siftDown 路徑。所有操作的成本累加起來,總的時間復雜度其實是線性的 O(N)。

因此,自底向上的 siftDown 方法是構建堆的標準、最高效的算法。 它完美地體現了算法設計的智慧:通過改變看問題的角度,找到一個更深刻的結構性規律(葉子節點天然是堆),從而設計出更優的解決方案。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/920710.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/920710.shtml
英文地址,請注明出處:http://en.pswp.cn/news/920710.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于 GPT-OSS 的成人自考口語評測 API 開發全記錄

1?? 需求與指標 在項目啟動前&#xff0c;我們設定了核心指標&#xff1a; 字錯率&#xff08;WER&#xff09;< 5%響應延遲 < 800 ms高可用、可擴展 這些指標將貫穿整個開發和測試流程。 2?? 數據準備 準備訓練數據是關鍵步驟&#xff0c;我們使用了 1k 條自考口…

Linux初始——基礎指令篇

Linux常用指令pwdlscdtouchmkdirrmmancpmvcatmorelesswhichwhereisaliasgrepfilezip/unzip 指令rzsztarpwd 在xshell中輸入pwd并回車&#xff0c;將輸出當前用戶所存在的目錄位置 可看到當前用戶是在/home/hhw這個目錄下 ls 在xshell中輸入ls會顯示當前目錄所包含的文件 其中…

Vue-24-利用Vue3的element-plus庫實現樹形結構數據展示

文章目錄 1 項目啟動 1.1 創建和啟動項目(vite+vue) 1.2 清理不需要的代碼 1.3 下載必備的依賴(element-plus) 1.4 完整引入并注冊(main.sj) 1.5 設置@別名(vite.config.js) 2 el-tree樹形控件 2.1 TreeComponents.vue 2.1.1 模板部分 2.1.2 類型定義(Tree) 2.1.3 樹形數據(dat…

Kubernetes 部署與發布完全指南:從 Pod 到高級發布策略

引言:告別手動,擁抱聲明式 在傳統的部署流程中,我們常常需要手動執行一系列命令:SSH 到服務器、拉取新代碼、編譯、重啟服務、檢查日志、處理錯誤…這個過程不僅繁瑣低效,而且極易出錯,難以保證環境的一致性。 Kubernetes 徹底改變了這一切。它通過一種 “聲明式” 的模…

支持向量機核心知識總結

一、核心基礎概念核心目標&#xff1a;在樣本空間中找到劃分超平面&#xff0c;將不同類別樣本分開&#xff0c;且該超平面對訓練樣本局部擾動的 “容忍性” 最優&#xff08;即抗干擾能力強&#xff09;。超平面定義超平面是 n 維空間中的 n-1 維子空間&#xff0c;是 SVM 分類…

Spark學習記錄

1、Spark基礎介紹 1.1、Spark基礎概念 Spark是一種基于內存的快速、通用、可擴展的大數據分析計算引擎 1.2、Spark運行架構 運行過程&#xff1a; Driver 執行用戶程序&#xff08;Application&#xff09;的main()方法并創建 SparkContext&#xff0c;與 Cluster Manager 建…

二進制方式安裝部署 Logstash

背景說明 Logstash 是一個開源的數據收集和處理引擎&#xff0c;是 Elastic Stack 的重要組件之一。在本方案中&#xff0c;我們使用 Logstash 作為 Kubernetes 集群日志收集的關鍵組件&#xff0c;主要用于&#xff1a; 從 Kafka 消費各服務的日志數據對日志數據進行過濾和轉…

如何用 Kotlin 在 Android 手機開發一個計算器

使用 Kotlin 開發 Android 計算器1. 創建新項目 打開 Android Studio&#xff0c;選擇新建項目&#xff0c;模板選擇 "Empty Activity"&#xff0c;語言選擇 Kotlin&#xff0c;確保最低 API 級別為 21 或更高。2. 設計用戶界面 在 res/layout/activity_main.xml 中定…

【Hadoop】Zookeeper、HBase、Sqoop

Zookeeper概述Zookeeper可以監視HDFS系統的name node和data node&#xff0c;HBase也極度依賴zookeeper&#xff0c;因為zookeeper維護了HBase的源數據以及監控所有region server的健康狀態&#xff0c;如果region server宕機會通知master 。它也可以避免腦裂&#xff08;只有一…

MLIR - Linalg

簡介 Linalg是MLIR中的HHO&#xff08;High-level Hierarchical Optimization&#xff09;中的核心方言&#xff0c;設計用于支持如下的核心Transformation&#xff1a; Progressive Buffer Allocation.Parametric Tiling.Promotion to Temporary Buffer in Fast Memory.Tile…

SQL相關知識 CTF SQL注入做題方法總結

SQL MySQL基礎 MySQL基本操作 1.查詢本地所有數據庫&#xff1a; show databases; 2.使用數據庫&#xff1a;use 數據庫名; 3.查看當前使用的數據庫名&#xff1a;select database(); 4.查看當前使用的數據庫的所有表&#xff1a;show tables; 5.查看數據庫版本&#xff1a;sel…

魔方的使用

三階魔方入門玩法教程 【簡單實用11個公式】三階魔方分步還原公式圖解 【初級篇】三階魔方入門教程 1、底棱歸位&#xff08;底十字對中層&#xff09; 先頂黃白十字&#xff0c;旋轉對齊中層后&#xff0c;R’2翻到底層 2、底角歸位 上右-前-》右下 &#xff1a;URU’R’…

新手友好!剪映:開啟你的視頻剪輯之旅!(國際版)

一.軟件介紹 剪映&#xff08;CapCut&#xff09;是一款由??抖音旗下深圳市臉萌科技有限公司??開發的全功能視頻編輯軟件&#xff0c;自2019年5月上線以來&#xff0c;因其簡單易用且功能強大&#xff0c;受到了大量用戶的喜愛。 1.功能和作用&#xff1a; 功能類別主要…

使用AI大模型Seed1.5-VL精準識別開車接打電話等交通違法行為

原文鏈接 本案例根據用戶上傳的電子警察或道路卡口抓拍的圖片,使用豆包全新視覺深度思考模型Doubao-1.5-thinking-vision-pro,精準識別車牌號碼、車牌顏色、車身顏色、車輛品牌等車輛信息,同時通過算法精確識別開車打電話、未系安全帶等交通違法行為,具有極強的實用價值。…

騎行商城怎么開發

隨著騎行運動普及與數字化消費升級&#xff0c;“騎行中控數據變現積分商城”模式成為新趨勢。以下從核心步驟、關鍵要點、風險規避三方面&#xff0c;詳解如何搭建該類型小程序。一、明確核心架構與需求定位在開發前需確定小程序的核心邏輯與目標用戶&#xff0c;避免功能冗余…

揭秘表格推理的“思維革命”:RoT模型介紹

–– RoT: Enhancing Table Reasoning with Iterative Row-Wise Traversals今天&#xff0c;我想和大家探討一個我們每天都會遇到&#xff0c;卻可能從未深思過其背后奧秘的事物——表格。從公司的財務報表、醫療數據&#xff0c;到體育賽事統計&#xff0c;表格無處不在&#…

【C++】AVL樹(詳解)

文章目錄 上文鏈接一、什么是 AVL 樹二、AVL 樹的實現1. 引入平衡因子2. 整體結構3. AVL 樹中的插入操作(1) 插入節點(2) 更新平衡因子更新規則停止更新條件 4. 旋轉(1) 旋轉的目的(2) 右單旋(3) 左單旋(4) 左右雙旋(5) 右左雙旋 5. AVL 樹的查找與刪除6. AVL 樹的平衡檢測 三、…

shell編程-核心變量知識

文章目錄shell簡介如何學好shell初識shell什么是shell執行shell腳本常用的三種方式shell變量變量相關的配置文件變量的定義shell核心位置變量shell簡介 為什么學習shell&#xff0c;shell的作用 面試題&#xff1a;給你一臺主機你的操作流程是什么&#xff1f; 1.自動化安裝操…

微電網調度(風、光、儲能、電網交互)(MatlabPython代碼實現)

贈讀者&#xff1a;正在埋頭科研的你&#xff0c;或許有時你會困惑于 “投入” 與 “回報” 的時差&#xff0c;會疲憊于 “未知” 與 “確定” 的博弈&#xff0c;但請記得&#xff1a;那些看似 “無用” 的試錯&#xff0c;都是在為突破搭建階梯&#xff1b;那些獨自深耕的日…

CentOS 7 環境下安裝 JDK 1.8 及解決 wget 命令缺失問題

個人名片 &#x1f393;作者簡介&#xff1a;java領域優質創作者 &#x1f310;個人主頁&#xff1a;碼農阿豪 &#x1f4de;工作室&#xff1a;新空間代碼工作室&#xff08;提供各種軟件服務) &#x1f48c;個人郵箱&#xff1a;[2435024119qq.com] &#x1f4f1;個人微信&a…