VVbuys Blog

standalone Linux lover

LeetCode题目 相关题目类型 相关链接
455 分发饼干(简单难度) 383. 赎金信 - 力扣(LeetCode) (leetcode-cn.com)

为了了满足更多的小孩,就不要造成饼干尺寸的浪费。

大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。

这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩

可以尝试使用贪心策略,先将饼干数组和小孩数组排序。

然后从后向前遍历小孩数组,用大饼干优先满足

image-20211029212703299

这个例子可以看出饼干9只有喂给胃口为7的小孩,这样才是整体最优解,并想不出反例,那么就可以撸代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 时间复杂度:O(nlogn)
// 空间复杂度:O(1)
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int index = s.size() - 1; // 饼干数组的下标
int result = 0;
for (int i = g.size() - 1; i >= 0; i--) {
if (index >= 0 && s[index] >= g[i]) {
result++;
index--;
}
}
return result;
}
};

从代码中可以看出我用了一个index来控制饼干数组的遍历,遍历饼干并没有再起一个for循环,而是采用自减的方式,这也是常用的技巧。

有的同学看到要遍历两个数组,就想到用两个for循环,那样逻辑其实就复杂了。

也可以换一个思路,小饼干先喂饱小胃口

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(),g.end());
sort(s.begin(),s.end());
int index = 0;
for(int i = 0;i < s.size();++i){
if(index < g.size() && g[index] <= s[i]){
index++;
}
}
return index;
}
};
LeetCode题目 相关题目类型 相关链接
1005 K次取反后最大化的数组和(简单难度) 1005. K 次取反后最大化的数组和 - 力扣(LeetCode) (leetcode-cn.com)

本题思路其实比较好想了,如何可以让数组和最大呢?

贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。

局部最优可以推出全局最优。

那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。

虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。

我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!

那么本题的解题步骤为:

  • 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和

对应C++代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
static bool cmp(int a,int b){
return abs(a)>abs(b);
}

public:
int largestSumAfterKNegations(vector<int>& A, int K) {
//将数组按照绝对值大小,从大到小排列
sort(A.begin(),A.end(),cmp);
//如果数组中有负数,转为正数,同时k--
for(int i=0;i<A.size()-1;i++){
if(A[i]<0&&K>0){
A[i]*=-1;
K--;

}
}
//如果k不为0,把数组中最小的数反复乘以-1,直到k用完
//K%2==0的话,数组中的数还是原数
if(K%2==1){
A[A.size()-1]*=-1;
}
//求和
int res=0;
for(int a:A){
res+=a;
}
return res;

}
};
LeetCode题目 相关题目类型 相关链接
860 柠檬水找零(简单难度) 860. 柠檬水找零 - 力扣(LeetCode) (leetcode-cn.com)

本题只需维护三种金额的数量:5,10,20,

情况一:账单是5,直接收下。

情况二:账单是10,消耗一个5,增加一个10

情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5

(收款20时,局部最优:优先消耗10和5,完成本次找零。少消耗5,因为5可以给10和20找零。全局最优:完成全部账单的找零。

局部最优可以推出全局最优,且找不到反例,就可以尝试贪心算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
int five=0,ten=0,twenty=0;
for(int bill:bills){
//收到5
if(bill==5){
five++;
}
//收到10
else if(bill==10){
if(five<=0){
return false;
}
else{
ten++;
five--;
}

}
//收到20
else if(bill==20){
if(five>=1&&ten>=1){//优先用10来找零
five--;
ten--;
twenty++;//可以不记录20块钱的个数,不用20找零
}
else if(five>=3){
five-=3;
twenty++;//可以不记录20块钱的个数,不用20找零
}
else{
return false;
}
}
}
return true;
}
};
LeetCode题目 相关题目类型 相关链接
53 最大子序和(简单难度) 53. 最大子序和 - 力扣(LeetCode) (leetcode-cn.com)

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。

中间的最大和由result记录下来了。

题目中的答案有个示例【-2,-1】

if(result<count)和if(count<=0),写的时候我颠倒了,会改变了count的值,res就记录不到正确的最大值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;
for(int i=0;i<nums.size();i++){
count+=nums[i];
if(result<count){
result=count;
}
if(count<=0){
count=0;
}
}
return result;
}
};
LeetCode题目 相关题目类型 相关链接
452 用最少数量的箭引爆气球(中等难度) 452. 用最少数量的箭引爆气球 - 力扣(LeetCode) (leetcode-cn.com)

直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少

试一下举反例,发现没有这种情况。

那么就试一试贪心吧!

局部最优:当气球出现重叠,一起射,所用弓箭最少。

全局最优:把所有气球射爆所用弓箭最少。

为了让气球尽可能的重叠,需要对数组进行排序。用到sort函数

这里的排序不是简单的对整数排序,是按照数组的第一元素从小到大排序,不能简单地用sort(nums.begin(),nums.end()),

而是用sort(nums.begin(),nums.end(),cmp),cmp函数必须加上static,返回值是bool类型。例如:

static bool cmp(vector& a,vector& b){

​ return a[0]<b[0];

}

传入参数时,如果是数组,用引用&不会另外开辟内存,而是直接用原数组的数进行运算。不用引用&,会为参数另外开辟内存,每次用cmp函数都要花时间开辟内存,可能会运行超出时间限制。

按照气球的起始位置排序,从前向后遍历气球数组,尽可能让气球重复,前两个区间:比较第一个的右边界>=第二个的左边界,则气球重叠。有两个已经重叠,再比较第三个时,要比较前两个区间的最小右边界和第三个区间的左边界。因为如果想三个都重叠,必须满足最小的右边界(画图可以看出)。

前两个重叠时,不用弓箭加一,因为刚开始默认弓箭为1(因为数组数>=1).不重叠,那肯定需要再有一个弓箭射后面的气球,这时弓箭数加一。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
static bool cmp(vector<int>& a,vector<int>& b){
return a[0]<b[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
/*输入一个二维数组,输出一个整数*/
if(points.size()==1){
return 1;
}
// 先将二维数组排序
sort(points.begin(),points.end(),cmp);
int result=1;// 需要几只箭
for(int i=1;i<points.size();i++){
// 会发生重叠
if(points[i-1][1]>=points[i][0]){
points[i][1]=min(points[i][1],points[i-1][1]);// 更新当前气球的右边界
}
// 没有重叠
else{
result++;
}

}
return result;
}
};
LeetCode题目 相关题目类型 相关链接
435 无重叠区间(中等难度) 435. 无重叠区间 - 力扣(LeetCode) (leetcode-cn.com)

本题与i452的区别:

移除重叠区间,按照右边界从小到大排序,如果前一个的右边界大于下一个的左边界,则算是重叠,然后保留重叠两个区间的右边界最小值(较大的可能会重叠更多区间,题目要求是移除区间的最小数量)

cmp两种都可以,第二种,考虑了如果两个元素相等,再怎么判断。但a[0]<b[0];应该会对相等情况给一个结果应该。

