栈的定义和分类
栈是我们线性结构中的一种常见应用。在函数调用、内存分配等也常常跟栈打交道,栈可以简单的理解为是一种可以实现“先进后出”的存储结构。栈又分为静态栈和动态栈。静态栈以类似于数组方式存放,而动态栈以类似于链表的方式存放。
栈区(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来对栈中有效元素进行压栈、出栈、遍历等相关操作。
结束语
有关对线性结构中的栈操作今天就写到这了,明天开始学习线性结构常见应用中的队列。