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

[TOC]

前言

其實這篇文章早就想寫了,因爲自己太懶,到現在才更新。雖然這三個例子都是最簡單的算法,但是不得不說,相比較暴力的做法,確實提升了效率,也節省了程序員的時間。三個例子中用到的分別是二分查找、二維平均卷積、異步改同步。

二分查找

應用背景

給定一個URL資源接口,如XXX_001.mp4代表視頻的第一秒、XXX_002.mp4代表視頻的第二秒,以此類推;如何寫一個多線程爬蟲,把整個視頻資源快速爬下來?

對照算法

容易想到的就是順序爬取,直接假設資源具有999個,將URL生成好存在隊列裏,開50個線程依次獲取,當有線程返回404時,通知隊列清空,其他排在之後的線程也停止工作,等待未下載完畢的線程處理,之後程序退出。

存在的問題

  1. 假設資源只有20個,50個線程很明顯一瞬間打出來30個404,這對網站也是一種攻擊行爲,很容易被反爬策略限制。
  2. 各個線程之間的調度需要有效處理,假設2號線程返回404,在它之後從隊列裏拿取URL的所有線程都需要被停止,雖然Python的隊列是線程安全的,但是需要操作已經運行的其他線程仍存在一定的問題。
  3. 因爲網絡io的不穩定問題,很可能接口返回異常值如500,這時候需要處理重試,同時還需要及時確定資源是否已爬取完畢。

解決方案

假設我們一開始就可以知道資源的總數,那麼很容易得到隊列裏應有的URL,組織多線程爬取也就變得簡單,只需要超時重試這一個操作即可。

這裏可以對URL做一個抽象,我們進行二分查找的對象可以視爲一個前半部分爲1(200),後半部分爲0(404)的數組:{1, 1, 1, 1, 0, 0, 0},於是問題變爲,用最小的數組訪問次數,找出來最右邊的一個1。

代碼方案

這裏是完整的代碼實現:

    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做加法、減法。
  2. 假設我們需要對N對圖片都進行這樣的處理,即對視頻的每一幀處理,算法的執行時間會成爲嚴重性能瓶頸。

解決方案

有一種加速類似運算的操作叫做卷積,一般我們選擇的卷積核是爲了提取圖像關鍵部分、或爲了圖像增強,但是在這裏,可以有效的利用卷機的硬件加速效果實現我們的算法加速;同時採用平均卷積避免加和出來的結果過大導致計算放慢。

代碼方案

這裏是完整的代碼實現:

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. 由於節點的展開與節點內容的獲取時異步的,遞歸算法不知道需要等待多長時間再開始遍歷其子節點,導致樹的展開不徹底。
  2. 由於算法使用遞歸,很難控制一個有效的延遲時間,使得每秒請求數不至於過高。

解決方案

將源代碼中異步的方法嘗試改爲同步,但又不影響原來的代碼邏輯;這裏想到了Java的synchronized和ReentrantLock,這裏選用前者實現,因爲後者還需要在類中維護一個變量,較爲麻煩。

代碼方案

首先我們在異步方法里加上這一句:

 synchronized (ChxGUI.this) {
     ChxGUI.this.notify();
 }

這樣既不會影響原來代碼的邏輯,又方便了我們獲取進程執行結束的信息。之後實現我們需要的業務代碼即可:

    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的次數也就變得安全可控。於是就可以愉快的刷網課啦(誤

後記

這些應該是些常見的優化思路,寫在這裏是因爲我確實運用這些解決了實際問題,也取得了不錯的效果,coding改變世界,我堅信這一點,也希望分享這段經歷給更多的人,學而不已、闔棺乃止,今天是國家公祭日,願逝者安息,願生者奮發,願祖國昌盛,致敬英雄!

原文出處:https://www.cnblogs.com/licsber/p/engineering-algo.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章