一.概念
1、栈的定义
栈 是仅限在 表尾 进行 插入 和 删除 的 线性表。
2、栈顶
栈 是一个线性表,我们把允许 插入 和 删除 的一端称为 栈顶。
二。栈的顺序表实现
1、数据结构定义
对于顺序表,在 C语言 中表现为 数组,在进行 栈的定义 之前,我们需要考虑以下几个点:
1)栈数据的存储方式,以及栈数据的数据类型;
2)栈的大小;
3)栈顶指针;
- 我们可以定义一个 栈 的 结构体,C语言实现如下所示:
#define DataType int // (1)
#define maxn 100005 // (2)
struct Stack { // (3)
DataType data[maxn]; // (4)
int top; // (5)
};
- (1)(1) 用
DataType
这个宏定义来统一代表栈中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等; - (2)(2)
maxn
代表我们定义的栈的最大元素个数; - (3)(3)
Stack
就是我们接下来会用到的 栈结构体; - (4)(4)
DataType data[maxn]
作为栈元素的存储方式,数据类型为DataType
,可以自行定制; - (5)(5)
top
即栈顶指针,data[top-1]
表示栈顶元素,top == 0
代表空栈;
2.入栈
void stackpush(struct stack*stk, datatype dt)//因为会改变stk,所以传入指针
{
stk->data[stk->top]=dt;
stk-<top++;
}
3、出栈
1、动画演示
如图所示,蓝色元素 为原本在栈中的元素,红色元素 为当前需要 出栈 的元素,执行完毕以后,栈顶的指针减一。具体来看下代码实现。
2、源码详解
- 出栈 操作,只需要简单改变将 栈顶 减一 即可,代码实现如下:
void StackPopStack(struct Stack* stk) {
--stk->top;
}
4、清空栈
1、动画演示
如图所示,对于数组来说,清空栈的操作只需要将 栈顶指针 置为栈底,也就是数组下标 0 即可,下次继续 入栈 的时候会将之前的内存重复利用。
2、源码详解
- 清空栈的操作只需要将 栈顶 指针直接指向 栈底 即可,对于顺序表,也就是 C语言 中的数组来说,栈底 就是下标 0 的位置了,代码实现如下:
void StackClear(struct Stack* stk) {
stk->top = 0;
}
5、只读接口
- 只读接口包含:获取栈顶元素、获取栈大小、栈的判空,实现如下:
DataType StackGetTop(struct Stack* stk) {
return stk->data[ stk->top - 1 ]; // (1)
}
int StackGetSize(struct Stack* stk) {
return stk->top; // (2)
}
bool StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk); // (3)
}
- (1)(1) 数组中栈元素从 0 开始计数,所以实际获取元素时,下标为 栈顶元素下标 减一;
- (2)(2) 因为只有在入栈的时候,栈顶指针才会加一,所以它 正好代表了 栈元素个数;
- (3)(3) 当 栈元素 个数为 零 时,栈为空。
三。栈的链表实现//区别就是栈本身是数组还是链表
1.数据结构定义
struct stacknode{
Datatype data;
struct stacknode*next;
}
struct stack{
struct stacknode*top;
int size;
}
- (1) 栈结点元素的 数据域,这里定义为整型;
- (2)(2)
struct StackNode;
是对链表结点的声明; - (3)(3) 定义链表结点,其中
DataType data
代表 数据域;struct StackNode *next
代表 指针域; - (4)(4)
top
作为 栈顶指针,当栈为空的时候,top == NULL
;否则,永远指向栈顶; - (5)(5) 由于 求链表长度 的算法时间复杂度是 O(n)O(n) 的, 所以我们需要记录一个
size
来代表现在栈中有多少元素。每次 入栈时size
自增,出栈时size
自减。这样在询问栈的大小的时候,就可以通过 O(1)O(1) 的时间复杂度。
2.入栈
void push(struct stack*stk,Datatype dt)
{
struct stacknode*insertnode=(struct stacknode*)malloc(sizeof(struct stacknode) );
insertnode->next=stk->top;
stk->top=insertnode;
stk->size++;
}
3.出栈
void delete(struct stack*stk)
{
struct stacknode*cur=stk->top;//由于是已经存在的所以不用malloc
stk->top=cur->next;
free(cur);
stk->size--;
}
4.清空栈
清空栈 可以理解为,不断的出栈,直到栈元素个数为零。
void StackClear(struct Stack* stk) {
while(!StackIsEmpty(stk)) { // (1)
StackPopStack(stk); // (2)
}
stk->top = NULL; // (3)
}
用数组实现的话stk->top=0表示空栈
5、只读接口
- 只读接口包含:获取栈顶元素、获取栈大小、栈的判空,实现如下:
DataType StackGetTop(struct Stack* stk) {
return stk->top->data; // (1)
}
int StackGetSize(struct Stack* stk) {
return stk->size; // (2)
}
int StackIsEmpty(struct Stack* stk) {
return !StackGetSize(stk);
}
- (1)(1)
stk->top
作为 栈顶指针,它的 数据域data
就是 栈顶元素的值,返回即可; - (2)(2)
size
记录的是 栈元素个数; - (3)(3) 当 栈元素 个数为 零 时,栈为空。
四、两种实现的优缺点
1、顺序表实现
在利用顺序表实现栈时,入栈 和 出栈 的常数时间复杂度低,且 清空栈 操作相比 链表实现 能做到 O(1)O(1),唯一的不足之处是:需要预先申请好空间,而且当空间不够时,需要进行扩容,扩容方式本文未提及,可以参考以下文章:《C/C++ 面试 100 例》(四)vector 扩容策略。
2、链表实现
在利用链表实现栈时,入栈 和 出栈 的常数时间复杂度略高,主要是每插入一个栈元素都需要申请空间,每删除一个栈元素都需要释放空间,且 清空栈 操作是 O(n)O(n) 的,直接将 栈顶指针 置空会导致内存泄漏。好处就是:不需要预先分配空间,且在内存允许范围内,可以一直 入栈,没有顺序表的限制。