一.前言
解题之前,我们首先需要了解栈堆队列的概念,只有充分理解它们两者的联系和区别才能更好的解题。
栈: 是限定在表尾进行插入或删除操作的线性表,表尾端称为栈顶,表头端称为栈底,不含元素的空表称为空栈。
队列: 是一种先进先出的线性表,它只允许在表的一端(队尾)插入,在另一端(队头)删除元素。
二、栈和堆的区别
其中栈跟堆的区别主要分为两个方面的含义:
1)程序内存布局场景下,堆与栈表示两种内存管理方式;
2)数据结构场景下,堆与栈表示两种常用的数据结构。
1.栈和堆在程序内存上定义
栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。参考如下代码:
csharp
int main() {
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
}
其中函数中定义的局部变量按照先后定义的顺序依次压入栈中,也就是说相邻变量的地址之间不会存在其它变量。栈的内存地址生长方向与堆相反 ,由高到低,所以后定义的变量地址低于先定义的变量,比如上面代码中变量 s 的地址小于变量 b 的地址,p2 地址小于 s 的地址。栈中存储的数据的生命周期随着函数的执行完成而结束。
堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。参考如下代码:
c
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 所指向的内存地址,原因是先申请的内存空间一旦被释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系。堆中存储的数据若未释放,则其生命周期等同于程序的生命周期。
2.堆与栈在程序内存上的区别
堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
(1)管理方式不同。栈由操作系统自动分配释放 ,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
(2)生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
(3)分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca()
函数分配,但是栈的动态分配和堆是不同的,它的动态分配是由操作系统进行释放,无需我们手工实现。
(4)分配效率不同:栈是由操作系统自动分配的,会在硬件层级上提供支持,并且出栈进栈都会有对应的指令执行,堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
3.# 数据结构中的堆与栈的区别
数据结构中,堆与栈是两个常见的数据结构,理解二者的定义、用法与区别,能够利用堆与栈解决很多实际问题。
栈是一种运算受限的线性表 ,其限制是指只仅允许在表的一端进行插入和删除操作 ,这一端被称为栈顶 (Top),相对地,把另一端称为栈底 (Bottom)。把新元素放到栈顶元素的上面,使之成为新的栈顶元素称作进栈、入栈或压栈(Push);把栈顶元素删除,使其相邻的元素成为新的栈顶元素称作出栈或退栈(Pop)。这种受限的运算使栈拥有"先进后出"的特性(First In Last Out),简称 FILO。
栈分顺序栈和链式栈 两种。栈是一种线性结构,所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。使用数组实现的栈叫做顺序栈 ,使用链表实现的栈叫做链式栈 ,二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。
堆是一种常用的树形结构 ,是一种特殊的完全二叉树 ,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。下面是一个小顶堆示例:
堆的具体应用------堆排序
堆排序(Heapsort)是堆的一个经典应用,有了上面对堆的了解,不难实现堆排序。由于堆也是用数组来存储的,故对数组进行堆化后,第一次将A[0]与A[n - 1]交换,再对A[0...n-2]重新恢复堆。第二次将A[0]与A[n -- 2]交换,再对A[0...n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。
因此,完成堆排序并没有用到前面说明的插入操作,只用到了建堆和节点向下调整的操作,堆排序的操作如下:
scss
// 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)。
因此,堆排序和快速排序在效率上是差不多的,但是堆排序一般优于快速排序的重要一点是数据的初始分布情况对堆排序的效率没有大的影响。
参考文档:
栈堆区别详解:blog.csdn.net/K346K346/ar...
十大排序方法:blog.csdn.net/K346K346/ar...
归并排序:dablelv.blog.csdn.net/article/det...
4. 用两个栈实现队列
那么如何用两个栈实现一个队列呢?
我们可以将两个栈进行拼接,一个当做队头(删除元素),一个当做队尾(插入元素)
stack1中入栈的顺序是4,3,2,1。其中 4 是先入栈的,我们要想使得这两个栈完成队列的功能,那么就需要让 4 先出栈,显然如果我们只有一个栈是不可能完成这个目的的
但是我们还有另外一个空栈可以使用,我们可以将stack1中的数据先出栈在将出栈的数据同时入到stack2中,这样再从stack2 中依次出栈就可以达到队列的效果了。
从上面的分析我们可以得出,两个栈实现一个队列需要注意的点:
push时:如果stack1 满了就先将stack1中的数据转移到stack2中,再向stack1中插入数据。如果两个栈都满了,说明队列满,无法插入。 pop时:出队列时,我们要优先查看stack2中有没有数据,有数据的话先出stack2中的数据。在stack2中没有数据的情况下,我们要将stack1中的所有数据全部转移到stack2中之后,再从stack2中pop数据。
csharp
public class CQueue {
//用两个栈实现一个队列,大体的思路就是,stack1用来实现队列尾部追加,负责增加数据,stack2负责弹出数据
//stack2弹出数据要判断,stack1当中是否有数据,有数据的情况下需要将stack1的数据push到stack2堆中在进行弹出
Stack<int> stack1 = new Stack<int>();
Stack<int> stack2 = new Stack<int>();
public CQueue() {
}
public void AppendTail(int value) {
//先将元素添加到栈stack1中,取元素操作发生在stack2中
stack1.Push(value);
}
public int DeleteHead() {
if(stack2.Count!=0){
return stack2.Pop(); //stack2中的元素直接弹出栈顶元素即可
}
//如果stack2的栈内没有元素,我们需要判断stack1是否有元素,有元素的话就要将元素添加到stack2中
while(stack1.Count!=0){
stack2.Push(stack1.Pop());
}
if(stack2.Count == 0) return -1;
return stack2.Pop(); //stack2中的元素直接弹出栈顶元素即可
}
}