【初阶数据结构】7.二叉树(2)

3.实现顺序结构二叉树

一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。

3.1 堆的概念与结构

如果有一个关键码的集合 K = {k0, k1, k2, …,kn−1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足: Ki<= K2∗i+1Ki>= K2∗i+1Ki<= K2∗i+2 ),i = 0、1、2... ,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。

小根堆示例大根堆示例
6602d54790edac562f096f247e0f6c9737a409ee51f1104c1da9e9b576a36adf
父节点小于等于孩子节点父节点大于等于孩子节点
小堆堆顶是堆的最小值大堆堆顶是堆的最大值

堆具有以下性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树。

二叉树性质

  • 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0 开始编号,则对于序号为 i 的结点有:
  1. i > 0i 位置结点的父结点序号: (i-1)/2

    i=0i 为根结点编号,无双亲结点

  2. 2i+1 < n ,左孩子序号: 2i+12i+1 >= n 否则无左孩子

  3. 2i+2 < n ,右孩子序号: 2i+22i+2 >= n 否则无右孩子


3.2 堆的实现

Heap.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

//定义堆的结构---数组
typedef int HPDataType;

typedef struct Heap {
	HPDataType* arr;
	int size;//有效的数据大小
	int capacity;//空间大小
}HP;

//堆的初始化
void HPInit(HP* php);

//堆的销毁
void HPDestroy(HP* php);

//交换函数
void Swap(int* x, int* y);

//堆的向上调整算法
void AdjustUp(HPDataType* arr, int child);



//往堆里面插入数据
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->arr = NULL;
	php->size = php->capacity = 0;
}

//堆的销毁
void HPDestroy(HP* php) {
	assert(php);
	if (php->arr)
		free(php->arr);

	php->arr = NULL;
	php->size = php->capacity = 0;
}

//交换函数
void Swap(int* x, int* y) {
	int tmp = *x;
	*x = *y;
	*y = 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;
		}
	}
}

//堆的向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n) {
	int child = parent * 2 + 1;//左孩子
	while (child < n) {
		//小堆:找左右孩子中最小的
		//大堆:找左右孩子中最大的
		//看arr[child] > arr[child + 1]和if (arr[child] < arr[parent])里面的符号,符号不变就是小堆,反过来就是大堆
		if (child + 1 < n && 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 HPPush(HP* php, HPDataType x) {
	assert(php);
	//判断空间是否足够
	if (php->size == php->capacity) {
		//扩容
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
		//空间申请失败
		if (tmp == NULL) {
			perror("relloc fail!");
			exit(1);
		}
		//空间申请成功
		php->arr = tmp;
		php->capacity = newCapacity;
	}
	php->arr[php->size] = x;

	AdjustUp(php->arr, php->size);//如果上一步size++的话,这里要size-1

	++php->size;
}

//往堆里面删除数据
//出堆:出的是堆顶是数据
//用到了堆的向下调整算法
void HPPop(HP* php) {
	assert(php && php->size);
	Swap(&php->arr[0], &php->arr[php->size - 1]);//交换栈顶和栈底元素

	--php->size;//删除交换后的原栈顶元素

	AdjustDown(php->arr, 0, php->size);
}

//取堆顶数据
HPDataType HPTop(HP* php) {
	assert(php && php->size);

	return php->arr[0];
}

// 判断堆是否为空
bool HPEmpty(HP* php) {
	assert(php);
	return php->size == 0;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

//冒泡排序
//时间复杂度:0(N^2)
void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n; i++){
		int exchange = 0;
		for (int j = 0; j < n - i - 1; j++){
			//升序
			if (arr[j] > arr[j + 1]){
				exchange = 1;
				Swap(&arr[j], &arr[j + 1]);
			}
		}
		if (exchange == 0){
			break;
		}
	}
}


//堆排序
//空间复杂度为0(1)
//时间复杂度为O(n*logn)
void HeapSort(int* arr, int n)
{
	//建堆
	//打印升序---大堆
	//打印降序----小堆

	//向上调整算法建堆
	/*for (int i = 0; i < n; i++)
	{
		AdjustUp(arr, i);
	}*/

	//向下调整算法建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--){
		AdjustDown(arr, i, n);
	}

	//循环将堆顶数据跟最后位置(会变化,每次减少一个数据)的数据进行交换
	int end = n - 1;
	while (end > 0){
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);
		end--;
	}
}


