10天速通力扣hot100(只刷easy、mid)第二天(明天休息)

第二天 keep going

34. 在排序数组中查找元素的第一个和最后一个位置(二分查找)(mid)

思路:

一般的二分查找:

if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid - 1;
else return mid;

寻找第一个满足target的二分查找,这里以找到第一个>=target的元素为例

在找到一个满足条件的元素后,向左缩小区间,检查左边的区间中是否还有满足条件的元素

这里的关键就是 满足条件时,要记录ret = mid

if(nums[mid] >= target) {
    rigth = mid -1; //这里已经找到了一个满足条件的了,向左缩小区间
    //记录当前的元素下标,因为每一次找到满足条件的都会向左继续寻找,可以保证这里的ret一定是满足条件中最小的
    ret = mid; 
}
else left = mid + 1;//不满足条件,就向右寻找
return ret;

代码:

为什么左边界要找第一个大于等于的呢?为什么右边界要找第一个大于的呢?

分析给定数组的情况:

  • 有目标元素
    • 左边界为第一个target,右边界为第一个大于target元素的位置或者为nums.size()(这种情况下,就是target为数组中的最大元素)
  • 没有
    • 全部小于目标元素:左边界=右边界=nums.size()
    • 全部大于目标元素:左边界=右边界=0;
    • 有大于、有小于:左边界=右边界,都是找的第一个大于target元素的位置
class Solution {
public:
    int findLeft(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1, ret = nums.size();
        while(left <= right) {
            int mid = (left + right)/2;
            if(nums[mid] >= target) {
                right = mid - 1;
                ret = mid; //这里用ret记录每一次找到满足条件的mid,由于会缩小区间,可以保证ret的值一定是递减的
            }
            else left = mid + 1 ;
        }
        return ret;
    }

    int findRight(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1, ret = nums.size();
        while(left <= right) {
            int mid = (left + right)/2;
            if(nums[mid] > target) {
                right = mid - 1;
                ret = mid;
            }
            else left = mid + 1 ;
        }
        return ret;
    }

    vector<int> searchRange(vector<int>& nums, int target) {
        int left = findLeft(nums,target);
        int right = findRight(nums,target) - 1;
        //1.没有对应元素 left > right 2.有一个 left = right 3.有两个及以上 left < right
        if(left <= right) return {left,right};
        return {-1,-1};
    }
};

131. 分割回文串(回溯)(mid)

回溯的一般解法

void func() {
    if(终止条件)
        结束要做的事
        return 
    for(按序遍历) {
        if(判断是否剪枝) {
            //todo
            递归去子问题
            回溯。以便进行for循环的下一轮
        }   
    }
}

代码:

class Solution {
private:
    vector<vector<string>> ret;
    vector<string> path;

    bool isPalindrome(const string& s, int start, int end) {
        for(int i = start, j = end; i < j; ++i,--j) {
            if(s[i]!=s[j]) return false;
        }
        return true;
    }

    void backTracking(const string& s, int start) {
        if(start >= s.size()) { //顺利的切割完
            ret.push_back(path);
            return;
        }
        for(int i = start; i < s.size(); ++i) {
            if(isPalindrome(s,start,i)) { //这里的判断可以做剪枝
                string str = s.substr(start, i-start+1);
                path.push_back(str);
                backTracking(s, i+1); //继续向后切割
                path.pop_back(); // 回溯过程,弹出本次已经添加的子串
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        ret.clear();
        path.clear();
        backTracking(s,0);
        return ret;
    }
};

322. 零钱兑换(动态规划)(mid)

dp数组:dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

dp[j] = min(dp[j - coins[i]] + 1, dp[j])

背包问题的外层遍历和内层遍历

针对本题,外层物品、内层背包;外层背包、内层物品都可以

以外层物品、内层背包为例

  • 外层遍历硬币,尝试将一种硬币放入背包
  • 内存遍历背包,就是遍历dp数组,
    • 这里为什么要从cons[i]开始遍历呢?因为当价格小于当前币值时,无法将硬币放入,所以这样可以减少不必要的计算
    • 最开始时,由dp[j - coins[i]]可以找到dp[0] = 0;这样对于j恰好为币种面值局的情况时,硬币的数量当然最少为1。
    • 在内存循环的判断逻辑中,如果dp[j - coins[i]]为初始值,说明目前还没有找到可以满足j - coins[i]元的硬币方案,因此就没有必要尝试将该种硬币放入了。

外层背包、内层物品思路

对于每一种价格,尝试将一种硬币放入,如果j - coins[i]元对应的方案还是初始值,说明将该种硬币放入后,剩下的钱没有合理的分配方案,所有会被淘汰。

代码:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, INT_MAX);
        dp[0] = 0;
        for(int i = 0; i < coins.size(); ++i) { //遍历物品
            for(int j = coins[i]; j <= amount; j++) {//遍历背包
                if(dp[j - coins[i]] != INT_MAX) {
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }
        if(dp[amount] == INT_MAX) return -1;
        return dp[amount];
    }
};

560. 和为 K 的子数组 (子串-前缀和&哈希表优化)(mid)

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

思路:

暴力枚举的优化方案:

【0…j-1】【j…i】

pre[j-1] + k

【0…i】

pre[i]

pre[i]−pre[j−1]==k

将前缀和的出现次数记录到哈希表中,当遍历到i时,用pre-k来在哈希表中检索,如果pre-k这个前缀和曾经出现过,则表明当前存在mp[pre-k]个以当前元素为最后一个元素的子数组满足入选的条件。

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp; // <前缀和,前缀和的出现次数>
        mp[0] = 1; //说明只有当前的单个元素作为一个数组
        int count = 0, pre = 0;
        for(auto & x : nums) {
            pre += x;
            if(mp.find(pre - k) != mp.end()) {
                count += mp[pre - k]; 
            }
            mp[pre]++; //表明遇到了一个新的前缀和,其和为 pre
        }
        return count;
    }
};

