数据结构与算法(5)- 栈

695 阅读8分钟

栈初识

1.栈结构图

顺序栈结构

顺序栈top状态

1、结构定义

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

typedef int Status;
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */

/* 顺序栈结构 */
typedef struct
{
    SElemType data[MAXSIZE];
    int top; /* 用于栈顶指针 */
}SqStack;

2、构建一个空栈

Status initStack(SqStack *s) {
    s->top = -1
    return OK;
}

3、清空栈

Status clearStack(SqStack *s) {
    //只需要修改top值,不需要将顺序栈的元素都清空
    s->top = -1
    return OK;
}

4、 判断顺序栈是否为空

Status isEmpty(SqStack s) {
    if (s.top == -1) {
        return  1;
    } else {
        return 0;
    }
}

5、返回栈的长度

int stackLength(SqStack s) {
    return s.top + 1;
}

6、获取栈顶

Status getTop(SqStack s, SElemType *e) {
    if (s.top == -1) {
        return Error;
    } else {
        *e = s.data[s.top];
    }
    return OK;
}

7、插入元素e为新栈顶元素

Status pushData(SqStack *s, SElemType e) {
    if (s->top == MAXSIZE + 1) return ERROR;
    s->top += 1;
    s->data[s->top+1] = e;
    retur OK;
}

8、删除S栈顶元素,并且用e带回

Status pop(SqStack *s, SElemType *e) {
    if (s->top == -1) return ERROR;
    *e = s->data[s->top];
    s->top--;
    return;
}

9、从栈底到栈顶依次对栈中的每个元素打印

void stackTraverse(SqStack s) {
    if (s.top == -1)
    printf("此栈为空!");
    
    printf("此栈中所有元素");
    for(int i = 0; i < s.top; i++) {
        printf("%d ",s.data[s.top]);
    }
    printf("\n");
}

验证:

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("顺序栈的表示与实现!\n");
    
    
    SqStack S;
    int e;
    
    if (initStack(&S) == OK) {
        for (int j = 1 ; j < 10; j++) {
            pushData(&S, j);
        }
    }
    
    printf("顺序栈中元素为:\n");
    stackTraverse(S);
    
    pop(&S, &e);
    printf("弹出栈顶元素为: %d\n",e);
    stackTraverse(S);
    printf("是否为空栈:%d\n",StackEmpty(S));
    getTop(S, &e);
    printf("栈顶元素:%d \n栈长度:%d\n",e,stackLength(S));
    clearStack(&S);
    printf("是否已经清空栈 %d, 栈长度为:%d\n",StackEmpty(S),stackLength(S));
    

    return 0;
}

打印结果:

链式栈结构

链式栈结构图

1、定义链式栈结构

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

typedef int Status;
typedef int SElemType;/* SElemType类型根据实际情况而定,这里假设为int */

//链式结构
typedef struct StackNode {
    SElemType data;
    struct StackNode *next;
}StackNode,*LinkStackPtr;

typedef struct {
    LinkStackPtr top;
    int count;
}LinkStack;

2、构造一个空栈S

Status initStack(LinkStack *s) {
    s->top = NULL;
    s->count = 0;
    return OK;
}

3、把链栈S置为空栈

Status clearStack(LinkStack *s) {
    LinkStackPtr p, q;
    p = s->top;
    while(p) {
        q = p->next;
        free(p);
        p = q;
    }
    s->count = 0;
    return OK;
}

4、若栈S为空栈,则返回TRUE, 否则返回FALSE

Status isEmpty(LinkStack s) {
    if(s.count == 0) {
        return TURE;
    } else {
        return FALSE;
    }
}

5、返回S的元素个数,即栈的长度

int stackLength(LinkStack s) {
    return s.count;
}

6、若链栈S不为空,则用e返回栈顶元素,并返回OK ,否则返回ERROR

Status getPop(LinkStack s, SElemType *e) {
    if(s.count == NULL) return ERROR;
    *e = s.top->data;
    return OK;
}

7、插入元素e到链栈S (成为栈顶新元素)

Status pushData(LinkStack *s, SElemType e) {
    LinkStackPtr q;
    //创建新结点
    q = (LinkStackPtr)malloc(sizeof(LinkStackPtr));
    q->data = e;
    //将新结点的后继结点赋值为s->top
    q->next = s->top;
    //将s->top地址指向q
    s->top = q;
    //技术要加1
    s->count++;
    return OK;
}

8、若栈不为空,则删除S的栈顶元素,用e返回其值. 并返回OK,否则返回ERROR

Status pop(LinkStack *s, SElemType *e) {
    if(s->top == NULL) return ERROR;
    LinkStackPtr p;
    //将栈顶元素返回
    *e = s->top->data;
    //将栈顶元素赋给 p保存
    p = s->top;
    //将栈顶指向往后移一位
    s->top = p->next;
    //释放
    free(p);
    s->count--;
    return = OK;
}

9、遍历链栈

void stackTraverse(LinkStack s) {
    if (s.top == NULL) 
        printf("此栈为空!");
    LinkStackPtr p;
    p = s->top;
    printf("栈数据为:");
    while(p)
        printf("%d ");
    }
    printf("\n");
}

