数据结构基础篇-栈

502 阅读7分钟

数据结构系列篇章-可以参照如下顺序阅读

关于栈

  • 栈是一种操作受限的线性表,只允许从一端插入和删除数据;
  • 栈有两种存储方式,即线性存储和链接存储(链表);
  • 栈的一个最重要的特征就是栈的插入和删除只能在栈顶进行,所以每次删除的元素都是最后进栈的元素,故栈也被称为后进先出(LIFO)表;
  • 一个栈顶指针,它初始值为-1,且总是指向最后一个入栈的元素,栈有两种处理方式,即压栈(push)和出栈(pop);
  • 在进栈只需要移动一个变量存储空间,所以它的时间复杂度为O(1)
  • 对于出栈分两种情况,栈未满时,时间复杂度也为O(1),但是当栈满时,需要重新分配内存,并移动栈内所有数据,所以此时的时间复杂度为O(n)
  • 如下图,左边为顺序栈,右边为链式栈

一、栈的顺序存储

1.1 顺序栈的结构设计

#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int SElemType;/* SElemType类型根据实际情况而定,这里假设为int */

//定义栈结构
typedef struct{
    SElemType data[MAXSIZE]; // 提前分配好的固定内存空间
    int top;// /*栈顶标示*/
}Node;

1.2 顺序栈的创建

Status InitStack(Stack *S){
    // 因为我们结构体中定义了一个数组元素,系统自动分配了内存空间,所以这里不用重新申请内存空间
    S->top = -1; // 将栈顶标示位指向-1,代表空栈
    return OK;
}

1.3 顺序栈的置空

BOOL IsEmptyStack(Stack S){
    if (S.top == -1) return FALSE;
    return TRUE;
}

1.4 顺序栈的非空判断

BOOL IsEmptyStack(Stack S){
    if (S.top == -1) return FALSE;
    return TRUE;
}

1.5 顺序栈的长度

int GetStackLength(Stack S){
    return S.top + 1;
}

1.6 顺序栈的栈顶元素

// 1. 非空判定
// 2. 获取栈顶元素 S.data[S.top]
Status GetTopStack(Stack S, SElemType *top){
    // 非空判定
    if (S.top == -1) return ERROR;
    *top = S.data[S.top];
    return OK;
}

1.7 顺序栈的压栈

// 1.溢出判定
// 2.栈长度+1
// 3.添加新的栈顶元素
Status PushStack(Stack *S, SElemType e){
    // 判断栈溢出
    if (S->top > MAXSIZE - 1) return ERROR;
    // 栈顶位置+1
    S->top++;
    // 设置当前栈顶元素
    S->data[S->top] = e;
    return OK;
}

1.8 顺序栈的出栈

// 1.非空判定
// 2.返回栈顶元素
// 3.栈长度-1
Status PopStack(Stack *S, SElemType *e){
    // 判断栈非空
    if (S->top == - 1) return ERROR;
    // 取出当前栈顶元素
    *e = S->data[S->top];
    // 栈顶标示-1
    S->top--;
    return OK;
}

1.9 顺序栈的遍历

// 1.非空判断
// 2.打印栈数据
Status TravelStack(Stack S){
    int p = 0;
    // 非空判定
    if (S.top == -1) return ERROR;
    printf("当前栈的数据:\n");
    while (p < S.top + 1) { // 当前非空栈
        printf("%d  ",S.data[p]);
        p++;
    }
    printf("\n");
    return OK;
}

1.10 结果预览

二、栈的链式存储

2.1 栈的链式存储结构设计

#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1

typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int SElemType;/* SElemType类型根据实际情况而定,这里假设为int */

// 栈节点结构
typedef struct StackNode{
    SElemType data; // 节点数据
    struct StackNode *next; // next指针
}StackNode,*LinkStackNode;

// 链式栈结构
typedef struct{
    LinkStackNode top; // 栈顶节点
    int length; // 栈大小
}StackLink;

2.2 链式栈的创建

// 1. 栈顶元素为空
// 2. count为0
Status InitLinkStack(StackLink *S){
    // 由于空栈不需要重新分配空间,我们只需要将栈顶指向标示位NULL即可
    S->top = NULL;
    // 空栈长度为0
    S->length = 0;
    return OK;
}

2.3 链式栈的清空

// 1. 从栈顶开始找
// 2. 释放每个节点空间
// 2.链式栈的清空
Status ClearLinkStack(StackLink *S){
    // 找到栈顶节点
    LinkStackNode top, temp;
    top = S->top;
    while (top->next) { // 循环查找
        temp = top->next;
        free(top);
        top = temp;
    }
    return OK;
}

