逻辑结构:完全二叉树
小堆:要求父节点始终小于孩子节点
大堆:要求父节点始终大于孩子节点
存储结构:顺序存储
堆就是一个顺序存储的完全二叉树,外加了父节点和孩子节点之间的大小关系
基本操作:初始化、增、删
因为顺序表的特性,直接在最后插入的效率最高,所以我们将数据直接插入到最后,然后再用向上调整算法使其重新变成一个堆。
向上调整算法的前提是本身就是一个堆,插入进来的是孩子节点,每次去和自己的父节点大小比较,不满足堆的逻辑结构就要交换数据,直到满足或者到根节点为止。
void AdjustUp(DataType*arr,int child)
{
assert(arr);
int parent=(child-1)/2;
while(child>0){
if(arr[child]<arr[parent]){
DataType tmp=arr[child];
arr[child]=arr[parent];
arr[parent]=tmp;
child=parent;
parent=(child-1)/2;
}else{
break;
}
}
}
删除操作中,我们删除根节点是最有效的,因为小堆的根节点是所有数据中最小的元素,大堆的根节点是所有数据中最大的元素。但是不能直接删除,因为删除后所有的关系全乱了,所以我们选择第一个和最后一个数据元素交换后删除最后一个元素即可,这样我们可以使用向下调整算法将换上去的新元素调整到合适的位置。
向下调整算法:使用的前提是它的左右子树必须是堆,本节点是父节点,向下找孩子节点,比如是大堆,我们换上去了一个比较小的节点,那我们要将这个结点与它的孩子节点中比较大的比较并交换,直到调整到合适的位置或者是叶子节点为止。叶子节点怎么判断?叶子节点肯定没有孩子,所以此时的parent*2+1一定>=我们数组中的总元素的个数?。
void AdjustDown(int*arr,int parent,int size)
{
assert(arr);
int child=2*parent+1;
if(child<size&&child+1<size&&arr[child]<arr[child+1]){
child++;
}
while(child<n){
if(arr[child>arr[parent]){
int tmp=arr[child];
arr[child]=arr[parent];
arr[parent]=tmp;
parent=child;
child=2*parent+1;
if(child<size&&child+1<size&&arr[child]<arr[child+1]){
child++;
}
}else{
break;
}
}
}
这就是堆这个数据结构,下面介绍堆的应用:堆排序和TOP-K问题
堆排序:我们用上面堆的数据结构来对一个数组进行堆排序也可以,依次插入+依次删除也能排序,但是有额外空间的消耗和要先创建一个堆这个数据结构,比较麻烦。
第一种堆排序方法:模拟堆这个数据结构。我们直接将要排序的数组看成一个空的堆,一个指针从头开始,依次向后扫描,扫描到第一个表示我们将第一个数据插入到堆里面了,然后用向上调整算法将其调整成一个堆,第二个第三个一直到第n个数据全部扫描完成。这里用的是向上调整算法建堆,时间复杂度算出来是O(N*logN)。我们要记住,如果要排升序,建立的就是大堆,因为这样能先找到最大的数据交换到最后,然后是次大的以此类推,排降序反之。我们现在已经排好大堆了,交换根节点和最后一个数据就找到了数组中最大的元素。向下调整算法+交换根节点就排好了第二大的元素、、、、、、如果我们有10个数据需要排序,那我们需要向下调整算法8次,需要交换数据9次。
void Swap(int* n1, int* n2)
{
assert(n1 && n2);
int tmp = *n1;
*n1 = *n2;
*n2 = tmp;
}
void AdjustUp(int* arr, int child)
{
assert(arr);
int parent = (child - 1) / 2;
while (child>0) {
if (arr[child] > arr[parent]) {
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}
void AdjustDown(int* arr, int parent, int size)
{
assert(arr);
int child = (2 * parent) + 1;
while (child<size) {
if ((child+1)<size && arr[child] < arr[child + 1]) {
child++;
}
if (arr[child] > arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = (2 * parent) + 1;
}
else {
break;
}
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//向上调整建堆,时间复杂度O(N*logN),因为向上调整建堆所有的叶子节点也要调整,叶子节点占了节点个数的一半
int i = 0;
int size = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < size; i++) {
AdjustUp(arr, i);
}
//向下调整排序(排升序,先找最大的,建大堆;排降序,先找最小的,建小堆)
Swap(&arr[0], &arr[size - 1]);
for (i = 1; i < size - 1; i++) {
AdjustDown(arr, 0, size - i );
Swap(&arr[0], &arr[size - i - 1]);
}
//打印
for (i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
return 0;
}
第二种排序方法是向下调整建堆,这个建堆的时间复杂度很低,可以到O(N),但是向下调整的前提是左右子树都要是堆,那我们抛开所有的叶子节点不谈,第一个父节点下面一定连着一个或者两个叶子节点,从这里开始进行调整,如何找到它?第一个父节点就是最后一个叶子结点的父节点。建堆完成后用向下调整算法排序即可。
void Swap(int* n1, int* n2)
{
assert(n1 && n2);
int tmp = *n1;
*n1 = *n2;
*n2 = tmp;
}
void AdjustDown(int* arr, int parent, int size)
{
assert(arr);
int child = (2 * parent) + 1;
while (child<size) {
if ((child+1)<size && arr[child] > arr[child + 1]) {
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = (2 * parent) + 1;
}
else {
break;
}
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//排降序建小堆,向下调整建堆时间复杂度O(N),叶子节点都不用调整,从第一个父节点开始(满足左右子树都是小堆的特点)
int size = sizeof(arr) / sizeof(arr[0]);
int i = (size - 1 - 1) / 2;
while (i>=0) {
AdjustDown(arr, i, size);
i--;
}
//向下调整排序,这里和模仿的堆排序那里一样
Swap(&arr[0], &arr[size - 1]);
i = 1;
while (i < size - 1) {
AdjustDown(arr, 0, size - i);
Swap(&arr[0], &arr[size - i - 1]);
i++;
}
//打印
for (i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
return 0;
}
堆排序的时间复杂度是排序算法中很低的,只有O(N*LogN)
TopK问题,这个问题是从大量数据中找出前10个最大的类似的问题
有100000个数据在文件中,找10个最大的首先我们要建立一个小堆,从文件中读取10个数据进来建立小堆,然后扫描整个文件,如果文件中有比我们的小堆中的根节点大的数据就交换并进行向下调整算法重新调整为堆然后继续向后扫描。我们这样想,在1000000个数据中排名前10大的数据分散在其中,我们的堆只要碰到其中一个一定会交换进来,直到最后10个全部进堆。
void Swap(int* n1, int* n2)
{
assert(n1 && n2);
int tmp = *n1;
*n1 = *n2;
*n2 = tmp;
}
void AdjustDown(int* arr, int parent, int size)
{
assert(arr);
int child = 2 * parent + 1;
if (child<size&&(child+1)<size && arr[child] > arr[child + 1]) {
child++;
}
while (child<size) {
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
if (child < size && (child + 1) < size && arr[child] > arr[child + 1]) {
child++;
}
}
else {
break;
}
}
}
int main()
{
FILE* F = fopen("data.txt", "w");
srand(time(NULL));
if (F == NULL) {
printf("%s\n", strerror(errno));
}
else {
int k = 10000;
while (k > 0) {
int n = rand() % 10000000;
fprintf(F, "%d\n", n);
k--;
}
}
fclose(F);
FILE* F1 = fopen("data.txt", "r");
//建"堆"
int arr[100] = { 0 };
int i = 0;
for (i = 0; i < 10; i++) {
fscanf(F1, "%d", &arr[i]);
}
i = (10 - 1 - 1) / 2;
for (i = (10 - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(arr, i, 10);
}
//扫描完整个文件中的数据
int n = 0;
while (fscanf(F1, "%d", &n) != EOF) {
if (n > arr[0]) {
arr[0] = n;
AdjustDown(arr, 0, 10);
}
}
//打印看看
for (i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
return 0;
}
这就是二叉树之一的完全二叉树的顺序存储--堆,这个数据结构与它在排序和Topk问题中的应用。