【数据结构】堆综合的运用

简介: 【数据结构】堆综合的运用

一,运用堆结构进行排序

       当我们用堆结构进行排序时,首先要明确的是升序排列要建大堆,降序排列要建小堆,不是说升序建小堆就不行,降序建大堆就不行,而是如果运用这种建堆方式进行排序的话效率会比较低,下面我会跟大家详细分析。

       首先,我们来回顾一下堆的概念,大堆结构:双亲结点>=孩子结点。小堆结构:双亲结点<=孩子结点。其中,根节点是所有结点的"祖宗",也就是在大堆中,根节点是在堆中最大的数据,在小堆中,根节点是在堆中最小的数据。排升序时,当我们运用建小堆思想,虽然第一个数据已确定序列位置,但后面的数据要想确定序列位置就必须对除去上一次数据外剩下的元素再次建堆,也就是建一次小堆相当于排好一个数据的序列位置。排降序时运用大堆同理。此时的时间效率为O((n*logn)*n)。由此可见,此种方法效率太低。

       当升序用建大堆思想时,大堆中根结点是最大数据,升序序列中最后的数据都是最大的数据,我们只需在大堆中将首元素与尾元素进行交换后即可确定最后一个元素的序列位置,然后再除去大堆中最后一个元素将根结点运用向下调整算法进行调整,这将又得到除去最后一个元素的大堆,以次不断循环,最终将有序。由此可见,大堆排升序是从后到前的排序。

       降序用小堆的思想与之同理,都是先建立堆,然后不断交换确定数据序列位置,都是从后到前的排序。具体代码如下:

运用大堆排升序:

void Swap(int* x, int* y) {
   int t = *x;
   *x = *y;
   *y = t;
}
void AdjustUp(int* a, int child) {
   assert(a);
   int parent = (child - 1) / 2;
   while (parent >= 0) {
      //调整建大堆
       if (a[parent] < a[child]) {
           Swap(a + parent, a + child);
           child = parent;
           parent = (child - 1) / 2;
       }
       if (a[parent] >= a[child]) {
           return;
       }
   }
}
void AdjustDown(int* a, int n, int parent) {
   assert(a);
   int child = parent * 2 + 1;
   while (child < n) {
       //调整建大堆
       if (child + 1 < n && a[child] < a[child + 1]) {
           child++;
       }
       if (a[child] > a[parent]) {
           Swap(a + child, a + parent);
           parent = child;
           child = parent * 2 + 1;
       }
       if (a[child] <= a[parent]) {
           return;
       }
   }
}
void HeadSort(int* a, int n) {
   //运用向上调整算法建大堆
   for (int i = 1; i < n; i++) {
       AdjustUp(a, i);
   }
   //end是堆中最后一个元素的下标
   int end = n - 1;
   while (end > 0) {
      //将堆中最大数据排到最后
       Swap(&a[0], &a[end]);
       //将end个数据建成堆
       AdjustDown(a, end, 0);//因为根节点的左右子树都可看成大堆,所以只要向下调节根结点就可使end个数据成为大堆
       //后面已排好了一个数据,继续将剩下的数据建堆排序即可

       end--;
   }
}

运用小堆排降序

void Swap(int* x, int* y) {
   int t = *x;
   *x = *y;
   *y = t;
}
void AdjustUp(int* a, int child) {
   assert(a);
   int parent = (child - 1) / 2;
   while (parent >= 0) {
       //调整建小堆
       if (a[parent] > a[child]) {
           Swap(a + parent, a + child);
           child = parent;
           parent = (child - 1) / 2;
       }
       if (a[parent] <= a[child]) {
           return;
       }
   }
}
void AdjustDown(int* a, int n, int parent) {
   assert(a);
   int child = parent * 2 + 1;
   while (child < n) {
      //调整建小堆
       if (child + 1 < n && a[child] > a[child + 1]) {
           child++;
       }
       if (a[child] < a[parent]) {
           Swap(a + child, a + parent);
           parent = child;
           child = parent * 2 + 1;
       }
       if (a[child] >= a[parent]) {
           return;
       }
   }
}
void HeadSort(int* a, int n) {
  //运用向上调整算法建小堆
   for (int i = 1; i < n; i++) {
       AdjustUp(a, i);
   }
   //end是堆中最后一个元素的下标
   int end = n - 1;
   while (end > 0) {
       //将堆中最小数据排到最后
       Swap(&a[0], &a[end]);
      //将end个数据建成堆
       AdjustDown(a, end, 0);//因为根节点的左右子树都可看成小堆,所以只要向下调节根结点就可使end个数据成为小堆
       //因为后面排好了一个数据,继续将剩下的数据建堆排序即可

       end--;
   }
}

效率分析:

       我们先观察前面的建堆效率:时间复杂度:O(nlogn)     空间复杂度:O(1)

       在后面排序时,每当确定一个数据的序列位置时就要运用调整算法进行调整一次,所以,要对n个数据排序时的时间复杂度:O(nlogn)   空间复杂度:O(1)。因此,运用堆结构排序的整体效率:时间复杂度:O(nlogn)   空间复杂度:O(1)。


二,经典的Topk问题

      Topk问题:假设有100万个数据(也可以是1千万,1亿个数据,只要数据足够大即可),而这些数据内存又存储不下,所以要靠文件来进行存储,在文件中,我们需要找出最大的前k个数据。

       此问题可以用堆结构来完成,具体步骤如下:

       1,读取文件的前k个数据,在内存数组中建立一个大小为k的小堆。

       2,再依次读取剩下的数据与堆顶数据比较,大于堆顶就替换这个数据进堆,然后向下调整,调整后的数据仍为堆,即堆顶数据是堆中最小的数据。

       3,所有数据读完,堆里面数据就是最大的前k个数据。

       首先,我们先造出这么多个数据,将其写入文件中。

