数据结构与算法-栈-Day5

502 阅读9分钟

顺序表链表一样,也是来存储逻辑关系为“一对一”的线性结构。如下图所示:

栈的特点

栈的特点如下:

  • 栈只能从表的一端栈顶存储取数据,另一端栈底是封闭的
  • 在栈中,无论是存数据还是取数据,都必须遵循“先进后出(First in last out)”的原则,即最先进栈的最后出栈。

栈的操作

基于栈结构的特点,在实际应用中,通常只会对栈执行以下两种操作:

  • 向栈中添加元素,此过程被称为"进栈"(入栈或压栈)
  • 从栈中提取出指定元素,此过程被称为"出栈"(或弹栈)

栈的应用

  • 括号匹配
  • 浏览器中页面的回退
  • ...

栈的实现

栈是一种特殊的线性结构,因此有以下两种方式:

  1. 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构
  2. 链式栈:采用链式存储结构实现栈结构

栈的具体实现

顺序栈

顺序栈的结构如下:

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()函数之间调用的过程及参数传递在内存的示意图如下:

  1. 首先执行main()函数,系统为main()函数在栈顶分配一定大小的空间,为a、b局部变量分配空间
  2. 调用add()函数,main()函数压入栈底,栈顶指针上移,系统为add()函数在栈顶分配一定大小的空间,其次为num1、num2局部变量分配空间
  3. 执行两个整数的加法运算,在add()函数帧中新开辟一块空间存放计算后的结果tempSum
  4. 最后add()函数返回,在main()函数帧中开辟一块新的空间存放add()函数的返回值sum
  5. add()函数帧调用结束出栈,系统释放其空间并且栈顶指针下移,main()函数重新回到栈顶

递归函数

递归函数

在调用一个函数过程中又出现直接或间接调用该函数本身的情况,称为函数的递归调用。

一个递归函数的运行过程类似于多个函数之间的嵌套调用,只是调用函数和被调用函数是同一个函数,因此,和每次调用相关的一个重要概念是递归函数运行的“层数”。假设调用该递归函数的主函数为第 0 层,主函数调用递归函数进入第一层;从第 i 层调用本函数为进入第 i+1 层,反之,退出第 i 层递归则应返回至第 i - 1 层。

为了保证递归函数正确执行,系统建立一个“递归工作栈”作为整个递归函数运行期间使用的数据存储区,每一层递归所需信息构成一个“工作记录”,其中包括所有的实参、局部变量以及上一层的返回地址。每进入一层递归,就产生一个新的工作记录压入栈顶。每退出一层递归,就从栈顶弹出一个工作记录。当前活动的工作记录成为“活动记录”,并称活动记录的栈顶指针为“当前环境指针”。

一个递归问题可以分为“递推”和“回溯”两个阶段,要经历若干步才能求出最后的结果。但是其原理和一般函数的调用没有本质区别。递归函数调用次数越多,在栈上为其分配的空间就越大,所以我们应该避免调用次数过多的递归函数,因为该操作很可能会使栈的容量“溢出”。

部分内容引用自:

C语言-内存管理深入