二叉树的顺序结构存储(堆的实现)

先附上之前写的关于树的知识点的回顾
树的概念
二叉树有两种常见的结构方式,一种是顺序存储(就是用数组来存储),一种是链表结构来存储
但是普通的二叉树是不适合用数组来存储,因为可能会存在大量的空间浪费,而完全二叉树是可以用顺序结构来存储的,现实中我们通常把堆(一种二叉树)使用顺序结构数组来存储,需要注意的是这里的堆和我们操作系统虚拟进程地址空间的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域

二叉树的顺序结构存储(堆的实现)_第1张图片

堆的概念及结构

如果有一个集合K= {k0,k1,k2,k3,k4,k5,kn-1},把他的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足:
K1<=K2i+1且Ki<=K2i+2 (i = 0,1,2,)则成为小堆 ,那么反之如果 K1>= K2i+1且Ki>=K2i+2 就是大堆啦
他的性质:
堆中的某个节点的值总是不大于或不小于其父节点的值(大于还是小于父亲节点,就要看建立的是什么堆了)
堆是一棵完全二叉树

满足这两点就是一个堆

堆的向下调整算法(堆创建的重要一步!)

首先我们要给出一个数组,逻辑上可以看做是一棵完全二叉树,我们通过从根节点开始向下调整,把他调整成小堆,但是向下调整有一个前提:左右子树必须是一个堆才可以调整,如果不是就不能调整!!!

int a[] = {27,15,19,18,28,34,65,49,25,37}

下面我们画出这个完全二叉树,并给出向下调整法

二叉树的顺序结构存储(堆的实现)_第2张图片
注意的是我们这个图除了根节点引导的不满足小堆,剩余的两个子树均满足小堆性质,我们这样设置只是为了方便理解,理解之后,我闷才可以将乱序的堆调整成我们想要的堆
向下调整的几个步骤
:
1>找出根节点
2>判断其两个子节点的大小,我们这里调整成小堆,所以找出较小的孩子
3>判断父节点和较小的孩子,如果父节点比那个较小子节点小,那么不用交换,如果大就继续交换
当我们如果发生了交换可能上面满足了性质,但是又导致下面不满足性质,所以需要循环判断
利用 parent = child;可以进行到下一层判断,然后又从1>开始判断,
何时跳出呢,当我们parent不断被child赋值,child又会等于2*parent+1;
这样当我们的child>size的时候说明下一层已经没有了元素,刚才判断的就是最后一层,到底了

二叉树的顺序结构存储(堆的实现)_第3张图片
注意看我们最后一层,其实还是会赋值的,因为我们在我们的循环里面,最终赋值之后 child = 17
这时候就需要跳出循环了
下面我们来实现代码

int parent = 0;   			//注意此处我们从根节点开始调整也就是0号位置,我们将Parent的下标传进去
void AdjustDown(int *arr,int parent,int size){	
	int child = 2*parent+1;
	//我们进来可以先判断一下是否满足小堆特性,满足就不用执行程序了
	if(arr[parent] < arr[child]) {
		return;
	}
	while(childarr[child+1]) {
		child = child +1;		
		}
	//判断父子节点是否交换,如果发生了交换就需要进入下一层进行循环判断,一直到 child >= size
	//注意等于size也意味着没有,因为我们进行的是数组元素的比较
	//最大的数组小标也仅仅是arr[size-1];
	if(arr[parent] < arr [child]) {
		Swap (&arr[parent],&arr[child]);
		parent = child;
		chlid = 2*parent+1;
		}
	else {
		rerutn ;
		}
	}
}

这是一个特殊情况,我们为了说明小堆的性质,构建了一个特殊的小堆,除了根节点剩下子树都符合小堆的特性,那么假如一个数组是随机的,没有这种特性的话,我们应该怎么构建这个小堆呢???

1>首先把他初始化为一个二叉树,
二叉树的顺序结构存储(堆的实现)_第4张图片
在我们上面写的程序中,我们是从根节点开始的,根节点下面又是子树,两个孩子节点又是其自己孩子节点的父节点,那么我们可以利用这个不挺循环的性质,来对二叉树进行堆的创建了,我们从最后一个非叶子节点开始,把他当他们当成一个根节点来遍历子树,
例如上面的树中,我们先来遍历数字为7的那个非叶子节点,其下标为[size-2]/2 因为我们的size记录的是数组的总长,所以size-1 是树的最后一个节点,利用孩子节点和父节点的性质,我们得出其父节点为(size-2)/2 ,也就是数字为7 的那个元素在二叉树中的下标,这样我们可以做一层封装如下面的程序

//创建一个堆
//我们在堆中不停的调用向下调整法,与上面不同的是,我们的根节点在不停的变换,不断的--操作
void MakeHeap(int *arr,int size) {
	for(int root = (size-2)/2; root >= 0; root++) {
		AdjustDown(arr,root,size);
	}
}

上面的函数中我们表示我的是创建一个堆
我们在函数中不停的调用向下调整法,与上面不同的是,我们的根节点在不停的变换,不断的–操作,也就是遍历最后一个非叶子节点,遍历完了之后就遍历上一个非叶子节点.这样我们就可以完成调整了

我们其实是倒着讲的,我们首先完成了堆的创建,但是我们并没有给出堆的结构是怎么构成的,现在我们给出堆的结构

typedef int DataType;
typedef struct Heap{
	DataType *arr;				//存储堆的数组
	int size;						//堆的大小
	int capacity;					//堆的容量
}

其实是和线性表一样的操作,围绕堆也有一些操作,我们贴出来:

