快速导航
程序员在写程序时,最常遇到的就是排序问题
对于N个待排序的数据结构,使用冒泡插入等排序方法,时间复杂度为O(N2),往往不能满足要求,因此需要使用更加高效的排序方法(归并,二分等)将时间复杂度降低为O(logN)
因此,本文在这里介绍堆排序的方法,与堆的通用数据结构代码,以供参考
1.堆基础知识
什么是堆?
定义:堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
要点1:堆可以类比为完全二叉树
要点2:父亲节点的键值或索引总是小于(或者大于)左右孩子的键值或索引
要点3:堆是一种递归的数据存储结构
要点4:父亲节点比孩子节点小的叫做小顶堆,父亲节点比孩子节点大的叫做大顶堆
我们可以看到,对于堆,可以类比为完全二叉树,根据这一特性,我们就可以使用数组来实现一个堆。使用数组来实现堆的话,可以使用元素在数组中的索引来快速确认其父亲节点,孩子节点,关系如下:
左孩子索引=当前节点索引*2+1
右孩子索引=当前节点索引*2+2
父亲节点索引=(当前节点索引-1)/2
如图所示,我们使用一个int[]数组实现小顶堆,来存放数据{3,8,15,31,25}
由于堆的定义,我们不难发现,同一节点的左右孩子的大小关系是无法确定的
要点5:同一节点的左右孩子节点的大小关系不定
那么如何实现一个堆呢?接下来介绍堆的原操作
堆的原操作
入堆操作
当我们要向堆中添加新的元素,需执行以下流程:
- 将新元素增加到数组的末尾
- 将添加的数据与其父亲节点的数据进行比较,如果新增节点的数据根据排序准则先于其父亲节点,交换两者的位置
- 递归比较至根节点结束或者满足排序准则结束
以整形数组为例,假设我们向图1的堆中增加一个元素1,流程如下:
可以看到,向堆中增加元素是一个上浮的过程,将当前节点与父亲节点进行对比,如果优先度更高,向上调整。 时间复杂度为O(logN)
实现代码如下:
int a[100]={
3,8,15,31,25};
int push=1;
int heapsize=5;//当前堆有5个元素
//首先将push放入数组队尾
a[heapsize++]=push;
//比较a[5]与其父亲节点
int cur=heapsize-1;
int father=(cur-1)/2;
while(cur>0&&a[cur]<a[father])
{
//孩子节点应该排序在父亲节点之上,交换
int tmep=a[cur];
a[cur]=a[father];
a[father]=temp;
cur=father;
father=(father-1)/2;
}
出堆操作
同样以上面为例,当一个堆要弹出顶端的元素时,我们先将0号索引位置的元素记录下来,然后将堆尾的元素放在0号位置,然后将目前0号位的元素跟孩子们进行比对,如果优先度更低,执行下沉操作
下沉操作有两个流程:
- 寻找左右孩子排序优先度更高的孩子
- 当前节点与优先度更高的孩子进行对比,如果孩子节点的优先度更高,执行下沉操作
流程图如下:
代码如下:
int pop=a[0];
a[0]=a[--heapsize];
int cur=0;
int child;
while(cur*2+1<heapsize)
{
//首先寻找孩子节点中优先度高的
if(cur*2+2==heapsize)//如果没有右孩子,跟左孩子比对即可
child=cur*2+1;
else
child=a[cur*2+1]<a[cur*2+2]?cur*2+1:cur*2+2;
if(a[child]<a[cur])//孩子优先度更高,交换
{
int temp=a[child];
a[child]=a[cur];
a[cur]=temp;
}
cur=child;
}
return pop;
根据以上,我们基本可以实现一个数组构建的堆
2.扩展到存储复杂数据结构的堆
上述我们论述了一个整形数组的堆排序的方法,但是在实际中,需要排序的数据结构往往多样且复杂,比较优先度的逻辑也不尽相同,因此我们需要构建一个通用的堆模板来满足多变的情况
假设我们有这样一种数据结构
typedef struct
{
int a;
double b;
char c;
}TYPEA;
有三种排序方法:
- 按照该数据结构的成员a来排序
- 按照该数据结构的成员b来排序
- 按照该数据结构的成员c来排序
假如要建立三种不同排序准则的堆,代码繁琐复杂。因此在这里,笔者归纳整理了一种通用的堆模板
头文件Heap.h
创建头文件Heap.h
#ifndef _Heap_H_
#define _Heap_H_
#define MAX_HEAPSIZE 100 //堆最大容量
//交换函数指针的宏
#define SWAP(a,b) do{void *temp=a;a=b;b=temp;}while(0)
class Heap
{
private:
bool (*cmp)(void* a, void* b);//排序的函数指针
int heapsize;//当前堆中元素的个数
public:
void* element[MAX_HEAPSIZE];//堆中元素的指针数组
void init(bool (*compare_function)(void* a, void* b));//堆的初始化
void push(void* a);//入堆操作
void* pop();//出堆操作
void* top();//返回堆顶元素的操作
};
#endif
该类中 使用指针数组void* element[MAX_HEAPSIZE]
来替换之前的整形数组,其中element[i]
指向要排序的数据结构,指针数组的好处在于,我们可以用指针来指向不同的数据结构,从而满足对不同的数据结构进行排序
其次,在该类中,定义bool (*cmp)(void* a, void* b)
函数指针,通过函数指针,我们可以为不同的排序方式初始化不同的堆
其他API
使用
void init(bool (*compare_function)(void* a, void* b))
函数来初始化堆
使用void push(void* a)
函数将元素a的指针入堆
使用void* pop()
函数将堆顶的元素弹出
使用void* top()
函数返回堆顶的元素(不弹出)
实现文件Heap.cpp
在实现文件Heap.cpp中,对每个函数进行定义
#include"Heap.h"
void Heap::init(bool(*compare_function)(void* a, void* b))
{
heapsize = 0;
cmp = compare_function;
return;
}
void Heap::push(void* a)
{
element[heapsize++] = a;
int cur = heapsize - 1;
int father = (cur - 1) / 2;
while (cur > 0 && cmp(element[cur], element[father]))
{
SWAP(element[cur], element[father]);
cur = father;
father = (father - 1) / 2;
}
return;
}
void* Heap::pop()
{
if (top() == nullptr)
return nullptr;
else
{
void* ret = element[0