前端小白的算法之路


時隔多日終於解決了埋在心頭的一道難題,霎時雲開霧散,今天把一路而來碰到的疑惑和心得都記錄下來,也算是開啟了自己探索算法的大門。

問題背景

曾經有一個年少輕狂的職場小白,在前端圈子里摸爬滾打將近兩年,本計划在js的道路上越走越遠,以至於每天沉浸在js紅皮書里不能自拔,突然有一天腦抽想找leader比划兩下,於是出現了下面的對話,小白:leader,您最近在干嘛?手里有需要亟待解決的難題嗎?leader:咦,確實有哎,咱的項目隨着業務的不斷發展,日均PV也越來越多,公司的兩台機器已經快滿足不了需求,現在需要解決一下機器的問題。小白:那還不簡單,就是多搞幾台機器,四核換八核,可以並行處理就OK了。leader:小伙子想法很美好啊,錢從哪來?那我先問你個簡單的問題[1],你寫個算法出來。於是這個問題應用而生,小白也開始了苦苦的算法中。。。

問題闡述

假設一台雙核處理器可以並行處理任務,它們的處理速度都為1k/s,每個任務均以k為單位,如[300, 600, 300, 500, 1000, 700, 300],且每個任務不能拆分必須由單獨的核來執行,求一堆任務的最短時間算法?

(如果你對這個問題感興趣或者覺得自己很NB,可以停下來試着寫一下這個算法,不要偷看我的代碼哈😃高手略過😂)

算法之路

看到這個問題,第一反應很簡單,無非就是先排個序,然后看情況再分配任務,於是有了下面的第一版程序:

let arr = [300, 600, 300, 500, 1000, 700, 300];
function task(arr) {
let left = [];
let right = [];
let lefts = 0;
let rights = 0;
let flag = true; // 第一次累加最大值 第二次累加最小值 平分兩組任務
// 平分兩組任務
let newArr = arr.sort((a, b) => b - a);
if (flag) {
left.push(newArr[0]);
right.push(newArr[1]);
newArr = newArr.slice(2);
} else {
left.push(newArr[newArr.length - 1]);
right.push(newArr[newArr.length - 2]);
newArr = newArr.slice(0, newArr.length - 2);
}
// 開關循環 第一次加最大值 第二次加最小值 依次累加
flag = !flag;
// 兩組任務分別之和
lefts = left.reduce((a, b) => a + b);
rights = right.reduce((a, b) => a + b);
// 只剩下一個任務或0個任務時,最終結果計算
if (newArr.length <= 1) {
if (newArr.length == 1) {
if ((lefts - rights) > newArr[0]) {
return lefts;
} else {
right.push(newArr[0]);
rights = right.reduce((a, b) => a + b);
return rights;
}
} else {
if (lefts < rights) {
return rights;
} else {
return lefts;
}
}
}
// 遞歸調用實現循環
return task(newArr);
};
alert("最短時間為:" + task(arr) + 's');

基本思路就是先把一堆任務排序,然后開始分配,第一次給第一台機子最大值,第二台機子次大值,第二次給第一台機子最小值,第二台機子次小值,依次遞歸調用累加,直至最后結束,如果是奇數個任務最后剩下一個任務的話,需要把這個任務分給時間較小的一組,最后返回一組時間較大的即是最終所需的最短時間。

顯然這個程序是有問題的,於是開始了研究,多天之后依舊沒有給出正確的答案,憑借一己之力顯然不能解決,然后開始在segmentfault上提問,沒想到很快就有人回復了,是NP-hard問題。近似算法參見partition problem

看到回復后迫不及待的開始百度Google,竟然讓我大吃一驚,2000年,美國克萊數學研究所公布了世界七大數學難題,又稱千禧年大獎難題。其中P與NP問題被列為這七大世界難題之首,看到這大大激發了我對這一問題的研究熱情,於是開始了NP問題的研究。

NP-hard,其中NP是指非確定性多項式(non-deterministic polynomial,縮寫NP)。所謂的非確定性是指,可用一定數量的運算去解決多項式時間內可解決的問題。NP-hard問題通俗來說是其解的正確性能夠被“很容易檢查”的問題,這里“很容易檢查”指的是存在一個多項式檢查算法。相應的,若NP中所有問題到某一個問題是圖靈可歸約的,則該問題為NP困難問題。

旅行推銷員問題就是最著名的NP問題之一,當然我要解決的這個問題(多線程多機調度問題)也屬於NP問題之一,一般使用貪心算法來解決,於是我就開始了貪心算法之路。

算法描述

