两数之和
算法标签:数组、哈希表
给出一个数组和一个目标值,让我们在数组中找到两个不同的数,满足这两个数的和等于目标值,要求返回它们的下标,保证一定有解,不需要考虑无解的情况
如果有多组解,随便输出一组解即可
样例:在 [ 2,7,11,15 ] 中选两个数,使得它们的和是 9,选择 2 和 7 即可,注意返回的是这两个数的下标,因此返回的是 0 和 1
暴力枚举两个数,两重循环枚举下标 i、j,如果和 nums[ i ] + nums[ j ] 等于目标值 target 就返回,时间复杂度为 O(n^2)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target)
{
vector<int> res;
for (int i = 0; i < nums.size(); i++)
{
for (int j = 0; j < i; j++)
{
if (nums[i] + nums[j] == target)
{
res = vector<int>({j, i});
break;
}
}
if (res.size() > 0) break;
}
return res;
}
};
排序后双指针,但是排序的时间复杂度为 O(nlogn)
下面介绍时间复杂度为 O(n) 的做法:
每次枚举第二个数,在哈希表中找这个数前面有没有一个数,使得这个数加上前面那个数和等于 target
如果这个数是 Si,就相当于问这个数前面有没有某一个数的值是 target - Si,就是查询某一个数有没有存在,使用哈希表
哈希表可以用 O(1) 的时间找出来一个数是否存在
开一个哈希表记录前面所有数,从前往后扫描,每扫描一个数就把这个数放到哈希表里面,当我们扫描到 Si 的时候,相当于把 Si 前面所有的数都放到了哈希表中,在哈希表中查询前面是否有一个数的值为 target - si,如果存在的话把下标找出来
每个数最多往哈希表里面插一次,查询哈希表一次,哈希表时间复杂度为 O(1),因此整个算法的时间复杂度就是 O( n )
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//定义哈希表把每个数值映射到它的下标
unordered_map<int,int> heap;
//从前往后去遍历一下每个数
for(int i = 0;i < nums.size();i++) {
//要查询的数r
int r = target - nums[i];
//看查询的值是不是存在 如果存在r,说明已经找到两个数它们的和是target
//返回键对应的值也就是下标
if(heap.count(r)) return {heap[r],i};
//如果不存在r 就把当前这个数插到哈希表里面
heap[nums[i]] = i;
}
//保证一定有解,前面一定会return 这里是为了避免语法错误
return{};
}
};
两数相加
算法标签:递归、链表、数学
模拟加法,用一个链表来表示一个数,表示的时候是反着来表示的,先表示个位 8 ,再表示十位 0,最后表示百位 7
题目给出两个用链表表示的数,计算它们的和,并且这个和也要用链表来表示
上面一个数加上下面一个数,这两个数的位数不一定一样
这个链表是从右往左串的,先看两个链表的第一个数,两个链表的第一个数相加除以10 的余数(只保留个位)就是和的第一个数(个位)
进位用 t 来表示,可能是 0(如果小于 10),可能是 1(如果大于等于 10)
从下一位开始是第一个链表的值加上第二个链表的值,再加上进位,最终存储的是这三个数的和的个位(也就是除以 10 的余数),如果和大于等于 10 要往前进一位,否则不需要进位
从个位开始循环,循环的时候看一下:如果某一个链表没有循环完就要一直做,如果有进位,并且进位不是 0 也要一直做,直到每个链表都循环完了,并且进位为 0 为止
技巧:凡是需要特判第一个点的时候,都可以加一个虚拟头节点,这样就不需要特判第一个点(插入的点永远不可能是第一个点),虚拟头节点的值任意,这个点永远不会用到
如果没有定义虚拟头节点,当插入的点是第一个点的时候,需要特判,插入第一个点的时候,这个链表还不存在,需要把它创建出来,就要加 if 特判。定义虚拟头节点后,就不需要特判,因为插入的点永远不可能是第 1 个点
cur 存储和链表的尾节点,每次插入一个新节点需要在尾节点做插入,需要用一个变量存储尾节点是哪一个
/**
* 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* addTwoNumbers(ListNode* l1, ListNode* l2) {
//定义一个虚拟头节点 做插入的时候就不用判断当前点是不是第一个点 和链表的虚拟头节点
//cur 表示当前和链表的尾节点
auto dummy = new ListNode(-1),cur = dummy;
//进位
int t = 0;
//1.l1没有循环完 2.l2没有循环完 3.进位不为0
while(l1 || l2 || t) {
//l1没有循环完 把第一个数加上
if(l1) t += l1->val,l1 = l1->next;
//l2没有循环完 把第二个数加上
if(l2) t += l2->val,l2 = l2->next;
//和的这个位是 t 的个位数字
//每次插入一个新节点之后 需要把尾节点更新成新节点
cur = cur->next = new ListNode(t % 10);
//进位
t /= 10;
}
//返回虚拟头节点的next指针 也就是真正的链表头节点
return dummy->next;
}
};
无重复字符的最长子串
算法标签:哈希表、字符串、滑动窗口
给出一个字符串,要找到其中不含有重复字符的最长的一段
枚举样例发现,最长的不包含重复字符的子串长度是 3
双指针、滑动窗口,给出一个序列,考虑怎么把答案找出来(怎么能够把所有情况都枚举到,然后取 max)
一共有 2 / n^2 个不同的子串,在这 2 / n^2 个子串中,找不包含重复字符中的子串里面最长的一个
以这个子串的尾节点分类,可以分成 n 类,子串的最后一个字母在第 0 个位置是第一类,在第 1 个位置是第二类,依次类推,一共可以分成 n 类,每次处理一类即可
枚举所有以 i 为尾节点的所有子串,在它所有子串中找到最长的一个不包含重复字符的子串
相当于找一个最靠左的 j,使得从 j 到 i 不包含任何一个重复字符
用哈希表找 j 需要 O( n ) 的时间复杂度,需要枚举 j,暴力做法就是 O( n^2 ) 的时间复杂度
看数据有没有单调关系,有单调关系就利用单调关系把 O( n^2 ) 的复杂度变成 O(n)
如果 i 变成 i '(往后移动了一格), i ' 也会有一个最靠左的 j ',如果 i 往后移动到 i ',i ' 所对应的 j ' 应该也会往后移动 ( 应该在 j 的右边,或者在 j 的位置上 )
反证法:假设 i ' 对应的 j ' 在 j 的左边,也就是说从 i ' 到 j ' 之间是不包含任何一个重复字符,出现矛盾:从 j ' - i ' 之间不包含任何一个重复的字符,从 j ' - i 之间也不包含任何一个重复的字符,所以 j 就可以取到 j ' 的位置,说明 j 不是 i 最靠左的一个满足要求的 j
可以发现:当第二个尾端点往后移动的时候,这个尾端点所对应的一个最靠左的端点一定要么不动,要么往右走
每一次枚举完 i 之后,再枚举一个新的 i ' 的时候,可以从上一个 j 的位置继续往后枚举就可以了,每个指针都会从前往后走不会回头,每个指针最多只会走 n 次,(两个指针最多只会扫描 2n 次)整个算法的时间复杂度为 O(n)
每次 i 往后移动 1 格,就把下一个字母 i + 1 加到哈希表中去,然后看此时集合里面是否有重复元素,由于这里面只把 i + 1 加进去了,如果里面有重复元素,那么重复元素必然是 Si+1,然后就把 j 一直往后移动,直到移动到某一个 Si+1 为止,然后把 Si+1 剔除,保证 Si+1 只有 1个,最终满足要求
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//定义一个哈希表 统计j - i中每个字符出现的次数
unordered_map<char,int> heap;
//定义一个最大值
int res = 0;
//从前往后枚举 i 是后面的指针 j 是前面的指针
for(int i = 0,j = 0;i < s.size();i++) {
//每次把当前新的字符加到哈希表中去
heap[s[i]] ++;
//如果有重复 必然是 s[i] 重复 因为只有 s[i] 变多了
//当 s[i] 大于 1 的时候,每一次将 s[j] 删除并把 j 往后移动一位
while(heap[s[i]] > 1) heap[s[j++]]--;
//退出循环之后 当前从 j 到 i 就是以 i 结尾的不包含重复字符最长的一段
//用这一段更新答案
res = max(res,i - j + 1);
}
return res;
}
};
寻找两个正序数组的中位数😅
算法标签:数组、二分查找、分治、递归
给出两个有序数组,第一个数组的长度是 m,第二个数组的长度是 n,求这两个数组合并之后的中位数
中位数的定义:
如果是奇数个数,中位数是最中间那个数,如果是偶数个数,中位数是最中间两个数的平均数
暴力:把两个数组合并在一块,排序后输出,时间复杂度是 O(nlogn)
法一:递归😎,时间复杂度为 O(log(m + n))
两个有序数组,求当前这两个数组当中从小到大排列,第 k 个数是多少,让 k = ( n + m ) / 2 就可以求出中位数
原问题难以直接递归求解,所以我们先考虑这样一个问题:
如果该问题可以解决,那么第 ( n + m ) / 2 小的数就是我们要求的中位数
在两个有序数组中,第一个数是最小值,第二个数是次小值,求第 k 小数是多少?
先在这两个数组中找到从小到大排列第 k / 2 个元素,有 3 种情况
第一种情况 A[ k / 2 ] < B[ k / 2 ]:在 A 数组里面 小于 A[ k / 2 ] 的个数 有 k / 2 个,在 B 数组里面小于 A[ k / 2 ] 的个数应该小于 k / 2(由于 A[ k / 2 ] < B[ k / 2 ]),最终小于 A[ k / 2 ] 的个数一定是小于 k,说明 A[ k / 2 ] 一定是在前 k 个数的前面,不可能在第 k 个数的位置,所以这一部分的数一定不会是答案,可以把这一部分删掉(这样就可以删掉 k / 2 个元素了)
第二种情况:如果 A[ k / 2 ] > B[ k / 2 ],同理,最终小于 B[ k / 2 ] 的个数也是小于 k,说明 B[ k / 2 ] 一定是在前 k 个数的前面,不可能在第 k 个数的位置,所以这一部分的数一定不会是答案,可以把这一部分删掉(这样就可以删掉 k / 2 个元素了)
第三种情况:如果 A[ k / 2 ] == B[ k / 2 ],这两个值相同,说明 A[ k / 2 ] 或 B[ k / 2 ] 这个值恰好就是第 k 个元素,这两个值都可以作为第 k 个数,删除的时候,可以删除前面的也可以删除后面的
如果 A[ k / 2 ] == B[ k / 2 ],把 A 数组和 B 数组合并在一块去排序,由于这两个数组都是有序的,所以第一个数组里面 k / 2 - 1 个数都是小于等于 A[ k / 2 ],第二个数组里面前 k / 2 - 1 个数都是小于等于 B[ k / 2 ],A 数组 和 B 数组前面一共有 2 × ( k / 2 - 1 ) 个元素,也就是 k - 2 个元素,所以排行在第 k 位的元素一定是 B[ k / 2 ] 或者 A[ k / 2 ]
每次比较完这两个数之后,一定可以删掉 k / 2 个元素,每次删除 k / 2 个元素之后,相当于在红色区间中找到第 k - k / 2 个数,就变成了一个递归的子问题
思路
每次都可以去掉 k / 2 个元素,所以 k 每次会除以 2,当 k = 1 的时候,相当于找两个数组的最小值,比较两个数组的开头元素,取小的那个即可
k 从头减到 1,k 每次除以 2,所以这个做法最多只会递归 log k 次,k 的大小是 ( m + n ) / 2,时间复杂度就是 1 / 2 O(log(m + n)),也就是 O(log(m + n)),时间复杂度不考虑常数
注意
如果存在空数组,直接取任意一个数组的第 k 位元素返回
由于 k 分为奇数和偶数,所以在计算下标时要用 k / 2 和 k - k / 2 ( si 和 sj 是第 k / 2 个元素的下一个位置 )
如果有数组长度不足 k / 2,A[ k / 2 ] == B[ k / 2 ] 的做法就不成立了,需要把相等时的处理合并到 > 或 < 的做法中的任意一组
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
/*
计算中位数需要根据数组长度进行判断
1.如果数组长度是偶数,计算中间两个数字的平均值
2.如果数组长度时奇数,直接返回中间的数字
*/
//得到两个数组的元素总数
int tot = nums1.size() + nums2.size();
//偶数
if (tot % 2 == 0) {
//找中间两个数
//左侧找第 k / 2 个数
int left = find(nums1, 0, nums2, 0, tot / 2);
//右侧找第 k / 2 + 1 个数
int right = find(nums1, 0, nums2, 0, tot / 2 + 1);
//返回它们的平均值 / 2.0 保证返回的是浮点数
return (left + right) / 2.0;
} else {
//奇数
//直接返回中间的数字
return find(nums1, 0, nums2, 0, tot / 2 + 1);
}
}
//找到两数组中第 k 大的元素 第一个数组 开始查找的下标 第二个数组 开始查找的下标 找第 k 大的数
int find(vector<int>& nums1, int i, vector<int>& nums2, int j, int k) {
//需要保证 num1 的长度不超过 num2 的长度
//假定第一个数组比较短,如果第一个数组比较长就要做交换
if (nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);
//特判 如果 num1 没有元素,要找的第 k 个数就是 num2 的第 k 个元素,k 从 1 开始
if (nums1.size() == i) return nums2[j + k - 1];
//递归终止条件 k = 1 直接返回两数组第一个元素的最小值
if (k == 1) return min(nums1[i], nums2[j]);
//num1 对应的下标是si,num2 对应的下标是sj
//注意 num1 的长度可能不足 k / 2,可能会越界,需要特殊处理
int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;
//判断第 k / 2 个元素的后一个元素 si 的前一个元素
if (nums1[si - 1] > nums2[sj - 1])
//把 num2 的前 k / 2 个元素删掉,改变第二个数组的起始位置,第一个数组的起始位置没有变,k 要减去被删掉的元素个数
return find(nums1, i, nums2, sj, k - (sj - j));
else
//把 num1 的前 k / 2 个元素删掉
return find(nums1, si, nums2, j, k - (si - i));
}
};
法二:二分😭,时间复杂度为 O(log(min(m,n))
最长回文子串
算法标签:字符串、动态规划
给出一个字符串,要在这个字符串中找到最长的一个回文子串
马拉车算法,时间复杂度 O(n)
字符串哈希 + 二分,时间复杂度 O(nlogn)
什么是回文串?
按长度分为两种:长度是奇数的回文串:满足左右两边对称,长度是偶数的回文串:两两配对,分别相等
可以先枚举每个回文串的中心,中心确定之后,用两个指针同时从中心开始往两边走,直到走到两个字符不一样,或者某一个指针走出边界为止,这样就可以找到以这个点为中心的最长一个回文串
边走边对比两个指针指向的字符是不是一样就可以了,如果一样就继续走,如果不一样就停止
停下来后,从 [ L + 1,R - 1 ] 这一段就是以 i 为中心的最长的一个回文串,长度应该是 R - 1- ( L + 1 ) + 1
回文串有两种,第一种长度是奇数,第二种长度是偶数
长度是奇数,在初始化的时候,让 L 和 R 从 i 的左右两边一个位置开始走
长度是偶数,L 从 i 开始走,R 从 i + 1 开始走
枚举中心需要 O(n) 次操作,枚举左右两个指针也是 O(n),整个算法的时间复杂度就是 O(n^2)
class Solution {
public:
string longestPalindrome(string s) {
//表示最长的一个回文子串
string res;
//枚举中心点
for(int i = 0;i < s.size();i++) {
//枚举长度是奇数的情况
int l = i - 1,r = i + 1;
//两个指针分别往两边走 当两个指针都没有越界的时候,并且两个指针指向的数值相同
//左指针就往左走一格右指针就往右走一格
while(l >= 0 && r < s.size() && s[l] == s[r]) l --,r ++;
//停下来的时候 从[l + 1,r - 1]中间这一段就是最长的一个回文串
if(res.size() < r - l - 1) res = s.substr(l + 1,r - l - 1);
//枚举长度是偶数的情况
l = i,r = i + 1;
while(l >= 0 && r < s.size() && s[l] == s[r]) l --,r ++;
//停下来的时候 从[l + 1,r - 1]中间这一段就是最长的一个回文串
if(res.size() < r - l -1) res = s.substr(l + 1,r - l - 1);
}
return res;
}
};