https://leetcode.cn/problems/min-stack/description/
一、题目描述
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
二、相关知识点了解
栈的基本概念及结构:
栈:一种特殊的线性表,其只允许在固定一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last in First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫出栈。出数据也在栈顶。
三、示例分析
输入: ["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]] 输出: [null,null,null,null,-3,null,0,-2] 解释: MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.
通过示例可知,每次往栈中压入一个数据,之后得出此时栈内的最小值。当栈内元素pop时,栈内元素的最小值也会有相应的变化,简单来说就是需要用getMin()函数获取到当前栈内元素的最小值。
四、解题思路&代码实现
今天的题目其实难度不大,只不过要完成的模块相对较多。首先来分析一下各个模块内都需要填写哪些内容。
// 定义一个名为 MinStack 的结构体,用于表示最小栈
// 这里的结构体成员用于实现最小栈的各种功能,目前结构体内容为空,后续会填充具体成员
typedef struct {
} MinStack;
// 创建一个最小栈的函数,返回一个指向 MinStack 结构体的指针
// 该函数负责初始化最小栈所需的资源
MinStack* minStackCreate() {
}
// 将一个整数 val 压入最小栈 obj 中的函数
// 该函数实现了最小栈的入栈操作,并且可能需要处理与最小值相关的逻辑
void minStackPush(MinStack* obj, int val) {
}
// 从最小栈 obj 中弹出栈顶元素的函数
// 该函数实现了最小栈的出栈操作,并且可能需要更新与最小值相关的信息
void minStackPop(MinStack* obj) {
}
// 获取最小栈 obj 栈顶元素的函数,返回栈顶元素的值
// 该函数用于获取当前位于最小栈顶部的元素
int minStackTop(MinStack* obj) {
}
// 获取最小栈 obj 中最小元素的函数,返回最小元素的值
// 该函数实现了快速获取最小栈中当前最小元素的功能
int minStackGetMin(MinStack* obj) {
}
// 释放最小栈 obj 所占用资源的函数
// 该函数负责释放创建最小栈时分配的内存等资源,避免内存泄漏
void minStackFree(MinStack* obj) {
}
大概框架有了了解,那么我们就可以开始填充内容了,这里我们可以联想一下喝水的步骤,首先喝水最重要的是什么?要有容器吧,没有容器怎么喝。本题也是一样,首先我们需要先创建一个堆栈出来,也就是在这里定义上我们所需要的变量(容器)。
// 定义一个新的类型别名 MinStack,它是一个结构体类型
// 该结构体用于表示一个栈,其中包含了存储栈数据的指针和栈的容量信息
typedef struct {
// 定义一个整型指针 data,用于指向存储栈中元素的内存区域
// 这个指针将用来动态分配内存以存储栈中的数据
int* data;
// 定义一个整型变量 capacity,用于表示这个栈的最大容量
// 它决定了这个栈最多能容纳多少个元素
int capacity;
} MinStack;
有了“容器”,那么下一步我们就要给“容器”进行初始化。
// 创建一个最小栈的函数,返回一个指向 MinStack 结构体的指针
MinStack* minStackCreate() {
// 使用 malloc 函数为 MinStack 结构体分配内存空间
// 并将分配的内存地址强制转换为 MinStack* 类型,然后赋值给指针 s
MinStack* s = (MinStack*)malloc(sizeof(MinStack));
// 将 s 所指向的 MinStack 结构体中的 data 指针初始化为 NULL
// 表示此时栈中还没有存储任何数据
s->data = NULL;
// 将 s 所指向的 MinStack 结构体中的 capacity 初始化为 0
// 表示此时栈的初始容量为 0
s->capacity = 0;
// 返回指向已初始化的 MinStack 结构体的指针 s
return s;
}
“容器”也有了,也进行了初始化。那么下一步就该“倒水”了吧,这里也就是把数据压入栈的操作。
// 向最小栈中压入一个元素的函数
// obj 是指向 MinStack 结构体的指针,代表要操作的最小栈
// val 是要压入栈中的整数值
void minStackPush(MinStack* obj, int val) {
// 使用 realloc 函数重新分配 obj->data 所指向的内存空间
// 新的内存大小为当前容量(obj->capacity)加 1 个 int 类型的大小
// 这是为了在栈中添加一个新的元素
obj->data = realloc(obj->data, sizeof(int) * (obj->capacity + 1));
// 将传入的 val 值存储到新分配内存的最后一个位置
// 这个位置就是当前栈的栈顶位置
obj->data[obj->capacity] = val;
// 将栈的容量增加 1,以反映栈中已经增加了一个元素
obj->capacity++;
}
那么现在就差最后一步,栈的基本功能就快实现完了,就是“喝水”(喝水就是取数据,这里pop和top类似,就一起说了)。
// 从最小栈中弹出栈顶元素的函数
// obj 是指向 MinStack 结构体的指针,代表要操作的最小栈
void minStackPop(MinStack* obj) {
// 将栈的容量减 1
// 这相当于模拟了弹出栈顶元素的操作,虽然并没有真正释放栈顶元素的内存
// 后续如果有新元素压入,会覆盖原来栈顶元素的位置
obj->capacity--;
}
// 获取最小栈栈顶元素的函数
// obj 是指向 MinStack 结构体的指针,代表要操作的最小栈
int minStackTop(MinStack* obj) {
// 返回栈顶元素的值
// 由于栈顶元素位于数组 data 的第 capacity - 1 个位置
// 所以通过 obj->data[obj->capacity - 1] 来获取栈顶元素
return obj->data[obj->capacity - 1];
}
然后就是最重要的一步,在上期和大家说了堆区开辟的空间需要手动释放,在这里有个专门的函数进行空间销毁。
// 释放最小栈所占用资源的函数
// obj 是指向 MinStack 结构体的指针,代表要释放资源的最小栈
void minStackFree(MinStack* obj) {
// 释放存储栈中数据的内存空间
// obj->data 是指向存储栈数据的指针,通过 free 函数释放该内存
free(obj->data);
// 这一步完成后,整个最小栈所占用的内存都被释放,避免内存泄漏
free(obj);
}
至此,整个栈的基本功能就已经实现完毕了,现在我们需要的是求出栈内的最小元素。这里首先咱们用第一种思路,暴力法。直接循环遍历,找到栈中最小的元素,return即可。
// 获取最小栈中最小元素的函数
// obj 是指向 MinStack 结构体的指针,代表要操作的最小栈
int minStackGetMin(MinStack* obj) {
// 检查栈是否为空
if (obj->capacity == 0) {
// 如果栈为空,返回一个特定的错误值,这里返回 -1 表示错误情况
return -1;
}
// 初始化最小值为栈中的第一个元素
int min = obj->data[0];
// 遍历栈中从第二个元素开始到最后一个元素
for (int i = 1; i < obj->capacity; i++) {
// 如果当前遍历到的元素小于当前记录的最小值
if (min > obj->data[i]) {
// 更新最小值为当前遍历到的元素
min = obj->data[i];
}
}
// 返回找到的最小元素
return min;
}
这种方法行倒是行,但是堆栈本身是并不提供遍历的,这样遍历的话会丧失堆栈本身后进先出的数据结构。而且在时间复杂度上这种方法为O(N),并不符合题目所给出的要求在常数时间内检索到最小元素。
所以这里,我们可以转变一下思路,我们可以利用空间换时间啊。
只需要在结构体内部再多定义一个变量,专门用来存放每次压入栈之后的数据中的最小值就OK了。整体代码如下:
// 1. int* data:用于存储栈中的数据
// 2. int* min:用于存储每个位置对应的最小元素
// 3. int capacity:表示栈的当前容量
typedef struct {
int* data;
int* min;
int capacity;
} MinStack;
// 创建一个最小栈的函数,返回一个指向 MinStack 结构体的指针
// 该函数负责初始化最小栈所需的资源
MinStack* minStackCreate() {
// 使用 malloc 函数为 MinStack 结构体分配内存空间
// 并将分配的内存地址强制转换为 MinStack* 类型,然后赋值给指针 s
MinStack* s = (MinStack*)malloc(sizeof(MinStack));
// 将 s 所指向的 MinStack 结构体中的 data 指针初始化为 NULL
// 表示此时栈中还没有存储任何数据
s->data = NULL;
// 将 s 所指向的 MinStack 结构体中的 min 指针初始化为 NULL
// 表示此时还没有记录任何最小元素
s->min = NULL;
// 将 s 所指向的 MinStack 结构体中的 capacity 初始化为 0
// 表示此时栈的初始容量为 0
s->capacity = 0;
// 返回指向已初始化的 MinStack 结构体的指针 s
return s;
}
// 将一个整数 val 压入最小栈 obj 中的函数
// 该函数实现了最小栈的入栈操作,并且在入栈过程中更新最小元素记录
void minStackPush(MinStack* obj, int val) {
// 使用 realloc 函数重新分配 obj->data 所指向的内存空间
// 新的内存大小为当前容量(obj->capacity)加 1 个 int 类型的大小
// 这是为了在栈中添加一个新的元素
obj->data = realloc(obj->data, sizeof(int) * (obj->capacity + 1));
// 使用 realloc 函数重新分配 obj->min 所指向的内存空间
// 新的内存大小同样为当前容量(obj->capacity)加 1 个 int 类型的大小
// 这是为了记录新元素加入后的最小元素信息
obj->min = realloc(obj->min, sizeof(int) * (obj->capacity + 1));
// 将传入的 val 值存储到新分配内存的最后一个位置
// 这个位置就是当前栈的栈顶位置
obj->data[obj->capacity] = val;
// 如果当前栈为空(obj->capacity == 0)或者新加入的元素 val 小于当前记录的最小元素
if (obj->capacity == 0 || obj->min[obj->capacity - 1] > val) {
// 则将新元素 val 记录为当前最小元素
obj->min[obj->capacity] = val;
}
// 否则,当前最小元素保持不变
else {
obj->min[obj->capacity] = obj->min[obj->capacity - 1];
}
// 将栈的容量增加 1,以反映栈中已经增加了一个元素
obj->capacity++;
}
// 从最小栈 obj 中弹出栈顶元素的函数
// 该函数实现了最小栈的出栈操作
void minStackPop(MinStack* obj) {
// 将栈的容量减 1
// 这相当于模拟了弹出栈顶元素的操作,虽然并没有真正释放栈顶元素的内存
// 后续如果有新元素压入,会覆盖原来栈顶元素的位置
obj->capacity--;
}
// 获取最小栈 obj 栈顶元素的函数,返回栈顶元素的值
int minStackTop(MinStack* obj) {
// 返回栈顶元素的值
// 由于栈顶元素位于数组 data 的第 capacity - 1 个位置
// 所以通过 obj->data[obj->capacity - 1] 来获取栈顶元素
return obj->data[obj->capacity - 1];
}
// 获取最小栈 obj 中最小元素的函数,返回最小元素的值
int minStackGetMin(MinStack* obj) {
// 返回当前记录的最小元素的值
// 由于最小元素记录在数组 min 的第 capacity - 1 个位置
// 所以通过 obj->min[obj->capacity - 1] 来获取最小元素
return obj->min[obj->capacity - 1];
}
// 释放最小栈 obj 所占用资源的函数
// 该函数负责释放创建最小栈时分配的内存等资源,避免内存泄漏
void minStackFree(MinStack* obj) {
// 释放存储栈中数据的内存空间
free(obj->data);
// 释放存储最小元素记录的内存空间
free(obj->min);
// 释放 MinStack 结构体本身所占用的内存空间
free(obj);
}
千万千万不要忘记了,这里咱们的min也是在堆区,一定要记得手动释放,避免造成内存泄露!!!
五、题目总结
本题旨在设计 MinStack,除常规入栈、出栈、查看栈顶元素操作外,重点要在常数时间检索最小元素。先回顾栈知识,依示例理解需求,暴力法遍历找最小元素虽可行却破坏栈结构且不满足时间要求,后用空间换时间策略,结构体新增指针记录最小元素,各操作函数按规则实现对应功能,最终达成设计要求,还让我们掌握算法优化与复杂度权衡。
本期就到这里,谢谢大家!!!荆轲刺秦!!!