Week 9. 第151-166题

151. 翻转字符串里的单词

分析

单词不能翻转, 但是所有单词要颠倒下前后位置

字符串前后会有多余的空格, 中间至多2个空格, 题目要求前后删除, 中间改成1个
要求空间O(1)

  1. 先将字符串以字母为最小单位翻转
  2. 每个单词内部翻转(双指针)

在这里插入图片描述
多余空格处理: 直接将原数组排到前面
在这里插入图片描述

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[k1], 第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]>2f, 第2个栈顶元素一定不是 f [ k ] f[k] f[k], 所以第2个栈顶可以不用删

另外一种情况 a [ k ] < 第 2 个 f 栈 栈 顶 a[k] < 第2个f栈栈顶 a[k]<2f, a [ k ] ≤ f [ k − 1 ] a[k] \leq f[k - 1] a[k]f[k1], 考虑第2个栈顶有无可能不是 f [ k ] f[k] f[k]
假设不是 f [ k ] f[k] f[k], 那么栈顶是 f [ k − 1 ] f[k - 1] f[k1]咯, 那就意味着 a [ k ] ≤ f [ k − 1 ] a[k] \leq f[k - 1] a[k]f[k1], 那么必定要存 f [ k ] f[k] f[k]

最后 a [ k ] = = f [ k − 1 ] a[k] == f[k - 1] a[k]==f[k1]的时候, 能否不存 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[k1]==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 是lr的下取整, 那么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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值