怎么存储堆
① 什么事堆栈,堆栈有哪些运算,堆栈怎样存储
stack,其实就是一块内存空间,关键在于他的用途.
1.对于程序指令来说
执行exe时,程序都会默认分配1M堆栈空间,vs2008等开发软件都可以进行调整实际大小.
指令变成一条条机器码,cpu会一条条执行.
例子:
xxxxxxx
call 0x403650 <- --
yyyyy
在执行call命令时,cpu会把下一条指令地址写入堆栈地址空间中,当然也包括其他信息.
0x403650相当于一个子程序的地址,完事后,必然有一个ret之类的指令.这时,cpu根据先前保存的地址,也就是yyyyy这条指令所对应的地址.这样就能继续往下执行了.
关于这一点,用ollydbg好好玩一下,马上就清楚了.
2.一般的应用程序编写.
我们在编写程序时,有时采用堆栈结构,有时采用队列结构,这跟所采用的算法有很大关系.
最常见的递归算法,按递归展开的话,所有的细节就跟第1点完全一样,好处是,大都程序员根本不关心象第1点所描述的细节.只知道其调用过程和最终执行结果.具体细节可能就不关心了.
当把递归算法 用非递归算法写时,很可能你就要引入堆栈结构(其实就是人为手动申请一个内存空间,比如buffer[递归最深层数], 这样,就可以编写出直观的顺序结构的代码. cpu也不用着因调用子程序,一次次把相关信息写入系统的堆栈中(第1点所说)..因为buffer[]的存在就是为了避免这种情况.
--------------------
stack最常用两种操作,push和pop. 你可以用c或是c++ 标准库提供的实现.
如果不是大工程,基本上没必要这么做.
搞个数组 buf[], 再搞个索引变量int index,用来指示top位置. 写入数据时,index++,取出数据时index--
3.最常用的,但易忽略的.
平常所说的,局部变量就是在堆栈中分配的.所以他出了作用域就自动释放了.
c语言很容易理解,不容易出错.
但c++中,编译器有不同的策略.
比如
CTeacher t= bar();
--
CTeacher bar()
{
CTeacher xx;
为CTeacher的成员赋值
return xx.
}
你一定为这里xx对象是局部变量,出了函数作用域,对应的内存主释放了.
CTeacher t= bar();
因为bar()是返回一个Cteacher对象,所以这里就要执行拷贝构造函数,
你会奇怪,问题是bar()返回的对象是无效的.但执行却不会出错.
为什么?
首先对堆栈的理解是对.只是c++编译器内部会改写bar()这个函数.
变成 void bar(CTeacher& tmp)
这样,t就作为引用参数传入了,函数内部创建临时对象,然后赋值给引用对象就成了.结果当然正确了.
4. 是第2点的延伸,相当重要.
一些大的应用工程,往往配合堆来对内存进行管理.
以后你接触一些第三方程序,一定会奇怪,要动态申请内存,直接new或malloc一个对象不就行了么,为什么要这么麻烦.
其中一个重要原因:减少碎片,提高内存使用效率.
你先申请大的空间(new/malloc),然后借助stack的特性来管理和控制这块空间!!!
-------------------------------
ps:理解到这几层差不多够用了
② 堆和堆排序
1,堆是一个完全二叉树;
完全二叉树要求除了最后一层,其他层的节点都是满的,最后一层的节点都靠左排列。
2,堆中每个节点都必须大于等于(或小于等于)其子树中每个节点的值。
堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。
3,对于每个节点的值都大于等于子树中每个节点值的堆,叫作“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,叫“小顶堆”。
要实现一个堆,要先知道堆都支持哪些操作,已及如何存储一个堆。
1,如何存储一个堆:
完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
2,往堆中插入一个元素
往堆中插入一个元素后,需要继续满足堆的两个特性
(1)如果把新插入的元素放到堆的最后,则不符合堆的特性了,于是需要进行调整,让其重新满足堆的特性,这个过程叫做 堆化(heapify)
(2)堆化实际上有两种,从下往上和从上往下
(3)从下往上的堆化方法:
堆化非常简单,就是顺着节点所在的路径,向上或者向下,对比,然后交换。
(1)从堆的定义的第二条中,任何节点的值都大于等于(或小于等于)子树节点的值,则堆顶元素存储的就是堆中数据的最大值或最小值。
(2)假设是大顶堆,堆堆顶元素就是最大的元素,但删除堆顶元素之后,就需要把第二大元素放到堆顶,那第二大元素肯定会出现在左右子节点中。然后在迭代地删除第二大节点,以此类推,直到叶子节点被删除。
但这种方式会使堆化出来的堆不满足完全二叉树的特性
(3)可以把最后一个节点放到堆顶,然后利用同样的父子节点对比方法,对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止,这是从上往下的堆化方法。
一个包含n个节点的完全二叉树,树的高度不会超过log2n。堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,即O(log n)。插入数据和删除堆顶元素的主要逻辑就是堆化,所以往堆中插入一个元素和删除堆顶元素的时间复杂度都是O(log n)。
(1)排序方法有时间复杂度是O(n^2)的冒泡排序,插入排序,选择排序,有时间复杂度是O(nlogn)的归并排序,快速排序,线性排序。
(2)借助堆这种数据结构实现的排序算法就叫作堆排序,这种排序方法的时间复杂度非常稳定,是O(nlogn),并且它还是原地排序算法。
堆排序的过程大致分解为两大步骤:建堆和排序
(3)建堆:
1,首先将数组原地建成一个堆。“原地”:是指不借助另一个数组,就在原地数组上操作。
2,建堆有两种思路:
第一种:在堆中插入一个元素的思路。
尽管数组中包含n个数据,但是可以假设起初堆中只包含一个数据,就是下标为1的数据。然后,调用插入方法,将将下标从2到n的数据依次插入到堆中,这样就将包含n个数据的数组,组织成了堆
第二种:是从后往前处理数组,并且每个数据都是从上往下堆化。
第二种和第一种思路截然相反,第一种建堆思路的处理过程是从前往后处理数据,并且每个数据插入堆中时,都是从下往上堆化。
对下标从n/2开始到1的数据进行堆化,下标是n/2 + 1到n的节点,是叶子节点,不需堆化
3,建堆的时间复杂度
每个节点堆化的时间复杂度是O(logn),则n/2+1个节点堆化的总时间复杂度是O(n)。
①:因为叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始。每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点高度k成正比。
(4)排序:
建堆结束后,数组中的数据已是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。
将它和最后一个元素交换,最大元素就放到了下标为n的位置
这个过程有点类似“删除堆顶元素”的操作,当堆顶元素移除后,把下标为n的元素放到堆顶,然后在通过堆化的方法,将剩下的n-1个元素重新构建成堆。堆化完成之后,在取堆顶元素,放到下标是n-1的位置,一直重复这个过程,直到最后堆中只剩下标为1的一个元素,排序工作就完成了。
(5)时间,空间复杂度,以及稳定性分析
①:整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。
②:堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是O(n),排序过程的时间复杂度是O(nlogn),所以堆排序的时间复杂度是O(nlogn)
③:堆排序不是稳定的排序算法,可能改变值相等的数据原始相对顺序。