数据结构分为:逻辑结构和物理结构
- 逻辑结构:线性结构、集合结构、树形结构、图形结构
- 物理结构:顺序存储结构、链式存储结构
一、栈的定义
栈是一种特殊的线性结构,先进后出,只能在一段进行操作,我们把允许插入和删除的一端称为栈顶,另一端称为栈底。
- 不含任何数据元素的栈称为空栈。
- 栈的插入操作叫做进栈,也叫做压栈、入栈
- 栈的删除操作,叫做出栈,也叫做弹栈。
- 我们一般吧运行操作的一端叫做top(栈顶),并用一个变量进行标示
栈结构:
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;
typedef int SElemType; /* SElemType类型根据实际情况而定,这里假设为int */
顺序栈实现:
顺序栈是利用顺序存储的形式的栈,本质上用一块连续的内存空间来存储元素,可以用下标来标示,所以top可以是下标的格式
我们用一幅图来形容下顺序栈的几个状态:
我们是top来标记栈里面有几个元素的,当top = -1时候代表是空栈,当top = 0 时代表有一个元素,当top 指向栈顶时候代表栈满了。
下面我们实现一下顺序存储的栈:
定义一个栈: /* 顺序栈结构 / typedef struct { //定义的是,就相当于创建了一个MAXSIZE大小的int数组 SElemType data[MAXSIZE]; int top; / 用于栈顶指针 */ }SqStack;
创建:
//4.1 构建一个空栈S
Status InitStack(SqStack *S){
//将top置为-1,代表是空栈,因为栈结构定义时候就已经创建了一个数组,所以现在不用初始化了
S->top = -1;
return OK;
}
将栈置空
//4.2 将栈置空
Status ClearStack(SqStack *S){
//疑问: 将栈置空,需要将顺序栈的元素都清空吗?
//不需要,只需要修改top标签就可以了.因为需要修改栈元素,所以需要将指针传进来,如果只是为了读取不需要将指针传进来,直接将栈传进来,然后用点语法读取
S->top = -1;
return OK;
}
判断顺序栈是否为空
//4.3 判断顺序栈是否为空;
Status StackEmpty(SqStack S){
if (S.top == -1)
return TRUE;
else
return FALSE;
}
返回栈的长度:
int StackLength(SqStack S){
//因为top标示从0开始的,所以长度要加1
return S.top + 1;
}
获取栈顶
Status GetTop(SqStack S,SElemType *e){
if (S.top == -1)//如果为空栈的话,就不存在栈顶,所以返回错误
return ERROR;
else
*e = S.data[S.top];
return OK;
}
插入元素e为新栈顶元素
Status PushData(SqStack *S, SElemType e){
//栈已满
if (S->top == MAXSIZE -1) {
return ERROR;
}
//将栈顶先加1,再插入,这样避免在插入过程中有新的元素插入进来导致错误
//栈顶指针+1;
S->top ++;
//将新插入的元素赋值给栈顶空间
S->data[S->top] = e;
return OK;
}
删除S栈顶元素,并且用e带回
Status Pop(SqStack *S,SElemType *e){
//空栈,则返回error;
if (S->top == -1) {
return ERROR;
}
//将要删除的栈顶元素赋值给e
*e = S->data[S->top];
//栈顶指针--;
S->top--;
return OK;
}
从栈底到栈顶依次对栈中的每个元素打印
Status StackTraverse(SqStack S){
int i = 0;
printf("此栈中所有元素");
while (i<=S.top) {
printf("%d ",S.data[i++]);
}
printf("\n");
return OK;
}
链式栈
链式栈结构:
链式栈进栈操作:
链式栈就是以链式存储的形式的栈,因为是链式结构,所以就不存在下标的说法,也不存在容量的问题,所以top是以指针形式指向栈顶的元素,并且因为栈是先进后出的,所以需要用前插法进行插入操作
/* 链栈结构 */
定义节点
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStackPtr;
定义链式栈结构
typedef struct
{
LinkStackPtr top;
int count;
}LinkStack;
构造一个空栈
Status InitStack(LinkStack *S)
{
S->top=NULL;
S->count=0;
return OK;
}
插入元素e到链栈S (成为栈顶新元素)
Status Push(LinkStack *S, SElemType e){
//创建新结点temp
LinkStackPtr temp = (LinkStackPtr)malloc(sizeof(StackNode));
//赋值
temp->data = e;
//前插法把当前的栈顶元素赋值给新结点的直接后继, 参考图例第①步骤;
temp->next = S->top;
//将新结点temp 赋值给栈顶指针,参考图例第②步骤;
S->top = temp;
S->count++;
return OK;
}
若栈不为空,则删除S的栈顶元素,用e返回其值. 并返回OK,否则返回ERROR
Status Pop(LinkStack *S,SElemType *e){
LinkStackPtr p;
if (StackEmpty(*S)) {
return ERROR;
}
//将栈顶元素赋值给*e
*e = S->top->data;
//将栈顶结点赋值给p,参考图例①
p = S->top;
//使得栈顶指针下移一位, 指向后一结点. 参考图例②
S->top= S->top->next;
//释放p
free(p);
//个数--
S->count--;
return OK;
}
遍历链栈
Status StackTraverse(LinkStack S){
LinkStackPtr p;
p = S.top;
while (p) {
printf("%d ",p->data);
p = p->next;
}
printf("\n");
return OK;
}
二、栈和递归
我们先来了解一下递归:
函数直接或者间接的调用函数本身,这种函数我们叫做递归
当我们遇到下面情况时候可以选择用递归来解决:
- 定义是递归的:例如数学上的定义,阶乘,斐波拉契数列
- 数据结构是递归的:链表(节点都是一样的,有数据域和指针域,而指针域指向的是另一个相同结构的节点)这个叫数据结构式递归
- 链表的打印,可以用递归的方式
- 所有递归都能改成循环
- 问题递归:
分治法:就是用递归的方式解决问题,需要满足三个条件:
- 大问题拆为小问题,并且大问题和小问题解决方法是一样或者类似
- 复杂问题简单化
- 有一个出口,就是有一个结束的条件,也就是递归边界
斐波拉契数列就是,一个数等于前两个数之和,当我们求位置a的值时候需要拿到前两个数组,而前两个数的值需要再前面的数,而截止点就说a,并且我们就可以把这个问题拆成很多个小问题,这个符合递归的定义。
例如我们看一下斐波拉契数列的实现:
int Fbi(int i){
if(i<2)
return i == 0?0:1;
return Fbi(i-1)+Fbi(i-2);
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("斐波拉契数列!\n");
// 1 1 2 3 5 8 13 21 34 55 89 144
for (int i =0; i < 10; i++) {
printf("%d ",Fbi(i));
}
printf("\n");
return 0;
}
从上面实现,我们可以想到,不管是高级语言还是低级语言,调用方法时候都要完成下面操作:
- 将所有的实参、返回地址等信息传递给被调函数保存
- 为被调函数的局部变量分配存储区
- 将控制转移到被调函数入口,也就是将控制权交给被调函数
当一个函数完成之后进行出栈操作,出栈之前同样要完成三件事:
- 保存被调函数的计算结果
- 释放被调函数的数据区
- 依照被调函数保存的返回地址将控制转移到被调函数
根据上面规则我们发现函数先调用后返回,所以上面操作必须通过栈来实现,也就是整个程序的运行空间安排在一个栈中,每当运行一个函数时,就在栈顶分配空间,函数退出后,释放这块空间,这样才不会造成函数调用的混乱,所以当前运行的函数一定在栈顶。我们可以看一下下面这个图:
调用时候从上往下,当调用返回时候就是从下往上
因为使用递归,每个函数都要开一个函数堆栈空间,所以在日常开发中使用递归很耗内存,所以用的很少