工程中的演算法應用 - 簡單的三個例子
阿新 • • 發佈:2020-04-04
[TOC]
## 前言
其實這篇文章早就想寫了,因為自己太懶,到現在才更新。雖然這三個例子都是最簡單的演算法,但是不得不說,相比較暴力的做法,確實提升了效率,也節省了程式設計師的時間。三個例子中用到的分別是二分查詢、二維平均卷積、非同步改同步。
## 二分查詢
### 應用背景
給定一個URL資源介面,如XXX_001.mp4代表視訊的第一秒、XXX_002.mp4代表視訊的第二秒,以此類推;如何寫一個多執行緒爬蟲,把整個視訊資源快速爬下來?
### 對照演算法
容易想到的就是順序爬取,直接假設資源具有999個,將URL生成好存在佇列裡,開50個執行緒依次獲取,當有執行緒返回404時,通知佇列清空,其他排在之後的執行緒也停止工作,等待未下載完畢的執行緒處理,之後程式退出。
### 存在的問題
1. 假設資源只有20個,50個執行緒很明顯一瞬間打出來30個404,這對網站也是一種攻擊行為,很容易被反爬策略限制。
1. 各個執行緒之間的排程需要有效處理,假設2號執行緒返回404,在它之後從佇列裡拿取URL的所有執行緒都需要被停止,雖然Python的佇列是執行緒安全的,但是需要操作已經執行的其他執行緒仍存在一定的問題。
1. 因為網路io的不穩定問題,很可能介面返回異常值如500,這時候需要處理重試,同時還需要及時確定資源是否已爬取完畢。
### 解決方案
假設我們一開始就可以知道資源的總數,那麼很容易得到佇列裡應有的URL,組織多執行緒爬取也就變得簡單,只需要超時重試這一個操作即可。
這裡可以對URL做一個抽象,我們進行二分查詢的物件可以視為一個前半部分為1(200),後半部分為0(404)的陣列:{1, 1, 1, 1, 0, 0, 0},於是問題變為,用最小的陣列訪問次數,找出來最右邊的一個1。
### 程式碼方案
這裡是完整的程式碼實現:
```python
queue = []
max_num = 1000
for i in range(max_num):
ts = url.replace('{2}', '%03d' % i)
save = save_dir + '/%03d.ts' % i
queue.append((ts, save))
left = 0
right = max_num - 1
mid = 0
r = requests.get(queue[mid][0])
if r.status_code != 200:
print(str(video) + ' total: %d' % mid)
os.removedirs(save_dir)
return
while left <= right:
mid = (left + right) // 2
q = queue[mid]
u = q[0]
r = requests.get(u)
if r.status_code == 200:
left = mid + 1
with open(q[1], 'wb') as f:
f.write(r.content)
else:
right = mid - 1
r = requests.get(queue[mid][0])
if r.status_code == 200:
if not spider.file_exists_or_has_content(queue[mid][1]):
with open(queue[mid][1], 'wb') as f:
f.write(r.content)
mid += 1
queue = queue[:mid]
print(str(video) + ' total: %d' % mid)
```
可以看出,主要程式碼部分就是下面的二分查詢,使用這樣的臨界處理,可以找出最右邊的元素,以最小的訪問次數獲取URL資源總數,處理完畢後queue裡就是所有的資源了,其中在中間階段已經將爬取的部分視訊儲存,方便後面的執行緒不重複發起網路請求。這樣我們就可以愉快的爬小黃網了(誤
## 二維平均卷積
### 應用背景
給定一個原影象,一個輸出框和一個代表原影象顯著性分佈的顯著性影象(即畫面主體的灰度圖、越顯著灰度值越高),如何調整輸出框的位置,使得框住的影象顯著性最高(即裁剪問題)?
### 對照演算法
暴力解決的話很容易想到dp的方法,將輸出框左上角從(0, 0)開始位移,第一次計算所有畫素灰度的sum,每次移動都加上新覆蓋的一行(列)的灰度並減去取消覆蓋的一行(列)的灰度。
### 存在的問題
1. 耗時,是非常耗時,這種密集的cpu操作,io極短,相當於一直在讓cpu做加法、減法。
1. 假設我們需要對N對圖片都進行這樣的處理,即對視訊的每一幀處理,演算法的執行時間會成為嚴重效能瓶頸。
### 解決方案
有一種加速類似運算的操作叫做卷積,一般我們選擇的卷積核是為了提取影象關鍵部分、或為了影象增強,但是在這裡,可以有效的利用卷機的硬體加速效果實現我們的演算法加速;同時採用平均卷積避免加和出來的結果過大導致計算放慢。
### 程式碼方案
這裡是完整的程式碼實現:
```python
def crop_fix(sframe, sheight, swidth):
skenerl = np.ones((sheight, swidth)) / (sheight * swidth)
s = signal.convolve2d(sframe, skenerl, mode='valid')
m = np.argmax(s)
r, c = divmod(m, s.shape[1])
return r, c
```
簡潔易懂,這大概我2020上半年寫的最優雅的程式碼了吧,有效利用硬體加速效果,全部使用庫裡優化過的函式。於是就可以愉快的省下時間划水啦(誤
## 非同步改同步
這個理論上不算演算法的解決方案,但是也屬於程式碼的小trick,一併介紹了。
### 應用背景
假如你有一個JTree(Java Swing知識、未獲取前置知識點的同學請看書),即一個樹形選單,其中每個節點的展開都會觸發Expand事件(委託事件模型、Swing的nb之處),其會啟動一個執行緒用以發起網路請求,動態載入樹的子節點;現在新增了一個需求,需要完全展開這個樹,同時保證請求數不至於爆炸,怎麼實現?
### 對照演算法
正常來說展開這個樹,我們會使用遞迴演算法,展開一個節點,遍歷其子節點依次呼叫這個演算法,當這個節點是子節點時停止。
### 存在的問題
1. 由於節點的展開與節點內容的獲取時非同步的,遞迴演算法不知道需要等待多長時間再開始遍歷其子節點,導致樹的展開不徹底。
1. 由於演算法使用遞迴,很難控制一個有效的延遲時間,使得每秒請求數不至於過高。
### 解決方案
將原始碼中非同步的方法嘗試改為同步,但又不影響原來的程式碼邏輯;這裡想到了Java的synchronized和ReentrantLock,這裡選用前者實現,因為後者還需要在類中維護一個變數,較為麻煩。
### 程式碼方案
首先我們在非同步方法里加上這一句:
```java
synchronized (ChxGUI.this) {
ChxGUI.this.notify();
}
```
這樣既不會影響原來程式碼的邏輯,又方便了我們獲取程序執行結束的資訊。之後實現我們需要的業務程式碼即可:
```java
allButton.addActionListener(e -> new Thread(() -> {
ChxGUI gui = ChxGUI.this;
gui.label.setText("請不要操作 稍等一會");
gui.tree.setEnabled(false);
synchronized (ChxGUI.this) {
try {
gui.tree.expandRow(0);
Thread.sleep((int) (1000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
TreeNode root = (TreeNode) gui.treeModel.getRoot();
for (int i = root.getChildCount() - 1; i > = 0; i--) {
TreeNode course = root.getChildAt(i);
synchronized (ChxGUI.this) {
try {
gui.tree.expandPath(ChxUtility.getPath(course));
Thread.sleep((int) (1000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
for (int j = course.getChildCount() - 1; j > = 0; j--) {
TreeNode lesson = course.getChildAt(j);
synchronized (ChxGUI.this) {
try {
gui.tree.expandPath(ChxUtility.getPath(lesson));
Thread.sleep((int) (2000 * Math.random()));
ChxGUI.this.wait();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
gui.tree.setEnabled(true);
gui.label.setText("就緒。");
}).start()
);
```
這裡的重點就是使用同一個同步量進行兩者的同步,這樣可以把程式碼從非同步執行改為同步執行,請求API的次數也就變得安全可控。於是就可以愉快的刷[網課](https://github.com/MikeWang000000/ChxUtility)啦(誤
## 後記
這些應該是些常見的優化思路,寫在這裡是因為我確實運用這些解決了實際問題,也取得了不錯的效果,coding改變世界,我堅信這一點,也希望分享這段經歷給更多的人,學而不已、闔棺乃止,今天是國家公祭日,願逝者安息,願生者奮發,願祖國昌盛,致敬