目录
03 peek(查看栈顶元素) pop(出栈)push(入栈)
03 peek(查看栈顶元素) pop(出栈)push(入栈)
前言
栈是一种数据结构,它按照后进先出(LIFO)的原则管理数据。这意味着最后放入栈的元素将首先被取出。栈有两个主要操作:压栈(push),将元素放入栈的顶部;出栈(pop),从栈的顶部取出元素。栈常常用于跟踪程序执行过程中的函数调用、表达式求值等操作,也可用于解决一些计算问题。
指针数组
/**
* 数组: 数组是一种数据结构,可以存储相同类型的多个元素,这些元素在内存中是连续排列的。
* 指针: 指针是一个特殊的变量,它存储了一个内存地址,即某个数据的位置。
* 指针数组: 当我们将指针放入数组中,这个数组就成为指针数组。数组的每个元素都是一个指针,指向内存中的某个位置。
*/
int numbers[3]; // 这是一个整数数组
int *ptrArray[3]; // 这是一个指针数组,每个元素是一个整数指针
int a = 10, b = 20, c = 30;
ptrArray[0] = &a; // 第一个元素指向变量 a
ptrArray[1] = &b; // 第二个元素指向变量 b
ptrArray[2] = &c; // 第三个元素指向变量 c
1 用数组(指针数组)的形式构建栈
01 创建结构体,栈大小初始化
#define INITIAL_CAPACITY 5 // 初始容量
#define GROWTH_FACTOR 2 // 扩容因子
typedef struct {
void** data ; // 栈中元素
int top; // 栈顶指针
int capacity; // 栈容量
} Stack;
/**
* @brief 初始化栈
*
* 初始化给定的栈结构,为其分配内存空间,并设置初始状态。
*
* @param stack 栈指针
*/
void cstack_initStack(Stack* stack) {
// 为数据部分分配内存空间
stack->data = (void**)malloc(INITIAL_CAPACITY * sizeof(void*));
// 初始化栈顶为-1,表示栈为空 这是因为栈的索引是从0开始的,所以当top为-1时,表示栈中没有元素
stack->top = -1;
// 初始化栈的容量为初始容量
stack->capacity = INITIAL_CAPACITY;
}
02 内部必须实现的函数
/**
* @brief 判断栈是否为空
*
* 判断给定的栈是否为空,如果栈顶指针为-1,则返回1,否则返回0。
*
* @param stack 栈指针
*
* @return 如果栈为空,则返回1;否则返回0。
*/
int cstack_isEmpty(Stack* stack) {
// 判断栈顶指针是否为-1,如果是则返回1,表示栈为空
return stack->top == -1;
}
/**
* @brief 判断栈是否已满
*
* 判断给定的栈是否已满。如果栈已满,返回1;否则返回0。
*
* @param stack 栈指针
*
* @return int 如果栈已满,返回1;否则返回0
*/
int cstack_isFull(Stack* stack) {
// 判断栈顶指针是否等于栈的最大容量减去1,如果是,说明栈已满
return stack->top == stack->capacity - 1;
}
/**
* @brief 扩大栈的空间
*
* 根据设定的增长因子,扩大给定栈的空间。
*
* @param stack 栈指针
*/
void cstack_grow(Stack* stack) {
// 扩大栈的空间,增长因子为预先设定的常量
stack->capacity *= GROWTH_FACTOR;
// 重新分配内存空间,以容纳更多的元素
stack->data = (void**)realloc(stack->data, stack->capacity * sizeof(void*));
// 如果内存分配失败,则输出错误信息并退出程序
if (stack->data == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
}
/**
* @brief 释放堆栈内存
*
* 释放给定堆栈的内存资源。
*
* @param stack 堆栈指针
*/
void cstack_freeStack(Stack* stack) {
free(stack->data);
}
03 peek(查看栈顶元素) pop(出栈)push(入栈)
/**
* @brief 将元素推入堆栈
*
* 将指定的元素推入给定的堆栈中。如果堆栈已满,则会扩展堆栈的大小。
*
* @param stack 堆栈指针
* @param value 要推入堆栈的元素指针
*/
void cstack_push(Stack* stack, void* value) {
if (cstack_isFull(stack)) {
cstack_grow(stack);
}
stack->data[++stack->top] = value;
}
/**
* @brief 出栈操作
*
* 从给定的栈中弹出一个元素,并返回该元素的值。如果栈为空,则无法执行出栈操作,会输出错误信息。
*
* @param stack 栈指针
*
* @return 成功出栈的元素值,如果栈为空则返回NULL
*/
void* cstack_pop(Stack* stack) {
if (cstack_isEmpty(stack)) {
printf("栈为空,无法出栈\n");
return NULL;
} else {
return stack->data[stack->top--];
}
}
/**
* @brief 查看栈顶元素
*
* 从给定的栈中获取栈顶元素,并返回其地址。如果栈为空,则返回NULL。
*
* @param stack 栈指针
*
* @return 返回栈顶元素的地址,如果栈为空则返回NULL
*/
void* cstack_peek(Stack* stack) {
// 检查栈是否为空
if (cstack_isEmpty(stack)) {
// 如果栈为空,打印提示信息
printf("栈为空\n");
// 返回NULL
return NULL;
} else {
// 如果栈不为空,返回栈顶元素的地址
return stack->data[stack->top];
}
}
2 用链表的形式构建栈
01 创建结构体,栈大小初始化
// 定义链表节点
typedef struct Node {
void* data; // 存储任意类型的数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 定义栈结构
typedef struct {
Node* top; // 栈顶指针
} Stack;
// 初始化栈
void initStack(Stack* stack) {
stack->top = NULL;
}
02 内部必须实现的函数
// 判断栈是否为空
int isEmpty(Stack* stack) {
return stack->top == NULL;
}
// 释放栈的内存
void freeStack(Stack* stack) {
while (!isEmpty(stack)) {
pop(stack);
}
}
03 peek(查看栈顶元素) pop(出栈)push(入栈)
// 元素入栈
void push(Stack* stack, void* value) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
newNode->data = value;
newNode->next = stack->top;
stack->top = newNode;
}
// 元素出栈
void* pop(Stack* stack) {
if (isEmpty(stack)) {
printf("栈为空,无法出栈\n");
return NULL;
}
Node* topNode = stack->top;
void* value = topNode->data;
stack->top = topNode->next;
free(topNode);
return value;
}
// 获取栈顶元素但不出栈
void* peek(Stack* stack) {
if (isEmpty(stack)) {
printf("栈为空\n");
return NULL;
}
return stack->top->data;
}
总结
- 栈的数据结构定义: 定义清楚栈的数据结构,包括栈顶指针以及可能需要的其他信息。
- 栈的大小和扩容: 如果栈的大小是固定的,确保栈不会溢出。如果栈的大小不固定,考虑实现自动扩容的机制。
- 内存管理: 如果使用动态内存分配,确保在适当的时候释放内存,以防止内存泄漏。
- 栈的初始化: 在使用栈之前,确保进行了初始化。这通常包括将栈顶指针设置为适当的初始值。
- 数据类型的一般性: 如果希望栈能够处理不同类型的数据,考虑使用泛型机制或者采用一般性的数据表示方式。
- 栈的操作: 实现基本的栈操作,如入栈、出栈、获取栈顶元素等。确保这些操作能够正确处理边界条件,例如空栈的情况。
- 错误处理: 在可能出现错误的地方进行适当的错误处理,例如内存分配失败或尝试在空栈上执行出栈操作。
使用链表构建栈:
优点:
- 动态大小: 链表实现的栈可以动态分配内存,因此可以灵活地增加或减少栈的大小。
- 内存利用率: 不需要预先定义栈的大小,可以根据需要分配内存,减少内存的浪费。
- 插入和删除操作效率高: 在链表中进行插入和删除元素的操作效率较高,不涉及数据的移动。
缺点:
- 访问元素速度慢: 在链表中访问元素的效率较低,需要从头节点开始遍历直到找到目标元素。
- 额外的空间消耗: 每个节点需要存储数据和指向下一个节点的指针,可能会导致额外的空间消耗。
使用数组构建栈:
优点:
- 直接访问元素: 数组实现的栈可以通过索引直接访问元素,因此在访问元素方面效率较高。
- 内存消耗小: 不需要额外的指针存储空间,因此在内存使用方面较为紧凑。
- 适合小规模固定大小的栈: 对于知道栈大小的情况,使用数组可以更简单和高效。
缺点:
- 固定大小: 静态数组实现的栈在创建时需要指定大小,不能动态扩展,可能导致空间浪费或栈溢出。
- 插入和删除效率低: 在数组中插入和删除元素的操作可能涉及大量元素的移动,效率较低。
- 不灵活: 无法灵活地适应动态变化的栈大小需求,不适用于不确定大小的情况。
结论
- 如果栈的大小不确定,需要动态变化,或者对于频繁的插入和删除操作,链表实现的栈更为适用。
- 如果栈的大小是已知的且较小,对于直接访问元素的需求较高,数组实现的栈可能更合适。