53. 最大子数组和 (动态规划/分治)(dp简单、分治mid)

dp解法:

dp[i] = max(nums[i], dp[i-1] + nums[i])

这里可以使用前缀和pre去优化内存空间

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        // vector<int> dp(nums.size());
        // dp[0] = nums[0];
        int pre = 0;
        int ret = nums[0];
        for(int i = 0; i < nums.size(); ++i) {
            pre = max(nums[i], pre + nums[i]);
            ret = max(ret, pre);
        }
        return ret;
    }
};
分治法:
#include <iostream>
#include <vector>
using namespace std;

struct Node {
    int lSum,rSum,sum,mSum;
    //lsum 以左边界为开头的最大的子段和
    //rsum 以右边界为开头的最小的子段和
    //sum 整段的和
    //mSum 最大的子段和
};
    
Node dfs(vector<int>& nums, int l, int r)
{
    if(l==r) return {nums[l],nums[l],nums[l],nums[l]};
        
    int mid = l+r>>1;
    auto left = dfs(nums,l,mid); //左半部分
    auto right = dfs(nums,mid+1,r); //右半部分
        
    return {
        max(left.lSum, left.sum+right.lSum),
        max(right.rSum, left.rSum + right.sum),
        left.sum + right.sum,
        max(left.rSum+right.lSum, max(left.mSum,right.mSum))
    };
}
int maxSubArray(vector<int>& nums) {
    return dfs(nums,0,nums.size()-1).mSum;
}

int main()
{
    vector<int> array1 = {-2,1,-3,4,-1,2,1,-5,4};
    cout << maxSubArray(array1) << endl;
}

160. 相交链表(链表)(easy)

情况:

  • 相交:
    • 相交点前节点数相同:直接按序遍历走到一起,走到相交点结束循环
    • 相交点前节点数不同:当走完AC时,继续走BC;走完BC时,继续走AC
      • 在第二次会走进C时,会同时走到相交点
  • 不相交:
    • 有一方为空:返回null
    • 都不为空:
      • 等长:第一次为null时,对方也是null,相等,退出循环
      • 不等长:第二次为null时,对方也是null,相等,退出循环
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(!headA || !headB) return nullptr;
        ListNode* pA = headA, *pB = headB;
        while(pA != pB) {
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};

56. 合并区间 (数组、排序技巧)(mid)

先将数组按照左边界进行排序,遍历数组时,如果merged数组为空,或者最后一个数组与当前遍历访问的数组不相交,就将该新数组加入merged

如果有相交的部分,就将merged中最后一个数组的边界进行扩展

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        //排序后合并
        sort(intervals.begin(),intervals.end());
        vector<vector<int>> merged;
        for(int i = 0; i < intervals.size(); ++i) {
            int L = intervals[i][0], R = intervals[i][1];//记录当前数组的左右边界
            if(!merged.size() || merged.back()[1] < L) //如果列表为空,或者列表中的最后一个的右边界小于该数组的左边界(说明没有相交部分)
                merged.push_back(intervals[i]);
            else 
                merged.back()[1] = max(R,merged.back()[1]);
        }
        return merged;
    }
};

102. 二叉树的层序遍历(二叉树)(mid)

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        //队列
        queue<TreeNode*> q;
        vector<vector<int>> ret;
        if(root) q.push(root);
        while(!q.empty()) {
            int count = q.size();
            vector<int> vec;  
            for(int i = 0 ; i < count; ++i) {
                TreeNode* node = q.front();
                q.pop();
                vec.push_back(node->val);
                if(node->left) q.push(node->left);
                if(node->right) q.push(node->right);
            }
            ret.push_back(vec);
        }
        return ret;
    }
};

39. 组合总和 (回溯+剪枝)(mid)

常规回溯写法

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    int sum = 0;

    void backTracking(vector<int>& candidates, int target, int start) {
        //终止条件
        if(sum >= target) {
            if(sum == target) ret.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size(); ++i) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backTracking(candidates, target ,i);
            path.pop_back();
            sum -= candidates[i];
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
       ret.clear();
       path.clear();
       backTracking(candidates, target, 0); 
       return ret;
    }
};

剪枝优化需要先对candidates数组进行递增排序

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    int sum = 0;

    void backTracking(vector<int>& candidates, int target, int start) {
        //终止条件
        if(sum == target) {
            ret.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size(); ++i) {
            if(sum + candidates[i] > target) return; //剪枝
            sum += candidates[i];
            path.push_back(candidates[i]);
            backTracking(candidates, target ,i);
            path.pop_back();
            sum -= candidates[i];
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
       ret.clear();
       path.clear();
       sort(candidates.begin(),candidates.end()); //排序
       backTracking(candidates, target, 0); 
       return ret;
    }
};
  • 56
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值