栈的实现之链式栈
相比较于数组和链表,栈是一种操作受限的线性结构。
这种操作受限体现在:
- 栈只能在同一端添加、删除以及访问元素(栈顶)
- 另一端无法执行任何操作(栈底),栈底的元素既不能直接增删,也不能直接访问。
你可以把栈想象成一个杯子,杯底相当于栈底,无法直接进行任何操作。杯口就相当于栈顶,是唯一可以进行添加、删除和访问操作的地方。
在栈中,最先添加到栈中的元素总是最后被删除的,遵循**后进先出(LIFO, Last In First Out)**的原则。
如下图所示:
栈这种数据结构的基本操作主要包括以下几种:
- 入栈/压栈(push):在栈顶添加一个元素,成为新的栈顶元素,其余元素则被压入栈底。时间复杂度O(1)
- 出栈/弹栈(pop):删除栈顶元素,下一个元素成为新的栈顶元素。时间复杂度O(1)
- 访问栈顶元素(peek):返回栈顶元素的值。时间复杂度O(1)
- 判空(is_empty):检查栈中是否有任何元素。时间复杂度O(1),因为只需要检查一下栈顶元素即可。
说白了,栈就是一个线性结构(数组或链表都可以),然后只提供栈顶的增删访问操作。
1. 创建栈帧结点
1.1链式栈模型
以链表为基础实现一个栈,首先要基于以下结构体:
typedef int E;
// 栈帧结点
typedef struct node {
E data;
struct node *next;
} StackFrame;
我们现在先在头文件my_stack.h头文件里声明结构体,实现创建栈帧结点。
这个链式栈的模型如下图所示:
一般而言,我可以再定义一个结构体用于表示整个栈,但这里我们直接在main函数中使用以下语句创建一个栈:
StackFrame *stack = NULL; // stack指针指向栈顶,代指整个栈。NULL表示栈为空
于是栈的四个基本操作,就可以变为链表的四个操作:
- 入栈:也就是头插法在链表中插入一个结点
- 出栈:删除链表的第一个结点
- 访问栈顶元素:访问链表的第一个结点
- 判断:判断链表的第一个结点是不是NULL
于是我们就可以把这四个操作转换为四个函数声明在头文件中:
// 入栈
bool stack_push(StackFrame** stack_p, E data);
// 出栈
E stack_pop(StackFrame** stack_p);
// 访问栈顶元素
E stack_peek(const StackFrame* stack);
// 判空
bool stack_is_empty(const StackFrame* stack);
在实现这四个函数功能的.c文件里面需要注意:
- 入栈和出栈的函数需要对传入的栈顶指针做出修改,所以需要传入栈顶指针的二级指针!!
- 访问和判空则不需要修改栈帧指针,直接传入指针即可。可以使用const修饰它,这是一个好习惯。
2. 实现函数功能my_stack.c
2.1 实现判空
判空是很容易实现的,而且判空对后续某些操作有一定作用,所以我们可以最先实现它。参考代码如下:
// 判空
bool stack_is_empty(const StackFrame* stack) {
return stack == NULL;
}
2.2 实现入栈
单向链表是由一系列结点组成的线性数据结构,其中每个结点包含数据和一个指向下一个结点的指针。如下图所示:
在很多时候,我们往往还会定义一个LinkedList
结构体类型以表示单链表结构,但这里我们先不用。主要练习二级指针的使用。
只在需要创建单向链表的地方,用下列代码创建一个NULL结点指针,表示此时的链表一个结点没有,是一个空链表。
StackFrame *top = NULL; // stack指针指向栈顶,代指整个栈。NULL表示栈为空
所谓入栈,也就是在链表的头插法插入一个结点,这里只需要注意下二级指针就可以了,参考代码如下:
// 入栈
bool stack_push(StackFrame** stack_p, E data) {
// 1.创建新栈帧
StackFrame *new_frame = malloc(sizeof(StackFrame));
if (new_frame == NULL){
// 分配失败处理
printf("malloc failed in stack_push.\n");
return false;
}
// 2.初始化新栈帧
new_frame->data = data;
new_frame->next = *stack_p;
// 3.更新栈顶指针
*stack_p = new_frame;
return true;
}
首先,我们需要创建一个新的栈帧(StackFrame),栈帧是栈中的每个元素。我们通过调用malloc函数来分配内存空间,用来存储新的栈帧。如果分配失败,即malloc返回NULL,说明内存分配失败,我们打印错误信息,并返回false表示入栈失败。
接着,我们初始化新的栈帧。将传入的数据(data)赋值给新栈帧的data成员,这样新栈帧就存储了我们要入栈的数据。然后,将新栈帧的next成员指向原来的栈顶指针,这样新栈帧就连接到了原来的栈顶元素之上。
最后,我们更新栈顶指针。将栈顶指针(stack_p)指向新栈帧,这样新栈帧就成为了新的栈顶元素。
最后,返回true表示入栈成功。
2.3 实现出栈
所谓出栈,也就是删除链表的第一个结点,这个逻辑我们也很熟悉了,参考代码如下:
// 出栈
E stack_pop(StackFrame** stack_p) {
if (stack_is_empty(*stack_p)){
// 栈为空,出栈失败
printf("error: stack is EMPTY!\n");
exit(1);
}
// 初始化一个top指针指向栈顶,用于删除栈顶元素
StackFrame* top = *stack_p;
*stack_p = top->next;
int data = top->data;
free(top);
return data;
}
2.4 实现访问栈顶元素
// 访问栈顶元素
E stack_peek(const StackFrame* stack)
{
if (stack_is_empty(stack)) //判空
{
printf("error: stack is empty.\n");
exit(1);
}
return stack->data;//返回栈顶元素值
}
3. 测试
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include "my_stack.h"
/**********************************************************************
* 链式栈之测试 *
**********************************************************************/
int main(void) {
// 表示创建一个空的栈,这是一个指向栈顶元素的指针
StackFrame* top = NULL;
stack_push(&top, 1);
stack_push(&top, 2);
printf("%d\n",stack_peek(top));
stack_push(&top, 3);
stack_push(&top, 4);
stack_push(&top, 5);
printf("%d\n", stack_pop(&top));
printf("%d\n", stack_pop(&top));
printf("%d\n", stack_pop(&top));
printf("%d\n", stack_pop(&top));
printf("%d\n", stack_pop(&top));
printf("%d\n", stack_pop(&top));
return 0;
}