1
2
3
4
 static bool cmp(vector<int>& a,vector<int>& b){

return a[0]<b[0];
}

 static bool cmp(vector<int>& a,vector<int>& b){
    if(a[0]==b[0]){
         return a[1]<b[1];
    }
    return a[0]<b[0];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
static bool cmp(vector<int>& a,vector<int>& b){

if(a[0]==b[0]){
return a[1]<b[1];
}
return a[0]<b[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
/*输入一个二维数组,输出一个整数*/
if(points.size()==1){
return 1;
}
// 先将二维数组排序
sort(points.begin(),points.end(),cmp);
int result=1;// 需要几只箭
for(int i=1;i<points.size();i++){
// 会发生重叠
if(points[i-1][1]>=points[i][0]){
points[i][1]=min(points[i][1],points[i-1][1]);// 更新当前气球的右边界
}
// 没有重叠
else{
result++;
}

}
return result;
}
};
LeetCode题目 相关题目类型 相关链接
56 合并区间(中等难度) 56. 合并区间 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static bool cmp(vector<int> &a,vector<int> &b){
return a[0]<b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
/*输入一个二维数组,输出一个二维数组*/
if(intervals.size()==1){
return intervals;
}
//对数组进行排序
sort(intervals.begin(),intervals.end(),cmp);
//
vector<vector<int>> result;
bool flag = false;// 标记最后一个区间有没有合并
for(int i=1;i<intervals.size();i++){
// 当相邻区间重叠时,合并,当前区间i就是合并后的区间
int start=intervals[i-1][0];
int end = intervals[i-1][1];

while(i<intervals.size()&&end>=intervals[i][0]){
end = max(intervals[i][1],end);
if (i == intervals.size() - 1) flag = true;
i++;
}
result.push_back({start,end});
}
// 如果最后一个区间没有和前一个区间重叠,将需单独加入result
if (flag == false) {
result.push_back({intervals[intervals.size() - 1][0], intervals[intervals.size() - 1][1]});
}
return result;
}
LeetCode题目* 相关题目类型 相关链接
55 跳跃游戏(中等难度) 55. 跳跃游戏 - 力扣(LeetCode) (leetcode-cn.com)

该题判断是否能跳到终点。

刚看到本题一开始可能想:当前位置元素如果是3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?

其实跳几步无所谓,关键在于可跳的覆盖范围!不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。

这个范围内,别管是怎么跳的,反正一定可以跳过来。

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
bool canJump(vector<int>& nums) {
int cover = 0;
if (nums.size() == 1) return true; // 只有一个元素,就是能达到
for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
cover = max(i + nums[i], cover);
if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
}
return false;
}
};
题目 相关链接
763划分字母区间(中等难度)

记录每个字符在数组中出现的最远距离,找到之前遍历过的所有字母的最远边界,这个边界点就是分割点。前面出现的所有字母最远就到这个边界。

分为两步:

1 统计每个字符出现的最远位置

2 从头遍历字符,更新字符出现的最远下表,如果找到字符最远出现位置下表和当前下标相等,则找到了分割点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution{
public:
vector<int> partitionLabels(string s){
// 记录每个字符出现的最远下标
int hash[27]={0};
for(int i=0;i<s.size();i++){
hash[s[i]-'a']=i;
}
vector<int> res;
for(int i=0;i<s.size();i++){
right=max(right,hash[s[i]-'a']);
if(i==right){
res.push_back(right-left+1);
left=i+1;
}
}
return res;

}

};
LeetCode题目* 相关题目类型 相关链接
135 (中等难度) 55. 跳跃游戏 - 力扣(LeetCode) (leetcode-cn.com)

这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。

先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

2.再确定左孩子大于右孩子的情况(从后向前遍历)

遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢?

因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了。

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。

那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

整体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candyVec(ratings.size(), 1);
// 从前向后
for (int i = 1; i < ratings.size(); i++) {
if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
}
// 从后向前
for (int i = ratings.size() - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1] ) {
candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
}
}
// 统计结果
int result = 0;
for (int i = 0; i < candyVec.size(); i++) result += candyVec[i];
return result;
}
};

​ 《Adaptive Neural Trees》是来自英国伦敦的Ryutaro Tanno等人发表在ICML 2019(CCF推荐的A类会议)上的一篇论文,这里是原文链接原文代码

摘要

​ 深层神经网络和决策树在很大程度上依赖于不同的范式;通常,前者使用 预先指定的体系结构进行表征学习,而后者通过使用数据驱动的体系结构学习预先指定的特征的层次结构来实现。本文通过自适应神经树(ANTs)将这两种方法结合起来,该算法将表征学习融入决策树的边缘(edges)、路由函数(routing functions)和叶节点(leaf nodes),以及基于反向传播的训练算法,该算法从原始模块(例如卷积层)自适应地增长体系结构。本文证明,ANTs不仅在实现分类和再分类数据集上具有竞争性能,而且具备如下好处:

(i)通过条件计算进行轻量级推理;

(ii)对任务有用的特征进行更高层次的分离,例如学习有意义的类关联,如分离自然和人造对象;

(iii)使体系结构适应训练数据集大小和复杂性的机制。

1、简介

​ 神经网络(NNs)和决策树(DTs)都是强大的机器学习模型,在学术和商业应用中都取得了成功。然而,这两种方法通常具有相互排斥的优点和局限性。

模型 优点 局限性 常见改进思路
神经网络(NN) 自动学习数据的分层表示,减少了对数据特征工程的需求;使用随机优化法进行训练,可以扩展到大型数据集;在现代硬件加持下可以在众多问题中取得前所未有的精度。 体系结构通常需要针对特定任务或数据集进行设计或修改;大型模型具备重量级推理,计算量很大 知识蒸馏、迁移学习
决策树(DT) 自动学习分层数据集群和如何分割输入空间,具备高度可解释性;体系结构可以根据训练数据进行优化;轻量级推理 需要手动设计好的数据特征,路径函数表达能力不如神经网络,无法直接使用神经网络的优化方法, 增加决策树数量的方法,如随机森林等

​ 该文的工作的目标是结合NNs和DTs,以获得这两种方法的互补优势。

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的改进.png)

​ 根据该文介绍,ANTs从DTs和NNs继承了以下理想特性:

​ (1)表征学习:由于ANTs的每个根到叶路径都是一个神经网络,因此可通过基于梯度的优化进行端到端的特征学习。

​ (2)架构学习:通过逐渐增长的ANTs,架构适应数据集的可用性和复杂性,增长过程可以看作是对模型类具有硬约束的体系结构搜索。

​ (3)轻量级推理:在推理时,ANTs进行条件计算,在每个样本的基础上选择树上的单个根到叶路径,只激活模型参数的一个子集。

​ 该文通过基于如下三个数据集的实验验证了ANTs在回归任务和分类任务的高性能表现:

数据集 作者年份 论文题目
MNIST分类数据集 LeCun等人,1998年 《Gradient-based learning applied to document recognition》
CIFAR-10分类数据集 Krizhevsky和Hinton,2009年 《Learning multiple layers of features from tiny images》
SARCOS多元回归数据集 Vijayakumar和Schaal,2000年 《Locally weighted projection regression: An o(n) algorithm for incremental real time learning in high dimensional space》

​ 实验结果显示,在SARCOS多元回归数据集上性能最好的方法都是基于树的,ANTs的在均方误差(MSE)指标上最小;ANT在图像分类方面远远优于最先进的深度随机森林RF(Zhou&Feng,2017)和GBT(Ponomareva等人,2017)方法,其结构在MNIST上的准确率超过99%,在CIFAR-10上的准确率超过90%。

2、相关工作

​ 决策树是机器学习中的一种预测模型,常应用于数据挖掘中。模糊决策树是传统决策树的一个扩充和完善,使决策树可以处理不确定性。

软决策树是模糊决策树中的一类,通过结合树的生长和修剪来确定软决策树的结构,并对树进行了修正来提高它的泛化能力。软决策树的决策准则是一个范围区间,而非一个特定的数值。相比较于标准决策树,软决策树的分类结果更准确。

​ 决策树和神经网络结合的相关工作:

