数据结构---二叉树的顺序结构及实现

深渊向深渊呼唤

二叉树的顺序结构及实现

1. 二叉树的顺序结构 2. 堆的概念及结构 3. 堆的实现 3.1 堆向下调整算法 3.2 堆排序 3.2.1 堆排序完整代码 3.3 堆的插入 3.3.1 堆的向上排序算法 3.4 堆的删除 4. 完整堆代码 5.topK问题 5.1 剑指offer第40题---最小的K个数

1. 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
在这里插入图片描述

2. 堆的概念及结构

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。(简单点说就是所有的孩子都大于双亲结点的值称为小堆----就是要把最小的不停的往上浮。所有的孩子都小于双亲结点的值称为大堆—就是要把最大的值不停的向上浮)

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

在这里插入图片描述

3. 堆的实现

3.1 堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

int a[] = {27,15,19,18,28,34,65,49,25,37};
在这里插入图片描述
左右子树是一个小堆的特点,就可以使用向下调整算法,进行调整。
在这里插入图片描述

//交换
void Swap(HPDateType* p1,HPDateType* p2)
{
	HPDateType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//向下调整算法
//前提:左右子树都是小堆
void AdjustDown(HPDateType* a, int n, int root)
{
//这个root表示从哪里开始调整的位置
	int parent = root;
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		//找出左右孩子中小的那一个
		//这是一个完全二叉树,右孩子可能是不存在的,所以child +1 有可能是会越界的
		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;
		}
	}
}

3.2 堆排序

有了向下调整算法就可以考虑实现堆排序的问题!
但是你要使用向下调整算法的前提应该保证他的左右子树都是小堆,所以初始化要先完成。

void HeapInit(Heap* php, HPDateType* a, int n)
{
	php->_a = (HPDateType)malloc(sizeof(HPDateType)*n);
	if (php->_a == NULL)
	{
		printf("内存不足\n");
		exit(-1);
	}
	memcpy(php->_a, a, sizeof(HPDateType)*n); //我有一个数组,我一上来就希望你把这个数组里面的值都放在你所开辟的这个堆里面
	php->_size = n;
	php->_capacity = n;
	//你把数组的元素都放在了你所开辟的数组空间里面就是堆了吗?
	//显然目前还不是堆
	
	//构建小堆
	//首先保证左右子树都是小堆的情况下才能进行向下调整算法的操作
	//但是这里进行调整左右子树的时候要从最下面开始调起(倒着来)
	//把每一个三角都看作是一个小堆,进行调整
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(php->_a, php->_size, i);
	}

}

这里就有思考的问题:堆排序的时间复杂度是多少?为什么要使用堆排序

