栈的定义和分类
栈是我们线性结构中的一种常见应用。在函数调用、内存分配等也常常跟栈打交道,栈可以简单的理解为是一种可以实现“先进后出”的存储结构。栈又分为静态栈和动态栈。静态栈以类似于数组方式存放,而动态栈以类似于链表的方式存放。
栈区(Stack)和堆区(heap)
栈区主要用于存放局部变量、定义的形参,在定义时局部变量或形参时由系统自动分配,在函数结束时由系统自动回收存储单元。
堆区主要通过new(常用于C++、Java中),malloc(用于C中)等动态开辟的存储块,函数结束时需要我们通过delete(c++中)、free(c中)手动释放
举例说明:
void f(int n)
{
int m;
char ch;
double *q=(double *)malloc(200);
}
其中的n,m,ch,q由栈区分配(由操作系统帮我们自动分配), 200由堆区分配(需要我们手动开辟一块存储单元)。
栈和堆表示的是分配数据的一种方式。静态的或局部变量它们是以压栈和出栈的方式分配内存的(这个就叫栈区),而动态内存它们是以一种叫堆排序的方式分配的内存(这个叫堆区)。笼统的讲:凡是静态分配的全部在栈里面分配, 凡是动态分配的全部在堆里面分配。
对栈进行操作的思路
先讲下思路:我们知道链表主要通过头指针(指向头结点的地址)来对链表中的其它结点进行操作,而头结点本身并没有实际含义(既没有存放有效节点,也没有存放有效节点的个数),但通过它却可以方便我们对链表进行相关操作,如链表的遍历:
void TraverseList(PNODE pHead)
{
PNODE p=pHead->pNext;//将头结点的指针域指向首结点(链表的第一个有效结点),并赋给指针变量p,此时p指向首节点的地址
printf("遍历整个链表:");
while(NULL!=p)//当p指向首节点的地址不为NULL(即链表不为空),循环输入个结点的值
{
//当p指向尾结点的地址时,可输出尾结点的值
//但此时尾结点指针域为NULL,将跳出while循环
printf("%d ",p->data);
p=p->pNext;//将p指向下一个结点的地址赋给p指针
}
printf("\n");
}
在这个对链表进行遍历的函数中,我们可以看到我们只需要一个头指针,就可以遍历整个链表。
同理,我们可以通过初始化造出一个空栈,产生头结点,进而对栈进行相关操作。
typedef struct Node //定义结点的数据类型
{
int data;//数据域
struct Node *pNext;//指针域
}NODE,*PNODE;
typedef struct Stack //定义一个Stack结构类型,它含有两个指针成员
{
PNODE pTop;//指向栈顶元素
PNODE pBottom;//指向栈底元素的下一个没有实际含义的元素
}STACK,*PSTACK;
伪算法:
1.造空栈
void init(PSTACK pS)
{
动态产生一个头结点, 并让pTop指向该节点;
让pTop和pBottom都指向头结点;
再让成员pBottom或pTop所指向头结点的指针域为空
}
2.压栈(也叫进栈)
void push(PSTACK pS,int val)
{
创建一个新临时结点;
把val赋给该节点的数据域;
让该新节点的指针域指向栈顶;
最后让该新节点成为新栈顶;
}
3.出栈
bool pop(PSTACK pS)
{
先判断栈是否为空;
临时保存一份栈顶结点;
让栈顶指针指向下一个结点的地址;
释放预先保存的栈顶结点所占的内存;
}
完整实例说明:
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
typedef struct Node //定义结点的数据类型
{
int data;//数据域
struct Node *pNext;//指针域
}NODE,*PNODE;
typedef struct Stack
{
PNODE pTop;//指向栈顶元素
PNODE pBottom;//指向栈底元素的下一个没有实际含义的元素(头结点)
}STACK,*PSTACK;
void init(PSTACK);//产生一个没有实际含义的头结点,并让pTop和pBottom都指向该头结点
void push(PSTACK,int);//压栈(即进栈)
void traverse(PSTACK);//遍历
bool pop(PSTACK);//出栈
int length(PSTACK);//求栈的长度
bool clear(PSTACK);//清空栈
int main()
{
STACK S;//STACK等价于struct Stack,为变量S分配内存,其中变量S含两个成员pTop,pBottom
init(&S);//目的是造出一个空栈,产生一个头结点,以便对栈就行操作
push(&S,1);
push(&S,2);
push(&S,3);
traverse(&S);
pop(&S);
traverse(&S);
printf("栈的长度为:%d\n",length(&S));
clear(&S);
printf("清空后:");
traverse(&S);
push(&S,7);
push(&S,5);
push(&S,9);
printf("进栈后:");
traverse(&S);
return 0;
}
void init(PSTACK pS)//造空栈
{
pS->pTop=(PNODE)malloc(sizeof(NODE));//产生头结点,并让pTop指向该头结点
if(NULL==pS->pTop)
{
printf("动态内存分配失败!\n");
exit(-1);//终止程序
}
pS->pBottom=pS->pTop; //让pTop和pBottom都指向头结点
pS->pBottom->pNext=NULL;
}
void push(PSTACK pS,int val)
{
PNODE pNew=(PNODE)malloc(sizeof(NODE));//创建新临时节点
if(NULL==pNew)
{
printf("动态内存分配失败!\n");
exit(-1);//终止程序
}
pNew->data=val;
pNew->pNext=pS->pTop;
pS->pTop=pNew;
return;
}
void traverse(PSTACK pS)
{
PNODE p=pS->pTop;
while(p!=pS->pBottom)
{
printf("%d ",p->data);
p=p->pNext;
}
printf("\n");
}
bool pop(PSTACK pS)
{
if(pS->pBottom==pS->pTop)
{
printf("栈为空,无法进行出栈操作!\n");
return false;
}
PNODE p=pS->pTop;
printf("出栈元素为:%d\n",p->data);
pS->pTop=p->pNext;
free(p);
p=NULL;
return true;
}
int length(PSTACK pS)
{
int len=0;
if(pS->pBottom==pS->pTop)
{
printf("栈为空! ");
return 0;
}
PNODE p=pS->pTop;
while(p!=pS->pBottom)
{
++len;
p=p->pNext;
}
return len;
}
bool clear(PSTACK pS)
{
if(pS->pBottom==pS->pTop)
{
printf("栈为空,清空失败! \n");
return false;
}
PNODE p=pS->pTop,q;
while(p!=pS->pBottom)
{
q=p->pNext;
pS->pTop=q;
free(p);
p=q;
}
pS->pTop=pS->pBottom;
return true;
}
注意:
1.free(p); 删除的是p指向的那个结点所占的内存,而不是删除p本身所占的内存;释放结点目的主要是为了防止内存泄露(这不像Java,有垃圾回收机制---由垃圾回收器自动帮你回收)。
2.pBottom始终指向的是栈底元素的下一个没有实际含义的元素(头结点),头结点是我们人为造出来的,并不存放有效数据;添加struct Stack(含两个成员pTop、pBottom)这个结构体的目的主要是为了方便我们对栈进行操作。
3.初始化时,让pTop和pBottom都指向头结点的目的:一是让它们初始化时所指向的内存地址相同,在整个程序运行过程中,让pBottom始终指向pTop的初始地址,也就是头结点的地址。这样就可以通过pS->pBottom==pS->pTop?是否成立来判断pTop是否指向初始化时原头结点(不存放有效数据的,为方便对栈进行操作的附加结点)的地址,即可以判断栈是否为空,因为如果栈含有有效元素的话,那么pTop必定存放的是新元素的地址。
4.结构体Stack成员pBottom的好处主要有:一是在初始化造空栈时,指向头结点;二是可以在pS->pBottom==pS->pTop成立时判断栈为空。 结构体Stack成员pTop的好处主要有:一是和pBottom一样,在初始化造空栈时,指向头结点;二是通过pTop来对栈中有效元素进行压栈、出栈、遍历等相关操作。
结束语
有关对线性结构中的栈操作今天就写到这了,明天开始学习线性结构常见应用中的队列。
分享到:
相关推荐
通过对Java中堆和栈的介绍及示例分析,我们可以看到两者在Java程序中的重要作用。掌握这些基础知识有助于更好地理解和优化程序的性能。 在实际开发过程中,合理利用堆和栈的特点能够有效提升程序的运行效率和资源...
### Java中堆内存和栈内存详解 #### 一、引言 Java作为一种广泛使用的编程语言,其内存管理机制是理解程序行为的关键。本文将深入探讨Java中的两种主要内存区域:堆内存(Heap Memory)和栈内存(Stack Memory)。...
堆和栈是计算机内存管理中的两个...总的来说,理解堆和栈的区别和特性,对于优化代码、防止内存泄漏、提升程序性能至关重要。在编程实践中,应根据具体需求选择合适的数据存储方式,以实现更高效、更稳定的程序设计。
"堆内存和栈内存详解" 堆内存和栈内存是程序运行时的两个主要内存区域,它们在程序的执行过程中扮演着非常重要的角色。栈内存是由编译器自动分配释放的,存放函数的参数值、局部变量的值等,而堆内存则是由程序员...
本文将深入探讨C++中的堆、栈以及静态数据区,帮助理解这些内存区域的区别和使用场景。 1. 栈(Stack): 栈是编译器自动管理的内存区域,主要用来存放函数参数、局部变量等。每当进入一个函数调用,栈就会为函数的...
LwIP协议栈源码详解是一项对LwIP协议栈源代码进行深度解析和评解的工作,旨在帮助开发者更好地理解和使用LwIP协议栈。LwIP(Light-Weight IP)是一个开源的TCP/IP协议栈实现,专门针对嵌入式系统进行优化,以减少...
Java中栈内存和堆内存详解,非常容易理解
### 堆内存和栈内存详解 #### 一、预备知识—程序的内存分配 当一个程序被编译并运行时,它所占用的内存会被分成几个不同的区域,每个区域都有其特定的功能和管理方式。以下是对这些内存区域的具体解释: 1. **栈...
C++的栈是一种抽象数据类型,它按照后进先出(LIFO,Last In First Out)的原则进行操作。栈在程序设计中有着广泛的应用,如表达式求值、递归调用、内存管理等。本篇我们将深入探讨C++中栈的实现方式,特别是顺序栈...
- 蓝牙协议栈的基础是蓝牙核心规范,描述了蓝牙系统的基本架构和操作。这里提到的"core2.0-4.0规格书"涵盖了蓝牙从2.0到4.0版本的演变,这些版本引入了增强数据速率(EDR)和低功耗蓝牙(Bluetooth Low Energy, BLE)等...
综上所述,文档《LwIP协议栈源码详解》通过深入分析LwIP的核心组件,为读者提供了一个理解和实现TCP/IP协议栈的参考。文档中提到的各部分都是实现网络通信所必须的,而作者在完成文档的过程中展现了对LwIP协议栈源码...
### 栈的基本操作详解 栈(Stack)是一种线性数据结构,其特点是“后进先出”(Last In First Out,LIFO)。在计算机科学中,栈被广泛应用于各种算法和程序设计中,如函数调用、表达式求值、括号匹配、回溯搜索等。...
### Java中堆内存与栈内存分配浅析 #### 一、引言 在Java编程语言中,内存管理是一项至关重要的技术。程序运行时所使用的内存主要分为两类:堆内存(Heap Memory)和栈内存(Stack Memory)。理解这两种内存类型的...
Zigbee协议栈是无线传感器网络中常用的一种通信协议,其设计基于OSI七层模型的简化版本。本文将深入探讨Zigbee协议栈的结构、...理解Zigbee协议栈的这些核心概念对于开发基于Zigbee技术的无线传感器网络系统至关重要。
通过对堆和栈的基本概念、特点及应用场景的深入分析,我们可以更好地理解这两种内存区域的差异。在实际编程过程中,根据需求合理选择使用栈或堆能够提高程序的性能和资源利用效率。例如,对于频繁使用的、生命周期较...
Java栈是Java虚拟机(JVM)内存模型的重要组成部分,主要负责存储方法调用过程中的局部变量、操作数和方法返回信息。栈的特点是后进先出...理解Java栈的工作原理对于优化代码性能、理解和排查内存相关问题至关重要。
### 详解Java学习中堆与栈的内容 #### 一、引言 在Java学习过程中,堆(Heap)和栈(Stack)是两个非常重要的概念,它们对于理解Java内存管理机制至关重要。很多初学者在接触到这两个概念时往往感到困惑,本文将...
通过对这段代码的学习,我们可以更好地理解栈的工作机制及其应用场景。在实际开发过程中,根据具体需求合理选择数据结构对于提高程序效率至关重要。栈作为一种简单而高效的数据结构,在解决许多问题时都有着不可替代...