151. 翻转字符串里的单词
分析
单词不能翻转, 但是所有单词要颠倒下前后位置
字符串前后会有多余的空格, 中间至多2个空格, 题目要求前后删除, 中间改成1个
要求空间O(1)
- 先将字符串以字母为最小单位翻转
- 每个单词内部翻转(双指针)
多余空格处理: 直接将原数组排到前面
code
注意观察细节, 刚开始
j = i, t = k, 赋值, 因为需要从这里开始变换
中间位置变换
k = t, i = j, // k变为变化后t的位置, i变为变化后j的位置
class Solution {
public:
string reverseWords(string s) {
int k = 0;
for (int i = 0; i < s.size(); i ++ ){
// i ++ 需要保留, 因为下面i = j, j的位置是while循环出来的时候s[j]是空格, i ++, 正好跳到下一个单词开头
if (s[i] == ' ') continue;
int j = i, t = k; // k表示单词的起始位置, t表示终止位置, j表示原字符串位置
while (j < s.size() && s[j] != ' ') s[t ++ ] = s[j ++ ]; // 单词多余空格去掉, 挪到开头
reverse(s.begin() + k, s.begin() + t); // [t, k]区间的单词翻转
s[t ++ ] = ' '; // 添加单词结尾的空格
k = t, i = j;
}
if (k) k --; // 表示至少有一个单词, 因为上面在末尾多加了空格, 需要删掉
s.erase(s.begin() + k, s.end());
reverse(s.begin(), s.end());
return s;
}
};
152. 乘积最大子数组
分析
本质上是dp问题
暴力做的话
枚举区间的起始位置, 终止位置, 然后循环一下, 看下有没有超过答案
起始位置, 终止位置, 然后总乘积O(n^3)
f[i], 以i为右端点的区间的最大乘积
考虑f[i - 1] 与f[i]关系的时候, 因为第i个数可能是负数, 负数的话, 想要乘积最大, 那么必须要*一个更小的负数
因此需要用g[i] 表示 最小值
降低空间至O(1)
可以发现空间是O(n), 然后可以发现f[i], g[i] 只与f[i - 1], g[i - 1]相关, 因此可以用滚动数组去实现, 也就是算f[i]的时候, 只需要f[i - 1]的状态, 不需要f[i - 1], g[i - ]之前的数
可以f[i]无论什么情况, 都是三种情况max, g[i]都是三种情况min 所以在写的时候,不需要分情况, 直接无脑max, min
code
class Solution {
public:
int maxProduct(vector<int>& 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;
}
};
153. 寻找旋转排序数组中的最小值
分析
由于严格递增的, 所以后一段末尾一定小于前一段靠头, 而且后一段开头下标一定在第一段下标后
怎么逐步缩小区间
枚举下中点, 如果中点在上面部分, 那么答案在中点右边
如果中点在下面部分, 说明答案一定在中点的位置, 或者中点的左边
不管什么情况, 一定可以将答案所在范围缩小一般
怎么判断中点在上面还是在下面
直接判断中点是否> 上面的第1个数, 如果>=的话, 在上面
小于的话在下面
code
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
if (nums[r] >= nums[0]) return nums[0];
while (l < r){
int mid = l + r >> 1;
if (nums[mid] < nums[0]) r = mid; // 比nums[0]小, 说明答案在中点左边 r = mid
else l = mid + 1;
}
return nums[r];
}
};
154. 寻找旋转排序数组中的最小值 II
分析
一样, 可能有重复元素
重复元素意味着第一段开头和第二段结尾可能一样
这样就会导致 nums[mid] = nums[0]的话, 就无法判断nums[mid]在第1个部分还是第2个部分
所以要干掉后面和前面一样的数, 才能二分
联动
leetcode 81.旋转数组求target
code
81题是要找target, 特殊情况是所有数都相同
而这里是找最小值, 不一样
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r && nums[l] == nums[r]) r --;
if (nums[l] <= nums[r]) return nums[0]; // 这里是特殊情况, 表示只有一个区间
while (l < r){
int mid = l + r >> 1;
if (nums[mid] < nums[0]) r = mid;
else l = mid + 1;
}
return nums[r];
}
};
155. 最小栈
分析
需要维护一个数组f[i], 表示栈里a1 ~ ai的最小值
如果在原栈里删除一个数a_{k +1}的话, 那就删除f[k + 1]
如果插入一个数的话, 那就加f[k + 1]
考虑优化
(并不影响时间空间复杂度, yxc本着精益求精的态度说一下), 只影响常数
如果a_k > f[k - 1], 那么f[k]就不需要存(因为我们只要可以保证能够完美维护f这个栈)
这样栈里可以少存一些元素
删除a_k的时候, 能否判断出第2个栈里元素不是f[k], 即能不能判断第2个栈能够不删除
答案是肯定的, 可以判断出
首先f[k] <= f[k - 1]
在弹出a_k的时候, 如果a_k比栈顶元素大, 那么说明f[k] = f[k - 1]
如果a_k <= f[k - 1], 那么需要将f栈里a_k对应的位置删掉
a[k] == f[k - 1]的时候, 第2个栈顶一定要存f[k], 如果不存f[k], 因为第2个栈顶是f[k] = f[k - 1] = a[k - 1], 就不能判断栈顶能否删除 , 就不对了
证明:
命题: 当 a k > f [ k − 1 ] a_k > f[k - 1] ak>f[k−1], 第2个栈顶不存 f [ k ] f[k] f[k], 当删除 a k a_k ak的时候, 能否判断出f栈栈顶有没有保存 f [ k ] f[k] f[k]
可以判断出来, 我们只需要判断下栈顶元素是否是
f
[
k
]
f[k]
f[k]
反证法:
a
[
k
]
>
f
[
k
]
a[k] > f[k]
a[k]>f[k]的情况下, 如果第2个栈顶元素是
f
[
k
]
f[k]
f[k], 由f的定义
f
[
k
]
≤
a
[
k
]
f[k] \leq a[k]
f[k]≤a[k], 又有第2个栈顶是
f
[
k
]
f[k]
f[k],
a
[
k
]
>
f
[
k
]
a[k] > f[k]
a[k]>f[k], 那么矛盾
所以
a
[
k
]
>
第
2
个
f
栈
栈
顶
a[k] > 第2个f栈栈顶
a[k]>第2个f栈栈顶, 第2个栈顶元素一定不是
f
[
k
]
f[k]
f[k], 所以第2个栈顶可以不用删
另外一种情况
a
[
k
]
<
第
2
个
f
栈
栈
顶
a[k] < 第2个f栈栈顶
a[k]<第2个f栈栈顶,
a
[
k
]
≤
f
[
k
−
1
]
a[k] \leq f[k - 1]
a[k]≤f[k−1], 考虑第2个栈顶有无可能不是
f
[
k
]
f[k]
f[k]
假设不是
f
[
k
]
f[k]
f[k], 那么栈顶是
f
[
k
−
1
]
f[k - 1]
f[k−1]咯, 那就意味着
a
[
k
]
≤
f
[
k
−
1
]
a[k] \leq f[k - 1]
a[k]≤f[k−1], 那么必定要存
f
[
k
]
f[k]
f[k]
最后 a [ k ] = = f [ k − 1 ] a[k] == f[k - 1] a[k]==f[k−1]的时候, 能否不存 f [ k ] f[k] f[k]
第2个栈顶一定要存f[k], 如果不存f[k], 因为第2个栈顶是f[k] = f[k - 1] = a[k - 1], 就不能判断栈顶应不应该删除了, 因为
f
[
k
−
1
]
=
=
f
[
k
]
f[k - 1] == f[k]
f[k−1]==f[k], 因为根本无法辨别f[k - 1] 是f[k], 还是自身f[k - 1], 无法区分啊了, 俩一样怎么区分是前一个, 还是后一个
因此此种情况,不存就不对了, 一定要存f[k]
code
class MinStack {
public:
/** initialize your data structure here. */
stack<int> stk, f;
MinStack() {
}
void push(int x) {
if (f.empty() || f.top() >= x) f.push(x); // 当前数比f栈顶小, 一定要存f[k];
stk.push(x);
}
void pop() {
if (stk.top() <= f.top()) f.pop(); // 如果stk栈顶元素比f栈顶小, 说明肯定存了f[k], 要删
stk.pop();
}
int top() {
return stk.top();
}
int getMin() {
return f.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
160. 相交链表
分析
可以证明, p指针一直走, q指针一直走, p与q相遇点一定是交点
证明:
当p指针走绿色线的时候, 走到分叉路p会走a + c + b
当q指针走红色线的时候, 走到分叉路q会走b + c + a
所以当两个指针相遇的时候, 必然是交点
如果没有相交点的话, 第1个指针走到第2条直线上走到空的时候, 第2个指针走在第1条直线上, 也会走到空
code
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto p = headA, q = headB;
while (p != q){
p = p ? p->next : headB; // p 如果为空, 那么就从B开始走
q = q ? q->next : headA;
}
return p;
}
};
164. 最大间距
分析
请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。
先将Min, Max之间的区间分成若干个区间
希望做到每一个区间内部的差值一定不可能是答案, 不在内部的话, 只能在区间之间
在区间之间的话, 只要看下当前区间的最小值, 因为这个区间的最小值才和上面某个区间相邻的数, 如果不是最小值的话, 那么说明不是和上一个区间相邻(???)
解答: 因为不是最小值的话, 一定不会和其他区间相邻, 区间分段保证了最大差值只在两个区间段之间
如果我们希望每个区间内部相邻两个数的差值不是答案的话, 如果每个区间都取最大差都不足以扩充到[min, max]一样长的话, 那说明答案不可能在区间内部
如果区间为(0, x] 那么区间的最大差值为x - 1, 一共有(n - 1)个区间, 假设所有的差都取到x - 1, (n - 1)(x - 1) < MAX - MIN, 说明答案不可能是区间内的差
因为现在是取遍数组, 所有max(差值) 的和, 并且假设区间内能取到最大差值, 都凑不出, 尽然凑不出原来的长度?? 说明假设有问题
因此答案一定在某两个区间之间
yxc:我们再捋一下逻辑, 我们看下x在哪些范围的时候, 答案一定不是区间内某两个数的差值
首先区间里两个数最大值就是x - 1, 一共n - 1个差, (n - 1) (x - 1) < INT_MAX - INT_MIN, 那就矛盾,
因此只要x满足(n - 1) (x - 1) < INT_MAX - INT_MIN, 那么答案一定不在区间内
计算x的范围, 如果x(区间划分长度) 满足上式, 那么能够保证每个区间内没有答案, 然后就只需要看区间之间的差就可以了
然后只要统计每个区间内的最小值和最大值就可以了, 因为如果它不是最小值或者最大值的话, 这个数一定不会和其他的区间相邻()
yxc评价
一起来欣赏下思路, 这思路我也没见过(yxc)
code
class Solution {
public:
int maximumGap(vector<int>& nums) {
struct Range{
int min, max;
bool used;
Range() : min(INT_MAX), max(INT_MIN), used(false){}
};
int n = nums.size();
int Min = INT_MAX, Max = INT_MIN; // 初始化注意1
for (auto x : nums) {
Min = min(Min, x);
Max = max(Max, x);
}
if (n < 2 || Min == Max) return 0;
vector<Range> r(n - 1);
int len = (Max - Min + n - 2) / (n - 1);
for (auto x : nums){
if (x == Min) continue;
int k = (x - Min - 1) / len;
r[k].used = true;
r[k].min = min(r[k].min, x);
r[k].max = max(r[k].max, x);
}
int res = 0;
for (int i = 0, last = Min; i < n - 1; i ++ ) // last初始化为Min
if (r[i].used){
res = max(res, r[i].min - last);
last = r[i].max;
}
return res;
}
};
162. 寻找峰值
分析
这题反应了二分不一定需要有单调性
比方说这题可以二分, 但数组不具有单调性
有没有可能无解啊?
没有可能, 如果左端点第1个点是下降的, 那么左端点是一个峰值
;否则, 那么是上升的, 如果出现下降趋势, 那么开始下降趋势的点就是峰值,
如果没有下降趋势, 就是全增, 全增的话, 最后一个点就是峰值
看下中点和下一个点的关系, 如果中点 < 下一个点, 从中点往右走, 第1次出现下降趋势的位置就是峰值; 否则就会一直走, 走到边界的话, 边界就是峰值
即: 中点 < 中点+1位置上的数, 说明右边一定有峰值
反过来, 如果中点 > 中点+1位置上的数, 往左看, 第1次下降的位置就是峰值
否则, 一直可以走到边界
因此无论那种情况都可以将区间缩小一半
如果在左边, 可以保证答案左边的右边界, 比答案的位置靠前
同理, 在右边的情况, 左边界会比mid靠后一个
mid + 1不会越界, 因为一旦mid + 1越界, mid = nums.size() - 1
, 因为mid 是l
和r
的下取整, 那么l == r = nums.size() - 1
, 那么必定不会进入while循环
code
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while (l < r){
int mid = l + r >> 1;
if (nums[mid] > nums[mid + 1]) r = mid; // 如果当前数比下一个数大, 那么当前mid可能是峰值[~, mid]; 比如mid - 1 = 1 mid = 2, mid + 1 = 1
else l = mid + 1; // 当前数比下一个数小, 那么当前数必定不是峰值[mid + 1, ~]
}
return r;
}
};
165. 比较版本号
分析
从前往后分别抠出来每个版本的数就可以了
[7 5 2 4]
[7 5 2] 两个比较, 数不足的话, 自动补0,比较,
相当于[7 5 2 4] [7 5 2 0] 比较
a == i的时候, 说明没有进入while (a < v1.size() && v1[a] != '.')
因为v1[a]不可能是'.'
, 因为程序逻辑中跳过了'.'
因此只能是到末尾了
code
class Solution {
public:
int compareVersion(string v1, string v2) {
for (int i = 0, j = 0; i < v1.size() || j < v2.size(); ){
int a = i, b = j;
while (a < v1.size() && v1[a] != '.') a ++;
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));
if (x > y) return 1;
if (x < y) return -1;
i = a + 1, j = b + 1; // +1是为了跳过'.'
}
return 0;
}
};
166. 分数到小数
分析
找循环节本质就是找相同余数
只需要维护余数, 还需要记录余数是否出现过
时间复杂度: 分母是n, 那么余数有 0 ~ n - 1种情况, 所以最多循环n次, n + 1必然相同
因此是O(n)的
答案可能爆int, 所以用long long存储
code
class Solution {
public:
string fractionToDecimal(int numerator, int denominator) {
typedef long long LL;
LL x = numerator, y = denominator;
string res;
if (x % y == 0) return to_string(x / y);
if ((x < 0) ^ (y < 0)) res += '-';
x = abs(x), y = abs(y);
res += to_string(x / y) + '.', x %= y; // 先计算小数点前面的数
unordered_map<LL, int> hash;
while (x){
hash[x] = res.size(); // 记录当前x的位置, 下面如果重复的话会用到
x *= 10;
res += to_string(x / y); // 加上当前的商
x %= y; // 算出余数
if (hash.count(x)){ // 如果当前计算的新的 x, 如果与以前重复, 那么就直接输出
res = res.substr(0, hash[x]) + '(' + res.substr(hash[x]) + ')';
break;
}
}
return res;
}
};