1. 课程表 II
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
分析:
构建一个edges与redges邻接矩阵,若edges[i]为空则表示i可以学习加入队列queue中。依次从队列queue中取出i,放入result中,然后再redges中找出依赖i的节点j,在edges[j]中删除i,若此时edges[j]为空,则将其加入queue。直到queue为空,此时result.size()=n节点数,表示可以学习完,若不等于表示不可学习。
vector<int> findOrder(int numCourses, vector<pair<int, int>>& prerequisites) {//课程表 II
vector<unordered_set<int>> edges(numCourses, unordered_set<int>());
vector<unordered_set<int>> redges(numCourses, unordered_set<int>());
for (auto edge : prerequisites) {
edges[edge.first].insert(edge.second);
redges[edge.second].insert(edge.first);
}
queue<int> qe;
vector<int> result;
for (int i = 0; i < numCourses; ++i) {
if (edges[i].size() == 0) {
qe.push(i);
}
}
while (!qe.empty()){
int tem = qe.front();
qe.pop();
result.push_back(tem);
for (auto end : redges[tem]) {
edges[end].erase(tem);
if (edges[end].size() == 0) {
qe.push(end);
}
}
}
if (result.size() == numCourses) {
return result;
}
else {
return {};
}
}
2. 二叉树展开为链表
给定一个二叉树,原地将它展开为链表。
例如,给定二叉树
1
/ \
2 5
/ \ \
3 4 6
1
\
2
\
3
\
4
\
5
\
6
分析:
后序遍历,先找到最左节点,然后将该节点的左孩子放到右孩子处,将右孩子放到左孩子的右孩子。在处理右孩子时,需要找到左孩子最右的节点。
void flatten(TreeNode* root) {//二叉树展开为链表
if (root == NULL) return;
flatten(root->left);
flatten(root->right);
TreeNode *tem = root->right;
root->right = root->left;
root->left = NULL;
while (root->right)
{
root = root->right;
}
root->right = tem;
}
3. 解码方法
一条包含字母 A-Z 的消息通过以下方式进行了编码:
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
分析:动态规划。
i位置判断能跟i-1组成字母,dp[i]+=dp[i-2]或1
i位置判断能自己组成字母,dp[i]+=dp[i-1]或1
bool isLetter(string s) {
int num = atoi(s.c_str());
if (s.size() == 2 && s[0] == '0') return false;
if (num > 26 || num < 1) {
return false;
}
else {
return true;
}
}
int numDecodings(string s) {//解码方法
int n = s.size();
if (s[0] == '0') return 0;
vector<int> help(n, 0);
for (int i = 0; i < n; ++i) {
if (i - 1 >= 0) {
string cur = "";
cur += s[i - 1];
cur += s[i];
if (isLetter(cur)) {
if (i - 2 >= 0) {
help[i] += help[i - 2];
}
else {
help[i] = 1;
}
}
}
string cur = "";
cur += s[i];
if (isLetter(cur)) {
if (i - 1 >= 0) {
help[i] += help[i - 1];
}
else {
help[i] = 1;
}
}
}
return help[n - 1];
}
4. 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
分析:
动态规划,使用一个help[0]=-1数组,保存到当前位置i能够拆分的i。若i与help中的任意一个index后的string在wordDict中,则将i加入help。若help最后一位为有n-1,则返回true,否则false。
bool wordBreak(string s, vector<string>& wordDict) {//单词拆分
int n = s.size();
vector<int> help;
help.push_back(-1);
for (int i = 0; i < n; ++i) {
int flag = 0;
for (auto index : help) {
string cur = s.substr(index + 1, i - index);
for (auto key : wordDict) {
if (key == cur) {
help.push_back(i);
flag = 1;
break;
}
}
if (flag == 1) {
break;
}
}
}
if (*(help.end() - 1) == n - 1) return true;
return false;
}
5. 乘积最大子序列
给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
分析:
动态规划,但是正负号很难处理,因此用两个dp,一个保存当前位置乘积最小子序列(可能是自身),另一个保存当前位置乘积最大子序列(可能是自身)。每次选出当前位置与当上一位置dp最大最小值相乘结果的最大最小值保存。
int maxProduct(vector<int>& nums) {
int lastMax = nums[0];
int lastMin = nums[0];
int result = nums[0];
for (int i = 1; i < nums.size(); ++i) {
int temMax = lastMax;
int temMin = lastMin;
lastMax = std::max(nums[i], std::max(nums[i] * temMax, nums[i] * temMin));
lastMin = std::min(nums[i], std::min(nums[i] * temMax, nums[i] * temMin));
result = std::max(result, lastMax);
}
return result;
}
6. 最大正方形
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4
分析:动态规划,当matrix[i][j]==1时,dp[i][j]为min{dp[i-1][j],dp[i][j-1]dp[i-1][j-1]}+1。上或左或左上中的最小值+1。
int maximalSquare(vector<vector<char>>& matrix) {//最大正方形
int n = matrix.size();
if (n == 0) return 0;
int m = matrix[0].size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
int result = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (matrix[i-1][j-1] == '1') {
dp[i][j] = std::min(dp[i - 1][j], std::min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
if (dp[i][j] > result) {
result = dp[i][j];
}
}
}
}
return result*result;
}
7. 只有两个键的键盘
最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:
Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) : 你可以粘贴你上一次复制的字符。
给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。
输入: 3
输出: 3
解释:
最初, 我们只有一个字符 ‘A’。
第 1 步, 我们使用 Copy All 操作。
第 2 步, 我们使用 Paste 操作来获得 ‘AA’。
第 3 步, 我们使用 Paste 操作来获得 ‘AAA’。
分析:这里没有插入一个A的操作。当想写i个A时,只能找之前能够i%j==0的j,然后复制粘贴,粘贴的次数为i/j-1,加上一次复制为i/j。
int minSteps(int n) {//只有两个键的键盘
if (n == 1) return 0;
if (n < 6) return n;
vector<int> dp(n + 1, n);
dp[1]=0;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= (i / 2); ++j) {
if(i%j==0){
dp[i] = std::min(dp[i], dp[j] + i / j);
}
}
}
return dp[n];
}
8. 最佳买卖股票时机含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
分析:本题共有三个状态,以后的题注意区分状态。[buy,sell,rest]。可以写出状态转移方程
buy[i] = max{rest[i-1]-price[i], buy[i-1]}
sell[i] = max{buy[i-1]+price[i], sell[i-1]}
rest[i] = max{sell[i-1], rest[i-1]}
又由于冷却状态时,rest[i] = sell[i-1]
由此可简化上式
buy[i] = max{sell[i-2]-price[i], buy[i-1]}
sell[i] = max{buy[i-1]+price[i], sell[i-1]}
int maxProfit(vector<int>& prices) {//最佳买卖股票时机含冷冻期
int n = prices.size();
if (n < 2) return 0;
if (n == 2) return std::max(0, prices[1] - prices[0]);
vector<int> buy(n, 0);
vector<int> sell(n, 0);
buy[0] = -prices[0];
buy[1] = std::max(buy[0], -prices[1]);
sell[1] = std::max(sell[0], buy[0] + prices[1]);
for (int i = 2; i < n; ++i) {
buy[i] = std::max(buy[i - 1], sell[i - 2] - prices[i]);
sell[i] = std::max(buy[i - 1] + prices[i], sell[i - 1]);
}
return sell[n - 1];
}
9. 戳气球
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
分析:
每一次最后一次运算为为i乘以最左边leftMax与最右边rightMax的值。因此可以使用逆序来将nums[]分为左右两边来处理。
用动态规划+分而治之来处理,dp[left][right]等于left到
right之间任意一个i与left和right相乘的值加上i到dp[left][i]+dp[i][right]。因此先从最小间距2开始一直到n-1进行如上迭代。
10. 最大的二维点集,右上角没有点的点集。
P为给定的二维平面整数点集。定义 P 中某点x,如果x满足 P 中任意点都不在 x 的右上方区域内(横纵坐标都大于x),则称其为“最大的”。求出所有“最大的”点的集合。(所有点的横坐标和纵坐标都不重复, 坐标轴范围在[0, 1e9) 内)
如下图:实心点为满足条件的点的集合。请实现代码找到集合 P 中的所有 ”最大“ 点的集合并输出。
分析:可以将x按照降序排序,将y按照升序排序。然后从前往后依次比较y,若 y i > max y_i>\max yi>max则满足条件,然后令 max = y i \max=y_i max=yi。
11. 判断一个数字是否为回文数
每次取第一位跟最后一位比较是否相同,采用除或取余操作。
12. 给定N个整数组成的序列,编程计算该序列的最优M段分割,使M段子序列的和的最大值达到最小
动态规划:
构造一个矩阵dp[n][m],dp[i][j]表示索引i及其之前的数分成j组的最大值。因此有如下状态转移方程:
j
=
1
:
d
p
[
i
]
[
1
]
=
d
p
[
i
−
1
]
[
1
]
+
n
u
m
[
i
]
j
≠
1
:
d
p
[
i
]
[
j
]
=
m
i
n
{
m
a
x
{
d
p
[
i
]
[
1
]
−
d
p
[
k
]
[
1
]
,
d
p
[
k
]
[
j
−
1
]
}
}
j=1: dp[i][1] = dp[i-1][1] + num[i]\\ j\neq 1: dp[i][j] = min\{max\{dp[i][1] - dp[k][1],dp[k][j-1]\}\}
j=1:dp[i][1]=dp[i−1][1]+num[i]j̸=1:dp[i][j]=min{max{dp[i][1]−dp[k][1],dp[k][j−1]}}
dp[i][1]-dp[k][1]可以理解为将k+1~i分成第j组,然后与前k个数组成j-1组的最大值比较取大。然后遍历所有可能的k,取最小值。
13. LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
分析:
为快速添加和删除,我们需要使用链表来设计cache,链表中从头到尾的数据顺序依次是:(最近访问)->…(最旧访问);
因为是最近访问的在链表的最前,我们使用list来保存key值,而使用hashmap来保存(key,value)对即可,同时需要对value进行改造,因为我们还需要将每一个value与list中的key结点进行绑定,组成pair,以便每次操作进行list的最近访问的调整;
添加节点put(key, value):
判断hashmap中是否存在key节点,如果存在则调整List,将key置为头部(最近访问),更新hashmap指向key节点的指针;
如果不存在key值对,则判断capacity是否满容,若不满容量则直接在list和hashmap中加入新节点(list在表头添加);否则则删除list末尾key和hashmap中的对应值对,再进行添加。
新节点插入到表头即可,时间复杂度O(1)。
查找节点get(key):
在hashmap中查询,若不存在则返回-1;
若存在,则调整list位置更新hashmap对应指针,返回key对应的value;
每次节点被查询到时,将节点移动到链表头部,时间复杂度O(1)。
class LRUCache {
public:
LRUCache(int capacity) {
_capacity = capacity;
}
int get(int key) {
auto it = cache.find(key);
if (it == cache.end()) {//不在缓存中
return -1;
}
else {//在缓存中
adjust(it);//更新key在链表中的位置
return it->second.first;;
}
}
void put(int key, int value) {//判断是否在,若不在,判断是否满,如果满了删了再加。若在,则提前
auto it = cache.find(key);
if (it == cache.end()) {//不在
if (cache.size() < _capacity) {//不满直接加
used.push_front(key);
valListPair tem;
tem.first = value;
tem.second = used.begin();
cache[key] = tem;
}
else {
cache.erase(used.back());//map可以直接删key
used.pop_back();
used.push_front(key);
cache[key] = { value,used.begin() };
}
}
else{//key在缓存中,只需要调整位置
adjust(it);//将key调整到链表的最前端
it->second.first = value;//更新key对应的value
}
}
private:
int _capacity;
typedef pair<int, list<int>::iterator> valListPair;
typedef unordered_map<int, valListPair> keyValMap;
list<int> used;
keyValMap cache;
void adjust(keyValMap::iterator it) {//将命中的key移到链表的最前面
int key = it->first;
used.erase(it->second.second);
used.push_front(key);
it->second.second = used.begin();
}
};
14. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
分析:设计左右指针lindex,rindex,保存左右指针遇到过的最大值lmax,rmax。左右指针同时向中间移动,若lmax<rmax,water = lmax-high[lindex];lindex++,否则water=rmax-high[rindex];rindex–。
int trap(vector<int>& height) {//接雨水
if (height.size() <= 1) return 0;
int lindex = 0;
int rindex = height.size() - 1;
int lmax = 0;
int rmax = 0;
int result = 0;
while (lindex<=rindex)
{
lmax = std::max(height[lindex], lmax);
rmax = std::max(height[rindex], rmax);
if (lmax <= rmax) {
result += lmax - height[lindex];
lindex++;
}
else {
result += rmax - height[rindex];
rindex--;
}
}
return result;
}