题源:LeetCode
LeetCode每日一题:
1725. 可以形成最大正方形的矩形数目
给你一个数组 rectangles ,其中 rectangles[i] = [li, wi] 表示第 i 个矩形的长度为 li 、宽度为 wi 。
如果存在 k 同时满足 k <= li 和 k <= wi ,就可以将第 i 个矩形切成边长为 k 的正方形。例如,矩形 [4,6] 可以切成边长最大为 4 的正方形。
设 maxLen 为可以从矩形数组 rectangles 切分得到的 最大正方形 的边长。
请你统计有多少个矩形能够切出边长为 maxLen 的正方形,并返回矩形 数目 。
示例 1:
输入:rectangles = [[5,8],[3,9],[5,12],[16,5]]
输出:3
解释:能从每个矩形中切出的最大正方形边长分别是 [5,3,5,5] 。
最大正方形的边长为 5 ,可以由 3 个矩形切分得到。
示例 2:
输入:rectangles = [[2,3],[3,7],[4,3],[3,7]]
输出:3
提示:
1 <= rectangles.length <= 1000
rectangles[i].length == 2
1 <= li, wi <= 109
li != wi
class Solution {
public:
int countGoodRectangles(vector<vector<int>>& rectangles) {
int res = 0, maxLen = 0;
for(auto & rectangle : rectangles){
int l = rectangle[0], w = rectangle[1];
int k = min(l, w);
if(k == maxLen){
res++;
}else if(k > maxLen){
res = 1;
maxLen = k;
}
}
return res;
}
};
143. 重排链表
给定一个单链表 L 的头节点 head ,单链表 L 表示为:
L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:输入:head = [1,2,3,4]
输出:[1,4,2,3]
示例2:
输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]
提示:
链表的长度范围为 [1, 5 * 104]
1 <= node.val <= 1000
方法一:线性表
因为链表不支持下标访问,所以我们无法随机访问链表中任意位置的元素。
因此比较容易想到的一个方法是,我们利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可。
class Solution {
public:
void reorderList(ListNode *head) {
if (head == nullptr) return;
//利用线性表存储该链表
vector<ListNode *> vec;
ListNode *node = head;
while (node != nullptr) {
vec.emplace_back(node);
node = node->next;
}
//利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可
int i = 0, j = vec.size() - 1;
while (i < j) {
vec[i]->next = vec[j];
i++;
if (i == j) break;
vec[j]->next = vec[i];
j--;
}
vec[i]->next = nullptr;
}
};
复杂度分析
时间复杂度:O(N),其中 N 是链表中的节点数。
空间复杂度:O(N),其中 N 是链表中的节点数。主要为线性表的开销。
方法二:寻找链表中点 + 链表逆序 + 合并链表
注意到目标链表即为将原链表的左半端和反转后的右半端合并后的结果。
这样我们的任务即可划分为三步:
-
找到原链表的中点(参考「876. 链表的中间结点」)。
- 我们可以使用快慢指针来 O(N)O(N)O(N) 地找到链表的中间节点。
-
将原链表的右半端反转(参考「206. 反转链表」)。
- 我们可以使用迭代法实现链表的反转。
-
将原链表的两端合并。
- 因为两链表长度相差不超过 1,因此直接合并即可。
/**
* 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* middleNode(ListNode* head){
ListNode* slow = head;
ListNode* fast = head;
while(fast->next != NULL && fast->next->next != NULL){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* reverseList(ListNode* head){
ListNode* prev = NULL;
ListNode* curr = head;
while(curr != NULL){
ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
void mergeList(ListNode* l1, ListNode* l2){
ListNode* l1_tmp;
ListNode* l2_tmp;
while(l1 != NULL && l2 != NULL){
l1_tmp = l1->next;
l2_tmp = l2->next;
l1->next = l2;
l1 = l1_tmp;
l2->next = l1;
l2 = l2_tmp;
}
}
void reorderList(ListNode* head) {
if(head == NULL) return;
ListNode* mid = middleNode(head);//寻找链表中点
ListNode* l1 = head;
ListNode* l2 = mid->next;
mid->next = NULL;
l2 = reverseList(l2);//将原链表的右半端反转
mergeList(l1, l2);//将原链表的两端合并
}
};
复杂度分析
- 时间复杂度:O(N),其中 N 是链表中的节点数。
- 空间复杂度:O(1)。
394. 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
示例 2:
输入:s = “3[a2[c]]”
输出:“accaccacc”
示例 3:
输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”
示例 4:
输入:s = “abc3[cd]xyz”
输出:“abccdcdcdxyz”
提示:
1 <= s.length <= 30
s 由小写英文字母、数字和方括号 ‘[]’ 组成
s 保证是一个 有效 的输入。
s 中所有整数的取值范围为 [1, 300]
方法一:栈操作
思路和算法
本题中可能出现括号嵌套的情况,比如 2[a2[bc]],这种情况下我们可以先转化成 2[abcbc],再转化成 abcbcabcbc。我们可以把字母、数字和括号看成是独立的 TOKEN,并用栈来维护这些 TOKEN。具体的做法是,遍历这个栈:
- 如果当前的字符为数位,解析出一个数字(连续的多个数位)并进栈
- 如果当前的字符为字母或者左括号,直接进栈
- 如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串,此时取出栈顶的数字(此时栈顶一定是数字),就是这个字符串应该出现的次数,我们根据这个次数和字符串构造出新的字符串并进栈
- 重复如上操作,最终将栈中的元素按照从栈底到栈顶的顺序拼接起来,就得到了答案。
- 注意:这里可以用不定长数组来模拟栈操作,方便从栈底向栈顶遍历
class Solution {
public:
string getDigits(string &s, size_t &ptr){
string ret = "";
while(isdigit(s[ptr])){
ret.push_back(s[ptr++]);
}
return ret;
}
string getString(vector<string> &v){
string ret;
for(const auto &s : v){
ret += s;
}
return ret;
}
string decodeString(string s) {
vector<string> stk;
size_t ptr = 0;
while(ptr < s.size()){
char cur = s[ptr];
if(isdigit(cur)){
//获得一个数字并进栈
string digits = getDigits(s, ptr);
stk.push_back(digits);
} else if (isalpha(cur) || cur == '['){
//获取一个字母并进栈
stk.push_back(string(1, s[ptr++]));
} else {
++ptr;
vector<string> sub;
//如果当前的字符为右括号,开始出栈,一直到左括号出栈,出栈序列反转后拼接成一个字符串
while(stk.back() != "["){
sub.push_back(stk.back());
stk.pop_back();
}
reverse(sub.begin(), sub.end());
//左括号出栈
stk.pop_back();
//此时栈顶为当前sub对应的字符串应该出现的次数
int repTime = stoi(stk.back());
stk.pop_back();
string t, o = getString(sub);
//构造字符串
while(repTime--) t+=o;
stk.push_back(t);
}
}
return getString(stk);
}
};
复杂度分析
时间复杂度:记解码后得出的字符串长度为 S,除了遍历一次原字符串 s,我们还需要将解码后的字符串中的每个字符都入栈,并最终拼接进答案中,故渐进时间复杂度为 O(S+∣s∣),即 O(S)。
空间复杂度:记解码后得出的字符串长度为 S,这里用栈维护 TOKEN,栈的总大小最终与 S 相同,故渐进空间复杂度为 O(S)。
方法二:递归
从左向右解析字符串:
如果当前位置为数字位,那么后面一定会包含一个用方括号表示的字符串,即属于这种情况:k[…]:
- 我们可以先解析出一个数字,然后解析到了左括号,递归向下解析后面的内容,遇到对应的右括号就返回,此时我们可以根据解析出的数字 x 解析出的括号里的字符串 s’ 构造出一个新的字符串 x×s‘;
- 我们把k[…]解析结束后,再次调用递归函数,解析右括号右边的内容。
如果当前位置是字母位,那么我们直接解析当前这个字母,然后递归向下解析这个字母后面的内容。
class Solution {
public:
string src;
size_t ptr;
int getDigits(){
int ret = 0;
while(ptr < src.size() && isdigit(src[ptr])){
ret = ret * 10 + src[ptr++] - '0';
}
return ret;
}
string getString(){
if(ptr == src.size() || src[ptr] == ']'){
return "";
}
char cur = src[ptr];
int repTime = 1;
string ret;
if(isdigit(cur)){
//解析Digits
repTime = getDigits();
//过滤左括号[
ptr++;
//解析String
string str = getString();//递归
//过滤右括号]
ptr++;
//构造字符串
while(repTime--) ret+=str;
} else if(isalpha(cur)){
//解析Char
ret = string(1, src[ptr++]);
}
return ret + getString();
}
string decodeString(string s){
src = s;
ptr = 0;
return getString();
}
};
复杂度分析
时间复杂度:记解码后得出的字符串长度为 S,除了遍历一次原字符串 s,我们还需要将解码后的字符串中的每个字符都拼接进答案中,故渐进时间复杂度为 O(S+∣s∣),即 O(S)。
空间复杂度:若不考虑答案所占用的空间,那么就只剩递归使用栈空间的大小,这里栈空间的使用和递归树的深度成正比,最坏情况下为 O(∣s∣),故渐进空间复杂度为 O(∣s∣)。