ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。
Algorithm
题目解析:
之所以把这三道题目放在一起记录是因为它们都对位运算进行了很好的考察,更准确点说是对位运算里面的 异或操作 进行了很好的考察,我们一起来看看。
第一道题目,告诉你输入数组中除了其中的一个元素只出现了一次,其余的元素都出现了两次,让你找出这个只出现一次的元素。异或操作有一个性质就是 两个相同的整数异或的结果等于 0,另一个特性是 0 和任意整数异或,结果是这个整数本身。我们利用这两点就可以轻松解决这道题,其实就是数组中所有元素进行异或操作即可,出现两次的元素都相互彼此抵消,剩下的那个元素就是只出现一次的。
第二道题目,相比第一题,输入数组的条件变了,数组中除了其中的一个元素只出现了一次,其余的元素都出现了 三 次,最后的问题还是让你找出这个只出现一次的元素。这道题目,一开始看起来从位运算思考貌似是不可能的,但如果你从集合的角度去思考或许可以想到解法。如果我们遍历数组里面的元素,在遍历的过程中,我们会发现 对于每个元素来说只有三种情况,出现一次,出现两次,出现三次。因为我们要找的是出现一次的那个元素,而且最终除了我们要找的元素,其他所有的元素都会出现三次,因此我们需要想办法排除掉出现三次的元素。一开始的时候可以想,我们用两个集合,集合 1 用于存放出现一次的元素,集合 2 用于存放出现两次的元素,于是我们可以发现下面的逻辑对应关系:
如果遍历到的元素不在集合 1 中,也不在集合 2 中: 该元素第一次出现,加入集合 1
如果遍历到的元素在集合 1 中,不在集合 2 中: 该元素第二次出现,移出集合 1,加入集合 2
如果遍历到的元素不在集合 1 中,在集合 2 中: 该元素第三次出现,移出集合 2
上面的逻辑对应关系你应该很容易理解,但是我想说的是通过位操作可以做到这一点,我们不需要真正的集合,我们只需要用一个整数来代替集合即可。怎么解释呢?假设我们用整数 ones 表示集合 1,整数 twos 表示集合 2,这两个整数的值初始化均为 0。ones ^ ele[i] 表示把元素 ele[i] 加入到集合 1 中,如果说下一个元素 ele[i + 1] 来了,并且 ele[i] != ele[i + 1],那么 ones ^ ele[i] ^ ele[i + 1] 肯定会产生一个不为零的值,至于这个值是多少,你不用关心。但如果 ele[i] == ele[i + 1],那么 ones ^ ele[i] ^ ele[i + 1] 的结果肯定为 0,到这里,你应该知道通过异或运算,我们已经可以做到,将出现一次的元素加入集合 1,将出现两次的元素移出集合 1。但是这还不够,因为元素还有可能出现三次,如果仅仅是上面的异或表达式,第三次出现的元素还是会被加入到集合 1,我们还需要保证该元素不在集合 2 中,(ones ^ ele[i]) & (~twos) 就可以保证这一点。对集合 2 来说也是一样的,(twos ^ ele[i]) & (~ones) 保证将不存在于集合 1 中,且不存在集合 2 中的元素加入到集合 2。如果我们先更新集合 1,再更新集合 2,就可以实现我们之前说的逻辑对应关系。说到这里,如果你还是不理解,那么你 可以尝试把一个元素看作是一堆值为 1 的 bit 位的组合,比如 12 的二进制是 0001 0100,如果说 12 出现了三次,那么从右往左数第三位和第五位 bit 的就出现了三次。我们把这个结论放在数组中也是一样的,对于那些出现了 3 的整数倍次的 bits 位我们要进行消除,找到那些出现了 3 * n + 1 次的 bit 位,将它们组合在一起就是我们要找的元素,上面的位运算做的就是这个事情,与其说把元素放入集合中,我们也可以说 将元素的所有值为 1 的 bit 位放入集合中,这样会更好理解些。
第三道题目,和第一道题目只变化了一点,就是输入数组中除了 两 个元素出现了一次,其余的都出现了两次。我们依然可以从第一道题目的解法去思考这道题,如果我们还是按照第一题的做法,最后我们得到的答案将会是 ele1 ^ ele2 的结果,我们需要思考如何从这个结果出发,得到 ele1 和 ele2。首先思考一个问题 ele1 ^ ele2 的结果具体是什么,或者说里面有什么样的信息,异或操作是将相同的 bit 位置 0,相异的置 1,也就是说 ele1 和 ele2 异或的结果中为 1 的 bit 位是两个元素相异的 bit 位,再进一步讲,我们可以用这个 bit 位来区分两个元素。于是在第一题的基础之上,用一个 bit 位作为判断条件,来决定当前遍历到的元素和那个值进行异或,因为这时我们要求的值有两个。
从上面这些题目中你可以看到位运算的强大,上面三道题目的时间复杂度均为 O(n),但是位运算是更加底层的运算,实际时间消耗会比正常操作要更快一些。在理解位运算的时候,试着把 bit 作为最小单位去思考,或许会有不一样的发现。
参考代码(LC 136. Single Number):
public int singleNumber(int[] nums) {
int result = 0;
for (int i = 0; i < nums.length; ++i) {
result ^= nums[i];
}
return result;
}
参考代码(LC 137. Single Number II):
public int singleNumber(int[] nums) {
int ones = 0, twos = 0;
for (int i = 0; i < nums.length; ++i) {
ones = (nums[i] ^ ones) & (~twos);
twos = (nums[i] ^ twos) & (~ones);
}
return ones;
}
参考代码(LC 260. Single Number III):
public int[] singleNumber(int[] nums) {
if (nums == null || nums.length == 0) {
return new int[2];
}
int different = 0;
for (int i : nums) {
different ^= i;
}
// 这个操作是取 different 从左往右最后一个为 1 的 bit 位
different &= -different;
int[] ans = {0, 0};
for (int i : nums) {
if ((different & i) == 0) {
ans[0] ^= i;
} else {
ans[1] ^= i;
}
}
return ans;
}
Review
Mistakes Junior React Developers Make
初级 React 程序员容易犯的几个错误:
-
直接操作 DOM 节点
对于那些熟悉 jQuery 的前端程序员来说,比较容易犯这样的错误。但是在 React 中,我们都是通过去改变 state 来去进行页面的再刷新,如果你熟悉 React 的内部机制的话,你也应该知道 React 其实会建立虚拟的 DOM 树,然后拿虚拟的 DOM 树去和实际的 DOM 树进行对比,来决定要不要重新渲染 DOM 树。直接操作 DOM 节点会于 React 的初衷相违背,如果项目再大一点的话,将会很难测试和 debug。
-
忘记清理
比如说当组件 mount 的时候,你在 body 中加了事件监听函数,一定要记得在组件 unmount 的时候删除你之前添加的函数。不然的话,很容易造成内存的泄漏。
-
不写测试
这又是一个老生常谈的问题,放到前端也不例外。没有写测试的习惯或者说意识是很危险的一件事,首先你会把函数或者是模块写的非常的大且臃肿,这样别人很难从你的函数或者模块名中得知你写的函数所要做的事情,再者说来没有测试,当要增添新逻辑的时候,阅读代码将变得举步维艰。
-
不懂 Webpack
不是职业写前端的程序员,这一点我还真没注意。作者提到 Webpack 可以帮助我们理解 CSS 和 JS 是怎样绑在一起然后展现在用户浏览器的,理解 Webpack 也能够帮助我们发现并解决一些更为底层的前端的问题。
Tip
这周继续积累两个 Linux 下的常用指令:
ps
这个指令主要用于获取正在运行的程序,是 Process Status 的缩写
-
ps aux
列出所有正在运行的进程
-
ps auxww
和上面一样,也是列出所有正在运行的进程,但是会包括整个指令
-
ps aux | grep {string}
非常常用的一个指令,和 grep 结合,通过字符串匹配列出对应的进程
-
ps -u root
显示指定用户信息
find
这个命令非常好用,也非常常用,通常用于寻找指定文件 (目录也会被找到)
-
find {path} -name {filename}
在 path 下通过文件名寻找文件,并打印出文件的绝对路径
-
find {path} -type d -iname {dname}
在 path 下通过目录名寻找目录,并不区分大小写
-
find {path} -path /lib//*.ext
上面是一个例子,表明通过路径寻找文件
-
find {path} -name *.py -not -path /site-packages/
寻找文件的时候,我们也可以指定排除某些特定的路径
-
find {path} -size +500k -size -10M
通过指定文件的大小范围来进行查询
-
find {path} -mtime -7 -delete
找到那些过去 7 天内修改过的文件,删除它们
-
find {path} -name '{*.ext}' -exec {command {}} \;
通过文件名找到文件,然后执行
-exec后面的命令,这里我们可以用{}指代该文件
Share
本来是想写文章的,但是看到 Medium 上的一篇短文,觉得很赞,读完才意识到了什么才叫做真正的思考。
This 3-Minute Exercise Will Change the Way You Solve Problems
文章开头的时候给出了一个真实的例子,说是在一次面试中,有两个候选人,面试官问这两个候选人同样的问题,问题是 “对面的高楼的顶尖部分有多高?”。其中一个候选人是知道答案的,不加思考就给出了答案,另外一个候选人则是通过楼的倒影,做了初略的估计和计算,然后给出答案。最后面试的结果是,后面一个候选人被录用。
作者在这篇文章中想表达的意思有两点,第一点,相对于答案和结果来说,过程更加重要;第二点,也是更加重要的一点,我们要学会通过已知的东西去推导得出未知的东西。作者将第二点取了一个名字,结构化思考(Structure Thinking),当你遇到一个问题的时候,你需要做的不是反复检索并回忆你的过往经历,更不是借助 Google,百度等搜索引擎,亦或是求助他人的帮助。你需要做的是用你当前知道的信息,一步步地去推导得出答案,举个例子可能更好解释。比如说我想知道某个地铁站一天的人流量,当然我完全可以在网上直接找到答案,但是我想做的是通过现有的数据推导得出,比如我知道这个片区住了有 10 万的人口,首先我可以假设有 1/2 的人都需要借助公共交通上下班,至于 1/2 这个是怎么来的,并不重要,这是我结合实际情况估算出来的;1/2 的人中有 3/4 都是乘坐地铁上下班的,但是除了这些上下班的人流之外,还是有一些其他的人流,比如旅客、学生上下学等等,我们也可以估算出这样的人流大概有 2 万左右,于是我们就可以得出这个地铁站每天大概有 5 ~ 6 万的人流在里面。这个结果可能正确,也可能差之甚远,但是 关键在于我们从已知的信息出发,不断地问一些问题,然后最终逼近于答案。在这个过程中,你可能会发现一些问题,或是一些答案,这些都是宝,一个简单问题往深了想,都可以收获满满。
如果你观察仔细,生活当中这种可以让人深入思考的问题非常多。比如在某个城市,平时 9 点之前起床的人占总人数的多少百分比?周末呢?一个城市的地下铺了多长的输水管道,路线是如何的?高铁每分钟的人均行驶距离是多少?飞机呢?在市中心开一家中等规模的川菜馆(30桌),需要保证多少的日客流量才能让餐馆至少不亏本?。。。
这些都是随处可见的问题,有些甚至你都尝试去思考过,但是结果呢?你是不是又直接去找百度或者 Google 了?身处在这个快节奏的时代下,我们往往更希望快速地得到我们想知道的答案,一些工具代替我们去思考一些本该我们自己去思考的问题,这样会让我们产生一种所谓的固定思维,或者说是固定模式。 久而久之,我们的思考有了 边界性,我们只会去思考那些我们已经知道答案的东西,我们的答案依赖于别人告诉我们的答案,这样,何谈创新呢?
学会如何思考比学会用什么来思考更重要。处处皆学问,思想远比答案重要!