leetcode中等题题解top100

146. LRU 缓存机制//本周背诵重点了

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:
LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]
解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
双向链表模板

struct Node
{
    
    
    int key,val;
    Node *left,*right;
    Node(int _key,int _val):key(_key),val(_val),left(nullptr),right(nullptr){
    
    }
} *L,*R;
void init(void)
{
    
    
      L=new Node(-1,-1),R=new Node(-1,-1);
      L->right=R;
      R->left=L;
}
 void remove(Node* 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;
} 

用双向链表维护键值对被使用顺序 ,表头是最近被使用的,表尾是最久未使用的,便于删除

class LRUCache {
    
    
public:
    struct Node
    {
    
    
        int key,val;
        Node *left,*right;
        Node(int _key,int _val):key(_key),val(_val),left(nullptr),right(nullptr){
    
    }
    } *L,*R;
    unordered_map<int,Node*> hash;
    int n;
    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.find(key)!=hash.end())
        {
    
    
            Node* p=hash[key];
            //对于已有键值对更新使用顺序
            remove(p);
            insert(p);
            return p->val;
        }
        else
            return -1;
    }
    
    void put(int key, int value) {
    
    
        if(hash.find(key)==hash.end())
        {
    
    
            if(hash.size()==n)
            {
    
    	//删掉最久没用的
                Node* ff=R->left;
                remove(ff);
                hash.erase(ff->key);
            }
            Node* p=new Node(key,value);
            hash[key]=p;
            insert(p);
        }
        else
        {
    
    
            Node*p=hash[key];
            p->val=value;
            remove(p);
            insert(p);
        }
    }
    void remove(Node* 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;
    }
};

343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
*动态规划,n的拆分情况可以是所有k<=n-1的拆分情况加上n-k,所以最外面的框架是for(int k=1;k<n;k++),
然后里面是n-k乘dp[k]和(n-k)k因为n一定要拆成2个以上整数,dp[k]不一定比k大

class Solution {
    
    
public:
    int integerBreak(int n) {
    
    
        vector<int> dp(n+1,1);
        dp[0]=0;
        dp[1]=0;
        for(int k=2;k<=n;k++)//动态规划遍历前面递推的情况
        {
    
    
            for(int j=1;j<k;j++)//所有小于k的拆分出来的值
            {
    
    
                dp[k]=max(dp[k],j*(k-j));
                dp[k]=max(dp[k],j*dp[k-j]);
            }
        }
        return dp[n];
    }
};

剑指 Offer 14- I. 剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

class Solution {
    
    
public:
    int cuttingRope(int n) {
    
    
        vector<int> dp(n+1,0);
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<n+1;i++)
        {
    
    
            for(int j=1;j<i;j++)
            {
    
    
            //dp[j]和j不一定哪个更大,dp[j]是当j必须被分成至少两部分的乘机最大结果
                dp[i]=max(dp[i],dp[j]*(i-j));
                dp[i]=max(dp[i],j*(i-j));
            }
        }
        return dp[n];
    }
};

139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
动态规划,记录s从0开始的所有子串,是不是可拆分的,然后看可不可以和哪个可拆分的子串一起凑

class Solution {
    
    
public:
    bool wordBreak(string s, vector<string>& wordDict) {
    
    
        unordered_set<string> hash;//hash记录wordDict便于查找
        for(auto a:wordDict)
            hash.insert(a);
        bool dp[s.size()];
        memset(dp,false,sizeof dp);
        for(int i=0;i<s.size();i++)
        {
    
    
            string curr=s.substr(0,i+1);
            if(hash.find(curr)!=hash.end())//如果0到i的子串就可以是个word直接true
                dp[i]=true;
            else
            {
    
    
                for(int j=0;j<i;j++)//遍历小于i的0开始的子串有没有是true的
                {
    
    
                    if(dp[j])
                    {
    
    
                        string curr=s.substr(j+1,i-j);
                        if(hash.find(curr)!=hash.end())//j+1到i的子串是不是一个word
                            dp[i]=true;
                    }
                }
            }
        }
        return dp[s.size()-1];
    }
};

763. 划分字母区间
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
循环第一次记录每个字母最后出现的位置,循环第二次用一个end保存end=max(end,last[S[i]]),必须当前的字母最后一次出现就是在这个i,且这个last[S[i]]比前面任何一个出现过的字母最后一次出现的位置都要靠后,这里就是一个片段的结束

class Solution {
    
    
public:
    vector<int> partitionLabels(string S) {
    
    
        vector<int> last(26,0);
        vector<int> result;
        for(int i=0;i<S.size();i++)
            last[S[i]-'a']=i;
        int end=0;
        int pre=-1;
        for(int i=0;i<S.size();i++)
        {
    
    
            end=max(end,last[S[i]-'a']);
            if(end==i)
            {
    
    
                result.push_back(end-pre);
                pre=end;
            }
        }
        return result;
    }
};

快排模板
笔记上的自己写一遍总有各种问题,还是背下来这一版本,最简洁

