程序员都说自己平时日常的工作就是搬砖,后端说自己就是写写 CRUD,前端说自己就是对着设计稿调调 CSS 参数。但这样永远都无法向高级工程师岗位迈进,也就是业界所说的三年一大坎。如果你工作了三年,还依旧被迫或迷恋于做这些事情,那你的工资仍然没有长进也是没有理由抱怨的。
那么如何区分一个程序员的水平在一个什么级别上呢,关键的一个指标就是对算法的掌握程度,这就涉及到标题中所说的计算机基础知识。如果你是一个计算机相关专业毕业的本科生,你应该听过或学过《高等数学》和《线性代数》这两门课。当学生的时候你可能会觉得学习这东西对我以后工作有啥用呢,下面我们就来举个例子证明这些理论知识对编程具体有什么用。
假如给你一个非空数组(注意哦是非空,不需要考虑判空),然后里面是 N 个无序的整数,其中只有一个数只出现了一次,其他的都出现了两次,现在让你找出这个只出现过一次的数。
那么现在你可能打开 IDE 就开始敲,这问题很常见啊,我遍历一下数组,依次把元素放到另一个空数组中,遇到不存在于新数组的元素,我就扔进去,遇到已经存在于新数组的,那我就不往新数组扔了,而且我还要把新数组里这个数丢出来,因为它不是我要找的只出现过一次的数,等遍历结束,自然最后新数组就只剩下那个只出现过一次的数了。好,那么用代码写出来:
var singleNumber = function (nums) {
var list = [];
for (var i = 0; i < nums.length; i++) {
if (list.indexOf(nums[i]) < 0) {
list.push(nums[i]);
} else {
list.splice(list.indexOf(nums[i]), 1);
}
}
return list[0];
};
实现了,但是有什么问题呢?这时候就是第一个区分程序员水平的分水岭
普通程序员会觉得这方法很好,代码简洁、易懂、好维护,然后就提交代码了,剩下的交给测试,提了 bug 再解决就好了
那么这样的程序员如果不是到某一天突然觉悟,如此下去,就会变成业界所说的那种“工作三年但只有一年经验”的人。想要自我救赎,不沦为咸鱼应该怎么办呢?写完这段代码,你盯着它再看看,用批判性的眼光挑挑毛病。或许你就会发现,如果我们要找的数在数组的最后一个,那你前面的数组遍历、数组插入、数组移出的操作就都是在浪费计算机的时间和资源。如果这个待处理的数组有几百万几千万的长度呢,那你要拿到结果可能就会感觉你的电脑卡住了一样,其实它在疯狂地运行你写的代码,去找到结果,只不过在找到结果之前它在疯狂地写操作、读操作,么得办法。
这就是你导致的啊,你写的表面看上去岁月静好的程序放到实际应用中去跑,有可能就被用户吐槽是辣鸡的 APP。那么想要对得起起公司产品的质量,又想提高自己作为程序员的编程能力,我们再来品这段代码,细细地品。
if (list.indexOf(nums[i]) < 0)
这里虽然是用语言内置方法,看似没有遍历数组,但实际上是不是又得从头到尾遍历一遍数组才能给出这个判断结果,于是你在for (var i = 0; i < nums.length; i++)
的每一次里面又循环了一编数组,这就是所谓的时间复杂度为 O(n 的平方),想一下,是不是这样,假如要找的数在数组的最后一个,你就要遍历数组 N 次,每一次遍历过程中还要再遍历 N 次,外层套内层,结果就是遍历操作执行了 N * N 次。那么有没有什么办法可以优化循环内部的这个查找操作呢。
编程语言中是不是不只有数组这种存储结构,不要需求告诉我们要处理数组,我们就用数组去思考,编程语言本身提供了很多强有力的基础工具,数组只是最普通的数据结构,那还有一种可以快速查找的数据结构是什么呢?
就是字典,意如其名,我们如果使用一个字典找某个字,是不是通过索引目录就可以快速翻到某页找到要找的字,而不是像字谱一样从头到尾找一遍。
那么我们试着把if (list.indexOf(nums[i]) < 0)
这部分用字典替换掉
var map = {};
for (var i = 0; i < nums.length; i++) {
if (map[nums[i]]) {
delete map[nums[i]];
} else {
map[nums[i]] = 1;
}
}
return Object.keys(map)[0];
程序的运行变得稍微快了一点,并且没有影响程序的可读性,只是巧用了不同的数据结构,这是一名中级程序员应该做到的。
那么有没有更厉害的办法呢,这时就是所谓大牛的思路了(是时候展现真正的技术了!)大牛都是学过高数的,而且能把高数中的知识实实在在用到编程中的(不然怎么说学数学的如果转行做程序员,起步就很高,很多时候大家都是知道一些事实,但是不知道为什么,当你知道足够多的场景后你就会知道为什么)
看一下这个数学公式2 * (a + b + c) - (a + a + b + b + c) = c
是不是很简单,但是这公式里就藏着我们这道题的答案。不捉迷藏,解释一下,我们换个好理解的场景,给你 N 个物品,然后这里面只有一种类型数量为 1,其他类型的数量都为 2,那么你把所有类型加起来再乘 2 是不是就会比所有物品加起来多出那个只有一个的物品。(原谅我已经尽力了,这个例子也能解释为什么教别人和自己理解东西的难度不能相提并论,因为你理解一个东西,也许某个点你就顿悟了,但你要把你理解的东西清晰传达给别人,你需要想出足够简洁又有说服力的例子来让别人也能通过看你举得例子就能明白你想讲的东西,而且不同听者在听同样的东西时,达到理解的时机也不同。教授不是件容易的事,不然就不会有老师的水平参差不齐了,有时候你学不懂一个东西,也许不是你理解能力的问题,而是你的老师讲解能力的问题)
那么我们根据这个数学公式编写出如下的代码
var sumOfNums = 0,
sumOfSet = 0;
var set = new Set();
for (var i = 0; i < nums[i]; i++) {
if (set.has(nums[i])) {
sumOfSet += nums[i];
} else {
set.add(nums[i]);
sumOfNums += nums[i];
}
}
这就是单纯利用数学公式得出的结果,感受到数学的力量了吗。
但这也没多厉害,因为数学的本质是通过挖掘规律总结出一系列的公式,从而使得计算变得更快,但数学没有数据结构的概念,也就是数学的公式是不提供存储功能,只是一个公式,你给它输入,它给你结果。
这就要提到计算机为什么会成为第三次工业革命的标志物了,因为计算机不仅可以把人类交给它的公式用电来计算出来,它还可以提供存储。
存储这个词对于学过计算机的人太熟悉了,从操作系统中学过计算机基本组成中就包括存储,其中包含硬盘用于固定存储随机的数据,即 RAM,内存条用于存储计算机运行时的过程数据,即 ROM,这是物理层面上的存储。
还有宏观上的存储,比如数据库,用于存储计算机运行产生的数据,对于软件,也就是保存我们使用计算机所产生的一切内容,包括我此时写的这些文字,以及我是谁,我是在什么时间写的这篇文章,还有我的修改时间,修改过几次,这些都存储在数据库中。最初在没有计算机的时候,我们所使用的计算器,也曾提供过存储功能(我指的是 30 块钱以上的那种按键很多的计算器),可能有人没用过,但我当时发现它可以把 135 + 324 的结果先存储到一个存储器中,然后我再读取存储器中的内容继续去乘 3256,因为计算器是顺序输入的,所以如果你直接输入 135 + 324 x 3256,会按照输入顺序计算,也就是会先计算加法,但我们知道这是不对的,当然你也可以使用括号,或者把中间过程记在纸上,我只是举个例子解释当时的计算器就已经有了设计存储这个概念,但其只能说是帮助人类生活加速的一个尝试。
到后来计算机普及,才真正加速了人们的生活,它把前面铺好的路都整合了起来,成为一个功能巨大的机器。此时应该插播一句每个计算机专业学生都听过的话“编程=算法+数据结构”,到这里你应该知道为什么不是只有算法或只有数据结构。因为只有算法,你无法存储过程数据,而只有数据结构,你无法利用更优秀的数学公式,所以二者结合才是利用计算机为你做事的最佳实践。其产物也就是程序,通过编程所产生,有了程序,人们通过计算机才诞生今天的千姿百态的玩法。
好了,继续说这个例子,第三种解法利用数学公式,虽然更巧妙,但实际对于计算机运行来说,并没有变得更快,也没有节省内存。
那么我们想想还有没有其他解法,这就要利用文首提到的另一门计算机课程《线性代数》了,这门课是只针对于计算机专业的,虽然名字里有代数,听着像数学相关的课程,但其讲的内容都是计算机里的数学,说白了就是计算机的核心基本——二进制运算。
可能稍微了解过的人都知道,电脑虽然功能这么多,但它还是靠电运行的,也就是第三次工业革命是在第二次工业革命的基础上发展而出的,如果没有电,第三次工业革命的一切产物都是空谈,无法运作。你把你家电闸拉了,你看你还能玩游戏吗,你还能看视频听歌吗,都不能。
计算机是如何利用电实现了这么多功能呢,其实就回到了初中物理——通路和短路,电路正常接电就是通路,电路中间有断开的部分,整个电路都不工作,就是断路,这就是二进制的理论基础,计算机就是通过不断地组合线路板上数量非常庞大的电子元件实现了不同的逻辑组合,那数量有多庞大呢,一开始是没多大的,所以电脑所产生的功能也有限,但后来有了集成电路,计算机主板上能放的电子元件数量爆炸性增长,电脑的功能随之变得越来越多。甚至有一个叫摩尔的外国人提出“集成电路上可容纳的晶体管数量,约每隔两年就会增加一倍”(摩尔定律)这样的预言,这个预言到现在都一直没被打破,的确是按照这个规律发展的。
这样我们就好理解为什么在今天有了人工智能、大数据,为什么阿尔法狗可以在围棋上战胜人类,答案就是因为同样大小的一台电脑,每过两年,其功能数量就能翻倍,其计算能力地增长更是不可预测。这样就把计算机从组成到发展串起来了,这样再来看看今天你手中的手机,手机中的 APP,是不是就更好理解了。
那么回归正题,我们看看线性代数是怎么用于日常编程的,线性代数里有一种很强但是稍微有些难理解的运算叫做“异或”,我们试着从名字拆解以理解,“异或”即“不同”+“或”,或运算是线性代数中的基本运算,比如 1 或 0 就是 1,0 或 0 还是 0,1 或 1 也是 1,0 或 1 即是 1,有没有发现规律,这就是初中物理中的并联,如果两条电路是并联的,其中一条电路断掉是不影响整个电路通电的,这相当于什么呢,就相当于备用电器,你用两个插板都接着电,其中一个坏掉是不会影响你正常用电的,代码中也常见,比如
if (a || b) {
console.log("hello world");
}
这段代码中 a 或 b 有一个为真,就会打印出“hello world”。当然,程序员看到这里还会指出一个知识点就是如果 a 已经是真了,那么计算机就不会再去看 b 是否为真了,就直接打印“hello world”了,俗称“断路”,即或操作中先为真的数会导致后面判断断掉而不去进行判断。当然,这都是程序员关心的小知识了,无足挂齿。
那么我们继续看“异或”,先说结果,”异或“意味着两个数相同的时候结果就为假,而不同的时候则为真,即 1 异或 0 是 1,0 异或 1 也是 1,但 1 异或 1 是 0,0 异或 0 也是 0,纵观其规律就是两个字”拧巴“,就是两个数闹别扭,我偏不和你一样,这也符合当今时代年轻人的个性,不喜欢苟同,喜欢个性,与众不同。这样记“异或”就够了,就不用去咬文嚼字理解为什么这样的操作叫“异或”,记住“异或”重点在于“ 异”。只要“异”了就是真,“异”了就像在做“或”操作了,而“或”操作的精髓在于“有真则真”(后面这段解释可以不看,免得产生误导)
那么怎么利用”异或计算“这个东西呢,有以下这样的推导公式:
a ^ (0 == a);
a ^ (a == 0);
a ^ b ^ (a == a) ^ a ^ (b == 0) ^ (b == b);
一个数和 0 进行异或操作,如果这个数是 0,那么结果为 0,因为 0 异或 0 是 0,那么结果是不是就和这个数相同了,如果这个数不是 0 呢,那就和 0 不同,结果就是这个数了;那如果这个数和自身进行异或操作呢,那永远是 0,因为和自身异或,相当于两个相同的数进行异或;同时多个数进行异或操作允许交换顺序,利用交换顺序,结合上面两个规律,便得出如果有两个相同的数和一个只出现一次的数进行异或操作,最后得出的便是那个只出现过一次的数,也就是本题的解。
那我们只需要依次将数组的每个数都进行异或操作,即能得到那个”另类“的数,但给我们的是一个数组,我们只能依次遍历每一个数,那么如何记住上一次两个数的遍历结果呢,我们可以用一个变量存储这个值,那么这个数初始化为多少合适呢,只能是 0,假如是别的数,比如是 1,那么数组第一个数如果是 1,那么第一次异或结果就是 0,这样外部数据影响了数组内部数据的异或结果。但如果我们把外部变量初始化为 0,那么数组第一个数无论是几,第一次的异或结果都是数组第一个数本身,因为有理论做支撑(a 异或 0 等于 a)。因为所给数组中只存在出现两次的数和一个只出现一次的数,所以结果必然符合我们的异或推到公式(a ^ b ^ a == a ^ a ^ b == 0 ^ b == b),即多个出现两次的数和一个单独的仅出现一次的数在一次,都进行异或,顺序无所谓,最后只等于那个只出现一次的数。
最终代码如下:
var a = 0;
for (var i = 0; i < nums.length; i++) {
a = a ^= nums[i];
}
return a;
这样地计算既利用了计算机的数据结构,也利用到了算法,是应该优先考虑的解法,至于代码可读性自然没有前面的容易理解,这就要求编程人员掌握足够的计算机基础知识,知道什么”异或“操作,以及”异或“操作可以推导多个数进行”异或“的规律,而“异或”的知识就藏在《线性代数》那本书中。(这里我也不是推荐程序员都去写一些晦涩难懂的代码,从而其他和你水平不相当的人接手骂爹,因为看不懂你写的东西而全部重写。我们提供的是一种思路,作为程序员,应该拥有程序员精神,即是要有意识地提高你程序运行的速度,并使用尽可能少的内存资源,这种追求极致的精神其实在任何行业都是高手的基本素养)
总算是首尾呼应,自圆其说了。曾经有很多人说大学学习的东西和毕业后工作的内容没什么联系,也就是大学学的东西都没什么用。我也曾慢慢加入到这样的呼声中,但可能再过一个阶段,你回过头再去看一切,也许就会理解到这世上不可能存在没有任何联系的事物,一切都在一个大的网络联络之中。如果你觉得两个事情没有任何联系,那可能只是你还没找到联络其两物之间的那复杂的联系。
虽然标题是说计算机基础知识,但是字里行间插播了很多其他的内容,这也是为什么我把此文发在这里,而不是技术论坛,因为它的定位是杂谈,不算是技术文章,面向群体也是大众,所以尽量把很多计算机专业知识用白话解释,而不是一带而过。