优先堆也可以用链表
2015-12-12 00:00
246 查看
摘要: priority heap通常都是数组实现的,本文尝试用链表实现。没有具体代码,有了算法思想很容易实现的,我就不献丑了。
priority heap通常是以数组来实现的,用数组下标作为堆的序号来实现heap。但是在C语言中数组是不能改变大小的(新标准里可能可以改变哈),于是便不能动态地增加堆的大小。
于是想到链表动态增加的特•。链表实现优先堆的难点在于,堆序的乘2除2操作要求能即时获得数组下标,而链表不能随机存取只能顺序存取,对于DeleteMin操作,getMin很容易,只要保存了Child和Parent节点的指针,上滤操作也没什么难度。但是Insert操作就呵呵了,因为并不知道当前堆的末尾在哪,遍历寻至末尾的话,复杂度又上升至O(N^2)。BuildHeap更不用说。
先看看堆树的特点
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15
...
显然,对于堆序而言,序号确定了,在堆树中的位置也确定了。由此可见,如果知道当前堆的元素个数,那么最后一个元素的位置也就确定了。但是怎样找到这个位置呢?我在火车上睡不着,想出了这个方法。。。
先将序号转化成二进制:
1
10 | 11
100 101 | 110 111
1000 1001 1010 1011 | 1100...
...
用|来分隔树的左右两边。
观察后可以得出,位于左半部分的最高两位都是10,位于右半部分的最高两位都是11。
这是显然的,除开根的序号,对于堆树的每一层而言,左边部分的第一个,都是2、4、8、16...2^D。右边部分第一个都是3、6、12... 2^D+2^(D-1),也就是左边部分的序号加上这一层的节点个数的一半,就处于右边部分了:2+1,4+2,8+4,16+8...,换成二进制就是10XXX...+01XXX...=11XXX...,这就是前面观察的结论了。
首先要想到堆树是个二叉树,从根开始,不断地向左子树或者右子树寻找,便可以最终到达树底下,中途无非就是个判断是左还是右的问题。联系上面的观察结果,算法就很明朗了:
确定一个数处于堆树中的哪个位置,只要先把这个数用二进制表示,不断左移,取溢出位,从遇到的第一个1开始(第一个1表示遇到根了),遇到一个0便转向左子树,遇到一个1便转向右子树。简言之就是遇0则左,遇1则右。这个算法具有递归性,对子树又实施本算法,当到达叶子节点时,这个数便左移完了,这时也正好到达这个数在堆树中的位置。
所以,如果知道了堆的大小,便知道了如何从根走到末尾的位置,也就知道了新元素要Insert的位置。显然,这个算法的复杂度是```
这里输入代码
NlogN的。代价是需要两个指针变量来指向LeftChild和RightChild,以及还要维持一个Parent指针,用来实现插入之后的上滤操作。
所以堆树节点的结构体可以是:
因为堆树的位置是相对固定的,所以在进行Insert后的上滤操作和DeleteMin后的下滤操作时,在需要交换的地方,只交换Element就可以了,不必交换Node指针。可以想象成堆树就是一个框,元素就是放在框中的东西,交换时只交换东西而不必交换整个框。数组和链表的两种实现,不同之处在于数组实现的优先堆是用数组下标作为框,链表实现的优先堆是用结构体作为框而已。
实际上,数组实现的优先堆中,乘二和除二操作正是左移和右移运算。
实现了Insert操作,BuildHeap可以用Insert来间接实现,从一个空的堆树开始逐个Insert就可以了。有了LeftChild和RightChild以及Parent,DeleteMin的上滤操作也很容易实现。
priority heap通常是以数组来实现的,用数组下标作为堆的序号来实现heap。但是在C语言中数组是不能改变大小的(新标准里可能可以改变哈),于是便不能动态地增加堆的大小。
于是想到链表动态增加的特•。链表实现优先堆的难点在于,堆序的乘2除2操作要求能即时获得数组下标,而链表不能随机存取只能顺序存取,对于DeleteMin操作,getMin很容易,只要保存了Child和Parent节点的指针,上滤操作也没什么难度。但是Insert操作就呵呵了,因为并不知道当前堆的末尾在哪,遍历寻至末尾的话,复杂度又上升至O(N^2)。BuildHeap更不用说。
先看看堆树的特点
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15
...
显然,对于堆序而言,序号确定了,在堆树中的位置也确定了。由此可见,如果知道当前堆的元素个数,那么最后一个元素的位置也就确定了。但是怎样找到这个位置呢?我在火车上睡不着,想出了这个方法。。。
先将序号转化成二进制:
1
10 | 11
100 101 | 110 111
1000 1001 1010 1011 | 1100...
...
用|来分隔树的左右两边。
观察后可以得出,位于左半部分的最高两位都是10,位于右半部分的最高两位都是11。
这是显然的,除开根的序号,对于堆树的每一层而言,左边部分的第一个,都是2、4、8、16...2^D。右边部分第一个都是3、6、12... 2^D+2^(D-1),也就是左边部分的序号加上这一层的节点个数的一半,就处于右边部分了:2+1,4+2,8+4,16+8...,换成二进制就是10XXX...+01XXX...=11XXX...,这就是前面观察的结论了。
首先要想到堆树是个二叉树,从根开始,不断地向左子树或者右子树寻找,便可以最终到达树底下,中途无非就是个判断是左还是右的问题。联系上面的观察结果,算法就很明朗了:
确定一个数处于堆树中的哪个位置,只要先把这个数用二进制表示,不断左移,取溢出位,从遇到的第一个1开始(第一个1表示遇到根了),遇到一个0便转向左子树,遇到一个1便转向右子树。简言之就是遇0则左,遇1则右。这个算法具有递归性,对子树又实施本算法,当到达叶子节点时,这个数便左移完了,这时也正好到达这个数在堆树中的位置。
所以,如果知道了堆的大小,便知道了如何从根走到末尾的位置,也就知道了新元素要Insert的位置。显然,这个算法的复杂度是```
这里输入代码
NlogN的。代价是需要两个指针变量来指向LeftChild和RightChild,以及还要维持一个Parent指针,用来实现插入之后的上滤操作。
所以堆树节点的结构体可以是:
struct Node { ElementType Element; Node * LeftChild; Node * RightChild; Node * Parent; }Node;
因为堆树的位置是相对固定的,所以在进行Insert后的上滤操作和DeleteMin后的下滤操作时,在需要交换的地方,只交换Element就可以了,不必交换Node指针。可以想象成堆树就是一个框,元素就是放在框中的东西,交换时只交换东西而不必交换整个框。数组和链表的两种实现,不同之处在于数组实现的优先堆是用数组下标作为框,链表实现的优先堆是用结构体作为框而已。
实际上,数组实现的优先堆中,乘二和除二操作正是左移和右移运算。
实现了Insert操作,BuildHeap可以用Insert来间接实现,从一个空的堆树开始逐个Insert就可以了。有了LeftChild和RightChild以及Parent,DeleteMin的上滤操作也很容易实现。
相关文章推荐
- [C/C++]反转链表
- C#实现基于链表的内存记事本实例
- C语言实现带头结点的链表的创建、查找、插入、删除操作
- C++实现简单的学生管理系统
- Linux内核链表实现过程
- C++链表倒序实现方法
- C#通过链表实现队列的方法
- C#实现的简单链表类实例
- 找出链表倒数第n个节点元素的二个方法
- Java数据结构之简单链表的定义与实现方法示例
- C语言单循环链表的表示与实现实例详解
- C++实现的链表类实例
- PHP小教程之实现链表
- 基于java中stack与heap的区别,java中的垃圾回收机制的相关介绍
- C语言双向链表的表示与实现实例详解
- js链表操作(实例讲解)
- Grow heap (frag case) 堆内存过大的深入解析
- C语言实现输出链表中倒数第k个节点
- C++语言实现线性表之链表实例
- STL list链表的用法详细解析