今天我给大家介绍的是堆,其实大家要深刻理解数据结构这个概念,队列也好,栈也好,二叉树也好,还有我今天要说的堆,其实他并不是说,他就是一个单独的数据结构或者说是一种和之前完全不一样的东西,这些东西是你用之前的知识对他进行特定的设定而构造出来的。就比如说栈经常是用数组来实现,队列,二叉树经常是用链表来实现,而并不是说他就是一个单独的全新的数据结构,为什么我要说这些是因为这个堆其实包括形状也好甚至思路也好他和之前的二叉树都是有很多的相同点,有的人说那为什么不依然叫二叉树呢?那你用数组实现了栈不也是叫栈而不叫数组。
堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。这是我在百度词条搜索的解释,这些解释并不重要重要的是你对这个数据结构深刻的理解之后,要明白什么时候用这个数据结构解决问题,栈有队不能有的长处,同样栈和队列比也有他的缺点,所以你要在特定的情况下选择特定的数据结构。
堆有一些 它的特性,堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
进入咱们的正题。
typedef int HeapDateType;
typedef struct Heap
{
HeapDateType *_a;
size_t _size;
size_t _capacity;
}Heap;
void HeapInit(Heap* hp, HeapDateType* a, size_t n);
void HeapMake(Heap* hp);
void HeapPush(Heap* hp, HeapDateType x);
void HeapPop(Heap* hp);
size_t HeapSize(Heap* hp);
size_t HeapEmpty(Heap* hp);
HeapDateType HeapTop(Heap* hp);
void HeapSort(Heap* hp);
void HeapAdjustDown(Heap* hp, int root);
void HeapAdjustUp(Heap* hp, int child);
这是我们需要实现的一些函数和创建的结构体。
void HeapInit(Heap *hp, HeapDateType *_arr, size_t size)
{
assert(_arr);
assert(size > 0);
hp->_a = (HeapDateType *)malloc(sizeof(HeapDateType)*size*2);
assert(hp->_a);
hp->_size = size;
hp->_capacity = size*2;
for (size_t i = 0; i < size; i++)
{
hp->_a[i] = _arr[i];
}
}
这是在初始化我们的堆,因为结构体中的a是一个指针,所以我们需要给a分配足够的空间来存放我们的额数据,之后是两个成员size和capacity,这个函数有三个参数,分别是你的堆hp,然后是初始化需要用到的外部数组arr,size代表的就是你这个数组的长度,因为你这里传进来的外部数组依然是一个指针,所以你不能确定这个数组有多少元素,就需要使用size来控制长度。
void HeapMake(Heap* hp)
{
HeapDateType i = 0;
if (hp == NULL)
return;
for ((i = hp->_size - 1-1)/2; i >= 0; i--)
{
HeapAdjustDown(hp, i);
}
}
其实这个函数和上边的函数很容易和上一个数组混淆,上边主要是在分配空间和获取数据,甚至感觉,上边init完成之后,这已经就是一个完全二叉树了,make就是在做一些堆独有的事情,那就是对他进行特定的排序。因为堆中某个节点的值总是不大于或不小于其父节点的值。
在排序堆的时候主要是使用了两个函数,分别是从上往下调整和从下往上调整,当你将数组中的数据放入到堆中的时候,这时候需要对你的堆进行排序,最大的在最上边的堆叫做大堆,最小的在最上边的叫小堆,当你的所有数据已经在堆数组中的时候,需要你从第一个非叶子结点来开始调整你的堆,一层一层
就拿这个来说,你这个堆成为大堆的条件是,你每一个小的子树也都是一个大堆,所以你先从第一个非叶子结点49开始,将你的第一个树变成大堆,然后直接让你的下标减去1,因为你当前已经是最后一个非叶子节点了,而且堆就是一个完全二叉树,所以你现在下标减1就是倒数第二个非叶子结点开始调整。
void HeapAdjustDown(Heap* hp, int root)//向下调整
{
int parent = root;
int child = root * 2 + 1;
while (child < hp->_size)
{
if ((child + 1 < hp->_size) && hp->_a[child + 1] > hp->_a[child])
child++;
if (hp->_a[parent] < hp->_a[child])
{
int temp = 0;
temp = hp->_a[parent];
hp->_a[parent] = hp->_a[child];
hp->_a[child] = temp;
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
这是向下调整函数传入的参数是你某一个子树的根节点,然后从这个根节点开始向下调整,使你这个子树完全变成一个大堆。这里将根节点和你两个孩子进行比较,如果存在有孩子比父亲大的情况,就要将你的父亲和孩子进行交换,如果两个都比父亲大的话,那就选大的那个和父亲交换。
int child = root * 2 + 1;
if ((child + 1 < hp->_size) && hp->_a[child + 1] > hp->_a[child])
child++;
这里默认的是将孩子结点指向的是左孩子,如果左孩子比右孩子小的话,那就让孩子下标加一指向右孩子。
if (hp->_a[parent] < hp->_a[child])
{
int temp = 0;
temp = hp->_a[parent];
hp->_a[parent] = hp->_a[child];
hp->_a[child] = temp;
parent = child;
child = parent * 2 + 1;
}
如果父亲比他小的话,那就需要进行交换。
size_t HeapSize(Heap* hp)
{
return hp->_size;
}
size_t HeapEmpty(Heap* hp)
{
if (hp->_size == 0)
return 0;
else
return 1;
}
这两个函数是返回堆中的元素还有堆是不是为空。、
void HeapPush(Heap* hp, HeapDateType x)
{
if (hp->_size == hp->_capacity)
{
hp->_capacity *= 2;
hp->_a =(HeapDateType*)realloc(hp->_a,(sizeof(HeapDateType)*hp->_capacity));
}
hp->_a[hp->_size] = x;
HeapAdjustUp(hp, hp->_size);
hp->_size++;
}
这个函数是用来给堆中插入元素的,因为堆是用数组来实现的,所以插入数据当然是在数组的最后插入元素,因为堆是一个完全二叉树,所以,数组最后一个元素插入之后,当然也是在完全二叉树最底层最后一个数据插入元素。这里其实你就想象成现在刚刚把数组中的数据给了堆,重新开始创建而已,所以还是和上次一样开始在最后位置开始进行一个向上调整即可,因为除了这个位置其他位置都已经符合大堆了。
void HeapAdjustUp(Heap* hp, int child)//从下往上调整
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (hp->_a[parent < hp->_a[child]])
{
int temp = 0;
temp = hp->_a[parent];
hp->_a[parent] = hp->_a[child];
hp->_a[child] = temp;
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
void HeapPop(Heap* hp)
{
if (hp->_size == 0)
return;
hp->_a[0] = hp->_a[hp->_size-1];
hp->_size--;
HeapAdjustDown(hp, 0);
}
既然有插入那也就有删除,删除这里和插入一样也有他的规矩,这里是从堆顶删除,这里有一些简单的技巧,大家也都知道,数组在数组头删除一个元素是一个很复杂的事情,这里我们并没有像以前一样将之后的数据一个一个覆盖到前边,这里有个很巧妙的地方就是将数组中的最后一个元素覆盖到第一个元素,其他位置都不变,这时候只需要用一次向下调整即可。
void HeapSort(Heap* hp)
{
size_t n = hp->_size;
HeapDateType temp = 0;
while (HeapSize(hp))
{
temp = hp->_a[0];
hp->_a[0] = hp->_a[hp->_size - 1];
hp->_a[hp->_size - 1] = temp;
hp->_size--;
HeapAdjustDown(hp, 0);
}
hp->_size = n;
}
最后一个要实现的函数就是堆的排序,其实也能看出来,无论是大堆还是小堆,确实他的每一个子树都是大小堆,但是如果你是大堆你的数组顺序就是从大到小吗?显然不是因为你并没有比较过两个孩子的大小,只是说父亲一定是三个里边最大的,所以这里你需要对堆进行一个排序,这里也是一个在我看来比较巧妙的地方,因为你堆顶的元素一定是最大的,所以选出来堆顶之后和pop的程序一样,但是这里并不是真正的让他pop而是让他放到数组的最后一个,让最后一个上来到堆顶,然后让你的数组长度减去1这里,减去1是让你再次排序的时候不会排到刚刚放到后边的那个元素,保证最大的已经在后边不用再去排序了,一直这么下去直到数组是空的时候,所以这就是我们经常说的,如果你要是想让你的数组升序也就是说越来越大应该排出来大堆还是小堆呢?显然是需要拍出来是大堆,看了程序之后我感觉大家会对这句话有一个更好更深层次的理解。其实堆这里要学的不是这个数据结构而是这个数据结构的思想,是这个数据结构的应用,是他的特性。