说明
主要用来记录刷题时,遇到的一些小技巧,小经验。每个刷过的题目都复盘一下。废话不多说,现在直接开始。
198.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
INPUT: vector<int> val
OUTOUT: int key
思路:
动态规划。没啥好说的,转移方程
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
1
]
,
d
p
[
i
−
2
]
+
v
a
l
[
i
]
)
dp[i] = max(dp[i-1],dp[i-2]+val[i])
dp[i]=max(dp[i−1],dp[i−2]+val[i])
v
a
l
[
i
]
val[i]
val[i]是第
i
i
i个房屋的现金数目。
213.打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额.
INPUT: vector<int> val
OUTOUT: int key
思路:
与第一题的区别在于,房子是环形的。如果按照解决第一题的想法来做,就会陷入找不到转移方程(
i
−
>
i
+
1
i->i+1
i−>i+1)的境地(最后一间房屋是否被偷要考虑到第一间房屋是否被偷)。
为了解决首尾相互影响的问题,我们可以考虑分两类情况。
类型 | 情况 |
---|---|
1 | 第一个房屋不被偷 |
2 | 最后一个房屋不被偷 |
于是当第一个房屋不被偷时,我们就可以选择
[
1
,
v
a
l
.
s
i
z
e
(
)
−
1
]
[1,val.size()-1]
[1,val.size()−1]的房子进行盗窃,问题同第一题,
当最后一个房屋不被偷时,我们可以选择
[
0
,
v
a
l
.
s
i
z
e
(
)
−
2
]
[0,val.size()-2]
[0,val.size()−2]的房子进行盗窃,问题依旧同第一题。
而后我们可以比较两者的大小,取其中较大的数值。
k
e
y
=
m
a
x
(
s
u
m
(
1
,
v
a
l
.
s
i
z
e
(
)
−
1
)
,
s
u
m
(
0
,
v
a
l
.
s
i
z
e
(
)
−
2
)
key = max(sum(1,val.size()-1),sum(0,val.size()-2)
key=max(sum(1,val.size()−1),sum(0,val.size()−2)
337.打家劫舍III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。 计算在不触动警报的情况下,小偷一晚能够盗取的最高金额.
INPUT: TreeNode * root
OUTPUT: int key
思路:
这个变式是比较新颖的,代入了树的背景。不过得益于树的结构,树的问题一般都可以用递归的方式解决。
基础思想依旧是动态规划,用哈希表来辅助记录结果(以每个节点为根节点的数值)。
d
p
[
r
o
o
t
]
=
m
a
x
(
r
o
o
t
→
v
a
l
+
d
p
[
r
o
o
t
→
r
i
g
h
t
→
l
e
f
t
]
+
d
p
[
r
o
o
t
→
r
i
g
h
t
→
r
i
g
h
t
]
+
d
p
[
r
o
o
t
→
l
e
f
t
→
l
e
f
t
]
+
d
p
[
r
o
o
t
→
l
e
f
t
→
r
i
g
h
t
]
,
d
p
[
r
o
o
t
→
r
i
g
h
t
]
+
d
p
[
r
o
o
t
→
l
e
f
t
]
)
dp[root] =\\ max(root \rightarrow val+dp[root \rightarrow right \rightarrow left]+dp[root \rightarrow right \rightarrow right]\\+dp[root \rightarrow left \rightarrow left]+dp[root \rightarrow left \rightarrow right],\\dp[root \rightarrow right]+dp[root \rightarrow left])
dp[root]=max(root→val+dp[root→right→left]+dp[root→right→right]+dp[root→left→left]+dp[root→left→right],dp[root→right]+dp[root→left])
这是基础的转移方程。
里面的需要注意几个小细节:root->right == nullptr
和root->left == nullptr
需要进行判别,不可以直接两层调用,否则报错。root == nullptr
时可以直接返回0。
参考代码:
map<TreeNode *,int> mp;//用地址作为唯一的标识符
int cal(TreeNode *root)
{
if(root == nullptr)
return 0;
if(mp.find(root)!=mp.end())
return mp[root];
int temp=0;
if(root->left != nullptr)
temp+=cal(root->left->left)+cal(root->left->right);
if(root->right != nullptr)
temp += cal(root->right->right)+cal(root->right->left);
mp.insert(make_pair(root,max(cal(root->left) + cal(root->right),temp + root->val)) );
return mp[root];
}
4.寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗。
INPUT : vactor<int> nums1,nums2
OUTPUT :int key
思路:
两个排序好的数组进行组合,这种熟悉的感觉。不由得想起归并排序的做法。
直接将两个数组归并排序O(m+n)
,然后求中位数即可。
不过题目上要求O(log(m+n))
的复杂度,所以需要我们找到更优化的算法。
未完待续……
42.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
INPUT: vector<int> heights
OUTPUT: int key
思路:
这是一道极为经典的题目,也是一道有相当难度的题目。
单调栈解法:设计一个单调递减的栈,栈内存储索引
i
n
d
e
x
index
index,而单调递减是由
h
e
i
g
h
t
s
[
i
n
d
e
x
]
heights[index]
heights[index]的大小来实现。
下面直接粘贴代码来解释:
class Solution {
public:
int trap(vector<int>& height) {
if(height.size()<=2)
return 0;
stack<int> one;
int key = 0;
for(int i = 0; i < height.size(); i++)
{
if(one.empty() == 1) //栈为空直接压入数据
one.push(height[i]);
else //栈不为空,则
{
int sub_key = 0; //记录这一循环的雨水量
int num = 0;
int temp;
while(one.empty() == 0 && one.top() < height[i])
{
num++; //记录最新的数据一共超过了多少个栈内的元素
temp = one.top();
one.pop(); //弹出小于height[i]的数据
sub_key += height[i] - temp; //记录弹出数据所占那一个竖条所能存的水量
}
if(one.empty() == 1) //新来的数据很大,直接把栈清空了
{
sub_key = sub_key - num * (height[i] - temp); //计算本轮循环的雨水量,要删除,被多计数的雨水,因为之前计算雨水是以(height[i]-temp)进行计算的,而真实的数据应该是栈底的数据减去每个项目的高度来进行计算。
one.push(height[i]);
}
else //新来数据不够大,栈底还有数据
{
for(int j = 0; j < num+1;j++) //把已经计算水量的单元格,改成和新来数据一样大,后面如果有更大的数据,可以继续蓄水。
{
one.push(height[i]);
}
}
key += sub_key;
}
}
return key;
}
};
具体情况图示如下:
图中阴影面积表示由sub_key += height[i] - temp;
循环计算得来的。
而真实的阴影应该是
所以在计算完成之后,需要执行sub_key = sub_key - num * (height[i] - temp);
来消去多计算的一块容量。
当输入不是足够大时,情况如下所示:
此时由sub_key += height[i] - temp;
计算的面积就是正确的,不需要去掉多余项目。但此时还不能删除中间的较低项目,而是将4-2-1-4
统一变成4-4-4-4
如此考虑,是因为如果后面有很大的高度,那么前面依旧可以蓄水(因为左侧的高度为6,仍有2格的潜力可以蓄水)。
这就有疑问了,为啥当清空栈之后,可以不用重新加入进来,那是因为被删除的项目的蓄水潜力已经完全被挖掘了,后面的高度再高也无法增加了,因为其左侧高度已经固定了。
左右矩阵解法
其实再多考虑一些,蓄水总和是每个单元格的蓄水和。而每个单元格的蓄水能力为
v
a
l
=
m
i
n
(
l
e
f
t
M
a
x
,
r
i
g
h
t
M
a
x
)
−
h
e
i
g
h
t
[
i
]
val = min(leftMax,rightMax)-height[i]
val=min(leftMax,rightMax)−height[i]
在这里
l
e
f
t
M
a
x
leftMax
leftMax和
r
i
g
h
t
M
a
x
rightMax
rightMax是单元格左侧和右侧最高的数据点。可以通过两次遍历就能获得这两个数组。leftmax[i+1] = max(height[i] , leftmax[i])
和rightmax[i-1] = max(height[i],rightmax[i])
48. 旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
INPUT : vector<vector<int>>
OUTPUT: vector<vector<int>>
思路:
最开始的做法是严格按照定义来做,顺时针旋转90度。因为我们都了解,给定一个坐标
(
x
,
y
)
(x,y)
(x,y)就可以直接写出其旋转90度的坐标。
(
x
,
y
)
→
(
y
,
−
x
)
→
(
−
x
,
−
y
)
→
(
−
y
,
x
)
(x,y) \rightarrow (y,-x) \rightarrow (-x,-y) \rightarrow (-y,x)
(x,y)→(y,−x)→(−x,−y)→(−y,x)
这样做的思路确实是很简单,但运算上的细节是需要很多考虑,边界问题,转换问题等。
但其实这个题目的做法很巧妙。
试想一个方阵旋转90度相当于先做上下镜像对称,然后再做对角线对称。
另外,vector<int>
类的数据,可以直接通过swap
进行交换。
136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 说明: 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
INPUT : vector<int> & nums
OUTPUT: int key
思路:
这个题目的思路是比较多的,但如果只要线性时间复杂度和不使用额外空间是只有比较单一的方法。
常规方法:哈希表,对每个出现的数字记录其出现的次数,之后遍历输出即可。
高阶方法:使用位运算的性质。
a
∧
a
∧
b
=
b
a
∧
0
=
a
a \land a \land b = b\\ a \land 0 = a
a∧a∧b=ba∧0=a
显而易见,我们对数组内的各个元素进行异或运算,就可以得到只出现一次的数字。
137.只出现一次的数字II
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
INPUT : vector<int> & nums
OUTPUT : int key
思路:
有了上一题的经验,是不是可以继续用位运算?事实上并没有那么简单。位运算的性质无法应用上来。
常规解法:哈希表,依旧是可以用的。
非常规解法:因为除了目标数字之外,其他数字都是
3
3
3个,所以考虑到以某一位为
0
0
0或
1
1
1来进行分类。那么一定有一个数字是
3
3
3的倍数,而另一个不是(因为有特殊数字的影响)。
对32位依次进行计算,就可以得到特殊数字的每一位的数值。
其他的拓展思路:
有限状态机、数字电路设计法
260.只出现一次的数字III
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。 进阶:你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?
INPUT : vector<int> & nums
OUTPUT : vector<int>
思路:
大概思考一下,常规做法哈希表依旧是可以做的。然后位运算在这里面的技巧性是比较强的,我们可以多考虑一下。
位运算方法: 对
n
u
m
s
nums
nums内所有的元素进行异或运算:
s
=
a
1
∧
a
2
∧
a
3
⋯
∧
a
n
=
m
∧
n
s = a_1 \land a_2 \land a_3 \dots \land a_n = m \land n
s=a1∧a2∧a3⋯∧an=m∧n
由之前的性质,就可以得到
s
=
m
∧
n
s = m \land n
s=m∧n的结论。
之后我们假定
s
s
s的第
i
i
i位不为
0
0
0 对数组元素进行分类。
第 i i i位状态 | 组别 | 元素 |
---|---|---|
0 | A | m , a 1 , a 2 … a m m,a_1,a_2\dots a_m m,a1,a2…am |
1 | B | n , b 1 , b 2 … b n n,b_1,b_2 \dots b_n n,b1,b2…bn |
然后分别对A组和B组求异或运算,就能得到目标值 m , n m,n m,n.
34.在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置 如果数组中不存在目标值 target,返回 [-1, -1]。 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
INPUT : vector<int> nums, int target
OUTPUt vector<int> key
思路:
很显然,最暴力的方法就是遍历一次数组,在开始处记录一次,在终止处记录一次。
但这样完全就没利用,数组的排序特性。是一种可耻的浪费。
所以为了提高效率,我们可以考虑使用二分查找。思路很简单,但二分查找有很多的细节需要去考虑,这里就专门来捋一下。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> key;
int begin = 0;
int end = nums.size() - 1; //设置最开始的边界
int mid = 0;
bool f = 0;
while(begin <= end) //循环执行的条件
{
mid = (begin+end)/2; //计算中间值
if(target>nums[mid])
{
begin = mid + 1;
}
else if(target<nums[mid])
{
end = mid - 1;
}
else
{
f = 1;
break;
}
cout<<begin<<" "<<end<<endl;
}
if( f == 0)
{
key.push_back(-1);
key.push_back(-1);
return key;
}
int left = mid;
int right = mid;
while(left >=0 && nums[left] == target)
{
left--;
}
while(right <= nums.size()-1 && nums[right] == target)
{
right++;
}
key.push_back(++left);
key.push_back(--right);
return key;
}
};
当
[
b
e
g
i
n
,
e
n
d
]
[begin,end]
[begin,end]是偶数时,
m
i
d
=
b
e
g
i
n
+
e
n
d
−
1
2
mid = \frac{begin+end-1}{2}
mid=2begin+end−1,因为int型的数据是向下取整的。
偶数情况:
奇数情况:
当运算到长度较低的情况时:
n
=
3
n = 3
n=3
n
=
2
n = 2
n=2
n
=
1
n =1
n=1
记住一个准则,begin和end不参与数值的比较,只是用来计算mid的工具,mid用于对数值进行遍历,比较。
只要二分法正确,这个问题就得以解决。
74.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
INPUT: vector<int> nums
OUTPUT : vector<vector<int>>
思路:
这个题目是比较基础的回溯问题,可以试想有
n
n
n个元素,然后就是做
n
n
n次选择,是否选择当前元素?当完成这
n
n
n个选择后,就可以获得一个子集。
当然,还有更为简洁的方法,直接用一个数字(循环数字
[
0
,
2
n
−
1
]
[0,2^n-1]
[0,2n−1])的第
i
i
i位表示其是否选择第
i
i
i个元素进入子集。 这就要求数据的集合不能过大。至少是不能超过
i
n
t
int
int型数据的上界限。
141. 环形链表
给定一个链表,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。 如果链表中存在环,则返回 true 。 否则,返回 false 。 进阶: 你能用 O(1)(即,常量)内存解决此问题吗?
思路:
简单题目,随便说一下。用快慢指针,两者同时从第一个节点出发,然后快指针一次走两个节点,慢指针一次走一个节点。
p
=
p
→
n
e
x
t
→
n
e
x
t
q
=
q
→
n
e
x
t
p = p \rightarrow next \rightarrow next\\ q = q \rightarrow next
p=p→next→nextq=q→next
因为需要指向两个指针,所以一定要注意有效的条件,不可以盲目的指向。
if(fast->next != nullptr && fast->next->next != nullptr)
fast = fast->next->next;
else if(fast->next == nullptr || fast->next->next == nullptr)
return 0;
139.单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明: 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。
INPUT: string s
OUTPUT: vector<string> & wordDict
思路:
这个题目一开始是把我看懵了,看完答案之后才有了头绪。整体思路还是比较常规的。dp+哈希表常规方法。
首先呢,需要先对wordDict里有的单词进行一次统计。也就是用map<string,int>mp
进行一个统计。
然后就是设计转移方程:
for(int i = 1;i < len+1 ; i++)
{
for(int j = 0; j < i; j++)
{
string slice = s.substr(j,i-j); //二分法:对所有可能的分段点进行拆分,分为两截
if(dp[j] == 1 && mp[slice] == 1)
{
dp[i] = 1;
break;
}
}
}
56.合并区间
未完待续……
560.和为k的子数组
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
INPUT: vectot<int> nums,int k
OUTPUT: int key
思路:
最朴素的方法就是设计一个dp矩阵,
d
p
[
i
]
[
j
]
=
s
u
m
(
n
u
m
s
,
i
,
j
)
dp[i][j] = sum(nums,i,j)
dp[i][j]=sum(nums,i,j),转移方程也相对很好求解。如果有值为
k
k
k,则数值加
1
1
1.但这种方法,需要
O
(
n
2
)
O(n^2)
O(n2)的空间和时间。
我们就考虑使用哈希表简化这个步骤。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
unordered_map<int,int> mp;
int sum = 0;
int key = 0;
mp[0] = 1;
for(int i = 0; i < n; i++)
{
sum += nums[i];
if(mp[sum - k])
key = key + mp[sum - k];
mp[sum]++;
}
return key;
}
};
主要的依据如下:
s
u
m
(
n
u
m
s
,
i
,
j
)
=
s
u
m
(
n
u
m
s
,
0
,
j
)
−
s
u
m
(
n
u
m
s
,
0
,
i
)
sum(nums,i,j) = sum(nums,0,j)-sum(nums,0,i)
sum(nums,i,j)=sum(nums,0,j)−sum(nums,0,i)
621.任务调度器
给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。 然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 你需要计算完成所有任务所需要的 最短时间 。
思路:
最傻逼的方法:贪心+优先队列+缓存机制+hash表
这是笔者自己想的方法,可以说是非常傻逼了,工作量很大,思路也不是很清楚。
首先,最朴素的想法:优先安排当前任务数最多的任务执行(未进入冷却的任务),为了实现这种效果,我们采用了优先队列来存储未进入冷却的任务,进入冷却的任务不进入优先队列。当一个任务执行后,其数值-1,并且从优先队列弹出,进入buffer队列。
然后为了实现间隔时间,我们设计了一个buffer队列,当一个任务被执行后,其进入buffer队列,不参与排序,需要等待n个周期,才能再次进入备选任务优先队列。
class Solution {
public:
struct node //自己设计的结构体
{
char c;
int num;
};
struct cmp{ //自己设计的比较函数
bool operator()(node a, node b){
return a.num < b.num;
}
};
priority_queue<node, vector<node>, cmp> max_heap; //大根堆
int leastInterval(vector<char>& tasks, int n) {
map <char,int> index;
vector<node> nodes;
int num = tasks.size();
int flag = 0;
for(int i = 0; i < num; i++) //对每种任务类型进行计数
{
if(index.find(tasks[i]) == index.end())
{
index[tasks[i]] = nodes.size();
node temp;
temp.c = tasks[i];
temp.num = 1;
nodes.push_back(temp);
}
else
{
nodes[index[tasks[i]]].num++;
}
}
for(int i = 0; i < nodes.size();i++) //进入大根堆,备选
{
max_heap.push(nodes[i]);
}
deque<node> buffer; //实现缓存功能的buffer队列
node cont;
cont.c = 'a'; //特殊的空任务
cont.num = num;
buffer.assign(n,cont);
int key = 0;
//cout<<"ok";
while(num > 0)
{
//cout<<num;
int temp_num = 0;
char temp_c = 'a';
if(max_heap.empty() != 1) //备选不为空时,可以执行有效的命令
{
num--;
temp_c = max_heap.top().c;
//cout<<temp_c<<"->";
temp_num = --nodes[index[temp_c]].num;
buffer.push_back(nodes[index[temp_c]]);
}
else //执行空命令
{
buffer.push_back(cont);
//cout<<"NULL"<<"->";
}
if(max_heap.empty() != 1) //退出备选队列
max_heap.pop();
//cout<<"in";
if(buffer.front().c != 'a' && buffer.front().num != 0) //等待队列的有效任务进入备选队列
{
max_heap.push(buffer.front());
}
//cout<<buffer.size();
buffer.pop_front(); //弹出buffer队列
//cout<<"out";
key++;
}
return key;
}
};
739.每日温度
请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
INPUT: vector<int> & temperatures
OUTPUT: vector<int>
思路:
思路很简单,单调栈直接解决。也是一个经典题目。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> key;
int num = temperatures.size();
key.assign(num,0);
stack<int> st;
st.push(0);
for(int i = 1; i < num; i++)
{
while(st.empty() != 1 && temperatures[i] > temperatures[st.top()])
{
key[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return key;
}
};
维持一个单调递减的栈,并且栈内存储的是索引值,当被弹出时,可以直接修改对应天数状态。
单调栈,yyds。
补充项目
哈希表的遍历方法
map<int, int> count;
for (auto p : count) {
int front = p.frist; //key
int end = p.second; //value
}
map<int, int> mp;
for(auto it = mp.begin() ; it != mp.end() ; it++)
{
int front = it->frist; //key
int end = it->second; //value
}