目錄
什麼是邊框迴歸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