1. 程式人生 > >工程中的演算法應用 - 簡單的三個例子

工程中的演算法應用 - 簡單的三個例子

[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改變世界,我堅信這一點,也希望分享這段經歷給更多的人,學而不已、闔棺乃止,今天是國家公祭日,願逝者安息,願生者奮發,願祖國昌盛,致敬