貪心算法:(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的是在某種意義上的局部最優解。貪心算法不是對所有問題都能得到整體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具備無后效性,即某個狀態以前的過程不會影響以后的狀態,只與當前狀態有關。

思想: 貪心算法的基本思路是從問題的某一個初始解出發一步一步地進行,根據某個優化測度,每一步都要確保能獲得局部最優解。每一步只考慮一個數據,他的選取應該滿足局部優化的條件。若下一個數據和部分最優解連在一起不再是可行解時,就不把該數據添加到部分解中,直到把所有數據枚舉完,或者不能再添加算法停止。

過程:

  1. 建立數學模型來描述問題;
  2. 把求解的問題分成若干個子問題;
  3. 對每一子問題求解,得到子問題的局部最優解;
  4. 把子問題的解局部最優解合成原來解問題的一個解。

解決思路

多線程問題主要是多個服務器可以並行處理多個任務,尋求處理所有任務的情況下,用掉最少時間的問題。因為任務並不局限於在某一個服務器上處理,而且任務不能拆分,所以還是要綜合考慮怎么分配任務,屬於多線程問題。

核心思路:(n代表任務,m代表機器)

  1. 將n個獨立的任務按照時間從大到小排序;
  2. 如果n<=m,則需要的最短時間就是n個任務當中的最大時間;
  3. 如果n>m,則先給每個機器依次分配任務,第一次就分配了m個作業;
  4. 然后循環第一次分配的m個任務時間,選取處理時間最短的機器分配第m+1個任務;
  5. 依次循環所有機器所需時間,並選取最短時間的機器分配下一個任務;
  6. 最后比較返回最長時間的機子時間則為所需的最短時間。

實現過程:

程序設計

第二版程序:

let arr = [700, 400, 300, 500, 100, 900];
function task(arr) {
// 1. 任務排序
let newArr = arr.sort((a, b) => b - a);
// 2. 兩組各取最大值和次大值
let left = [newArr[0]];
let right = [newArr[1]];
newArr = newArr.slice(2);
// 3. 分別計算兩組所用的時間
let lefts = newArr[0];
let rights = newArr[1];
// 4. 比較哪一組時間少就依次把下一個任務分給少的那組
newArr.forEach((item, index) => {
if (lefts < rights) {
left.push(item);
} else {
right.push(item);
}
// 分別計算每組所用的時間
lefts = left.reduce((a, b) => a + b);
rights = right.reduce((a, b) => a + b);
});
// 5. 返回較大值則是所用最短時間
return Math.max(lefts, rights);
};
alert("最短時間為:" + task(arr) + 's');

以上的第二版程序還是以最初的問題雙核處理器(相當於兩個機子)實現的,經測試正確通過,於是又拓展了多線程多機器的常見問題,就有了最終版的程序。

第三版程序:

let tasks = [300, 600, 300, 500, 1000, 700, 300];
function task(tasks, nums) {
// 1. 對任務進行從大到小排序
tasks = tasks.sort((a, b) => b - a);
// 2. 第一次給nums個機器分配前nums個任務
let machine = JSON.parse(JSON.stringify(Array(nums).fill([])));
tasks.forEach((item, index) => {
if(index < nums) {
machine[index].push(item);
}
});
// 3. 分別計算每個機器執行任務的時間
let times = Array(nums);
machine.forEach((item, index) => {
times[index] = item.reduce((a, b) => a + b);
});
// 4. 全部任務去掉第一次分配的nums個任務
tasks = tasks.slice(nums);
// 5. 比較哪台機器用的時間少就給哪台機器分配下一個任務
tasks.forEach((item, index) => {
// 給最短時間的機器分配任務
times.some((items, indexs) => {
if(items == Math.min(...times)) {
machine[indexs].push(item);
return true;
}
});
// 分別計算每台機器的執行時間
machine.forEach((items, indexs) => {
times[indexs] = items.reduce((a, b) => a + b);
});
});
// 6. 返回所有機器中時間最長的即是所有任務執行的最短時間
return Math.max(...times);
};
alert("最短輸出時間為:" + task(tasks, 3) + 's');

哈哈,終於可以松口氣了,這一路下來也是歷盡艱辛,在此非常感謝清華大學的@蘿卜的指點迷津,一語驚醒夢中人,讓我找到了解法,雖然不是最優的算法,也讓我醍醐灌頂,打開了探索算法的大門。以上代碼是用JavaScript實現的(你可以用你熟悉的語言實現一下哈😃),其他語言也是一樣的邏輯,所以做前端的千萬不要在js的世界里妄自尊大,要站在CTO的角度放眼全局,尤其是多熟悉一些算法,這樣的話編程思維更有邏輯性,解決問題能力更強,在公司的不可替代性也就更大了。

反思總結

  1. 算法是計算機科學領域最重要的基石之一,因為計算機語言和開發平台日新月異,但萬變不離其宗的是最基礎的算法和理論,比如數據結構、算法設計、編譯原理、計算機操作系統和數據庫原理等等。在“開復學生網”上,有位同學生動地把這些基礎課程比喻為“內功”,把新的語言、技術、標准比擬為“外功”。整天趕時髦的人最后只懂得招式,沒有功力,是不可能成為武林高手的。由此知道了算法的重要性,以后要多加學習。
  2. 善於向別人請教,計算機這個領域博大精深,自己不懂的還有很多很多,就比如這次腦子里就沒有貪心算法這種思想,只能硬碰運氣試答案,顯然是浪費時間瞎折騰,遇到研究好久都沒答案的問題一定要多加請教。
  3. 善於歸納總結,積少成多,厚積薄發。
最后以村上春樹的一句話送給大家共勉:不必太糾結於當下,也不必太憂慮未來,當你經歷過一些事情的時候,眼前的風景已經和從前不一樣了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM