栈
同顺序表和链表一样,栈也是来存储逻辑关系为“一对一”的线性结构。如下图所示:
栈的特点
栈的特点如下:
- 栈只能从表的一端
栈顶存储取数据,另一端栈底是封闭的 - 在栈中,无论是存数据还是取数据,都必须遵循
“先进后出(First in last out)”的原则,即最先进栈的最后出栈。
栈的操作
基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:
- 向栈中添加元素,此过程被称为
"进栈"(入栈或压栈); - 从栈中提取出指定元素,此过程被称为
"出栈"(或弹栈);
栈的应用
- 括号匹配
- 浏览器中页面的回退
- ...
栈的实现
栈是一种特殊的线性结构,因此有以下两种方式:
- 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构
- 链式栈:采用链式存储结构实现栈结构
栈的具体实现
顺序栈
顺序栈的结构如下:
typedef int Status;
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
/* 顺序栈结构 */
typedef struct {
SElemType data[MAXSIZE];
int top;
} Stack;
初始化
Status InitStack(Stack *S) {
S->top = -1;
return OK;
}
入栈
Status PushStack(Stack *S, SElemType e){
if(S->top == MAXSIZE-1) return printError("当前栈已满", ERROR);
S->top++;
S->data[S->top] = e;
return OK;
}
出栈
Status PopStack(Stack *S, SElemType *e) {
if(S->top == -1) return printError("当前栈为空", ERROR);
//取出栈顶元素
*e = S->data[S->top];
//栈顶指针下移
S->top--;
return OK;
}
置空
Status ClearStack(Stack *S){
S->top = -1;
return OK;
}
判空
Status StackEmpty(Stack S) {
if(S.top == -1) return TRUE;
return FALSE;
}
获取栈顶元素
Status GetTop(Stack S,SElemType *e){
if(S.top == -1) return printError("当前栈为空", ERROR);
*e = S.data[S.top];
return OK;
}
遍历
Status StackTraverse(Stack S){
if(StackEmpty(S)) return printError("当前栈为空", ERROR);
int i = S.top;
printf("此栈中所有元素:");
while(i >= 0) {
printf("%3d", S.data[i]);
i--;
}
printf("\n");
return OK;
}
测试
int main(int argc, const char * argv[]) {
Stack S;
InitStack(&S);
for(int i = 0; i < 10; i++) {
PushStack(&S, i);
}
StackTraverse(S);
SElemType e;
PopStack(&S, &e);
printf("出栈%3d\n", e);
GetTop(S, &e);
printf("栈顶元素为%3d\n", e);
ClearStack(&S);
StackTraverse(S);
return 0;
}
此栈中所有元素: 9 8 7 6 5 4 3 2 1 0
出栈 9
栈顶元素为 8
当前栈为空
Program ended with exit code: 0
链式栈
链式栈的结构如下:
//结点
typedef struct StackNode{
SElemType data;
struct StackNode* next;
} StackNode, *StackNodePtr;
/* 顺序栈结构 */
typedef struct {
int count;
StackNodePtr top;
} LinkStack;
初始化
Status InitLinkStack(LinkStack *S){
S->count = 0;
//栈顶指针初始化为NULL
S->top = NULL;
return OK;
}
入栈
Status PushLinkStack(LinkStack *S, SElemType e) {
if(S->count == MAXSIZE) return printError("当前栈已满", ERROR);
//创建新的入栈结点
StackNodePtr node = (StackNodePtr)malloc(sizeof(struct StackNode));
if(node == NULL) return printError("开辟空间失败", ERROR);
node->data = e;
//新结点的next指向栈顶
node->next = S->top;
//改变栈顶指针的位置
S->top = node;
//个数+1
S->count++;
return OK;
}
置空
Status ClearStack(LinkStack *S) {
StackNodePtr p = S->top;
StackNodePtr temp;
while(p) {
temp = p;
p = p->next;
free(temp);
}
S->count = 0;
S->top = NULL;
return OK;
}
判空
Status StackEmpty(LinkStack S) {
if(S.count == 0 && S.top == NULL) return TRUE;
return FALSE;
}
出栈
//出栈
Status PopLinkStack(LinkStack *S, SElemType *e) {
if(StackEmpty(*S)) return printError("当前栈为空", ERROR);
StackNodePtr temp = S->top;
*e = temp->data;
S->top = S->top->next;
free(temp);
S->count--;
return OK;
}
获取栈顶
Status GetTop(LinkStack S,SElemType *e) {
if(StackEmpty(S)) return printError("当前栈为空", ERROR);
*e = S.top->data;
return OK;
}
遍历
Status LinkStackTraverse(LinkStack S) {
if(StackEmpty(S)) return printError("当前栈为空", ERROR);
StackNodePtr p = S.top;
while(p) {
printf("%d ", p->data);
p = p->next;
}
return OK;
}
测试
int main(int argc, const char * argv[]) {
LinkStack S;
InitLinkStack(&S);
for(int i = 0; i < 10; i++) {
PushLinkStack(&S, i);
}
LinkStackTraverse(S);
SElemType e;
PopLinkStack(&S, &e);
printf("出栈%3d\n", e);
GetTop(S, &e);
printf("栈顶元素为%3d\n", e);
ClearStack(&S);
LinkStackTraverse(S);
return 0;
}
此栈中所有元素: 0 1 2 3 4 5 6 7 8 9
出栈 9
栈顶元素为 8
当前栈为空
Program ended with exit code: 0
扩展
函数调用与内存分配
主函数、被调函数
如果一个函数 A() 在定义或调用过程中出现了对另外一个函数 B() 的调用,那么我们就称 A() 为主调函数或主函数,称 B() 为被调函数。
当主调函数遇到被调函数时,主调函数会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回主调函数,主调函数根据刚才的状态继续往下执行。
在运行被调用函数之前,系统需要先完成3件事情
- 将所有实参,返回地址等信息传递给被调用函数保存。
- 未被调用函数的局部变量在栈上分配内存
- 将控制转移到被调用函数入口
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码,当遇到函数调用时,CPU 首先要记录下当前代码块中下一条代码的地址(假设地址为 0X1000),然后跳转到另外一个代码块,执行完毕后再回来继续执行 0X1000 处的代码。
被调用函数返回调用函数之前,系统也需要相应的完成3件事情:
- 在栈中保存被调用函数的计算结果(返回值)
- 释放在栈中为被调用函数分配的数据区
- 依照被调用函数保存的返回地址将控制转移到调用函数
一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。
当有多个函数嵌套调用时,按照 “后调用先返回”的原则依次进行,函数之间的信息传递和控制转移必须通过 “栈”来实现。系统将整个程序运行所需的数据空间安排在一个栈中,每当调用一个函数就为它在栈顶分配一个存储区,每当从一个函数退出时,就释放它的存储区,当前正在运行的函数存储区必在栈顶。
帧栈
帧栈也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数形参、返回地址、局部变量等信息。简而言之栈帧就是一个函数执行的环境。有时候函数嵌套调用,栈中会有多个函数的信息,每个函数占用一个连续的区域。“存储区”就是指一个函数对应的分配空间也就是一个函数帧。
参数的传递和内存分配
被调用函数的形参,在未出现函数调用时不占用内存空间,发生函数调用时,形参按照确定的类型在该函数帧中被分配指定大小的空间。并且由调用函数的实参传递给被调用函数的形参保存。
函数具体调用过程:
为被调用函数在栈顶分配空间,为被调用函数的形参分配空间 将实参的值传递给形参 被调用函数利用形参(如果存在)进行运算 通过return语句将返回值带回调用函数 调用结束,释放被调用函数空间,释放其形参分配空间
int add(int num1, int num2) {
int tempSum = num1 + num2;
return tempSum;
}
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
int sum = add(a, b);
printf("%i\n",sum); // sum = 30
return 0;
}
上面的main()、add()函数之间调用的过程及参数传递在内存的示意图如下:
- 首先执行
main()函数,系统为main()函数在栈顶分配一定大小的空间,为a、b局部变量分配空间 - 调用
add()函数,main()函数压入栈底,栈顶指针上移,系统为add()函数在栈顶分配一定大小的空间,其次为num1、num2局部变量分配空间 - 执行两个整数的加法运算,在
add()函数帧中新开辟一块空间存放计算后的结果tempSum - 最后
add()函数返回,在main()函数帧中开辟一块新的空间存放add()函数的返回值sum add()函数帧调用结束出栈,系统释放其空间并且栈顶指针下移,main()函数重新回到栈顶
递归函数
递归函数
在调用一个函数过程中又出现直接或间接调用该函数本身的情况,称为函数的递归调用。
一个递归函数的运行过程类似于多个函数之间的嵌套调用,只是调用函数和被调用函数是同一个函数,因此,和每次调用相关的一个重要概念是递归函数运行的“层数”。假设调用该递归函数的主函数为第 0 层,主函数调用递归函数进入第一层;从第 i 层调用本函数为进入第 i+1 层,反之,退出第 i 层递归则应返回至第 i - 1 层。
为了保证递归函数正确执行,系统建立一个“递归工作栈”作为整个递归函数运行期间使用的数据存储区,每一层递归所需信息构成一个“工作记录”,其中包括所有的实参、局部变量以及上一层的返回地址。每进入一层递归,就产生一个新的工作记录压入栈顶。每退出一层递归,就从栈顶弹出一个工作记录。当前活动的工作记录成为“活动记录”,并称活动记录的栈顶指针为“当前环境指针”。
一个递归问题可以分为“递推”和“回溯”两个阶段,要经历若干步才能求出最后的结果。但是其原理和一般函数的调用没有本质区别。递归函数调用次数越多,在栈上为其分配的空间就越大,所以我们应该避免调用次数过多的递归函数,因为该操作很可能会使栈的容量“溢出”。