目录
一、堆的概念和性质(大堆小堆)
堆是一个数据结构,不是操作系统上的堆,堆的原理就是完全二叉树(只不过堆需要满足小堆和大堆才是堆结构),它底层是通过数组存储方式进行存储的,物理结构是线性的,但是逻辑结构不是线性的。
大堆是由大到小的顺序开始建堆,而小堆是从小到到进行排列
堆有以下性质:
1.堆中某个结点的值总是不大于(大堆)或不小于(小堆)父结点
2.堆总是一颗完全二叉树。
二叉树性质:对于具有n个结点的完全二叉树,如果按照从上到下从左到右顺序从0开始为根结点,对于序列为i的节点:
1.i>0,i位置节点的双亲序列为(i-1)/2;i=0,为根结点无双亲结点。
2.若2i+1<n,则左孩子的序列为2i+1,2i+2<n。右孩子的序列为2i+2。
二、堆的实现
2.1 堆的初始化
初始化时先定义堆结构
//堆结构
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;//数组存储
int size;//有效元素
int capacity;//空间
}Heap;
初始化
//初始化
void HeapInit(Heap* php)
{
php->arr = NULL;
php->capacity = php->size = 0;
}
2.2 堆的销毁
//销毁
void HeapDestroy(Heap* php)
{
assert(php);
php->arr = NULL;
php->capacity = php->size = 0;
}
2.3 堆的插入(向上调整算法)
//堆的插入(向上调整算法)
void HeapPush(Heap* php, HPDataType x)
{
//空间不够
if (php->capacity == php->size)
{
int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
php->arr[php->size] = x;
//向上建堆
AdjustUp(php->arr,php->size);
php->size++;
}
这里需要应该向上调整算法,插入完后调整。
堆的插入我们用图来描述一下
纠正一下parent = (child-1)/2。
通过这个图,不难发现当child=0时,跳出循环,child=parent,parent=(child-1)/2,然后当我们发现插入的值比parent大时向上兑换(大堆),向上调整算法代码实现就很简单了:
//交换
void Swap(HPDataType* change1, HPDataType* change2)
{
HPDataType tmp = *change1;
*change1 = *change2;
*change2 = tmp;
}
//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{
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;
}
}
}
2.4 删除堆顶元素(向下调整算法)
//删堆顶
void HeapPop(Heap* php)
{
assert(!HeapEmpty);
//堆顶和最后一个叶子结点交换
Swap(&php->arr[0], php->arr[php->size - 1]);
php->size--;
//向下调整
AdjustDown(php->arr, 0, php->size);
}
删除堆顶数据时需要堆顶和最后一个叶子结点交换然后size减减,然后进行向下调整算法调整堆。
删堆顶可以用这个gif图来表示
通过这个gif图可知这个向下调整算法需要对比左右孩子哪个大(小)才能和parent进行比较,并将大的孩子与parent进行比较,比孩子小则要调整。
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int size)
{
int child = parent * 2 + 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 = parent * 2 + 1;
}
else
{
break;
}
}
}
2.5 堆的删除
//取堆顶数据
HPDataType HeapTop(Heap* php)
{
assert(!HeapEmpty(php));
return php->size[0];
}
2.6 堆的判空
//判空
bool HeapEmpty(Heap* php)
{
assert(php);
return php->size == 0;
}
2.7 求堆中元素个数
//堆中元素个数
int HeapSize(Heap* php)
{
assert(php);
return php->size;
}
三、堆的应用
3.1 向下调整算法建堆
利用堆的思想来给数组建堆
//向下调整算法
void AdjustDown(HPDataType* arr, int parent, int size)
{
int child = parent * 2 + 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 = parent * 2 + 1;
}
else
{
break;
}
}
}
void CreateHeap(int* arr, int size)
{
//向下建堆
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
//通过遍历父节点来建堆
AdjustDown(arr, i, size);//大堆
}
for (int j = 0; j < size; j++)
{
printf("%d ", arr[j]);
}
}
3.2 向上调整算法建堆
void CreateHeap(int* arr, int size)
{
//向上建堆
for (int i = size; i > 0; i--)
{
//遍历子结点建堆
AdjustUp(arr, i);
}
for (int j = 0; j < size; j++)
{
printf("%d ", arr[j]);
}
}
3.3 堆排序
在进行堆排序之前,我们先讨论两个建堆算法的时间复杂度,
这里可得向下排序算法时间复杂度要小,所以我们在用堆排序时,这里是用的向下建堆的。
void HeapSort(int* arr, int size)
{
//大堆---升序
//小堆---降序
for(int i = size-1; i > 0;i--)
{
Swap(&arr[0], &arr[i]);//堆顶和尾叶子结点交换
AdjustDown(arr, 0,i);//调整为大堆
}
//打印顺序
printf("堆排序后:");
for (int j = 0; j < size; j++)
{
printf("%d ", arr[j]);
}
printf("\n");
}
void CreateHeap(int* arr, int size)
{
//向下建堆
for (int i = (size - 1 - 1) / 2; i >= 0; i--)
{
//通过遍历父节点来建堆
AdjustDown(arr, i, size);//大堆
}
//向上建堆
//for (int i = size; i > 0; i--)
//{
// //通过遍历子结点来建堆
// AdjustUp(arr, i);
//}
printf("堆排序前:");
for (int j = 0; j < size; j++)
{
printf("%d ", arr[j]);
}
printf("\n");
}
int main()
{
int arr[] = { 10,21,2,9,20,13,12,58 };
int size = sizeof(arr) / sizeof(arr[0]);
CreateHeap(arr,size);
HeapSort(arr, size);
return 0;
}
3.4 TOP-K问题
对与这种问题我们一般都是将所以数据都存储到数组中,然后进行堆排序。但是当数据非常大时,加上内存有限的条件下,这种方法不行。那我们该如何解决,当然这里还是离不开堆排序,我们可以吧前k个数据存储到数组中,然后进行建堆,并堆排序,取堆顶和升序n-k个数据进行比较,比较大小进行调堆,获得最k个最值。
//Top——K问题
void PrintTok(int k)
{
//申请空间
int* arr = (int*)malloc(sizeof(int) * k);
if (arr == NULL)
{
perror("malloc");
exit(2);
}
//打开空间
FILE* file = fopen("data.txt", "r");
if (file == NULL)
{
perror("fopen fail");
exit(3);
}
//将k个数据移入数组中
for (int i = 0; i < k; i++)
{
fscanf(file, "%d", &arr[i]);
}
//向下建堆
for (int j = (k - 1 - 1) / 2; j >= 0; j--)
{
AdjustDown(arr, j, k);//大堆
}
//将n-k个数据与堆顶进行比较
//堆顶 > n-k内的数据则交换,再向下调整
int x = 0;
while (fscanf(file, "%d", &x) != EOF)
{
if (arr[0] > x)
{
arr[0] = x;
AdjustDown(arr, 0, k);
}
}
for(int m = 0;m < k;m++)
{
printf("%d ", arr[m]);
}
printf("\n");
free(arr);
arr = NULL;
//关闭文件
fclose(file);
}
//写数据到文件中
void CreateNData()
{
//int n = 100000;
//srand((unsigned int)time(0));
////打开文件
//const char* txt = "data.txt";
//FILE* file = fopen(txt, "w");
//if (file == NULL)
//{
// perror("fopen fail");
// exit(1);
//}
////存储到data.txt
//for (int i = 0; i < n-10; i++)
//{
// fprintf(file, "%d\n", rand() % 100000);
//}
//for (int j = n - 10; j < n; j++)
//{
// fprintf(file, "%d\n", j % 10000);
//}
//
////关闭文件
//fclose(file);
//
int n = 100000;
srand((unsigned int)time(0)); // 设置种子
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen fail");
exit(1);
}
// 生成普通随机数
for (size_t i = 0; i < n - 10; i++)
{
int x = rand() % 100000;
fprintf(fin, "%d\n", x);
}
// 添加10个明显的大值(100000以上)
for (int i = 0; i < 10; i++)
{
fprintf(fin, "%d\n", 100000 - i);
}
fclose(fin);
}
int main()
{
CreateNData();
int n = 0;
printf("输入k为:");
scanf("%d", &n);
//大堆---前k个最小
//小堆---前k个最大
PrintTok(n);
return 0;
}
四、总结与反思
堆在排序中优势很好,时间复杂度就为O(nlogn),但是无论如何都避免不了和冒泡排序一样将所有的都遍历了一边,在解决TOP——K问题上门还是有很大用处的,可以避免在操作系统上申请空间,在提高性能方面有很大作用。而相对于向上调整算法,向下调整算法建堆的时间复杂度要低,能够优化算法,提高编译效率。这个建堆的思想能够为我们收获最大值(小堆)和最小值(大堆),为我们优化排序提供了一个好方法。