C的回归基础学习——数据结构(1)栈
前言
我好像忘了去补前面的内容了。。。。
这次终于来到数据结构部分。数据结构很重要,上手也只需要背点模板(有时甚至可以直接套用函数库),但是要深入研究数据结构的变形与应用真的费脑子,之前学这一模块只注意前者(所以我现在很菜),这次重学数据结构重点一定要放在后面啊。
那么,言归正传,回到这次的主题——栈
栈的实现
一句话说明栈的作用:先进后出(LIFO-last in first out)
打结构体我jio得是真的没有必要,因为栈的操作真的很简单(除非你想搞什么特别的定义),所以一般一个数组和一个栈顶指针(说是指针实际上一般是整型top)。另外,stack可以直接套用#include中的stack<数据类型>名称。
stack<int>st;
st.push(a);//入栈
st.pop(a);//将栈顶元素出栈
a = st.top();//返回栈顶元素
st.empty();//栈是空的就返回true,反之返回false
来点实战
注:习题来自leetcode
1.括号
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
输入: "()"
输出: true
输入: "()[]{}"
输出: true
输入: "(]"
输出: false
分析:
- 基础栈的应用,遇到前括号就入栈,遇到后括号就判断是否现在栈顶为匹配的的前括号,如果不是则说明括号串无效,如果是则将这个前括号退出。记得要判断最后栈内元素是否为空,为空才完全有效。
- 其次补充一点新东西:string。来自c++的字符串类型,而且是动态的。
详情请见此处
代码:
bool is(string s) {
int size = s.size();
int top = 0;
char *stack = (char*)malloc(s.size()+1);//此处要加1,因为我定义的top是指向数组中栈顶的后一位,接下来要放元素的位置
for(int i = 0; i < size ;i++)
{
if ((s[i] == '(') || (s[i] == '[') || (s[i] == '{')) {
stack[top++]= s[i];
} else {
if(top == 0) return false;
if (int(s[i] - stack[--top]) > 2 ||) return false;// 这里用了一个小技巧,所有同一类型的前后括号的ascll码相差不过2
}
}
if (top != 0) return false;//判断栈是否为空,因为会出现“{”这种情况
free(stack);
return true;
}//leetcode 20
2.逆波兰表达式
根据逆波兰表示法,求表达式的值。
有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: ((2 + 1) * 3) = 9
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: (4 + (13 / 5)) = 6
输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
输出: 22
解释:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
分析:
- 这个其实也还好想,遇到数字就入栈,遇到符号就出两个,进行运算,在把结果入栈,搞定!
- 其次这个地方补充一点新东西:vector(向量),其实你也可以理解为动态数组,用法与数组类似,详情请见此处
- 还有auto是c++的一种新的定义方式,它让编译器自己根据初始化判断定义的是什么类型(为什么不用int?因为vector的遍历使用的是迭代器,不是int),简直是天使一般的存在(私以为)
代码:
bool check(string s)
{
if (s == "+" || s == "-" || s == "*" || s == "/")
return true;
return false;
}
int evalRPN(vector<string>& tokens) {
int stack[10010]; //这个地方我一开始打得char,错了好几次,蠢
int top = 0,num1,num2;
for(auto i = tokens.begin(); i != tokens.end() ; i++)//
{
//可能需要考虑一下不规范的输入 ,比如除以0
if (!check(*i)) stack[top++] = stoi(*i);
else {
num1 = stack[--top];
num2 = stack[--top];
if (*i == "+") stack[top++] = num1 + num2;
else if (*i == "-") stack[top++] = num2 - num1;
else if (*i == "*") stack[top++] = num2 * num1;
else if (*i == "/") stack[top++] = num2 / num1;
}
}
return stack[0];
} //leetcode 150
3.重头戏——接雨水(单调栈)
给定n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水
分析:
- 没错,我说过数据结构最让人难受的是变形与运用,而如何变形只有方向却没有固定答案。这就来了一道,当然这个还是相对与其他的,还是没那么BT。
- 首先想到的肯定是最简单的暴力方法:对于每根柱子计算此处能存多少水,具体操作就是分别向左向右找到最高的柱子,然后用两者最低的柱高减去此处的柱高,时间复杂度是O(N2)
- 上面O(N2)的复杂度中遍历每一个柱子的复杂度肯定是没法优化,那么就可以优化向左向右寻找最高柱子的过程,可以开两个数组leftMax和rightMax,leftMax[i]表示从i到最左端最高的柱子。进行预处理就可以将复杂度优化到O(N)
- 所以我们会发现这道题的关键就是找最高的柱子。现在用栈的思路来看的话,如果我们遇到一个较低的柱子,什么事也不能做,因为可能有更低的柱子;如果出现了一个比之前高(或者等高)的柱子,那么前面的柱子就没有用了,且他可以和栈顶之后的柱子之间形成积水。那么就可得出,记当前遍历到第now个柱子,如果height[now] < stack.top(),则stack.push(now),如果height[now] >= stack.top(),则进行计算并维护栈。
- 计算与维护:维护就是把比now低的柱子踢出去,而计算则是每次踢柱子时实际上我们得到了三根柱子:踢出的柱子(就是栈顶的柱子)s1,now,还有栈顶后面的柱子s2,把前者看作底部,后两者看作挡板,则有width=now-s2-1,depth=min(height[now],height[s2])-height[s1],此次获得的水体积就是width*depth。其实时间复杂度还是O(N),空间也是O(N),但是常数也许会小一点。
- 最后我们看一看我们在每次维护后的stack有什么特点:栈顶元素最小,然后从顶到底依次递增,而这就是单调栈的一种情型。
代码(栈)
class Solution {
public:
int trap(vector<int>& height) {
stack<int>s;
int ans = 0,now = 0;
int distance,nowTop,depth;
while (now < height.size())
{
while (!s.empty() && height[now] >= height[s.top()])
{
nowTop=s.top();
s.pop();
if (s.empty()) break;
distance = now - s.top() - 1;
depth = min(height[s.top()] , height[now]) - height[nowTop];
ans += distance * depth;
}
s.push(now++);
}
return ans;
}
};//leetcode 42
//因为打代码之前只想出单调栈的思路,没有想到怎么实践,所以跑去看了眼题解,结果就忘不了了。。。。
后记
- 实际上,我对于如何用栈的理解在于,怎么样找到入栈的时机和东西以及出栈的时机和东西,只要找到这个,说白了也就是用栈进行比较难的模拟罢了(也许?)
- leetcode是真心用不惯,因为c++的STL用的少,一开始用这个很不熟练,但是学了一点后就觉得:c++真的会比c好用,特别是c++自带的东西。stack,vector,string包括我记得的queue,map都是特别方便的数据结构,关键一点是它们是动态的,c总是要声明大小或者手动动态,确实不太方便。