10、验证

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("链栈定义与实现\n");
    
    int j;
    LinkStack s;
    int e;
    if(initStack(&s)==OK)
        for(j=1;j<=10;j++)
            pushData(&s,j);
    printf("栈中元素依次为:");
    stackTraverse(s);
    pop(&s,&e);
    printf("弹出的栈顶元素 e=%d\n",e);
    stackTraverse(s);
    printf("栈空否:%d(1:空 0:否)\n",isEmpty(s));
    getPop(s,&e);
    printf("栈顶元素 e=%d 栈的长度为%d\n",e,stackLength(s));
    clearStack(&s);
    printf("清空栈后,栈空否:%d(1:空 0:否)\n",isEmpty(s));
    
    return 0;
}

打印结果:

递归

什么是递归?若在一个函数,过程或数据结构定义的内部又直接(或间接)出现定义本身的应用;则称为他们是递归的,或者是递归定义。

1、采用递归算法解决的问题的情况

1.1、定义是递归的

比如很多数学定义本身就是递归定义,如,阶乘、二阶裴波拉契数列

(1).阶乘Fact(n)

  1. 若 n = 0,则返回 1;

  2. 若 n > 1, 则返回 n*Fact(n-1);

long Fact(n) {
    if n == 0 return 1;
    if(n>1) {
        return n * Fact(n - 1);
    }
}

(2).二阶斐波拉契数列Fib(n)

1.若 n = 1 或者 n = 2,则返回 1;

2.若 n > 2,则返回 Fib(n - 1) + Fib(n - 2);

long Fib(n) {
    if (n <= 2) {
        return 1;
    } else  {
        return Fib(n - 1) + Fib(n - 2);
    }
}

对于类似这种复杂问题,若能够分解成几个简单且解法相同或类似的子问题来求解,便称为递归求解。

例如,在求解4!时先求解 3!,然后再进一步分解进行求解,这种求解方式叫做“分治法”。

采取“分治法”进行递归求解的问题需要满足以下三个条件:

  • 能将一个问题转换变成一个小问题,而新问题和原问题解法相同或类同,不同的仅仅是处理的对象,并且这些处理更小且变化有规律的,
  • 可以通过上述转换而使得问题简化
  • 必须有一个明确的递归出口,或称为递归边界
void p(参数表) {
    if(递归结束条件成立) 可直接求解;//递归终止条件
    else p(较小的参数);//递归步骤
}

1.2、数据结构是递归的

其数据结构本身具有递归的特性。

例如,对于链表,其结点LNode的定义由数据域 data 和指针域 next 组成,而指针域 next 是一种指向 LNode 类型的指针,即 LNode 的定义中又用到了其自身,所以链表是一种递归的数据结构;

void TraverseList(LinkList P) {
    //递归终止
    if (p == NULL) return;
    else {
        //输出当前结点的数据域
        printf("%d", p->data);
        //p指向后继结点继续递归
        TraverseList(p->next);
    }
}

总结,在递归算法中,如果当递归结束条件成立,只执行return 操作时,分治法求解递归问题算法一般形式可以简化为:

void p(参数表) {
    if(递归结束条件成立)
        p(较小参数);
}
void TraverseList(LinkList p) {
    if(p) {
        printf("%d", p->data);
        TraverseList(p->next);
    }
}

1.3、问题的解法是递归的 有一类问题,虽然问题本身并没有明显的递归结构,但是采用递归求解比迭代求解更简单,如,Hanoi塔问题、八皇后问题、迷宫问题。

思路:

2、递归过程与递归工作栈

一个递归函数,在函数的执行过程中,需要多次进行自我调用,那么,一个递归函数是如何执行的?

在了解递归函数是如何执行之前,先来了解一下任何的2个函数之间调用是如何进行的。

在高级语言的程序中,调用函数和被调用的函数之间的链接与信息交换都是通过栈来进行的。

通常,当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统需要先完成 3 件事:

1.将所有的实参,返回地址等信息调用传递被调用函数保存;

2.为被调用函数的局部变量分配存储空间;

3.将控制转移到被调用函数入口。

而从被调用函数返回调用函数之前,系统同样需要完成 3 件事;

1.保存被调用函数的计算结果;

2.释放被调用函数的数据区;

3.依照被调用函数保存的返回地址将控制移动到调用函数。

当多个函数构成嵌套调用时,按照“先调用后返回”的原则,上诉函数之间的信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行时的所需要的数据空间都安排在一个栈中,每当调用一个函数时,就在它的栈顶分配一个存储区,每当这个函数退出是,就释放它的存储区,则当前运行时的函数的数据区必在栈顶。

    int second(int d) {
        int x, y;
        //...
    }
    
    int first(int s, int t) {
        int i;
        //...
        second(i)
        //2.入栈
        //...
    }
    
    void main() {
        int m, n;
        first(m,n);
        //1.入栈
        //...
    }

在主函数main中调用函数 first,而在函数first 又嵌套调用了 second 函数,则,当我们执行当前在执行的first 函数时,则图(a)栈空间里保存了这些信息;而当前我们执行second 则图(b)保存了这些信息。

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

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