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];
}
};