本节是括号序列专题,我们将了解如何进行括号匹配,和括号匹配的合法条件等 …
(一)LeetCode20:有效的括号
这个题目很明显就是要利用“栈”来实现,先进后出,但如何将其转化为代码呢?
让我们来看看“栈”在程序中的具体应用吧:
左括号一直入栈,后面进入的一直更新地在栈顶位置,一直入栈一旦遇到一个右括号就开始匹配出栈,栈顶元素出栈后,它前面的一个左括号又成为了栈顶,又开始一个出栈匹配的过程。
当然,本题本身很简单,但它的延伸问题(括号序列问题)却非常的多,dfs,dp 。。。敬请期待
代码示例:
class Solution {
public:
bool isValid(string s) {
// 一个好消息,C++中有现成的栈可以用,不用我们自己去模拟
if(s.size() % 2 != 0) return false;
stack<char> stk; // 注意这里使用了char,指的是栈中元素的数据类型
for(auto& c : s) {
if(c == '(' || c == '[' || c == '{') stk.push(c); // 遇见左括号的过程,//还有这里不是push_back,而是push,不是尾插 啊啊啊
else {
// 假如栈中还有元素可以匹配,且匹配成功就出栈。还有为什么不能把stk.size()放在最外部去做判断呢?
// 其实下面就会讲到?因为可能存在栈中元素已经全部匹配完了,但还有右括号未匹配的情况。
if(stk.size() && c == ')' && '(' == stk.top()) stk.pop();
else if(stk.size() && c == ']' && '[' == stk.top()) stk.pop();
else if(stk.size() && c == '}' && '{' == stk.top()) stk.pop();
else return false; // 这是为了排除栈中没有元素了,但s中还有右括号没有匹配的情况
}
}
// 如果最后栈中元素为空,那么就是全部都匹配成功了
// if(!stk.size()) return true;
// else return false;
// 更简洁的方式
return stk.empty();
}
};
遇见左括号入栈,否则遇见右括号就是出栈进行匹配。当然匹配的方式不是 c == stk.top(),而是一个映射匹配,遇见 ‘)’ 就用 ‘(’ 和top()匹配!
如果遍历过程中右括号一个左括号也没匹配上,就直接return false;
最后的返回值是stk.empty() ,这代表左括号一定有人认领,而右括号可以找到左括号认领的情况在else匹配的时候已经筛选过了!
当然上面左右括号匹配的方式比较蠢,我们可以利用左右括号ASCii上的接近去做文章:
class Solution {
public:
bool isValid(string s) {
// 一个好消息,C++中有现成的栈可以用,不用我们自己去模拟
if(s.size() % 2 != 0) return false;
stack<char> stk;
for(auto& c : s) {
if(c == '(' || c == '[' || c == '{') stk.push(c);
else {
// 假如栈中还有元素可以匹配,且匹配成功就出栈
if(stk.size() && abs(stk.top() - c) <= 2) stk.pop(); // 这里利用ASC码上的一个小技巧,左括号与右括号之间的数值差别
else return false;
}
}
return stk.empty();
}
};
(二)LeetCode: 合并两个有序链表
这是一个经典的二路归并算法(归并排序的核心思想),只是转化为了链表的形式:两个指针,每次拿比较小的那一个来放到新的数组中,然后指针再向后移动一位
本题也是先建立一个虚拟头节点,之前说过凡是虚拟头节点会改变的,最好都建立一个虚拟头节点,这样就不用特判头节点了
代码示例:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
// 这里还要用一个虚拟尾节点,因为是尾插
auto dummy = new ListNode(-1), tail = dummy; // 我自己写的时候就忘记了,直接在头节点上操作,然后让头节点都改变了
while(l1 && l2) {
if(l1 -> val > l2 -> val) {
tail = tail -> next = l2;
l2 = l2 -> next;
} else {
tail = tail -> next = l1; // 把l1尾插后,还要更新新链表的尾结点
l1 = l1 -> next;
}
}
// 循环完之后,必有一个链表是非空的,就把非空的部分接到新链表的尾部
if(l1) tail -> next = l1; // 相对数组来说,链表很方便了,都不用遍历的
if(l2) tail -> next = l2;
return dummy -> next;
}
};
这里就是要记住一下:新链表的尾结点也是要更新的: tail = tail -> next = l ;
(三)LeetCode22:括号生成
本题又是一个括号序列的问题,经历了LeetCode20之后,我们现提出一个问题:
如何判断一个括号序列合不合法?
More importantly, 一个合法括号序列的充分必要条件是什么?(只有一个括号构成的)
①. 任意前缀中,左括号数量 >= 右括号数量。当前缀是整个序列时,左括号数量 == 右括号数量
②. 左右括号数量相等
当然,这里还有一个扩展:
n个左括号和n个右括号构成的所有的序列里面,有多少个合法的括号序列?
结论:
本题的考虑思路和lc17电话号码的字母组合很像(但填的只有左括号和右括号),是括号的组合,所以我们也可以考虑dfs算法:
现总共有2n位空格,有n个左括号和n个右括号,要将它们填充到这2n个空格当中去 ?
当然,现在我们填进去时不是和“电话号码”一样时无序的,填的时候还要保证其合法性(这是本题的难点)!!!
根据合法括号序列的充分必要条件,
①. 在填左括号的时候要满足:l_cnt < n;
②. 在填右括号的时候则要满足:r_cnt < n && l_cnt > r_cnt (为啥l_cnt > r_cnt不取等,要是取等,就说明已经已经没有左括号可以匹配,你现在填右括号就是犯错误!)
代码示例:
class Solution {
public:
vector<string> ans;
vector<string> generateParenthesis(int n) {
dfs(n, 0, 0, "");
return ans;
}
void dfs(int n, int lc, int rc, string seq) {
if(lc == n && rc == n) ans.push_back(seq);// 这只是一种情况的结束
else { // 由于栈是帮我保留了状态的,所以回溯的时候才可以完成子树的成功遍历
if(lc < n) dfs(n, lc + 1, rc, seq + '(');
if(rc < n && lc > rc) dfs(n, lc, rc + 1, seq + ')'); // 右括号必须要有人嫁才行,否则没左括号匹配,自己加上去就错了
// 本题最重要的就是了解到暴填的规则,并以此来控制树到底往哪里延伸!
// 这里是回溯的区域,当lc==n且rc==c时就执行完程序,结束搜索
}
}
};
【小结】时间复杂度的分析:
由于方案数为卡特兰数,然后要push_back 2n个括号,所以最终的时间复杂度为两者相乘 !