Hot 100(1~10)
Hot 100(1~10)
1.两数之和
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
暴力搜索
使用两层for
循环来枚举,第一层枚举每个数x
,第二层从x
的下标加1开始枚举,如果两数相加等于target
,那么返回两数下标即可。循环结束时说明没有答案,返回空值。
vector<int> twoSum(vector<int>& nums, int target)
{
int n = nums.size();
for (int i = 0; i < n; i ++)
for (int j = i + 1; j < n; j ++)
if (nums[i] + nums[j] == target) return {i, j};
return {};
}
时间复杂度为O(N2), 没有使用额外空间,所以空间复杂度是 O(1)
哈希表
在暴力搜索中,后面每次枚举都和之前的数匹配过,所以出现了重复枚举。
使用哈希表<int, int>
,第一个存储的是数组中每个的值,第二个存储每个值的下标。对于每一个x
,先查询表中是否存在对应的target - x
,存在直接返回,不存在则将x
插入哈希表中。
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map<int, int> hash;
for (int i = 0; i < nums.size(); i ++)
{
auto it = hash.find(target - nums[i]); // find函数返回一个迭代器,未找到则返回最后一个元素的迭代器
if (it != hash.end()) return {it->second, i};
hash[nums[i]] = i;
}
return {};
}
时间复杂度为O(N)因为只枚举了一遍数组,空间复杂度为O(N)最坏情况下存储所有数组。
2.两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
因为两个链表是逆序存储,所以第一位直接是个位,同时遍历两个链表,用一个变量carry
来存储进位,逐位计算它们的和sum = n1 + n2 + carry
。其中,答案链表处相应位置的数字为 sum % 10
,而新的进位值为 sum / 10
。当都遍历到链表末尾时,如果carry
还有值,则添加到末尾,比如链表9999,另一链表已到末尾为空,最后刚好进位1。
为什么创建两个空指针,tail
为了表示每个位置上的数,head
为了返回答案。
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
ListNode *head = nullptr, *tail = nullptr;
int carry = 0;
while (l1 || l2)
{
int n1 = l1 ? l1->val : 0;
int n2 = l2 ? l2->val : 0;
int sum = n1 + n2 + carry;
if (!head) head = tail = new ListNode(sum % 10);
else
{
tail->next = new ListNode(sum % 10);
tail = tail->next;
}
carry = sum / 10;
if (carry > 0) tail->next = new ListNode(carry);
}
return head;
}
时间复杂度O(max(m, n)),需要遍历完最长的那个链表,空间复杂度为O(1)
3.无重复最长子串
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串 的长度。
滑动窗口+集合
对于字符串,如果从每个位置依次开始枚举的话,会产生重复枚举。举个例子:abcded,从a开始枚举到e是不重复的,那么从b枚举到e也是不重复的,c也是...所以,使用滑动窗口来优化掉这个问题。对于判断重复字符的问题,可以用哈希集合这种数据结构来解决。
int lengthOfLongestSubstring(string s)
{
unordered_set<char> set;
int n = s.size();
int rk = -1, ans = 0;
for (int i = 0; i < n; i ++)
{
if (i != 0) set.erase(s[i - 1]); // 左指针i往右边移动时,在集合中移除这个字符
while (rk + 1 < n && !set.count(s[rk + 1]))
{ // 集合中没有右指针后一个字符,则右指针向右移动
set.insert(s[rk + 1]);
rk ++;
}
ans = max(ans, rk - i + 1);
}
return ans;
}
时间复杂度是O(N),双指针会遍历一遍字符串,空间复杂度是常数级,最多需要存放不重复的子串字符
5.最长回文串
给你一个字符串
s
,找到s
中最长的回文子串。回文串是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
动态规划
回文串有一个性质,去掉左右两边的字符依然还是回文串。所以用f(i, j)
来表示当前状态是否为回文串,状态转移方程:f(i, j) = f(i + 1, j - 1) ^ (s[i] == s[j])
,只有 s[i + 1 : j - 1]
是回文串,并且字符串的第 i
和 j
个字母相同时,s[i : j]
才会是回文串。对于边界条件,长度为1是回文串,长度为二且字母相同则是回文串。
string longestPalindrome(string s)
{
int n = s.size();
if (n < 2) return s;
int maxLen = 1, begin = 0;
vector<vector<int>> f(n, vector<int>(n));
for (int i = 0; i < n; i ++) f[i][i] = true; // 单个字符都是回文串,s的[1-1],[2-2],[3-3]
for (int L = 2; L <= n; L ++)
{
for (int i = 0; i < n; i ++) // 枚举左边界
{
int j = L + i - 1; // 右边界
if (j >= n) break;
if (s[i] != s[j]) f[i][j] = false;
else
{
if (j - i < 3) f[i][j] = true;
else f[i][j] = f[i + 1][j - 1];
}
if (f[i][j] && j - i + 1 > maxLen)
maxLen = j - i + 1;
begin = i;
}
}
return s.substr(begin, maxLen);
}
时间复杂度O(n2), 空间复杂度O(n2)
中心扩展算法
回文串两侧互为镜像
string longestPalindrome(string s)
{
if (s.size() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.size(); i ++)
{
int len1 = expandAroundCenter(s, i, i); // 以奇数为中心展开
int len2 = expandAroundCenter(s, i, i + 1); // 以偶数为中心展开
int len = max(len1, len2); // 选择最长的为
if (len > end - start)
{
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substr(start, end - start + 1);
}
int expandAroundCenter(string s, int left, int right)
{
int l = left, r = right;
while (l >= 0 && r < s.size() && s[l] == s[r])
l ++;
r --;
return r - l - 1;
}
时间复杂度O(n2),空间复杂度O(1)
7.盛水最多的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
双指针
目的就是找出与x轴构成最大的面积。那首先高度和长度尽可能大,那么最好固定一方的最大值,长度又需要左右两条边,所以可以使用双指针算法来遍历。长度是两个指针下标之差,高度是min(height[l], height[r])
,遍历时用ans
更新答案,如果height[l]
比height[r]
小,则l
往右移动,去寻找是否有更长的高度,反之则往左移动。
int maxArea(vector<int>& height)
{
int l = 0, r = height.size() - 1, ans = 0;
while (l < r)
{
ans = max(ans, min(height[l], height[r]) * (r - l));
if (height[l] < height[r]) l ++;
else r --;
}
return ans;
}
时间复杂度是O(N),双指针遍历了整个数组长度,空间复杂度是O(1)
8.三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
要找出三个元素相加等于0,可以想到用三重循环来暴力解题,但是不能有重复的元素,所以要对数组进行排序,以免出现重复枚举,并且第2个下标要大于第1个,第3个大于第2个。这还是三重循环O(N3)的做法,如果 a 和 b固定,那么 c 也是固定的,此时 a + b + c = 0,数组是排好序的,新的三元组只有可能 b‘ > b 并且 c' < c,所以可以在第一重循环的基础上,加上双指针算法。
排序+双指针
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> ans;
int n = nums.size();
sort(nums.begin(), nums.end());
// 枚举第一个元素a
for (int a = 0; i < n; i ++)
{
if (a > 0 && nums[a] == nums[a - 1]) continue; //如果和上一个数一样,则跳过
int c = n - 1; // 第三个数,右指针
int target = -nums[a]; // b + c = -a则是答案
// 枚举b
for (int b = a + 1; b < n; b ++)
{
if (b > a + 1 && nums[b] == nums[b - 1]) continue; //b一样的话跳过
while (b < c && nums[b] + nums[c] > target) c --;
if (b == c) break;
if (nums[c] + nums[b] == target) ans.push_back({nums[a], nums[b], nums[c]});
}
}
return ans;
}
时间复杂度O(N2),两重循环。空间复杂度O(logN)
9.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
深度搜索
题目要求返回所有组合,像这种全排列问题,可以用深搜去解题。
string tmp;
vector<string> res;
vector<string> phone_map = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector<string> letterCombinations(string digits)
{
if (digits.size() == 0) return res;
dfs(0, digits);
return res;
}
void dfs(int pos, string digits)
{
if (pos == digits.size()) // 当所有位都被填满时,tmp存的就是答案之一
{
res.push_back(tmp);
return;
}
int num = digits[pos] - '0'; // 计算出在phone_map上的索引
for (int i = 0; i < phone_map[num].size(); i ++)
{
tmp.push_back(phone_map[num][i]);
dfs(pos + 1, dights); // 递归获取填下一位
tmp.pop_back(); // 恢复现场
}
}
时间复杂度O(3m+4n) ,3m表示的是3个字母的个数,空间复杂度是O(m + n)
10.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。
对于链表,想要删除某个节点,必须遍历到那个节点,题目有需要返回头节点,于是可以用dummy哑节点。
两层遍历
很容易想到第一次遍历长度求出 len
,然后第二次遍历到 len
- n
,删除节点后返回dummy
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* dummy = new ListNode(0, head);
int len = 0;
while (head)
{
len ++;
head = head->next;
}
ListNode* cur = dummy;
for (int i = 0; i < len - n; i ++) cur = cur->next;
cur->next = cur->next->next;
return dummy->next;
}
时间复杂度O(L),L是链表长度,会遍历L+n次,但是都是常数级且L>n,所以O(L),空间复杂度是O(1)
快慢针
可以使用双指针的来优化掉一层遍历,快指针先走 n 个节点,然后慢针从头开始和快针一起向后,当快针走到链表末尾时,此时慢针恰好在倒数第n个节点前一个节点,删掉后返回哑节点即可。
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* dummy = new ListNode(0, head);
ListNode* first = head;
ListNode* second = dummy;
for (int i = 0; i < n; i ++) first = first->next;
while (first)
{
first = first->next;
second = second->next;
}
second->next = second->next->next;
return dummy->next;
}
时间复杂度是O(L),一次遍历链表长度,空间复杂度O(1)