LeetCode 每周算法 8(栈、堆)
栈算法:
class Solution {
public:
// 判断括号是否有效的函数
bool isValid(string s) {
int n = s.size(); // 获取字符串s的长度
// 如果字符串长度为奇数,则括号无法有效匹配,直接返回false
if (n % 2 == 1) {
return false;
}
// 创建一个哈希表(unordered_map),用于存储右括号到对应左括号的映射
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
// 使用栈来辅助判断括号的匹配情况
stack<char> stk;
// 遍历字符串s中的每个字符
for (char ch : s) {
// 如果当前字符是右括号(即在pairs的键中存在)
if (pairs.count(ch)) {
// 如果栈为空,或者栈顶元素不是当前右括号对应的左括号,则括号无效,返回false
if (stk.empty() || stk.top() != pairs[ch]) {
return false;
}
// 如果栈顶元素是当前右括号对应的左括号,则从栈中弹出该左括号
stk.pop();
}
// 如果当前字符是左括号,则直接压入栈中
else {
stk.push(ch);
}
}
// 遍历完所有字符后,如果栈为空,则说明所有括号都有效匹配,返回true
// 如果栈不为空,则说明有未匹配的左括号,返回false
return stk.empty();
}
};
class MinStack {
// 使用一个栈来存储元素,其中每个元素都是一个pair<int, int>
// pair的第一个元素是用户压入栈的值,第二个元素是当前栈中的最小值
stack<std::pair<int, int>> stk;
public:
// 构造函数,初始化一个空的栈
MinStack() {
}
// 将一个元素val压入栈中,并更新栈中的最小值
void push(int val) {
// 使用min函数比较当前val和栈顶的最小值,取两者中的较小者作为新的最小值
// 然后将这个值和val作为一个pair压入栈中
stk.push({val, std::min(val, getMin())});
}
// 弹出栈顶元素
void pop() {
stk.pop();
}
// 获取栈顶元素的值
int top() {
// 返回栈顶元素的第一个值,即用户压入栈的值
return stk.top().first;
}
// 获取栈中的最小元素
// 注意,这里使用了一个技巧:每次push时都更新栈顶元素为当前的最小值
// 因此,栈顶元素的第二个值总是当前栈中的最小值
int getMin() {
// 如果栈为空,则返回INT_MAX(表示没有最小值)
// 否则,返回栈顶元素的第二个值,即当前的最小值
return stk.empty() ? INT_MAX : stk.top().second;
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(val);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
class Solution {
public:
// 存储待解码的原始字符串
string src;
// 用于在解码过程中跟踪当前处理的字符位置
size_t ptr;
// 辅助函数,用于连续读取并转换数字字符为整数
// 返回从当前ptr位置开始读取的数字,并将ptr移动到数字之后的第一个非数字字符
int getDigits() {
int res = 0; // 用于存储读取到的数字
// 循环直到ptr指向的字符不是数字或达到src的末尾
while (ptr < src.size() && isdigit(src[ptr])) {
// 将当前数字乘以10并加上新读取的数字字符(减去'0'转换为整数)
res = res * 10 + src[ptr++] - '0';
}
return res;
}
// 辅助函数,用于递归地解码字符串
// 它会根据括号、数字和字母的组合来构建并返回解码后的字符串
string getString() {
// 如果ptr已经到达src的末尾或遇到']',说明当前没有更多的字符可以解码,返回空字符串
if (ptr == src.size() || src[ptr] == ']') {
return "";
}
char cur = src[ptr]; // 当前处理的字符
int repTime = 1; // 重复次数,默认为1(如果没有显式指定)
string res; // 用于存储解码后的字符串
// 如果当前字符是数字,说明后面跟着的是重复次数
if (isdigit(cur)) {
repTime = getDigits(); // 读取并设置重复次数
++ptr; // 移动ptr到'['之后
// 递归调用getString来解码括号内的字符串
string str = getString();
// 遇到']'表示括号内字符串结束,移动ptr到']'之后
++ptr;
// 根据重复次数构建结果字符串
while (repTime--) res += str;
}
// 如果当前字符是字母,直接添加到结果字符串中,并移动ptr
else if (isalpha(cur)) {
res = string(1, src[ptr++]); // 将当前字母添加到结果字符串
}
// 继续递归解码剩余部分,并将结果与当前解码结果拼接
return res + getString();
}
// 主要的解码函数,初始化src和ptr,并调用getString开始解码过程
string decodeString(string s) {
src = s;
ptr = 0;
return getString(); // 返回解码后的字符串
}
};
class Solution {
public:
// 函数接收一个整数向量作为输入,返回一个整数向量作为输出
// 每个输出元素表示输入向量中对应元素后面第一个更高温度的天数差
// 如果没有更高的温度,则输出为0
vector<int> dailyTemperatures(std::vector<int>& temperatures) {
// 初始化一个与输入向量相同大小的答案向量,并将所有元素初始化为0
// 这表示默认情况下,我们还没有找到任何更高的温度
vector<int> answer;
answer.resize(temperatures.size(), 0);
// 使用一个栈来存储温度索引,栈顶是最近加入且尚未找到更高温度的温度索引
stack<int> s;
// 遍历输入的温度向量
for (int i = 0; i < temperatures.size(); i++) {
// 当栈不为空且当前温度大于栈顶索引对应的温度时
// 说明我们找到了栈顶索引对应温度的下一个更高温度
while (!s.empty() && temperatures[i] > temperatures[s.top()]) {
// 计算天数差(当前索引减去栈顶索引)并存储在答案向量中
answer[s.top()] = i - s.top();
// 弹出栈顶索引,因为它已经找到了下一个更高温度
s.pop();
}
// 将当前索引压入栈中,以便后续查找更高温度
s.push(i);
}
// 返回填充好的答案向量
return answer;
}
};
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> s; // 使用一个栈来存储柱子的索引
int answer = 0; // 存储最大矩形面积
// 在heights数组的开始和结束各添加一个高度为0的柱子,这是为了方便处理边界情况
// 特别是当第一个柱子或最后一个柱子很高时,可以确保它们也能被正确处理
heights.insert(heights.begin(), 0);
heights.push_back(0);
// 栈的初始元素为第一个柱子的索引(即0,高度为0的柱子)
s.push(0);
// 遍历每个柱子的索引(从1开始,因为0是添加的辅助柱子)
for (int i = 1; i < heights.size(); i++) {
// 如果当前柱子的高度大于栈顶柱子的高度,直接入栈
if (heights[i] > heights[s.top()]) {
s.push(i);
}
// 如果当前柱子的高度等于栈顶柱子的高度,也入栈(这里其实可以优化为不入栈,因为等高的柱子不会影响面积计算)
else if (heights[i] == heights[s.top()]) {
s.push(i);
}
// 如果当前柱子的高度小于栈顶柱子的高度,说明找到了一个矩形的右边界
else {
while (heights[i] < heights[s.top()]) {
int mid = s.top(); // 栈顶柱子的索引,即当前矩形的“高”的柱子
s.pop(); // 弹出栈顶元素,因为我们已经找到了以它为高的矩形的右边界
// 计算矩形的宽度
// 如果栈为空,说明左边没有柱子,宽度为i;否则为i与栈顶柱子索引之差减1
int h = heights[mid]; // 矩形的高度
int w = i - s.top() - 1;
// 更新最大面积
answer = max(answer, h * w);
}
// 将当前柱子的索引入栈,作为新的可能矩形的高度的起点
s.push(i);
}
}
// 遍历结束后,返回最大面积
return answer;
}
};
class Solution {
public:
int trap(vector<int>& height) {
// 创建一个栈来存储索引,而不是高度值
stack<int> s;
int sum = 0; // 用于记录总共接到的雨水量
// 栈中至少需要有一个元素作为起始边界,这里选择第一个元素作为起始
s.push(0);
// 从第二个元素开始遍历高度数组
for (int i = 1; i < height.size(); i++) {
// 如果当前元素的高度小于栈顶元素对应的高度,说明当前位置可能是一个水坑的一部分
// 将当前索引压入栈中,以便后续可能形成水坑时计算宽度
if (height[i] < height[s.top()]) {
s.push(i);
}
// 如果当前元素的高度等于栈顶元素对应的高度,同样压入栈中
// 这里其实可以优化,因为等高的元素不会增加新的水坑容量
else if (height[i] == height[s.top()]) {
s.push(i);
}
// 如果当前元素的高度大于栈顶元素对应的高度,说明找到了一个水坑的右边界
// 开始处理栈中存储的左边界到当前右边界之间的所有可能的水坑
else {
while (!s.empty() && height[i] > height[s.top()]) {
int mid = s.top(); // 当前处理的水坑的底部索引
s.pop(); // 弹出栈顶元素,即当前水坑的底部索引
// 如果栈不为空,说明存在左边界
if (!s.empty()) {
// 计算水坑的高度:左右边界中较低的高度减去当前水坑底部的高度
int h = min(height[s.top()], height[i]) - height[mid];
// 计算水坑的宽度:右边界索引减去左边界索引再减一(因为左右边界本身不计入宽度)
int w = i - s.top() - 1;
// 将当前水坑的容量加到总容量上
sum += h * w;
}
}
// 将当前元素(右边界)压入栈中,作为下一个可能水坑的左边界
s.push(i);
}
}
// 返回总共接到的雨水量
return sum;
}
};
堆算法:
class Solution {
public:
// int quickselect(vector<int> &nums, int l, int r, int k) {
// if (l == r)
// return nums[k];
// int partition = nums[l], i = l, j = r;
// while (i < j) {
// while (nums[i] < partition) i++;
// while (nums[j] > partition) j--;
// if (i < j)
// swap(nums[i], nums[j]);
// }
// if (k <= j) return quickselect(nums, l, j, k);
// else return quickselect(nums, j + 1, r, k);
// }
// int findKthLargest(vector<int>& nums, int k) {
// int n = nums.size();
// return quickselect(nums, 0, n - 1, n - k);
// }
// 将以i为根的子树调整为最大堆
void maxHeapify(vector<int>& a, int i, int heapSize) {
int l = i * 2 + 1; // 左子节点的索引
int r = i * 2 + 2; // 右子节点的索引
int largest = i; // 假设当前根节点是最大的
// 如果左子节点存在且大于当前最大节点,则更新最大节点
if (l < heapSize && a[l] > a[largest]) {
largest = l;
}
// 如果右子节点存在且大于当前最大节点,则更新最大节点
if (r < heapSize && a[r] > a[largest]) {
largest = r;
}
// 如果最大节点不是根节点,则交换它们,并递归地调整受影响的子树
if (largest != i) {
swap(a[i], a[largest]);
maxHeapify(a, largest, heapSize);
}
}
// 从给定的无序数组构建最大堆
void buildMaxHeap(vector<int>& a, int heapSize) {
// 从最后一个非叶子节点开始向上构建最大堆
// 因为叶子节点不需要调整,且非叶子节点的索引为(heapSize/2)-1到0
for (int i = heapSize / 2 - 1; i >= 0; i--) {
maxHeapify(a, i, heapSize);
}
}
// 找到数组中第k大的元素
int findKthLargest(vector<int>& nums, int k) {
int heapSize = nums.size();
buildMaxHeap(nums, heapSize); // 将nums构建为最大堆
// 移除堆顶元素(即当前最大元素),直到堆中只剩下k个元素
// 每次移除堆顶元素后,重新调整堆
for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) {
swap(nums[0], nums[i]); // 将堆顶元素与堆尾元素交换
--heapSize; // 堆的大小减一
maxHeapify(nums, 0, heapSize); // 重新调整堆顶元素以满足最大堆的性质
}
// 此时,堆顶元素即为第k大的元素
return nums[0];
}
// 注意:在findKthLargest函数中,循环的条件是i >= nums.size() - k + 1
// 这意味着循环会执行k次,每次移除一个元素,直到堆中只剩下k个元素
// 由于最大堆的性质,堆顶元素始终是最大的,因此最后剩下的堆顶元素就是第k大的元素
};
class Solution {
public:
// 静态比较函数,用于比较两个pair的第二个元素(即频率)
// 如果m的频率大于n的频率,则返回true,使得频率高的元素在优先队列中有更高的优先级
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second;
}
// 函数用于找出nums中出现频率最高的k个元素
vector<int> topKFrequent(vector<int>& nums, int k) {
// 使用unordered_map记录每个元素的出现次数
unordered_map<int, int> occurrences;
for (auto& v: nums) {
occurrences[v]++;
}
// 定义一个最小堆(但通过自定义比较函数cmp实现最大堆的效果)
// 堆中存储的是pair<int, int>,其中first是元素值,second是元素的出现次数
// 使用decltype(&cmp)作为比较函数的类型,确保类型安全
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
// 遍历每个元素及其出现次数
for (auto& [num, count] : occurrences) {
// 如果堆的大小已经达到k,则比较堆顶元素(当前堆中最小的元素,即频率最低的)与当前元素的频率
if (q.size() == k) {
if (q.top().second < count) {
// 如果当前元素的频率大于堆顶元素的频率,则弹出堆顶元素,并将当前元素加入堆中
q.pop();
q.emplace(num, count);
}
// 如果当前元素的频率不大于堆顶元素的频率,则不做任何操作,继续遍历下一个元素
} else {
// 如果堆的大小还未达到k,则直接将当前元素加入堆中
q.emplace(num, count);
}
}
// 构建结果向量,从堆中依次取出元素(即频率最高的k个元素),并将它们逆序排列(因为是从堆中依次弹出的)
// 但由于题目要求只是返回这些元素,而不关心它们的具体顺序,所以这里不需要额外处理顺序问题
vector<int> result;
while (!q.empty()) {
result.emplace_back(q.top().first);
q.pop();
}
// 返回结果向量
return result;
}
};
class MedianFinder {
public:
// 使用最小堆来存储较大的一半元素(确保堆顶为较小的一半中的最大值)
priority_queue<int, vector<int>, greater<int>> queMax;
// 使用最大堆来存储较小的一半元素(确保堆顶为较大的一半中的最小值)
priority_queue<int, vector<int>, less<int>> queMin;
// 构造函数,初始化两个堆
MedianFinder() {}
// 添加一个数到中位数查找器中
void addNum(int num) {
// 如果queMin为空或者新加入的元素小于等于queMin的堆顶元素(即较小一半中的最大值)
if (queMin.empty() || num <= queMin.top()) {
queMin.push(num);
// 如果此时queMin的大小比queMax大2,说明queMin中多出了一个元素,需要将其移动到queMax中
if (queMax.size() + 1 < queMin.size()) {
queMax.push(queMin.top());
queMin.pop();
}
} else {
// 否则,新加入的元素大于queMin的堆顶元素,应该加入到queMax中
queMax.push(num);
// 如果此时queMax的大小比queMin大1,说明queMax中多出了一个元素,需要将其移动到queMin中
if (queMax.size() > queMin.size()) {
queMin.push(queMax.top());
queMax.pop();
}
}
// 通过这种方式,我们保持两个堆的大小平衡,或者queMin比queMax多一个元素(对于奇数个元素的情况)
}
// 查找当前的中位数
double findMedian() {
// 如果queMin中的元素比queMax多,说明元素总数是奇数,中位数就是queMin的堆顶元素
if (queMin.size() > queMax.size()) {
return queMin.top();
}
// 否则,元素总数是偶数,中位数是两个堆顶元素的平均值
return (queMin.top() + queMax.top()) / 2.0;
}
};