//空间复杂度为0(n)
void test01() {
    HP hp;
    HPInit(&hp);

    int arr[] = { 17,20,10,13,19,15 };

    for (int i = 0; i < 6; i++) {
        HPPush(&hp, arr[i]);
    }

    while (!HPEmpty(&hp)) {
        printf("%d ", HPTop(&hp));
        HPPop(&hp);
    }

    //HPPop(&hp);

    //HPDestroy(&hp);
}

int main() {
    //test01();
    //给定一个数组,对数组中的数据进行排序
    int arr[] = { 17,20,10,13,19,15 };
    //BubbleSort(arr, 6);
	HeapSort(arr, 6);
    for (int i = 0; i < 6; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

3.2.1 向上调整算法

堆的插入

将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。

向上调整算法

  • 先将元素插入到堆的末尾,即最后一个孩子之后

  • 插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可

img

void AdjustUp(HPDataType* a, int child)
{
    int parent = (child - 1) / 2;
    while(child > 0)
    {
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (parent - 1) / 2;
        }
        else
        {
            break;
        }
    }
}
void HPPush(HP* php, HPDataType x)
{
    assert(php);
    if (php->size == php->capacity)
    {
        size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
        HPDataType* tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
        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);
}

计算向上调整算法建堆时间复杂度:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)

img

img

如果是根节点,不需要向上调整。

img

img

  1. T(n):这个表达式代表的是在堆的向上调整算法中,对于一个具有n个节点的堆,完成调整所需的移动步数。

    通过分层说明和移动步数计算解释,给出了一个关于h(堆的高度)的函数表达式,并最终通过一系列变换得到了T(h)的表达式。

  2. F(h):这个表达式代表的是在堆的向上调整算法中,对于高度为h的堆,完成调整所需的移动步数的另一种表达形式。

    最终目的是为了推导出F(n)

  3. F(n):这个表达式代表的是对于具有n个节点的堆,完成向上调整所需的移动步数的最终表达式。通过将T(h)的表达式转换为关于n的表达式,得到了F(n)的表达式。

    这个表达式是基于二叉堆的性质,即堆的节点数n与堆的高度h之间的关系==(n = 2h - 1),以及高度h与节点数n之间的关系(h = log2(n + 1))==。

由此可得:

向上调整算法建堆时间复杂度为:O(n ∗ log2n)


3.2.2 向下调整算法

堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

img

向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

向下调整算法

  • 将堆顶元素与堆中最后一个元素进行交换

  • 删除堆中最后一个元素

  • 将堆顶元素向下调整到满足堆特性为止

img

void AdjustDown(HPDataType* a, int n, int parent)
{
    int child = parent * 2 + 1;
    while (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;
        }
    }
}
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);
}

计算向下调整算法建堆时间复杂度:

img

img

最底层不需要向下移动

img

向下调整算法建堆时间复杂度为:O(n)


3.3 堆的应用

3.3.1 堆排序

代码1:基于已有数组建堆、取堆顶元素完成排序版本

// 1、需要堆的数据结构
// 2、空间复杂度 O(N)
void HeapSort(int* a, int n)
{
    HP hp;
    for(int i = 0; i < n; i++)
    {
        HPPush(&hp,a[i]);
    }
    int i = 0;
    while (!HPEmpty(&hp))
    {
        a[i++] = HPTop(&hp);
        HPPop(&hp);
    }
    HPDestroy(&hp);
}

该版本有一个前提,必须提供有现成的数据结构堆


