给出一个有n个元素的数组A[1...n],要创建一个包含这些元素的堆,可以这样进行:从空的堆开始,不断插入每一个元素,直到A完全被转移到堆中为止。因为插入第j个键值用时O(log j),因此用这种方法创建堆栈的时间复杂性是O(n log n)。
我们知道对应于堆H[1...n]的树的节点可以方便地以自顶向下、从左到右的方式从1到n编码。在这样编码之后,可以用下面的方法,把一棵n个节点的几乎完全的二叉树转换成堆H[1...n]。从最后一个节点开始(编码为n的那一个)到根节点(编码为1的节点),逐个扫描所有的节点,根据需要,每一次将以当前节点为根节点的子树转换成堆。
每一棵只有一片叶子的子树已经是一个堆,因此叶子被跳过。
当以第i个节点为根的子树不是堆时,我们就对i进行Sift-down运算一边把它转换成堆。如此进行下去,使整棵二叉树符合堆得性质。
上面说明了如何对树进行运算。直接对输入的数组执行同样的过程是相当容易的。
令A[1...n]是已知数组,T是对应于A的一棵几乎完全的二叉树,我们注意到 A[⌊n/2⌋+1],A[⌊n/2⌋+2],...,A[n] 它们对应于T的叶子,这样我们可以直接从A[⌊n/2⌋]开始调整数组,然后继续调整 A[⌊n/2⌋-1],A[⌊n/2⌋-2],...,A[1]。这样得到的数组就是我们需要的堆。
过程 MakeHeap
输入 n个元素的数组A[1...n]
输出 A[1...n]转换成堆
算法描述
for i ← ⌊n/2⌋ downto 1
Sift-down(A, i)
end for
注意这里算法描述的数组的索引都是1...n,而不是大家习惯的0...n-1。
下面我们来计算算法MakeHeap的运行时间
设T是对应于数组A[1...n]的一棵几乎完全的二叉树,那么由观察结论可知,T的高是k=⌊log n⌋。
令A[j]对应该树的第i层中第j个节点,当语句Sift-down(A, j)调用过程Sift-down时,重复执行的次数最多是k-i。
因为在第i层上正好有2i个节点,0≤i<k,所以每一层循环执行的总次数的上界是 (k-i)2i。
总的循环执行的总次数的上界就是:
由于在过程Sift-down的每一个循环中,最多有两次元素的比较,因此元素比较的总次数是2×2n=4n。
但是在每次调用Sift-down时,都至少执行一次循环,因此元素比较的最小次数是2⌊n/2⌋≥n-1,这个就是元素比较的总次数的下界。
因此,MakeHeap算法需要Θ(n)时间来构造一个n元素的堆,似乎并不是之前我们认为的O(n log n)时间。
int* makeHeap(int* array, int arrayLength)
{
if (array == NULL || arrayLength <= 0)
return NULL;
int heapLength = arrayLength;
int* heap = (int*)malloc(sizeof(array) * (heapLength + 1));
if (heap == NULL)
return NULL;
// heap[0]用来存储数组中堆数据的长度,堆数据heap[1]...heap[heapLength]
// 所以数组的实际长度是heapLength+1,我们只对从数组索引为1开始的heapLength个数据进行操作
heap[0] = heapLength;
for (int i = 0; i < arrayLength; i++)
{
heap[i + 1] = array[i];
}
for (int i = heapLength / 2; i >= 1; i--)
{
siftDown(heap, i);
}
return heap;
}
我使用的数组是这样定义的:
const int ARRAY_LENGTH = 10;
int array[ARRAY_LENGTH] = { 4, 3, 8, 10, 11, 13, 7, 30, 17, 26 };
// 如果用我上面写的方法创建堆,不要忘了在操作完成之后释放内存哦
int* heap = makeHeap(array, ARRAY_LENGTH);
// do something ...
free(heap);
更多内容请参考:
算法 之 堆 - 简介
算法 之 堆 - Sift-up和Sift-down
算法 之 堆 - 插入和删除
分享到:
相关推荐
堆的创建与堆排序是在计算机科学中非常重要的数据结构与算法之一。通过合理的数据结构设计与高效的算法实现,可以在很多实际应用中发挥重要作用,比如在操作系统、数据库管理、编译器优化等领域都有着广泛的应用。...
1. 初始化堆:创建一个空堆,然后将初始解集中的解按照目标函数值排序并插入堆中。 2. 迭代过程:在每一轮迭代中,从堆顶取出当前最优解,然后生成新的候选解。根据优化目标,将新解与堆顶解比较,如果新解更优则...
算法是解决问题的步骤,可以分为排序算法(如冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等)、搜索算法(如深度优先搜索、广度优先搜索)、图算法(如最短路径问题的Dijkstra算法、Floyd-Warshall...
算法执行过程中,通过一个优先队列(通常使用二叉堆实现)来维护待处理节点,以保证每次选取的都是当前最短路径距离最小的节点。 以下是详细的算法步骤: 1. 初始化:创建一个空的最短路径集合,将源节点s的最短...
常用的排序算法--堆排序,通过创建堆的方法进行排序
4. 堆:如果系统包含优先级队列功能(如最近路径优先),堆可以用来高效地管理和调整路径优先级。 接下来是算法设计。在校园导航系统中,可能会应用以下算法: 1. 深度优先搜索(DFS)或广度优先搜索(BFS):用于...
2. **排序算法**:排序是算法中的重要部分,包括冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序等。理解它们的工作原理和时间复杂度,能帮助开发者在实际问题中选择最合适的排序方法。 3. **查找算法**...
6. **C++实现**:书中会介绍如何在C++中实现这些数据结构和算法,包括类的设计、对象的创建、模板的使用、STL库的运用等,让读者掌握C++在算法实现上的优势。 7. **源码分析**:书中附带的两版代码可能是作者对书中...
在算法部分,可能会涉及排序算法(如冒泡排序、选择排序、插入排序、快速排序、归并排序和堆排序)、查找算法(如线性查找、二分查找、哈希查找)以及图算法(如深度优先搜索、广度优先搜索)。排序算法用于组织和...
6. **排序算法**:包括冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等。这些排序算法各有优劣,适用于不同场景。C语言实现时,主要依赖于数组操作。 7. **查找算法**:二分查找、哈希查找、线性查找...
在压缩包中的实现可能包括使用优先队列(如二叉堆)来存储待处理的路径,以及使用邻接矩阵或邻接表来表示图。Java的集合框架使得创建和操作这些数据结构变得简单高效。 为了实现k最短路径,开发者可能会采用迭代或...
最小生成树是图中一个包含所有顶点的子图,其中边的权重之和尽可能小。克鲁斯卡尔算法采用贪心策略,按照边的权重从小到大依次选择边,确保每次添加一条边不会形成环路,直至连接所有顶点。 在克鲁斯卡尔算法的具体...
1. 初始化:创建一个大矩形(通常为装箱区域),并将其转化为二叉树的根节点。 2. 分析矩形:对每个待装入的矩形,计算其尺寸并准备相应的数据结构。 3. 插入矩形:使用二叉搜索策略找到能容纳新矩形的最佳位置。...
类和对象可以用来封装数据结构,模板机制可以创建泛型算法,而虚函数和多态则为算法设计提供了面向对象的支持。理解C++的内存管理(包括堆栈和堆的区别,以及new和delete的操作)也是实现高效算法的关键。 在实际...
可能还会涉及一些高级主题,如堆(优先队列)、哈希表、图的遍历算法(深度优先搜索和广度优先搜索)等。 总的来说,掌握数据结构和算法对于任何想要深入学习计算机科学,特别是软件开发的人来说都是至关重要的。...
目录: 1.s8算法1-1 算法基础 10.s8算法2-4 链表 哈希表 11.s8算法2-5 算法题 ...5.s8算法1-5 堆排序 6.s8算法1-6 归并排序+希尔排序 7.s8算法2-1 线性时间排序 8.s8算法2-2 数据结构 栈 9.s8算法2-3 队列 迷宫问题
例如,链表可以通过创建一个包含数据和指向下一个节点的指针的结构体来构建。队列可以由两个指针分别指向队首和队尾来维护。 栈的实现通常使用数组或链表。当使用数组时,我们需要注意栈顶指针的更新;而链表则通过...