作者年份 论文题目 主要贡献
Jordan和Jacobs,1994 Hierarchical mixtures of experts and the em algorithm 提出了一种树结构的监督学习体系结构:分层混合专家(HME),其模型的学习被视为一个最大似然问题,然后使用一种期望最大化(EM)算法来调整结构的参数。该模型中树的分割不是硬决策(hard decision),而是由软概率(soft probabilistic)决定的,但是其树结构是固定的,路由器是简单的线性分类器。
Suarez和Lutsko ,1999 Globally optimal fuzzy decision trees for classification and regression 提出了一种新颖的模糊决策树,其通过在CART决策树的骨架上叠加模糊结构将决策树转换为一种强大的函数逼近,并可以使用一种自动生成方法来处理分类和回归问题。
Irsoy等人,2012 Soft decision trees 提出了一种新的决策树结构,其既具备HME的软决策,又可以在需要时添加新节点,并使用梯度下降学习参数。
Irsoy等人,2014 Budding trees 提出了一种萌芽树BT,其中一个节点既可以是叶子,也可以是内部决策节点。每个花蕾节点从一个叶节点开始,然后可以生长子节点,但随后,如果必要,可以修剪其子节点。
Laptev和Buhmann, 2014. Convolutional decision trees for feature learning and segmentation 提出了一种通用的分割算法ConvDT,在构建多元决策树的同时,将信息量最大、可解释性最强的特征提取为卷积核。该算法的训练速度比常规CNN快几个数量级,并在基准数据集的处理质量方面达到了最先进的水平。
Rota Bulo和Kontschieder, 2014 Neural decision forests for semantic image labelling 提出了神经决策森林NDT,用于分割语义图像;该文章通过引入随机多层感知器(rMLP)作为新的分割节点来弥补这一差距,该节点可以学习非线性、数据特定的表示,并通过为新出现的子节点找到最佳预测来利用它们。
Kontschieder等人,2015 Deep neural decision forests 提出了一种新的深度决策森林NDF,通过端到端的方式对分类树进行训练,将分类树与深度卷积网络中已知的表示学习功能统一起来。
Leon和Denoyer, 2016 Policy-gradient methods for decision trees. 提出了一种新的强化决策树(RDT),主要思想是将分类问题视为一个序贯决策过程,通过直接求解全局(可导)目标函数,而不使用基于启发式的贪婪技术来学习分类策略;
Ioannou等人,2016 Decision forests, convolutional networks and the models 将决策森林和卷积神经网络结合在一起,提出了条件网络(CNet)。
Frosst和Hinton,2017 Distilling a neural network into a soft decision tree 使用软决策树来从一个训练好的神经网络中提取知识,从而尝试解释神经网络的分类决策,其结果证明,这种软决策树比直接从训练数据中学习的软决策树具有更好的泛化能力。
Xiao ,2017 Neual decision tree towards fully functioned neural graph 提出了一种神经决策树的变体NDT,其在反向传播过程中,可以近似地计算出特定条件变量的梯度,从而生成一个功能完备的神经网络图。

3、自适应神经树

​ ANT使用了自己的一套术语来定义其结构,其结构基本统一了许多现有的树结构模型。

​ 首先ANT的学习仍然是一种监督学习,其训练数据简记为一组N个标记过的样本:$\left(\mathbf{x}^{(1)}, \mathbf{y}^{(1)}\right), \ldots,\left(\mathbf{x}^{(N)}, \mathbf{y}^{(N)}\right) \in \mathcal{X} \times \mathcal{Y}$,ANT的目标是从中学习一个条件分布 $p(\mathbf{y} \mid \mathbf{x})$。

​ 该文将ANT定义为一个二元组 $(\mathbb{T}, \mathbb{O})$,其中$\mathbb{T}$表示模型的拓扑结构(本质是一个有限图结构),$\mathbb{O}$表示其拓扑结构上进行的操作集(本质是一系列非线性函数)。

(1)拓扑结构$\mathbb{T}$

拓扑结构$\mathbb{T}$定义为$\mathbb{T}:={\mathcal{N}, \mathcal{E}}$,其中$\mathcal{N} $是所有节点的集合,$\mathcal{E}$是所有节点边的集合,

ANT的拓扑结构也是基于二叉树的图结构,除了顶部根节点外,每个节点都是一个父节点的子节点。

种类 符号表示 定义和其他标准树对比
内部节点 $j\in \mathcal{N}_{\text {int }}$ 基本相同,每个内部节点$j\in \mathcal{N}_{\text {int }}$正好有两个子节点,由$\operatorname{left}(j)$和$\operatorname{right}(j)$表示。
叶子节点 $l \in \mathcal{N}_{\text {leaf }}$ 基本相同,没有子节点的节点叫做叶节点$\mathcal{N}_{\text {leaf }}$
$e \in \mathcal{E}$ 略有不同,$\mathcal{E}$包含输入数据$\mathbf{x}$到根节点的边。

(2)操作集$\mathbb{O}$

操作集$\mathbb{O}$定义为一个三元组$\mathbb{O}=(\mathcal{R}, \mathcal{T}, \mathcal{S})$,其所有操作都由三个可微模块构建:

可微模块 模块特点 内部计算
路由器(Routers,$\mathcal{R}$) 每个内部节点$j\in \mathcal{N}_{\text {int }}$对应一个路由器模块,其负责将来自传入边的样本发送到左子节点或右子节点。 $r_{j}^{\theta}: \mathcal{X}{j} \rightarrow[0,1] \in \mathcal{R} $、 由$\theta$参数化,其中$\mathcal{X}{j}$表示节点j处的输入,对于每个$\mathbf{x}{j} \in \mathcal{X}{j}$,输入$r_{j}^{\boldsymbol{\theta}}$(可以是一个小的CNN),得到输出值$r_{j}^{\boldsymbol{\theta}}\left(\mathbf{x}{j}\right)$求若干个$r{j}^{\boldsymbol{\theta}}\left(\mathbf{x}_{j}\right)$的均值,然后构建一个伯努利分布,从分步中采样一个一个0或1的决策(左分支为1,右分支为0)
转换器(Transformers,$\mathcal{T}$) 每条边$e \in \mathcal{E}$对应一或多个转换器模块,每个转换器负责将前一个模块的样本转换为下一个模块的样本。 每个转换器$t_{e}^{\psi} \in \mathcal{T}$是一个非线性函数,由$\psi$参数化。其形式可以是一个卷积层和ReLU。和标准的决策树不同,其每条边允许通过添加更多转换器来“增长”。
求解器(Solvers,$\mathcal{S}$) 每个叶节点$l \in \mathcal{N}_{\text {leaf }}$对应一个求解器模块,其负责将输入数据转换为一个条件分布$p(\mathbf{y} \mid \mathbf{x})$的近似值。 $s_{l}^{\phi}: \mathcal{X}{l} \rightarrow \mathcal{Y} \in \mathcal{S}$、 参数化为$\phi$,对于分类任务,$s^{\phi}$可以定义为特征空间$\mathcal{X}{l}$上的线性分类器,该分类器输出类的分布。

整个ANT树的结构由下图所示:

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT树的结构.png)

在上图中,黑色圆圈表示边上的转换器,白色圆圈表示内部节点上的路由器,灰色圆圈表示叶子节点上的求解器。

以红色阴影路径为例,其计算如下

输入$\mathbf{x}$经历一系列转换器和路由器之后:$\mathbf{x} \rightarrow \mathbf{x}{0}^{\psi}:= t{0}^{\psi}(\mathbf{x}) \rightarrow \mathbf{x}{1}^{\psi}:=t{1}^{\psi}\left(\mathbf{x}{0}^{\psi}\right) \rightarrow \mathbf{x}{4}^{\psi}:=t_{4}^{\psi}\left(\mathbf{x}_{1}^{\psi}\right) $,

经过求解器模块产生预测分布:$p_{4}^{\phi, \dot{\psi}}(\mathbf{y}):=s_{4}^{\phi}\left(\mathbf{x}_{4}^{\psi}\right)$。

选择这条路径的概率:$\pi_{2}^{\psi, \theta}(\mathbf{x}):= r_{0}^{\boldsymbol{\theta}}\left(\mathbf{x}{0}^{\boldsymbol{\psi}}\right) \cdot\left(1-r{1}^{\boldsymbol{\theta}}\left(\mathbf{x}_{1}^{\boldsymbol{\psi}}\right)\right)$。

(3)概率模型

假设我们有L个叶节点,参数$ \Theta=(\boldsymbol{\theta}, \boldsymbol{\psi}, \boldsymbol{\phi})$,其中$\boldsymbol{\theta}, \boldsymbol{\psi}, \boldsymbol{\phi}$分别表示树中路由器、转换器和解算器模块的参数,则完整的概率分布预测计算由叶子分配概率$\pi_{l}^{\theta, \psi} $和叶子预测概率$p_{l}^{\phi, \psi}$组成:

![](/img-post/论文分享/2021-10-29-自适应神经树/全预测分布公式.png)

其中$\mathbf{z} \in{0,1}^{L}$是一个L维的二元潜在变量,并且满足$\sum_{l=1}^{L} z_{l}=1$,其用来描述叶节点的选择(例如,$z_{l}=1$表示使用叶节点L)。

A 叶子分配概率$\pi_{l}^{\theta, \psi} $的计算

$\pi_{l}^{\theta, \psi}(\mathbf{x}):=p\left(z_{l}=1 \mid \mathbf{x}, \boldsymbol{\psi}, \boldsymbol{\theta}\right)$量化了x被分配给叶l的概率,其由从根节点到叶节点$l$的唯一路径$P_{1}$上所有路由器模块的决策概率的乘积给出:

