排序与查找算法目录
【归并排序。桶排序】C语言实现归并排序、计数排序、桶排序、基数排序
【C语言实现交换排序】交换排序(冒泡排序、快速排序)
【C语言实现插入排序】插入排序(直接、折半、二路、希尔)
【三大经典查找】三大经典查找(跳跃、插值、斐波那契)
【三大基础查找】三大基础查找(顺序、二分、分块)
目录
一、优先队列和二叉堆
1.二叉堆的概念
2.堆的性质
3.优先队列
二、优先队列的实现
1.插入
2.删除最小值
三、堆排序
前言
查找和排序是数据结构与算法中不可或缺的一环,是前辈们在算法道路上留下的重要且方便的一些技巧,学习这些经典的查找和排序也能让我们更好和更快的解决问题。在这个专栏中我们会学习六大查找和十大排序的算法与思想,而本篇将详细讲解其中的堆排序;
注意:本文中所有排序按照升序排序,降序只需要把逻辑反过来即可!
一、优先队列和二叉堆
1.二叉堆的概念
在学习堆排序之前,我们先要学习二叉堆(binary heap);
如果说数据结构中的栈是模仿硬件中的栈而构建的,那么堆完全不是一回事!二叉堆是一个基于完全二叉树的数据结构(在下文中简称堆);
如同二叉查找树一样,堆也有两个性质:
- 结构性和堆序性;
正如AVL树一样,对堆的操作很可能破坏这两个性质中的一个,所以堆的操作必须要到堆的性质都被满足才能停止;
一项重要的观察发现,因为完全二叉树的规律性,所以它可以用一个数组而不需要指针来表示;
- 我们可以通过数学关系得到:在从下标为0开始的数组中,如果一个父节点的下标为x,那么它的左孩子的下标是2 * x + 1,它的右孩子的下标是2 * x + 2,同样的可以反推父节点的下标是(x - 1) / 2;
也就是说,一个堆数据结构将由一个数组、一个代表最大值的整数和当前堆的大小组成;
2.堆的性质
堆分为大顶堆和小顶堆:
- 大顶堆 根节点的值永远大于孩子;
-
小顶堆 根节点的值永远小于孩子;
这正是堆序性(heap order)的体现,根据这个性质我们能够快速地找出最小值或最大值,因为最小值或最大值永远在根上;
3.优先队列
在实际项目中通常会有多项工作,而其中有一项特别重要的工作要先去做,那么这个工作就应该拥有优先权;针对这种特殊应用场景的队列,我们称之为:优先队列(PriorityQueue);
不难看出,优先队列要求队列中的每一个元素都具有一定的优先级;优先级高的或优先级低的(人为定义的)要先出队,而优先级相同就按照顺序依次出队,先进先出;
二叉堆就刚好能够满足优先队列的需求;
二、优先队列的实现
根据上文中讲述的堆的性质,我们能很容易构建出二叉堆和优先队列:
typedef int ElementType;
typedef struct HeapStruct
{
int capacity;//容量
int size;//当前的大小
ElementType* elements;//元素指针
}Heap, *PriorityQueue;
当我们构建一个小顶堆时,最小值总是可以在根处找到,无论是从逻辑上还是实际上考虑,实现这种操作都是很容易的,只需要始终保持堆序性质就行了;
1.插入
传统上执行插入操作时,我们可以在数组的末尾插入,然后向上回溯,比较插入的元素和它的父节点之间的大小关系(无所谓兄弟之间的)并作出调整——(对于小顶堆而言)小于就交换,直至实现正确的序,这个过程我们就称之为堆化处理;
void swap(int a[], int i, int j)
{
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//这里实现大顶堆的堆调整
void heapify(int a[], int len, int k)
{
//k是要调整的元素下标
if (k < len)
{
int root = k;//根节点
int left_child = 2 * k + 1;//左孩子
int right_child = 2 * k + 2;//右孩子
//查找左右孩子中大的孩子节点
if (left_child < len && a[root] < a[left_child])
{
root = left_child;
}
if (right_child < len && a[root] < a[right_child])
{
root = right_child;
}
//交换
if (root != k)
{
swap(a, k, root);
//递归下去
heapify(a, len, root);
}
}
}
但是执行一次交换需要3条赋值语句,如果一个元素的回溯 d 层,那么交换的次数就达到了3*d,而如果我们用上滤(percolate up)的策略就只需要赋值d + 1次;
2.删除最小值
删除最小值的实现方式类似于插入。找出最小值是容易的,就是根节点,困难的部分是删除它;
参考插入步骤,不过这一次要反过来,只要根节点与最后一个元素交换,因为最后一个元素一定是一个叶子,而且是最后一个叶子,我们对它进行操作就一定不会破坏堆的结构性,保证堆永远是一棵完全二叉树,接下来只需要从根节点开始不断向下堆化处理就行了;
同样的我们也可以类似于上滤的操作避免交换:在根处建立空穴,由于现在堆少了一个元素,我们同样的要移动最后一个元素x,将x置入沿从根开始包含最小儿子的一条路径上的一个正确的位置。这种策略一般叫做下滤(percolate down);
通过上滤和下滤实现插入和删除的算法时间复杂度为O(log N);
#include#include #include //二叉堆实现优先队列 typedef int ElementType; typedef struct HeapStruct { int capacity;//容量 int size;//当前的大小 ElementType* elements;//元素指针 }Heap, *PriorityQueue; //初始化 这里是小顶堆构建的优先队列 PriorityQueue init(int MaxSize) { PriorityQueue H; H = (PriorityQueue)malloc(sizeof(Heap)); if (H == NULL) { printf("MALLOC FAILn"); } H->capacity = MaxSize; H->size = 0; H->elements = (ElementType*)malloc(sizeof(ElementType) * (MaxSize + 1));//分配数组加上一个额外的哨兵位 if (H->elements == NULL) { printf("MALLOC FAILn"); } //如果新插入的指是新的最小值,那么它将一直被推向堆顶,在某一时刻i=1,我们就需要跳出循环,所以将0处标记为哨兵位使循环终止 //保证这个值要小于任何一个元素才能使循环终止 H->elements[0] = INT_MIN; return H; } int isFull(PriorityQueue H) { return H->capacity == H->size; } int isEmpty(PriorityQueue H) { return H->size == 0; } //插入 void insert(PriorityQueue H, ElementType x) { int i; //判断是否已经满容了 if (isFull(H)) { printf("PriorityQueue is fulln"); } //在最后一位插入 如果该点的父节点大于插入的值 就要交换 for (i = ++H->size; H->elements[i / 2] > x; i /= 2) { H->elements[i] = H->elements[i / 2]; } H->elements[i] = x; } ElementType deleteMin(PriorityQueue H) { //判断是否为空 if (isEmpty(H)) { printf("PriorityQueue is emptyn"); return H->elements[0]; } ElementType child, parent; ElementType min = H->elements[1]; //记录最后一个元素的值 ElementType last = H->elements[H->size--]; for (parent = 1; parent * 2 <= H->size; parent = child) { //找出左右子树中最小的 child = parent * 2; //如果右子树存在 并且右子树小于左子树 if (child != H->size && H->elements[child] > H->elements[child + 1]) { child++; } //如果还没有找到最后一个元素正确的位置 if (last > H->elements[child]) { H->elements[parent] = H->elements[child]; } else break; } H->elements[parent] = last; return min; } void destroy(PriorityQueue H) { free(H->elements); free(H); } void makeEmpty(PriorityQueue H) { H->size = 0; } int main() { PriorityQueue H = init(100); int arr[] = { 50, 45, 15, 25, 10 }; int length = sizeof(arr) / sizeof(arr[0]); for (int i = 0; i < length; i++) { insert(H, arr[i]); } for (int i = 0; i < length; i++) { printf("%d ", deleteMin(H)); } system("pause"); return 0; }
三、堆排序
事实上我们在实现优先队列的功能的时候就已经实现了堆排序——在删除最小值时,我们不断出队最小值并寻找下一个最小值,就是一个从小到大排序的结果(降序排序只需要构建大顶堆即可);
但是在算法题中我们再按照上述步骤去构建一个优先队列实在太过于麻烦,我们只需要利用这种思想并对上述过程进行简化,就可以实现一个简单的堆的构建和堆排序了;
#include#include void swap(int a[], int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; } //最大堆调整 void heapify(int a[], int len, int k) { if (k < len) { int root = k;//根节点 int left_child = 2 * k + 1;//左孩子 int right_child = 2 * k + 2;//右孩子 //查找左右孩子中大的孩子节点 if (left_child < len && a[root] < a[left_child]) { root = left_child; } if (right_child < len && a[root] < a[right_child]) { root = right_child; } //交换 if (root != k) { swap(a, k, root); //递归下去 heapify(a, len, root); } } } //构建大顶堆 void creatHeap(int a[], int n) { int last = n - 1;//最后一个节点的下标 int parent = (last - 1) / 2;//最后一个节点的父节点的下标 //从最后一个节点的父节点到根节点进行最大堆调整 for (int i = parent; i >= 0; i--) heapify(a, n, i); } //堆排序 void heapSort(int a[], int n) { creatHeap(a, n); for (int i = n - 1; i >= 0; i--) { //将根节点(最大值)和最后一个节点交换,也就是最大值到最后一个下标位置上 swap(a, i, 0); //因为此时根节点不有序,整体从根节点开始最大堆调整 //而此时根结点小于所有父结点,因而在调整时只需考虑最大孩子的分支即可 heapify(a, i, 0); } } int main() { int arr[] = { 50, 45, 15, 25, 10 }; int length = sizeof(arr) / sizeof(arr[0]); heapSort(arr, length); for (int i = 0; i < length; i++) { printf("%d ", arr[i]); } system("pause"); return 0; }
运行结果



