個人主頁:C++忠實粉絲
歡迎 點贊👍 收藏? 留言? 加關注💓本文由 C++忠實粉絲?原創數據結構之二叉樹的超詳細講解(2)--(堆的概念和結構的實現,堆排序和堆排序的應用)
收錄于專欄【數據結構初階】
本專欄旨在分享學習數據結構學習的一點學習筆記,歡迎大家在評論區交流討論💌
之前發布過數據結構之二叉樹的超詳細講解(1)--(樹和二叉樹的概念和結構)?,今天重點講解堆的概念和結構的實現,堆排序和堆排序的應用,感興趣的寶子們趕緊點贊收藏起來吧!💓💓💓
目錄
1.二叉樹的順序結構及實現?
1.1二叉樹的順序結構
1.2 堆的概念及結構?
1.3堆算法的實現
1.3.1堆結構的實現
1.3.2堆操作函數?
1.3.2.1堆的初始化
1.3.2.2堆的銷毀
1.3.2.3堆頂和堆尾元素的交換
1.3.2.4堆的調整算法
1.3.2.4.1向上調整建堆(以小根堆為例)
1.3.2.4.2向下調整建堆(以小根堆為例)
1.3.2.5數據進堆
1.3.2.6堆頂元素出堆
1.3.2.7輸出堆頂元素
1.3.2.8堆的判空
練習:
2.堆排序
2.1向上調整建堆
2.1.1小根堆
2.1.2大頂堆
2.2向下調整建堆
2.2.1小根堆
2.2.2大根堆?
2.3堆排序的完整代碼參考:
2.4建堆的時間復雜度?
2.4.1向下調整建堆的時間復雜度:?
2.4.1向上調整建堆的時間復雜度:?
2.5堆排序的應用--?TOP-K問題
方法一:
方法二:
?參考代碼:
Heap.h:
Heap.c:
text.c:
1.二叉樹的順序結構及實現?
1.1二叉樹的順序結構
普通的二叉樹是不適合用數組來存儲的,因為可能會存在大量的空間浪費。而完全二叉樹更適合使用順序結構存儲。現實中我們通常把堆(一種二叉樹)使用順序結構的數組來存儲,需要注意的是這里的堆和操作系統 虛擬進程地址空間中的堆是兩回事,一個是數據結構,一個是操作系統中管理內存的一塊區域分段。
1.2 堆的概念及結構?
如果有一個關鍵碼的集合K = { k0,k1,k2,,,,,,,,kn-1},把它的所有元素按完全二叉樹的順序存儲方式存儲在一個一維數組中,并滿足: ki<=k2*i?且 ki <= k2*i+2?(k >= k2*i+1 且ki >= k2*i+2)?i = 0,1, 2…,則稱為小堆(或大堆)。將根結點最大的堆叫做最大堆或大根堆,根結點最小的堆叫做最小堆或小根堆。
這里運用了二叉樹性質五:
堆的性質:
堆中某個結點的值總是不大于或不小于其父結點的值;
堆總是一棵完全二叉樹。?
如下圖所示:?
1.3堆算法的實現
1.3.1堆結構的實現
之前說到過,堆就是完全二叉樹,?把它的所有元素按完全二叉樹的順序存儲方式存儲在一個一維數組中,所以我們這里使用動態數組的方式建堆,類似于使用動態數組構建順序表
順序表的構建和基本操作-CSDN博客
typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;
?a就是我們的一個動態數組
size是我們堆中的元素個數
capacity是我們堆的容量,方便后面動態開辟空間
HPDataType方便后面我們進行類型的修改,比如我們不需要整形了,而是char類型,直接修改HPDataType就可以了
1.3.2堆操作函數?
//堆頂和堆尾元素的交換
void Swap(HPDataType* p1, HPDataType* p2);
//向上調正算法
void AdjustUp(HPDataType* a, int child);
//向下調整算法
void AdjustDown(HPDataType* a, int n, int parent);
//堆的初始化
void HPInit(HP* php);
//堆的銷毀
void HPDestroy(HP* php);
//數據近堆
void HPPush(HP* php, HPDataType x);
//堆頂元素出堆
void HPPop(HP* php);
//輸出隊頂元素
HPDataType HPTop(HP* php);
//堆的判空
bool HPEmpty(HP* php);
1.3.2.1堆的初始化
void HPInit(HP* php)
{assert(php);php->a = NULL;php->size = php->capacity = 0;
}
1.3.2.2堆的銷毀
void HPDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}
注意:
free釋放掉a之后,還需要將a置為NULL?
1.3.2.3堆頂和堆尾元素的交換
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}
為后面的調整算法算法埋下鋪墊?
1.3.2.4堆的調整算法
1.3.2.4.1向上調整建堆(以小根堆為例)
如下圖所示(以小根堆為例):
我們將新插入的數據不斷的與它的parent節點進行比較?
代碼展示:
void AdjustUp(HPDataType* a, int child)
{// 初始條件// 中間過程// 結束條件int parent = (child - 1) / 2;//while (parent >= 0)while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
注意:
根據我們之前學過的二叉樹的性質,左child可以通過(child-1)/2和右孩子(child-2)/2來找到它的母親節點,因為編譯器的向下取整,所以我們直接使用int parent = (child - 1) / 2;來解決,不需要判斷左右孩子
如下圖?:
這里我們的循環停止條件有兩種:
第一種:while (parent >= 0)?
第一種需要注意:當我們的parent = 0時,假設當時child還是比parent小,child? = parent,parent = (child - 1) / 2;還是會進入下一次循環,此時child = parent = 0,由于存在if的判斷循環會結束,不會造成死循環現象,屬于歪打正著
第二種就嚴謹很多:
第二種采用child作為循環條件的判斷,當我們的child大于0,就會一直進行交換直到child等于0時,即為堆頂元素時,循環停止
1.3.2.4.2向下調整建堆(以小根堆為例)
?
注意:
向下調整建堆需要以27為根的左右子樹都滿足小堆的性質,只有根節點不滿足.因此只需要將根節點往下調,往下調時需要與最小的那個值進行比較
代碼展示:
void AdjustDown(HPDataType* a, int n, int parent)
{// 先假設左孩子小int child = parent * 2 + 1;while (child < n) // child >= n說明孩子不存在,調整到葉子了{// 找出小的那個孩子if (child + 1 < n && a[child + 1] < a[child]){++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
這里我們使用假設法,先假設左孩子小,然后進入循環,進入循環后進行判斷,因為我們需要將較小的孩子與我們的parent進行交換,如果它本來就小,那就直接跳過,然后交換parent和child,如圖所示:
1.3.2.5數據進堆
這里需要注意,數據進堆時,我們需要對進堆的數據進行調整,這樣我們進堆結束后,即建堆完畢
代碼展示:
void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newcapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}
1.3.2.6堆頂元素出堆
void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}
刪除堆是刪除堆頂的數據,將堆頂的數據根最后一個數據一換,然后刪除數組最后一個數據,再進行向下調整算法。這樣我們向下調整時兩邊都是小根堆,滿足向下調整的條件
1.3.2.7輸出堆頂元素
HPDataType HPTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}
1.3.2.8堆的判空
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}
練習:
1.下列關鍵字序列為堆的是:()
A 100, 60, 70, 50, 32, 65
B 60, 70, 65, 50, 32, 100
C 65, 100, 70, 32, 50, 60
D 70, 65, 100, 32, 50, 60
E 32, 50, 100, 70, 65, 60
F 50, 100, 70, 65, 60, 32
2.已知小根堆為8, 15, 10, 21, 34, 16, 12,刪除關鍵字 8 之后需重建堆,在此過程中,關鍵字之間的比較次
數是()。
A 1
B 2
C 3
D 4
3.一組記錄排序碼為(5 11 7 2 3 17), 則利用堆排序方法建立的初始堆為
A(11 5 7 2 3 17)
B(11 5 7 2 17 3)
C(17 11 7 2 3 5)
D(17 11 7 5 3 2)
E(17 7 11 3 5 2)
F(17 7 11 3 2 5)
4.最小堆[0, 3, 2, 5, 7, 4, 6, 8], 在刪除堆頂元素0之后,其結果是()
A[3,2,5,7,4,6,8]
B[2,3,5,7,4,6,8]
C[2,3,4,5,7,8,6]
D[2,3,4,5,6,7,8]
解析:
第一題:我們上面說到過,堆其實就是完全二叉樹,所以我們直接構建完全二叉樹:
只有A選項滿足大堆的需求
第二題:
第三題:
只有c選項滿足大頂堆的要求
第四題:
刪除后,堆的調整如下:
2.堆排序
2.1向上調整建堆
2.1.1小根堆
for (size_t i = 0; i < sizeof(a) / sizeof(int); i++){HPPush(&hp, a[i]);}
?對每一次插入堆中的數據進行調整建堆
測試數據:
?? ?int a[] = { 4,2,8,1,5,6,9,7,3,2,23,55,232,66,222,33,7,1,66,3333,999 };?
while (!HPEmpty(&hp)){printf("%d ", HPTop(&hp));//a[i++] = HPTop(&hp);HPPop(&hp);}printf("\n");
根據小根堆的性質:它的堆頂元素一定是數組中最小的數據,我們將堆頂數據輸出,在重新建堆,直到堆中沒有數據,這樣就可以實現堆排序?
輸出結果:
2.1.2大頂堆
建立大頂堆只需要改變我們之前寫的向上向下調整代碼:
向上調整代碼:
void AdjustUp(HPDataType* a, int child)
{// 初始條件// 中間過程// 結束條件int parent = (child - 1) / 2;//while (parent >= 0)while (child > 0){//如果孩子比parent大if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}
再向上調整代碼中,我們只需要將if的判斷改為>就可以了?
向下調整代碼:
void AdjustDown(HPDataType* a, int n, int parent)
{// 先假設左孩子小int child = parent * 2 + 1;while (child < n) // child >= n說明孩子不存在,調整到葉子了{// 找出小的那個孩子//找出大的那個孩子if (child + 1 < n && a[child + 1] > a[child]){++child;}//如果孩子比母親大if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}
?再向下調整代碼中,我們需要調整兩處,一個是我們需要找大的那個孩子,將<改為>,還有我們交換的條件需要孩子比母親大,也是將<改為>
測試數據和代碼不變:
所以你只需要會小根堆,大頂堆只需要改變三個地方?
2.2向下調整建堆
再實際應用中,我們建堆并不會使用向上調整建堆,因為時間復雜度不夠低,但向上調整建堆更容易理解,大家可以根據自己的情況進行選擇,這里我還是推薦向下調整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}
?
我們這里第一個非葉子節點即最后一個節點的母親節點:
也就是(n - 1 - 1) / 2,如圖所示:
2.2.1小根堆
測試數據?? ?int a[] = { 4,2,8,1,5,6,9,7,2,7,9 };
2.2.2大根堆?
數據和小根堆一樣:
2.3堆排序的完整代碼參考:
void HeapSort(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){printf("%d ", a[0]);Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}int main()
{int a[] = { 4,2,8,1,5,6,9,7,2,7,9 };HeapSort(a, sizeof(a) / sizeof(int));return 0;
}
2.4建堆的時間復雜度?
?我們上面說到過,向下建堆的時間復雜度是優于向上建堆的,這里我們具體分析一下:
2.4.1向下調整建堆的時間復雜度:?
因為堆是完全二叉樹,而滿二叉樹也是完全二叉樹,此處為了簡化使用滿二叉樹來證明(時間復雜度本來看的就是近似值,多幾個結點不影響最終結果):
這里我們是向下調整建堆,是從第一個非葉子節點開始一直到第一個節點的向下調整,如下圖所示:?
根據上面的圖,我們可以很簡單的得到下面的等式:?
再根據我們高中學過的錯位相減法,進一步化簡:?
因此:向下建堆的時間復雜度為O(N)。?
2.4.1向上調整建堆的時間復雜度:?
因此向上調整建堆的時間復雜度為N*logN?
總結:
?向下調整:節點數量少的層*調整次數多的層?
?向上調整:節點數量多的層*調整次數多的層
?向下建堆的時間復雜度為O(N)。?
上調整建堆的時間復雜度為N*logN?
2.5堆排序的應用--?TOP-K問題
TOP-K問題:即求數據結合中前K個最大的元素或者最小的元素,一般情況下數據量都比較大。
比如:專業前10名、世界500強、富豪榜、游戲中前100的活躍玩家等。
再比如:大家都玩過王者榮耀吧,金標或者國標都需要取前100名玩家,再幾億玩家中找出戰力最高的100名玩家,如果你用快排的話,電腦估計要轉冒煙了,但TOP-K可以很好的解決這個問題
方法一:
建立一個N個數的大堆,TOP K次?
這個方法是可行的,不過不夠完美,因為再TOP K問題中,N往往是很大的,這樣你建堆的一個內存就很大,算法的空間損耗會很大?
方法二:
用前K個數建一個小堆
?
?建堆的時間復雜度為O(K),然后還進行了(N-K)比較,所以總的時間復雜度為O(K + (N-K)*log(K)),假設是最壞的情況,每一次比較都需要向下調整,因為K是遠小于N的,所以K,logK可以忽略不記,總時間復雜度為O(N),也就是上億的數據,也能再秒之內完成?
測試方法二:
創建數據:
void CreateNDate()
{// 造數據int n = 100000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 10000000;fprintf(fin, "%d\n", x);}fclose(fin);
}
輸出結果:
創造一百完個隨機數據
TOP-K求解:
void TestHeap3()
{int k;printf("請輸入k>:");scanf("%d", &k);int* kminheap = (int*)malloc(sizeof(int) * k);if (kminheap == NULL){perror("malloc fail");return;}const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}// 讀取文件中前k個數for (int i = 0; i < k; i++){fscanf(fout, "%d", &kminheap[i]);}// 建K個數的小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(kminheap, k, i);}// 讀取剩下的N-K個數int x = 0;while (fscanf(fout, "%d", &x) > 0){if (x > kminheap[0]){kminheap[0] = x;AdjustDown(kminheap, k, 0);}}printf("最大前%d個數:", k);for (int i = 0; i < k; i++){printf("%d ", kminheap[i]);}printf("\n");
}
手動更改十個數據大于10000000的數,查看結果:
?輸出結果:
?參考代碼:
Heap.h:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;
//堆頂和堆尾元素的交換
void Swap(HPDataType* p1, HPDataType* p2);
//向上調正算法
void AdjustUp(HPDataType* a, int child);
//向下調整算法
void AdjustDown(HPDataType* a, int n, int parent);
//堆的初始化
void HPInit(HP* php);
//堆的銷毀
void HPDestroy(HP* php);
//數據近堆
void HPPush(HP* php, HPDataType x);
//堆頂元素出堆
void HPPop(HP* php);
//輸出隊頂元素
HPDataType HPTop(HP* php);
//堆的判空
bool HPEmpty(HP* php);
Heap.c:
#define _CRT_SECURE_NO_WARNINGS 1#include"Heap.h"void HPInit(HP* php)
{assert(php);php->a = NULL;php->size = php->capacity = 0;
}void HPDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}void AdjustUp(HPDataType* a, int child)
{// 初始條件// 中間過程// 結束條件int parent = (child - 1) / 2;//while (parent >= 0)while (child > 0){//如果孩子比parent大if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}void HPPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity = newcapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}void AdjustDown(HPDataType* a, int n, int parent)
{// 先假設左孩子小int child = parent * 2 + 1;while (child < n) // child >= n說明孩子不存在,調整到葉子了{// 找出小的那個孩子//找出大的那個孩子if (child + 1 < n && a[child + 1] < a[child]){++child;}//如果孩子比母親大if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}// logN
void HPPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}HPDataType HPTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}
text.c:
#define _CRT_SECURE_NO_WARNINGS 1#include <time.h>
#include"Heap.h"void TestHeap1()
{int a[] = { 4,2,8,1,5,6,9,7,3,2,23,55,232,66,222,33,7,1,66,3333,999 };HP hp;HPInit(&hp);for (size_t i = 0; i < sizeof(a) / sizeof(int); i++){HPPush(&hp, a[i]);}int i = 0;while (!HPEmpty(&hp)){printf("%d ", HPTop(&hp));//a[i++] = HPTop(&hp);HPPop(&hp);}printf("\n");// 找出最大的前k個/*int k = 0;scanf("%d", &k);while (k--){printf("%d ", HPTop(&hp));HPPop(&hp);}printf("\n");*/HPDestroy(&hp);
}// 堆排序 O(N*logN)
// 冒泡排序 O(N^2)
void HeapSort(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){printf("%d ", a[0]);Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}void CreateNDate()
{// 造數據int n = 100000;srand((unsigned int)time(NULL));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 10000000;fprintf(fin, "%d\n", x);}fclose(fin);
}void TestHeap3()
{int k;printf("請輸入k>:");scanf("%d", &k);int* kminheap = (int*)malloc(sizeof(int) * k);if (kminheap == NULL){perror("malloc fail");return;}const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen error");return;}// 讀取文件中前k個數for (int i = 0; i < k; i++){fscanf(fout, "%d", &kminheap[i]);}// 建K個數的小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(kminheap, k, i);}// 讀取剩下的N-K個數int x = 0;while (fscanf(fout, "%d", &x) > 0){if (x > kminheap[0]){kminheap[0] = x;AdjustDown(kminheap, k, 0);}}printf("最大前%d個數:", k);for (int i = 0; i < k; i++){printf("%d ", kminheap[i]);}printf("\n");
}int main()
{//CreateNDate();TestHeap3();return 0;
}