时隔多日终于解决了埋在心头的一道难题,霎时云开雾散,今天把一路而来碰到的疑惑和心得都记录下来,也算是开启了本身探索算法的大门。javascript
问题背景
曾经有一个年少轻狂的职场小白,在前端圈子里摸爬滚打将近两年,本计划在js的道路上越走越远,以致于天天沉浸在js红皮书里不能自拔,忽然有一天脑抽想找leader比划两下,因而出现了下面的对话,小白:leader,您最近在干吗?手里有须要亟待解决的难题吗?leader:咦,确实有哎,咱的项目随着业务的不断发展,日均PV也愈来愈多,公司的两台机器已经快知足不了需求,如今须要解决一下机器的问题。小白:那还不简单,就是多搞几台机器,四核换八核,能够并行处理就OK了。leader:小伙子想法很美好啊,钱从哪来?那我先问你个简单的问题[1],你写个算法出来。因而这个问题应用而生,小白也开始了苦苦的算法中。。。html
问题阐述
假设一台双核处理器能够并行处理任务,它们的处理速度都为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问题的研究。编程
NP-hard,其中NP是指非肯定性多项式(non-deterministic polynomial,缩写NP)。所谓的非肯定性是指,可用必定数量的运算去解决多项式时间内可解决的问题。NP-hard问题通俗来讲是其解的正确性可以被“很容易检查”的问题,这里“很容易检查”指的是存在一个多项式检查算法。相应的,若NP中全部问题到某一个问题是图灵可归约的,则该问题为NP困难问题。segmentfault
旅行推销员问题就是最著名的NP问题之一,固然我要解决的这个问题(多线程多机调度问题)也属于NP问题之一,通常使用贪心算法来解决,因而我就开始了贪心算法之路。服务器
算法描述
贪心算法:(又称贪婪算法)是指,在对问题求解时,老是作出在当前看来是最好的选择。也就是说,不从总体最优上加以考虑,他所作出的是在某种意义上的局部最优解。贪心算法不是对全部问题都能获得总体最优解,关键是贪心策略的选择,选择的贪心策略必须具有无后效性,即某个状态之前的过程不会影响之后的状态,只与当前状态有关。数据结构
思想: 贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能得到局部最优解。每一步只考虑一个数据,他的选取应该知足局部优化的条件。若下一个数据和部分最优解连在一块儿再也不是可行解时,就不把该数据添加到部分解中,直到把全部数据枚举完,或者不能再添加算法中止。
过程:
- 创建数学模型来描述问题;
- 把求解的问题分红若干个子问题;
- 对每一子问题求解,获得子问题的局部最优解;
- 把子问题的解局部最优解合成原来解问题的一个解。
解决思路
多线程问题主要是多个服务器能够并行处理多个任务,寻求处理全部任务的状况下,用掉最少时间的问题。由于任务并不局限于在某一个服务器上处理,并且任务不能拆分,因此仍是要综合考虑怎么分配任务,属于多线程问题。
核心思路:(n表明任务,m表明机器)
- 将n个独立的任务按照时间从大到小排序;
- 若是n<=m,则须要的最短期就是n个任务当中的最大时间;
- 若是n>m,则先给每一个机器依次分配任务,第一次就分配了m个做业;
- 而后循环第一次分配的m个任务时间,选取处理时间最短的机器分配第m+1个任务;
- 依次循环全部机器所需时间,并选取最短期的机器分配下一个任务;
- 最后比较返回最长时间的机子时间则为所需的最短期。
实现过程:
程序设计
第二版程序:
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的角度放眼全局,尤为是多熟悉一些算法,这样的话编程思惟更有逻辑性,解决问题能力更强,在公司的不可替代性也就更大了。
反思总结
- 算法是计算机科学领域最重要的基石之一,由于计算机语言和开发平台突飞猛进,但万变不离其宗的是最基础的算法和理论,好比数据结构、算法设计、编译原理、计算机操做系统和数据库原理等等。在“开复学生网”上,有位同窗生动地把这些基础课程比喻为“内功”,把新的语言、技术、标准比拟为“外功”。成天赶时髦的人最后只懂得招式,没有功力,是不可能成为武林高手的。由此知道了算法的重要性,之后要多加学习。
- 善于向别人请教,计算机这个领域博大精深,本身不懂的还有不少不少,就好比此次脑子里就没有贪心算法这种思想,只能硬碰运气试答案,显然是浪费时间瞎折腾,遇到研究很久都没答案的问题必定要多加请教。
- 善于概括总结,聚沙成塔,厚积薄发。