void quick_sort(vector<int>& q, int l, int r)
{
    
    
    if(l >= r) return;
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while(i < j)
    {
    
    
        do i++; while(q[i] < x);
        do j--; while(q[j] > x);
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

环形动态规划
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
此题是 198. 打家劫舍 的拓展版: 唯一的区别是此题中的房间是环状排列的(即首尾相接),而 198.198. 题中的房间是单排排列的;而这也是此题的难点。
环状排列意味着第一个房子和最后一个房子中只能选择一个偷窃,因此可以把此环状排列房间问题约化为两个单排排列房间子问题:
1.在不偷窃第一个房子的情况下(即 nums[1:]nums[1:]),最大金额是 p_1p
2.在不偷窃最后一个房子的情况下(即 nums[:n-1]nums[:n−1]),最大金额是 p_2p ​
。综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)max(p1,p2) 。

    int rob(vector<int>& nums) {
    
    
        if(nums.size()==0)
            return 0;
        if(nums.size()==1)
            return nums[0];
        if(nums.size()==2)
            return max(nums[0],nums[1]);
        //不偷窃第一个房子的情况
        vector<int> dp1=nums;
        dp1[0]=0;
        for(int i=2;i<nums.size();i++)
        {
    
    
            dp1[i]=max(dp1[i-1],dp1[i-2]+nums[i]);
        }
        //不偷窃最后一个房子的情况
        vector<int> dp2=nums;
        dp2[1]=max(nums[0],nums[1]);
        for(int i=2;i<nums.size()-1;i++)
        {
    
    
            dp2[i]=max(dp2[i-1],dp2[i-2]+nums[i]);
        }
        return max(dp1[nums.size()-1],dp2[nums.size()-2]);
    }

134.加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
示例 1:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
环形动态规划,首先对于这种环状数组问题常见的操作方式就是复制一遍 破环成链;
[1,2,3,4,5]变成[1,2,3,4,5,1,2,3,4,5], 再用一个长度为n的窗口 即为从不同起点开始的路径。
每一个加油站i 能到达的花费实际上是gas[i] - cost[i] 才能开到下一站
只要满足连续n个小段每一小段大于等于0就证明可以完整走完这一段路程。

class Solution {
    
    
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
    
    
        int n=gas.size();
        if(n==0)    return -1;
        vector<int> s(2*n+2,0);
        for(int i=0;i<n;i++)
        {
    
    
            s[i]=s[i+n]=gas[i]-cost[i];
        }

        int i=0;
        while(i<n)
        {
    
    
            int cnt=0;
            int sum=0;
            while(cnt<n)
            {
    
    
                sum+=s[i+cnt];
                if(sum<0)
                {
    
    
                    break;
                }
                cnt++;
            }
            if(cnt==n)
            {
    
    
                return i%n;
            }
            else
            {
    
    
                i=i+cnt+1;
            }

        }
        return -1;
    }
};

剑指 Offer 59 - II. 队列的最大值
单调双端队列
本算法基于问题的一个重要性质:当一个元素进入队列的时候,它前面所有比它小的元素就不会再对答案产生影响。

举个例子,如果我们向队列中插入数字序列 1 1 1 1 2,那么在第一个数字 2 被插入后,数字 2 前面的所有数字 1 将不会对结果产生影响。因为按照队列的取出顺序,数字 2 只能在所有的数字 1 被取出之后才能被取出,因此如果数字 1 如果在队列中,那么数字 2 必然也在队列中,使得数字 1 对结果没有影响。

按照上面的思路,我们可以设计这样的方法:从队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。

那么如何高效实现一个始终递减的队列呢?我们只需要在插入每一个元素 value 时,从队列尾部依次取出比当前元素 value 小的元素,直到遇到一个比当前元素大的元素 value’即可。

上面的过程保证了只要在元素 value 被插入之前队列递减,那么在 value 被插入之后队列依然递减。
而队列的初始状态(空队列)符合单调递减的定义。
由数学归纳法可知队列将会始终保持单调递减。
上面的过程需要从队列尾部取出元素,因此需要使用双端队列来实现。另外我们也需要一个辅助队列来记录所有被插入的值,以确定 pop_front 函数的返回值。
保证了队列单调递减后,求最大值时只需要直接取双端队列中的第一项即可。

#include<deque>
class MaxQueue {
    
    
public:
    queue<int> que;
    deque<int> due;
    MaxQueue() {
    
    
    }
    
    int max_value() {
    
    
        if(due.empty())
            return -1;
        else
            return due.front();
    }
    
    void push_back(int value) {
    
    
        que.emplace(value);
        while(!due.empty()&&due.back()<value)
            due.pop_back();
        due.push_back(value);
    }
    
    int pop_front() {
    
    
        if(que.empty())
        {
    
    
            return -1;
        }
        else
        {
    
    
            int temp=que.front();
            que.pop();
            if(due.front()==temp)
                due.pop_front();
            return temp;
        }
    }
}; 

96. 不同的二叉搜索树
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
在这里插入图片描述
给定一个有序序列 1⋯n,为了构建出一棵二叉搜索树,我们可以遍历每个数字 ii,将该数字作为树根,将 1⋯(i−1) 序列作为左子树,将(i+1)⋯n 序列作为右子树。接着我们可以按照同样的方式递归构建左子树和右子树。
leetcode官方题解

class Solution {
    
    
public:
    int numTrees(int n) {
    
    
        vector<int> G(n+1,0);//记录n个数来构建可以得到的种类
        G[0]=1;
        G[1]=1;
        for(int i=2;i<n+1;i++)
        {
    
    
            for(int j=1;j<=i;j++)//可以有i个不同根结点
            {
    
    
                    G[i]+=G[j-1]*G[i-j];
            }
        }
        return G[n];
    }
};

在这里插入图片描述

catalan卡塔兰数

猜你喜欢

转载自blog.csdn.net/weixin_43900210/article/details/113062281