LeetCode 1. 两数之和
/*
这道题保证一定有解,所以不需要特判数组为空的边界条件
1.暴力枚举,两重循环 O(n^2)
2.排序,双指针 O(nlogn)+O(n) 但是排序下标会改变 可以用pair存储数值和原下标
3.一次遍历 O(n):哈希表
每次枚举第二个数,查看当前枚举的元素的前面有没有对应的答案,要查询某个数是否存在,可以用哈希表,它可以在O(1)时间复杂度内找到某个数是否存在
这里哈希表用unordered_map,它的底层实现是哈希表,每步操作O(1)
map底层实现是平衡二叉树,每步操作O(logn)
*/
class Solution {
public:
vector twoSum(vector& nums, int target) {
unordered_map h;//首先定义一个哈希表,把数组中遍历到的每个数映射成对应的下标
for(int i=0;i
LeetCode 2. 两数相加
/**
* 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) {}
* };
*/
/*
(链表模拟人工加法) O(n)
这是道模拟题,所给链表存储数是低位在前,高位在后,模拟我们小时候列竖式做加法的过程:
从最低位至最高位,逐位相加,如果和大于等于10,则保留个位数字,同时向前一位进1.
如果最高位有进位,则需在最前面补1.
做有关链表的题目,有个常用技巧:添加一个虚拟头结点:ListNode *dummy = new ListNode(-1);,可以简化边界情况的判断。
时间复杂度:由于总共扫描一遍,所以时间复杂度是 O(n)
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
//为了方便,先定义一个虚拟头结点,这样就不用特判头结点
auto dummy = new ListNode(-1), cur = dummy;//cur表示当前这个和的尾节点
int t=0;//进位
while(l1||l2||t){
//当l1没有循环完,或者l2没有循环完,或者进位t不为0时就一直做
if(l1) t+=l1->val,l1=l1->next;
if(l2) t+=l2->val,l2=l2->next;
cur=cur->next=new ListNode(t%10);
t/=10;
}
return dummy->next;
}
};
LeetCode 3. 无重复字符的最长子串
/*
给定一个字符串,请找出最长的不包含重复字符的子串
请注意子串和子序列的区别:
子串一定是连续的一段
子序列不一定是连续的一段,但下标要求是递增的
双指针,滑动窗口算法
考虑如何枚举到所有情况,取一个max,所有子串可以根据尾节点分为n类
枚举以i为尾端点的所有子串,在其中找到以i为尾端点的最远的j使得[j,i]为无重复字符的最长子串
如果直接用哈希表,双重循环的话,时间复杂度为O(n^2)
双指针优化考虑有无单调性,在这道题中也就是尾端点i往后的话,对应的起始点j是否也会往后,
答案是肯定的,反证法可证明
用哈希表动态维护[j,i]这个区间每个字符出现的次数
*/
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//定义一个哈希表动态维护统计区间中每个字符出现的次数
unordered_map h;
int res=0;
for(int i=0,j=0;i1) h[s[j++]]--;//把j往后移动一位
res=max(res,i-j+1);//更新答案
}
return res;
}
};
LeetCode 4. 寻找两个正序数组的中位数
//时间空间复杂度都是O(n+m)
class Solution {
public:
double findMedianSortedArrays(vector& nums1, vector& nums2) {
//归并排序思路,创建合并数组nums,并指定长度为nums1和nums2长度之和
vector nums;
int size1 = nums1.size(), size2 = nums2.size();
int size = size1 + size2;
//nums.resize(size);这一步如果不加,往nums数组放元素只能push_back
//建立变量i1和i2分别表示遍历数组nums1和nums2的下标,i表示数组nums下标
int i1 = 0, i2 = 0, i = 0;
//循环直到i1和i2均遍历到数组末尾
while (i1 < size1 || i2 < size2) {
//nums1首元素大于nums2或i2已遍历到末尾
if (i2 >= size2 || (i1 < size1 && nums1[i1] <= nums2[i2])) {
//nums[i++] = nums1[i1++];
nums.push_back(nums1[i1++]);
}
//nums2首元素大于nums1或i1已遍历到末尾
else {
// nums[i++] = nums2[i2++];
nums.push_back(nums2[i2++]);
}
}
//根据长度奇偶情况决定返回单个数还是两个数的平均值
//注意题目要求所求中位数为double型,求值和返回值时得先转换为double型
//注意这里的-1是因为下标从0开始,比如size==4的话,是要求下标为1和2的平均值
return size % 2 == 0 ? (nums[size / 2 - 1] + nums[size / 2]) / 2.0 : (double)nums[size/2];
}
};
class Solution {
public:
double findMedianSortedArrays(vector& nums1, vector& nums2) {
/*
因为我们的最终目的是得到中位数即可,不必真正合并数组,只要得到目标元素即可,
因此可以改成用两个变量记录下求中位数所要的元素,时间复杂度任然线性,但相较于上个做法时间复杂度减半
*/
int size1 = nums1.size(), size2 = nums2.size();
int size = size1 + size2;
int val1,val2;
int i1 = 0, i2 = 0, i = 0;
while ((i1 < size1 || i2 < size2) && i <= size / 2) {
if (i2 >= size2 || (i1 < size1 && nums1[i1] <= nums2[i2])) {
if (i == size / 2 - 1) val2 = nums1[i1];
else if (i == size / 2) val1 = nums1[i1];
i++;
i1++;
}
else {
if (i == size / 2 - 1) val2 = nums2[i2];
else if (i == size / 2) val1 = nums2[i2];
i++;
i2++;
}
}
return size % 2 == 0 ? ((double)val1+ (double)val2) / 2 : (double)val1;
}
};
class Solution {
public:
double findMedianSortedArrays(vector& nums1, vector& nums2) {
/*
首先考虑:在两个有序数组中,找出第k小数,那么第 (n+m)/2小数就是我们要求的中位数
我们从nums1和nums2中各取前k/2个元素,如果nums1[k/2−1]>nums2[k/2−1],所以总体来看,两个数组中小于nums2[k/2−1]
的元素一定不足k个,所以我们可以把nums2中前k/2个数去掉,在剩下数中找第k/2小数,变成一个递归的子问题,每次要减少一半元素,最多递归logk次,所以时间复杂度是log(n+m)
*/
int tot = nums1.size() + nums2.size();
if (tot % 2 == 0) {//中位数要注意奇偶数,这里+1是因为要找第几个元素,size==4表示要找第2和第3个元素
int left = find(nums1, 0, nums2, 0, tot / 2);
int right = find(nums1, 0, nums2, 0, tot / 2 + 1);
return (left + right) / 2.0;//一定注意/2.0,不然会下取整
} else {
return find(nums1, 0, nums2, 0, tot / 2 + 1);
}
}
int find(vector& nums1, int i, vector& nums2, int j, int k) {
//为了方便,假定第一个数组要查询的部分较短
if (nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);
if (k == 1) {//第一个边界情况,求最小值,也注意较小数组要查询的部分可能为空
if (nums1.size() == i) return nums2[j];
else return min(nums1[i], nums2[j]);
}//第二个边界,较小数组为空
if (nums1.size() == i) return nums2[j + k - 1];
int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;
//这里si是从i开始第k/2个元素的下一个位置,所以比较时候要-1,sj一样的道理
if (nums1[si - 1] > nums2[sj - 1])
return find(nums1, i, nums2, sj, k - (sj - j));
else
return find(nums1, si, nums2, j, k - (si - i));
}
};
/*
(二分) O( log(min(n,m)) )
二分查找,为了方便讨论我们首先假设两个数组的长度分别为n,m,并且有n <= m,并且n + m是奇数,那么我们要找的数字其实就是两个数组合并后第k = (n + m + 1) / 2小的数字。我们尝试将两个数组划分成两个部分,A数组左侧有i个元素,B数组左侧有j个元素,并且i + j = k。
left_part | right_part
A[0], A[1], ..., A[i-1] | A[i], A[i+1], ..., A[n-1]
B[0], B[1], ..., B[j-1] | B[j], B[j+1], ..., B[m-1]
如果我们能够保证A[i - 1] < B[j] && B[j - 1] < A[i],那么说明left_part中的所有元素都小于right_part中的元素,并且第k小的元素就是max(A[i - 1],B[j - 1])。
如果A[i - 1] > B[j]说明i偏大,我们需要将i缩小尝试得到A[i - 1] < B[j]。
如果B[j - 1] > A[i]说明i偏小,我们需要将i放大尝试得到B[j - 1] < A[i]。
那么我们使用二分查找来找到左边放多少个i数字比较合适,初始搜索区间为[0:n],(可以一个元素都不放,也可以全放)如果左边放置i个元素,那么右边放置j = k - i个元素。
接下来我们考虑一些边界条件:
如果i = 0,相当于最小的k个数都在B中,这时整体第k小的元素就是B[k - 1]
如果j = 0,相当于最小的k个数都在A中,这时整体第k小的元素就是A[k - 1]
否则,最小的k个数,i个在A中,j个在B中,这时整体第k小的元素就是max(A[i - 1],B[j - 1])
上面我们的讨论,我们是基于n + m是奇数的,这时候我们只需要找到上述元素就好了,但是当n + m是偶数的时候,我们还需要找到right_part中最小的元素,这个值也就是min(A[i],B[j]),这时候仍然需要讨论一些边界情况:
如果i = n,那么A中没有比A[i - 1]还大的了,那么只能是B[j]
如果j = m,那么B中没有比B[j - 1]还大的了,那么只能是A[i]
否则,整体第k + 1小的元素就是min(A[i],A[j])
*/
class Solution {
public:
double findMedianSortedArrays(vector& nums1, vector& nums2) {
int n = nums1.size(),m = nums2.size();
if(n > m){
swap(nums1,nums2);
swap(n,m);
}
int l = 0,r = n,k = (n + m + 1) >> 1;
while(l <= r)
{
int i = (l + r) >> 1,j = k - i;
if(i < n && nums1[i] < nums2[j - 1] )
l = i + 1;
else if(i > 0 && nums1[i - 1] > nums2[j])
r = i - 1;
else
{
int max_left,min_right;
if(i == 0) max_left = nums2[k - 1];
else if(j == 0) max_left = nums1[k - 1];
else max_left = max(nums1[i - 1],nums2[j - 1]);
if((n + m) % 2 == 1) return max_left;
if(i == n) min_right = nums2[j];
else if(j == m) min_right = nums1[i];
else min_right = min(nums1[i],nums2[j]);
return (max_left + min_right)/2.0;
}
}
return 0.0;
}
};
LeetCode 5. 最长回文子串
/*
1.双指针 时间复杂度是O(n^2)
回文串,左右对称,根据回文串长度奇偶性分为两种
奇数:满足左右两边两两对称即可,中间元素无所谓
偶数:两两对称相等
先枚举每个回文串的中心点,当中心点确定之后,用两根指针同时从中心开始往两边走,直到两个字符不一样,或者某根指针走到边界为止,这样就找到了以当前点为中心的最长回文子串 此时回文串是[l+1,r-1] 对应长度是(r-1)-(l+1)+1
如果是长度为奇数的回文串,l和r初始化为l=i-1 r=i+1
如果是长度为偶数的回文串,l和r初始化为l=i r=i+1
首先枚举中心点,遍历一次,时间复杂度为O(n),中心点确定之后,枚举两根指针是O(n),总共时间复杂度是O(n^2)
另外两种方法,第一种字符串哈希+二分,时间复杂度较低,但比较难,第二种DP时间复杂度一样,但需要开额外数组,空间复杂度较高
*/
class Solution {
public:
string longestPalindrome(string s) {
string res;
for(int i=0;i=0&&r=0&&r
/*
2.区间DP 时间复杂度和空间复杂度都是O(n^2)
*/
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
bool dp[1001][1001];//dp[i][j]表示(i,j)是否为回文子串
string ans;
for(int len = 0; len < n; ++len){//len表示j与i相差几个元素
for(int i = 0; i+len < n; ++i){
int j = i + len;
dp[i][j] = s[i]==s[j];
if(len > 1)//len==0或1表示区间只有一个或两个元素,严格大于1表示区间至少有三个元素
dp[i][j] = dp[i+1][j-1] && dp[i][j];
if(dp[i][j] && len+1 > ans.size())
ans = s.substr(i,len+1);
}
}
return ans;
}
};
/*
左右两半边子串相等,左边子串正序哈希值和右边子串倒序哈希值相等
回文串两大类,长度分为奇数和偶数
先看奇数类,枚举中心点,二分求出当前中心点的最大半径
对原来字符串变形,使得所有回文串长度都是奇数,在每两个字母之间加上一个没出现的字符即可
*/
#include
#include
using namespace std;
typedef unsigned long long ULL;
const int N = 2000010, base = 131;
char str[N];
ULL hl[N], hr[N], p[N];//正序逆序所有字符串的哈希值
ULL get(ULL h[], int l, int r)
{//求子串哈希值
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
int T = 1;
while (scanf("%s", str + 1), strcmp(str + 1, "END"))
{
int n = strlen(str + 1);
for (int i = n * 2; i; i -= 2)
{
str[i] = str[i / 2];
str[i - 1] = 'a' + 26;
}
n *= 2;
p[0] = 1;
for (int i = 1, j = n; i <= n; i ++, j -- )
{//预处理正序,逆序所有前缀哈希值和次方数组
hl[i] = hl[i - 1] * base + str[i] ;
hr[i] = hr[i - 1] * base + str[j] ;
p[i] = p[i - 1] * base;
}
int res = 0, k=0;
//char str_res[N];
for (int i = 1; i <= n; i ++ )
{//枚举中点,二分半径
int l = 0, r = min(i - 1, n - i);
while (l < r)
{
int mid = l + r + 1 >> 1;
//下标,正序第i - mid到第i - 1个,倒数第 n - (i + mid) + 1到第n - (i + 1) + 1)个
if (get(hl, i - mid, i - 1) == get(hr, n - (i + mid) + 1, n - (i + 1) + 1)) l = mid;
else r= mid-1;
}
//这样求出来是包括*的长度 整个回文子串要么是*比字母多一个,要么是字母比*多一个
if (str[i - l] <= 'z') res = max(res, l + 1);//如果边界是字母,说明字母多一个
else res = max(res, l);
// if (str[i - l] <= 'z') {
// if (l+1>res) res=l+1,k=i;
// }
// else{
// if (l>res) res=l,k=i;
// }
}
// for(int j=k-res, m=0;j<=k+res;j++){
// if(str[j]<='z') str_res[m++]=str[j];
// }
//cout<
class Solution {
public:
/*
先进行正向和反向hash
假设中点在 mid,臂长为l
如果当前mid-l的哈希值不等于mid+l的哈希值
说明当前中点到两边的回文长度小于前面的 直接continue进行下一个
如果哈希值相等 则判断l+1之后的两字符是否相等,如果相等,说明l+1后还为回文串继续判断l+1直到不满足
储存更新的 mid和l
输出
*/
typedef unsigned long long ULL;
int t=0,P=131;
char a[3000];
ULL hl[3000],hr[3000],p[3000];
string longestPalindrome(string s) {
string res;
int ans=0,mid=0,l;
a[++t]='#';
for(int i=0;it||i-l<1) continue;//说明遍历到最后一小部分,不用继续判断了
//下标,正序第i - l到第i - 1个,倒数第 n - (i + mid) + 1到第n - (i + 1) + 1)个
if(hl[i-1]-hl[i-l-1]*p[l]!=hr[t-i]-hr[t-i-l]*p[l]) continue;
while(i+l+1<=t&&i-l-1>=1&&a[i+l+1]==a[i-l-1]) l++;
if(l>=ans)
{
ans=l;
mid=i;
}
}
for(int i=mid-ans;i<=mid+ans&&mid+ans<=t;i++)
{
if(a[i]!='#')
res+=a[i];
}
return res;
}
};
LeetCode 6. Z 字形变换
/*
找规律,第一行是以0(下标)开头的,公差是2n-2的等差数列;中间的行可以拆成两个等差数列,分为在竖线上的数和在斜线上的数,最后一行也是一个等差数列,所有等差数列的公差都是2n-2
竖线上的开头元素是0到n-1,斜线上的开头是2n-2-竖线上的开头
*/
class Solution {
public:
string convert(string s, int n) {
string res;//定义答案序列
if (n == 1) return s;//边界条件判断,因为如果不判断的话,在n==1的情况下,公差算出来为0,会死循环
for (int i = 0; i < n; i ++ ) {//枚举每一行
if (i == 0 || i == n - 1) {//第一行或最后一行只有一个等差数列
for (int j = i; j < s.size(); j += 2 * n - 2)
res += s[j];
}
else {
for (int j = i, k = 2 * n - 2 - i; j < s.size() || k < s.size(); j += 2 * n - 2, k += 2 * n - 2) {//两个等差数列,每一行两个等差数列的首项分别是i和2n-2-i
if (j < s.size()) res += s[j];
if (k < s.size()) res += s[k];
}
}
}
return res;
}
};
LeetCode 7. 整数反转
/*
1.转换成字符串,反转之后变成整数
2.数学做法
先看怎么把数的每一位抠出来,以正数为例, x%10得到x的最后一位,x/10去掉最后一位
注意数学中取模之后,得到的数大于等于0,c++中对正数取模得正数,负数取模得负数,这样之后,这个代码也可以适用于负数
*/
class Solution {
public:
int reverse(int x) {
//先用longlong来写,最后改成int
long long r=0;
while(x){
r=r*10+x%10;//不用单独考虑正负情况,因为cpp取模的性质
x/=10;
}
if(r>INT_MAX) return 0;
if(r
class Solution {
public:
int reverse(int x) {
int r=0;
while(x){//先试探一步,可能和三数之和这道题无关,但试探这个思想很像
if(r>0&&r>(INT_MAX-x%10)/10) return 0;
if(r<0&&r<(INT_MIN-x%10)/10) return 0;
r=r*10+x%10;
x/=10;
}
return r;
}
};
LeetCode 8. 字符串转换整数 (atoi)
class Solution {
public:
int myAtoi(string str) {
int k = 0;
while (k < str.size() && str[k] == ' ') k ++ ;//先把前面的空格删掉
if (k == str.size()) return 0;//如果整个字符串都是空格的话,返回0
int minus = 1;//对可能出现的正负符号处理
if (str[k] == '-') minus = -1, k ++ ;
else if (str[k] == '+') k ++ ;
//先用longlong来存,最后再改成int
// long long res=0;
// while(k='0'&&str[k]<='9'){//k指向数字字符
// res=res*10+str[k]-'0';//字符转换为数字
// k++;
// if(res>INT_MAX) break;//此时的res必然为正数,只是输出的时候考虑要不要加符号
// }
// res*=minus;
// if(res>INT_MAX) return INT_MAX;
// if(res= '0' && str[k] <= '9') {
int x = str[k] - '0';
//正数res * 10 + x>INT_MAX前者会溢出,转化为res > (INT_MAX - x) / 10
//负数-res * 10 - x 0 && res > (INT_MAX - x) / 10) return INT_MAX;
if (minus < 0 && -res < (INT_MIN + x) / 10) return INT_MIN;
//注意负数绝对值比正数绝对值多1,使用特判负无穷,注意这个只能单独判断,绝对不能在上面改成小于等于,因为这样的话像
//"-2147483647"也会输出-2147483648
if (-res * 10 - x == INT_MIN) return INT_MIN;
res = res * 10 + x;
k ++ ;
}
res *= minus;
return res;
}
};
LeetCode 9. 回文数
//数值方法,类似于LeetCode 7. 整数反转,而且这道题并没有long long限制
class Solution {
public:
bool isPalindrome(int x) {
if(x<0||x&&x%10==0) return false;//如果x本身是负数,或者说x非0但是它最后一位是0的话,直接return
int y=x;//先把所给整数存下来
long long res=0;
//从个位开始把x的每一位抠出来放前面
while(x){
res=res*10+x%10;
x/=10;
}
return y==res;
}
};
class Solution {
public:
bool isPalindrome(int x) {
//数值方法,有longlong限制,只能用int,可以只翻转后一半,再判断是否和前一半相等即可
if (x < 0 || x && x % 10 == 0) return false;
int s = 0;
while (s <= x)
{
s = s * 10 + x % 10;
if (s == x || s == x / 10) return true; // 分别处理整数长度是奇数或者偶数的情况
x /= 10;
}
return false;
}
};
class Solution {
public:
bool isPalindrome(int x) {
//字符串方法,转换成字符串
if (x < 0) return 0;
string s = to_string(x);
return s == string(s.rbegin(), s.rend());
}
};
LeetCode 10. 正则表达式匹配
/*
类似dp问题很多,比如给两个字符串,求最长公共子序列,能否匹配
正则表达式匹配,'.' 匹配任意单个字符,'*' 表示它前面的那一个字符可以出现任意多次(包括0次)
动态规划(序列模型):
状态表示 f[i,j]
集合:所有s[1-i]和p[1-j]的匹配方案
属性:bool 是否存在合法方案
状态计算
如果p[j]不是'*',先看s[i]和p[j]是否匹配,两种情况:s[i]==p[j]或者p[j]=='.',并且f[i-1,j-1]也匹配
如果p[j]是'*',枚举一下,这个'*'表示多少个字符,如果是0个字符f[i,j-2],1个字符f[i-1,j-2]&&s[i]匹配
2个字符f[i-2,j-2]&&s[i]匹配&&s[i-1]匹配...
f[i,j] =f[i,j-2] | f[i-1,j-2]&s[i] | f[i-2,j-2]&s[i]&s[i-1]...
f[i-1,j]=f[i-1,j-2] | f[i-2,j-2]&s[i-1] | f[i-3,j-2]s[i-1]&s[i-2]...
上面一个式子: f[i,j] =f[i,j-2] | f[i-1,j] & s[i]匹配 优化和完全背包很像
*/
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;//下标都从1开始,所以前面补上一个空格
vector> f(n + 1, vector(m + 1));//布尔数组
f[0][0] = true;//初始化
for (int i = 0; i <= n; i ++ )//它可以从0开始,因为没有字符的时候也可能匹配
for (int j = 1; j <= m; j ++ ) {
//如果j==0的话,因为f[0][0]已经初始化过了,其他i不为0的情况一定不匹配,所以j从0开始没有意义
//*和前面一个字符看作一个整体,如果遇到类似a*的a的话要跳过a
if (j + 1 <= m && p[j + 1] == '*') continue;
if ( p[j] != '*') {//如果i指向某个非空字符,并且p[j]!='*',i从1开始,否则i-1没有意义
f[i][j] = i &&f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
} else if (p[j] == '*') {
f[i][j] = f[i][j - 2] || i && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.');
}
}
return f[n][m];
}
};
LeetCode 11. 盛最多水的容器
/*
给定一个数组,数组中有n个非负整数,表示二维平面的n条直线的高度,找到其中的两条直线使得它们与 x 轴共同构成的容器可以容纳最多的水
(贪心 + 双指针) O(n)
储水容量取决于边界的间距以及两边的短板高度;
移动较高的边界,储水容量一定会减少,反之则未必,所以可以贪心的移动较短的边界,并不断更新答案;
利用反证法也可以严格证明这样操作的答案必然是最优解。
证明:假设左边指针先到达最优解的一边,要证明的是右边指针会一直向前移动到最优解的右边,反证法,假设某个时刻右边高度大于等于左边,右指针就不会往前移动,此时对应的面积一定大于最优解,矛盾
*/
class Solution {
public:
int maxArea(vector& height) {
if (height.empty()) return 0;
//res要取最大值,初始化一般为0或者负无穷,因为题目中均为非负整数,且求的是面积,初始化为0即可
int i = 0, j = height.size() - 1, res = 0;
while (i < j){
res = max(res, (j - i) * min(height[i], height[j]));//每次是先更新一下最大值,再移动
if (height[i] <= height[j]) i++;
else j--;
}
return res;
}
};
LeetCode 12. 整数转罗马数字
/*
基本字符 I V X L C D M
阿拉伯数字 1 5 10 50 100 500 1000
1.相同的数字连写,所表示的数等于这些数字相加得到的数,如:III=3;
2.小的数字在大的数字的右边,所表示的数等于这些数字相加得到的数,如:VIII=8, XII=12;
3.小的数字在大的数字的左边(限于 IV、IX、XL、XC、CD和CM),所表示的数等于大数减小数得到的数,如:IV=4, IX=9;
4.正常使用时,连写的数字重复不得超过三次;
将所有减法操作看做一个整体,当成一种新的单位。从大到小整理所有单位得到:
M CM D CD C XC L XL X IX V IV I
1000 900 500 400 100 90 50 40 10 9 5 4 1
此时我们可以将目标整数看成这些单位值的加和,且同一种单位不能使用超过3次。所以我们尽可能优先使用值较大的单位
时间复杂度分析:计算量与最终罗马数字的长度成正比,对于每一位阿拉伯数字,罗马数字最多用4个字母表示(比如VIII=8),所以罗马数字的长度和阿拉伯数字的长度是一个数量级的,而阿拉伯数字的长度是O(logn),因此时间复杂度是 O(logn),时间复杂度和位数成正比,一个整数的位数是O(logn)级别。
*/
class Solution {
public:
string intToRoman(int num) {
int values[] = {
1000,
900, 500, 400, 100,
90, 50, 40, 10,
9, 5, 4, 1
};
string reps[] = {
"M",
"CM", "D", "CD", "C",
"XC", "L", "XL", "X",
"IX", "V", "IV", "I",
};
string res;
for (int i = 0; i < 13; i ++ ) {
while (num >= values[i]) {
num -= values[i];
res += reps[i];
}
}
return res;
}
};
LeetCode 13. 罗马数字转整数
class Solution {
public:
int romanToInt(string s) {
unordered_map hash;
hash['I'] = 1, hash['V'] = 5;
hash['X'] = 10, hash['L'] = 50;
hash['C'] = 100, hash['D'] = 500;
hash['M'] = 1000;
int res = 0;
for (int i = 0; i < s.size(); i ++ ) {
if (i + 1 < s.size() && hash[s[i]] < hash[s[i + 1]])
res -= hash[s[i]];
else
res += hash[s[i]];
}
return res;
}
};
LeetCode 14. 最长公共前缀
class Solution {
public:
string longestCommonPrefix(vector& strs) {
/*
两重循环暴力搜索 O(nm)
*/
string res;
if (strs.empty()) return res;//边界条件,判断是否为空
for (int i = 0;i < strs[0].size(); i ++ ) {//枚举第i个字母是否完全一致
char c = strs[0][i];
for (auto& str: strs)
if (str.size() <= i || str[i] != c)
return res;
res += c;
}
return res;
}
};
class Solution {
public:
string longestCommonPrefix(vector& strs) {
//字符串数组为空则置res为空,否则置为第一个字符串
string res = strs.empty() ? "" : strs[0];
//遍历字符串数组
for (string s : strs)
{
/*
在字符串s中查找res并返回首字母的位置(find函数)
如果首地址不为零,每次令res-1以缩短公共前缀
比如说再flow中查找flower,没有找到,返回值为迭代器结尾(非0)
公共前缀会减掉最后一个字母,为flowe。继续循环直到为flow
如果是首字母不一样则公共前缀会清空
*/
while (s.find(res) != 0)
{
res = res.substr(0, res.length() - 1);
}
}
return res;
}
};
LeetCode 15. 三数之和
/*
这道题与后面的LeetCode 16. 最接近的三数之和 LeetCode 18. 四数之和是一类题
这种问题一般来说最优做法是双指针,次优是哈希表,这两者时间复杂度是一样的,但后者会多用一些空间
先不考虑是否重复的问题,首先双指针做法一定要有序,这是双指针的核心,所以要先将数组排序,先去枚举第一个数
为了避免重复,要求第一个指针指向的数最小,i> threeSum(vector& nums) {
vector> res;//首先定义一下答案
sort(nums.begin(),nums.end());//这道题要用双指针做法,所以先排个序
for(int i=0;i0&&nums[i]==nums[i-1]) continue;
for(int j=i+1,k=nums.size()-1;ji+1&&nums[j]==nums[j-1]) continue;//同理跳过
/*这里我们是要找到三数相加大于等于0的最小的数,所以在ij固定的情况下,每次试探一下nums[k]的前面一个数nums[k-1]是否满足,满足的话k就往前面进一步*/
while(j=0) k--;
if(nums[i]+nums[j]+nums[k]==0){
res.push_back({nums[i],nums[j],nums[k]});
}
}
}
return res;
}
};
LeetCode 16. 最接近的三数之和
/*
注意这道题是最接近,所以可能是小于target的最大值,也可能是大于target的最小值,所有左右两边情况都要考虑到,这道题保证答案只有一个,所以不用去判重,最坏情况是三重循环
优化和上一题一样,还是排序之后用双指针算法,先枚举i,i固定之后,枚举j和k,还是和刚才一样,对于每个j找到一个最小的k使得三者之和>=target,这样就可以枚举出来所有大于等于target的最小值
还要找另外一种情况,这里没必要特意找,只要能找出来大于等于target的最小的k,如果k可以-1的话,那加上nums[k-1]之后必然是小于target的最大值
*/
class Solution {
public:
int threeSumClosest(vector& nums, int target) {
/*因为任意三数之和都可能会是答案,而这些答案都与target有一个差值,最终是要找到差值最小的那个答案,所以最开始定义res的时候,用pair定义,后面那个数是可能的答案,前面那个是这个可能的答案与target的差值,放前面是因为min默认对pair前面那个数比较求最小,而且是要求最小值,所以初始化为INT_MAX*/
pair res(INT_MAX, INT_MAX);
sort(nums.begin(),nums.end());//双指针,先排序
for(int i=0;ij&&nums[i]+nums[j]+nums[k-1]>=target) k--;
/*跳出循环要么是k不能往前走了,要么是k再往前走一步之后,三数相加必然小于target,至于加上nums[k]之后是否一定>=target,这个是不确定的*/
//首先先求第一种情况,也就是大于等于target的最小的三数之和
int s=nums[i]+nums[j]+nums[k];
res=min(res, make_pair(abs(s-target),s));//注意这里为什么一定要加绝对值
if(k-1>j){
s=nums[i]+nums[j]+nums[k-1];
res=min(res, make_pair(target-s,s));//此时target一定大于s
}
}
}
return res.second;
}
};
LeetCode 17. 电话号码的字母组合
class Solution {
public:
/*
这道题是非常经典的dfs问题,dfs问题的话考虑清楚还是简单的,画一个递归搜索树即可,而且也不用回溯,遍历到某一条路径的终点的时候,直接把结果存下来就行,也不用干其他的事情
在递归的时候需要存下来方案或者说路径是什么,string path; 还需要存下来当前是第几位int u
这个做法时间复杂度和长度有关,最坏情况下每个数字有四种选择,总的就是4^n,外加push_back需要O(n)的时间复杂度
所有总的时间复杂度是n*4^n
*/
vector ans;
string strs[10] = {
"", "", "abc", "def",
"ghi", "jkl", "mno",
"pqrs", "tuv", "wxyz",
};//每个数字可能的情况
vector letterCombinations(string digits) {
if (digits.empty()) return ans;
dfs(digits, 0, "");//当前遍历第几位,最开始的路径
return ans;
}
void dfs(string& digits, int u, string path) {//path是dfs当前的路径
if (u == digits.size()) ans.push_back(path);//遍历到最后一位,在答案当中加入当前的方案
else {
for (auto c : strs[digits[u] - '0'])//u的下标从0开始,但strs里面的下标,数字2就是对于2
dfs(digits, u + 1, path + c);
}
}
};
LeetCode 18. 四数之和
//双指针
class Solution {
public:
vector> fourSum(vector& nums, int target) {
sort(nums.begin(), nums.end());
vector> res;
for (int i = 0; i < nums.size(); i ++ ) {
if (i && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < nums.size(); j ++ ) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
for (int k = j + 1, u = nums.size() - 1; k < u; k ++ ) {
if (k > j + 1 && nums[k] == nums[k - 1]) continue;
while (u - 1 > k && nums[i] + nums[j] + nums[k] + nums[u - 1] >= target) u -- ;
if (nums[i] + nums[j] + nums[k] + nums[u] == target) {
res.push_back({nums[i], nums[j], nums[k], nums[u]});
}
}
}
}
return res;
}
};
/*
由最开始的二数之和,到三数之和,到现在的四数之和,虽然都可以用双指针,但如果扩张到n数之和呢,可以尝试dfs,又因为要不能有重复元素,所以要么哈希表,要么排序,这里我就直接sort排个序
深搜的搜索顺序,依次探索每个位置可以放哪些元素,如果放这个元素的话,下一个位置怎么放合适
另外这道题深搜必须加上剪枝,否则一定会超时
1.如果数组中剩余可选的数字数量少于待找数量n,则剪掉
2.如果 当前数字 + 已确定数字的和 + (n - 1) * 排序后数组中当前数字的下一个数字 > target,则说明后面的数无论怎么选,加起来都一定大于target,所以剪掉(递归返回)
3.如果 当前数字 + 已确定数字的和 + (n - 1) * 排序后数组最后一个数字 < target,则说明后面的数无论怎么选,加起来都一定小于target,所以剪掉(进行下一次循环)
*/
class Solution
{
private:
vector> ans;
vector myNums, subans;
int tar, numSize;//这四个变量除了subans之外,其他定义成全局变量是为了dfs时也可以使用,否则只能在dfs时传入这些参数
void DFS(int low, int sum)//前者是探索第low个位置可以放哪些数(从0开始),sum是已经选了的数的和
{
if (sum == tar && subans.size() == 4) {
ans.push_back(subans);
return;
}
for (int i = low; i < numSize; ++i) {
if (i > low && myNums[i] == myNums[i - 1]) //去重
continue;
if (numSize - i < int(4 - subans.size())) //剪枝
return;
if (i < numSize - 1 && sum + myNums[i] + int(3 - subans.size()) * myNums[i + 1] > tar) //剪枝
return;
if (i < numSize - 1 && sum + myNums[i] + int(3 - subans.size()) * myNums[numSize - 1] < tar) //剪枝
continue;
subans.push_back(myNums[i]);
DFS(i + 1, myNums[i] + sum);
subans.pop_back();
}
return;
}
public:
vector> fourSum(vector& nums, int target)
{
sort(nums.begin(), nums.end());
myNums = nums;
tar = target;
numSize = nums.size();
// if (numSize < 4)
// return ans;
DFS(0, 0);
return ans;
}
};
LeetCode 19. 删除链表的倒数第N个节点
/**
* 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* removeNthFromEnd(ListNode* head, int k) {
//两次循环
auto dummy = new ListNode(-1);//常用技巧,凡是头结点可能改变,建一个虚拟头结点
dummy->next = head;
int n = 0;//一次遍历找到链表总长度
for (auto p = dummy; p; p = p->next) n ++ ;
auto p = dummy;
//遍历到倒数第k个点,从dummy开始,走1步,到达倒数第n-1个点,走n-k-1步,到达倒数第k+1个点
for (int i = 0; i < n - k - 1; i ++ ) p = p->next;
p->next = p->next->next;
return dummy->next;
}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int k) {
//一次循环
auto dummy = new ListNode(-1); // 首先新建一个链表的结点
dummy->next = head; //令这个结点指向head
auto first = dummy, second = dummy; //同时设置双指针指向该节点
for (int i = 0; i < k; i ++ )
//将first指针向前走k步,second 指针不动,还是在最前面,此时first和second这个节点相差k
{
first = first->next;
}
while (first -> next)
{
//始终保持两个指针之间间隔n个结点,在first到达终点时,second的下一个结点就是从结尾数第n个结点
first = first->next;
second = second->next;
}
//直接删掉这个倒数第n个结点
second->next = second->next->next;
return dummy->next;
}
};
LeetCode 20. 有效的括号
class Solution {
public:
bool isValid(string s) {
/*
非常经典的括号序列问题,这道题是很简单的栈,但括号序列会有很多衍生的问题,可以考dfs,也可以考dp,也可以考数据结构,也可以什么卡特函数相关
*/
stack stk;
for (auto c : s) {
if (c == '(' || c == '[' || c == '{') stk.push(c);
else {
if (stk.size() && abs(stk.top() - c) <= 2) stk.pop();
else return false;
}
}
return stk.empty();
}
};
LeetCode 21. 合并两个有序链表
/**
* 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;
//凡是需要特判头结点的做法,建立一个头结点dummy node,每次都是在最后加节点,定义尾部tail
while (l1 && l2) {
if (l1->val < l2->val) {
tail = tail->next = l1;
l1 = l1->next;
} else {
tail = tail->next = l2;
l2 = l2->next;
}
}
if (l1) tail->next = l1;
if (l2) tail->next = l2;
return dummy->next;
}
};
LeetCode 22. 括号生成
class Solution {
public:
vector ans;//记录答案
/*
(暴力枚举,合法性判断) O(n*2^(2n))
生成所有的括号序列,然后用栈逐个检查合法性
*/
/*
这道题也是一个括号序列问题,给n对小括号,输出所有合法的括号序列方案,合法只要保证每一个左括号都有一个右括号对应即可
括号序列问题的推论,n对小括号什么情况下是合法的:
1.任意前缀中,左括号数量大于等于右括号数量
2.左右括号数量相等,这道题中这个条件一定满足
如果只是求数量的话有一个公式可以直接求,卡特兰数
每个位置左括号只有有一定可以填,右括号除了有之外,还需要满足之前的左括号数量严格大于右括号数量
*/
vector generateParenthesis(int n) {
dfs(n, 0, 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 + ')');//严格大于才能保证放一个右括号之后大于等于
}
}
};
LeetCode 23. 合并K个排序链表
/**
* 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:
/*
这道题比较直观的想法就是和合并两个有序链表类似,每次把k个序列最小的拿出来放到新序列的最后
找到k个中的最小值可用用堆,找到最小值时间复杂度是O(1),找到之后把最小值指针后移,再插入堆中,时间复杂度是
O(logn) 所以总的时间复杂度是O(nlogk)
这里有一个注意的地方,就是要给优先队列传入一个自定义的比较函数
*/
struct Cmp {
bool operator() (ListNode* a, ListNode* b) {//重载一下括号
return a->val > b->val;//优先队列是大根堆,会把大的放前面,但我们要把小的放前面
//所以翻转一下,写成大于号
}
};
ListNode* mergeKLists(vector& lists) {
//传入自定义比较函数,不然会按照地址比较,但是我们要按照值来比较,所以要加额外两个参数
//优先队列,容器的比较函数要传的是一个结构体
priority_queue, Cmp> heap;
auto dummy = new ListNode(-1), tail = dummy;
//先把所有非空的头结点插入堆中
for (auto l : lists) if (l) heap.push(l);
while (heap.size()) {
auto t = heap.top();
heap.pop();
tail = tail->next = t;
if (t->next) heap.push(t->next);
}
return dummy->next;
}
};
/*
(二分治合并) O(nlogk)
类似于归并排序
将所有待合并的有序单向链表进行递归分治处理,即将当前链表的序列分成两部分,每部分递归进行合并,然后将当前左右两部分合并的结果再进行 二合并 即可。
递归每层的时间复杂度是整个结点个数O(n),由于每次是二分,所有总共有O(logk) 层。
故总时间复杂度为 O(nlogk)
*/
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeKLists(vector& lists) {
int n = lists.size();
if(n == 0) return NULL;
if(n==1) return lists[0];
return merge(lists,0,n - 1);
}
ListNode* merge(vector& lists,int begin,int end)
{
if(begin == end)
return lists[begin];
else{
int mid = (begin + end) /2;
ListNode* l1 = merge(lists,begin,mid);
ListNode* l2 = merge(lists,mid+1,end);
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while(l1 && l2)
{
if(l1->val < l2->val)
{
cur->next = l1;
l1 = l1->next;
}else{
cur->next =l2;
l2 = l2->next;
}
cur = cur->next;
}
cur->next = (l1)?l1:l2;
return dummy->next;
}
}
};
LeetCode 24. 两两交换链表中的节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
/*
注意是交换节点,而不是值,关于链表题目,首先会发现链表头结点会改变,所以建一个虚拟头结点
*/
ListNode* swapPairs(ListNode* head) {
auto dummy = new ListNode(-1);
dummy->next = head;//p指向要交换的两个节点的前面一个点
for (auto p = dummy; p->next && p->next->next;) {
auto a = p->next, b = a->next;
p->next = b;
a->next = b->next;
b->next = a;
p = a;
}
return dummy->next;
}
};
LeetCode 25. K 个一组翻转链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
/*
上一道题是交换相邻两个元素,这道题是翻转相邻k个元素,头结点会改变,创建虚拟头结点,这道题和上一道题类似,弄一根指针,每次指向需要交换的k个元素的前一个元素
首先判断是否足够k
如果够k的话,翻转:
其中先将k个节点内部的边反向
然后是将前面的边搞定
最后是后面的边
*/
ListNode* reverseKGroup(ListNode* head, int k) {
auto dummy = new ListNode(-1);//定义虚拟头结点
dummy->next = head;
for (auto p = dummy;;) {//从虚拟头结点开始遍历整个链表,p指向需要交换的k个元素的前一个元素
auto q = p;//先遍历p后面够不够k个元素
for (int i = 0; i < k && q; i ++ ) q = q->next;//往后走k步看q是否为空
if (!q) break;//不足k个,这个for循环是常数次
auto a = p->next, b = a->next;//指向每一组的前两个元素
for (int i = 0; i < k - 1; i ++ ) {//要交换k-1个相邻的位置,要走k-1步
auto c = b->next;//先存下来b的下一个元素
b->next = a;
a = b, b = c;
}
auto c = p->next;//先存下来p的下一个元素
p->next = a, c->next = b;
p = c;//结束之后p继续指向下一组k个元素的前一个位置
}
return dummy->next;
}
};
LeetCode 26. 删除排序数组中的重复项
class Solution {
public:
int removeDuplicates(vector& nums) {
/*
原地算法就是不能开数组,不能写递归
双指针算法
*/
int k = 0;
for (int i = 0; i < nums.size(); i ++ )
if (!i || nums[i] != nums[i - 1])
nums[k ++ ] = nums[i];
return k;
}
};
LeetCode 27. 移除元素
class Solution {
public:
int removeElement(vector& nums, int val) {
int k = 0;
for (int i = 0; i < nums.size(); i ++ )
if (nums[i] != val)
nums[k ++ ] = nums[i];
return k;
}
};
LeetCode 28. 实现 strStr()
public:
/*
这道题是KMP的裸题,给定两个字符串s和p,输出p在s当中第一次出现的位置
KMP核心求next数组,最长前缀后缀,所有p[i-i]的相等的前缀和后缀中长度的最大值,不考虑平凡即只有自己的情况
*/
int strStr(string s, string p) {
if (p.empty()) return 0;
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;//字符串改成下标从1开始
vector next(m + 1);
for (int i = 2, j = 0; i <= m; i ++ ) {
while (j && p[i] != p[j + 1]) j = next[j];
if (p[i] == p[j + 1]) j ++ ;
next[i] = j;
}
for (int i = 1, j = 0; i <= n; i ++ ) {
while (j && s[i] != p[j + 1]) j = next[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m) return i - m;
}
return -1;
}
};
/*
双指针
时间复杂度: O((N-L)*L) 最好为O(n)
空间复杂度: O(1)
*/
class Solution {
public:
int strStr(string haystack, string needle) {
int L=needle.length(), n=haystack.length();
if(L==0) return 0;
int p = 0;
while(p < n-L+1){
while(p < n-L+1 && haystack[p] != needle[0]) ++p;
int a=0, b=0;//a记录以及匹配的长度,b记录匹配到了第几个
while(b < L && p < n && haystack[p] == needle[b])
++p, ++b, ++a;
if(a == L) return p-L;
p = p-a+1;
}
return -1;
}
};
LeetCode 29. 两数相除
class Solution {
public:
int divide(int x, int y) {
/*
要求不能乘除,那就只能加减,那这道题就用减法
先考虑两个都是正数比较简单的情况
最简单的就是减,x一直-=y,直到x exp;
bool is_minus = false;
if (x < 0 && y > 0 || x > 0 && y < 0) is_minus = true;
LL a = abs((LL)x), b = abs((LL)y);
for (LL i = b; i <= a; i = i + i) exp.push_back(i);
LL res = 0;
for (int i = exp.size() - 1; i >= 0; i -- )
if (a >= exp[i]) {
a -= exp[i];
res += 1ll << i;
}
if (is_minus) res = -res;
if (res > INT_MAX || res < INT_MIN) res = INT_MAX;
return res;
}
};
/*
可以用long 倍增思想,先减一个能减的最大的数,避免减的次数太多
*/
class Solution {
public:
int divide(int x, int y) {
if (x == INT_MIN && y == -1) {
return INT_MAX; //如果除法溢出,返回2 ^ 31 - 1
}
long a = labs(x), b = labs(y), ans = 0;
int sign = x > 0 ^ y > 0 ? -1 : 1;
while (a >= b) { //当a比b大,说明a/b至少等于1
long temp = b, m = 1;
while (temp << 1 <= a) { //找到最大的小于被除数的除数倍数
temp <<= 1; //除数倍增
m <<= 1; //减法个数倍增
}
a -= temp; //除数变小
ans += m; //加上个数
}
ans *= sign;
return ans;
}
};
/*
只能用int,将被除数和除数都转换成负数做,因为负数的数域比正数多1
*/
class Solution {
public:
int divide(int a, int b) {
// corner case
if (a == INT_MIN && b == -1) return INT_MAX;
// 初始化
const int limit = -1073741824;
int res = 0, sign = (a > 0) ^ (b > 0) ? 1 : -1;
if (a > 0) a = -a;
if (b > 0) b = -b;
// 倍增求解
while (a <= b){
int tmp = b, m = -1;
while (tmp >= limit && m >= limit && tmp + tmp >= a){
//因为只能用int的限制,所以循环条件除了要与a比较之外,还要与2^31/2比较,这里有一个比较蛋疼的地方
//int范围-2147483648~2147483647[-2^31~2^31-1] -2^31/2==-1073741824这个要直接写,不能用2^30这个形式
tmp += tmp;
m += m;
}
a -= tmp;
res += m;
}
return sign * res;
}
};
class Solution {
public:
int divide(int dividend, int divisor) {
int x = dividend, y = divisor;
if (x == INT_MIN && y == -1) return INT_MAX;
int sign = (x > 0) ^ (y > 0) ? 1 : -1;
if (x > 0) x = -x;
if (y > 0) y = -y;
vector> ys;
int ans = 0;
const int HALF_INT_MIN = -1073741824;
for (int t1 = y, t2 = -1; t1 >= x; t1 += t1, t2 += t2) {
ys.emplace_back(t1, t2);//预处理
if (t1 < HALF_INT_MIN)
break;
}
for (int i = ys.size() - 1; i >= 0; i--)
if (x <= ys[i].first) {
x -= ys[i].first;
ans += ys[i].second;
}
return sign*ans;
}
};
LeetCode 30. 串联所有单词的子串
class Solution {
public:
vector findSubstring(string s, vector& words) {
/*
这道题最优做法时间复杂度O(n),需要用到字符串哈希,但其实就是接下来这种O(nw)做法的优化
双指针滑动窗口+哈希表
先考虑要枚举哪些情况,要枚举字符串s中所有长度是mw的子串,普通枚举是从0开始,一直枚举到n-wm,枚举O(n)次,但是我们可以在枚举时候根据所有起始位置对w的余数分类,起始位置模w为0分为第一类:0,w,2w,3w,...;第二类:1,w+1,2w+1,...;...直到第w类:w-1,2w-1,...;这样就把所有起始位置分成了w组,这样做有什么好处呢
首先对于第一类也就是下标从0开始找到连续m个子区间恰好对应到m个单词,可以把每个子区间看作一个整体或者说是一个比较大的元素,问题就变成了,给了一堆连续的元素,在其中找到连续的m个元素恰好是给定的m个元素,顺序无所谓,这是典型的滑动窗口双指针问题,把给定的m个单词存入哈希表,然后用一个窗口大小是m的滑动窗口来维护所有长度是m的连续子段,这个窗口中所有元素也存入另外一个哈希表中,每次窗口往后慢慢移动的时候只会增加一个元素,删除一个元素,哈希表中增删操作是O(1)的,每次都要判断两个哈希表是否对应,如果对应的话,那么此时这个窗口的首元素下标就是一个解
那么如何判断两个哈希表是否对应呢?我们可以在维护窗口之外,再另外用一个变量cnt维护当前窗口的有效元素或者说有效单词,这样就很容易判断两个哈希表是否完全对应,就直接看cnt这个变量是否为m就行
时间复杂度:对于每一组而言(一共有w组),一共有n/w个起始位置,也就是n/w个元素(小区间),每次遍历的时候,每一个元素(小区间)只会进出哈希表一次,所以每一组计算次数是O(n/w),每一个小区间往哈希表里面插的时候,时间复杂度是是这个小区间的长度,也就是O(m)
所以总时间复杂度是O(n/w*w*w)也就是O(nw) 字符串哈希的思路就是把每一个单词映射成一个数字,这样每一个小区间往哈希表中插入的时候,时间复杂度就是O(1)所以总时间复杂度是O(n/w*w*1)==O(n) 变快了w倍
*/
vector res;
if (words.empty()) return res;//边界条件 记住就行,边界case返回空
int n = s.size(), m = words.size(), w = words[0].size();
unordered_map tot;//存第一个哈希表 每个单词映射成出现次数
for (auto& word : words) tot[word] ++ ;//&加不加都行,加了稍微快一些,不影响时间复杂度
for (int i = 0; i < w; i ++ ) {//枚举每一组
unordered_map wd;//记录每一组里面的哈希表
int cnt = 0;//窗口中有效单词的个数
for (int j = i; j + w - 1 < n; j += w) {
/*枚举当前这一组每个单词的起始下标,起始下标在j长度为w的单词的最后一个字母的下标是j + w - 1,j + w - 1 < n,j + w - 1最多为n-1,因为下标从0开始
*/
if (j >= i + m * w) {//窗口滑动要增删一个元素,增加必须,删除得等到窗口大小为m才能删
auto word = s.substr(j - m * w, w);//先把前面要删除的单词找出来
wd[word] -- ;
if (wd[word] < tot[word]) cnt -- ;//如果减去的这个元素是有效单词的话
}
auto word = s.substr(j, w);
wd[word] ++ ;
if (wd[word] <= tot[word]) cnt ++ ;
if (cnt == m) res.push_back(j - (m - 1) * w);
}
}
return res;
}
};
//字符串哈希版本
typedef unsigned long long ULL;
class Solution {
public:
static const int base = 131;
public:
ULL get(ULL h[], ULL p[], int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
vector findSubstring(string s, vector& words) {
vector res;
if(words.empty()) return res;
int n = s.size(), m = words.size(), w = words[0].size();
s = ' ' + s;
ULL h[n + 1], p[n + 1];
memset(h, 0, sizeof h);
// 1. 字符串哈希 -> s
p[0] = 1;
for(int i = 1; i <= n; i++)
{
h[i] = h[i - 1] * base + s[i] - 'a' + 1;
p[i] = p[i - 1] * base;
}
// 2. 字符串哈希 -> words
unordered_map tot;
for(string& word: words)
{
ULL hash = 0;
for(auto c: word)
hash = hash * base + c - 'a' + 1;
tot[hash]++;
}
for(int i = 1; i <= w; i++)
{
int suc = 0;
unordered_map window;
for(int j = i; j + w -1 < n+1 ; j += w)
{
if(j >= i + m * w)
{
ULL cur = get(h, p, j - m * w, j - (m - 1) * w - 1);
window[cur]--;
if(window[cur] < tot[cur])
suc--;
}
ULL cur = get(h, p, j, j + w - 1);
window[cur] ++;
if(window[cur] <= tot[cur])
suc++;
if(suc == m)
res.push_back(j - (m - 1) * w - 1);
}
}
return res;
}
};
LeetCode 31. 下一个排列
class Solution {
public:
void nextPermutation(vector& nums) {
/*
要求当前这个序列在字典序的下一个序列,如果当前已经是最大的序列,返回最小的,这是一个循环
我们应该尽可能保证前面的高位不变,只改变最后几位低位,变得稍微大一点点,从后往前考虑
从后往前考虑到某一位时,如果这个数以及它后面的元素是一个降序(不一定严格降,等于也可)的关系,那么这一位无论如何也不可能变大,所以我们要从后往前找到第一个非降序的元素,那么这一位可以稍微变大一点,与它后面大于它自身的元素中最小的那一个元素交换,并且在这之后把后面的元素变为升序
*/
int k=nums.size()-1;//从后往前遍历,k指向最后一个元素
while(k>0&&nums[k-1]>=nums[k]) k--;
/*这行代码意思是如果k可以往前走一步,并且往前走一步之后元素变大,就往前走一步,所以边界条件k至少为1,严格大于0,否则前面没有元素之后你再k-1就会出错*/
if(k==0){//如果跳出循环是因为k==0说明,当前是最大的字典序,逆序输出即可
reverse(nums.begin(),nums.end());
}
else{//如果是因为再往前走一步会变小的话,要找到k-1后面比它大的最小的元素
int t=k;//从k开始找
while(tnums[k-1]) t++;
swap(nums[t],nums[k-1]);
//交换之后,把k-1后面的降序变为升序
reverse(nums.begin()+k,nums.end());
}
}
};
LeetCode 32. 最长有效括号
/*
注意这道题说的是子串,代表求得结果在所给字符串中是连续的,如果说的是子序列的话,那就是非连续的
首先对于括号序列问题,合法括号序列满足两个条件
1.左右括号数量相等
2.任意前缀中,"("数量大于等于")"
这道题不太具有适用性,直接说吧
可以先考虑把序列分为若干段,可以证明任何一个合法序列必然在某一段里面,不会跨越两段之间
先考虑怎样分段,从前往后考虑,记录左右括号数量,找到第一个前缀不合法,也就是"("数量小于")"的位置,这就是第一段,我们可以证明任何一个合法序列必然在某一段里面,不会跨越两段之间,先假设成立,等下再证明,这样的话,继续往后看,从新开始计左右括号的数量,然后找到第二个不合法的位置,所以只需要在每一段内部找答案即可
证明:反证法
所以在每一段内,除了最后一个右括号之外,其他的右括号一定可以找到一个与之对应的以它为最后字符的最长的包含有效括号的子串,那怎样找到这个长度呢,这个可以用栈来做,每来一个右括号,pop掉栈顶的左括号,此时以这个右括号为右端点的最大区间长度是i-stk.top()(注意此时的栈顶元素是已经pop掉与之对应的左括号之后,新的栈顶元素)
*/
class Solution {
public:
int longestValidParentheses(string s) {
stack stk;//定义一个栈,栈中存的是所有左括号的下标,这样方便计算长度
int res=0;
for(int i=0, start=-1;i
class Solution {
public:
/*
动态规划
状态表示:dp[i]表示以下标i结尾的最长合法括号序列长度,所有以左括号结尾的序列都不是合法括号序列,所以我们从前往后扫描,如果遇到了右括号s[i],那么有两种情况
s[i - 1] = '(',那么很明显dp[i] = dp[i - 2] + 2
s[i - 1] = ')',我们需要判断i - dp[i - 1] - 1的位置是否是左括号,这个位置是以s[i - 1]结尾的最长合法括号序列开头的前一个字符。如果他不是左括号,说明当前右括号没有合法匹配。
如果是左括号,那么dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]。分别代表以s[i - 1]结尾的最长合法括号长度,s[i]的右括号和对应的左括号,s[i]对应的左括号前面一个位置结尾的最长合法括号序列
*/
int longestValidParentheses(string s) {
int res=0,n=s.size();
vector f(n,0);
for(int i=1;i=2) f[i]+=f[i-2];
}
else if((i>=(f[i-1]+1))&&s[i-f[i-1]-1]=='('){
f[i]=2+f[i-1];
if(i>=(f[i-1]+2)) f[i]+=f[i-f[i-1]-2];
}
}
res=max(res,f[i]);
}
return res;
}
};
class Solution {
public:
/*
双向扫描。不需要使用额外空间
第一遍从左往右扫描,left和right分别代表当前合法的左括号个数和右括号个数。遇到左括号left ++,遇到右括号right ++,如果left = right,说明找到了一个合法的括号对,更新答案,如果left < right,说明后面怎么匹配都不可能合法了,此时把left和right置为0。
但是这样对于((())的括号序列,得不到正确解,因此我们继续从右往左匹配一次。
第二遍第一遍从右往左扫描,left和right分别代表当前合法的左括号个数和右括号个数。遇到左括号left ++,遇到右括号right ++,如果left = right,说明找到了一个合法的括号对,更新答案,如果left > right,说明前面怎么匹配都不可能合法了,此时把left和right置为0。
*/
int longestValidParentheses(string s) {
int res = 0,n = s.size(), left = 0,right = 0;
for(int i = 0 ; i < n ; i ++)
{
if(s[i] == '(') left ++;
else right ++;
if(left == right) res = max(res,2 * right);
if(left < right) {left = 0;right = 0;}
}
left = 0,right = 0;
for(int i = n - 1;i >= 0; i --)
{
if(s[i] == ')') right ++;
else left ++;
if(left == right) res = max(res,2 * right);
if(right < left) {left = 0;right = 0;}
}
return res;
}
};
LeetCode 33. 搜索旋转排序数组
class Solution {
public:
int search(vector& nums, int target) {
/*
所给数组严格递增,没有重复元素,要二分找到某个元素,必须保证搜索区间有序
第一步先把待搜索元素所在有序区间边界点二分出来,确定答案所在区间
*/
if (nums.empty()) return -1;
int l = 0, r = nums.size() - 1;
while (l < r) {//在所给数组非空情况下找到两端区间边界点,大于等于nums[0]的最后一个元素,最好不求小于nums[0]的第一个元素,因为可能有数组完全单调上升的特殊情况
int mid = l + r + 1 >> 1;
if (nums[mid] >= nums[0]) l = mid;
else r = mid - 1;
}
if (target >= nums[0]) l = 0;
else l = r + 1, r = nums.size() - 1;
while (l < r) {//在答案所在有序区间二分出大于等于target的第一个元素
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[r] == target) return r;
return -1;
}
};
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public:
vector searchRange(vector& nums, int target) {
/*
给定一个有序数组和目标值target,找到找出给定目标值在数组中的开始位置和结束位置
要求时间复杂度O(log n) 明显二分
*/
if (nums.empty()) return {-1, -1};//如果给定数组为空
int l = 0, r = nums.size() - 1;//答案范围在下标区间范围内
while (l < r) {//找左端点,数组中大于等于目标值的第一个元素
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[r] != target) return {-1, -1};//如果没找到左端点的话,直接输出
int L = r;
l = 0, r = nums.size() - 1;
while (l < r) {//找右端点,数组中小于等于目标值的最后一个元素
int mid = l + r + 1 >> 1;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
return {L, r};
}
};
LeetCode 35. 搜索插入位置
class Solution {
public:
int searchInsert(vector& nums, int target) {
/*
找到从前往后看第一个大于等于当前数的位置
*/
int l = 0, r = nums.size();
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
return l;
}
};
LeetCode 36. 有效的数独
class Solution {
public:
bool isValidSudoku(vector>& board) {
/*
根据数独矩阵的定义,我们在遍历每个数字的时候,就看看包含当前位置的行和列以及3x3小方阵中是否已经出现该数字,那么我们需要三个标志矩阵,分别记录各行,各列,各小方阵是否出现某个数字,其中行和列标志下标很好对应,就是小方阵的下标需要稍稍转换一下
*/
vector> row(9,vector(9,false));
vector> col(9,vector(9,false));
vector> sub(9,vector(9,false));
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]!='.'){
int c=board[i][j]-'1';//字符1到9对应数字下标0到8
if(row[i][c]||col[j][c]||sub[(i/3)*3+(j/3)][c])
return false;
row[i][c]=true;
col[j][c]=true;
sub[(i/3)*3+(j/3)][c]=true;
}
}
}
return true;
}
};
class Solution {
public:
/*
用位运算优化
*/
bool isValidSudoku(vector>& board) {
vector row(9), col(9), squ(9); // 使用三个整型数组判重。
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.')
continue;
int num = board[i][j] - '0';
// 以row[i] & (1 << num)为例,这是判断第i行中,num数字是否出现过。
// 即row[i]值的二进制表示中,第num位是否是1。
// 以下col和squ同理。
if ((row[i] & (1 << num)) ||
(col[j] & (1 << num)) ||
(squ[(i / 3) * 3 + (j / 3)] & (1 << num)))
return false;
row[i] |= (1 << num);
col[j] |= (1 << num);
squ[(i / 3) * 3 + (j / 3)] |= (1 << num);
}
return true;
}
};
LeetCode 37. 解数独
class Solution {
public:
bool row[9][9], col[9][9], cell[9][9];//判断每行每列每个小方格是否出现过某个数字
void solveSudoku(vector>& board) {
memset(row, 0, sizeof row);//最开始时候先把所有行列小方格清空
memset(col, 0, sizeof col);
memset(cell, 0, sizeof cell);
for (int i = 0; i < 9; i ++ )//遍历棋盘,把已经填的数更新到数组里面
for (int j = 0; j < 9; j ++ )
if (board[i][j] != '.') {
int t = board[i][j] - '1';//把字符1到9映射成数字0到8
row[i][t] = col[j][t] = cell[(i / 3)*3+ j / 3][t] = true;
}
dfs(board, 0, 0);//从左上角开始dfs
}
bool dfs(vector>& board, int x, int y) {
if (y == 9) x ++, y = 0;//遍历时候,为了方便,每次往下遍历时,让纵坐标+1,一开始判断y是否越界
if (x == 9) return true;
if (board[x][y] != '.') return dfs(board, x, y + 1);//如果当前位置已经有数了,直接return下一个位置
for (int i = 0; i < 9; i ++ )//枚举当前位置可以填哪些数
if (!row[x][i] && !col[y][i] && !cell[(x / 3)*3+ y / 3][i]) {//如果i这个数没有出现过的话
board[x][y] = '1' + i;//填上字符1到9
row[x][i] = col[y][i] = cell[(x / 3)*3+ y / 3][i] = true;
if (dfs(board, x, y + 1)) return true;//在这个格子填上这个字符之后,再去探索下一个位置
board[x][y] = '.';//dfs恢复现场
row[x][i] = col[y][i] = cell[(x / 3)*3+ y / 3][i] = false;
}
return false;
}
};
LeetCode 38. 外观数列
class Solution {
public:
/*
双指针
*/
string countAndSay(int n) {
string s = "1";//前一项
for (int i = 0; i < n - 1; i ++ ) {//变换n-1次
string t;//当前这次变换之后,也就是当前循环最后t是变换之后的结果
for (int j = 0; j < s.size();) {//遍历前一项每个字符,这里的j由循环更新
int k = j + 1;//从j的后一项开始往后找
while (k < s.size() && s[k] == s[j]) k ++ ;//跳出循环时,k指向下一个不重复的元素
t += to_string(k - j) + s[j];
j = k;
}
s = t;
}
return s;
}
};
LeetCode 39. 组合总和
/*
这道题没有用完全背包是因为要返回所有方案
这道题就是要输出所有方案,所以就是暴搜,重点在于考虑搜索顺序
递归枚举,依次以每个点为起点枚举,每个点可以有一个或者多个,直到和大于等于target返回。
*/
class Solution {
private:
vector> res;
vector path;
public:
vector> combinationSum(vector& candidates, int target) {
if (candidates.empty()) return {};
dfs(candidates, 0, target);
return res;
}
void dfs(vector &candidates, int idx, int target){
if (target == 0){
res.push_back(path);
return;
}
for (int i = idx; i < candidates.size(); ++i){
if (candidates[i] <= target){
path.push_back(candidates[i]);
dfs(candidates, i, target - candidates[i]); // 因为可以重复使用,所以还是i。
path.pop_back();
}
}
}
};
LeetCode 40. 组合总和 II
/*
上一道题不包含重复元素,每个元素能选无限个,这道题是可能包含重复元素,但是每个只能选一次,这道题其实就是告诉了我们每个数最多选几个,多加了一个数量限制,就像多重背包问题,刚才那道题是枚举每个数能选几个,直到总和大于target为止,这道题每个数选几个除了被总和限制之外,还要被自身数量限制
这道题需要把每个数个数求出来,你可以排序,也可以哈希表,这里选排序
*/
class Solution {
public:
vector> ans;//最终答案
vector path;//遍历过程中的方案
vector> combinationSum2(vector& c, int target) {
sort(c.begin(), c.end());
dfs(c, 0, target);//从第0个数开始遍历
return ans;
}
void dfs(vector& c, int u, int target) {//当前枚举到第几个数,以及剩余的target
if (target == 0) {
ans.push_back(path);//把当前的路径push_back进去
return;
}
if (u == c.size()) return;//这个方案行不通,直接return
int k = u + 1;//要先看一下当前这个数有几个
while (k < c.size() && c[k] == c[u]) k ++ ;
int cnt = k - u;//这一段代码我觉得可以直接背住,很常用
for (int i = 0; c[u] * i <= target && i <= cnt; i ++ ) {
dfs(c, k, target - c[u] * i);
path.push_back(c[u]);
}
for (int i = 0; c[u] * i <= target && i <= cnt; i ++ ) {
path.pop_back();
}
}
};
class Solution {
private:
vector> res;
vector path;
public:
vector> combinationSum2(vector& candidates, int target) {
if (candidates.empty()) return {};
sort(candidates.begin(), candidates.end());
dfs(candidates, 0, target);
return res;
}
void dfs(vector &candidates, int idx, int target){
if (target < 0) return;
if (target == 0){
res.push_back(path);
return;
}
for (int i = idx; i < candidates.size(); ++i){
if (i > idx && candidates[i - 1] == candidates[i]) continue; // 去重语句
if (candidates[i] <= target){
path.push_back(candidates[i]);
dfs(candidates, i + 1, target - candidates[i]); // 每个数只能用一次,所以是i + 1
path.pop_back();
}
}
}
};
LeetCode 41. 缺失的第一个正数
/*
这道题可用先排序,然后从前往后扫描 但是这样时间复杂度O(nlogn) C++最多1e8
也可以用哈希表,先把所有数放入哈希表中,之后再去从小到大枚举所有正整数(1-n),直到找到第一个没有出现过的正整数为止,这样时间空间复杂度都是O(n)
这道题让我们找到没有出现的最小的正整数,可能是1,2,....
遍历每一个元素,如果当前遍历元素可以放到它该在的位置,并且不在它该在的位置的话,就一直交换,直到最后每一个可以放到该在的位置的元素放入该在的位置
说的具体一点就是如果数组中有元素1的话,把它放到第一个位置(下标为0),如果有2的话,把它放到第二个位置(下标为1)...如果有元素i的话,把它放到i-1这个位置
时间复杂度看着是两层循环,但每个元素最多执行一次,所以时间复杂度是O(n)
*/
class Solution{
public:
int firstMissingPositive(vector& nums)
{
//哈希表做法
// unordered_set h;
// for(auto x:nums) h.insert(x);
// int res=1;
// while(h.count(res)) res++;
// return res;
int n = nums.size();
for(int i = 0; i < n; ++ i)
while(nums[i] > 0 && nums[i] <= n
&& nums[nums[i] - 1] != nums[i])
swap(nums[i], nums[nums[i] - 1]);
for(int i = 0; i < n; ++ i)
if(nums[i] != i + 1)
return i + 1;//这里nums[i]不一定是重复的数,因为它可能不在1到n这个范围内
return n + 1;
}
};
LeetCode 42. 接雨水
class Solution {
public:
//方法1 两次遍历
/*
对于每一个柱子,它上方能盛多少水,取决于左右两边的高度最大值的最小值。所以我们可以先使用两遍扫描求出每个位置左边的最大值和右边的最大值(包括当前柱子)。再使用一遍扫描求出每个柱子能盛多少水。
*/
int trap(vector& height) {
int n = height.size(),res = 0;
if(n == 0) return 0;
int f[n],g[n];
f[0] = height[0];
for(int i = 1 ; i < n ; i ++)
f[i] = max(f[i - 1],height[i]);
g[n - 1] = height[n - 1];
for(int i = n - 2;i >= 0 ; i --)
g[i] = max(g[i + 1],height[i]);
for(int i = 0 ; i < n ; i ++)
res += min(f[i],g[i]) - height[i];
return res;
}
};
class Solution {
public:
//方法2 双指针优化
/*
分别从两边进行求解最大值,我们可以使用双指针来优化上述操作。初始时i = 0, j = n - 1,left_max,right_max分别代表i指针左侧最大值,和j指针右侧最大值。如果s[i] < s[j],说明此时阻碍当前盛水的是左侧,所以我们将i指针右移。
*/
int trap(vector& height) {
int n = height.size(),i = 0, j = n - 1,res = 0;
if(n == 0) return 0;
int left_max = 0,right_max = 0;
while(i < j)
{
if(height[i] < height[j])
{
left_max = max(left_max,height[i]);
res += left_max - height[i];
i ++;
}else
{
right_max = max(right_max,height[j]);
res += right_max - height[j];
j --;
}
}
return res;
}
};
LeetCode 43. 字符串相乘
class Solution {
public:
string multiply(string num1, string num2) {
vector A, B;//vector存下每一位数,低位在前,高位在后
int n = num1.size(), m = num2.size();
//逆序放入vector
for (int i = n - 1; i >= 0; i -- ) A.push_back(num1[i] - '0');//字符i以数字i的形式存入
for (int i = m - 1; i >= 0; i -- ) B.push_back(num2[i] - '0');
vector C(n + m);
for (int i = 0; i < n; i ++ )//相乘先不考虑进位
for (int j = 0; j < m; j ++ )
C[i + j] += A[i] * B[j];
for (int i = 0, t = 0; i < C.size(); i ++ ) {//很像高精度加法的做法
t += C[i];
C[i] = t % 10;
t /= 10;
}
int k = C.size() - 1;//去掉最高位的0,并且在跳出循环时,k指向最高位
while (k > 0 && !C[k]) k -- ;
string res;
while (k >= 0) res += C[k -- ] + '0';//数字映射成字符
return res;
}
};
LeetCode 44. 通配符匹配
/*
与第十题相比,这里的*可以表示任意字符串
状态计算:
如果p[j]!='*'
如果p[j]=='*' 不确定当前*可以匹配几个字符
f[i,j]=f[i,j-1]|f[i-1,j-1]|f[i-2,j-1]|...
f[i-1,j]=f[i-1,j-1]|f[i-2,j-1]|f[i-3,j-1]|...
所以f[i,j]=f[i,j-1]|f[i-1,j]
*/
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector> f(n + 1, vector(m + 1));
f[0][0] = true;
for (int i = 0; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ) {
if (p[j] == '*') {
f[i][j] = f[i][j - 1] || i && f[i - 1][j];
} else {
f[i][j] = (s[i] == p[j] || p[j] == '?') && i && f[i - 1][j - 1];
}
}
return f[n][m];
}
};
LeetCode 45. 跳跃游戏 II
/*
这道题不能直接用贪心,每次不一定要跳到最远,这道题本身是一个图论的问题,每个位置是一个点,比如第一个位置数值为2表示可以跳2步,也可以跳1步,代表这个点有两条有向边分别到达第二个和第三个点,所以这道题等价于给我们一个图,求从起点到终点的最短距离,如果用图论来做,由于每个点最坏情况下会向后面所有点相连,,所以边数最坏是n^2,所以时间复杂度最坏是n^2,所以尝试优化,看有无dp做法
数组 f[i] 表示到达 i 所需要的最少步数,定义 last 为第一次到达 i 时上一步的位置,last 从 0 开始。令 f[i] = f[last] + 1 后,f[i] 就会是最优值
*/
class Solution {
public:
int jump(vector& nums) {
int n = nums.size();
vector f(n);
f[0] = 0;
int last = 0;
for (int i = 1; i < n; i++) {
// 依次求 f[i] 的值。
while (i > last + nums[last]) // 根据 i 来更新 last。
last++;
f[i] = f[last] + 1; // 根据 f[last] 更新 f[i]。
}
return f[n - 1];
}
};
LeetCode 46. 全排列
/*
递归爆搜首先要考虑按照什么顺序搜索,可以从前往后依次枚举每个位置填哪些数,也可以依次枚举每个数放在哪个位置上,最多的是枚举每个位置可以填哪些数
转化成代码:dfs中要记住哪些状态,可以是函数参数,也可以是全局变量,首先要记录每个位置填什么,所以需要开一个int数组,其次需要判断当前填的是第几位,所以需要一个变量下标,最后还要记录一下当前哪些数用过了,所以需要一个布尔数组
最后如何实现每一条分支
*/
class Solution {
public:
vector> ans;
vector path;
vector st;
vector> permute(vector& nums) {
path = vector(nums.size());
st = vector(nums.size());
dfs(nums, 0);
return ans;
}
void dfs(vector& nums, int u) {
if (u == nums.size()) {
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i ++ ) {
if (st[i] == false) {
path[u] = nums[i];
st[i] = true;
dfs(nums, u + 1);
st[i] = false;//恢复现场,path不用改,因为每次会覆盖掉
}
}
}
};
LeetCode 47. 全排列 II
/*
与上道题区别是可能会有相同元素,两个搜索顺序,依次枚举每个位置填哪些数,也可以枚举每个数填到哪些位置上,有些题可能会要求按照字典序输出,所以要尽可能保证前面位置的值越小越好,所以这种情况一定要枚举每个位置可以填哪些数,枚举时候可能会有重复的值,比如有两个连续的1,这个位置可以填第一个也可以填第二个1,但这两种情况是等价的,所以要想一个措施不枚举重复的方案,重复原因是因为考虑这两个1不相同,为了保证不枚举重复元素,需要限定条件,可以保证相同的数相对的顺序始终不发生改变,考虑某个位置放哪个元素时,比如有多个连续的1,要放第一个没有被用过的1,所以先排序,把所有相同的元素放在一起
*/
class Solution {
public:
vector> ans;
vector path;
vector st;//默认初始化为0
vector> permuteUnique(vector& nums) {
sort(nums.begin(), nums.end());
path = vector(nums.size());
st = vector(nums.size());
dfs(nums, 0);
return ans;
}
void dfs(vector&nums, int u) {
if (u == nums.size()) {
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i ++ ) {
if (!st[i]) {//!st[i - 1]表示当前遍历的元素不是第一个没有被用过的元素
if (i && nums[i - 1] == nums[i] && !st[i - 1]) continue;
st[i] = true;
path[u] = nums[i];
dfs(nums, u + 1);
st[i] = false;
}
}
}
};
LeetCode 48. 旋转图像
/*
看旋转操作能否通过某些操作拼起来
先以左上-右下对角条线为轴做翻转;
再以中心的竖线为轴做翻转;
*/
class Solution {
public:
void rotate(vector>& matrix) {
int n = matrix.size();
for (int i = 0; i < n; i ++ )
for (int j = 0; j < i; j ++ )//沿对角线翻转,枚举左下角
swap(matrix[i][j], matrix[j][i]);
for (int i = 0; i < n; i ++ )
for (int j = 0, k = n - 1; j < k; j ++, k -- )//沿中心竖线翻转
swap(matrix[i][j], matrix[i][k]);
}
};
LeetCode 49. 字母异位词分组
/*
如何判断一个字符串属于哪个组,判断两个字符串是否一组,可以尝试两者能否达到某个中间状态
所以对于每一组字符串来说,能否找到一个中间状态使得这组字符串都能达到,可以把每个字符串按照ASCII码排序,一组里面的所有字符串排序完之后一定是相等的,分组的话可以开一个哈希表,把一个str映射到一个vector
时间复杂度分析:N 是字符串个数,L 是字符串平均长度。对于每个字符串,哈希表和vector的插入操作复杂度都是 O(1),排序复杂度是 O(LlogL)。所以总时间复杂度是 O(NLlogL)
*/
class Solution {
public:
vector> groupAnagrams(vector& strs) {
unordered_map> hash;//每一组的标准字符串映射到每一组
for (auto& str: strs) {//枚举每个字符串
string nstr = str;
sort(nstr.begin(), nstr.end());
hash[nstr].push_back(str);
}
vector> res;
for (auto& item : hash) res.push_back(item.second);
return res;
}
};
class Solution {
public:
//O(n∗l)
vector> groupAnagrams(vector& strs) {
unordered_map> hash;
if (strs.empty()) return {};
for (string s: strs){
vector cnt(26);
for (char c: s) ++cnt[c - 'a'];
string key;
for (int i = 0; i < 26; ++i) key += string(cnt[i], 'a' + i);
hash[key].push_back(s);
}
vector> res;
for (auto k_v: hash){
res.push_back(k_v.second);
}
return res;
}
};
LeetCode 50. Pow(x, n)
class Solution {
public:
double myPow(double x, int n) {
/*
考察快速幂,因为n很大,一个一个乘会超时,可以用二进制的思想,先处理n是正数情况
先预处理x^(2^0), x^(2^1),...x^(2^30)这些数,一共是logn个
n为INT_MIN可能会溢出,所以这里都把n转换到负数域进行计算了,偷懒的话直接开个long long变量就行
*/
// typedef long long LL;
// bool is_minus = n < 0;
// double res = 1;
// for (LL k = abs(LL(n)); k; k >>= 1) {
// if (k & 1) res *= x;//最后一位
// x *= x;
// }
// if (is_minus) res = 1 / res;
// return res;
if (n > 0) n = -n, x = 1 / x;
double res = 1;
while (n){
if (n & 1) res *= x;
x *= x;
n /= 2;
}
return 1 / res;
}
};
LeetCode 51. N 皇后
/*
不能有任意两个皇后在同一行,同一列或者同一条对角线上,这一道题要求输出所有方案,下一道题要求输出所有方案数,本质都一样,直接暴力搜索即可,观察可得一些性质,由于所有皇后之间不能相互攻击到,
所以每一行最多只能有一个皇后,又由于只有n行,所以每一行恰好放一个皇后,所以可以每一行每一行的去搜索该行的皇后可以放在哪个位置,依次类推即可
这个搜索方式类似于全排列,依次搜索每个位置可以放哪些数
判断某一行某一列是否有皇后比较好判断,而判断某一条对角线是否有皇后可以把对角线映射到一个数组中来判断,对角线有很多条,一个方向的对角线数量是2n-1,要把对角线映射到一个数组中也就是做一个映射使得不同对角线上的格子算出来的数组下标不一样即可,将棋盘放入坐标系中,则两个方向的对角线上的格子分别可以表示为y=x+k,y=x-k;所以k=y-x,k=y+x,因此每个点可以通过横纵坐标+-求出在哪一条斜线上,又由于y-x可能为负,用k映射数组下标不能为负,所以可以加上一个偏移量n即可
因此两个方向的对角线可以用以下两个表达式:
k=y-x+n
k=y+x
*/
// class Solution {
// public:
// int n;
// vector col, dg, udg;
// vector> ans;
// vector path;
// vector> solveNQueens(int _n) {
// n = _n;
// col = vector(n);
// dg = udg = vector(n * 2);
// path = vector(n, string(n, '.'));//初始化最开始的棋盘
// dfs(0);//从第0行开始暴力搜索
// return ans;
// }
// void dfs(int u) {
// if (u == n) {
// ans.push_back(path);
// return;
// }
// for (int i = 0; i < n; i ++ ) {//枚举第u行,皇后可以放入哪个位置
// if (!col[i] && !dg[u - i + n] && !udg[u + i]) {
// col[i] = dg[u - i + n] = udg[u + i] = true;
// path[u][i] = 'Q';
// dfs(u + 1);
// path[u][i] = '.';
// col[i] = dg[u - i + n] = udg[u + i] = false;
// }
// }
// }
// };
class Solution {
private:
vector path;
vector> res;
vector col, diag_s, diag_m;
public:
vector> solveNQueens(int n) {
if (n <= 0) return {};
col = vector(n);
diag_s = vector(2 * n - 1);
diag_m = vector(2 * n - 1);
dfs(0, n);
return res;
}
void dfs(int r, const int &n){
if (r >= n){
vector tmp(n, string(n,'.'));
for (int r = 0; r < n; ++r){
tmp[r][path[r]] = 'Q';
}
res.push_back(tmp);
return;
}
for (int c = 0; c < n; ++c){
if (!col[c] && !diag_s[r + c] && !diag_m[r - c + n - 1]){
col[c] = diag_s[r + c] = diag_m[r - c + n - 1] = true;
path.push_back(c);
dfs(r + 1, n);
path.pop_back();
col[c] = diag_s[r + c] = diag_m[r - c + n - 1] = false;
}
}
}
};
LeetCode 52. N皇后 II
// class Solution {
// public:
// int n;
// vector col, dg, udg;
// int totalNQueens(int _n) {
// n = _n;
// col = vector(n);
// dg = udg = vector(2 * n);
// return dfs(0);
// }
// int dfs(int u) {
// if (u == n) return 1;
// int res = 0;
// for (int i = 0; i < n; i ++ ) {
// if (!col[i] && !dg[u - i + n] && !udg[u + i]) {
// col[i] = dg[u - i + n] = udg[u + i] = true;
// res += dfs(u + 1);
// col[i] = dg[u - i + n] = udg[u + i] = false;
// }
// }
// return res;
// }
// };
class Solution {
private:
int res = 0;
vector col, diag_s, diag_m;
public:
int totalNQueens(int n) {
if (n <= 0) return 0;
col = vector(n);
diag_s = vector(2 * n - 1);
diag_m = vector(2 * n - 1);
dfs(0, n);
return res;
}
void dfs(int r, const int &n){
if (r >= n){
++res;
return;
}
for (int c = 0; c < n; ++c){
if (!col[c] && !diag_s[r + c] && !diag_m[r - c + n - 1]){
col[c] = diag_s[r + c] = diag_m[r - c + n - 1] = true;
dfs(r + 1, n);
col[c] = diag_s[r + c] = diag_m[r - c + n - 1] = false;
}
}
}
};
LeetCode 53. 最大子序和
/*
f(i)表示所有以nums[i]结尾的区间中的最大和
f(i)可分为两类,只包含nums[i]一个元素,或者包含前面一些元素
第二类是f(i-1)+nums[i]
又因为每个f(i)只与f(i-1)有关,所有不用开数组,直接一个变量维护即可
*/
class Solution {
public:
int maxSubArray(vector& nums) {
int res = INT_MIN;
for (int i = 0, last = 0; i < nums.size(); i ++ ) {
last = nums[i] + max(last, 0);
res = max(res, last);
}
return res;
}
};
/*
考虑区间 [l, r] 内的答案如何计算,令 mid = (l + r) / 2,则该区间的答案由三部分取最大值,分别是:
(1). 区间 [l, mid] 内的答案(递归计算)。
(2). 区间 [mid + 1, r] 内的答案(递归计算)。
(3). 跨越 mid 和 mid + 1 的连续子序列。
其中,第 (3) 部分只需要从 mid 开始向 l 找连续的最大值,以及从 mid+1 开始向 r 找最大值即可,在线性时间内可以完成。
递归终止条件显然是 l == r ,此时直接返回 nums[l]
每一层时间复杂度是 O(n),总共有 O(logn) 层,故总时间复杂度是 O(nlogn)
*/
class Solution {
public:
int calc(int l, int r, vector& nums) {
if (l == r)
return nums[l];
int mid = (l + r) >> 1;
int lmax = nums[mid], lsum = 0, rmax = nums[mid + 1], rsum = 0;
for (int i = mid; i >= l; i--) {
lsum += nums[i];
lmax = max(lmax, lsum);
}
for (int i = mid + 1; i <= r; i++) {
rsum += nums[i];
rmax = max(rmax, rsum);
}
return max(max(calc(l, mid, nums), calc(mid + 1, r, nums)), lmax + rmax);
}
int maxSubArray(vector& nums) {
int n = nums.size();
return calc(0, n - 1, nums);
}
};
LeetCode 54. 螺旋矩阵
/*
做法很多,推荐用偏移量做法,跟bfs一样,这道题是要求把所给数组按照蛇形遍历一遍,定义四个方向,最开始往右走,到达一行最后之后往下走,然后往左走,然后往上走,一直循环,01230123...
*/
class Solution {
public:
vector spiralOrder(vector>& matrix) {
vector res;
int n=matrix.size(),m=matrix[0].size();
if(!n||!m) return res;
vector> st(n,vector(m));//定义判重数组表示每个格子是否被遍历过
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};//定义四个方向的横纵坐标,注意这里先右再下再左再上
for(int i=0,x=0,y=0,d=0;i=n||b<0||b>=m||st[a][b]==true){//如果走了之后越界或者这个格子走过的话,换到下一个方向
d=(d+1)%4;
a=x+dx[d],b=y+dy[d];
}
x=a,y=b;
}
return res;
}
};
LeetCode 55. 跳跃游戏
/*
另外一道题是让求从起点到终点最少跳几步,这道题是问从起点能否跳到终点
*/
class Solution {
public:
bool canJump(vector& nums) {
//i遍历每个位置,j表示i之前所能跳到的最后的位置
for (int i = 0, j = 0; i < nums.size(); i ++ ) {
if (j < i) return false;//如果在i之前始终跳不到i,false
j = max(j, i + nums[i]);
}
return true;
}
};
LeetCode 56. 合并区间
/*
首先按照左端点从小到大排序,
*/
class Solution {
public:
vector> merge(vector>& a) {
vector> res;
if (a.empty()) return res;
sort(a.begin(), a.end());//vector默认按照第一个关键字,在本题中也就是按照左端点排序
int l = a[0][0], r = a[0][1];//表示上一个区间,这个区间还未确定是否已合并完全
for (int i = 1; i < a.size(); i ++ ) {
if (a[i][0] > r) {//当当前遍历的区间与上一个区间没有交集时
res.push_back({l, r});//此时(l,r)这个区间才确定合并完成,然后push_back入答案
l = a[i][0], r = a[i][1];//更新(l,r)
} else r = max(r, a[i][1]);//如果有交集的话,更新(l,r)
}
res.push_back({l, r});
return res;
}
};
LeetCode 57. 插入区间
/*
把原来的区间分为三部分,分别是和所给区间完全没有交集的一部分,然后是有交集的一部分,然后是后面完全没有交集的一部分
*/
class Solution {
public:
vector> insert(vector>& a, vector& b) {
vector> res;
int k = 0;
while (k < a.size() && a[k][1] < b[0]) res.push_back(a[k ++ ]); // 左边完全没交集的部分
if (k < a.size()) {//所有有交集部分,最后会合并成一个区间,更新最终区间的左右端点
b[0] = min(b[0], a[k][0]);
while (k < a.size() && a[k][0] <= b[1]) b[1] = max(b[1], a[k ++ ][1]);
}
res.push_back(b);
while (k < a.size()) res.push_back(a[k ++ ]);
return res;
//把b push_back到a中,然后用合并区间的方案
// a.push_back(b);
// vector> res;
// sort(a.begin(), a.end());//vector默认按照第一个关键字,在本题中也就是按照左端点排序
// int l = a[0][0], r = a[0][1];//表示上一个区间,这个区间还未确定是否已合并完全
// for (int i = 1; i < a.size(); i ++ ) {
// if (a[i][0] > r) {//当当前遍历的区间与上一个区间没有交集时
// res.push_back({l, r});//此时(l,r)这个区间才确定合并完成,然后push_back入答案
// l = a[i][0], r = a[i][1];//更新(l,r)
// } else r = max(r, a[i][1]);//如果有交集的话,更新(l,r)
// }
// res.push_back({l, r});
// return res;
}
};
LeetCode 58. 最后一个单词的长度
class Solution {
public:
//就是把上道题到这写,也是双指针
int lengthOfLastWord(string s) {
for (int i = s.size() - 1; i >= 0; i -- ) {
if (s[i] == ' ') continue;
int j = i - 1;
while (j >= 0 && s[j] != ' ') j -- ;
return i - j;
}
return 0;
}
};
LeetCode 59. 螺旋矩阵 II
class Solution {
public:
vector> generateMatrix(int n) {
vector> res(n, vector(n));
int dx[] = {0, 1, 0, -1}, dy[] = {1, 0, -1, 0};
for (int i = 1, x = 0, y = 0, d = 0; i <= n * n; i ++ ) {
res[x][y] = i;
int a = x + dx[d], b = y + dy[d];
if (a < 0 || a >= n || b < 0 || b >= n || res[a][b]) {
d = (d + 1) % 4;
a = x + dx[d], b = y + dy[d];
}
x = a, y = b;
}
return res;
}
};
LeetCode 60. 第k个排列
/*
类似于计数dp,一位一位来确定,如果第一个位置填1的话,有(n-1)!个数
比如n==4,k==10 (n-1)!==6k,所以第一位填2
所以接下来是在第一位确定填2的基础上看接下来填哪些数,要在第一位填2的基础上,在这根分支找到第4个数
第二位填1的话,有(3-1)!==2个数,填3的话也有两个数,所以第二位填3
第三位是要在前两位为2,3的基础上找到这根分支的第二个数
第三位填1的话有(2-1)!==1个数,所以不能填1,填4
所以填2341
时间复杂度:有n位,每位要把没遍历过的数遍历一遍,O(n^2)
*/
class Solution {
public:
string getPermutation(int n, int k) {
string res;
vector st(10);//记录哪些数被用过
for (int i = 0; i < n; i ++ ) {
int fact = 1;//当前枚举的第i位放元素之后,还剩下(n-i-1)!个分支
for (int j = 1; j <= n - i - 1; j ++ ) fact *= j;
for (int j = 1; j <= n; j ++ ) {
if (!st[j]) {//如果当前这个数没有被用过的话
if (fact < k) k -= fact;//这个位置一定不选这个数
else {
res += to_string(j);
st[j] = true;
break;
}
}
}
}
return res;
}
};
LeetCode 61. 旋转链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
/*
首先k可能非常大,所以如果k>=n时,每移动n次,等价于不移动,所以要求k%n
*/
class Solution {
public:
ListNode* rotateRight(ListNode* head, int k) {
if (!head) return head;
int n = 0;
ListNode* tail;
for (auto p = head; p; p = p->next) {//遍历整个链表,求链表长度
tail = p;
n ++ ;
}
k %= n;
if (!k) return head;//如果k是n的倍数,移动没意义
//移动k次,就是把后面的k个节点移到前面
auto p = head;//移动一次,倒数第n-1个节点,移动n-k-1次,到达倒数第k+1个节点
for (int i = 0; i < n - k - 1; i ++ ) p = p->next;
tail->next = head;
head = p->next;
p->next = NULL;
return head;
}
};
LeetCode 62. 不同路径
/*
f(i,j)表示从起点走到(i,j)的方案数,一般情况下f(i,j)==f(i-1,j)+f(i,j-1),特殊情况是i==0,只能从左边过来,j==0只能从上面过来
*/
class Solution {
public:
int uniquePaths(int m, int n) {
vector> f(n, vector(m));
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (!i && !j) f[i][j] = 1;//起点
else {
if (i>0) f[i][j] += f[i - 1][j];//可以从上面下来
if (j>0) f[i][j] += f[i][j - 1];//可以从左边过来
}
return f[n - 1][m - 1];
// vector f(n, 1);
// for (int i = 1; i < m; ++i)
// for (int j = 1; j < n; ++j)
// f[j] += f[j - 1];
// return f[n - 1];
}
};
LeetCode 63. 不同路径 II
/*
上一道题是每个格子都可以走,这道题加了些障碍,某些格子不能走,方案数为0,如果能走的话,方案数一般情况下为上方和左方方案数的和
*/
class Solution {
public:
int uniquePathsWithObstacles(vector>& o) {
int n = o.size();
if (!n) return 0;
int m = o[0].size();
vector> f(n, vector(m));
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (!o[i][j]) {//如果这个格子可以走的话
if (!i && !j) f[i][j] = 1;
else {
if (i) f[i][j] += f[i - 1][j];
if (j) f[i][j] += f[i][j - 1];
}
}
return f[n - 1][m - 1];
}
};
// class Solution {
// public:
// int uniquePathsWithObstacles(vector>& g) {
// if (g.empty() || g[0].empty()) return 0;
// int m = g.size(), n = g[0].size();
// vector f(n);
// for (int i = 0; i < n && !g[0][i]; ++i) f[i] = 1;
// for (int i = 1; i < m; ++i){
// if (g[i][0]) f[0] = 0;
// for (int j = 1; j < n; ++j)
// if (!g[i][j]) f[j] += f[j - 1];
// else f[j] = 0;
// }
// return f[n - 1];
// }
// };
LeetCode 64. 最小路径和
/*
f(i,j)表示从起点走到(i,j)的最小路径的和,一般情况下f(i,j)==grid(i,j) + min( f(i-1,j) , f(i,j-1) ),特殊情况是i==0,只能从左边过来,j==0只能从上面过来
*/
class Solution {
public:
int minPathSum(vector>& grid) {
int n = grid.size(), m = grid[0].size();
if (!n||!m) return 0;
vector> f(n, vector(m, INT_MAX));//求最小值,初始化为正无穷
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ ) {
if (!i && !j) f[i][j] = grid[i][j];
else {
if (i) f[i][j] = min(f[i][j], f[i - 1][j] + grid[i][j]);
if (j) f[i][j] = min(f[i][j], f[i][j - 1] + grid[i][j]);
}
}
return f[n - 1][m - 1];
}
};
class Solution {
public:
int minPathSum(vector>& g) {
if (g.empty() || g[0].empty()) return 0;
int m = g.size(), n = g[0].size();
vector f(n);
f[0] = g[0][0];
for (int i = 1; i < n; ++i) f[i] = f[i - 1] + g[0][i];
for (int i = 1; i < m; ++i){
f[0] += g[i][0];
for (int j = 1; j < n; ++j){
f[j] = min(f[j], f[j - 1]) + g[i][j];
}
}
return f[n - 1];
}
};
LeetCode 65. 有效数字
/*
实现stf函数,string to float
先去除行首和行尾空格;
行首如果有一个正负号,直接忽略;
如果字符串为空或只有一个'.',则不是一个合法数;
循环整个字符串,去掉以下几种情况:
(1) '.'或'e'多于1个;
(2) '.'在'e'后面出现;
(3) 'e'后面或前面为空,或者'e'前面紧跟着'.';
(4) 'e'后面紧跟着正负号,但正负号后面为空;
剩下的情况都合法;
时间复杂度分析:整个字符串只遍历一遍,所以时间复杂度是 O(n)
*/
class Solution {
public:
bool isNumber(string s) {
int l = 0, r = s.size() - 1;//去掉前后空格
while (l <= r && s[l] == ' ') l ++ ;
while (l <= r && s[r] == ' ') r -- ;
if (l > r) return false;
s = s.substr(l, r - l + 1);
if (s[0] == '+' || s[0] == '-') s = s.substr(1);//如果开头结尾出现正负号
if (s.empty()) return false;//只有正负号的情况
if (s[0] == '.' && (s.size() == 1 || s[1] == 'e' || s[1] == 'E'))
return false;//特判单独 . 的情况
int dot = 0, e = 0;//记录"."和e的次数
for (int i = 0; i < s.size(); i ++ ) {//遍历每个字符
if (s[i] == '.') {
if (dot > 0 || e > 0) return false;//如果"."之前出现过,或者这个"."在e的后面
dot ++ ;
} else if (s[i] == 'e' || s[i] == 'E') {
//如果e的前面是空的,或者后面是空的,或者e之前有过,返回false
if (!i || i + 1 == s.size() || e > 0) return false;
if (s[i + 1] == '+' || s[i + 1] == '-') {//如果e的后面是正负号的话,后面必须要有数才行
if (i + 2 == s.size()) return false;
i ++ ;
}
e ++ ;
} else if (s[i] < '0' || s[i] > '9') return false;
}
return true;
}
};
LeetCode 66. 加一
class Solution {
public:
vector plusOne(vector& digits) {
// reverse(digits.begin(), digits.end());
// int t = 1;
// for (auto& x: digits) {
// t += x;
// x = t % 10;
// t /= 10;
// }
// if (t) digits.push_back(t);
// reverse(digits.begin(), digits.end());
// return digits;
int n = digits.size(), add = 1;
for (int i = n - 1; i >= 0; --i){
digits[i] += add;
add = digits[i] / 10;
if (!add) return digits; // 如果发现不会进位了,可以提前返回结果
digits[i] %= 10;
}
if (add) digits.insert(digits.begin(), add);
return digits;
}
};
LeetCode 67. 二进制求和
class Solution {
public:
string addBinary(string a, string b) {
// reverse(a.begin(), a.end());
// reverse(b.begin(), b.end());
// string c;
// for (int i = 0, t = 0; i < a.size() || i < b.size() || t; i ++ ) {
// if (i < a.size()) t += a[i] - '0';
// if (i < b.size()) t += b[i] - '0';
// c += to_string(t % 2);
// t /= 2;
// }
// reverse(c.begin(), c.end());
// return c;
int m = a.size(), n = b.size();
if (m < n) return addBinary(b, a);
int carry = 0, i = m - 1, j = n - 1;
string res;
while(j >= 0 || i >= 0 || carry){
int num1 = i >= 0 ? a[i--] - '0' : 0;
int num2 = j >= 0 ? b[j--] - '0' : 0;
int num = num1 + num2 + carry;
carry = num / 2;
res.push_back(num % 2 + '0');
}
return string(res.rbegin(), res.rend());
}
};
LeetCode 68. 文本左右对齐
class Solution {
public:
/*
(模拟) O(n)
一行一行处理,每次先求出这一行最多可以放多少个单词,然后分三种情况处理:
如果是最后一行,则只实现左对齐:每个单词之间插入一个空格,行尾插入若干空格,使这一行的总长度是 maxWidth;
如果这一行只有一个单词,则直接在行尾补上空格;
其他情况,则需计算总共要填补多少空格,然后按题意均分在单词之间;
时间复杂度分析:每个单词只会遍历一遍,所以总时间复杂度是 O(n)
*/
vector fullJustify(vector& words, int maxWidth) {
vector res;
for (int i = 0; i < words.size(); i ++ ) {//枚举每个单词
int j = i + 1;//首先看一下当前这一行能放哪些单词,j从下一个单词开始看
int len = words[i].size();
while (j < words.size() && len + 1 + words[j].size() <= maxWidth)
len += 1 + words[j ++ ].size();
string line;
if (j == words.size() || j == i + 1) { // 左对齐,前两种情况
line += words[i];
for (int k = i + 1; k < j; k ++ ) line += ' ' + words[k];
while(line.size() < maxWidth) line += ' ';
} else { // 左右对齐
int cnt = j - i - 1, r = maxWidth - len + cnt;
//cnt表示len中有多少空隙,j-i个单词,j-i-1个空隙
//r表示这一行总共要放多少个空隙
line += words[i];
int k = 0;//从第0个空隙开始
while (k < r % cnt) line += string(r / cnt + 1, ' ') + words[i + k + 1], k ++ ;
while (k < cnt) line += string(r / cnt, ' ' ) + words[i + k + 1], k ++ ;
}
res.push_back(line);
i = j - 1;
}
return res;
}
};
LeetCode 69. x 的平方根
class Solution {
public:
int mySqrt(int x) {
/*
求非负整数x平方根下取整,整数二分,找到一个最大的y使得y^2<=x
时间复杂度 O(logx)
*/
int l = 0, r = x;
while (l < r)
{
int mid = l + 1ll + (r - l) / 2;
//int mid = (l + 1ll + r) / 2;//可能越界
if (mid <= x / mid) l = mid;//为了防止mid*mid越界,把mid除过去
else r = mid - 1;
}
return l;
}
};
LeetCode 70. 爬楼梯
/*
(矩阵快速幂) O(logn)
我们有通项公式,ak=ak−1+ak−2,可以将其转化为矩阵形式。
时间复杂度:矩阵快速幂时间复杂度为logn
*/
class Solution {
public:
int climbStairs(int n) {
if (n <= 0) return 0;
return mat_pow({{1, 1}, {1, 0}}, n)[0][0];
}
vector> mat_mul(vector> &A, vector> &B){
int m = A.size(), n = A[0].size(), p = B[0].size();
vector> res(m, vector(p));
for (int i = 0; i < m; ++i)
for (int j = 0; j < p; ++j)
for (int k = 0; k < n; ++k)
res[i][j] += A[i][k] * B[k][j];
return res;
}
vector> mat_pow(vector> mat, int n){
if (n == 0) return {{1, 0}, {0, 1}};
vector> res = {{1, 0}, {0, 1}};
while (n > 0){
if (n & 1) res = mat_mul(res, mat);
mat = mat_mul(mat, mat);
n >>= 1;
}
return res;
}
};
class Solution {
public:
int climbStairs(int n) {
int a = 1, b = 1;
for (int i = 2; i <= n; i++){
int c = a + b;
a = b;
b = c;
}
return b;
}
};
//快速幂起始为00余01的区别在于答案一个取矩阵的第一个,一个取矩阵的第二个
class Solution {
public:
typedef long long ll;
ll a[2][2]{{1,0},{0,1}}, b[2][2]{{1,1},{1,0}}; // a for answer, b for base
public:
int climbStairs(int n) {
while(n){
if(n&1) ab();
bb();
n>>=1;
}
return a[0][0];
}
void ab(){ // a*b
ll t11=a[0][0], t12=a[0][1], t21=a[1][0], t22=a[1][1];
a[0][0]=(t11*b[0][0] + t12*b[1][0]);
a[0][1]=(t11*b[1][0] + t12*b[1][1]);
a[1][0]=a[0][1]; // a、b均是斐波那契矩阵中的某一个,所以是对称矩阵
a[1][1]=(t21*b[1][0] + t22*b[1][1]);
}
void bb(){ // b*b
ll t11=b[0][0], t12=b[0][1], t21=b[1][0], t22=b[1][1];
b[0][0]=(t11*t11 + t12*t21);
b[0][1]=(t11*t12 + t12*t22);
b[1][0]=b[0][1];
b[1][1]=(t21*t12 + t22*t22);
}
};
class Solution {
public:
int climbStairs(int n) {
if (n <= 0) return 0;
return mat_pow({{1, 1}, {1, 0}}, n)[0][0];
}
vector> mat_mul(vector> &A, vector> &B){
int m = A.size(), n = A[0].size(), p = B[0].size();
vector> res(m, vector(p));
for (int i = 0; i < m; ++i)
for (int j = 0; j < p; ++j)
for (int k = 0; k < n; ++k)
res[i][j] += A[i][k] * B[k][j];
return res;
}
vector> mat_pow(vector> mat, int n){
if (n == 0) return {{1, 0}, {0, 1}};
vector> res = {{1, 0}, {0, 1}};
while (n > 0){
if (n & 1) res = mat_mul(res, mat);
mat = mat_mul(mat, mat);
n >>= 1;
}
return res;
}
};
LeetCode 71. 简化路径
/*
类似于树上的递归,实现时候用栈即可,或者类似于栈的操作
线性扫描一遍,每个字符最多入栈出栈一次,整个是线性的
我们可以首先获取所有反斜杠之间的内容,那么总共有四种情况.,..,file_name,我们在使用一个栈来维护当前路径。
如果读到的是空格或者.,那么不做操作
如果读到的是文件名,入栈
如果读到的是..,如果栈不为空就退栈。
最后栈中的元素就是当前文件路径,注意是否为空。
要考虑特殊处理的情况包括:
连续斜杠换成一个斜杠;
一个点的目录,忽略;
两个点的目录,向上找一层;
答案末尾不要以斜杠结尾;
空字符输出斜杠。
*/
class Solution {
public:
string simplifyPath(string path) {
string res, name;//name是当前文件名
if (path.back() != '/') path += '/';//为了方便统一,因为每次读文件名时以斜杠截止
for (auto c : path) {
if (c != '/') name += c;
else {//此时name已经读出了一个完整的文件名
if (name == "..") {
while (res.size() && res.back() != '/') res.pop_back();
//返回上级目录就是去掉当前目录的最后一个文件名
if (res.size()) res.pop_back();//如果去掉文件名之后不是空的话,再去掉/
} else if (name != "." && name != "") {
res += '/' + name;
}
name.clear();
}
}
if (res.empty()) res = "/";
return res;
}
};
LeetCode 72. 编辑距离
/*
(动态规划) O(mn)
状态表示:f[i][j]表示word1前i个字符与word2前j个字符的编辑距离。
状态转移:
1.如果word1的第i个字符与word2的第j个字符相等,那么编辑距离不会增加,仍取决于word1的前i - 1个字符与word2的前j - 1个字符,即f[i][j]=f[i−1][j−1];
2.如果word1的第i个字符与word2的第j个字符不相等,有三种选择,修改、删除、插入:
修改:也就是在word1的前i - 1个字符与word2的前j - 1个字符的编辑距离上又加了1,有f[i][j]=f[i−1][j−1]+1
删除:删除word1的第i个字符,那么编辑距离为word1的前i - 1个字符与word2的前j个字符的编辑距离上加了1,有f[i][j]=f[i−1][j]+1
同理删除word2的第j个字符,那么编辑距离为word1的前i个字符与word2的前j - 1个字符的编辑距离上又加了1,有f[i][j]=f[i][j−1]+1
增加:在计算编辑距离时,增加操作和删除操作其实是完全一样的,例如”exection”与” execution”,可以给”exection”加个’u’,也可以给”execution”减去个’u’,所以转移方程跟删除完全一样。
*/
class Solution {
public:
int minDistance(string a, string b) {
int n = a.size(), m = b.size();
a = ' ' + a, b = ' ' + b;
vector> f(n + 1, vector(m + 1));
for (int i = 0; i <= n; i ++ ) f[i][0] = i;
for (int i = 1; i <= m; i ++ ) f[0][i] = i;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ) {
f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
}
return f[n][m];
}
};
LeetCode 73. 矩阵置零
class Solution {
public:
/*
总归还是要用数组记录状态,但题目中要求不能用新数组,那就用原数组
用原数组第一行(列)的数来表示每一列(行)是否有0,注意就可以表示出来除去第一行第一列的右下角的子矩阵的状态
另外要用两个额外的变量表示第一行第一列是否有0
*/
void setZeroes(vector>& matrix) {
if (matrix.empty() || matrix[0].empty()) return;
int n = matrix.size(), m = matrix[0].size();
int c0 = 1, r0 = 1;
for (int i = 0; i < n; i ++ )
if (!matrix[i][0]) c0 = 0;
// c0&=matrix[i][0];除非矩阵中全为01才行
for (int i = 0; i < m; i ++ )
if (!matrix[0][i]) r0 = 0;
for (int i = 1; i < n; i ++ )
for (int j = 1; j < m; j ++ )
if (!matrix[i][j])
{
matrix[i][0] = 0;
matrix[0][j] = 0;
}
for (int i = 1; i < n; i ++ )
if (!matrix[i][0])
for (int j = 1; j < m; j ++ )
matrix[i][j] = 0;
for (int i = 1; i < m; i ++ )
if (!matrix[0][i])
for (int j = 1; j < n; j ++ )
matrix[j][i] = 0;
if (!c0)
for (int i = 0; i < n; i ++ )
matrix[i][0] = 0;
if (!r0)
for (int i = 0; i < m; i ++ )
matrix[0][i] = 0;
}
};
LeetCode 75. 颜色分类
class Solution {
public:
/*
(双指针) O(n)
要求只扫描一遍,使用常数空间,本质上是双(三)指针算法,i,j从左往右扫描,k从右往左扫描
并且要保证指针j前面的所有元素(0到j-1)全部是0,从j到i-1全部是1,k后面的全部是2(从k+1到最后)
空间复杂度
双指针使用了O(1)的空间
*/
void sortColors(vector& nums) {
for (int i = 0, j = 0, k = nums.size() - 1; i <= k;) {
if (nums[i] == 0) swap(nums[i ++ ], nums[j ++ ]);
else if (nums[i] == 2) swap(nums[i], nums[k -- ]);//i不变,因为交换之后,从k来的数还需要继续探索
else i ++ ;
}
}
};
class Solution {
public:
/*
(计数排序) O(n)
就数一下三种颜色各有几个,直接往nums里写就行;
这个做法也很容易推广到多个颜色。
时间复杂度
遍历了2次整个数组,时间复杂度是O(n)
空间复杂度
需要k个元素记录颜色的数量,所以是O(k)
*/
void sortColors(vector& nums) {
int n = nums.size(), zero = 0, one = 0, two = 0;
for (auto num: nums){
if (num == 0) ++zero;
else if (num == 1) ++one;
else if (num == 2) ++two;
}
cout<
//快排
class Solution {
public:
void sortColors(vector& nums) {
quick_sort(nums, 0, nums.size() - 1);
}
void quick_sort(vector& q,int l, int r){//不用纠结为什么没有传引用,因为数组传递的是头指针
if(l>=r) return;//递归时,一开始注意终止条件
int i=l-1,j=r+1,x=q[(l+r)/2];//下取整
while(ix);
if(i
LeetCode 76. 最小覆盖子串
class Solution {
public:
string minWindow(string s, string t) {
/*
滑动窗口算法,枚举每一个可能会是终点的元素i,求出离i最近的j使得j到i之间包含t的所有字符,
长度最小值是i-j+1,如果能找到的话,用来更新最小值
如何优化:能用双指针,滑动窗口一定要有单调性,当i往后走的时候,i所对应的j一定不能往前走
如何判断当前这一段是否包含t所有字符:哈希表,首先用一个哈希表统计一下t中每个字符出现的次数
然后用另外一个哈希表统计窗口内每个字符出现的次数,用cnt这个变量统计t中有多少字符已经被包含
如何正确统计出cnt:只统计新加的有效字符个数
什么时候j可以往后移动:如果s[j]这个字符在窗口中出现的次数大于t中包含的次数,那就可以往后移动
总体思路:每次i往后移动一位,移动之后更新一下s[i]这个字符出现的次数,判断一下s[i]是否为有效的字符
也就是说加入s[i]这个字符后能否更加包含t,如果有效的话,更新一下cnt,然后再判断一下j能不能往后走
每次做完之后再用窗口长度更新最短距离
这样每个字符遍历一次,每次遍历时,对哈希表操作也是常数次,所以时间复杂度是O(n)
*/
unordered_map hs, ht;
for (auto c: t) ht[c] ++ ;//记录t中每个字母出现的次数
string res;//对应答案
int cnt = 0;//统计t中多少字符已经被包含,一定记住cnt初始化为0,否则会函数内定义会随机赋值,答案会出错
for (int i = 0, j = 0; i < s.size(); i ++ ) {
hs[s[i]] ++ ;//窗口中当前遍历元素出现次数+1
if (hs[s[i]] <= ht[s[i]]) cnt ++ ;//判断当前加了的字符是否有效,也就是这个字符不多余
while (hs[s[j]] > ht[s[j]]) hs[s[j ++ ]] -- ;//如果s[j]这个字符多余的话,往后走
if (cnt == t.size()) {
if (res.empty() || i - j + 1 < res.size())
res = s.substr(j, i - j + 1);
}
}
return res;
}
};
LeetCode 77. 组合
/*
组合枚举需要判重,选1,2和选2,1是一样的,判重只需按照某种顺序选即可,比如从前往后选
*/
class Solution {
public:
vector> ans;
vector path;
vector> combine(int n, int k) {
dfs(n, k, 1);//k表示当前还需要选几个数,由于要按照顺序选,所以要记录当前可以从哪个数开始选
return ans;
}
void dfs(int n, int k, int start) {
if (!k) {
ans.push_back(path);
return;
}
for (int i = start; i <= n; i ++ ) {
path.push_back(i);
dfs(n, k - 1, i + 1);
path.pop_back();
}
}
};
LeetCode 79. 单词搜索
/*
如何搜索所有方案,要求每个字母只能用一次,一条路径,先枚举起点,再枚举每一步的方向,对于每个方向,递归下去,相当于一条搜索树,时间复杂度大致估计一下:每一步有三个方向可选,最多走len(word)步,所以每次搜索约为O(3^len(word));
最坏情况下搜O(mn)O(mn)次。
*/
class Solution {
public:
bool exist(vector>& board, string word) {
for (int i = 0; i < board.size(); i ++ ) {//枚举起点
for (int j = 0; j < board[i].size(); j ++ ) {
if (dfs(board, word, 0, i, j)) return true;//从0开始搜,当前起点是(i,j)
}
}
return false;
}
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool dfs(vector>& board, string& word, int u, int x, int y) {
if (board[x][y] != word[u]) return false;
if (u == word.size() - 1) return true;
char t = board[x][y];
board[x][y] = '.';//避免重复搜索
for (int i = 0; i < 4; i ++ ) {
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= board.size() || b < 0 || b >= board[0].size() || board[a][b] == '.') continue;
if (dfs(board, word, u + 1, a, b)) return true;
}
board[x][y] = t;//回溯,恢复现场
return false;
}
};
LeetCode 80. 删除排序数组中的重复项 II
class Solution {
public:
int removeDuplicates(vector& nums) {
int k = 0;
for (int i = 0; i < nums.size(); i ++ )
if (k < 2 || (nums[k - 1] != nums[i] || nums[k - 2] != nums[i]))
nums[k ++ ] = nums[i];
return k;
}
};
LeetCode 81. 搜索旋转排序数组 II
class Solution {
public:
bool search(vector& nums, int target) {
/*
所给数组中存在重复元素,先去掉右边界的重复元素,确保下半段严格小于nums[0]
*/
if (nums.empty()) return false;
int l = 0, r = nums.size() - 1;
while (l < r && nums[r] == nums[l]) r -- ;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (nums[mid] >= nums[0]) l = mid;
else r = mid - 1;
}
if (target >= nums[0]) l = 0;
else l = r + 1, r = nums.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
return nums[r] == target;
}
};
LeetCode 82. 删除排序链表中的重复元素 II
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
/*
注意这道题是指如果某个元素出现了两次及以上,就把这些元素全部删掉
如何把当前连续相等的一段扫描出来,双指针算法,一根指针a指向下一段第一个元素,另外一根指针b向后扫描,直到当前所指元素与前一个元素不相等为止,直接看a.next是否为b即可
*/
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
auto dummy = new ListNode(-1);
dummy->next = head;
auto p = dummy;
while (p->next) {//当有下一段的时候
/*循环条件是当p后面有节点的时候,判断p是要往后移,还是更新next,p是当前新链表的最后一个节点,也是待判断的某一段的前面那根指针*/
auto q = p->next->next;//找出下一段的第二个数
while (q && q->val == p->next->val) q = q->next;/*当下一段第二个数存在,并且下一段第二个数的值等于下一段第一个数的值时,第二根指针就往后走*/
if (p->next->next == q) p = p->next;//如果下一段只有一个数
else p->next = q;//下一段元素重复,删掉这一段,更新p.next
}
return dummy->next;
}
};
LeetCode 83. 删除排序链表中的重复元素重复元素
/**
* 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* deleteDuplicates(ListNode* head) {
if (!head) return head;
auto cur = head;//链表第一个节点存在的话一定可以存下来
for (auto p = head->next; p; p = p->next)//从链表第二个节点开始往后遍历
if (p->val != cur->val)//如果当前遍历节点是这一段的第一个数的话,存下来
cur = cur->next = p;
cur->next = NULL;
return head;
}
};
LeetCode 84. 柱状图中最大的矩形
class Solution {
public:
/*
枚举以每个柱顶为矩形上边界往左往右最远能到哪儿
此题的本质是找到每个柱形条左边和右边最近的比自己低的矩形条,然后用宽度乘上当前柱形条的高度作为备选答案。
解决此类问题的经典做法是单调栈,维护一个单调递增的栈,如果当前柱形条 i 的高度比栈顶要低,则栈顶元素 cur 出栈。出栈后,cur 右边第一个比它低的柱形条就是 i,左边第一个比它低的柱形条是当前栈中的 top。不断出栈直到栈为空或者柱形条 i 不再比 top 低。
*/
int largestRectangleArea(vector& heights) {
int n = heights.size(), ans = 0;
heights.push_back(-1);
// 为了算法书写方便,在数组末尾添加高度 -1
// 这会使得栈中所有数字在最后出栈。
stack st;
for (int i = 0; i <= n; i++) {
while (!st.empty() && heights[i] < heights[st.top()]) {
int cur = st.top();
st.pop();
if (st.empty()) ans = max(ans, heights[cur] * i);
else ans = max(ans, heights[cur] * (i - st.top() - 1));
}
st.push(i);
}
return ans;
}
};
LeetCode 85. 最大矩形
class Solution {
public:
/*
84扩展到二维
一行一行考虑,一行内所有柱形条的高度 heights 就是当前 (i, j) 位置能往上延伸的最大高度。
*/
int maximalRectangle(vector>& matrix) {
int n = matrix.size(), m, ans = 0;
if (n == 0)
return 0;
m = matrix[0].size();
vector heights(m + 1, 0);
heights[m] = -1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++)
if (matrix[i][j] == '0')
heights[j] = 0;
else
heights[j]++;
stack st;
for (int j = 0; j <= m; j++) {
while (!st.empty() && heights[j] < heights[st.top()]) {
int cur = st.top();
st.pop();
if (st.empty())
ans = max(ans, heights[cur] * j);
else
ans = max(ans, heights[cur] * (j - st.top() - 1));
}
st.push(j);
}
}
return ans;
}
};
LeetCode 86. 分隔链表
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
auto lh = new ListNode(-1), rh = new ListNode(-1);
auto lt = lh, rt = rh;
for (auto p = head; p; p = p->next) {
if (p->val < x) lt = lt->next = p;
else rt = rt->next = p;
}
lt->next = rh->next;
rt->next = NULL;
return lh->next;
}
};
LeetCode 88. 合并两个有序数组
class Solution {
public:
void merge(vector& nums1, int m, vector& nums2, int n) {
/*
归并排序的一部分,双指针
*/
int k = n + m - 1;//指针从后往前,否则会覆盖一些数
int i = m - 1, j = n - 1;
while (i >= 0 && j >= 0)
if (nums1[i] >= nums2[j]) nums1[k -- ] = nums1[i -- ];
else nums1[k -- ] = nums2[j -- ];
//因为是把 nums2 合并到 nums1 中,所以跳出循环时只需要考虑nums2没有放完的情况
while (j >= 0) nums1[k -- ] = nums2[j -- ];
}
};
LeetCode 89. 格雷编码
class Solution {
public:
/*
(递归)
假设我们生成了k位比特的格雷码,在生成k + 1位格雷码时,首先顺序将所有的k位格雷码前面添上0,然后逆序将所有的k位格雷码前面添上1。
*/
vector grayCode(int n) {
vector res(1, 0);//首先n==0的时候,只有一个数0
while (n -- ) {//枚举n次
for (int i = res.size() - 1; i >= 0; i -- ) {
res[i] *= 2;//前一半数最后补0,相当于*2
res.push_back(res[i] + 1);//后一半数最后补1,相当于+1
}
}
return res;
}
};
LeetCode 91. 解码方法
class Solution {
public:
int numDecodings(string s) {
if (s.empty()) return 0;
int n = s.size();
vector f(n + 1);
f[0] = 1;
for (int i = 1; i <= n; i ++ )
{
if (s[i - 1] < '0' || s[i - 1] > '9')
return 0;
f[i] = 0;
if (s[i - 1] != '0') f[i] = f[i - 1];
if (i > 1)
{
int t = (s[i-2]-'0')*10+s[i-1]-'0';
if (t >= 10 && t <= 26)
f[i] += f[i - 2];
}
}
return f[n];
}
};
LeetCode 92. 反转链表 II
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
auto dummy = new ListNode(-1);//链表可能会变,习惯创建一个虚拟头结点
dummy->next = head;
auto a = dummy;//从dummy开始走1步到位置1,走m-1步到位置m-1,也就是位置m的前一个节点
for (int i = 0; i < m - 1; i ++ ) a = a->next;
auto b = a->next, c = b->next;//b和c最开始指向位置m和m+1
for (int i = 0; i < n - m; i ++ ) {//翻转n-m条边
auto t = c->next;
c->next = b;
b = c, c = t;
}
a->next->next = c;
a->next = b;
return dummy->next;
}
};
LeetCode 94. 二叉树的中序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//递归写法
vector ans;//记录答案
vector inorderTraversal(TreeNode* root) {
dfs(root);//从根节点开始递归深搜
return ans;
}
void dfs(TreeNode* root) {
if (!root) return;
dfs(root->left);//先中序遍历左子树
ans.push_back(root->val);//再将当前点插入答案
dfs(root->right);//再中序遍历右子树
}
};
class Solution {
public:
//迭代写法
/*
(栈模拟递归) O(n)
这是递归变成栈的通用做法
用栈来模拟这个过程。栈中每个元素存储两个值:TreeNode节点和一个整型的标记。
标记 = 0,表示还没遍历该节点的左子树;
标记 = 1,表示已经遍历完左子树,但还没遍历右子树;
标记 = 2,表示已经遍历完右子树;
然后我们可以根据标记的值,来分别处理各种情况:
标记 = 0,则将该节点的标记改成1,然后将其左儿子压入栈中;
标记 = 1,则说明左子树已经遍历完,将根节点的值插入答案序列中,然后将该节点的标记改成2,并将右儿子压入栈中;
标记 = 2,则说明以该节点为根的子树已经遍历完,直接从栈中弹出即可;
时间复杂度分析:树中每个节点仅会遍历一遍,且进栈出栈一次,所以时间复杂度是 O(n)
*/
vector inorderTraversal(TreeNode* root) {
vector res;
stack>sta;
sta.push({root,0});
while (!sta.empty())
{
if (sta.top().first == NULL)
{
sta.pop();
continue;
}
int t = sta.top().second;
if (t == 0)
{
sta.top().second = 1;
sta.push({sta.top().first->left, 0});
}
else if (t == 1)
{
res.push_back(sta.top().first->val);
sta.top().second = 2;
sta.push({sta.top().first->right, 0});
}
else sta.pop();
}
return res;
}
};
class Solution {
public:
vector inorderTraversal(TreeNode* root) {
vector res;
stack stk;
while (true) {
while (root) {// 一直向左并将沿途结点压入堆栈
stk.push(root);
root = root->left;
}
if(stk.size()==0) break;
// 每次取出栈顶,访问它,再访问其右子树
root = stk.top();
stk.pop();
res.push_back(root->val);
root = root->right;
}
return res;
}
};
LeetCode 95. 不同的二叉搜索树 II
class Solution {
public:
/*
递归搜索所有方案。
对于每段连续的序列 l,l+1,…rl,l+1,…r,枚举二叉搜索树根节点的位置;
分别递归求出左右子树的所有方案;
左子树的任意一种方案和右子树的任意一种方案拼在一起,可以得到当前节点的一种方案,所以我们将左右子树的所有方案两两组合,并记录在答案中。
*/
vector generateTrees(int n) {
if (!n) return {};
return dfs(1, n);
}
vector dfs(int l, int r) {
if (l > r) return {NULL};
vector res;
for (int i = l; i <= r; i ++ ) {
auto left = dfs(l, i - 1), right = dfs(i + 1, r);
for (auto l: left)
for (auto r: right) {
auto root = new TreeNode(i);
root->left = l, root->right = r;
res.push_back(root);
}
}
return res;
}
};
LeetCode 96. 不同的二叉搜索树
class Solution {
public:
/*
(动态规划) O(n^2)
状态表示:f[n]f[n] 表示 n个节点的二叉搜索树共有多少种。
状态转移:左子树可以有 0,1,…n−10,1,…n−1 个节点,对应的右子树有 n−1,n−2,…,0n−1,n−2,…,0 个节点,f[n]f[n] 是所有这些情况的加和,
*/
int numTrees(int n) {
vector f(n + 1);
f[0] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
f[i] += f[j - 1] * f[i - j];
return f[n];
}
};
LeetCode 97. 交错字符串
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n = s1.size(), m = s2.size();
if (s3.size() != n + m) return false;
vector> f(n + 1, vector(m + 1));
s1 = ' ' + s1, s2 = ' ' + s2, s3 = ' ' + s3;
for (int i = 0; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
if (!i && !j) f[i][j] = true;
else {
if (i && s1[i] == s3[i + j]) f[i][j] = f[i - 1][j];
if (j && s2[j] == s3[i + j]) f[i][j] = f[i][j] || f[i][j - 1];
}
return f[n][m];
}
};
LeetCode 98. 验证二叉搜索树
/*
中序遍历,并检查当前节点是否比前一节点大,一旦不符合,就说明不是二叉搜索树。
*/
class Solution {
private:
TreeNode *pre = nullptr;
bool res = true;
public:
bool isValidBST(TreeNode* root) {
if (!root) return true;
dfs(root);
return res;
}
void dfs(TreeNode *root){
if (!root) return;
dfs(root->left);
if (pre && pre->val >= root->val){
res = false;
return;
}
pre = root;
dfs(root->right);
}
};
/*
中序遍历,并检查当前节点是否比前一节点大,一旦不符合,就说明不是二叉搜索树。
*/
class Solution {
private:
TreeNode *pre = nullptr;
bool res = true;
public:
bool isValidBST(TreeNode* root) {
if (!root) return true;
stack stk;
while (true){
while (root){
stk.push(root);
root = root->left;
}
if (stk.empty()) break;
root = stk.top(); stk.pop();
if (pre && pre->val >= root->val) return false;
pre = root;
root = root->right;
}
return true;
}
};
LeetCode 99. 恢复二叉搜索树
/*
首先要判断哪两个点被交换
中序遍历找逆序对
中序遍历的结果就是二叉树搜索树所表示的有序数列。有序数列从小到大排序,但有两个数被交换了位置。共有两种情况:
交换的是相邻两个数,例如 1 3 2 4 5 6,则第一个逆序对,就是被交换的两个数,这里是3和2;
交换的是不相邻的数,例如 1 5 3 4 2 6,则第一个逆序对的第一个数,和第二个逆序对的第二个数,就是被交换的两个数,这里是5和2;
找到被交换的数后,我们将它们换回来即可。
这道题目可以用Morris-traversal算法,该算法可以用额外 O(1) 的空间,以及 O(n) 的时间复杂度,中序遍历一棵二叉树。
从根节点开始遍历,直至当前节点为空为止:
如果当前节点没有左儿子,则打印当前节点的值,然后进入右子树;
如果当前节点有左儿子,则找当前节点的前驱。
(1) 如果前驱节点的右儿子为空,说明左子树没遍历过,则进入左子树遍历,并将前驱节点的右儿子置成当前节点,方便回溯;
(2) 如果前驱节点的右儿子为当前节点,说明左子树已被遍历过,则将前驱节点的右儿子恢复为空,然后打印当前节点的值,然后进入右子树继续遍历;
复杂度分析:Morris-traversal算法的时间复杂度是 O(n),额外空间复杂度是 O(1)。
这里可能有同学会问,Morris-traversal算法中每次都会求当前节点的前驱节点,而求前驱节点的最坏时间复杂度是 O(n),那遍历整棵树的时间复杂度为什么不是 O(n^2)呢?这是因为对于一棵二叉树,对所有节点求一遍前驱,则每条边只会被遍历两次,所以时间复杂度加一块也是 O(n)。
*/
class Solution {
public:
void recoverTree(TreeNode* root) {
TreeNode *first = NULL, *second, *last = NULL;//前两者是记录的第一个,第二个交换的点,last是上一个点
while (root) {
if (!root->left) {
if (last && last->val > root->val) {
if (!first) first = last, second = root;//第一个逆序对
else second = root;//第二个逆序对
}
last = root;
root = root->right;
} else {
auto p = root->left;
while (p->right && p->right != root) p = p->right;
if (!p->right) p->right = root, root = root->left;
else {
p->right = NULL;
if (last && last->val > root->val) {
if (!first) first = last, second = root;
else second = root;
}
last = root;
root = root->right;
}
}
}
swap(first->val, second->val);
}
};
LeetCode 100. 相同的树
/*
(递归) O(n)O(n)
前序遍历,在每个节点,递归地检查值是否相同,左子树、右子树是否相同。
时间复杂度
遍历所有节点,时间复杂度O(n)
*/
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p && !q) return true;
if (!p || !q || p->val != q->val) return false;
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
};
/*
(BFS遍历) O(n)
任意一种遍历,前序、中序、后序、层序等都可以用于检查树是否相同,这里选用迭代实现较为容易的BFS遍历进行检查
时间复杂度
遍历所有节点,时间复杂度O(n)
*/
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
queue Q; Q.push(p); Q.push(q);
while (Q.size()){
p = Q.front(); Q.pop();
q = Q.front(); Q.pop();
if (!p && !q) continue;
if (!p || !q || p->val != q->val) return false;
Q.push(p->left); Q.push(q->left);
Q.push(p->right); Q.push(q->right);
}
return true;
}
};
LeetCode 101. 对称二叉树
/*
递归 O(n)
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (!root) return true;
return dfs(root->left, root->right);
}
bool dfs(TreeNode* p, TreeNode* q) {
if (!p && !q) return true;
if (!p || !q || p->val != q->val) return false;
return dfs(p->left, q->right) && dfs(p->right, q->left);
}
};
/*
迭代 中序遍历 O(n)
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (!root) return true;
stack stk1, stk2;
TreeNode *p = root, *q = root;
while (true){
while (p) stk1.push(p), p = p->left;
while (q) stk2.push(q), q = q->right;
if (stk1.size() != stk2.size()) return false;
if (stk1.empty() || stk2.empty()) return false;
p = stk1.top(); stk1.pop();
q = stk2.top(); stk2.pop();
if (p->val != q->val) return false;
if (p == q) break;
p = p->right;
q = q->left;
}
return true;
}
};
/*
迭代 中序遍历 O(n)
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (!root) return true;
queue Q;
Q.push(root->left); Q.push(root->right);
while (Q.size()){
TreeNode *p = Q.front(); Q.pop();
TreeNode *q = Q.front(); Q.pop();
if (!p && !q) continue;
if (!p || !q || p->val != q->val) return false;
Q.push(p->left); Q.push(q->right);
Q.push(p->right); Q.push(q->left);
}
return true;
}
};
LeetCode 102. 二叉树的层序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
/*
层序遍历可以借助一个宽搜队列按照层来输出
*/
vector> levelOrder(TreeNode* root) {
vector> res;//对应答案
queue q;//定义宽搜队列,队列中动态存放每一层的节点
if(root) q.push(root);//如果根节点存在就将其放入队列中,注意队列是push
//宽搜过程模板
while(q.size()){
//遍历队列中当前这一层所有节点,将这一层节点的值放入level中,并且更新队列,存放下一层节点
vector level;//存储当前这一层所有节点的值
int len=q.size();//记录当前这一层的节点数
while(len--){
auto t=q.front();
q.pop();//取出队头元素并删掉
level.push_back(t->val);
if(t->left) q.push(t->left);
if(t->right) q.push(t->right);
}
res.push_back(level);
}
return res;
}
};
LeetCode 103. 二叉树的锯齿形层次遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
/*
还是和上道题一样,宽搜队列一层一层遍历,只是在把当前层插入答案之前,先判断是否需要翻转当前层,仅此
*/
vector> zigzagLevelOrder(TreeNode* root) {
vector> res;
queue q;
if (root) q.push(root);
int cnt = 0;//记录当前在第几层,用来判断当前层插入答案时是否需要翻转
while (q.size()) {
vector level;
int len = q.size();
while (len -- ) {
auto t = q.front();
q.pop();
level.push_back(t->val);
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
if ( ++ cnt % 2 == 0) reverse(level.begin(), level.end());
res.push_back(level);
}
return res;
}
};
LeetCode 104. 二叉树的最大深度
class Solution {
public:
int maxDepth(TreeNode* root) {
if (!root) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
class Solution {
public:
int maxDepth(TreeNode* root) {
if (!root) return {};
vector> res;
queue Q; Q.push(root);
int step = 0;
while (Q.size()){
int size = Q.size();
while (size--){
TreeNode *node = Q.front(); Q.pop();
if (node->left) Q.push(node->left);
if (node->right) Q.push(node->right);
}
++step;
}
return step;
}
};
LeetCode 105. 从前序与中序遍历序列构造二叉树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
/*
递归建立整棵二叉树:先递归创建左右子树,然后创建根节点,并让指针指向两棵子树。
具体步骤如下:
先利用前序遍历找根节点:前序遍历的第一个数,就是根节点的值;
在中序遍历中找到根节点的位置 k(这一步可以用哈希表来做),则 k 左边是左子树的中序遍历,右边是右子树的中序遍历;
假设左子树的中序遍历的长度是 l,则在前序遍历中,根节点后面的 l 个数,是左子树的前序遍历,剩下的数是右子树的前序遍历;
有了左右子树的前序遍历和中序遍历,我们可以先递归创建出左右子树,然后再创建根节点;
时间复杂度分析:我们在初始化时,用哈希表(unordered_map)记录每个值在中序遍历中的位置,这样我们在递归到每个节点时,在中序遍历中查找根节点位置的操作,只需要 O(1) 的时间。此时,创建每个节点需要的时间是 O(1),所以总时间复杂度是 O(n)。
*/
unordered_map pos;//记录每个值在中序遍历中的位置
TreeNode* buildTree(vector& preorder, vector& inorder) {
for (int i = 0; i < inorder.size(); i ++ ) pos[inorder[i]] = i;
return build(preorder, inorder, 0, preorder.size() - 1, 0, inorder.size() - 1);
}
TreeNode* build(vector& preorder, vector& inorder, int pl, int pr, int il, int ir) {
if (pl > pr) return NULL;//边界判断
auto root = new TreeNode(preorder[pl]);//创建根节点
int k = pos[root->val];//找到根节点在中序遍历的位置
root->left = build(preorder, inorder, pl + 1, pl + 1 + k - 1 - il, il, k - 1);
root->right = build(preorder, inorder, pl + 1 + k - 1 - il + 1, pr, k + 1, ir);
return root;
}
};
LeetCode 106. 从中序与后序遍历序列构造二叉树
class Solution {
public:
unordered_map pos;
TreeNode* buildTree(vector& inorder, vector& postorder) {
for (int i = 0; i < inorder.size(); i ++ ) pos[inorder[i]] = i;
return build(inorder, postorder, 0, inorder.size() - 1, 0, postorder.size() - 1);
}
TreeNode* build(vector& inorder, vector& postorder, int il, int ir, int pl, int pr) {
if (il > ir) return NULL;//严格大于才ac
auto root = new TreeNode(postorder[pr]);
int k = pos[root->val];
root->left = build(inorder, postorder, il, k - 1, pl, pl + k - 1 - il);
root->right = build(inorder, postorder, k + 1, ir, pl + k - 1 - il + 1, pr - 1);
return root;
}
};
LeetCode 107. 二叉树的层次遍历 II
class Solution {
public:
vector> levelOrderBottom(TreeNode* root) {
vector> res;
queue q;
if (root) q.push(root);
while (q.size()) {
vector level;
int len = q.size();
while (len -- ) {
auto t = q.front();
q.pop();
level.push_back(t->val);
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
res.push_back(level);
}
reverse(res.begin(),res.end());
return res;
}
};
LeetCode 108. 将有序数组转换为二叉搜索树
class Solution {
public:
TreeNode* sortedArrayToBST(vector& nums) {
return build(nums, 0, nums.size() - 1);
}
TreeNode* build(vector& nums, int l, int r) {
if (l > r) return NULL;
int mid = l + r >> 1;
auto root = new TreeNode(nums[mid]);
root->left = build(nums, l, mid - 1);
root->right = build(nums, mid + 1, r);
return root;
}
};
LeetCode 109. 有序链表转换二叉搜索树
class Solution {
public:
TreeNode* sortedListToBST(ListNode* head) {
if (!head) return NULL;
int n = 0;
for (auto p = head; p; p = p->next) n ++ ;
if (n == 1) return new TreeNode(head->val);
auto cur = head;
//从第一个点开始跳的,如果跳 k 次,会跳到第 k+1 个点
//跳n / 2 - 1次,跳到第n / 2下取整个点,找到中点的前一个点
/*
for(int i = 0; i <= n; i++ 这里会循环 n + 1次
同理
for(int i = 0; i <= n/2-1; i++ 这里会循环 (n/2-1 + 1 = n/2) 次, 所以找到的cur是当前的中点,而不是中点的前一个
*/
for (int i = 0; i < n / 2 - 1; i ++ ) cur = cur->next;
auto root = new TreeNode(cur->next->val);
root->right = sortedListToBST(cur->next->next);
cur->next = NULL;
root->left = sortedListToBST(head);
return root;
}
};
class Solution {
public:
TreeNode* sortedListToBST(ListNode* head) {
if (!head) return nullptr;
ListNode *pre = nullptr, *slow = head, *fast = head;
while (fast->next && fast->next->next){
fast = fast->next->next;
pre = slow;
slow = slow->next;
}
auto root = new TreeNode(slow->val);
root->right = sortedListToBST(slow->next);
if (pre) pre->next = NULL, root->left = sortedListToBST(head);
return root;
}
};
LeetCode 110. 平衡二叉树
class Solution {
public:
/*
(递归) O(n)
递归判断:
先递归判断两棵子树是否是平衡的,递归的过程中记录每棵树的最大深度值,然后判断两棵子树的最大深度的差是否不大于1。
时间复杂度分析:每个节点仅被遍历一次,且判断的复杂度是 O(1)。所以总时间复杂度是 O(n)。
*/
bool ans;
bool isBalanced(TreeNode* root) {
ans = true;
dfs(root);
return ans;
}
int dfs(TreeNode* root) {
if (!root) return 0;
int lh = dfs(root->left), rh = dfs(root->right);
if (abs(lh - rh) > 1) ans = false;
return max(lh, rh) + 1;
}
};
LeetCode 111. 二叉树的最小深度
class Solution {
public:
/*
这题需要判断当前点是不是叶节点,而104题不需要判断,因为如果不是叶节点,那么当前点就一定不是最深的点。
*/
int minDepth(TreeNode* root) {
if (!root) return 0;
if (!root->left && !root->right) return 1;
if (root->left && root->right) return min(minDepth(root->left), minDepth(root->right)) + 1;
if (root->left) return minDepth(root->left) + 1;
return minDepth(root->right) + 1;
}
};
class Solution {
public:
/*
(BFS层序遍历) O(n)
既然是求最短,那么可以联想到用BFS求解,层序遍历二叉树,遇到叶子节点,直接返回深度即可,它一定是最浅的了。
*/
int minDepth(TreeNode* root) {
if (!root) return 0;
int depth = 1;
queue Q; Q.push(root);
while (!Q.empty()){
int size = Q.size();
while (size--){
TreeNode *node = Q.front(); Q.pop();
if (!node->left && !node->right) return depth;
if (node->left) Q.push(node->left);
if (node->right) Q.push(node->right);
}
++depth;
}
return depth;
}
};
LeetCode 112. 路径总和
/*
(递归) O(n)
递归,自顶向下从根节点往叶节点走,每走过一个节点,就让 sum 减去该节点的值,则如果走到某个叶节点时,sum 恰好为0,则说明从根节点到这个叶节点的路径上的数的和等于 sum。
只要找到一条满足要求的路径,递归即可返回。
时间复杂度分析:每个节点仅被遍历一次,且递归过程中维护 sum 的时间复杂度是 O(1),所以总时间复杂度是 O(n)。
*/
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if (!root) return false;
if (!root->left && !root->right) return root->val == sum;
if (root->left && hasPathSum(root->left, sum - root->val)) return true;
if (root->right && hasPathSum(root->right, sum - root->val)) return true;
return false;
}
};
LeetCode 113. 路径总和 II
class Solution {
public:
/*
(递归)O(n^2)
这道题目和 Path Sum 基本一样,不同点在于需要把所有路径记录下来。
递归,自顶向下从根节点往叶节点走,每走过一个节点,就让 sum 减去该节点的值,则如果走到某个叶节点时,sum 恰好为0,则说明从根节点到这个叶节点的路径上的数的和等于 sum,此时需要把这条路径记录下来。
时间复杂度分析:除了记录路径这部分,每个节点仅被遍历一次,且维护 sum 和路径的复杂度都是 O(1),所以时间复杂度是 O(n);而记录路径这部分:n个节点的二叉树的叶子节点数最多是 O(n) 级别的,且路径的长度最多也是 O(n) 级别的,所以时间复杂度最坏是 O(n^2)。
*/
vector> ans;
vector path;
vector> pathSum(TreeNode* root, int sum) {
if (root) dfs(root, sum);
return ans;
}
void dfs(TreeNode* root, int sum) {//能进入dfs说明root必然存在
path.push_back(root->val);
sum -= root->val;//sum此时可能为正数,0,负数
if (!root->left && !root->right) {
if (!sum) ans.push_back(path);
/*
ans.push_back(path);这句话会把path复制一遍,path的长度是 O(n) 的,最坏情况下会复制 O(n) 次,所以总时间复杂度最坏是 O(n^2) 的。
*/
} else {
if (root->left) dfs(root->left, sum);
if (root->right) dfs(root->right, sum);
}
path.pop_back();
}
};
LeetCode 114. 二叉树展开为链表
/*
先定义右链:指一棵子树最右侧的路径。
我们从根节点开始迭代,每次将当前节点的左子树的右链,插入当前节点的右链,
时间复杂度分析:虽然有两重循环,但外层循环会将所有节点遍历一次,内层循环会将除了根节点之外的其他内部结点的右链遍历一次,所以每个节点最多被遍历两次,所以时间复杂度是 O(n)
*/
class Solution {
public:
void flatten(TreeNode* root) {
while (root) {
auto p = root->left;
if (p) {
while (p->right) p = p->right;
p->right = root->right;
root->right = root->left;
root->left = NULL;
}
root = root->right;
}
}
};
LeetCode 116. 填充每个节点的下一个右侧节点指针
/*
(BFS,树的遍历) O(n)
从根节点开始宽度优先遍历,每次遍历一层,遍历时按从左到右的顺序,对于每个节点,先让左儿子指向右儿子,然后让右儿子指向下一个节点的左儿子。最后让这一层最右侧的节点指向NULL。
遍历到叶节点所在的层为止。
为了便于理解,我们模拟一下样例的操作流程:
第一步,遍历根节点,我们让根节点的左儿子指向右儿子,即让2指向3;
第二步,从左到右遍历第二层,先遍历2,让2的左儿子指向右儿子,即让4指向5,再让2的右儿子指向下一个节点的左儿子,即5指向6;然后遍历3,依次类推;
第三步,我们发现第三层已经是叶节点,算法终止;
时间复杂度分析:每个节点仅会遍历一次,遍历时修改指针的时间复杂度是 O(1),所以总时间复杂度是 O(n)。
*/
class Solution {
public:
Node* connect(Node* root) {
if (!root) return root;
auto source = root;
while (root->left) {
for (auto p = root; p; p = p->next) {
p->left->next = p->right;//初始状态下,所有 next 指针都被设置为 NULL。
if (p->next) p->right->next = p->next->left;
}
root = root->left;
}
return source;
}
};
LeetCode 117. 填充每个节点的下一个右侧节点指针 II
class Solution {
public:
/*
不同点在于这道题目的输入数据不一定是一棵完美二叉树。
*/
Node* connect(Node* root) {
if (!root) return root;
auto cur = root;
while (cur) {
auto head = new Node(-1);
auto tail = head;
for (auto p = cur; p; p = p->next) {
if (p->left) tail = tail->next = p->left;
if (p->right) tail = tail->next = p->right;
}
cur = head->next;
}
return root;
}
};
LeetCode 118. 杨辉三角
class Solution {
public:
/*
从上到下依次计算每一行;
对于每一行,先把1放在首尾两个位置,然后计算中间的数:f[i][j] = f[i-1][j-1] + f[i-1][j];
*/
vector> generate(int n) {
vector> f;
for (int i = 0; i < n; i ++ ) {
vector line(i + 1);//i为0表示第0行,有1个数,第i行有i+1个数
line[0] = line[i] = 1;//首位置1
for (int j = 1; j < i; j ++ )
//不用考虑i-1是否会越界,因为j初始化为1,j
LeetCode 119. 杨辉三角 II
class Solution {
public:
/*
上道题打印前n行,这道题只需要打印出来最后一行即可,另外要求只能用O(k)的空间复杂度,所以滚动数组
(递推,滚动数组) O(n^2)
一行一行计算,由于每行的值仅与上一行的值有关,所以可以用滚动数组优化。
计算每一行的值时,先将1放在首位两个位置,然后计算中间的数:f[i][j]=f[i-1][j-1]+f[i-1][j];
先写二维情况,另外注意题中所给样例,输入3,返回的是第四行,所以输入n,返回第n+1行
*/
vector getRow(int n) {
vector> f(n+1,vector(n+1));
for (int i = 0; i <= n; i ++ ) {
f[i][0]=f[i][i]= 1;//首位置1
for (int j = 1; j < i; j ++ )
f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
}
return f[n];
}
};
class Solution {
public:
vector getRow(int n) {
vector> f(2,vector(n+1));//变为2
for (int i = 0; i <= n; i ++ ) {
f[i&1][0]=f[i&1][i]= 1;//转成滚动数组非常简单,数组中&1即可
for (int j = 1; j < i; j ++ )
f[i&1][j] = f[i - 1&1][j - 1] + f[i - 1&1][j];//直接&1
}
return f[n&1];//&1
}
};
LeetCode 121. 买卖股票的最佳时机
class Solution {
public:
/*
这道题只用买一次,所以只需要看那两天的差值最大即可,一次扫描即可,枚举在哪天卖出
*/
int maxProfit(vector& prices) {
int res = 0;//初始化为0,局部变量初始化给定值
for (int i = 0, minp = INT_MAX; i < prices.size(); i ++ ) {
res = max(res, prices[i] - minp);//枚举在哪天卖出,minp维护之前遍历元素的最小值
//minp初始化为INT_MAX,这样可以使得 prices[0] - minp 无限大,从而保证第一个res只能取0。即f[0] = 0
minp = min(minp, prices[i]);//更新最小值
}
return res;
}
};
LeetCode 122. 买卖股票的最佳时机 II
class Solution {
public:
/*
上一道题只能买卖一次,这道题可以进行任意多次交易,但是不能同时参与多笔交易
计算所有的单天交易,把交易利润为正的全部加起来即可,因为如果在第i天买入,第j天卖出,等价于我在第i天买入,第i+1天卖出,然后在第i+1天买入,第i+2天卖出...
p[j]-p[i]=(p[i+1]-p[i])+(p[i+2]-p[i+1])...+(p[j]-p[j-1])
*/
int maxProfit(vector& prices) {
int res = 0;
for (int i = 0; i + 1 < prices.size(); i ++ )//枚举所有单天交易的第一天
res += max(0, prices[i + 1] - prices[i]);//把交易利润为正的全部加起来
return res;
}
};
LeetCode 123. 买卖股票的最佳时机 III
class Solution {
public:
/*
前后缀分解,枚举两次交易的分界点,比如可以枚举第二次交易买入的时间i
对于第一次交易,记录前i天中买卖一次的最大收益(不一定在第i天卖)
对于第二次交易,可以从后往前扫描,求[i,n−1]也就是第i+1天到第n天的最大值,在这一天卖出
*/
int maxProfit(vector& prices) {
int n = prices.size();
vector f(n + 2);
for (int i = 1, minp = INT_MAX; i <= n; i ++ ) {//f[i]就表示前i天的最大收益,下标从1开始
f[i] = max(f[i - 1], prices[i - 1] - minp);
//第i天卖或不卖,注意数组f和prices下标开始不同,后者从0开始
minp = min(minp, prices[i - 1]);//更新最小值
}
int res = 0;
for (int i = n, maxp = 0; i; i -- ) {
//res = max(res, maxp - prices[i - 1] + f[i-1]);
res = max(res, maxp - prices[i - 1] + f[i-1]);
//两笔交易第一次卖出和第二天买入能否在同一天,f[i-1]和f[i]都能ac
maxp = max(maxp, prices[i - 1]);//更新最大值
}
return res;
}
};
LeetCode 125. 验证回文串
class Solution {
public:
bool check(char c){
return c>='a'&&c<='z'||c>='A'&&c<='Z'||c>='0'&&c<='9';
}
bool isPalindrome(string s) {
if(s.empty()) return true;
for(int i=0,j=s.size()-1;i
LeetCode 128. 最长连续序列
class Solution {
public:
/*
这道题有很多做法,可以用哈希表,也可以用像并查集的链表,后者时间复杂度用路径压缩优化是O(logn),不满足要求
哈希表也有两种方式,一种是用哈希表维护区间
首先将所有数字放入哈希表,遍历哈希表中的元素,比如当前遍历到x,查看从x开始的最长连续序列有多长,这样直接做时间复杂度是O(n^2)
为了保证O(n)的复杂度,为了避免重复枚举序列,因此只对序列的起始数字向后枚举(例如[1,2,3,4],只对1枚举,2,3,4时跳过),因此需要判断一下是否是序列的起始数字(即判断一下n-1是否在哈希表中)。
也就是说遍历到数x时,查看x-1是否在哈希表中,如果x-1存在,那他一定不会是最长连续序列的起点,不枚举它
*/
int longestConsecutive(vector& nums) {
unordered_set S;
for (auto x: nums) S.insert(x);
int res = 0;
for (auto x: nums) {
if (S.count(x) && !S.count(x - 1)) {//只枚举起点
int y = x;
S.erase(x);//避免重复的元素
while (S.count(y + 1)) {//注意这里是while,y总写成if
y ++ ;
S.erase(y);
}
res = max(res, y - x + 1);
}
}
return res;
}
};
class Solution {
public:
/*
(哈希) O(n)
从前往后扫描整个数组,过程中用两个哈希表unordered_maptr_left, tr_right维护所有连续整数序列,两个哈希表分别将序列的左右端点映射成序列长度。
然后我们考虑如何维护哈希表:
当我们遍历到一个新的数 x 时,先查找 x 左右两边分别存在多长的连续序列,两个值分别是tr_right[x-1]和tr_left[x+1],分别记为left和right,此时我们可以将左右两部分和 x 拼起来,形成一个更长的连续整数序列,然后更新新序列的左右两端的值:
新序列的左端点是 x-left,更新哈希表:tr_left[x - left] = max(tr_left[x - left], left + 1 + right);
新序列的右端点是 x+right,更新哈希表:tr_right[x + right] = max(tr_right[x + right], left + 1 + right);
最后我们不要忘记用新序列的长度left+right+1更新答案。
时间复杂度分析:对于每个数,仅被遍历一次,且遍历时只涉及常数次哈希表的增改查操作,所以总时间复杂度是 O(n)
*/
int longestConsecutive(vector& nums) {
int res = 0;
unordered_map tr_left, tr_right;
for (auto & x : nums)
{
int left = tr_right[x - 1];
int right = tr_left[x + 1];
tr_left[x - left] = max(tr_left[x - left], left + 1 + right);
tr_right[x + right] = max(tr_right[x + right], left + 1 + right);
res = max(res, left + 1 + right);
}
return res;
}
};
class Solution {
public:
/*
并查集。我们可以将数组中数值相邻的元素使用并查集合并,最后看一下哪一个连通块最大,我们就可以求出所需要的答案了。在使用并查集时,额外需要注意的一点就是,可能会有重复元素,所以我们先使用set求出所有的不重复元素,对这些不重复的元素进行并查集操作。我们使用了一个map来存储数值和数组下标的映射。
*/
vector fa,size;
int getfa(int x)
{
if(x != fa[x]) fa[x] = getfa(fa[x]);
return fa[x];
}
void uni(int x, int y)
{
int fx = getfa(x),fy = getfa(y);
if(fx != fy)
{
if(size[fx] < size[fy])
{
fa[fx] = fy;
size[fy] += size[fx];
}else
{
fa[fy] = fx;
size[fx] += size[fy];
}
}
}
int longestConsecutive(vector& nums) {
int n = nums.size(),res = 0,i = 0;
unordered_set _set;
unordered_map hash;
for(int i = 0 ; i < n ; i ++) _set.insert(nums[i]);
n = _set.size();
vector arr(n,0);
for(auto x : _set) arr[i ++] = x;
fa = vector(n,0),size = vector(n,1);
for(int i = 0 ; i < n ; i ++)
{
fa[i] = i;
hash[arr[i]] = i;
}
for(int i = 0 ; i < n ; i ++)
{
if(hash.find(arr[i] + 1) != hash.end())
uni(i,hash[arr[i] + 1]);
if(hash.find(arr[i] - 1) != hash.end())
uni(i,hash[arr[i] - 1]);
}
for(int i = 0 ; i < n ; i ++)
if(getfa(i) == i) res = max(res,size[i]);
return res;
}
};
LeetCode 129. 求根到叶子节点数字之和
class Solution {
public:
/*
(树的遍历) O(n)
从根节点递归遍历整棵树,遍历时维护从根节点到该节点的路径表示的数,当遍历到叶节点时,将路径表示的数累加到答案中。
时间复杂度分析:每个节点仅被遍历一遍,所以时间复杂度是 O(n)。
*/
int ans = 0;
int sumNumbers(TreeNode* root) {
if (root) dfs(root, 0);
return ans;
}
void dfs(TreeNode* root, int number) {
number = number * 10 + root->val;
if (!root->left && !root->right) ans += number;
if (root->left) dfs(root->left, number);
if (root->right) dfs(root->right, number);
}
};
LeetCode 131. 分割回文串
/*
首先所有不同分割方案的数量是指数级别,因为最坏情况下所给字符串n个字符都相同,总共有n-1个分割点,分割方案总数是2^(n-1),所以基本可以断定是暴力搜索,暴力搜索时可以加一些优化,先预处理出所给字符串任意一段子串是否为回文串,这样搜索过程中直接查表就行
递推+暴力搜索
*/
class Solution {
public:
vector> f;//预处理数组,定义为全局变量,这样深搜的时候可以直接使用
vector> ans;//全局变量,记录所有方案
vector path;//表示当前方案
vector> partition(string s) {
int n = s.size();
f = vector>(n, vector(n));
/*递推时候,判断f[i][j]时要确保f[i+1][j-1]已经确定,也就是要确保拓扑序,外层循环是从小到大枚举j,所以f[i + 1, j - 1]会在f[i, j]之前被计算出来*/
// for (int j = 0; j < n; j ++ )
// for (int i = 0; i <= j; i ++ )
// if (i == j) f[i][j] = true;
// else if (s[i] == s[j]) {
// //要么只有两个字符
// if (i + 1 > j - 1 || f[i + 1][j - 1]) f[i][j] = true;
// }
//以下这种形式是第五题最长回文子串的区间dp写法
for(int len = 0; len < n; ++len){//len表示j与i相差几个元素
for(int i = 0; i+len < n; ++i){
int j = i + len;
f[i][j] = s[i]==s[j];
if(len > 1)//len==0或1表示区间只有一个或两个元素,严格大于1表示区间至少有三个元素
f[i][j] = f[i+1][j-1] && f[i][j];
}
}
dfs(s, 0);//从第0个字母开始暴力搜索
return ans;
}
void dfs(string& s, int u) {//u表示当前搜到第几位
if (u == s.size()) ans.push_back(path);//搜索结束
else {//否则枚举下一段终点是什么,下一段起点固定是u,枚举终点i
for (int i = u; i < s.size(); i ++ )//枚举下一段终点
if (f[u][i]) {//如果[u,i]是回文串的话
path.push_back(s.substr(u, i - u + 1));//就把[u,i]这一段加到path中
dfs(s, i + 1);//再此基础上继续探索
path.pop_back();//恢复现场
}
}
}
};
LeetCode 132. 分割回文串 II
/*
这道题不是返回所有分割方案,而是返回最少的分割次数,也就是最少的分割部分-1,
f[i]表示s[1,i]的分割方案的最小值
状态计算:根据所有方案的最后一步分为若干种情况,最后一段都以i结尾,区别是最后一段是哪段,一共有i类,分别是[1,i], [2,i],...,[i,i],所以只需求出每一类最小值取min,比如某一类[k,i],前面[1.k-1]任意分段,最后一段确定是[k,i],因此它对应的最小值是f(k-1)+1
*/
class Solution {
public:
int minCut(string s) {
int n = s.size();
s = ' ' + s;//让下标从1开始,因为下面dp计算过程中要用到前一类的状态
vector> g(n + 1, vector(n + 1));
vector f(n + 1, 1e8);//求最小值,初始化为正无穷
// for (int j = 1; j <= n; j ++ )//预处理任意一段子串是否为回文串,注意先循环后面的j
// for (int i = 1; i <= n; i ++ )
// if (i == j) g[i][j] = true;
// else if (s[i] == s[j]) {
// if (i + 1 > j - 1 || g[i + 1][j - 1]) g[i][j] = true;
// }
for(int len = 0; len < n; ++len){//len表示j与i相差几个元素
for(int i = 1; i+len <= n; ++i){
int j = i + len;
g[i][j] = s[i]==s[j];
if(len > 1)//len==0或1表示区间只有一个或两个元素,严格大于1表示区间至少有三个元素
g[i][j] = g[i+1][j-1] && g[i][j];
}
}
f[0] = 0;
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= i; j ++ )//枚举最后一段的起点
if (g[j][i])
f[i] = min(f[i], f[j - 1] + 1);
}
return f[n] - 1;//f(n)表示最少分为多少部分,-1之后表示最少分割次数
}
};
LeetCode 134. 加油站
class Solution {
public:
/*
枚举每一个加油站作为起点,判断从该起点开始能否换绕一圈。
*/
int canCompleteCircuit(vector& gas, vector& cost) {
int n = gas.size();
for(int i = 0 ; i < n ; i ++)
{
int amount = 0 ,k = 0;
for(k = 0 ; k < n ; k ++)
{
amount += gas[(i + k) % n] - cost[(i + k) % n];
if(amount < 0) break;
}
if(k == n) return i;
}
return -1;
}
};
class Solution {
public:
/*
单调队列 时空O(n) AcWing 1088. 旅行问题 提高课
先枚举再优化 如果从i开始,走不到j,中间的任何站开始走,都走不到j
*/
int canCompleteCircuit(vector& gas, vector& cost) {
int n = gas.size();
for (int i = 0, j; i < n; ) { // 枚举起点
int left = 0;//剩余油量
for (j = 0; j < n; j ++ ) {//枚举从i开始,走0个,一直到走n-1个
int k = (i + j) % n;//从i走j个站到达k
left += gas[k] - cost[k];
if (left < 0) break;
}
if (j == n) return i;
i = i + j + 1;
}
return -1;
}
};
LeetCode 135. 分发糖果
class Solution {
public:
/*
(两次遍历) O(n)
我们分两次来考虑这个问题,我们使用两个数组分别表示仅考虑左边的孩子和仅考虑右边的孩子的结果。
首先给每个人都发一颗糖果,然后从左往右遍历,如果当前人表现大于左边孩子表现,那么f[i] = f[i - 1] + 1。
同样给每个人都发一颗糖果,然后从右往左遍历,如果当前人表现大于右边孩子表现,那么g[i] = g[i + 1] + 1。
为了满足上述条件,每个孩子应该发的糖果就是max(f[i],g[i]);
*/
int candy(vector& ratings) {
int n = ratings.size(),res = 0;
if(n < 2) return n;
vector f(n,1),g(n,1);
for(int i = 1 ; i < n ; i ++)
if(ratings[i] > ratings[i - 1])
f[i] = f[i - 1] + 1;
for(int i = n - 2 ; i >= 0 ; i --)
if(ratings[i] > ratings[i + 1])
g[i] = g[i + 1] + 1;
for(int i = 0 ; i < n ; i ++)
res += max(f[i],g[i]);//取最大值是因为又要满足左边,又要满足右边
return res;
}
};
class Solution {
public:
/*
这道题是动态规划记忆化搜索:滑雪的简化版,二维变一维
这道题求得是分发糖果总数最少,比如给每个小朋友分发糖果数量为f[i],我们希望的是所有的f[i]之和最小
这样和第一个做法两次遍历很像,只是写法不一样
每个小朋友都至少分一颗糖果,每可以往左边多走一步,就可以多分一颗糖果,所以它可以往左边最多走多少步,就可以多分多少克糖果,右边同理,两者取最大值,因为要保证左右两边都可以走
*/
vector f;//表示最小步数
vector w;//小朋友评分,全局变量以便于递归可直接使用
int n;
int candy(vector& ratings) {
n = ratings.size();
w = ratings;
f.resize(n, -1);//记忆化搜索注意初始化,-1表示未搜索过
int res = 0;
for (int i = 0; i < n; i ++ ) res += dp(i);
return res;
}
int dp(int x) {
if (f[x] != -1) return f[x];//已经计算过
f[x] = 1;//至少分一颗糖果,每可以多往任意一边走一步,就可以多分一颗糖果
//x>0表示它可能可以往左边走 w[x - 1] < w[x]表示它确实可以往左边走一步
if (x && w[x - 1] < w[x]) f[x] = max(f[x], dp(x - 1) + 1);
if (x + 1 < n && w[x + 1] < w[x]) f[x] = max(f[x], dp(x + 1) + 1);
return f[x];
}
};
class Solution {
public:
/*
(图论) O(n)
我们可以把这个问题建模成图论问题。
假设每个位置都是一个点,如果相邻两个点的 ratings 不等,则从 ratings 较小的点向较大的点连一条边权为 1 的边。
假设我们有一个超级源点 S,向每个位置都连接了边权为 1 的边,求从 S 到每个点的最长路。共有 n + 1 个点,最多有 2n - 1 条边。
这里相当于求带负权边的最短路,只能用 Bellman-Ford 求解,但 Bellman-Ford 的理论时间复杂度为 O(nm)。
我们发现,第 0 轮 Bellman-Ford 迭代会将全部 n 个点的距离更新为 1。所以我们就可以当做求每个点初值为 1 的最长路。
接下来,每个点的度数不超过 2,且边都是连向了左右邻居,所以我们可以仅用额外两轮迭代完成整个图的更新。
第 1 轮迭代,从点 0 到点 n-1 更新;第 2 轮迭代,从点 n-1 到点 0 更新。
时间复杂度
两轮线性扫描,故时间复杂度为 O(n)。
空间复杂度
需要 O(n) 的额外空间存储答案。
*/
int candy(vector& ratings) {
int n = ratings.size();
vector candies(n, 1);
for (int i = 0; i < n - 1; i++)
if (ratings[i + 1] > ratings[i])
candies[i + 1] = candies[i] + 1;
for (int i = n - 1; i >= 1; i--)
if (ratings[i - 1] > ratings[i] && candies[i - 1] <= candies[i])
candies[i - 1] = candies[i] + 1;
int ans = 0;
for (int x : candies)
ans += x;
return ans;
}
};
class Solution {
public:
/*
(一次遍历不使用额外空间) O(n)
我们可以将每个孩子的表现化成一个折线图,那么我们可以发现在上升过程中,肯定分配的糖果个数是[1,2,3,..k],如果是下降过程中肯定是[t,t-1,...,1]这样的趋势才能获得最小值。那么我们把整个走势图划分成若干个先上升再下降的山,那么不考虑山顶元素上山过程就是[1,2,..,k],下山就是[t,t-1,...,2,1],那么山顶就是max(k + t) + 1。不管在上山过程中还是下山过程中遇到了平地(ratings[i] == ratings[i - 1]),那么我们认为这座山已经提前结束。
根据上述思想,我们使用up,down两个变量分别代表上山的高度和下山的高度,old_state代表上一个状态,
1代表在上山,0代表在平地,-1代表在下山。接下来我们考虑什么时候山已经结束了:
连续两个平地(连续三个孩子的rating值相等),那么只需要给中间孩子分配一个糖果就可以了res ++。
上山过程中遇到了平地,那我们只需要从上山第一个位置分配一个,然后依次递增到山顶就可以了,这个时候因为rating[i - 1]是上一个山的山顶同时不会作为下一个山的起点,不会发生重复计算。
下山过程中遇到了平地,那我们只需要从两边山脚分配一个,然后依次递增到山顶,山顶分配的是max(up,down) + 1。这个时候因为rating[i - 1]是上一个山的山脚同时不会作为下一个山的起点,不会发生重复计算。
下山过程中遇到了上山,那我们只需要从两边山脚分配一个,然后依次递增到山顶,山顶分配的是max(up,down) + 1。这个时候因为rating[i - 1]是上一个山的山脚同时作为下一个山的起点,发生重复计算我们需要减去 1。
遍历完整个数组之后,将最后一座山的值加入答案。
*/
int count(int n){
return n * (n + 1) / 2;
}
int candy(vector& ratings)
{
int n = ratings.size();
if(n < 2) return n;
int res = 0,up = 0,down = 0,old_state = 0;
for(int i = 1 ; i < n ; i ++)
{
int new_state = (ratings[i] > ratings[i - 1]) ? 1 : (ratings[i] < ratings[i - 1] ? -1 : 0);
if(new_state == 0 && old_state == 0) res ++;
else if(new_state == 0 && old_state > 0)
{
res += count(up) + up + 1;
up = 0,down = 0;
}
else if(new_state == 0 && old_state < 0)
{
res += count(up) + count(down) + max(up,down) + 1;
up = 0,down = 0;
}else if(new_state > 0 && old_state < 0)
{
res += count(up) + count(down) + max(up,down) + 1 - 1;
up = 0,down = 0;
}
if(new_state > 0) up ++;
else if(new_state < 0) down ++;
old_state = new_state;
}
res += count(up) + count(down) + max(up,down) + 1;
return res;
}
};
class Solution {
public:
/*
代码优化
*/
int count(int n){
return n * (n + 1) / 2;
}
int candy(vector& ratings)
{
int n = ratings.size();
if(n < 2) return n;
int res = 0,up = 0,down = 0,old_state = 0;
for(int i = 1 ; i < n ; i ++)
{
int new_state = (ratings[i] > ratings[i - 1]) ? 1 : (ratings[i] < ratings[i - 1] ? -1 : 0);
if((old_state > 0 && new_state == 0) || (old_state < 0 && new_state >= 0))
{
res += count(up) + count(down) + max(up,down);
up = 0,down = 0;
}
if(new_state > 0) up ++;
if(new_state < 0) down ++;
if(new_state == 0) res ++;
old_state = new_state;
}
res += count(up) + count(down) + max(up,down) + 1;
return res;
}
};
LeetCode 136. 只出现一次的数字
/*
考察异或运算,异或运算具有交换律,结合律,两个相同的数异或之后是0,因为具有交换律,所以可以把相同的数交换到一起,所以只要遍历异或所有数,出现两次的数异或为0,只出现一次的数只剩它自己
*/
class Solution {
public:
int singleNumber(vector& nums) {
int res = 0;
for (auto x: nums) res ^= x;
return res;
}
};
LeetCode 137. 只出现一次的数字 II
/*
位运算,输入类型为整型,有32位,位运算是一个并行的算法,每一位独立做运算,所以每次做的时候只用考虑一位,考虑最终答案的每一位是0还是1,以某一位为例,最终答案在这一位上是0还是1,因为除了这个数之外其他的数都出现了三次,所以如果最终答案在这一位上是0的话,那么所有数在这一位上1的个数一定是三的倍数,否则为3的倍数+1,所以只需要统计在每一位上所有数总共1的个数,所以可以用一个DFA状态机的思路,把每一位上所有数总共1的个数对3取模之后余0,1,2看作三个状态,每次来一位之后,转移下状态,来一个数如果这一位是0就留在这个状态,否则就0-1-2-0-1-2这样变化。
因为要表示3个状态,所以至少要有两个二进制位,让00表示0,01表示1,10表示2,只要设计一种运算让它满足上面的转换即可
状态01是由two、one共同决定,所以要找余1的位应该找two为0并且one为1的位,但循环结束two为0,所以one就是要找的结果
*/
class Solution {
public:
int singleNumber(vector& nums) {
int two = 0, one = 0;
for (auto x: nums) {
one = (one ^ x) & ~two;
two = (two ^ x) & ~one;
}
return one;
}
};
class Solution {
public:
int singleNumber(vector& nums) {
int ans = 0;
for (int bit = 0; bit < 32; bit++) {
int counter = 0;
for (int i = 0; i < nums.size(); i++)
counter += (nums[i] >> bit) & 1;
ans += (counter % 3) << bit;
}
return ans;
}
};
LeetCode 138. 复制带随机指针的链表
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
for (auto p = head; p; p = p->next->next) { // 复制一个小弟
auto q = new Node(p->val);
q->next = p->next;
p->next = q;
}
// 复制random指针
for (auto p = head; p; p = p->next->next)
if (p->random)
p->next->random = p->random->next;//p的小弟指向p->random的小弟
// 拆分两个链表
auto dummy = new Node(-1), cur = dummy;
for (auto p = head; p; p = p->next) {
auto q = p->next;
cur = cur->next = q;
p->next = q->next;
}
return dummy->next;
}
};
LeetCode 141. 环形链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
/*
给一个链表,判断是否有环,最简单做法就是开一个哈希表,看遍历过程中是否出现重复点,但这样时间空间复杂度都是O(n),换,尝试双指针思路的一种,快慢指针,两根指针往后走的速度不一样,,快指针每次走两步,慢指针每次走一步
如果有环,两根指针必然相遇,无环的话一定会走到空节点
假设链表存在环,则当第一个指针走到环入口时,第二个指针已经走到环上的某个位置,距离环入口还差 xx 步。
由于第二个指针每次比第一个指针多走一步,所以第一个指针再走 xx步,两个指针就相遇了。
时间复杂度分析:第一个指针在环上走不到一圈,所以第一个指针走的总步数小于链表总长度。而第二个指针走的路程是第一个指针的两倍,所以总时间复杂度是 O(n)
时间复杂度:
无环:快指针走完,慢指针走一半,O(n)
有环:最坏也是O(n)
*/
if (!head || !head->next) return false;
auto fast = head, slow = head;
while (fast->next && fast->next->next){
fast = fast->next->next;
slow = slow->next;
if (slow == fast) return true;
}
return false;
}
};
LeetCode 142. 环形链表 II
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
/*
如果有环的话,返回环的入口,和上道题很像,只是在相遇之后要继续一些操作
用两个指针 first,second 分别从起点开始走,firstfirst 每次走一步,second 每次走两步。
如果过程中 second 走到null,则说明不存在环。否则当 first 和 second 相遇后,让 first 返回起点,second 待在原地不动,然后两个指针每次分别走一步,当相遇时,相遇点就是环的入口。
*/
if (!head) return nullptr;
ListNode *fast = head, *slow = head;
while (true){
if (!fast->next || !fast->next->next)
return nullptr;
fast = fast->next->next;
slow = slow->next;
if (slow == fast) break;
}
/*
不能直接这样写,因为这样跳出循环要么是两指针相遇,要么是环不存在,上道题中因为相遇时直接return true,所以因为环不存在而跳出的话能判断出来
如果这道题这样写的话,无法判断出跳出循环时因为什么
while (fast->next && fast->next->next){
fast = fast->next->next;
slow = slow->next;
if (slow == fast) return true;
}
*/
slow = head;
while (slow != fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
LeetCode 143. 重排链表
/**
* 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:
/*
整个链表总共扫描三次,第一次求总长度,第二次将后半段反向,第三次将后半段交替插入前半段,所以总时间复杂度是 O(n)
如果链表长度为奇数,让中间点归到左边,所以中点是n/2上取整,也就是(n+1)/2下取整
*/
void reorderList(ListNode* head) {
if (!head) return;
int n = 0;
for (auto p = head; p; p = p->next) n ++ ;//求总长度
auto mid = head;
//到达第(n + 1) / 2也就是下标为(n + 1) / 2 - 1的点,从头结点开始走(n + 1) / 2 - 1步到达
for (int i = 0; i < (n + 1) / 2 - 1; i ++ ) mid = mid->next;
auto a = mid, b = a->next;//将后面翻转,还剩下n/2下取整个点
for (int i = 0; i < n / 2; i ++ ) {
auto c = b->next;
b->next = a, a = b, b = c;
}
auto p = head, q = a;//各自指向头结点
for (int i = 0; i < n / 2; i ++ ) {//总共交错n/2次,每次需要将q插到p与p->next之间
auto o = q->next;
q->next = p->next;
p->next = q;
p = q->next, q = o;
}
//将最后一个点next指向空
if (n % 2) mid->next = NULL;
else mid->next->next = NULL;
}
};
LeetCode 144. 二叉树的前序遍历
class Solution {
public:
vector ans;
vector preorderTraversal(TreeNode* root) {
dfs(root);
return ans;
}
void dfs(TreeNode* root) {
if (!root) return;
ans.push_back(root->val);
dfs(root->left);
dfs(root->right);
}
};
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
if (!root) return {};
stack stk;
vector res;
while (true){
while (root){
res.push_back(root->val);
if (root->right)
stk.push(root->right);
root = root->left;
}
if (stk.empty()) break;
root = stk.top(); stk.pop();
}
return res;
}
};
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
vector res;
stack stk;
while (root || stk.size()) {
while (root) {
res.push_back(root->val);
stk.push(root);
root = root->left;
}
root = stk.top()->right;
stk.pop();
}
return res;
}
};
LeetCode 145. 二叉树的后序遍历
class Solution {
public:
/*
前序遍历是先遍历根节点再遍历左子树,右子树
后:左子树,右子树,根节点
用前序遍历的方式遍历根右左的顺序,这样得到的是后序遍历镜像的顺序
*/
vector postorderTraversal(TreeNode* root) {
vector res;
stack stk;
while (root || stk.size()) {
while (root) {
res.push_back(root->val);
stk.push(root);
root = root->right;
}
root = stk.top()->left;
stk.pop();
}
reverse(res.begin(), res.end());
return res;
}
};
class Solution {
private:
vector res;
public:
vector postorderTraversal(TreeNode* root) {
if (!root) return {};
dfs(root);
return res;
}
void dfs(TreeNode *root){
if (!root) return;
dfs(root->left);
dfs(root->right);
res.push_back(root->val);
}
};
LeetCode 146. LRU缓存机制
class LRUCache {
public:
/*
可以把缓存看作一个哈希表,不断往里插的过程中有容量限制
对于每一个坑位,记录它最近一次被使用的时间是什么,给一个时间戳,get或put都算用过它
删掉最早一个没有被用过的
(双链表+哈希) O(1)
使用一个双链表和一个哈希表:
哈希表维护整个缓存,我们要能够修改每个位置的时间戳,同时要能够快速找到时间戳最小的
我们可以用链表,每次一旦用过某个位置的数,就把它删掉然后放在链表头部,这样链表尾部元素一定是最久没有被用过的
所以我们要支持两个操作,分别是快速删除某个位置的数,另外是将一个节点插入到最左侧,双链表可以支持这个操作,单链表不能在O(1)时间内删除
双链表最近使用时间从左到右排好序,越往左就是越近被使用,每次需要删除时,删掉最后一个节点即可
哈希表存储key对应的链表中的节点地址;
时间复杂度分析:双链表和哈希表的增删改查操作的时间复杂度都是 O(1),所以get和set操作的时间复杂度也都是 O(1)
*/
struct Node {//定义双链表节点
int key, val;
Node *left, *right;
Node(int _key, int _val): key(_key), val(_val), left(NULL), right(NULL) {}//初始化
}*L, *R;//双链表定义两个左右端点
unordered_map hash;//定义哈希表从key映射到节点
int n;
void remove(Node* p) {//删除节点p
p->right->left = p->left;
p->left->right = p->right;
}
void insert(Node* p) {//插入到最左侧
p->right = L->right;
p->left = L;
L->right->left = p;
L->right = p;
}
LRUCache(int capacity) {
n = capacity;
L = new Node(-1, -1), R = new Node(-1, -1);//定义左右端点
L->right = R, R->left = L;//两端点相连
}
int get(int key) {//从缓存中得到数据
if (hash.count(key) == 0) return -1;//不存在直接返回-1
auto p = hash[key];
remove(p);//这时,这个节点已经被使用过,需要抽出来放到最左边
insert(p);
return p->val;
}
void put(int key, int value) {//向缓存中写数据
if (hash.count(key)) {//存在的话更新数据
auto p = hash[key];
p->val = value;
remove(p);
insert(p);
} else {//不存在就创建节点,往缓存放东西的时候先查看有没有空间让你放
if (hash.size() == n) {//缓存已满
auto p = R->left;//删除最左侧的节点,注意双链表一开始对应的L,R是维护边界的
remove(p);
hash.erase(p->key);
delete p;
}
auto p = new Node(key, value);
hash[key] = p;
insert(p);//插入节点
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
LeetCode 147. 对链表进行插入排序
/*
对链表做插入排序,每次找当前数应该插的位置,要找到第一个大于它的位置
时间复杂度分析:一共遍历n个节点,对于每个节点找合适位置时,最多需要遍历O(n)次,所以总时间复杂度是O(n^2)
*/
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
auto dummy = new ListNode(-1);
for (auto p = head; p;) {//遍历链表
auto cur = dummy, next = p->next;//每次遍历都重新把cur指向dummy
while (cur->next && cur->next->val <= p->val) cur = cur->next;//找到排好序的链表中第一个大于它的点
p->next = cur->next;
cur->next = p;
p = next;
}
return dummy->next;
}
};
LeetCode 148. 排序链表
/*
快速排序:时间复杂度平均情况O(nlogn),空间O(logn),因为需要递归开辟栈
归并排序:时间复杂度O(nlogn),如果递归写法,也需要开辟栈,所以空间O(logn),如果迭代写法,可以做到O(1)
*/
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* sortList(ListNode* head) {
int n = 0;//先求链表长度
for (auto p = head; p; p = p->next) n ++ ;
for (int i = 1; i < n; i *= 2) {//从最底层开始往上,i表示每一层的区间长度
auto dummy = new ListNode(-1), cur = dummy;//因为每一层头结点可能会变,所以要定义虚拟头结点
for (int j = 1; j <= n; j += i * 2) {//每次枚举相邻两个长度是i的区间,j表示两个待合并区间的起点
auto p = head, q = p;//要先找到两个区间的头结点,p就是head,q要从p往后走i步
for (int k = 0; k < i && q; k ++ ) q = q->next;
auto o = q;//o表示下一对相邻区间的起点
for (int k = 0; k < i && o; k ++ ) o = o->next;
int l = 0, r = 0;
while (l < i && r < i && p && q)
if (p->val <= q->val) cur = cur->next = p, p = p->next, l ++ ;
else cur = cur->next = q, q = q->next, r ++ ;
while (l < i && p) cur = cur->next = p, p = p->next, l ++ ;
while (r < i && q) cur = cur->next = q, q = q->next, r ++ ;
head = o;
}
cur->next = NULL;
head = dummy->next;
}
return head;
}
};
LeetCode 150. 逆波兰表达式求值
class Solution {
public:
int evalRPN(vector& tokens) {
stack stk;
for (auto& s: tokens)
if (s == "+" || s == "-" || s == "*" || s == "/") {
auto b = stk.top(); stk.pop();
auto a = stk.top(); stk.pop();
if (s == "+") a += b;
else if (s == "-") a -= b;
else if (s == "*") a *= b;
else a /= b;
stk.push(a);
} else stk.push(stoi(s));
return stk.top();
}
};
LeetCode 151. 翻转字符串里的单词
/*
翻转时需要删掉多余空格,每两个单词之间只能有一个空格,不开额外数组
操作分解
先将所有单词翻转,最后再将整体翻转,操作过程中注意去掉空格
*/
class Solution {
public:
string reverseWords(string s) {
int k = 0;//k表示去掉空格之后新的字符串每个单词的首元素下标
for (int i = 0; i < s.size(); i ++ ) {//枚举所有字符,
if (s[i] == ' ') continue;//过滤所有空格
int j = i, t = k;//把从i开始的单词找出来,j初始化为从i开始,不要从i+1开始
while (j < s.size() && s[j] != ' ') s[t ++ ] = s[j ++ ];//边找边把这个单词记到数组当中的最前面
//跳出循环时,j和k都指向对应单词的最后一个字符的下一个字符
reverse(s.begin() + k, s.begin() + t);//把每个单词翻转
s[t ++ ] = ' ';
k = t, i = j;
}
if (k) k -- ;//k>0表明至少有一个单词,k--之后指向最后一个确定的单词的后面的空格
s.erase(s.begin() + k, s.end());//就地操作,删掉后面所有的空格
reverse(s.begin(), s.end());//整体翻转
return s;
}
};
LeetCode 152. 乘积最大子数组
/*
本质上是一个dp问题,首先考虑暴力如何做,枚举区间的起始终止位置,循环求出区间的总乘积,这样做是O(n^3),考虑如何优化,考虑用dp优化,能否记录一些中间结果
考虑枚举完所有终点是i-1的子区间之后,考虑终点是i的子区间的乘积最大值和终点是i-1的子区间乘积最大值有没有什么关系
又因为乘法正负问题,比如第i个数为负的话,我们希望找到终点是i-1的子区间乘积最小值,负的最大值,这样两者相乘得正的最大值,所以用f(i-1)和g(i-1)表示终点是i-1的子区间乘积最大值和最小值
所以我们要考虑的就是已知f(i-1)和g(i-1),能否推出f(i)和g(i)
求f(i):
终点是i的子区间的乘积最大值可能只有a(i)一个元素,也可能包含前面一些元素
如果包含前面一些元素的话,当a(i)为正时,要得到最大值应该乘f(i-1),当a(i)为负时,要得到最大值应该乘g(i-1),为0时只能为0
求g(i):
终点是i的子区间的乘积最小值可能只有a(i)一个元素,也可能包含前面一些元素
如果包含前面一些元素的话,当a(i)为正时,要得到最小值应该乘g(i-1),当a(i)为负时,要得到最小值应该乘f(i-1),为0时只能为0
所以可以把所有f(i),g(i)递推出来,最后对所有f(i)取max
要开两个数组,空间复杂度是O(n),但是由于f(i)和g(i)只与f(i-1)和g(i-1)相关,所以可以用滚动数组求所有状态,用两个变量维护即可,而且计算时,没必要分情况怎么样,因为求f(i)就是三种情况取max,求g(i)就是三种情况取min
*/
class Solution {
public:
int maxProduct(vector& nums) {
int res = nums[0];
int f = nums[0], g = nums[0];
for (int i = 1; i < nums.size(); i ++ ) {
int a = nums[i], fa = f * a, ga = g * a;
f = max(a, max(fa, ga));
g = min(a, min(fa, ga));
res = max(res, f);
}
return res;
}
};
LeetCode 153. 寻找旋转排序数组中的最小值
class Solution {
public:
int findMin(vector& nums) {
/*
边界情况排除后,因为数组单调递增,二分查找小于nums[0]的第一个元素
*/
if (nums.back() > nums[0]) return nums[0];//边界情况,没有旋转
int l = 0, r = nums.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] < nums[0]) r = mid ;
else l = mid+1;
}
return nums[l];
}
};
LeetCode 154. 寻找旋转排序数组中的最小值 II
class Solution {
public:
int findMin(vector& nums) {
/*
因为数组中有重复元素,所以不考虑未旋转的边界情况,如果某个元素严格不等于nums[0],
都好判断它在上半段还是下半段,但是当它等于nums[0]时,不确定
所以可以先将右边界与nums[0]相同的元素一个个删掉,这样可以保证下半段所有元素严格小于nums[0]
*/
int l = 0, r = nums.size() - 1;
while (l < r && nums[r] == nums[l]) r -- ;
if (nums[0] <= nums[r]) return nums[0];
while (l < r)
{
int mid = (l + r ) / 2;
if (nums[mid] < nums[0]) r = mid;
else l = mid + 1;
}
return nums[r];
}
};
LeetCode 155. 最小栈
class MinStack {
public:
/** initialize your data structure here. */
stack stackValue;
stack stackMin;
MinStack() {
}
/*
除了维护基本的栈结构之外,还需要维护一个单调栈,来实现返回最小值的操作。
当我们向栈中压入一个数时,如果该数 ≤≤ 单调栈的栈顶元素,则将该数同时压入单调栈中;否则,不压入
从栈中弹出一个数时,如果该数等于单调栈的栈顶元素,则同时将单调栈的栈顶元素弹出。
单调栈的栈顶元素,就是当前栈中的最小数。
*/
void push(int x) {
stackValue.push(x);
if (stackMin.empty() || stackMin.top() >= x)
stackMin.push(x);
}
void pop() {
if (stackMin.top() == stackValue.top()) stackMin.pop();
stackValue.pop();
}
int top() {
return stackValue.top();
}
int getMin() {
return stackMin.top();
}
};
class MinStack {
public:
/** initialize your data structure here. */
stack> st;
/*
一个栈同时保存每个数字 x 进栈的时候的值 与 插入该值后的栈内最小值
*/
MinStack() {
}
void push(int x) {
if (st.size() == 0) {
st.push({x, x});
} else {
st.push({x, min(x, st.top().second)});
}
}
void pop() {
st.pop();
}
int top() {
return st.top().first;
}
int getMin() {
return st.top().second;
}
};
LeetCode 160. 相交链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
/*
用两个指针分别从两个链表头部开始扫描,每次分别走一步;
如果指针走到null,则从另一个链表头部开始走;
当两个指针相同时,
如果指针不是null,则指针位置就是相遇点;
如果指针是 null,则两个链表不相交;
*/
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto p = headA, q = headB;
while (p != q) {
p = p ? p->next : headB;
q = q ? q->next : headA;
}
return p;
}
};
LeetCode 165. 比较版本号
class Solution {
public:
/*
进行字典序比较,也就是从前往后分别抠出来每一个版本对应的数进行比较即可
*/
int compareVersion(string v1, string v2) {
for (int i = 0, j = 0; i < v1.size() || j < v2.size();) {//ij分别指向两个版本
int a = i, b = j;
while (a < v1.size() && v1[a] != '.') a ++ ;//跳出循环后从i到a-1的部分是第一版本的当前数
while (b < v2.size() && v2[b] != '.') b ++ ;
int x = a == i ? 0 : stoi(v1.substr(i, a - i));
int y = b == j ? 0 : stoi(v2.substr(j, b - j));
//xy是待比较的两个数
if (x > y) return 1;
if (x < y) return -1;
i = a + 1, j = b + 1;//继续判断,ab此时指向点,所以跳过
}
return 0;
}
};
LeetCode 166. 分数到小数
class Solution {
public:
/*
(模拟,高精度除法) O(n)
为了方便处理,我们先将所有负数运算转化为正数运算。
然后再算出分数的整数部分,再将精力集中在小数部分。
计算小数部分的难点在于如何判断是否是循环小数,以及找出循环节的位置。
回忆手工计算除法的过程,每次将余数乘10再除以除数,当同一个余数出现两次时,我们就找到了循环节。
所以我们可以用一个哈希表 unordered_map 记录所有余数所对应的商在小数点后第几位,当计算到相同的余数时,上一次余数的位置和当前位置之间的数,就是该小数的循环节。
时间复杂度分析:计算量与结果长度成正比,是线性的。所以时间复杂度是 O(n)
*/
string fractionToDecimal(int numerator, int denominator) {
typedef long long LL;//比如分子为-2^31,分母为-1,结果为2^31溢出
LL x = numerator, y = denominator;
if (x % y == 0) return to_string(x / y);
string res;
if ((x < 0) ^ (y < 0)) res += '-';
x = abs(x), y = abs(y);
res += to_string(x / y) + '.', x %= y;
unordered_map hash;//哈希表记录所有余数所对应的商在小数点后第几位
while (x) {
hash[x] = res.size();
x *= 10;
res += to_string(x / y);
x %= y;
if (hash.count(x)) {//更新后的余数出现过,不用再继续算
res = res.substr(0, hash[x]) + '(' + res.substr(hash[x]) + ')';
break;
}
}
return res;
}
};
LeetCode 167. 两数之和 II - 输入有序数组
/*
注意这道题下标从1开始,注意这道题说的不可以重复使用相同的元素是指同一个位置上的数只能用一次,不是数值相同的元素只能用一个
这是一个非常经典的双指针算法,先是考虑双层循环,暴力枚举,然后再看有无单调性,能否优化成双指针来做,时间复杂度降到O(n)
*/
class Solution {
public:
vector twoSum(vector& numbers, int target) {
for(int i=0,j=numbers.size()-1;itarget) j--;
if(i
LeetCode 168. Excel表列名称
class Solution {
public:
string convertToTitle(int n) {
int k = 1;//求位数
for (long long p = 26; n > p; p *= 26) {//先看数字n对应几个字母
n -= p;
k ++ ;
}
n -- ;
string res;
while (k -- ) {//把n转化成26进制数
res += n % 26 + 'A';
n /= 26;
}//这样得到的是逆序,最后一位在最前面
reverse(res.begin(), res.end());//翻转
return res;
}
};
LeetCode 169. 多数元素
class Solution {
public:
/*
(unordered_map) O(n)
使用 C++ 提供的 unordered_map 记录每个元素出现的次数。
遍历过程在,如果次数大于 n/2n/2,则返回该数字即可。
*/
int majorityElement(vector& nums) {
unordered_map h;
for (int i = 0; i < nums.size(); i++) {
h[nums[i]] += 1;
if (h[nums[i]] > nums.size() / 2)
return nums[i];
}
return 0;
}
};
class Solution {
public:
int majorityElement(vector& nums) {
/*
(投票算法) O(n)
从前往后扫描每个数,r表示当前存的数,c表示当前这个数的数量,如果当前遍历的数x与当前存的数相同的话,c+1,如果不同的话,c-1,意思是x抵消掉一个你所存的元素
边界条件是如果当前遍历的数x与存的数r不相同,且c==0,就把当前存的数r更新为x,c更新为1
遍历结束后,若数组中存在主要元素,则主要元素必定是 r
*/
int r, c = 0;
for (auto x: nums)
if (!c) r = x, c = 1;
else if (r == x) c ++ ;
else c -- ;
return r;
}
};
LeetCode 170. 两数之和 III - 数据结构设计
class TwoSum
{
public:
unordered_map dic;
/** Initialize your data structure here. */
TwoSum()
{
}
/** Add the number to an internal data structure.. */
void add(int number)
{
if (dic.count(number) != 0)
dic[number] ++;
else
dic[number] = 1;
}
/** Find if there exists any pair of numbers which sum is equal to the value. */
bool find(int value)
{
for (auto [x, freq]: dic)
{
long long y = (long long)value - x;//数据量较大
if (x == y)
{
if (dic[x] >= 2)
return true;
}
else
{
if (dic.count(y) != 0)
{
return true;
}
}
}
return false;
}
};
LeetCode 171. Excel表列序号
class Solution {
public:
int titleToNumber(string s) {
int a = 0;
for (long long i = 0, p = 26; i < s.size() - 1; i ++, p *= 26)
a += p;
//先把前面的数加上,比如ABCD,先加上26+26^2+26^3,然后再看ABCD是四位数的第几个,也就是把26进制数转化为10进制数
int b = 0;//26进制数转化为10进制数
for (auto c: s) b = b * 26 + c - 'A';
return a + b + 1;//+1是因为输出下标从1开始
}
};
LeetCode 172. 阶乘后的零
class Solution {
public:
/*
由于n!的后缀0是由起质因子2和质因子5相乘而来的,而2的个数总是比5多的,因此我们只需要计算n!中质因子5的个数即可。
*/
int trailingZeroes(int n) {
int res = 0;
while (n) res += n / 5, n /= 5;
return res;
}
};
LeetCode 173. 二叉搜索树迭代器
/*
(栈)
用栈来模拟 BST 的中序遍历过程,当前结点进栈,代表它的左子树正在被访问。栈顶结点代表当前访问到的结点。
求后继时,只需要弹出栈顶结点,取出它的值。然后将它的右儿子以及右儿子的左儿子等一系列结点进栈,这一步代表找右子树中的最左子结点,并记录路径上的所有结点。
判断是否还存在后继只需要判断栈是否为空,因为栈顶结点是下一次即将被访问到的结点。
*/
class BSTIterator {
public:
stack stk;
BSTIterator(TreeNode* root) {
while (root) {
stk.push(root);
root = root->left;
}
}
/** @return the next smallest number */
int next() {
auto root = stk.top();
stk.pop();
int val = root->val;
root = root->right;
while (root) {
stk.push(root);
root = root->left;
}
return val;
}
/** @return whether we have a next smallest number */
bool hasNext() {
return stk.size();
}
};
LeetCode 174. 地下城游戏
/*
(动态规划) O(mn)
此题不能直接从正向动态规划的原因是不确定起始点的值,但我们可以发现,到终点之后健康值为 1 一定是最优的。
可以考虑从终点到起点进行动态规划。
设状态f(i,j)表示从 (i, j) 成功到达终点,(i, j) 处需要具备的最少健康值。
初始时,f(m−1,n−1)为 max(1, 1 - w[i][j]),其余为正无穷。
转移时,f(i,j)=min(f(i+1,j),f(i,j+1))-w[i][j];如果 f(i,j)<=0,表示道路上的补给实在太多了,但此时健康值不能小于0,所以此时需要修正 f(i,j)=1,即下限为 1
最终答案为 f(0,0)
*/
class Solution {
public:
int calculateMinimumHP(vector>& w) {
int n = w.size(), m = w[0].size();
vector> f(n, vector(m, 1e8));
for (int i = n - 1; i >= 0; i -- )//从终点倒推,因为是从(i,j)状态是由(i+1,j),(i,j+1)决定
for (int j = m - 1; j >= 0; j -- )
if (i == n - 1 && j == m - 1) f[i][j] = max(1, 1 - w[i][j]);
//终点要保证f(i,j)+w(i,j)>=1,则f(i,j)>=1-w(i,j),越接近越好,并且f(i,j)至少为1,两者取max
//也就是说在满足1的前提下,能等于1-w(i,j)就等于1-w(i,j)
else {
if (i + 1 < n) f[i][j] = f[i + 1][j] - w[i][j];
if (j + 1 < m) f[i][j] = min(f[i][j], f[i][j + 1] - w[i][j]);
f[i][j] = max(1, f[i][j]);
}
return f[0][0];
}
};
LeetCode 179. 最大数
class Solution {
public:
/*
考虑简单的情况,如果只给了两个数字,那么只需要比较两个数字前后的拼接,即可确定顺序。
扩展到多个数字时,存在偏序关系,即当 A 一定要在 B 前且 B 一定要在 C 前时,A 一定在 C 前。
所以可以借助这个偏序关系直接对所有数字按照两个数字的情况进行排序。
注意最后需要去除前导 0。
*/
string largestNumber(vector& nums) {
sort(nums.begin(), nums.end(), [](int x, int y) {
string a = to_string(x), b = to_string(y);
return a + b > b + a;
});
string res;
for (auto x: nums) res += to_string(x);
int k = 0;
// while (k + 1 < res.size() && res[k] == '0') k ++ ;
//其实可以不用这样写,如果开头为0的话,后面必然全是0,所以特判0即可
if (res[0] == '0') return "0";
return res.substr(k);
}
};
LeetCode 187. 重复的DNA序列
class Solution {
public:
/*
(哈希表) O(n)
用哈希表记录所有长度是10的子串的个数。
从前往后扫描,当子串出现第二次时,将其记录在答案中。
时间复杂度分析:总共约 nn 个长度是10的子串,所以总共有 10n 个字符。计算量与字符数量成正比,所以时间复杂度是 O(n)。
*/
vector findRepeatedDnaSequences(string s) {
vector res;
unordered_map S;
for (int i = 0; i + 10 <= s.size(); i ++ )
{
string str = s.substr(i, 10);
if (S[str] == 1) res.push_back(str);
S[str] ++ ;
}
return res;
}
};
class Solution {
using ULL = unsigned long long;
public:
/*
哈希表。
记录每个长度为 10 的子串出现的次数,出现大于 1 次的就是答案。
有两种选择:
哈希表直接存储字符串
哈希表存储字符串哈希值
*/
vector findRepeatedDnaSequences(string s) {
int n = s.size();
if( n <= 10 ) return {};
const ULL P = 131;
ULL gap = 1, now = s[0];
for( int i = 1; i < 10; ++i ) {
gap *= P;
now = now * P + s[i];
}
unordered_map hash;
hash[now] = 1;
vector ret;
for( int i = 10; i < n; ++i ) {
now = ( now - s[i - 10] * gap ) * P + s[i];
++hash[now];
if( hash[now] == 2 )
ret.emplace_back( move(s.substr(i - 9, 10)) );
}
return ret;
}
};
LeetCode 188. 买卖股票的最佳时机 IV
/*
状态机分析过程类似图论建模
1.明确有几个状态作为节点
2.明确这几个节点能够怎样相互转换,并给他们连上带权值的有向边(包括自环)(权值可能为0,可能为正数或负数)
3.根据每个节点的入度写状态转移方程
状态表示:
f(i,j,0/1)表示股票交易进行到了第i天,已经进行了k次交易,此时手中有无股票
其中对于j更准确的说法是已经进行了j次交易或者正在进行第j次交易,股票还未卖出
状态计算:
有两个状态,当前有无股票
如果当天没有股票,则第二天可以选择什么都不做,状态不变,或者在第二天把股票买入(钱变少),变成有股票状态
如果当天有股票,则第二天可以选择什么都不做,状态不变,或者在第二天把股票卖出(钱变多),变成无股票状态
f[i][j][0] = max(f[i-1][j][0],f[i-1][j][1]+w[i]);
f[i][j][1] = max(f[i-1][j][1],f[i-1][j-1][0]-w[i]);
两个节点,四条边,边表示钱的变化,一个入口,一个出口
*/
const int N=1010,M=110,INF=0x3f3f3f3f;
int f[N][M][2];
class Solution {
public:
int maxProfit(int k, vector& prices) {
int n=prices.size();
memset(f,-INF,sizeof f);//状态不合法,不希望从这个状态转移过来
for(int i=0;i<=n;i++) f[i][0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=k;j++){//j也要从1开始,因为用到了j-1
f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]+prices[i-1]);
f[i][j][1]=max(f[i-1][j][1],f[i-1][j-1][0]-prices[i-1]);
}
}
int res=0;
for(int i=0;i<=k;i++) res=max(res,f[n][i][0]);
return res;
}
};
class Solution {
public:
int maxProfit(int k, vector& p) {
int n = p.size();
int f[2][2 * k + 1];
memset(f, -0x3f3f3f3f, sizeof f);
for (int i = 0; i <= 1; ++i) f[i][0] = 0;
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= k; ++j){
f[i & 1][2 * j - 1] = max(f[i - 1 & 1][2 * j - 1], f[i - 1 & 1][2 * j - 2] - p[i - 1]);
f[i & 1][2 * j] = max(f[i - 1 & 1][2 * j], f[i - 1 & 1][2 * j - 1] + p[i - 1]);
}
}
int res = 0;
for (int j = 1; j <= 2 * k; ++j) res = max(res, f[n & 1][j]);
return res;
}
};
class Solution {
public:
int maxProfit(int k, vector& p) {
int n = p.size();
int f[n + 1][2 * k + 1];
memset(f, -0x3f3f3f3f, sizeof f);
for (int i = 0; i <= n; ++i) f[i][0] = 0;
for (int i = 1; i <= n; ++i){
for (int j = 1; j <= k; ++j){
f[i][2 * j - 1] = max(f[i - 1][2 * j - 1], f[i - 1][2 * j - 2] - p[i - 1]);
f[i][2 * j] = max(f[i - 1][2 * j], f[i - 1][2 * j - 1] + p[i - 1]);
}
}
int res = 0;
for (int j = 1; j <= 2 * k; ++j) res = max(res, f[n][j]);
return res;
}
};
LeetCode 189. 旋转数组
class Solution {
public:
/*
我们可以使用额外的数组来将每个元素放至正确的位置。用 n 表示数组的长度,我们遍历原数组,将原数组下标为 i 的元素放至新数组下标为 (i+k)%mod的位置,最后将新数组拷贝至原数组即可。
*/
void rotate(vector& nums, int k) {
int n = nums.size();
vector temp = nums;
for (int i = 0; i < n; ++i)
nums[(i + k) % n] = temp[i];
}
};
class Solution {
public:
/*
方法一中使用额外数组的原因在于如果我们直接将每个数字放至它最后的位置,这样被放置位置的元素会被覆盖从而丢失。因此,从另一个角度,我们可以将被替换的元素保存在变量prev 中,从而避免了额外数组的开销,然后不断进行上述过程,直至回到初始位置
容易发现,当回到初始位置 0 时,有些数字可能还没有遍历到,此时我们应该从下一个数字开始重复的过程,可是这个时候怎么才算遍历结束呢?
使用单独的变量count 跟踪当前已经访问的元素数量,当count=n 时,结束遍历过程。
*/
void rotate(vector& nums, int k) {
int n = nums.size();
k = k % n;
int count = 0;//用count 跟踪当前已经右移的元素数量
for (int start = 0; ; start++) {//总共要循环count圈
int current = start;//当前要右移k位的元素的位置
int prev = nums[start];//每次交换之后,prev表示当前新待右移的元素
do {
int next = (current + k) % n;
swap(nums[next], prev);
count++;
current = next;//交换之后,更新当前要右移的元素的原下标
} while (start != current);//一直交换,直到回到起点结束
if(count==n) break;//当count=n 时,结束遍历过程
}
}
};
class Solution {
public:
/*
这道题目的数据比较坑,k 可能非常大,超出数组长度,所以我们要先将 k 对数组长度取模。
然后可以将数组的旋转操作分解为三次翻转操作:
将整个数组翻转;
将前k个数翻转;
将后 n−k 个数翻转,其中 n 是数组长度;
空间复杂度分析:所有翻转操作均基于交换两个数的操作:swap(),该函数仅使用一个额外的变量,所以整个程序仅使用额外 O(1) 的空间,双指针时间复杂度为O(n)。
时间复杂度分析:总共3次翻转操作,数组中的每个数总共被遍历两次。所以时间复杂度是 O(n)。
*/
void rotate(vector& nums, int k) {
int n = nums.size();
k %= n;
reverse(nums.begin(), nums.end());
reverse(nums.begin(), nums.begin() + k);
reverse(nums.begin() + k, nums.end());
}
};
LeetCode 190. 颠倒二进制位
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
uint32_t res = 0;
for (int i = 0; i < 32; i ++ )
res = res * 2 + (n >> i & 1);
return res;
}
};
LeetCode 191. 位1的个数
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
while (n) n -= n & -n, res ++ ;
return res;
}
};
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
for (int i = 0; i < 32; i ++ )
res += n >> i & 1;
return res;
}
};
LeetCode 199. 二叉树的右视图
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
/*
这道题要求返回每一层最右边的数,所以就是直接宽搜遍历每一层,然后记录每一层最右边的数
*/
vector rightSideView(TreeNode* root) {
queue q;
vector res;
if (!root) return res;
q.push(root);
while (q.size()) {
int len = q.size();//当前这层的元素个数
for (int i = 0; i < len; i ++ ) {//枚举当前层所有元素
auto t = q.front();
q.pop();
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
if (i == len - 1) res.push_back(t->val);//如果遍历到当前层的最后一个元素,记录下来
}
}
return res;
}
};
LeetCode 200. 岛屿数量
class Solution {
public:
vector> g;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int numIslands(vector>& grid) {
g = grid;
int cnt = 0;
for (int i = 0; i < g.size(); i ++ )
for (int j = 0; j < g[i].size(); j ++ )
if (g[i][j] == '1') {
dfs(i, j);
cnt ++ ;
}
return cnt;
}
void dfs(int x, int y) {
g[x][y] = 0;
for (int i = 0; i < 4; i ++ ) {
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < g.size() && b >= 0 && b < g[a].size() && g[a][b] == '1')
dfs(a, b);
}
}
};