void HeapInit(Heap * hp, HPDataType *arr, int size);
void HeapDerstory(Heap *hp);
void HeapPush(Heap *hp, HPDataType data);
void HeapPop(Heap *hp);
HPDataType HeapTop(Heap *hp);
int HeapSize(Heap *hp);
int EmptySize(Heap *hp);

我们来说一下堆的插入,我们将元素插入了堆中,那么这个堆里面的元素的位置是有可能发生变化的,我们不可能再用向上调整法再次排一遍这是比较浪费时间的,事实上我们只需要比较插入的元素和其父节点的关系,如果(我们是小堆关系)父节点比孩子节点小,那么就不用调整,如果大的话,就需要调整了,同理调整 之后可能引起上一层关系的不满足,那么我们循环调用就是了,每次比较结束之后

child = parent;
parent = (child-1)/2;

那么何时退出呢,假如我们比较到最后一层才比较结束,这时候该退出了吧,这时候parent是最上面的父节点 parent下标为0 ,赋给child ,那么我们用while(child) 不就可以判断循环是否结束了啦!

代码如下
:

//插入用的是向上调整
//建立在已经建立堆的前提下
void AdjustUp(HPDataType *arr, int child, int size){
	int parent = (child - 1) / 2;
	while (child){
		if (arr[parent] > arr[child]){
			Swap(&arr[parent], &arr[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else return;
	}
}
void HeapPush(Heap *hp, HPDataType data){
	CheckCapacity(hp);
	hp->arr[hp->_size] = data;
	hp->_size++;
	AdjustUp(hp->arr, hp->_size-1,hp->_size);

}

说完了堆的插入,我们再说堆的弹出,我们在弹出的时候只能弹出顶部的元素,我们弹出了顶部的元素,那么顶部的元素就为空了,这时候列表下标为0的数字就是空的,这时候我们怎样继续把他变为一个小堆呢??
我们可以用到交换,我们使最后一个尾部元素和根节点元素交换,然后size–,删除最后一个元素,然后再向下调整就好了!

void HeapPop(Heap *hp){
	if (EmptySize(hp)){
		return;
	}
	Swap(&hp->arr[0], &hp->arr[hp->_size - 1]);    //交换元素
	hp->_size--;
	AdjustDown(hp->arr, 0, hp->_size);
}

说完了插入和弹出,我们再来说最后一个应用,也就是堆的排序,那么堆如何排序呢,说到现在,可以说是比较简单了,我们完成了堆的创建,创建了一个小堆,注意的是你创建的大堆和小堆,决定了你这个序列只能排什么升序或者降序,为什么呢??

小堆的性质是其根节点绝对 是最小的元素,我们利用这个性质,和我们上面所讲的堆的弹出,我们将根节点元素和尾部元素交换,再 不断排除掉最后一个元素在我们要排序的数组的范围中,这样每次我们利用向下排序法都可以找到最小的元素,然后不断循环交换,就可以取得一个降序的序列了

void HeapSort(int *arr, int size){
	assert(arr);
	//建堆
	MakeHeap(arr,size);
	//调整
	for (size; size >= 1;size--){
		//根节点和最后一个数字调换
		Swap(&arr[0], &arr[size-1]);
		//从上开始调整,因为已经排成了我们要求的堆序
		//size的长度要减一
		HeapAdjust(arr, 0, size - 1);
	}
}

关于TOP k 的问题(海量数据问题)

TOPK问题就是在海量数据中,找出K个最大的数
既然我们提到了,就要用到我们的堆了,假如我们要找的是前K个最大的数,那么我们就要建立一个堆,我们先从这海量数据中读出前K个元素,进行建堆我们要建立一个大堆,将最大的放在根节点,大堆建立好之后,进行排序,之后,我们开始遍历海量数据,和上面的根节点元素进行比较,如果比根节点大那么就产生替换,再次进行大堆的调整,如此循环,最后排序得到的就是前K个大的元素

代码如下

#include
#include 
#include 
#include 
//1.将集合内前K个元素取出建立一个大堆

//2.建好之后去排序,生成升序序列

//3.排序完成继续导入数据,进行调整

#define MAX   1000
void Swap(int *a, int *b){
	int temp = *a;
	*a = *b;
	*b = temp;
}
void AdjustHeap(int *Heap,int parent,int size ){
	int child = 2 * parent + 1;
	while (child < size){
		if (child + 1 < size && Heap[child + 1] > Heap[child]){
			child = child + 1;
		}
		if (Heap[parent] < Heap[child]){

			Swap(&Heap[parent], &Heap[child]);
		}
		parent = child;
		child = 2 * parent + 1;
		
	}
}
void CreatHeap(int *Heap, int size){
	assert(Heap);
	for (int i = size / 2 - 1; i >= 0; i--){	
		AdjustHeap(Heap, i, size);
	}
	for (size; size >= 1; size--){
		Swap(&Heap[0], &Heap[size - 1]);
		AdjustHeap(Heap, 0, size - 1);
	}


}

int main(){

	/*int Heap[10] = { 3, 4, 5, 8, 1, 2, 0, 9, 7, 6 };*/
	int Heap[10];
	int Data[MAX];
	int size = sizeof(Heap) / sizeof(Heap[0]);
	srand((unsigned int)time(NULL));
	for (int i = 0; i < MAX; i++){
		Data[i] = rand();
		}
		for (int i = 0; i < 10; i++){
		Heap[i] = Data[i];
		}
	//创建堆并且排序
	CreatHeap(Heap, size);

	for (int i = 10; i < MAX; i++){
		if (Data[i]>Heap[0]){
			Heap[0] = Data[i];
			CreatHeap(Heap, size);
		}
	}
	for (int i = 0; i < size; i++){
		printf("%d ", Heap[i]);
	}
	return 0;
}

你可能感兴趣的:(数据结构)