![](/img-post/论文分享/2021-10-29-自适应神经树/某条路径的概率.png)

其中$l \swarrow j$是表示叶子结点$l$是否在内部节点$j$的左子树中,$\mathbf{x}_{j}^{\psi}$是$\mathbf{x}$在节点$j$下的特征表示。

假设$\mathcal{T}{j}=\left{t{e_{1}}^{\psi}, \ldots, t_{e_{n}}^{\psi}\right}$表示从根节点到节点j的路径上的n个转换器模块的有序集,特征向量$\mathbf{x}_{j}^{\psi}$计算如下:

$\mathbf{x}{j}^{\psi}:=\left(t{e_{n}}^{\psi} \circ \ldots \circ t_{e_{2}}^{\psi} \circ t_{e_{1}}^{\psi}\right)(\mathbf{x})$

B 叶子预测概率$p_{l}^{\phi, \psi}$的计算

叶子预测概率$p_{l}^{\phi, \psi}$的计算如下:

$p_{l}^{\phi, \boldsymbol{\psi}}(\mathbf{y}):=p\left(\mathbf{y} \mid \mathbf{x}, z_{l}=1, \boldsymbol{\phi}, \boldsymbol{\psi}\right)$

其预测了一个叶子节点l上的求解器$s_{l}^{\phi}\left(\mathbf{x}_{\text {parent }(l)}^{\psi}\right) $在目标$\mathbf{y}$上的近似值

(4)推理方案

该文在准确率和计算力的权衡上考虑了两个推理方案,分别是多路径推理和和单路径推理。

推理方案 预测分布的计算方式 特点
多路径推理 使用完整地概率预测分布,其需要平均所有叶节点上的分布,包括计算树的所有节点和边上的所有操作, 对于大型ANT来说是必要的
单路径推理 使用其中一条特殊路径的预测分布,该特殊路径通过贪婪地在路由器的最高置信度方向遍历树的决策而得到 将计算限制在一条路径上,从而实现更节省内存和时间的推理

4、优化

ANT的训练分两个阶段进行:

1)生长阶段,局部优化学习模型架构;

2)细化阶段,全局优化调整模型参数。

其损失函数使用负对数似然(NLL):

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的损失函数.png)

由于ANT的所有组件模块都是可微的,所以该方法可以使用基于梯度的优化。

(1)生长阶段

生长阶段的目的是局部优化模型的结构。

其方法是首先按广度优先顺序选择一个叶节点,对于选择的每个叶节点都可以使用如下3类评估:

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的生长阶段.png)

固定计算图的前一部分,局部优化新添加的(1)和(2)操作,然后计算各自损失值,如果损失值得到减少,则选择该类型的操作否则保留原来的模型。

(2)细化阶段

生长阶段确定了模型的拓扑结构后,然后继续执行全局优化来优化模型的整体参数。

5、实验

(1)实验设置

![](/img-post/论文分享/2021-10-29-自适应神经树/实验设置.png)

(2)消融实验

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的消融实验.png)

(3)SARCOS多元回归实验的性能对比

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的SARCOS回归实验.png)

(4)MNIST数字分类实验的性能对比

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的MNIST实验.png)

(5)CIFAR-10图像分类实验的性能对比

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的CIFAR-10实验.png)

(6)模型的可解释性

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的可解释性.png)

(7)细化阶段的影响

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的细化阶段的影响.png)

(8)自适应模型的复杂性

![](/img-post/论文分享/2021-10-29-自适应神经树/ANT的自适应模型复杂性.png)

6、总结

​ 本文引入了自适应神经树(ANTs),将决策树(DTs)的体系结构学习、条件计算和层次聚类与深层神经网络(DNN)的层次表示学习和梯度下降优化相结合,结合本文提出的训练算法来渐进式增长和优化ANTs的参数和结构。
​ 实验证明ANTs在回归(SARCOS数据集)和分类(MNIST和CIFAR10数据集)方面的优势,同时仍然实现了高性能。

​ ANT模型和shortcut connections的结合可能使ANT在图像分类上的性能进一步提升。

c++ string的详细用法

string a=”123”;

1.在字符串末尾添加一个字符

a.push_back(‘3’); //结果为 a=”1233”;

2.在字符串末尾删除一个字符

a.pop_back(); //结果为 a=”12”;

3.assign()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//字符串变量
string a="123";
string b="456";

1.字符串直接赋值
a.assign(b); //等于a=b赋值,结果为 a="456"
a.assign("789");//结果为 a="789"

2.一个字符串的子串赋值给另一个字符串
a.assign(b,begin,len);
//从字符串b的第(begin)个字符开始向后数(len)个字符(包括begin)的字符串赋值给字符串a

例:
a.assign(b,0,1); //结果为 a="4"
a.assign(b,1,1); //结果为 a="5"
a.assign(b,0,2); //结果为 a="45";
a.assign(b,1,2); //结果为 a="56";
a.assign("123456",1,3); //结果为 a="234";
/*说明
*如果第三个参数大于字符串本身的长度,则赋值到该字符串末尾
*如:a.assign(b,1,4); 结果为 a="56";
*如果第二个参数大于字符串本身长度则赋值为空
*如:a.assign(b,3,4); 结果为 a="";
*第二个参数最大不能超过字符串长度加一,否则程序会运行错误。因为string字符串后面会有一个"\n"符号,这个符号虽然不算在字符串长度里面,但是会占一个字符的空间。所以超过字符串长度加一后会出现std::out_of_range的错误。
*/

3.从字符串的某一个字符串开始到结束赋值给另一个字符串
a.assign(b,begin);
//从字符串b的第begin字符串开始到字符串结束赋值给字符串a

a.assign(b,0); //结果为 a="456";
a.assign(b,1); //结果为 a="56";
a.assign(b,4); //error 这样会运行错误(同2) vs2019

4.从字符串开始到字符串的某一个字符赋值给另一个字符串
a.assign("123456",2);//结果为 a="12"
string c="123456";
a.assign(c,2); //结果为 a="3456"
//根据vs2019运行情况来看,如过第一个参数是字符串常量则按从头到第n个字符赋值,如果第一个参数是字符串变量的话,则从第n个字符开始到字符串结尾赋值(不知道其他编译会是怎么样的,vs2019是调用不同的重载函数)

5.用相同的n个字符给字符串赋值
a.assign(10,'b'); //结果为 a="bbbbbbbbbb";
a.assign(5,'c'); //结果为 a="ccccc";

6.template<class inputIterator> string& assign(inputIterator first,inputIterator last);
//使用这个函数需要引入#include<iterator>头文件
a.assign(istream_iterator<char>(cin), istream_iterator<char>());
//键盘输入123abcd
//结果为 a="123abcd";
/*注意
*该函数不接收空格换行等符号,最后(windows系统)按ctrl+z结束输入
*如输入以下符号(既有空格也有换行)
123abc ABC 哈哈 !@#$



defg)(-=<>-+/*


123654
最后结果为 a="123abcABC哈哈!@#$defg)(-=<>-+/*123654";
*/

7.使用迭代器赋值
a.assign(b.begin()+1,b.end()); //结果为 a="67";
a.assign(a.begin(),a.end()); //结果为 a="123";

## 4.append()

4.append()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
string a="1234";
string b="5678";

1.把两个string字符串相连接
a.append(b); //结果为 a="12345678";
a.append("56789");//结果 a="123456789";

2.从string字符串b的某一个字符开始到结束连接在string字符串a后面
a.append(b,1);//结果为 a="1234678";
a.append(b,0);//结果为 a="12345678";
a.append(b,3);//结果为 a="12348";
//注意第二个参数不能大于字符串b的长度加一,否则会出错,至于为什么请看我的上一篇assign函数的用法

3.从字符串b的某一个字符开始到结束连接在string字符串a后面
a.append("5678",1);//结果为 a="12345";
a.append("5678",2);//结果为 a="123456";
a.append("5678",0);//结果为 a="1234";
// 注意2,3的区别

4.把string的子串连接到另一个string后面
a.append("5678",0,1);
a.append(b,0,1); //结果为 a="12345";
a,append("5678",1,3);
a,append(b,1,3); //结果为 a="1234678";

