栈与堆的区别(内存分配与数据结构)

参考自https://blog.csdn.net/K346K346/article/details/80849966/

堆(Heap)与栈(Stack)包含两层含义:

  1. 程序内存布局场景下的内存管理方式
  2. 数据结构中的两种常见的数据结构

1. 程序内存分配中的堆与栈

1.1 栈介绍

栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。参考如下代码:

int main()
{int b;				//栈char s[] = "abc"; 	//栈char *p2;			//栈
}

其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反,由高到底,所以后定义的变量地址低于先定义的变量,比如上面代码中变量 s 的地址小于变量 b 的地址,p2 地址小于 s 的地址。栈中存储的数据的生命周期随着函数的执行完成而结束。

1.2 堆介绍

堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。参考如下代码:

int main()
{// C 中用 malloc() 函数申请char* p1 = (char *)malloc(10);cout<<(int*)p1<<endl;		//输出:00000000003BA0C0// 用 free() 函数释放free(p1);// C++ 中用 new 运算符申请char* p2 = new char[10];cout << (int*)p2 << endl;		//输出:00000000003BA0C0// 用 delete 运算符释放delete[] p2;
}

其中 p1 所指的 10 字节的内存空间与 p2 所指的 10 字节内存空间都是存在于堆。堆的内存地址生长方向与栈相反,由低到高,但需要注意的是,后申请的内存空间并不一定在先申请的内存空间的后面,即 p2 指向的地址并不一定大于 p1 所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。堆中存储的数据若未释放,则其生命周期等同于程序的生命周期。

关于堆上内存空间的分配过程,首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确地释放本内存空间。由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表。

1.3 堆与栈区别

堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
(1)管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;

(2)空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;

(3)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。

(4)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。

(5)分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

(6)存放内容不同。栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。当主函数调用另外一个函数的时候,要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。

从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。

无论是堆还是栈,在内存使用时都要防止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。

2. 数据结构中的栈与堆

2.1 栈简介

栈是一种运算受限的线性表,其限制是指只仅允许在表的一端进行插入和删除操作,这一端被称为栈顶(Top),相对地,把另一端称为栈底(Bottom)。把新元素放到栈顶元素的上面,使之成为新的栈顶元素称作进栈、入栈或压栈(Push);把栈顶元素删除,使其相邻的元素成为新的栈顶元素称作出栈或退栈(Pop)。这种受限的运算使栈拥有“先进后出”的特性(First In Last Out),简称FILO。

栈分顺序栈和链式栈两种。栈是一种线性结构,所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈,二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。

栈的结构如下图所示:

栈的基本操作包括初始化、判断栈是否为空、入栈、出栈以及获取栈顶元素等。下面以顺序栈为例,使用 C++ 给出一个简单的实现。

