Hot 100(11~20)
Hot 100(11~20)
11.有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
辅助栈
对于判断这种左右匹配,可以考虑用栈和哈希表来辅助。
思路就是遍历字符串中的每个字符,如果遍历到哈希表中的key(也就是右括号),并且如果此时的栈为空(栈中没有与之对应的左括号)或者栈顶不是对应的左括号,则是不匹配的返回false,匹配的话就弹出栈顶。遍历到左括号直接压入栈即可。注意特殊情况,匹配肯定是成对匹配,如果字符串长度不是偶数,则直接返回。
bool isValid(string s)
{
if (string.size() % 2 != 0) return false;
unordered_map<char, char> hash = {
{')', '('},
{']', '['},
{'}', '{'}
};
stack<char> stk;
for (auto ch : s)
{
if (hash.count(ch)) // count函数直接返回的是一个数值,如果存在那么返回1,反之0
{
if (stk.empty() || stk.top() != hash[ch]) return false;
stk.pop();
}
else stk.push(ch);
}
return stk.empty();
}
时间复杂度O(n),n是字符串长度,空间复杂度是O(n+6),用栈保存字符串空间复杂度为n,6则是哈希表
12.合并两个链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
合并为一个新的链表,使用一个指针逐个遍历比较,然后修改next。要新建一个头节点,方便返回。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
ListNode* head = new ListNode(-1);
ListNode* pre = head;
while (l1 != nullptr && l2 != nullptr)
{
if (l1->val > l2->val)
{
pre->next = l2;
l2 = l2->next;
}
else
{
pre->next = l1;
l1 = l1->next;
}
}
pre->next = l1 == nullptr ? l2 : l1;
return head->next;
}
时间复杂度O(n+m),n和m是两个链表的长度, 空间复杂度O(1)
13.括号生成
数字
n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
看到这个类似全排列的问题,可以用暴力解题,题解思路是列出所有可能的组合,然后再判断它是否有效。但是这种时间复杂度过高,一般不会考虑。大多数会使用深搜dfs来解题,第一位放什么,然后往下递归,构建二叉树。
dfs递归
关键点在于搜索序列时的当前位,是选择左括号还是右括号,并且需要生成有效的括号序列。
首先左括号数量不能大于n,当条件成立时添加左括号。其次当右边括号小于n并且左边括号大于右括号时,添加右括号。递归的结束条件是左右两遍括号数量都等于n。
参数传递是值传递,值传递的好处就是下一层的结果不会对上一层造成任何影响,因此我们回溯到上一层时,程序会自动帮我们擦除当前层的选择。
public:
vector<string> res; //记录答案
vector<string> generateParenthesis(int n) {
dfs(n , 0 , 0, "");
return res;
}
void dfs(int n ,int lc, int rc ,string str)
{
if( lc == n && rc == n) res.push_back(str); //递归边界
else
{
if(lc < n) dfs(n, lc + 1, rc, str + "("); //拼接左括号
if(rc < n && lc > rc) dfs(n, lc, rc + 1, str + ")"); //拼接右括号
}
}
时间复杂度是$O(C_{2n}^n)$。空间复杂度$O(n)$,递归2n层,每层常数空间使用,所以渐进复杂为O(n)
15.下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。必须 原地 修改,只允许使用额外常数空间。
这题的题意是按照字典序找出下一个排列。下一个排列比当前排列要大,并且变大的幅度需要最小。如果更改前面的数字,那么变大的幅度是最大的,所以进行修改后面的数字。
两次扫描
第一次从后往前寻找第一个逆序。第二次扫描找出比第一个它大的位。
void nextPermutation(vector<int>& nums)
{
int i = nums.size() - 2; // 为了找出后面的第一个逆序
while (i >= 0 && nums[i] >= nums[i + 1]) i --; // 寻找逆序
if (i >= 0)
{
int j = nums.size() - 1; // 寻找第一个比i大的数字
while (nums[i] >= nums[j]) j --;
swap(nums[i], nums[j]);
}
reverse(nums.begin() + i + 1, nums.end()); // 将i后面的数反转
}
时间复杂度是$O(N)$,N是给定序列长度,最多需要两次扫描,以及一次反转。空间复杂度$O(1)$
17.搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
二分查找
最简单的办法就是开一个哈希表记住值和下标,sort排序看看 nums[target] 是否等于 target ,如果不是则返回-1,是则从哈希表中返回value。但是本题需要用$O(logn)$的方法求解,于是可以用二分的方法。将旋转后的数组一分为二,一部分是有序的,另一部分是部分有序的,
int search(vector<int>& nums, int target)
{
int n = nums.size();
if (!n) return -1; // 特判数组为空或者只有一个数的情况
if (n == 1) return nums[0] == target ? 0 : -1;
int l = 0, r = n - 1;
while (l <= r)
{
int mid = l + r >> 1;
if (nums[mid] == target) return mid;
if (nums[0] <= nums[mid]) // 说明这部分有序
{ // 如果target在0到mid这个区间,更新右边界
if (nums[0] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
}
else
{
if (nums[mid] < target && target <= nums[n - 1]) l = mid + 1;
else r = mid - 1;
}
}
return -1; // 退出while循环说明没有该target
}
时间复杂度$O(log n)$,空间复杂度$O(1)$
18.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:
你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
最容易让人想到的解题方式就是直接遍历,如果没搜到target,直接返回{-1,-1},如果搜到了,则记下当前下标,直到遍历比target大的数,返回下标组。但这种解题方式是O(n)的。
这题类似于ACWing上的数的范围,因为给出的数组是有序的,所以可以使用二分查找来优化时间复杂度。
二分查找
vector<int> searchRange(vector<int>& nums, int target)
{
int n = nums.size();
if (!n) return {-1, -1};
int l = 0, r = n - 1;
int left = 0;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[l] != target) return {-1, -1};
else
{
left = l;
r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
return {left, r};
}
return {-1, -1};
}
时间复杂度为$O(logn)$,空间复杂度为$O(1)$
19.组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
对于这类寻找可行解的问题,都可以用搜索回溯的方法。从数组第0位开始,每次选择该位或不选择该位,向下搜索可形成一棵树。
递归函数dfs需要维护三个参数,一是目标值target,每次选择一位后target减去该位。二是当前的组合combine。三是下标dix,如果选择当前位,则combine + 1,idx不变(因为可以有重复),如果不选择该位,则idx + 1。
递归结束条件:下标等于candidates的size;
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
vector<vector<int>> ans; // combine里加起来为target则添加到ans里
vector<int> combine; // 当前组合
dfs(candidates, target, ans, combine, 0); // 下标从0开始
return ans;
}
// 给定的数组 目标和 答案数组 当前组合 下标
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)
{
if (idx == candidates.size()) return;
if (target == 0)
{
ans.push_back(combine);
return;
}
// 跳过当前位
dfs(candidates, target, ans, combine, idx + 1);
// 选择当前位
if (target - candidates[idx] >= 0)
{
combine.push_back(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.pop_back(); // 恢复现场
}
}
时间复杂度$O(S)$,取决于搜索树所有叶子节点的深度之和。空间复杂度$O(target)$最坏递归target层