5.在string字符串后面添加n个字符
a.append(10,'>'); //结果为 a="1234>>>>>>>>>>";

6.template<class inputIterator> string& append(inputIterator first,inputIterator last);
//需要引入头文件#include<iterator>
a.append(istream_iterator<char>(cin),istream_iterator<char>());
//从键盘输入abcd
//结果为 a="1234abcd";
/**注意
*该函数不接收空格换行等符号,最后(windows系统)按ctrl+z结束输入
*如输入以下符号(既有空格也有换行)
@#¥%………

按时到场 sdf 456

asd
最后结果为 a="1234@#¥%……按时到场sdf456asd";
*/

7.使用迭代器使string字符串相连接
a.append(b.begin()+1,b.end());//结果为 a="1234678";
a.append(a.begin(),a.end()); //结果为 a="12341234";

4.at()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
string a="abcd";

1.获取string字符串某一个字符
auto s=a.at(1); //结果为 s='b';
for (unsigned int i=0;i<a.size();i++)
{
cout << a.at(i) << endl;
}
/* 结果为
a
b
c
d
*/
//等同于a[i],但是at()会有下标检查,如果超出则抛出out_of_range

2.修改string字符串某一个字符
a.at(2)='1'; //结果为 a="ab1d";


5 front()与back()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
string a="abcd";

1.获取字符串最后一个字符
auto b=a.back(); //结果为 b='d';

2.修改字符串最后一个字符
a.back()='!'; //结果为 a="abc!";

3.获取字符串第一个字符
auto b=a.front(); //结果为 b='a';

4.修改字符串第一个字符
a.front()='!'; //结果为 a="!bcd";

6.compare()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
string a="abcd";
string b="efgh";
string c="1fgh";
string d="fgh";
string e="123efg";

比较两个字符串的ASCII码,>0返回1,<0返回-1,相同,返回0
ASCII码比较是字符串的字符从前往后比较,如果之前的比较完成则后面的字符无需比较

1.直接比较两个字符串
auto number = a.compare(b); //结果为 number=-1;
auto number = b.compare(a); //结果为 number=1;
auto number = b.compare(b); //结果为 number=0;
auto number = a.compare(c); //结果为 number=1;
auto number = a.compare("abc"); //结果为 number=1;

2.一个字符串的子串与另一个字符串比较
auto number = b.compare(1,3,d); //结果为 number=0;
//字符串b从下标为1的字符开始的三个字符与字符串d比较,显然都是fgh,所以相等,返回0
auto number = b.compare(1,3,c); //结果为 number=1;
auto number = b.compare(1,3,"fgh"); //结果为 number=0;

3.一个字符串的子串与另一个字符串的子串比较
auto number = b.compare(1,3,c,1,3); //结果为 number= 0;
//字符串b从下标为1的地方开始的后3个字符是fgh,字符串c从下标为1的字符开始的后三个字符是fgh,所以相等
auto number = b.compare(0,3,e,3,3); //结果为 number=0;
//字符串b从下标为0的地方开始的后3个字符是efg,字符串e从下标为3的字符开始的后三个字符是efg,所以相等
auto number = b.compare(0,4,"1234efgh",4,4); //结果为 number=0;
//字符串b从下标为0的地方开始的后四个字符是efgh,字符串"1234efgh"从下标为4的字符开始的后4个字符是efgh,所以相等


7.copy()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *str = new char[64];
string a="12345abcdefg6789";

str[a.copy(str,7,5)]='\0';
// 结果为 str="abcdefg";

str[a.copy(str,7)]='\0';
// 结果为 str="12345ab";

delete[]str;

/*注意
*copy的第2,3个参数不能大于字符串str所能容纳的最长字符串长度
*/

8.data()与c_str() copy()的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
string a="123456";

1.c_str(),data()可以生成一个const char* 的指针,可以指向一个空字符终止的地址。

const char* str=nullptr;
str=a.c_str(); //结果为 str="123456";
str=a.data(); //结果为 str="123456";

但是如果改变string a的值,str的值随之改变
a="abcedf";
cout<<str<<endl; //结果为 str="abcdef";

2.如果不想让其指针指向的值改变可以使用copy()函数(如果不知道copy()函数如何使用,请看我的上一篇copy()的使用方法)
char* str = new char[64];
str[a.copy(str, a.size(), 0)]='\0';
cout << str << endl; //结果为 str="123456";

//改变string a的值
a="abcd";
cout<< str <<endl; //结果为 str="123456";


9、erase()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
string a="123456789";

1.删除所有字符
a.erase(); //结果为 a="";

2.从字符串的某一个位置开始删除
a.erase(n) //从字符串的第n个字符开始删除
a.erase(3); //结果为 a="123";
a.erase(5); //结果为 a="12345";
a.erase(0); //等同于a.erase() a="";

3.从字符串的某一个位置开始,向后删除m个字符
a.erase(n,m); //从字符的第n个字符开始删除m个字符
a.erase(2,3); //结果为 a="126789";
a.erase(4,1); //结果为 a="12346789";

4.删除迭代器位置处的字符,并返回下一个字符的迭代器
auot iter=a.erase(a.begin()+1); //结果为 a="13456789";
cout<<*iter<<endl; //结果为 *iter=3

5.删除迭代器所指向的区间,并返回下一个字符的迭代器
auto iter=a.erase(a.begin()+1,a.end()-2);//结果为 a="189";
cout<<*iter<,endl; //结果为 *iter=8;

6.删除字符时常常与find()函数配合使用(find()函数的用法会在以后写出)
a.erase(a.find("56"),2); //结果为 a="1234789";


c++ 栈的基本操作:

​ 一种可以实现“先进后出(后进先出)”的存储结构

s.empty(); //如果栈为空则返回true, 否则返回false;
s.size(); //返回栈中元素的个数
s.top(); //返回栈顶元素, 但不删除该元素
s.pop(); //弹出/删除栈顶元素, 但不返回其值
s.push(); //将元素压入栈顶

在压栈的过程中,栈顶的位置一直在”向上“移动,而栈底是固定不变的。

image-20211027221030901

如果我们要把栈中的元素弹出来: 先入后出

image-20211027221100703

c++ queue 的基本操作

入队,如例:q.push(x); 将x 接到队列的末端。
出队,如例:q.pop(); 弹出队列的第一个元素,注意,并不会返回被弹出元素的值。
访问队首元素,如例:q.front(),即最早被压入队列的元素。
访问队尾元素,如例:q.back(),即最后被压入队列的元素。
判断队列空,如例:q.empty(),当队列空时,返回true。
访问队列中的元素个数,如例:q.size()

参考链接:https://blog.csdn.net/qq_41855420/article/details/92836075

题目 链接
904水果成篮 904. 水果成篮 - 力扣(LeetCode) (leetcode-cn.com)

构造一个虚拟的窗口[left, right),当窗口tree[left, right)中的水果种数不多于2时,扩大右边界,此时窗口的大小就是可获取的水果种数,否则窗口中的水果种数超过2,则缩小左边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution {
public:
int totalFruit(vector<int>& tree) {
int treeSize = tree.size(), maxRes = 0;
int left = 0, right = 0, fruitOne, fruitTwo;//窗口的左、右边界,以及窗口中的两种水果
while (right < treeSize){
int tempRes = 1;//窗口的大小
fruitOne = tree[left++];//第一种水果
//寻找到第二种水果的第一次出现的位置(并且更新下一次的窗口的left)
while (left < treeSize && tree[left] == fruitOne){
++left;
tempRes += 1;
}
right = left;
if (right < treeSize){//第二种水果
fruitTwo = tree[right];
}
//当窗口tree[left, right)中的水果种数不多于2时,扩大右边界
while (right < treeSize){
if (tree[right] == fruitOne || tree[right] == fruitTwo){
++right;
tempRes += 1;
}
else {
break;
}
}
maxRes = max(maxRes, tempRes);//更新最大的水果数
}
return maxRes;
}

};

LeetCode题目 相关题目类型 相关链接
232 用栈实现队列(简单难度) 232. 用栈实现队列 - 力扣(LeetCode) (leetcode-cn.com)

在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从输出栈弹出数据就可以了。

最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。

在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。