#include<stdio.h>
#include<malloc.h>#define DataType int
#define MAXSIZE 1024
struct SeqStack
{DataType data[MAXSIZE];int top;
};//栈初始化,成功返回栈对象指针,失败返回空指针NULL
SeqStack* initSeqStack()
{SeqStack* s=(SeqStack*)malloc(sizeof(SeqStack));if(!s){printf("空间不足\n");return NULL;}else{s->top = -1;return s;}
}//判断栈是否为空
bool isEmptySeqStack(SeqStack* s)
{if (s->top == -1)return true;elsereturn false;
}//入栈,返回-1失败,0成功
int pushSeqStack(SeqStack* s, DataType x)
{if(s->top == MAXSIZE-1){return -1;//栈满不能入栈}else{s->top++;s->data[s->top] = x;return 0;}
}//出栈,返回-1失败,0成功
int popSeqStack(SeqStack* s, DataType* x)
{if(isEmptySeqStack(s)){return -1;//栈空不能出栈}else{*x = s->data[s->top];s->top--;return 0;}
}//取栈顶元素,返回-1失败,0成功
int topSeqStack(SeqStack* s,DataType* x)
{if (isEmptySeqStack(s))return -1;//栈空else{*x=s->data[s->top];return 0;}
}//打印栈中元素
int printSeqStack(SeqStack* s)
{int i;printf("当前栈中的元素:\n");for (i = s->top; i >= 0; i--)printf("%4d",s->data[i]);printf("\n");return 0;
}//test
int main()
{SeqStack* seqStack=initSeqStack();if(seqStack){//将4、5、7分别入栈pushSeqStack(seqStack,4);pushSeqStack(seqStack,5);pushSeqStack(seqStack,7);//打印栈内所有元素printSeqStack(seqStack);//获取栈顶元素DataType x=0;int ret=topSeqStack(seqStack,&x);if(0==ret){printf("top element is %d\n",x);}//将栈顶元素出栈ret=popSeqStack(seqStack,&x);if(0==ret){printf("pop top element is %d\n",x);}}return 0;
}

运行上面的程序,输出结果:

当前栈中的元素:7   5   4
top element is 7
pop top element is 7

2.2 堆简介

2.2.1 堆的性质

堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。下面是一个小顶堆示例:
在这里插入图片描述

堆的存储一般都用数组来存储堆,i节点的父节点下标就为(i–1)/2(i – 1) / 2(i–1)/2。它的左右子节点下标分别为 2∗i+12 * i + 12∗i+1 和 2∗i+22 * i + 22∗i+2。如第0个节点左右子节点下标分别为1和2。
在这里插入图片描述

2.2.2 堆的基本操作

(1)建立
以最小堆为例,如果以数组存储元素时,一个数组具有对应的树表示形式,但树并不满足堆的条件,需要重新排列元素,可以建立“堆化”的树。
1

(2)插入
将一个新元素插入到表尾,即数组末尾时,如果新构成的二叉树不满足堆的性质,需要重新排列元素,下图演示了插入15时,堆的调整。
在这里插入图片描述

(3)删除。
堆排序中,删除一个元素总是发生在堆顶,因为堆顶的元素是最小的(小顶堆中)。表中最后一个元素用来填补空缺位置,结果树被更新以满足堆条件。
在这里插入图片描述

2.2.3 堆操作实现

(1)插入代码实现
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父节点到根节点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中,这就类似于直接插入排序中将一个数据并入到有序区间中,这是节点“上浮”调整。不难写出插入一个新数据时堆的调整代码:

//新加入i节点,其父节点为(i-1)/2
//参数:a:数组,i:新插入元素在数组中的下标  
void minHeapFixUp(int a[], int i)  
{  int j, temp;  temp = a[i];  j = (i-1)/2;      //父节点  while (j >= 0 && i != 0)  {  if (a[j] <= temp)//如果父节点不大于新插入的元素,停止寻找  break;  a[i]=a[j];    		//把较大的子节点往下移动,替换它的子节点  i = j;  j = (i-1)/2;  }  a[i] = temp;  
}  

因此,插入数据到最小堆时:

//在最小堆中加入新的数据data  
//a:数组,index:插入的下标,
void minHeapAddNumber(int a[], int index, int data)  
{  a[index] = data;  minHeapFixUp(a, index);  
} 

(2)删除代码实现
按照堆删除的说明,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将数组最后一个数据与根节点交换,然后再从根节点开始进行一次从上向下的调整。

调整时先在左右儿子节点中找最小的,如果父节点不大于这个最小的子节点说明不需要调整了,反之将最小的子节点换到父节点的位置。此时父节点实际上并不需要换到最小子节点的位置,因为这不是父节点的最终位置。但逻辑上父节点替换了最小的子节点,然后再考虑父节点对后面的节点的影响。堆元素的删除导致的堆调整,其整个过程就是将根节点进行“下沉”处理。下面给出代码:

//a为数组,len为节点总数;从index节点开始调整,index从0开始计算index其子节点为 2*index+1, 2*index+2;len/2-1为最后一个非叶子节点 
void minHeapFixDown(int a[],int len,int index)
{if(index>(len/2-1))//index为叶子节点不用调整return;int tmp=a[index];lastIndex=index;while(index<=len/2-1)        //当下沉到叶子节点时,就不用调整了{ if(a[2*index+1]<tmp)     //如果左子节点小于待调整节点{lastIndex = 2*index+1;}//如果存在右子节点且小于左子节点和待调整节点if(2*index+2<len && a[2*index+2]<a[2*index+1]&& a[2*index+2]<tmp){lastIndex=2*index+2;}//如果左右子节点有一个小于待调整节点,选择最小子节点进行上浮if(lastIndex!=index) {  a[index]=a[lastIndex];index=lastIndex;}else break;             //否则待调整节点不用下沉调整}a[lastIndex]=tmp;           //将待调整节点放到最后的位置
}

根据堆删除的下沉思想,可以有不同版本的代码实现,以上是和孙凛同学一起讨论出的一个版本,在这里感谢他的参与,读者可另行给出。个人体会,这里建议大家根据对堆调整过程的理解,写出自己的代码,切勿看示例代码去理解算法,而是理解算法思想写出代码,否则很快就会忘记。

(3)建堆
有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图:
在这里插入图片描述

很明显,对叶子节点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:
在这里插入图片描述

写出堆化数组的代码:

//建立最小堆
//a:数组,n:数组长度
void makeMinHeap(int a[], int n)  
{  for (int i = n/2-1; i >= 0; i--)  minHeapFixDown(a, i, n);  
}  

2.2.4 堆的具体应用——堆排序

堆排序(Heapsort)是堆的一个经典应用,有了上面对堆的了解,不难实现堆排序。由于堆也是用数组来存储的,故对数组进行堆化后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。

因此,完成堆排序并没有用到前面说明的插入操作,只用到了建堆和节点向下调整的操作,堆排序的操作如下:

//array:待排序数组,len:数组长度
void heapSort(int array[],int len)
{//建堆makeMinHeap(array,len); //最后一个叶子节点和根节点交换,并进行堆调整,交换次数为len-1次for(int i=len-1;i>0;--i){//最后一个叶子节点交换array[i]=array[i]+array[0];array[0]=array[i]-array[0];array[i]=array[i]-array[0];//堆调整minHeapFixDown(array, 0, len-i-1);  }
}  

(1)稳定性。堆排序是不稳定排序。

(2)堆排序性能分析。由于每次重新恢复堆的时间复杂度为O(logN),共N-1次堆调整操作,再加上前面建立堆时N/2次向下调整,每次调整时间复杂度也为O(logN)。两次操作时间复杂度相加还是O(NlogN),故堆排序的时间复杂度为O(NlogN)。

最坏情况:如果待排序数组是有序的,仍然需要O(NlogN)复杂度的比较操作,只是少了移动的操作;

最好情况:如果待排序数组是逆序的,不仅需要O(NlogN)复杂度的比较操作,而且需要O(NlogN)复杂度的交换操作,总的时间复杂度还是O(NlogN)。

因此,堆排序和快速排序在效率上是差不多的,但是堆排序一般优于快速排序的重要一点是数据的初始分布情况对堆排序的效率没有大的影响。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444186.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入学习卷积神经网络(CNN)的原理知识

转载自https://www.cnblogs.com/wj-1314/p/9754072.html 在深度学习领域中&#xff0c;已经经过验证的成熟算法&#xff0c;目前主要有深度卷积网络&#xff08;DNN&#xff09;和递归网络&#xff08;RNN&#xff09;&#xff0c;在图像识别&#xff0c;视频识别&#xff0c;语…

C/C++中static的用法全局变量与局部变量

转载自C/C中static的用法全局变量与局部变量 1.什么是static? static 是C/C中很常用的修饰符&#xff0c;它被用来控制变量的存储方式和可见性。 1.1static的引入 我们知道在函数内部定义的变量&#xff0c;当程序执行到它的定义处时&#xff0c;编译器为它在栈上分配空间&…

李牛(Linux)脚本

Linux课堂笔记day01 主要总结内容&#xff1a; 一&#xff1a;Linux背景介绍 二&#xff1a;系统操作 三&#xff1a;服务管理 四&#xff1a;shell脚本 五&#xff1a;文本操作 六:常用服务搭建 01&#xff1a;初识linux 收获&#xff1a;可以熟练应对运维和开发 对以后的生…

李牛(Linux)打包

15&#xff1a;打包压缩以及解压缩 接下来我们来介绍打包压缩以及解压缩命令 首先我们要在脑海里想几个问题&#xff1a; 1.打包压缩以及解压缩在字面上理解到底是什么意思&#xff1f; 是不是像我们生活见到的事例那样 比如说&#xff1a;生产酒的厂商一般都是按照规则将12瓶…

jquery实现页面提示,数据正在加载中。(

简单代码&#xff1a; jsp中代码如下&#xff1a;<wbr> <div id"dataLoad" style"display:none"><!--页面载入显示--></wbr><wbr><wbr><table width100% height100% border0 aligncenter valignmiddle></wbr…

李牛(Linux)vi

16&#xff1a;强大的vi 引言&#xff1a;提到vi我们不得不提到vim 这两种编辑器就先当于我们Windows操作系统当中的记事本 不过vi以及vim编辑器熟练掌握之后是不需使用鼠标进行操作的 完全都是由键盘来进行控制 那为什么可以不用鼠标呢 就是因为我们的vi编辑器是基于多模式的…

李牛(Linux)

20&#xff1a;用户和用户组管理 引言&#xff1a; 新思维1&#xff1a;用户&#xff1f;用户是什么&#xff1f;能不能吃&#xff1f;好吃不&#xff01;哈哈 不开玩笑了 我们平常接触的用户就是window系统下的用户 用户名叫啥来着 哦 user 但是对于Windows操作系统来说 好像…

Date类(日期时间类)219

219节课堂笔记 1.概述&#xff1a;表示特定的时间 2.所在的类&#xff1a;java.util.Date(表示时间和日期的类) 类date标识特定的瞬间&#xff0c;精确到毫秒 3.毫秒的换算&#xff1a;1秒1000毫秒 tips&#xff1a;不可以认为是1秒等于60毫秒&#xff0c;与时钟换算是不一样的…

MYSQ产品

前言&#xff1a;MySQL数据库&#xff0c;隶属于MySQLAB公司&#xff0c;总部位于瑞典&#xff0c;后被Oracle收购 MySQLAB公司是由monky及他的两位好朋友创建的&#xff0c;先是被sun公司收购然后被偶尔甲骨文公司收购 MySQL的优点&#xff1a; 1.它的成本是比较低的&#xff…

DateFormat(炸窝)

222&#xff1a;DateFormat方法的使用以及功能&#xff1a; java.text.DateFormat是日期或者时间格式化子类的抽象类&#xff0c;作用&#xff1a;可以帮我们完成日期和文本之间的转换&#xff0c;也就是可以在Date对象与String对象之间进行来回转换 格式化&#xff1a; 按照指…

剑指offer:8-11记录

用两个栈实现一个队列。队列的声明如下&#xff0c;请实现它的两个函数 appendTail 和 deleteHead &#xff0c;分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素&#xff0c;deleteHead 操作返回 -1 ) 示例 1&#xff1a; 输入&#xff1a; ["…

剑指offer:26-30记录

输入两棵二叉树A和B&#xff0c;判断B是不是A的子结构。(约定空树不是任意一个树的子结构) B是A的子结构&#xff0c; 即 A中有出现和B相同的结构和节点值。 例如: 给定的树 A: 3 / \ 4 5 / \ 1 2 给定的树 B&#xff1a; 4 / 1 返回 true&#xff0c;因为…

Calendar类 set方法 get方法 add方法

Calendar类 set方法 get方法 add方法 package asd; import java.util.*; public class zixue { public static void main(String[] args) { demo01();//实验的是get()方法&#xff1b; demo02();//实验的是set()方法&#xff1b; } //---------------------------------------…

剑指offer:33-37记录

输入一个整数数组&#xff0c;判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true&#xff0c;否则返回 false。假设输入的数组的任意两个数字都互不相同。 参考以下这颗二叉搜索树&#xff1a; 5 / \ 2 6 / \ 1 3 示例 1&#xff1a; 输入: [1,6,…

剑指offer:45-48记录

输入一个正整数数组&#xff0c;把数组里所有数字拼接起来排成一个数&#xff0c;打印能拼接出的所有数字中最小的一个。 示例 1: 输入: [10,2] 输出: "102" 示例 2: 输入: [3,30,34,5,9] 输出: "3033459" 提示: 0 < nums.length < 100 说明:…

剑指offer:50-53记录

在字符串 s 中找出第一个只出现一次的字符。如果没有&#xff0c;返回一个单空格。 示例: s "abaccdeff" 返回 "b" s "" 返回 " " 限制&#xff1a; 0 < s 的长度 < 50000 思路&#xff1a;map记录次数&#xff0c;再…

返回地址【数据结构】

小问题&#xff1f; 1.我们是如何根据地址值来找到我们对应的数据的&#xff1f; 详细陈述一下&#xff1a;当我们开辟一个整数类型&#xff0c;取名为a&#xff0c;假设地址空间是从数值为2000进行存储&#xff0c;并且我们假设整形占用4个字节&#xff0c;那么我们在内存中需…

【超级详细的小白教程】Hexo 搭建自己的博客

– 前言 这是一篇有关如何使用 Github Pages 和 Hexo 搭建属于自己独立博客的详尽教程&#xff0c;本人是软件工程专业本科生&#xff0c;目前只学习了C和C编程语言&#xff0c;对网站开发的有关知识几乎为零&#xff0c;这也是我搭建好自己的博客之后写的第一篇博客&#xff…

面向对象思想精华总结

一、三大特性 封装继承多态 二、类图 泛化关系 (Generalization)实现关系 (Realization)聚合关系 (Aggregation)组合关系 (Composition)关联关系 (Association)依赖关系 (Dependency) 三、设计原则 S.O.L.I.D其他常见原则 参考资料 一、三大特性 封装 利用抽象数据类型将数据…

insert函数的修改,

我们来看一下图片当中的第2个圆圈&#xff0c;为什么使用size来相加呢&#xff1f;我们知道一开始我们定义的初始空间为init_size;我们想一下啊&#xff0c;如果是第1次进行空间的增加&#xff0c;那么我们使用InIt来进行相加是可以的&#xff0c;但是当第2次想加我们再想开辟空…