代码2:数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据

// 升序,建大堆
// 降序,建小堆
// O(N*logN)
void HeapSort(int* a, int n)
{
    // a数组直接建堆 O(N)
    for (int i = (n-1-1)/2; i >= 0; --i)
    {
        AdjustDown(a, n, i);
    }
    // O(N*logN)
    int end = n - 1;
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        --end;
    }
}

堆排序时间复杂度计算:

img

img

通过分析发现,堆排序第二个循环中的向下调整与建堆中的向上调整算法时间复杂度计算一致,因此,堆排序的时间复杂度为O(n + n ∗ log n) ,即O(n log n)

堆排序时间复杂度为:O(n *log n)


3.3.2 TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

比如:

  1. 要存储4GB的内存数据,现在只有1GB,怎么找到里面最大的10个数?

    分四次建堆,每次建堆分别找到堆里面最大的10个数,最后在40个数里找到最大的10个数。

  2. 如果只有1KB怎么办呢?

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆

    要取前k个最大的元素,则建小堆

    要取前k个最小的元素,则建大堆

  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素

为什么要取前k个最大的元素,则建小堆呢?

因为在最小堆中,堆顶元素总是堆中最小的元素。当处理一个新元素时,如果这个新元素比堆顶元素大,那么它就有可能是最大的k个数之一。此时,我们把堆顶元素弹出,将新元素加入堆中。然后调整新元素在堆里面的位置。这样,堆中始终保持了当前遇到的最大的k个数。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

//生成随机数
//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) % 1000000;
//        fprintf(fin, "%d\n", x);
//    }
//    fclose(fin);
//}

//定义堆的结构---数组
typedef int HPDataType;

typedef struct Heap {
    HPDataType* arr;
    int size;//有效的数据大小
    int capacity;//空间大小
}HP;

//交换函数
void Swap(int* x, int* y) {
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

//堆的向下调整算法
void AdjustDown(HPDataType* arr, int parent, int n) {
    int child = parent * 2 + 1;//左孩子
    while (child < n) {
        //小堆:找左右孩子中最小的
        //大堆:找左右孩子中最大的
        //看arr[child] > arr[child + 1]和if (arr[child] < arr[parent])里面的符号,符号不变就是小堆,反过来就是大堆
        if (child + 1 < n && arr[child] > arr[child + 1]) {//防止越界
            child++;
        }

        if (arr[child] < arr[parent]) {
            Swap(&arr[child], &arr[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else {
            break;
        }
    }
}

//TOP-K排序
void topk()
{
    printf("请输入k:>");
    int k = 0;
    scanf("%d", &k);

    //从文件中读取前k个数据,建堆
    const char* file = "data.txt";
    FILE* fout = fopen(file, "r");
    if (fout == NULL)
    {
        perror("fopen error");
        return;
    }

    int val = 0;
    int* minheap = (int*)malloc(sizeof(int) * k);//创建小堆
    if (minheap == NULL)
    {
        perror("malloc error");
        return;
    }
    for (int i = 0; i < k; i++)//循环读取数据,先读取k个
    {
        fscanf(fout, "%d", &minheap[i]);
    }
    // 建k个数据的小堆
    for (int i = (k - 1 - 1) / 2; i >= 0; i--)//循环读取数据,读取n-k个
    {
        AdjustDown(minheap, i, k);
    }

    int x = 0;
    while (fscanf(fout, "%d", &x) != EOF)
    {
        // 读取剩余数据,比堆顶的值大,就替换他进堆
        if (x > minheap[0])
        {
            minheap[0] = x;
            AdjustDown(minheap, 0, k);
        }
    }

    for (int i = 0; i < k; i++)//打印最大的k个数据
    {
        printf("%d ", minheap[i]);
    }

    fclose(fout);
}

int main() {
    //CreateNDate();
    topk();
    return 0;
}

时间复杂度:O(n) = k + (n − k)log2k

  • 35
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值