1 栈的分析
1.1 栈的介绍
栈(Stack)是一种动态集合,实现它所采用的策略为后进先出(last-in, first-out,LIFO),也就是说当我们进行出栈(即弹出)操作时,被弹出的是最近入栈的元素。可以把栈想象成一叠盘子,每次取用只能是最上面的盘子。
1.2 栈的操作
作为一个栈最少有两种基本操作,分别是:
(1)压入 (push):也可称为插入(insert)、入栈,它向栈添加一个新的元素。
(2)弹出(pop):也可称为出栈,它将栈最近压入的元素弹出并删除该元素(指的是逻辑上的删除)。
这两种基本操作可以有多种实现方式,被实现的功能大同小异。
除了这两种基本操作外,栈还可以有几种辅助操作和基本操作所需的子操作,比如检测栈上溢与栈下溢。
1.3 底层分析
实现栈时,底层可以用固定大小的数组,也可以申请动态空间以获得大小可变的数组,在这里我们使用的是大小固定的数组。
技巧:为了使用的方便和逻辑上的更为合理,我们将数组的第一个元素(即索引 0 处)设置为栈的辅助指标,它可以指定插入和删除的相对位置,并且索引 0 被占用后,数组在使用时更贴近我们逻辑实现,只有索引 1、2、3…n,才能真正存储数据(逻辑上我们不说第 0 个位置)。
注意:栈的弹出操作并非是真的从数组上删除这个元素。从效率时间复杂度上来讲,我们利用辅助指标并没有真的删除元素,它只是让我们无法访问屏蔽后的元素,当我们需要压入元素时,其实只是覆盖了栈中的废弃元素,省去了删除这一步操作;但如果我们采用真正删除的方式,也就进行了一些没有必要的操作。
上述理念如下图:
正如上图所示,索引 0 处的辅助指标存储的值是指向即将弹出元素的当前位置和压入元素的前一个位置。
1.4 过程演示
我们假定栈 S 以数组的方式实现,数组头元素 S[0] 为辅助指标 S.top。
下面为栈的简单操作过程(图中数组不包括 S[0] ):
解析:上图浅色格子才是栈内元素,深色部分在逻辑上已经不属于栈,因为辅助指标已经将它们屏蔽(并没有真正的删除)。
(a)栈 S 有 4 个元素,S.top 指向栈顶元素 9。
(b)调用 push(S, 17)和 push(S, 3),分别压入新的元素 17 和 3,S.top 指向新的栈顶元素 3。
(c)调用 pop(S),弹出栈顶元素 3,S.top 指向新的栈顶元素 17,此时索引 6 处的元素 3 已经不可见。
2 实现代码
代码实现分为三个部分(1)头文件 (2)函数实现 (3)测试,这三个部分一起放在一个项目中。
(1)头文件(stack.h):
//: stack.h
// 此处底层使用的是固定大小的数组,并且索引 0 处存储辅助指标,用于辅助压入、弹出操作
#ifndef STACK_H
#define STACK_H
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
// 操作:向栈数组输入指定数量的数据
void input();
// 操作:压入
void push();
// 操作:测试栈是否为满
bool stackFull();
// 操作:将栈数组的全部元素输出
void output();
// 操作:弹出
int pop();
// 操作:测试栈是否为空
bool stackEmpty();
#endif
(2)函数实现(stack.c):
//: stack.c--栈
// 此处底层使用的是固定大小的数组,并且索引 0 处存储辅助指标,用于辅助压入、弹出操作
#include "stack.h"
// 操作:向栈数组输入指定数量的数据
void input(int stack[], int neededNum, const int maxStack) {
int i, element;
puts("输入:");
for (i = 0; i < neededNum; i++) {
printf("请输入第 %d 个元素:", i + 1);
scanf("%d", &element);
push(stack, element, maxStack);
}
}
// 操作:压入元素
void push(int stack[], int element, const int maxStack) {
if (stackFull(stack)) {
printf("栈已满");
exit(false);
}
else
stack[++stack[0]] = element; // 压入元素时,提前将辅助指标加 1 以指向下一个空白位置
}
// 操作;测试栈是否为满
bool stackFull(int stack[], const int maxStack) {
if (maxStack == stack[0])
return true;
else
return false;
}
// 操作:将栈数组的全部元素输出
void output(int stack[]) {
int i = 0;
puts("\n输出:");
while (1) // 不需要终点指标,因为 pop() 在没有元素时程序会直接退出
printf("第 %d 个元素:%d\n", ++i, pop(stack));
}
// 操作:弹出元素
int pop(int stack[]) {
if (stackEmpty(stack)) {
printf("栈已经没有元素");
exit(false);
}
else
return stack[stack[0]--]; // 弹出元素后,将辅助指标减 1
}
// 操作:测试栈是否为空
bool stackEmpty(int stack[]) {
if (0 == stack[0])
return true;
else
return false;
}
(3)测试(main.c):
#include <stdio.h>
#define MAX_STACK 1000
int main(void) {
int neededNum;
int stack[MAX_STACK + 1]; // 索引 0 处用来存储辅助指标
stack[0] = 0; // 索引 0 处存储的是辅助指标,初始值为 0,表示没有元素
printf("想要多少个元素:");
scanf("%d", &neededNum);
input(stack, num);
output(stack);
return 0;
}
2.1 代码分析
栈数组的设置创建工作是由测试部分完成的,当在 main() 中得到数组后,我们就可以使用 stack.c 中的函数来对其进行栈的相应操作。
利与弊:
- 此处的栈实现因为底层是大小固定的数组,所以只能适应部分情况,当然,你可以在 main() 中修改 MAX_STACK 来改变栈的容量,虽然这种做法相比动态数组不太灵活,但它避免了内存泄漏的危险。
- 为了简洁,我并没有在这个项目中添加生成随机数函数,所以数据的生成要靠用户自己输入。
- 这个项目中并没有持续交互的功能,用户只能进行一整次输入操作,然后得到输出,如果想要功能丰富的栈应用,那还应该有个良好的交互菜单。
2.2 技巧
- 程序使用的各种方法、变量的命名应该具有自解释性,不要让注释去承担命名的解释工作。
- 虽然在 main() 中已经设置了全局变量 MAX_STACK,但是我们并不能随意的在 stack.c 中使用它,我们应当通过参数传递来使用它,这是避免让项目的各文件之间有过高的耦合性,因为我们不能因为没有全局变量就无法使用其它函数。
- 我们将大小可能会变的变量参数设置成 const 的,这是避免在其作用域中无意修改了它。
- 在使用 == 运算符时,将变量放在右边,不能改变的量(比如常量、const 变量)放在左边,也就可以避免无意间将 == 写成了 =。
备注:
更多细节可看《算法导论》、《数据结构:C 语言实现》,欢迎大家一起探讨学习。