void HeapSort(int* a, int n)
{
	//建堆
	//这里可以i=n-1开始调,也就是最后一个位置开始调整,但是你会发现那些都是叶子,你算他的孩子(但是它压根就没有孩子,所以这里对叶子进行向下调整是没有意义的,应该从他的最后一个双亲结点开始调整)
	//这里的n-1表示数组的最后一个元素下角标,然后带入知道孩子求双亲结点下角标的公式中
	for (int i = (n - 1-1)/2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
}

int main()
{
	int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
	HeapSort(a, sizeof(a) / sizeof(HPDateType));
	return 0;
}

在这里插入图片描述
在这里插入图片描述
因为你并没有规定左右孩子谁大谁小,所以堆排序一次并不是有序的。但是你会发现堆排序一次以后你选出了小堆里面最小的哪一个。

堆排序的时间复杂度为多少?,为什么要使用堆排序的?
对于一个满二叉树来说是件复杂度就是树的高度h=Log(N+1) 这里表示以2为底,N+1为对数。对于完全二叉树来说h=LogN 这里表示以2为底,N为对数。对于堆排序的时间复杂度是N * O(LogN)这里可以参考文章:链接: link.

解释:你每一次使用向下调整算法得到一个最小的数的时间复杂度就是其高度h=LogN,但是你一共有N个数需要遍历,所以堆排序的时间复杂度是N * LogN
你会发现堆排序的时间复杂度要小的多,那么当你需要排序的数字非常大的时候,他可以为你节省很多时间。(如果你每一次选出最小的数以后,在以这个小堆根结点的下一个数作为新的根结点,重新进行一次堆排序,不但会破坏掉原来的结构,而且时间复杂度为O(N*N) )

那么如果你想要的这个堆里面的次小的数如何得到呢?
排降序:建小堆
在这里插入图片描述
堆排序完一次以后可以选出最小的哪一个数,然后让他和数组的最后一个元素交换,数组在缩短一个(相当于把最小的那个数排除在外),重新进行一次堆排序,不断重复,就会得到一个降序的数组,从而得到次小的数。

排升序:建大堆,进行和上面相同的步骤(只需要把大孩子往上浮就行)

3.2.1 堆排序完整代码

void HeapSort(int* a, int n)
{
	//建堆
	//这里可以i=n-1开始调,也就是最后一个位置开始调整,但是你会发现那些都是叶子,你算他的孩子(但是它压根就没有孩子)
	for (int i = (n - 1-1)/2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1; //end代表的数组里面的元素个数
	while (end>0)
	{
		Swap(&a[0], &a[end]);
		//在继续选次小的
		AdjustDown(a, end, 0);
		end--;
	}
}
int main()
{
	int a[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
	HeapSort(a, sizeof(a) / sizeof(HPDateType));
	return 0;
}

在这里插入图片描述
在这里插入图片描述

3.3 堆的插入

这里一定要深刻记住完全二叉树的定义,你要往这个堆里面插入数据,应该在那个位置插入。
本来你这里的左右子树都是小堆,但是你现在插入的是一个大于父亲的数那么依旧保持着小堆的特点,但是你在这里如果插入的是一个小于父亲的数,则这里就不在保持小堆的特点,所以需要堆的向上排序算法

3.3.1 堆的向上排序算法

在这里插入图片描述
在这里插入图片描述

void AdjustUp(HPDateType* a, int n, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0) //因为当child=0的时候,此时parent早已经小于0了,想不通的话,自己画个堆来看
	{
		if (a[child] < a[parent])
		{
			//这里考虑的是如果你插入的这个数比父亲要小,说明此时你不能保持小堆的特点,需要交换
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
void HeapPush(Heap* php, HPDateType x)
{
	//这里一定要深刻记住完全二叉树的定义,你要往这个堆里面插入数据,应该在那个位置插入
	//本来你这里的左右子树都是小堆,但是你现在插入的是一个大于父亲的数那么依旧保持着小堆的特点,但是你在这里如果插入的是一个小于父亲的数
	//此时你的小堆的特点就不对了,你应该怎么办?(需要处理了)
	//堆这个特点就是借助数组的下标来表示父子关系的
	//所以此时这里需要一个向上调整算法
	assert(php);
	//只要是顺序表的加入数据就要考虑到是否需要增容
	if (php->_size == php->_capacity)
	{
		php->_capacity *= 2;
		HPDateType* tmp = (HPDateType*)realloc(php->_a, sizeof(HPDateType)*php->_capacity);
		if (tmp == NULL)
		{
			printf("开辟失败\n");
			exit(-1);
		}
		else
		{
			php->_a = tmp;
		}
	}
	php->_a[php->_size++] = x; //这里的++是后置的,先会使用php->_size然后使用完了之后才会++,因为你的size是有效的数据也在不断的增加
	AdjustUp(php->_a, php->_size, php->_size - 1);
}

3.4 堆的删除

需要删除的是堆顶的数值。

void HeapPop(Heap* php) //因为只有干掉堆顶的这个最小的值,你才有机会找到次小的数
{
	//这里的pop的目的是删除掉堆顶的数据
	//你要明白一旦改变了堆顶的数据,就会发现整个结构就会发生改变,父子关系什么的都会重新
	assert(php);
	assert(php->_size > 0);// 如果这里都没有数据了你还在那里删什么


	Swap(php->_a[0], php->_a[php->_size - 1]);
	php->_size--;//这里是真的要干掉堆顶这个数据
	AdjustDown(php->_a, php->_size, 0);

}

4. 完整堆代码

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
typedef int HPDateType;


//小堆
typedef struct Heap
{
	HPDateType* _a;
	int _size;
	int _capacity;
}Heap;


void AdjustDown(HPDateType* a, int n, int root);

void AdjustUp(HPDateType* a, int n, int child);

void HeapInit(Heap* php, HPDateType* a, int n);

void HeapDestory(Heap* php);

void HeapPush(Heap* php, HPDateType x);

void HeapPop(Heap* php);

HPDateType HeapTop(Heap* php);
#include"Heap.h"

//交换
void Swap(HPDateType* p1,HPDateType* p2)
{
	HPDateType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//向下调整算法
//前提:左右子树都是小堆
void AdjustDown(HPDateType* a, int n, int root)
{
	int parent = root;
	int child = parent * 2 + 1;//左孩子
	while (child < n)
	{
		//找出左右孩子中小的那一个
		//这是一个完全二叉树,右孩子可能是不存在的,所以child +1 有可能是会越界的
		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 HeapInit(Heap* php, HPDateType* a, int n)
{
	php->_a = (HPDateType)malloc(sizeof(HPDateType)*n);
	if (php->_a == NULL)
	{
		printf("内存不足\n");
		exit(-1);
	}
	memcpy(php->_a, a, sizeof(HPDateType)*n); //我有一个数组,我一上来就希望你把这个数组里面的值都放在你所开辟的这个堆里面
	php->_size = n;
	php->_capacity = n;
	//但是目前还不是堆,你只是把数组里面的内容都放在了这里面

	//构建堆
	//首先保证左右子树都是小堆的情况下才能进行向下调整算法的操作
	//但是这里进行调整左右子树的时候要从最下面开始调起(倒着来)
	//把每一个三角都看作是一个小堆,进行调整
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(php->_a, php->_size, i);
	}

}

void AdjustUp(HPDateType* a, int n, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0) //因为当child=0的时候,此时parent早已经小于0了,想不通的话,自己画个堆来看
	{
		if (a[child] < a[parent])
		{
			//这里考虑的是如果你插入的这个数比父亲要小,说明此时你不能保持小堆的特点,需要交换
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}



void HeapDestory(Heap* php)
{
	assert(php);
	free(php->_a);
	php->_a = NULL;
	php->_size = php->_capacity = 0;
}

void HeapPush(Heap* php, HPDateType x)
{
	//这里一定要深刻记住完全二叉树的定义,你要往这个堆里面插入数据,应该在那个位置插入
	//本来你这里的左右子树都是小堆,但是你现在插入的是一个大于父亲的数那么依旧保持着小堆的特点,但是你在这里如果插入的是一个小于父亲的数
	//此时你的小堆的特点就不对了,你应该怎么办?(需要处理了)
	//堆这个特点就是借助数组的下标来表示父子关系的
	//所以此时这里需要一个向上调整算法
	assert(php);
	//只要是顺序表的加入数据就要考虑到是否需要增容
	if (php->_size == php->_capacity)
	{
		php->_capacity *= 2;
		HPDateType* tmp = (HPDateType*)realloc(php->_a, sizeof(HPDateType)*php->_capacity);
		if (tmp == NULL)
		{
			printf("开辟失败\n");
			exit(-1);
		}
		else
		{
			php->_a = tmp;
		}
	}
	php->_a[php->_size++] = x; //这里的++是后置的,先会使用php->_size然后使用完了之后才会++,因为你的size是有效的数据也在不断的增加
	AdjustUp(php->_a, php->_size, php->_size - 1);
}

void HeapPop(Heap* php) //因为只有干掉堆顶的这个最小的值,你才有机会找到次小的数
{
	//这里的pop的目的是删除掉堆顶的数据
	//你要明白一旦改变了堆顶的数据,就会发现整个结构就会发生改变,父子关系什么的都会重新
	assert(php);
	assert(php->_size > 0);// 如果这里都没有数据了你还在那里删什么


	Swap(php->_a[0], php->_a[php->_size - 1]);
	php->_size--;//这里是真的要干掉堆顶这个数据
	AdjustDown(php->_a, php->_size, 0);

}

HPDateType HeapTop(Heap* php)
{
	//取堆顶的数据,就是0的位置
	assert(php);
	assert(php->_size > 0);
	return php->_a[0];
}

5.topK问题

N个数中找出最大的或者最小的前K个数。

5.1 剑指offer第40题—最小的K个数

链接: link.
在这里插入图片描述
解题思路:我取数组中的前k个数构建一个大堆,那么这个大堆的堆顶就是我所选出来的这k个数中最大的一个数,然后我让没有进入堆的数去和堆顶比较(大的数我是不想要的,因为我在找小的数,所以直接把大的数替换掉)最后你所剩下的这个大堆就是一个最小的前k个数所构建的大堆,刚好这个堆顶就是最大的数,通过这个思想还可以去考虑解决别的题目。
牛客网中也有这道题,最小的k个数。

链接: link.

牛客网:寻找第K大的数。

思想和这道题的解题思路类似,当好你最后所构建的前k个数的大堆的堆顶,就是我要找的第K个大的数。
链接: link.

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
 void Swap(int* p1,int* p2)
 {
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
 }
 void AdjustDown(int* a,int n,int root)
 {
    int parent = root;
    int child = parent*2 + 1;//这个是左孩子的下角标,我就假设他是两个孩子中较大的那一个
    while(child < n)//想的是结束的条件,但写的要是继续的条件,当你的parent到最后一个叶子结点处,此时你的孩子就越数组了
    {
        //确保选出两个孩子中较大的那一个
        if(child+1 < n && a[child+1]>a[child])
        {
            child++;
        }

        if(a[child]>a[parent])
        {
            Swap(&a[child],&a[parent]);
            //让其迭代起来,不要忘记他是向下调整算法,这里的root是从父亲结点开始调整的
            parent = child;
            child = parent*2 + 1;
        }
        else
        {
            break;
        }

    }     
 }
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize){
    *returnSize = k;
    //说明你要找的数就没有,返回的空的是一个空数组
    if(k == 0)
        return NULL;
    int* retArr = (int*)malloc(sizeof(int)*k);

    //建k个数的大堆
    for(int i = 0;i<k;i++)
    {
        retArr[i] = arr[i];  //我直接把数组里面的k个数拿到我所创建的这个所谓堆(此时还不能叫堆,或许不满足堆特点)里面,此时并不能保证最小的就在这个里面
    }
    //从他的最后一个双亲结点开始调整
    for(int i = (k-1-1)/2;i>=0;--i)
    {
        AdjustDown(retArr,k,i);
    } //此时你的大堆就已经构建完成了,你前k个数中最大的数就在堆顶,而小的数都在最下面

    //除了那K个数剩下的数
    for(int j = k;j < arrSize;j++)
    {
        //我要的是前k个小的数,我不要大的数,大的数可以直接被踢掉
        if(arr[j]<retArr[0])
        {
            retArr[0] = arr[j];
            AdjustDown(retArr,k,0);
        }
        //最后遍历完剩下的K-1个数,只要是前k个小的数,最后都会进堆
    }
    return retArr;
}

在这里插入图片描述

栏目