输入栈的元素出栈再放到输出栈中,这样输出栈的输出顺序和队列顺序一样了(手画一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class MyQueue {
public:
stack<int>sck1;
stack<int>sck2;
MyQueue() {

}

void push(int x) {
sck1.push(x);
return ;
}

int pop() {
// 把输入栈sck1中的元素放到输出栈sck2,清空sck1
// 这样stk2的出栈顺序和输入的元素入stk1的顺序是一样的了
while(!sck1.empty()){
int num=sck1.top();// 获取栈顶元素,但不删除
sck1.pop();// 删除栈顶元素,但不返回
sck2.push(num); // 将元素压入栈顶
}

int val=sck2.top();// 返回栈顶元素
sck2.pop();// 删除栈顶元素
// 清空输出栈sck2
while(!sck2.empty()){
sck1.push(sck2.top());
sck2.pop();
}
return val;
}
// 返回队列开头的元素
int peek() {
// 清空sck1,元素放入到sck2
while(!sck1.empty()){
sck2.push(sck1.top());
sck1.pop();
}
//得到“队列”开头的元素
int val=sck2.top();
// 清空sck2,元素放入到sck1
while(!sck2.empty()){
sck1.push(sck2.top());
sck2.pop();
}
return val;
}

bool empty() {
if(sck1.empty()){
return true;
}
return false;
}
};

/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/

LeetCode题目 相关题目类型 关键点 相关链接
225 用队列实现栈(简单难度) 225. 用队列实现栈 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class MyStack {
public:
queue<int> que1;
queue<int> que2; // 辅助队列,用来备份
/** Initialize your data structure here. */
MyStack() {

}

/** 将元素 x 压入栈顶。 */
void push(int x) {
que1.push(x);
}

/** 移除并返回栈顶元素。 */
int pop() {
int size=que1.size();
size--;
// 把que1元素放到que2,但保留最后一个
while(size--){
int num=que1.front();
que1.pop();
que2.push(num);
}
int res=que1.front();
que1.pop();
que1=que2;
// 清空que2
while(!que2.empty()){
que2.pop();
}
return res;
}

/** 返回栈顶元素。 */
int top() {
return que1.back();
}

/** 如果栈是空的,返回 true ;否则,返回 false */
bool empty() {
return que1.empty();
}
};

/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
LeetCode题目 相关题目类型 关键点 相关链接
20 有效的括号(简单难度) 20. 有效的括号 - 力扣(LeetCode) (leetcode-cn.com)

括号匹配是使用栈解决的经典问题。

由于栈结构的特殊性,非常适合做对称匹配类的题目。

首先要弄清楚,字符串里的括号不匹配有几种情况。

先来分析一下 这里有三种不匹配的情况,

  1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。

    image-20211028151300438

    第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false

2.第二种情况,括号没有多余,但是 括号的类型没有匹配上。

image-20211028151315983

​ 第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false

3.第三种情况,字符串里右方向的括号多余了,所以不匹配。

image-20211028151337388

​ 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找 到对应的左括号return false

我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。

那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。

但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
bool isValid(string s) {
stack<int> st;
for(int i=0;i<s.size();i++){
// 左括号入栈,保存对应的右括号,这样右括号来的时候,直接对比是否相等就行
if(s[i]=='('){
st.push(')');
}
else if(s[i]=='{'){
st.push('}');
}
else if(s[i]=='['){
st.push(']');
}
// 右括号出现
// 右括号多余s.empty()
// 左右不匹配
else if(st.empty()||s[i]!=st.top()){
return false;
}
else{
st.pop();//左右括号匹配,
}
}
// 左括号多余,则s.empty()==false;
//都匹配,就是true
return st.empty();
}
};
LeetCode题目 相关题目类型 关键点 相关链接
1047 删除字符串中的所有相邻重复项(简单难度) 1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode) (leetcode-cn.com)

题目中有字符串的基本操作:

s.back();返回字符串最后一个元素

s.push_back(a);字符串末尾添加一个元素

s.pop_back();删除字符串末尾的元素

本题要删除相邻相同元素,其实也是匹配问题,相同左元素相当于左括号,相同右元素就是相当于右括号,匹配上了就删除。

那么再来看一下本题:可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。

拿字符串直接作为栈,这样省去了栈还要转为字符串的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Solution {
public:
string removeDuplicates(string S) {
string res;
for(char s:S){
if(res.empty()||res.back()!=s){
res.push_back(s);
}
else{
res.pop_back();
}
}
return res;
}
};
LeetCode题目 相关题目类型 关键点 相关链接
150 逆波兰表达式求值(中等难度) 150. 逆波兰表达式求值 - 力扣(LeetCode) (leetcode-cn.com)

1、理解逆波兰表达式的求值方式

2、解题思路

遍历数组:

如果是运算符,弹出栈的两个栈顶元素num1,num2,进行相应运算,把结果放到栈中

如果是数字,把数字放到栈中

遍历结束:结果为栈中唯一的元素,返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(int i=0;i<tokens.size();i++){
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/"){
int nums1=st.top();
st.pop();
int nums2=st.top();
st.pop();
if(tokens[i]=="+")st.push(nums2+nums1);
if(tokens[i]=="-")st.push(nums2-nums1);
if(tokens[i]=="*")st.push(nums2*nums1);
if(tokens[i]=="/")st.push(nums2/nums1);
}
else{
st.push(stoi(tokens[i]));//stoi()将字符串转为int型
}
}
int res=st.top();
st.pop();
return res;
}
};

LeetCode题目 相关题目类型 相关链接
t替换空格(简单难度) 剑指 Offer 05. 替换空格

题解链接:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/solution/mian-shi-ti-05-ti-huan-kong-ge-ji-jian-qing-xi-tu-/
来源:力扣(LeetCode)

题解上有视频

1、初始化:空格数量 count ,字符串 s 的长度 len ;

2、统计空格数量:遍历 s ,遇空格则 count++ ;
3、修改 s 长度:添加完 “%20” 后的字符串长度应为 len + 2 * count ;

4、倒序遍历修改:i 指向原字符串尾部元素, j 指向新字符串尾部元素;当 i = j 时跳出(代表左方已没有空格,无需继续遍历);
当 s[i] 不为空格时:执行 s[j] = s[i] ;
当 s[i] 为空格时:将字符串闭区间 [j-2, j] 的元素修改为 “%20” ;由于修改了 3 个元素,因此需要 j -= 2 ;
5、返回值:已修改的字符串 s ;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
string replaceSpace(string s) {
int count = 0, len = s.size();
// 统计空格数量
for (char c : s) {
if (c == ' ') count++;
}
// 修改 s 长度
s.resize(len + 2 * count);//多加两个空格数,因为字符串本身有一个,再加两个(才能放下%20)
// 倒序遍历修改
for(int i = len - 1, j = s.size() - 1; i < j; i--, j--) {
if (s[i] != ' ')
s[j] = s[i];
else {
s[j - 2] = '%';
s[j - 1] = '2';
s[j] = '0';
j -= 2;
}
}
return s;
}
};


LeetCode题目 相关题目类型 相关链接
剑指offer左旋转字符串(简单难度) 剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
class Solution {
public:
string reverseLeftWords(string s, int n) {
string str1;
str1 =s.substr(0,n);
s.erase(0,n);//即从给定起始位置0处开始删除, 要删除字符的长度为n, 返回值修改后的string对象引用
return s+str1;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
string reverseLeftWords(string s, int n) {
string str="";
for(int i=n;i<s.size();i++){
str+=s[i];
}
for(int i=0;i<n;i++){
str+=s[i];
}
return str;
}
};

LeetCode题目 相关题目类型 相关链接
t替换空格(简单难度) 剑指 Offer 05. 替换空格

题解链接:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/solution/mian-shi-ti-05-ti-huan-kong-ge-ji-jian-qing-xi-tu-/
来源:力扣(LeetCode)

题解上有视频

1、初始化:空格数量 count ,字符串 s 的长度 len ;

2、统计空格数量:遍历 s ,遇空格则 count++ ;
3、修改 s 长度:添加完 “%20” 后的字符串长度应为 len + 2 * count ;

4、倒序遍历修改:i 指向原字符串尾部元素, j 指向新字符串尾部元素;当 i = j 时跳出(代表左方已没有空格,无需继续遍历);
当 s[i] 不为空格时:执行 s[j] = s[i] ;
当 s[i] 为空格时:将字符串闭区间 [j-2, j] 的元素修改为 “%20” ;由于修改了 3 个元素,因此需要 j -= 2 ;
5、返回值:已修改的字符串 s ;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
string replaceSpace(string s) {
int count = 0, len = s.size();
// 统计空格数量
for (char c : s) {
if (c == ' ') count++;
}
// 修改 s 长度
s.resize(len + 2 * count);//多加两个空格数,因为字符串本身有一个,再加两个(才能放下%20)
// 倒序遍历修改
for(int i = len - 1, j = s.size() - 1; i < j; i--, j--) {
if (s[i] != ' ')
s[j] = s[i];
else {
s[j - 2] = '%';
s[j - 1] = '2';
s[j] = '0';
j -= 2;
}
}
return s;
}
};


LeetCode题目 相关题目类型 相关链接
剑指offer左旋转字符串(简单难度) 剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
class Solution {
public:
string reverseLeftWords(string s, int n) {
string str1;
str1 =s.substr(0,n);
s.erase(0,n);//即从给定起始位置0处开始删除, 要删除字符的长度为n, 返回值修改后的string对象引用
return s+str1;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
string reverseLeftWords(string s, int n) {
string str="";
for(int i=n;i<s.size();i++){
str+=s[i];
}
for(int i=0;i<n;i++){
str+=s[i];
}
return str;
}
};
LeetCode题目 相关题目类型 相关链接
151 反转字符串里的单词(中等难度) 151. 翻转字符串里的单词 - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

class Solution {
public:
// 反转字符串
void reverse(string &s,int start,int end){
int i=start;
int j=end;
for(;i<j;i++,j--){
swap(s[i],s[j]);
}
}
// 移除冗余空格
// 考虑字符串s前中后空格的位置和fast的大小是否符合循环要求
void removeExtraStr(string &s){
int fast=0,slow=0;
//移除字符串前面多余的空格
while(s[fast]==' '//表示字符串前有空格
&&s.size()>0//字符串s不为空
&&fast<s.size())//fast不能指到s外
{
fast++;//结束循环后,fast指向第一个不是空格的字符
}
//移除字符串中间多余的空格
for(;fast<s.size();fast++){
if(fast-1>0&&
s[fast]==' '&&
s[fast-1]==' '){//连续两个空格不做操作
continue;
}
else{//直到fast指向一个字符,前一个是空格,把fast指向的字符赋值给slow指向的位置
s[slow]=s[fast];
slow++;
}
}
//移除字符串后面多余的空格
if(s[slow-1]==' '){//fast指向空格,fast-1指向字符,这时候也执行了s[slow]=s[fast];所以多加了一个空格,如果指向反转字符串的操作,该空格在最前面
s.resize(slow-1);
}
else{
s.resize(slow);
}
}
string reverseWords(string s) {
// 移除多余空格
removeExtraStr(s);
// 反转字符串
reverse(s,0,s.size()-1);
//反转单个单词
int start=0;
for(int i=0;i<s.size();i++){
if(s[i]==' '&&s[i-1]!=' '){
reverse(s,start,i-1);
start=i+1;
}
// 最后一个单词后没有空格的情况
if((i == (s.size() - 1) )&& s[i] != ' '){
reverse(s,start,i);

}

}
return s;

}
};

一、螺旋矩阵

LeetCode题目 相关题目类型 相关链接
54 螺旋矩阵(中等难度) https://leetcode-cn.com/problems/design-linked-list/

题解链接:https://leetcode-cn.com/problems/spiral-matrix/solution/cxiang-xi-ti-jie-by-youlookdeliciousc-3/

这里的方法不需要记录已经走过的路径,所以执行用时和内存消耗都相对较小

1、首先设定上下左右边界。

2、其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界。
3、判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案。

4、若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理
不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
// 定义结果数组
vector <int> ans;
if(matrix.empty()) return ans; //若数组为空,直接返回答案
//赋值上下左右边界
int u = 0;
int d = matrix.size() - 1;// 行的数目-1
int l = 0;
int r = matrix[0].size() - 1;// 列的数目-1
// 右下左上 。。
while(true)
{
for(int i = l; i <= r; ++i) ans.push_back(matrix[u][i]); //向右移动直到最右
if(++ u > d) break; //重新设定上边界,若上边界大于下边界,则遍历遍历完成,下同
for(int i = u; i <= d; ++i) ans.push_back(matrix[i][r]); //向下
if(-- r < l) break; //重新设定有边界
for(int i = r; i >= l; --i) ans.push_back(matrix[d][i]); //向左
if(-- d < u) break; //重新设定下边界
for(int i = d; i >= u; --i) ans.push_back(matrix[i][l]); //向上
if(++ l > r) break; //重新设定左边界
}
return ans;
}
};

螺旋矩阵2

LeetCode题目 相关题目类型 相关链接
5459 螺旋矩阵2(中等难度) 59. 螺旋矩阵 II - 力扣(LeetCode) (leetcode-cn.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n,vector<int>(n,0));// 定义一个二位数组

int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop=n/2;//每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid=n/2;// 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度
int i,j;

while(loop--){
i= startx;
j= starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < starty + n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < startx + n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 2;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
//,n为偶数不用
if(n%2){
res[mid][mid]=count;

}
return res;

}
};

一、

顺序结构:这里主要指的是数组或字符串。

一、链表的基本操作

LeetCode题目 相关题目类型 相关链接
707 设计链表(中等难度) https://leetcode-cn.com/problems/design-linked-list/

在链表中添加或删除元素时,记得修改链表长度;

代码:csdn博客:力扣 707. 设计链表 链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class MyLinkedList {
public:
/** 初始化数据结构 */
MyLinkedList():head(new node()),tail(head),size(0) {}

/** 得到链表中第index个结点的值,index小于0或大于链表长度(链表的index从0开始),返回-1 */
int get(int index) {
if(index>=size||index<0)
return -1;
else if(index==size-1)
return tail->val;

node *cur=head->next;
while(index--)
cur=cur->next;
return cur->val;
}

/** 添加新的头节点 */
void addAtHead(int val) {
++size;
node *tmp=new node(val);
tmp->next=head->next;
head->next=tmp;
//注意更新尾节点
if(size==1)
tail=tmp;
}

/** 在链表最后添加一个结点. */
void addAtTail(int val) {
++size;
node *tmp=new node(val);
tail->next=tmp;
tail=tmp;
}

/** 在链表第index个结点前添加结点,
如果index等于链表长度,则将结点添加到链表末尾。
如果index大于链表长度,则不会插入结点
如果index小于0,则在头部插入结点。
*/
void addAtIndex(int index, int val) {
if(index<=0)
addAtHead(val);
else if(index==size)
addAtTail(val);
else if(index<size){
node *cur=head;
while(index--)
cur=cur->next;
// node *nxt=cur->next;
// node *tmp=new node(val);
// cur->next=tmp;
// tmp->next=nxt;
node *tmp=new node(val);
// cur指向第index个结点的前一个
tmp->next=cur->next;
cur->next=tmp;
++size;
}
}

/** 索引有效,则删除链表的第index个结点*/
void deleteAtIndex(int index) {
if(index>=size||index<0)
return ;
node *cur=head;
while(index--)
cur=cur->next;
node *tmp=cur->next;
cur->next=tmp->next;
//tmp指向最后一个结点时,注意更新尾节点,
if(!tmp->next)
tail=cur;// cur指向第index个的前一个结点
delete(tmp);
--size;
}
private:
struct node{
int val;
node *next;
node():val(0),next(nullptr){}
node(int v):val(v),next(nullptr){}
};
node *head=nullptr;
node *tail=nullptr;
int size=0;
};

/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/

二、链表的相交

LeetCode题目 相关题目类型 关键点 相关链接
面试题02.07. 链表相交(简单难度) 长链表的遍历节点先多走和短链表的长度差个节点,然后同时遍历两个节点比较是否存在相同节点 https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/

链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/solution/dai-ma-sui-xiang-lu-dai-ni-gao-ding-lian-5ykc/
来源:力扣(LeetCode)

交点不是数值相等,而是指针相等。

看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:

![](/img-post/算法刷题/2021-10-25-【算法刷题】链表相关经典题目/示例图1.png)

我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:

![](/img-post/算法刷题/2021-10-25-【算法刷题】链表相关经典题目/示例图2.png)

此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到焦点。

否则循环退出返回空指针。

作者:carlsun-2
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/solution/dai-ma-sui-xiang-lu-dai-ni-gao-ding-lian-5ykc/
来源:力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *curA=headA;
ListNode *curB=headB;
int lenA=0;
int lenB=0;
//计算A长度
while(curA){
lenA++;
curA=curA->next;
}
//计算B长度
while(curB){
lenB++;
curB=curB->next;
}
curA=headA;
curB=headB;
// 让curA指向长的链表,lenA为其长度。
if(lenB>lenA){
swap(lenA,lenB);
swap(curA,curB);
}
// 计算两个链表的长度差;
int gap=lenA-lenB;
// curA指向链表的一个结点,使两个链表的末尾对齐
while(gap--){
curA=curA->next;
}
// 同时遍历,相等则返回结点
while(curA){
if(curA==curB){
return curA;
}
curA=curA->next;
curB=curB->next;
}
//不等,返回空。
return NULL;
}
};



LeetCode题目 相关题目类型 关键点 相关链接
92 反转链表2(中等难度) 长链表的遍历节点先多走和短链表的长度差个节点,然后同时遍历两个节点比较是否存在相同节点 92. 反转链表 II - 力扣(LeetCode) (leetcode-cn.com)

解题思路:
1、我们定义两个指针,分别称之为 g(guard 守卫) 和 p(point)。
我们首先根据方法的参数 m 确定 g 和 p 的位置。将 g 移动到第一个要反转的节点的前面,将 p 移动到第一个要反转的节点的位置上。我们以 m=2,n=4为例。
2、将 p 后面的元素删除,然后添加到 g 的后面。也即头插法。
3、根据 m 和 n 重复步骤(2)
4、返回 dummyHead.next
链接:https://leetcode-cn.com/problems/reverse-linked-list-ii/solution/java-shuang-zhi-zhen-tou-cha-fa-by-mu-yi-cheng-zho/

![](/img-post/算法刷题/2021-10-25-【算法刷题】链表相关经典题目/示例图3.png)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
// 定义一个虚拟头结点, 方便处理
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;

// 初始化指针
ListNode g = dummyHead;
ListNode p = dummyHead.next;

// 将指针移到相应的位置
for(int step = 0; step < m - 1; step++) {
g = g.next; p = p.next;
}

// 头插法插入节点
for (int i = 0; i < n - m; i++) {
ListNode removed = p.next;
p.next = p.next.next;

removed.next = g.next;
g.next = removed;
}

return dummyHead.next;
}
}
LeetCode题目 相关题目类型 关键点 相关链接
142 环形链表2(中等难度) 142. 环形链表 II - 力扣(LeetCode) (leetcode-cn.com)

思路

主要考察两知识点:

  • 判断链表是否环

  • 如果有环,如何找到这个环的入口

  • 判断链表是否有环

    可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

    为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

image-20211031165431940

ast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。
环形入口节点到 fast指针与slow指针相遇节点 节点数为y。
从相遇节点 再到环形入口节点节点数为 z。 如图所示:

image-20211031165706230

那么相遇时:
slow指针走过的节点数为: x + y
fast指针走过的节点数: x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};

参考链接:

哈尔小波变换的原理及其实现(Haar) (360doc.com)

csdn博客:Python 离散小波变换(DWT) pywt库

基础概念:

变换:不管是压缩、滤波还是图像处理,本质都是变换,就是基。例如傅里叶变换就是将信号用该空间的基的线性组合进行表示 •

正交: 如果两个向量的内积为0,它们就是正交的;如果一个向量序列相互对偶正交,并且长度为1,它们就是正交归一化的。

哈尔小波变换是,小波变换中最简单的一种变换,也是最早提出的小波变换。

一维哈尔小波变换

例:求只有4个像素[9 7 3 5]的图像的小波变换系数。 计算步骤如下:
1、求均值(averaging)。计算相邻像素对的平均值,得到一幅分辨率比较低的新图像,新的图像的分辨率是原来的1/2,相应的像素值为:[8 4]
2、求差值(differencing)。上面的均值存储了图像的整体信息,但很多细节被丢掉了。所以要记录图像的细节信息,这样在重构时能够恢复图像的全部信息。方法是使用这个像素对的差值除以2,结果为[8 4 1 -1]
以上两步形成第一次分解的结果[8 4 1 -1],包含了图像的整体信息和细节信息。
接下来重复1、2步,将整体信息再次分解,得到二级分解结果[6,2,1,-1]

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/table1.png)

从这个例子中我们可以看到:
① 对这个给定的变换,我们可以从所记录的数据中重构出各种分辨率的图像。例如,在分辨率为1的图像基础上重构出分辨率为2的图像,在分辨率为2的图像基础上重构出分辨率为4的图像。
②变换过程中没有丢失信息,因为能够从所记录的数据中重构出原始图像。
③ 通过变换之后产生的细节系数的幅度值比较小,这就为图像压缩提供了一种途径,例如去掉一些微不足道的细节系数并不影响对重构图像的理解。

这个过程就叫做哈尔小波变换,也称哈尔小波分解,这个概念可以推广到使用其他小波基的变换。

二维哈尔小波变换

​ 对于二维小波变换,通常一次分解形成了整体图像,水平细节,垂直细节,对角细节。首先我们按照一维小波分解的原理,按照行顺序对行进行处理,然后按照列顺序对行处理结果进行同样的处理。

经过小波变换后图像会生成低频信息和高频信息。低频信息对应于求均值,高频信息对应于求差值。
均值是局部的平均值,变化缓慢,属于低频信息,存储图片的轮廓信息,近似信息
差值是局部的波动值,变化较快,属于高频信息,存储图片的细节信息,局部信息,另外含有噪音

假设有一幅灰度图像,其中的一个图像块用矩阵A 表示:

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/matrix1.png)

​ 一个图像块是一个二维矩阵,进行小波变换时可以对矩阵的每一行进行变换,然后对行变换后的每一列进行变换,最后对经过变换之后的图像矩阵进行编码。

第一步:在第一行上取每一对像素的平均值,并将结果放到第一行的前四个位置,其余4个数是第一行每一对像素的第一个数和对应的平均值之差(也可以是 这个像素对的差值除以2 ,计算结果是一样的。)将结果放到第一行的最后四个位置。

第二步:对第一行的前四个数使用与第一步相同的方法,得到两个平均值和两个差(系数),并依次放在第一行的前四个位置,其余四个细节系数位置不动。

第三步:用与第一步和第二步相同的方法,对剩下的一对平均值求均值和差值。

用求均值和差值的方法,对矩阵每一行进行计算,得到矩阵A‘。

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/matrix2.png)

每行的第一个元素是该行像素值的平均值,其余是这行的细节系数。用同样的方法,对A’的每一列进行计算,得到A’’

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/matrix3.png)

左上角的元素是整个图像块的像素值的平均值,其余是该图像块的细节系数,根据这个事实,如果从矩阵中去掉图像的某些细节系数,事实证明重构的图像质量仍然可以接受。

具体做法是设置一个阈值D,应该是像素值小于等于5的细节系数就把它当做0看待。这样变换后的矩阵为A‘’‘

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/matrix4.png)

’0‘的数目增加了18个,也就是去掉了18个细节系数,这样可以提高编码的效率。

上面解释的图示:

(来自csdn博客:)

经过小波变换后图像会生成低频信息和高频信息。低频信息对应于求均值,高频信息对应于求差值。
均值是局部的平均值,变化缓慢,属于低频信息,存储图片的轮廓信息,近似信息
差值是局部的波动值,变化较快,属于高频信息,存储图片的细节信息,局部信息,另外含有噪音

水平和竖直两个方向进行低通和高通滤波(水平和竖直先后不影响),用图像表述如下图所示:

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/image1.png)

其中:

  • b: 原图信息
  • h1 :水平方向的细节(高频信息),
  • v1 表示竖直方向的细节(高频信息),
  • c1表示对角线方向的细节(高频信息)

![](/img-post/科研笔记/2021-10-19-【科研笔记】离散小波变换/image2.png)

0%