前端小白的算法之路

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

問題背景

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

問題闡述

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

(若是你對這個問題感興趣或者以爲本身很NB,能夠停下來試着寫一下這個算法,不要偷看個人代碼哈😃高手略過😂)java

算法之路

看到這個問題,第一反應很簡單,無非就是先排個序,而後看狀況再分配任務,因而有了下面的初版程序:算法

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問題的研究。segmentfault

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. 善於概括總結,聚沙成塔,厚積薄發。
最後以村上春樹的一句話送給你們共勉:沒必要太糾結於當下,也沒必要太憂慮將來,當你經歷過一些事情的時候,眼前的風景已經和從前不同了。
相關文章
相關標籤/搜索