2.4 链式栈的非空判定

BOOL IsEmptyLinkStack(StackLink S){
    if (S.length == 0) return TRUE;
    return FALSE;
}

2.5 获取链式栈的栈顶元素

Status GetTopLinkStack(StackLink S, SElemType *top){
    // 非空判定
    if (S.length == 0) return ERROR;
    // 取出当前栈顶元素
    *top = S.top->data;
    return OK;
}

2.6 链式栈的压栈

// 1. 创建栈节点, 失败判定
// 2. 赋值
// 3. 让创建节点的next指向top
// 4. top再指向新节点,count+1
Status PushLinkStack(StackLink *S, SElemType e){
    // 因为是链表,所以不用做溢出判定
    // 创建压栈节点
    LinkStackNode top = (LinkStackNode)malloc(sizeof(StackNode));
    if (top == NULL) return ERROR;
    top->data = e;
    // 压栈节点的next指向top,top指向压栈节点,length+1
    top->next = S->top;
    S->top = top;
    S->length++;
    
    return OK;
}

2.6 链式栈的出栈

// 1. 非空判定
// 2. 定义临时变量指向栈
// 3. 取出栈顶元素,栈顶指向栈顶元素后一位
// 4. 释放栈顶元素,count-1

    S->length++;
    
    return OK;
}

// 6.链式表的出栈
Status PopLinkStack(StackLink *S, SElemType *e){
    // 非空判定
    if (S->length == 0) return ERROR;
    LinkStackNode temp;
    // 取出栈顶节点数据
    *e = S->top->data;
    // 将top指向top的next,同时释放top的节点空间
    temp = S->top;
    S->top = S->top->next;
    free(temp);
    // 长度减1
    S->length--;
    return OK;
}

2.7 链式栈的遍历

Status TravelLinkStack(StackLink S){
    // 非空判定
    if (S.length == 0) return ERROR;
    // 定义临时变量
    int i = 0;
    printf("链式表的遍历:\n");
    while (i < S.length + 1) {
        printf("%d \n", S.top->data);
        S.top = S.top->next;
        i++;
    }
    printf("\n");
    
    return OK;
}

2.8 结果预期

三、栈和递归

3.1 递归的基本思想

  • 所谓递归,就是有去有回。
  • 递归的基本思想,是把规模较大的一个问题,分解成规模较小的多个子问题去解决,而每一个子问题又可以继续拆分成多个更小的子问题。
  • 最重要的一点就是假设子问题已经解决了,现在要基于已经解决的子问题来解决当前问题;或者说,必须先解决子问题,再基于子问题来解决当前问题。

3.2 递归的数据结构

3.2.1 从函数调用看广义递归

对于软件来说,函数的调用关系就是一个广义递归的过程,流程如下:

  • 调用函数A
  • 调用函数B
  • 调用函数C
  • 函数C返回;
  • 函数B返回;
  • 函数A返回;
func_A()
{
    func_B();
}
func_B()
{
    func_C();
}
func_C()
{
}
int main()
{
    func_A();
}

3.2.2 狭义递归函数

有一种特例,就是处理问题A/B/C的方法是一样的,这就是产生了狭义的“递归函数“,即函数内又调用函数自身。

3.2.3 递归的数据结构栈

从上述分析看,递归对问题的处理顺序,是遵循了先入后出(也就是先开始的问题最后结束)的规律。 这个其实就是我们栈的特性,经典的例子就是函数调用,就是依靠栈来实现的。

3.3 使用递归的条件

  • 1.数学定义是递归,比如很多数学定义本身就是递归的,例如阶乘,二阶斐波拉契数列;
  • 2.数据结构是递归的,例如链表,其节点由数据域data和指针域next组成,而指针域next是一个指向链表类型的指针,即链表的定义又用到了本身,所以链表是一种递归的数据结构;
  • 3.问题的解法是递归的,有一类问题,虽然问题本身没有明显的递归,但是采样递归求解比迭代求解更简单,如Hanoi塔问题,八皇后问题,迷宫问题

3.4 二阶斐波拉契数列实例

如果兔⼦2个月之后就会有繁衍能力,那么一对兔子每个月能生出一对兔子; 假设所有的兔子都不死,那么n个月后能⽣成多少只兔子?

3.4.1 分析:

  • 可以看到 从3月开始,每个月的兔子对数都是前两个月的兔子数量的和

3.4.2 递归实现:

// 二阶斐波拉契数列
int FBi(int n){
   if (n <= 1) {
       return n == 0 ? 0 : 1;
   }
   return FBi(n-1) + FBi(n-2);
}

3.4.3 结果预期: