目录
什么是边框回归Bounding-Box regression,以及为什么要做、怎么做
CV
介绍一下常用的CV网络
ssd网络/yolo/faster rcnn
yolo:主要分成backbone和head两部分。
(1)backbone
骨架采用新设计的darknet-53,每个convolutional都包括conv+bn+leakrelu模块。和vgg十分类似。darknet-19首 先在imagenet上面进行重新训练,然后把conv+avg+softmax去掉,得到的骨架权重作为目标检测的预训练权重。
(2) head
结合ssd的多尺度预测思想,提出一种新的 passthrough层来产生更精细的特征图,本质上是一种特征聚合操作,目的是增强小物体特征图信息。将原来尺度为 26x26x512特征图拆分13x13x2048的特征图,然后和darkent-19骨架最后一层卷积的13x13x1024特征图进行 concat,得到13x13x3072的特征图,然后经过conv+bn+leakrelu,得到最终的预测图。
(3)输出形式
yolov2引入了 anchor机制,预测学习的输出其实是相对于anchor的偏移。对于每个格子的每个位置,输出的值为(tx,ty,tw,th),且都是特征图尺度值,而不是原图尺度。在预测 得到上述4个值后,采用上述公式还原就可以得到归一化的(bx,by,bw,bh),然后乘上32就得到最终需要的bbox 值,分别代表bbox中心点座标和宽高值。tx,ty输出值代表bbox中心点相对于当前网格左上角的偏移,而tw,th输 出值代表真实bbox宽高除以anchor宽高的对数值。
ssd:ssd是典型的多尺度输出方式,其在多个尺度上进行bbox预测。ssd网络也分为backbone和head部分。
(1) backbone
骨架网络是标准的vgg16。原始论文没有采用BN,后面有很多新的复现加上了BN。输入图片是300x300和 512x512两种.
(2) extra
ssd在vgg16的后面扩展了几层卷积用于进行多尺度预测.
输出形式
采用多尺度预测的目的是希望大输出特征图检测小物体,小特征图检测大物体。ssd的预测输出形式和yolo类似, 也是学习基于当前anchor的偏移,注意此时的anchor就有中心座标的概念(yolo没有),而没有yolo中的网格概念。
faster r-cnn
图像数据预处理的常用方法
数据归一化
数据预处理中,标准的第一步是数据归一化。虽然这里有一系列可行的方法,但是这一步通常是根据数据的具体情况而明确选择的。特征归一化常用的方法包含如下几种:
- 简单缩放
- 逐样本均值消减(也称为移除直流分量)
- 特征标准化(使数据集中所有特征都具有零均值和单位方差)
逐样本均值消减
如果你的数据是平稳的(即数据每一个维度的统计都服从相同分布),那么你可以考虑在每个样本上减去数据的统计平均值(逐样本计算)。
Eg:对于图像,这种归一化可以移除图像的平均亮度值 (intensity)。很多情况下我们对图像的照度并不感兴趣,而更多地关注其内容,这时对每个数据点移除像素的均值是有意义的。注意:虽然该方法广泛地应用于图像,但在处理彩色图像时需要格外小心,具体来说,是因为不同色彩通道中的像素并不都存在平稳特性。
特征标准化
特征标准化指的是(独立地)使得数据的每一个维度具有零均值和单位方差。这是归一化中最常见的方法并被广泛地使用(例如,在使用支持向量机(SVM)时,特征标准化常被建议用作预处理的一部分)。在实际应用中,特征标准化的具体做法是:首先计算每一个维度上数据的均值(使用全体数据计算),之后在每一个维度上都减去该均值。下一步便是在数据的每一维度上除以该维度上数据的标准差。
Eg:处理音频数据时,常用 Mel 倒频系数 MFCCs 来表征数据。然而MFCC特征的第一个分量(表示直流分量)数值太大,常常会掩盖其他分量。这种情况下,为了平衡各个分量的影响,通常对特征的每个分量独立地使用标准化处理。
一般的预处理流程为:1灰度化->2几何变换->3图像增强
灰度化
对彩色图像进行处理时,我们往往需要对三个通道依次进行处理,时间开销将会很大。因此,为了达到提高整个应用系统的处理速度的目的,需要减少所需处理的数据量。在图像处理中,常用的灰度化方法:1.分量法2.最大值法3.平均值法4.加权平均法
几何变换
图像几何变换又称为图像空间变换,通过平移、转置、镜像、旋转、缩放等几何变换对采集的图像进行处理,用于改正图像采集系统的系统误差和仪器位置(成像角度、透视关系乃至镜头自身原因)的随机误差。此外,还需要使用灰度插值算法,因为按照这种变换关系进行计算,输出图像的像素可能被映射到输入图像的非整数座标上。通常采用的方法有最近邻插值、双线性插值和双三次插值。
图像增强
增强图像中的有用信息,它可以是一个失真的过程,其目的是要改善图像的视觉效果,针对给定图像的应用场合,有目的地强调图像的整体或局部特性,将原来不清晰的图像变得清晰或强调某些感兴趣的特征,扩大图像中不同物体特征之间的差别,抑制不感兴趣的特征,使之改善图像质量、丰富信息量,加强图像判读和识别效果,满足某些特殊分析的需要。图像增强算法可分成两大类:空间域法和频率域法。
非极大值抑制
R-CNN会从一张图片中找出n个可能是物体的矩形框,然后为每个矩形框为做类别分类概率:
就像上面的图片一样,定位一个车辆,最后算法就找出了一堆的方框,我们需要判别哪些矩形框是没用的。非极大值抑制的方法是:先假设有6个矩形框,根据分类器的类别分类概率做排序,假设从小到大属于车辆的概率 分别为A、B、C、D、E、F。
(1)从最大概率矩形框F开始,分别判断A~E与F的重叠度IOU是否大于某个设定的阈值;
(2)假设B、D与F的重叠度超过阈值,那么就扔掉B、D;并标记第一个矩形框F,是我们保留下来的。
(3)从剩下的矩形框A、C、E中,选择概率最大的E,然后判断E与A、C的重叠度,重叠度大于一定的阈值,那么就扔掉;并标记E是我们保留下来的第二个矩形框。
就这样一直重复,找到所有被保留下来的矩形框。
非极大值抑制(NMS)顾名思义就是抑制不是极大值的元素,搜索局部的极大值。这个局部代表的是一个邻域,邻域有两个参数可变,一是邻域的维数,二是邻域的大小。这里不讨论通用的NMS算法,而是用于在目标检测中用于提取分数最高的窗口的。
例如在行人检测中,滑动窗口经提取特征,经分类器分类识别后,每个窗口都会得到一个分数。但是滑动窗口会导致很多窗口与其他窗口存在包含或者大部分交叉的情况。这时就需要用到NMS来选取那些邻域里分数最高(是行人的概率最大),并且抑制那些分数低的窗口。
什么是深度学习中的anchor?
解析:当我们使用一个3*3的卷积核,在最后一个feature map上滑动,当滑动到特征图的某一个位置时,以当前滑动窗口中心为中心映射回原图的一个区域(注意 feature map 上的一个点是可以映射到原图的一个区域的,相当于感受野起的作用),以原图上这个区域的中心对应一个尺度和长宽比,就是一个anchor了。
什么是边框回归Bounding-Box regression,以及为什么要做、怎么做
解析:
这个问题可以牵扯出不少问题,比如
为什么要边框回归?
什么是边框回归?
边框回归怎么做的?
边框回归为什么宽高,座标会设计这种形式?
为什么边框回归只能微调,在离真实值Ground Truth近的时候才能生效?
如图1所示,绿色的框表示真实值Ground Truth, 红色的框为Selective Search提取的候选区域/框Region Proposal。那么即便红色的框被分类器识别为飞机,但是由于红色的框定位不准(IoU<0.5), 这张图也相当于没有正确的检测出飞机。
如果我们能对红色的框进行微调fine-tuning,使得经过微调后的窗口跟Ground Truth 更接近, 这样岂不是定位会更准确。 而Bounding-box regression 就是用来微调这个窗口的。
边框回归是什么?
对于窗口一般使用四维向量(x,y,w,h)(x,y,w,h) 来表示, 分别表示窗口的中心点座标和宽高。 对于图2, 红色的框 P 代表原始的Proposal, 绿色的框 G 代表目标的 Ground Truth, 我们的目标是寻找一种关系使得输入原始的窗口 P 经过映射得到一个跟真实窗口 G 更接近的回归窗口G^。
所以,边框回归的目的即是:给定(Px,Py,Pw,Ph)寻找一种映射f, 使得f(Px,Py,Pw,Ph)=(Gx^,Gy^,Gw^,Gh^)并且(Gx^,Gy^,Gw^,Gh^)≈(Gx,Gy,Gw,Gh)
边框回归怎么做的?
那么经过何种变换才能从图2中的窗口 P 变为窗口G^呢? 比较简单的思路就是: 平移+尺度放缩
先做平移(Δx,Δy),Δx=Pwdx(P),Δy=Phdy(P)这是R-CNN论文的:
G^x=Pwdx(P)+Px,(1)
G^y=Phdy(P)+Py,(2)
然后再做尺度缩放(Sw,Sh), Sw=exp(dw(P)),Sh=exp(dh(P)),对应论文中:
G^w=Pwexp(dw(P)),(3)
G^h=Phexp(dh(P)),(4)
观察(1)-(4)我们发现, 边框回归学习就是dx(P),dy(P),dw(P),dh(P)这四个变换。
下一步就是设计算法那得到这四个映射。
线性回归就是给定输入的特征向量 X, 学习一组参数 W, 使得经过线性回归后的值跟真实值 Y(Ground Truth)非常接近. 即Y≈WX。 那么 Bounding-box 中我们的输入以及输出分别是什么呢?
Input:
RegionProposal→P=(Px,Py,Pw,Ph)这个是什么? 输入就是这四个数值吗?其实真正的输入是这个窗口对应的 CNN 特征,也就是 R-CNN 中的 Pool5 feature(特征向量)。 (注:训练阶段输入还包括 Ground Truth, 也就是下边提到的t∗=(tx,ty,tw,th))
Output:
需要进行的平移变换和尺度缩放 dx(P),dy(P),dw(P),dh(P),或者说是Δx,Δy,Sw,Sh。我们的最终输出不应该是 Ground Truth 吗? 是的, 但是有了这四个变换我们就可以直接得到 Ground Truth。
这里还有个问题, 根据(1)~(4)我们可以知道, P 经过 dx(P),dy(P),dw(P),dh(P)得到的并不是真实值 G,而是预测值G^。的确,这四个值应该是经过 Ground Truth 和 Proposal 计算得到的真正需要的平移量(tx,ty)和尺度缩放(tw,th)。
这也就是 R-CNN 中的(6)~(9):
tx=(Gx−Px)/Pw,(6)
ty=(Gy−Py)/Ph,(7)
tw=log(Gw/Pw),(8)
th=log(Gh/Ph),(9)
那么目标函数可以表示为 d∗(P)=wT∗Φ5(P),Φ5(P)是输入 Proposal 的特征向量,w∗是要学习的参数(*表示 x,y,w,h, 也就是每一个变换对应一个目标函数) , d∗(P) 是得到的预测值。
我们要让预测值跟真实值t∗=(tx,ty,tw,th)差距最小, 得到损失函数为:
Loss=∑iN(ti∗−w^T∗ϕ5(Pi))2
函数优化目标为:
W∗=argminw∗∑iN(ti∗−w^T∗ϕ5(Pi))2+λ||w^∗||2
利用梯度下降法或者最小二乘法就可以得到 w∗。
请阐述下Selective Search的主要思想
解析:
1 使用一种过分割手段,将图像分割成小区域 (1k~2k 个)
2 查看现有小区域,按照合并规则合并可能性最高的相邻两个区域。重复直到整张图像合并成一个区域位置
3 输出所有曾经存在过的区域,所谓候选区域
其中合并规则如下: 优先合并以下四种区域:
①颜色(颜色直方图)相近的
②纹理(梯度直方图)相近的
③合并后总面积小的: 保证合并操作的尺度较为均匀,避免一个大区域陆续“吃掉”其他小区域 (例:设有区域a-b-④c-d-e-f-g-h。较好的合并方式是:ab-cd-ef-gh -> abcd-efgh -> abcdefgh。 不好的合并方法是:ab-c-d-e-f-g-h ->abcd-e-f-g-h ->abcdef-gh -> abcdefgh)
合并后,总面积在其BBOX中所占比例大的: 保证合并后形状规则。
上述四条规则只涉及区域的颜色直方图、梯度直方图、面积和位置。合并后的区域特征可以直接由子区域特征计算而来,速度较快。
模型量化压缩
模型压缩的主要方法有哪些?
(1)从模型结构上优化:模型剪枝、模型蒸馏、automl直接学习出简单的结构
(2)模型参数量化将FP32的数值精度量化到FP16、INT8、二值网络、三值网络等
剪枝流程
剪枝流程:取训练好的model;取出每一个CNN的卷积核;计算每个卷积核的所有权重之和大小;排序,设置一个阈值,也就是剪枝率,比如为60%;如果这个卷积核的权重值和小于阈值则直接剔除掉;对于caffe来讲一个要剔除caffemodel的卷积核,一个是要剔除网络文件定义的卷积核;最后生成新的model和网络文件;retrain不断循环训练剪枝达到模型最优;
Example:假设经过卷积之后的feature map 的维度为 h x w x c,h和w分别为特征图的高和宽,c为通道数,将其送入BN层会得到归一化之后的特征图,c个feature map中的每一个都对应一组γ和λ,通过设置剪枝率(%n)大小,取从小到大排序的缩放因子中n%的位置的缩放因子为阈值,剪掉小于阈值的γ对应的通道(直接剪掉这个feature map对应的卷积核)
注意:前面一两层最好不剪枝,或者少量剪枝,和SSD有关联的层尽量不剪枝,或者少剪枝,剪枝之后一定要retrain,acc会上升一些,但是不要次数过多,会over-fitting如果你的时间很多可以尝试,每次裁剪一层一点点,或者整个网络都裁剪一点点,之后retrain之后再裁剪,不断剪枝-》retrain-》再剪枝-》再retrain;
Int8量化流程
参考:https://zhuanlan.zhihu.com/p/58182172
量化
我们的目的是把原来的float 32bit 的卷积操作(乘加指令)转换为int8的卷积操作,这样计算就变为原来的1/4,但是访存并没有变少哈,因为我们是在kernel里面才把float32变为int8进行计算的。
就是把你一个layer的激活值范围的给圈出来,然后按照绝对值最大值作为阀值(因此当正负分布不均匀的时候,是有一部分是空缺的,也就是一部分值域被浪费了;这里有个小坑就是,假如我的激活址全是正的,没有负值,那么你怎么映射呢?),然后把这个范围直接按比例给映射到正负128的范围内来,公式如下:
FP32 Tensor (T) = scale_factor(sf) * 8-bit Tensor(t) + FP32_bias (b)
面是简单的max-max 映射,这是针对均匀分布的,很明显的可以知道,只要数据分布的不是很均匀,那么精度损失是很大很明显的,于是很多情况下是这么干的:
权重存的值是float32,将其映射到下面的int中的-127->127,都知道是映射,但是这边有个操作就是左面这些红色的叉点都映射到边界而不直接删掉,选择T值使整个映射的损失和效果最好是最后的优化问题,整个优化转换为一定的数学问题,求出最优解即可为所要的映射的T值。
理解:把无关的高频细节给去掉,从而获取性能上的好处!网络图像压缩技术不就是这么整的么!PCA主成分、傅立叶分解的思路不都是这样的么!抓住事物的主要矛盾,忽略细节,从而提高整体性能!就像机器学习里的正则化优化不也是这样么,避免你过于钻到细节里面从而产生过拟合啊!这么一想,其实,我们人生不也是这样么?什么事情都得抠死理,钻牛角尖么?!!有时候主动放弃一些东西首先你的人生肯定会轻松很多,其次说不定会收获到更稳定的人生幸福值(泛化性能)呢!
方法:NVIDIA选择的是KL-divergence,其实就是相对熵,那为什么要选择相对熵呢?而不是其他的别的什么呢?因为相对熵表述的就是两个分布的差异程度,放到我们的情境里面来就是量化前后两个分布的差异程度,差异最小就是最好的了~因此问题转换为求相对熵的最小值!
从编码的角度来讲一下相对熵,即什么是KL-divergence以?及为什么要用KL-divergence?
假设我们有一系列的符号,知道他们出现的概率,如果我要对这些符号进行最优编码,我会用T bits来表示,T即为表示原信息的最优的bit位数。我们把这个编码叫为A;
现在我们有同样的符号集合,只是他们出现的概率变了,假如我还是用A编码来对这个符合集合进行编码的话,那么编码的位长T'就是次优的了,是大于原来的T值的。
(假设我有一系列的符号,我知道它们发生的概率。如果我要对这些符号进行最优的编码,我会用“T”来表示。注意,T是位的最优数。让我们把这段代码称为“A”。现在,我有相同的符号集但是它们发生的概率已经改变了。现在,符号有了新的概率,如果我用代码A来编码符号,编码的比特数将会是次优的,大于T。)
KL散度就是来精确测量这种最优和次优之间的差异(由于选择了错误的编码导致的)。在这里F32就是原来的最优编码,int8就是次优的编码,我们用KL散度来描述这两种编码之间的差异;
- 相对熵表示的是采用次优编码时你会多需要多少个bit来编码,也就是与最优编码之间的bit差;
- 而交叉熵表示的是你用次优编码方式时确切需要多少个bits来表示;
- 因此,最优编码所需要的bits=交叉熵-相对熵。
工具:caffe-int8-convert-tools
结果:300ms->100ms
为什么用量化?
-
- 模型太大,比如alexnet就200MB,存储压力大的哟,必须要降一降温;
- 每个层的weights范围基本都是确定的,且波动不大,适合量化压缩;
- 此外,既减少访存又减少计算量,优势很大的啊!
为什么不直接训练低精度的模型?
-
- 因为你训练是需要反向传播和梯度下降的,int8就非常不好做了,举个例子就是我们的学习率一般都是零点几零点几的,你一个int8怎么玩?
- 其次大家的生态就是浮点模型,因此直接转换有效的多啊!
INT8量化流程
宏观处理流程如下,首先准备一个校准数据集,然后对每一层:
-
- 收集激活值的直方图;
- 基于不同的阀址产生不同的量化分布;
- 然后计算每个分布与原分布的相对熵,然后选择熵最少的一个,也就是跟原分布最像的一个。
此时阀值就选出来啦,对应的scale值也就出来了。
而其中最关键的就是校准算法部分了:
calibration:基于实验的迭代搜索阀值。
校准是其核心部分,应用程序提供一个样本数据集(最好是验证集的子集),称为“校准数据集”,它用来做所谓的校准。
在校准数据集上运行FP32推理。收集激活的直方图,并生成一组具有不同阈值的8位表示法,并选择具有最少kl散度的表示;kl-散度是在参考分布(即FP32激活)和量化分布之间(即8位量化激活)之间。
INT8量化实现-校准算法
公式是:FP32 Tensor (T) = scale_factor(sf) * 8-bit Tensor(t),bias实验得知可去掉。
矩阵乘法
static void mm_generate(float* matA,float* matB,float* matC,const int M,const int N,const int K,const int strideA,const int strideB,const int strideC)
{
for (int i = 0; i < M;i++) // A的每一行 C的每一行
{
for (int j = 0; j < N;j++)// B的每一列 C的每一列
{
float sum = 0.0f;
for (int k = 0; k < K;k++)// A的每一行的每一列* B的每一列的每一行
{
sum += matA[i*strideA + k] * matB[k*strideB + j];// 求和
}
matC[i*strideC + j] = sum;// 得到矩阵C的每一行的每一列
}
}
}
算法
判断单链表是否为回文串
思路:利用快慢指针,找到中间节点;将慢指针节点的值压入栈,到达中间节点后,依次出栈与后续节点的值比较。特别注意长度奇偶数。
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Palindrome {
public:
bool isPalindrome(ListNode* pHead) {
// write code here
if(pHead == NULL)
return true;
stack<int> ss;
ListNode* p = pHead;
ListNode* q = pHead;
ss.push(p->val);
while(q->next != NULL && q->next->next != NULL)
{
p = p->next;
ss.push(p->val);
q = q->next->next;
}
if(q->next == NULL) //长度为奇数
ss.pop();
p = p->next;
while(!ss.empty())
{
if(ss.top() != p->val)
break;
p = p->next;
ss.pop();
}
if(ss.empty())
return true;
else
return false;
}
};
链表是否有环,环节点怎么找
使用快慢指针,即采用两个指针walker和runner,walker每次移动一步而runner每次移动两步。当walker和runner第一次相遇时,证明链表有环
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
auto walker = head;
auto runner = head;
while(runner && runner->next)
{
walker = walker->next;
runner = runner->next->next;
if(walker == runner)
return true;
}
return false;
}
};
如何查找链表中间节点
可以采取建立两个指针,一个指针一次遍历两个节点,另一个节点一次遍历一个节点,当快指针遍历到空节点时,慢指针指向的位置为链表的中间位置,这种解决问题的方法称为快慢指针方法。
//查找单链表的中间节点,要求只能遍历一次链表
SListNode * FindMidNode(SListNode * phead)
{
SListNode *fast = phead;
SListNode *slow = phead;
while (fast)
{
if (fast->next != NULL)
{
fast = fast->next->next;
}
else
{
break;
}
slow = slow->next;
}
return slow;
}
也可以这样写,更为简洁
while (fast&&fast->next )
{
fast = fast->next->next;
slow = slow->next;
}
链表逆序
反转思路是:
(1)第一步反转,P1和P2, 也就是使得P2->next=P1. 如图: P1<----p2--->P3
(2)第二步,采用同样的方式,反转P3和P2,也就是使得;P1<---P2<---P3
既然是同第一步一样的方式,就不能简单地P3-->Next=P2完事了,否者的话得穷举所有结点,一相邻两个结点为单位,挨个手工反转了。
于是想到利用指针的特性,重用第一步的反转。这个时候只要使得P1指向P2,P2指向P3,再重用第一步反转P1和P2,即P2->next=P1.。相当于从P1开始整体指针往右移动,这样P2和P3之间的反转由于指针重新赋值了,变成了可以直接重用P1和P2的反转了。
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
if ((NULL==head) || (NULL==head->next) ) return head;
ListNode* P1 = head;
ListNode* P2 = P1->next;
P1->next=NULL;
while ( NULL!=P2 )
{
ListNode* tmp=P2->next;
P2->next=P1;
P1=P2;
P2=tmp;
}
return P1;
}
};
链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点
首先定义两个指针,让第一个指针从头开始移动k-1步,第二个指针保持不动(为空),从第k个节点开始,第二个指针和第一个指针同时开始遍历,由于两个指针始终保持在k-1的距离,当第一个节点走到尾节点的时候,第二个指针刚好指向倒数第k个节点。
比如输入链表的指针为空,或者k=0,无符号的k-1等于4294967295,不等于-1。还有链表的元素不足k的情况,如果无法处理这些情况,代码的鲁棒性就会很差,这也是面试官所看重的,所以在写代码的时候千万要考虑周到,并且能够处理这些特殊情况。
struct ListNode
{
int m_nValue;
ListNode* m_pNext;
};
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k)
{
if (pListHead == nullptr || k == 0)
return;
ListNode*pAhead = pListHead;
ListNode*pBehind = nullptr;
for (unsigned int i = 0; i < k; ++i)
{
if (pAhead->m_pNext != nullptr)
pAhead = pAhead->m_pNext;
else
{
return nullptr;
}
}
pBehind = pListHead;
while (pAhead->m_pNext!=nullptr)
{
pAhead = pAhead->m_pNext;
pBehind = pBehind->m_pNext;
}
return pBehind;
}
链表的局部反转
先找到需要反转的第一个节点的前一个节点prev,然后调用reverList来反转后面需要反转的部分,并返回头节点node,再将prev和node链接起来prev->next=node。注意到reverseList函数需要在Reverse Linked List反转链表的基础上稍微修改:反转了指定部分后,还需要将这部分与它后面没有反转的部分连起来。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
if (head == NULL || head->next == NULL || m == n) return head;
ListNode* help = new ListNode(0);
help->next = head;
ListNode* prev = help;
for (int i = 0; i < m - 1; ++i) // 找到反转的头节点的前一个节点
prev = prev->next;
ListNode* node = reverseList(prev->next, n - m); // 局部反转
prev->next = node; // 前后部分链接起来
return help->next;
}
private:
ListNode* reverseList(ListNode* head, const int length) { // length为反转次数
ListNode* rHead = NULL;
ListNode *prev = head, *curr = prev->next, *next = curr->next;
for (int i = 0; i < length; ++i) {
if (i == length - 1) { // 最后一次反转
head->next = next; // 链接上前面的反转部分和后面的未反转部分
curr->next = prev; // 反转最后一个指针
rHead = curr;
break;
}
else {
curr->next = prev; // 反转一个指针
prev = curr; // 所有指针前移一个节点
curr = next;
next = next->next;
}
}
return rHead;
}
};
求树的直径
/*树的直径是指树的最长简单路。求法: 两遍BFS :先任选一个起点BFS找到最长路的终点,再从终点进行BFS,则第二次BFS找到的最长路即为树的直径;
原理: 设起点为u,第一次BFS找到的终点v一定是树的直径的一个端点
证明: 1) 如果u 是直径上的点,则v显然是直径的终点(因为如果v不是的话,则必定存在另一个点w使得u到w的距离更长,则于BFS找到了v矛盾)
2) 如果u不是直径上的点,则u到v必然于树的直径相交(反证),那么交点到v 必然就是直径的后半段了
所以v一定是直径的一个端点,所以从v进行BFS得到的一定是直径长度
*/
#include <bits/stdc++.h>
using namespace std;
#define INF 10000000000
vector <int > G[1000005];
vector<int > E[1000005];
bool vis[1000005];
int d[1000005];
void init() {
memset(vis, 0, sizeof(vis));
}
void dfs(int u) {
vis[u] = 1;
int size = G[u].size(); //与顶点u相连的点数
for (int i = 0; i<size; i++) { //对与顶点u相连的点数进行扫描
int v = G[u][i];
if (!vis[v]) {
d[v] = d[u] + E[u][i];
dfs(v);
}
}
}
int main() {
int n;
cin >> n;
int u, v, w;
for (int i = 0; i<n-1; i++) { //建立树过程
scanf("%d%d%d",&u,&v,&w);
G[u-1].push_back(v-1); //顶点两边都要记录
E[u-1].push_back(w);
G[v-1].push_back(u-1);
E[v-1].push_back(w);
}
init();
for (int i = 0; i<n; i++) d[i] = (i == 0?0:INF);
dfs(0);
int start = 0;
int max = -1;
for (int i = 0; i<n; i++) {
if (d[i] > max && d[i] != INF) {
max = d[i];
start = i;
}
}
init();
for (int i = 0; i<n; i++) d[i] = (i == start?0:INF);
dfs(start);
int ans = -1;
for (int i = 0; i<n; i++) {
if (d[i] > ans && d[i] != INF) {
ans = d[i];
}
}
//ans = 10*ans + ans*(ans+1)/2;
cout << ans << endl; //ans 即为直径
return 0;
}
走格子
参考:https://blog.csdn.net/qq_30076791/article/details/50428285
现有一个m * n的网格,从最左上角出发,每次只能向右或者向下移动一格,问有多少种不同的方法可以到达最右下角的格子?
要从A到B,必须向左走6步,向下也走6步,一共12步,我们可以从向下走入手,向下走的方法即从12步里选出6步向下,一共有C(12,6)种,因此从A到B的路线共有组合数C(12,6)种。
对于m*n的格子,一样的,就是从m+n步中选出m步向下或n步向右,因此为C(m+n,m)=C(m+n,n)种。
#include<stdio.h>
int n,m,dp[10005][10005];
int main()
{
while(~scanf("%d%d",&n,&m))
{
dp[0][0]=0;
for(int i=1; i<=n; i++)
dp[i][0]=1;
for(int j=1; j<=m; j++)
dp[0][j]=1;//初始化
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
{
dp[i][j]=dp[i-1][j]+dp[i][j-1];//动态规划转移方程
}
printf("%d\n",dp[n][m]);
}
return 0;
}
C++相关
指针和引用的区别
参考:https://blog.csdn.net/will130/article/details/48730725
一、指针和引用的定义和性质区别:
(1) 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
int a=1;int *p=&a;
int a=1;int &b=a;
上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
(2) 可以有const指针,但是没有const引用;
(3) 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(4) 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
(5) 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了,从一而终。
(6)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(7)指针和引用的自增(++)运算意义不一样;
二、相同点
都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;
引用是某块内存的别名。
三、联系
1、引用在语言内部用指针实现(如何实现?)。
2、对一般应用而言,把引用理解为指针,不会犯严重语义错误。引用是操作受限了的指针(仅容许取内容操作)。
引用是C++中的概念,初学者容易把引用和指针混淆一起。以下程序中,n是m的一个引用(reference),m 是被引用物(referent)。
int m;
int &n = m;
- n 相当于m 的别名(绰号),对n 的任何操作就是对m 的操作。
引用的一些规则如下:
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
- 以下示例程序中,k 被初始化为i 的引用。语句k = j 是把k 的值改变成为6,由于k 是i 的引用,所以i 的值也变成了6.
int i = 5;
int j = 6;
int &k = i;
k = j; // k 和i 的值都变成了6
- 上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
“引用传递”的性质像“指针传递”,而书写方式像“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?
答案是“用适当的工具做恰如其分的工作”。
指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。
就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?
如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。
总的来说,在以下情况下你应该使用指针:
一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),
二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
还有一种情况,就是当你重载某个操作符时,你应该使用引用。
尽可能使用引用,不得已时使用指针。
当你不需要“重新指向”时,引用一般优先于指针被选用。这通常意味着引用用于类的公有接口时更有用。引用出现的典型场合是对象的表面,而指针用于对象内部。
进程和线程的区别
参考:https://blog.csdn.net/csdn_terence/article/details/77835781
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
安卓NDK
一、谈谈你对 JNI 和 NDK 的理解
JNI:
JNI 是 Java Native Interface 的缩写,即 Java 的本地接口。
目的是使得 Java 与本地其他语言(如 C/C++)进行交互。
JNI 是属于 Java 的,与 Android 无直接关系。
NDK:
NDK 是 Native Development Kit 的缩写,是 Android 的工具开发包。
作用是更方便和快速开发 C/C++ 的动态库,并自动将动态库与应用一起打包到 apk。
NDK 是属于 Android 的,与 Java 无直接关系。
总结:
JNI 是实现的目的,NDK 是 Android 中实现 JNI 的手段。
二、谈谈你对 JNIEnv 和 JavaVM 理解
JavaVM
JavaVM 是虚拟机在 JNI 层的代表。
一个进程只有一个 JavaVM。(重要!)
所有的线程共用一个 JavaVM。(重要!)
JNIEnv
JNIEnv 表示 Java 调用 native 语言的环境,封装了几乎全部 JNI 方法的指针。
JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv 彼此独立。(重要!)
注意:
在 native 环境下创建的线程,要想和 java 通信,即需要获取一个 JNIEnv 对象。我们通过 AttachCurrentThread 和 DetachCurrentThread 方法将 native 的线程与 JavaVM 关联和解除关联。
三、解释一下 JNI 中全局引用和局部引用的区别和使用
全局引用
通过 NewGlobalRef 和 DeleteGlobalRef 方法创建和释放一个全局引用。
全局引用能在多个线程中被使用,且不会被 GC 回收,只能手动释放。
局部引用
通过 NewLocalRef 和 DeleteLocalRef 方法创建和释放一个局部引用。
局部引用只在创建它的 native 方法中有效,包括其调用的其它函数中有效。因此我们不能寄望于将一个局部引用直接保存在全局变量中下次使用(请使用全局引用实现该需求)。
我们可以不用删除局部引用,它们会在 native 方法返回时全部自动释放,但是建议对于不再使用的局部引用手动释放,避免内存过度使用。
扩展:弱全局引用
通过 NewWeakGlobalRef 和 DeleteWeakGlobalRef 创建和释放一个弱全局引用。
弱全局引用类似于全局引用,唯一的区别是它不会阻止被 GC 回收。
四、JNI 线程间数据怎么互相访问
考察点和上体类似,线程本来就是共享内存区域的,因此我们需要使用 全局引用。
五、怎么定位 NDK 中的问题和错误
一般在开发阶段的话,我们可以通过 log 来定位和分析问题。
如果是上线状态(即关闭了基本的 log),我们可以借助 NDK 提供的 addr2line 工具和 objdump 工具来定位错误。详情:
so 动态库崩溃问题定位(addr2line与objdump)
其它还可以使用 C/C++ 的一些分析工具。
六、静态注册和动态注册
静态注册:
通过 JNIEXPORT 和 JNICALL 两个宏定义声明,Java + 包名 + 类名 + 方法名 形式的函数名。不好的地方就是方法名太长了。
动态注册:
通常在 JNI_OnLoad 方法中通过 RegisterNatives 方法注册,可以不再遵从固定的命名写法(当然为了代码容易理解,名称还是尽量和 Java 中保持一致)。
七、API
有的变态题目还是会考验你一些 API 的运用,比如怎么在 JNI 里面调用 Java 的方法,怎么在 JNI 里面抛异常等等。所以一些 API 还是要熟悉一下的,大致都是什么功能,名字大致是啥呀,这个太多了,看链接介绍吧:
https://blog.csdn.net/afei__/article/details/81016413