收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由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;
else
return 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
1
2 * i + 1
2∗i+1 和
2
∗
i
2
2 * i + 2
2∗i+2。如第0个节点左右子节点下标分别为1和2。
2.2.2 堆的基本操作
- 建立
以最小堆为例,如果以数组存储元素时,一个数组具有对应的树表示形式,但树并不满足堆的条件,需要重新排列元素,可以建立“堆化”的树。
- 插入
将一个新元素插入到数组末尾,如果新构成的二叉树不满足堆的性质,需要将新元素在其到堆顶的路径上,找到属于自己的位置,即进行上浮操作。
下图演示了插入 15 时,堆的调整。
- 删除
删除一个元素总是发生在堆顶,因为堆顶的元素是最小的(小顶堆中)。数组中最后一个元素用来填补空缺位置,然后对顶部元素进行下沉,如果左右孩子有比自己小的,则选择选择最小的那个进行交换。重复进行下沉操作,以满足堆的条件。
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个数据。为了便于重建堆,实际的操作是将数组最后一个数据与根节点交换,然后再从根节点开始进行一次从上向下的调整。
调整时先在左右儿子节点中找最小的,如果父节点不大于这个最小的子节点说明不需要调整了,反之将最小的子节点换到父节点的位置。此时父节点实际上并不需要换到最小子节点的位置,因为这不是父节点的最终位置。但逻辑上父节点替换了最小的子节点,然后再考虑父节点对后面的节点的影响。堆元素的删除导致的堆调整,其整个过程就是将根节点进行“下沉”处理。下面给出代码:
// minHeapFixDown 小顶堆结点下沉操作。
// a 为数组,len 为结点总数;从 index 结点开始调整,index 从 0 开始计算 index 其子结点为 2\*index+1, 2\*index+2;len/2-1 为最后一个非叶子结点。
void minHeapFixDown(int a[],int len,int index) {
// index 为叶子节点不用调整。
if(index>(len/2-1)) 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;
**收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。**
![img](https://img-blog.csdnimg.cn/img_convert/2359d6f56990abc0c0378de39c112e99.png)
![img](https://img-blog.csdnimg.cn/img_convert/69a9e0f409144fffc2fdb05eabdf6a78.png)
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人**
**都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
不用下沉调整
}
// 将待调整节点放到最后的位置。
a[lastIndex]=tmp;
**收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。**
[外链图片转存中...(img-Vbn2sRUP-1715644017459)]
[外链图片转存中...(img-SEk9JCmd-1715644017460)]
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人**
**都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**