//str是文件的路径
void CreatData(char* str) {
  FILE* file = fopen(str, "w");
  if (file == 0) {
    perror("fopen");
    exit(-1);
  }
  //造出1000000个数据,也可以造出更多
  srand(time(0));
  for (int i = 0; i < 1000000; i++) {
    //造出范围在[1, 1000]的数据
    int n = (rand() + i) % 1000 + 1;
    fprintf(file, "%d\n", n);
  }
  fclose(file);
}

       然后,先建立起小堆结构,再将前k个数据放入小堆中,最后遍历文件数据进行交换。

void CreatHeap(char* str, int k) {
  //建立堆结构并初始化
  int* a = (int*)malloc(sizeof(int) * k);
  if (a == 0) {
    perror("malloc");
    exit(-1);
  }
  memset(a, 0, sizeof(int) * k);
  //先将文件中的前k个数据放到堆中,将其调成小堆
  FILE* file = fopen(str, "r");
  if (!file) {
    perror("fopen");
    exit(-1);
  }
  for (int i = 0; i < k; i++) {
    fscanf(file, "%d", a + i);
  }
  for (int i = (k - 2) / 2; i >= 0; i--) {
    AdjustDown(a, k, i);
  }
  //然后遍历文件中剩下数据
  int x = 0;
  while (fscanf(file, "%d", &x) != EOF) {
    if (x > a[0]) {
      Swap(&x, &a[0]);
      AdjustDown(a, k, 0);
    }
  }
  //输出
  for (int i = 0; i < k; i++) {
    fprintf(stdout, "%d ", a[i]);
  }
  puts("");
  free(a); 
  fclose(file);
}

整套代码如下(我们将k设置为100,即从100万个数据选出100个最大数):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
void Swap(int* x, int* y) {
  int t = *x;
  *x = *y;
  *y = t;
}
void AdjustUp(int* a, int child) {
  assert(a);
  int parent = (child - 1) / 2;
  while (parent >= 0) {
    if (a[parent] > a[child]) {
      Swap(a + parent, a + child);
      child = parent;
      parent = (child - 1) / 2;
    }
    if (a[parent] <= a[child]) {
      return;
    }
  }
}
void AdjustDown(int* a, int n, int parent) {
  assert(a);
  int child = parent * 2 + 1;
  while (child < n) {
    if (child + 1 < n && a[child] > a[child + 1]) {
      child++;
    }
    if (a[child] < a[parent]) {
      Swap(a + child, a + parent);
      parent = child;
      child = parent * 2 + 1;
    }
    if (a[child] >= a[parent]) {
      return;
    }
  }
}
//str是文件的路径
void CreatData(char* str) {
  //注意:"w"表示以写入方式打开文件并清空文件内容
  FILE* file = fopen(str, "w");
  if (file == 0) {
    perror("fopen");
    exit(-1);
  }
  //造出1000000个数据,也可以造出更多
  int n = 1000000;
  srand(time(0));
  for (int i = 0; i < n; i++) {
    //造出范围在[1, 1000]的数据
    int n = (rand() + i) % 1000 + 1;
    fprintf(file, "%d\n", n);
  }
  fclose(file);
}
void CreatHeap(char* str, int k) {
  //建立堆结构并初始化
  int* a = (int*)malloc(sizeof(int) * k);
  if (a == 0) {
    perror("malloc");
    exit(-1);
  }
  memset(a, 0, sizeof(int) * k);
  //先将文件中的前k个数据放到堆中,将其调成小堆
  FILE* file = fopen(str, "r");
  if (!file) {
    perror("fopen");
    exit(-1);
  }
  for (int i = 0; i < k; i++) {
    fscanf(file, "%d", a + i);
  }
  for (int i = (k - 2) / 2; i >= 0; i--) {
    AdjustDown(a, k, i);
  }
  //然后遍历文件中剩下数据
  int x = 0;
  while (fscanf(file, "%d", &x) != EOF) {
    if (x > a[0]) {
      Swap(&x, &a[0]);
      AdjustDown(a, k, 0);
    }
  }
  //输出
  for (int i = 0; i < k; i++) {
    fprintf(stdout, "%d ", a[i]);
  }
  puts("");
  free(a); 
  fclose(file);
}
//删除路径在str中的文件
void DeleteFile(char* str) {
  if (remove(str) != 0) {
    fprintf(stdout, "无法删除文件\n");
  }
  else {
    fprintf(stdout, "文件已删除\n");
  }
}
int main() {
  //创建巨多数据,将其输出到文件中
  CreatData("E:\\test.txt");
  //k为100,建立大小为100的小堆,并将文件中最大的前100个数据放入小堆中
  CreatHeap("E:\\test.txt", 100);
  //删除文件
  DeleteFile("E:\\test.txt");
  return 0;
}

效率分析:

       从以上中不难发现:时间复杂度:O(n*logk)。空间复杂度:O(k)


补:从以上两个运用不难发现,堆结构的效率很高,在今后遇到的复杂问题中,我们都可运用高效率的堆结构来进行优化。

相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
40 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
86 16
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
100 1
|
3月前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
69 5
【数据结构】优先级队列(堆)从实现到应用详解
|
2月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
2月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
37 0
|
2月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
2月前
【数据结构】大根堆和小根堆
【数据结构】大根堆和小根堆
53 0