【四火】 你真的理解什么是全栈开发吗?

抱歉近几个月博客文章更新不够频繁,也有朋友问过,现在我想告诉大家,那是因为我写专栏去了。今天,专栏 《全栈工程师修炼指南》 终于上线了,下面的内容,就是我将这些年来,关于全栈开发,自己的一些经验、心得、感悟,总结起来,并且酝酿思考了很久,撰写的这个专栏。我想,目前市面上某一项具体技术的教程通常好找,但是系统的全栈技术关系树,包含这些技术之间的演进、权衡和本质介绍,并引发思考的学习材料却并不好找。 值得一提的是,这个专栏中我将 全程朗读所有的技术文章 ,为的就是能够尽可能地把原汁原味的技术内容传达给读者,希望你可以从中享受技术单纯原始的快乐。

下面的内容就是关于这个专栏的宣传介绍,你也可以直接拖动到文章底部,查看目录,点击 链接进入极客时间 购买,或扫描微信二维码购买。

 

提起“全栈工程师”,你最先想到的是什么?大神?全能?还是无用?

许多人对全栈的评价褒贬不一,不同人的理解也天差地别。有些人以为全栈是中小公司鼓吹的,有些人觉得大厂才招全栈,那么全栈究竟是做什么的?对于工程师而言,是全栈好,还是专注一个领域好?

我们先来看一个数据。下图来自 2018 Developer Skills Report,在开发者评价自己角色的时候,多数人投给了“全栈开发者”。

该如何学习成为一名全栈工程师?

很多人膜拜“全栈”,却在面对大量的技术栈时没有有效的学习路径和方法,尤其基于 Web 的全栈技术五花八门,涉及面广,迭代迅猛等等,我经常听到这样的困惑:

  • 想学 Web 全栈技术,期待能独立交付产品,但真的很迷茫;
  • 具体某项技术还好说,可全栈包含了那么多技术,怎么选?
  • 我该从哪里开始,遵循哪些原则,学习哪些技术?

为了帮大家解决这些问题,我在极客时间开了专栏 《全栈工程师修炼指南》,希望给你一条从碎片化到整体把握、清晰高效的学习路径,帮你系统掌握 Web 全栈的关键技术,真正从入门到技能实践。

我是谁?

我是熊燚,网上大家都叫我四火,现在在西雅图甲骨文(Oracle)的云计算部门就职,职位是首席软件工程师,负责云基础设施的分布式工作流引擎设计与开发,曾就职于华为、亚马逊(Amazon)。

最早我曾是华为某大型视频门户和视频平台的初创人员。后来加入了亚马逊,负责过数千万商品销量预测系统和成本利润计算平台的研发,重新设计并开发了数据分析和可视化系统,还维护和优化过数据分发的高可用服务,也改进过核算平台的分布式计算架构和工作流引擎。这些多领域的工作让我快速成长,并积累了大量的宝贵经验。

作为全栈工程的实践者,为了帮你更好的理解我所讲解的内容,特此给大家整理了一张「全栈开发核心知识框架图」,让你清晰地了解我们应该掌握的关键技术是什么。

我会如何讲解这个专栏?学完后能收获什么?

在专栏中,我会聚焦基于 Web 的全栈技术, 围绕“网络协议、MVC 架构、前端技术、持久层技术“等核心领域 ,梳理学习路径,对比剖析代表性技术,立足最佳实践、实战专题,带你从技术本质上理解、全面掌握全栈技能,培养“全栈高手思维”。

我在专栏中案例所用语言主要是 Java 和 JavaScript,由于全栈本身技术种类多、同类技术多的特点,专栏着重于讲原理、技术之间的演进、权衡和对本质的分析,并辅以非常多的实际项目和技术应用的案例。

  • 内容广度:我会选择每个核心领域的代表性技术来介绍,它们一定典型、常用,且深刻;
  • 内容深度:控制在合适的位置,让入门到进阶的工程师都有收获,我设计的“选修课堂”和“扩展阅读”,可以帮助你快速提升,一定不能略过。
  • 注重实践:我会引入最佳实践及自恰性强的专题,比如网站的性能优化、分页技术等,带你边学边做强化收获。

学习完后,希望你可以收获:

  1. 系统掌握 Web 全栈技能树
  2. 网络、前后端、持久化等核心技术解析
  3. 全栈开发的技术比较和选型
  4. 拓宽技术视野,培养全栈思维

1 分钟看看目录,你会发现你想要的

限时订阅福利

早鸟优惠¥68,原价¥99,你可以微信扫码购买,或者 点击此链接 进入极客时间购买。

这个世界需要专家,但更需要通晓各个层面知识,能够独立、快速解决问题的人。希望“全栈工程师”能成为你职业上升通道上的一个驿站,成为你的一个人生选择。

如果想领取高清版「全栈开发核心知识框架图 + 100 本架构师文集」,可以在「极客时间」公众号后台回复「全栈」领取。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/2ZQxYPd

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 闲话登月程序和程序员

2019年7月20日,是有纪念意义的一天,这天不是因为广大网民帮周杰伦在新浪微博上的超话刷到第一,而是阿波罗登月的50周年的纪念日。早在几年前,在Github上放出了当成Apollo飞船使用的源代码(当然是汇编的),但完全不明白为什么这几天会有一些中国的小朋友到这个github的issue里用灌水……,人类历史上这么伟大的一件事,为什么不借这个机会学习一下呢?下面是一些阿波罗登月与程序员相关的小故事,顺着这些东西,你可以把你的周末和精力用得更有价值。

首先,要说的是Apollo 11导航的源代码,这些代码的设计负责人叫Margaret Heafield Hamilton ,是一个女程序员,专业是数学和哲学,1960年得到一个MIT麻省理工大学的临时的软件开发职位,负责在PDP-1和LGP-30上运行天气预报的软件(注:在计算机历史上,PDP系统机器被称为Hack文化的重要推手,PDP-11且推了Unix操作系统,而Unix操作系统则是黑客文化的重要产品。参看《Unix传奇》)。然后,又为美国空军编写探测知敌方飞行的软件,之后,于1965年的时候,她加入了MIT仪器实验室,并成为了这个实验室的主管,这个实验实就是Apollo计划的一部分,她负责编写全新的月球登录的导航软件,以及后来该软件在其他项目中的各个版本。

上图是Hamilton站在她和她的麻省理工团队为阿波罗项目制作的导航软件源代码旁边,在Github上的开源的代码 – Apollo-11 (2016年开源)。我们可以看到,有两个重要的目录,一个目录叫“Comanche055”,一个目录叫“Luminary099”,前是指挥舱用的(英文为 Command Module )后者为登月舱用的(英文为 Lunar Module),这里需要说明一下的是,指挥舱是把登录舱推到月球上,在返回的时候,登录舱是被抛弃掉的,而返回到地球的是指挥舱。如果你想看这两份源代码的纸版,你可以访问这两个链接:Comanche 55 AGC Program ListingLuminary 99 REv.1 AGC Program Listing

如果你仔细比较一下这两个目录中的文件,你会发现有些文件是一样的,不但文件名一样,而且内容也一样。这说明这两个模块中的一些东西是相似的。

这些代码应该是很难读了,因为当时写这些代码的时候,C语言都没有被发明,所以基本上来说都是汇编代码了,而且还可以发现,这些代码的源文件全是以agc后缀结尾的, 看来这还不是我们平时所了解的汇编,所谓的AGC代表了运行这些代码的计算机 – Apollo Guideance Computer 。沿着这个Wikipedia的链接,你可以看到AGC这个电脑的指令是什么样的,看懂那几条指令后,这些源代码也就能读懂了。当然,因为是写成汇编的,所以,读起来还是要费点神的。不过,其中有一个文件是 THE_LUNAR_LANDING.apc 你会不会很好奇想去看看?

打开源文件,你还可以看到每个文件都有很多很多的注释,非常友好,但是也有一些注释比较有趣。这里有一组短视频带你读这些代码 – Exploring the Apollo Guidance Computer(AGC) Code,一供10个小视频,每个2分钟左右,如果你英文听边还行,可以看看,了解一下AGC的工作方式,挺有趣意思的。

下面是AGC在Apollo 1指挥舱里的样子(图片截自上面的视频),这个高质量的3D扫描来自 Simithsonian 3D: Apollo 11 Command Module

这个AGC的操作界又叫DSKY – Display 和 Keyboard的缩写,下图是一个 AGC 模拟器,其官方主页在 https://www.ibiblio.org/apollo/源代码在 Github/VirtualAGC。在这个界面上我们可以看到:下面的键盘上左边有两个键,一个是动词Verb一个是名词Noun,Verb指定操作类型,Noun指定要由Verb命令修改的数据。右边的显示器下面有三个5位的数字,这三个数值显示表示航天器姿态的矢量,以及所需速度变化的显示矢量。是的,当年的导航就靠这三个数字和里面的程序了。

 

如果你想了解AGC更多的细节,你可以看看 这篇 AGC for Dummies

另外,我在Youtube上找到了一个讲当时Apollo电脑的纪录片 – Navigation Computer,太有趣了。比如:21分51秒开始讲存储用的 Rope Memory 绕线内存,Hamilton 也出来讲了一下在这种内存上编程,画内切到一个人用个金属棍在一个像主板一样的东西上,左右穿梭,就像刺绣一样,但是绣的不是图案,而是程序……

看完上面这个纪录篇,我是非常之惊叹,惊叹于50年前的工程能力,惊叹于50年前这些人面对技术的的一丝不苟,对技术的尊重和严谨的这种精神和方法,一点都不比较今天差。

不过,最牛的还不是这个,我在Hamilton的Wikipedia词条上找到了他说的一个事件—— 当年Apollo登陆雷达开关放在了错误的位置,导致AGC收到了不少错误的信号。结果就是AGC既得执行着陆必须的计算,又要接受这些占用其15%时间的额外数据。但是AGC的程序居然可以用高优先级的任务打断低优先级的任务,于是,AGC自动剔除了低级别的任务以保证了重要的任务完成。Hamilton 原话说—— 如果当时的程序不能识别错误并从错误中恢复,我怀疑阿波罗不能成功登月。f the computer hadn’t recognized this problem and taken recovery action, I doubt if Apollo 11 would have been the successful moon landing it was。

看到这里,你有没有觉得——“这个女程序员的一小步,是整个人类的一大步”?

三年前,Apollo的源代码被开源时候,Twitter有个叫 Lin Clark 的人发了一条推:“我妈50年前的代码被放到Github上了”,是的,她是 Hamilton 的女儿,也是一个程序员,目前在 Mozilla工作,Staff Engineer,专长 WebAssembly, Rust, 和 JavaScript 。当她在Twitter上这么自豪地发了一条这样的推后,我不知道各位有什么想法?

 

尤其是那些到Apollo源代码的issue里灌水的人,你们是不是想让你们的孩子在登月100周年纪念的时候说——50年前我爹那个傻叉在Apollo的github的issue列表时灌了水?

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 July 21, 2019 at 07:00PM
from 酷 壳 – CoolShell https://ift.tt/2Z4Julw

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 如何超过大多数人

当你看到这篇文章的标题,你一定对这篇文章产生了巨大的兴趣,因为你的潜意识在告诉你,这是一篇“人生秘籍”,而且还是左耳朵写的,一定有很有参考意义,只要读完,一定可以找到超过大多数人的快车道和捷径……然而,当你看到我这样开篇时,你一定会觉得我马上就要有个转折,告诉你这是不可能的,一切都需要付出和努力……然而,你错了,这篇文章还真就是一篇“秘籍”,只要你把这些“秘籍”用起来,你就一定可以超过大多数人。而且,这篇文章只有我这个“人生导师”可以写得好。毕竟,我的生命过到了十六进制2B的年纪,踏入这个社会已超过20年,舍我其谁呢?!

P.S. 这篇文章借鉴于《如何写出无法维护的代码》一文的风格……嘿嘿

相关技巧和最佳实践

要超过别人其实还是比较简单的,尤其在今天的中国,更是简单。因为,你只看看中国的互联网,你就会发现,他们基本上全部都是在消费大众,让大众变得更为地愚蠢和傻瓜。所以,在今天的中国,你基本上不用做什么,只需要不使用中国互联网,你就很自然地超过大多数人了。当然,如果你还想跟他们彻底拉开,甩他们几个身位,把别人打到底层,下面的这些“技巧”你要多多了解一下。

在信息获取上,你要不断地向大众鼓吹下面的这些事:

  • 让大家都用百度搜索引擎查找信息,订阅微信公众号或是到知乎上学习知识……要做到这一步,你就需要把“百度一下”挂在嘴边,然后要经常在群或朋友圈中转发微信公众号的文章,并且转发知乎里的各种“如何看待……”这样的文章,让他们爱上八卦,爱上转发,爱上碎片。
  • 让大家到微博或是知识星球上粉一些大咖,密切关注他们的言论和动向……是的,告诉大家,大咖的任何想法一言一行都可以在微博、朋友圈或是知识星球上获得,让大家相信,你的成长和大咖的见闻和闲扯非常有关系,你跟牛人在一个圈子里你也会变牛。
  • 把今日头条和抖音这样的APP推荐给大家……你只需要让你有朋友成功地安装这两个APP,他们就会花大量的时间在上面,而不能自拔,要让他们安装其实还是很容易的,你要不信你就装一个试玩一会看看(嘿嘿嘿)。
  • 让大家热爱八卦,八卦并不一定是明星的八卦,还可以是你身边的人,比如,公司的同事,自己的同学,职场见闻,社会热点,争议话题,……这些东西总有一些东西会让人心态有很多微妙的变化,甚至花大量的时间去搜索和阅读大量的观点,以及花大量时间与人辩论争论,这个过程会让人上瘾,让人欲罢不能,然而这些事却和自己没有半毛钱关系。你要做的事就是转发其中一些SB或是很极端的观点,造成大家的一睦讨论后,就早早离场……
  • 利用爱国主义,让大家觉得不用学英文,不要出国,不要翻墙,咱们已经是强国了……这点其实还是很容易做到的,因为学习是比较逆人性的,所以,只要你鼓吹那些英文无用论,出国活得更惨,国家和民族都变得很强大,就算自己过得很底层,也有大国人民的感觉。

然后,在知识学习和技能训练上,让他们不得要领并产生幻觉

  • 让他们混淆认识和知识,以为开阔认知就是学习,让他们有学习和成长的幻觉……
  • 培养他们要学会使用碎片时间学习。等他们习惯利用碎片时间吃快餐后,他们就会失去精读一本书的耐性……
  • 不断地给他们各种各样“有价值的学习资料”,让他们抓不住重点,成为一个微信公众号或电子书“收藏家”……
  • 让他们看一些枯燥无味的基础知识和硬核知识,这样让他们只会用“死记硬背”的方式来学习,甚至直接让他们失去信心,直接放弃……
  • 玩具手枪是易用的,重武器是难以操控的,多给他们一些玩具,这样他们就会对玩具玩地得心应手,觉得玩玩具就是自己的专业……
  • 让他们喜欢直接得到答案的工作和学习方式,成为一个伸手党,从此学习再也不思考……
  • 告诉他们东西做出来就好了,不要追求做漂亮,做优雅,这样他们就会慢慢地变成劳动密集型……
  • 让他们觉得自己已经很努力了,剩下的就是运气,并说服他们去‘及时行乐’,然后再也找不到高阶和高效率学习的感觉……
  • 让他们觉得“读完书”、“读过书”就行了,不需要对书中的东西进行思考,进行总结,或是实践,只要囫囵吞枣尽快读完就等同于学好了……

最后,在认知和格局上,彻底打垮他们,让他们变成韭菜。

  • 让他们不要看到大的形势,只看到眼前的一亩三分地,做好一个井底之蛙。其实这很简单,比如,你不要让他们看到整个计算机互联网技术改变人类社会的趋势,你要多让他看到,从事这一行业的人有多苦逼,然后再说一下其它行业或职业有多好……
  • 宣扬一夜暴富以及快速挣钱的案例,最好让他们进入“赌博类”或是“传销类”的地方,比如:股市、数字货币……要让他们相信各种财富神话,相信他们就是那个幸运儿,他们也可以成为巴菲特,可以成为马云……
  • 告诉他们,一些看上去很难的事都是有捷径的,比如:21天就能学会机器学习,用区块链就能颠覆以及重构整个世界等等……
  • 多跟他们讲一些小人物的励志的故事,这样让他们相信,不需要学习高级知识,不需要掌握高级技能,只需要用低等的知识和低级的技能,再加上持续不断拼命重复现有的工作,终有一天就会成功……
  • 多让他们跟别人比较,人比人不会气死人,但是会让人变得浮躁,变得心急,变得焦虑,当一个人没有办法控制自己的情绪,没有办法让自己静下心来,人会失去耐性和坚持,开始好大喜欢功,开始装逼,开始歪门邪道剑走偏锋……
  • 让他们到体制内的一些非常稳定的地方工作,这样他们拥有不思进取、怕承担责任、害怕犯错、喜欢偷懒、得过且过的素质……
  • 让他们到体制外的那些喜欢拼命喜欢加班的地方工作,告诉他们爱拼才会赢,努力加班是一种福报,青春就是用来拼的,让他们喜欢上使蛮力的感觉……
  • 告诉他们你的行业太累太辛苦,干不到30岁。让他们早点转行,不要耽误人生和青春……
  • 当他们要做决定的时候,一定要让他们更多的关注自己会失去的东西,而不是会得到的东西。培养他们患得患失心态,让他们认识不到事物真正的价值,失去判断能力……(比如:让他们觉得跟对人拍领导的马屁忠于公司比自我的成长更有价值)
  • 告诉他们,你现有的技能和知识不用更新,就能过好一辈子,新出来的东西没有生命力的……这样他们就会像我们再也不学习的父辈一样很快就会被时代所抛弃……
  • 每个人都喜欢在一些自己做不到的事上找理由,这种能力不教就会,比如,事情太多没有时间,因为工作上没有用到,等等,你要做的就是帮他们为他们做不到的事找各种非常合理的理由,比如:没事的,一切都是最好的安排;你得不到的那个事没什么意思;你没有面好主要原因是那个面试官问的问题都是可以上网查得到的知识,而不没有问到你真正的能力上;这些东西学了不用很快会忘了,等有了环境再学也不迟……

最后友情提示一下,上述的这些“最佳实践”你要小心,是所谓,贩毒的人从来不吸毒,开赌场的人从来不赌博!所以,你要小心别自己也掉进去了!这就是“欲练神功,必先自宫”的道理。

相关原理和思维模型

对于上面的这些技巧还有很多很多,你自己也可以发明或是找到很多。所以,我来讲讲这其中的一些原理。

一般来说,超过别人一般来说就是两个维度:

  1. 在认知、知识和技能上。这是一个人赖以立足社会的能力(参看《程序员的荒谬之言还是至理名言?》和《21天教你学会C++》)
  2. 在领导力上。所谓领导力就是你跑在别人前面,你得要有比别人更好的能力更高的标准(参看《技术人员发展之路》)

首先,我们要明白,人的技能是从认识开始,然后通过学校、培训或是书本把“零碎的认知”转换成“系统的知识”,而有要把知识转换成技能,就需要训练和实践,这样才能完成从:认识 -> 知识 -> 技能 的转换。这个转换过程是需要耗费很多时间和精力的,而且其中还需要有强大的学习能力和动手能力,这条路径上有很多的“关卡”,每道关卡都会过滤掉一大部分人。比如:对于一些比较枯燥的硬核知识来说,90%的人基本上就倒下来,不是因为他们没有智商,而是他们没有耐心。

认知

要在认知上超过别人,就要在下面几个方面上做足功夫:

1)信息渠道。试想如果别人的信息源没有你的好,那么,这些看不见信息源的人,只能接触得到二手信息甚至三手信息,只能获得被别人解读过的信息,这些信息被三传两递后必定会有错误和失真,甚至会被传递信息的中间人hack其中的信息(也就是“中间人攻击”),而这些找不出信息源的人,只能“被人喂养”,于是,他们最终会被困在信息的底层,永世不得翻身。(比如:学习C语言,放着原作者K&R的不用,硬要用错误百出谭浩强的书,能有什么好呢?)

2)信息质量。信息质量主要表现在两个方面,一个是信息中的燥音,另一个是信息中的质量等级,我们都知道,在大数据处理中有一句名言,叫 garbage in garbage out,你天天看的都是垃圾,你的思想和认识也只有垃圾。所以,如果你的信息质量并不好的话,你的认知也不会好,而且你还要花大量的时间来进行有价值信息的挖掘和处理。

3)信息密度。优质的信息,密度一般都很大,因为这种信息会逼着你去干这么几件事,a)搜索并学习其关联的知识,b)沉思和反省,c)亲手去推理、验证和实践……一般来说,经验性的文章会比知识性的文章会更有这样的功效。比如,类似于像 Effiective C++/Java,设计模式,Unix编程艺术,算法导论等等这样的书就是属于这种密度很大的书,而像Netflix的官方blogAWS CTO的blog等等地方也会经常有一些这样的文章。

知识

要在知识上超过别人,你就需要在下面几个方面上做足功夫:

1)知识树(图)。任何知识,只在点上学习不够的,需要在面上学习,这叫系统地学习,这需要我们去总结并归纳知识树或知识图,一个知识面会有多个知识板块组成,一个板块又有各种知识点,一个知识点会导出另外的知识点,各种知识点又会交叉和依赖起来,学习就是要系统地学习整个知识树(图)。而我们都知道,对于一棵树来说,“根基”是非常重要的,所以,学好基础知识也是非常重要的,对于一个陌生的地方,有一份地图是非常重要的,没有地图的你只会乱窜,只会迷路、练路、走冤枉路!

2)知识缘由。任何知识都是有缘由的,了解一个知识的来龙去脉和前世今生,会让你对这个知识有非常强的掌握,而不再只是靠记忆去学习。靠记忆去学习是一件非常糟糕的事。而对于一些操作性的知识(不需要了解由来的),我把其叫操作知识,就像一些函数库一样,这样的知识只要学会查文档就好了。能够知其然,知其所以然的人自然会比识知识到表皮的人段位要高很多。

3)方法套路。学习不是为了找到答案,而是找到方法。就像数学一样,你学的是方法,是解题思路,是套路,会用方程式解题的和不会用方程式解题的在解题效率上不可比较,而在微积分面前,其它的解题方法都变成了渣渣。你可以看到,掌握高级方法的人比别人的优势有多大,学习的目的就是为了掌握更为高级的方法和解题思路

技能

要在技能上超过别人,你就需要在下面几个方面做足功夫:

1)精益求精。如果你想拥有专业的技能,你要做不仅仅是拼命地重复一遍又一遍的训练,而是在每一次重复训练时你都要找到更好的方法,总结经验,让新的一遍能够更好,更漂亮,更有效率,否则,用相同的方法重复,那你只不过在搬砖罢了。

2)让自己犯错。犯错是有利于成长的,这是因为出错会让人反思,反思更好的方法,反思更完美的方案,总结教训,寻求更好更完美的过程,是技能升级的最好的方式。尤其是当你在出错后,被人鄙视,被人嘲笑后,你会有更大的动力提升自己,这样的动力才是进步的源动力。当然,千万不要同一个错误重复地犯!

3)找高手切磋。下过棋,打个球的人都知道,你要想提升自己的技艺,你必需找高手切磋,在和高手切磋的过程中你会感受到高手的技能和方法,有时候你会情不自禁地哇地一下,我靠,还可以这么玩!

领导力

最后一个是领导力,要有领导力或是影响力这个事并不容易,这跟你的野心有多大,好胜心有多强 ,你愿意付出多少很有关系,因为一个人的领导力跟他的标准很有关系,因为有领导力的人的标准比绝大多数人都要高。

1)识别自己的特长和天赋。首先,每个人DNA都可能或多或少都会有一些比大多数人NB的东西(当然,也可能没有),如果你有了,那么在你过去的人生中就一定会表现出来了,就是那种大家遇到这个事会来请教你的寻求你帮助的现象。那种,别人要非常努力,而且毫不费劲的事。一旦你有了这样的特长或天赋,那你就要大力地扩大你的领先优势,千万不要进到那些会限制你优势的地方。你是一条鱼,你就一定要把别人拉到水里来玩,绝对不要去陆地上跟别人拼,不断地在自己的特长和天赋上扩大自己的领先优势,彻底一骑绝尘。

2)识别自己的兴趣和事业。没有天赋也没有问题,还有兴趣点,都说兴趣是最好的老师,当年,Linus就是在学校里对minx着迷了,于是整出个Linux来,这就是兴趣驱动出的东西,一般来说,兴趣驱动的事总是会比那些被动驱动的更好。但是,这里我想说明一下什么叫“真∙兴趣”,真正的兴趣不是那种三天热度的东西,而是那种,你愿意为之付出一辈子的事,是那种无论有多大困难有多难受你都要死磕的事,这才是“真∙兴趣”,这也就是你的“野心”和“好胜心”所在,其实上升到了你的事业。相信我,绝大多数人只有职业而没有事业的。

3)建立高级的习惯和方法。没有天赋没有野心,也还是可以跟别人拼习惯拼方法的,只要你有一些比较好的习惯和方法,那么你一样可以超过大多数人。对此,在习惯上你要做到比较大多数人更自律,更有计划性,更有目标性,比如,每年学习一门新的语言或技术,并可以参与相关的顶级开源项目,每个月训练一个类算法,掌握一种算法,每周阅读一篇英文论文,并把阅读笔记整理出来……自律的是非常可怕的。除此之外,你还需要在方法上超过别人,你需要满世界的找各种高级的方法,其中包括,思考的方法,学习的方法、时间管理的方法、沟通的方法这类软实力的,还有,解决问题的方法(trouble shooting 和 problem solving),设计的方法,工程的方法,代码的方法等等硬实力的,一开始照猫画虎,时间长了就可能会自己发明或推导新的方法。

4)勤奋努力执着坚持。如果上面三件事你都没有也没有能力,那还有最后一件事了,那就是勤奋努力了,就是所谓的“一万小时定律”了(参看《21天教你学会C++》中的十年学编程一节),我见过很多不聪明的人,悟性也不够(比如我就是一个),别人学一个东西,一个月就好了,而我需要1年甚至更长,但是很多东西都是死的,只要肯花时间就有一天你会搞懂的,耐不住我坚持十年二十年,聪明的人发明个飞机飞过去了,笨一点的人愚公移山也过得去,因为更多的人是懒人,我不用拼过聪明人,我只用拼过那些懒人就好了。

好了,就这么多,如果哪天你变得消极和不自信,你要来读读我的这篇文章,子曰:温故而知新。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 June 22, 2019 at 01:47PM
from 酷 壳 – CoolShell http://bit.ly/2Y5Op5d

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 分析运行中的 Python 进程

在 Java 中打印当前线程的方法栈,可以用 kill -3 命令向 JVM 发送一个 OS 信号,JVM 捕捉以后会自动 dump 出来;当然,也可以直接使用 jstack 工具完成,这些方法好几年前我在 这篇性能分析的文章 中介绍过。这样的需求可以说很常见,比如定位死锁,定位一个不工作的线程到底卡在哪里,或者定位为什么 CPU 居高不下等等问题。

现在工作中我用的是 Python,需要线上问题定位的缘故,也有了类似的需求——想要知道当前的 Python 进程“在干什么”。但是没有了 JVM 的加持,原有的命令或者工具都不再适用。传统的 gdb 的 debug 大法在线上也不好操作。于是我寻找了一些别的方法,来帮助定位问题,我把它们记录在这里。

signal

在代码中,我们可以使用 signal 为进程预先注册一个信号接收器,在进程接收到特定信号的时候,可以打印方法栈:

import traceback, signal

class Debugger():
    def __init__(self, logger):
        self._logger = logger
    
    def log_stack_trace(self, sig, frame):
        d={'_frame':frame}
        d.update(frame.f_globals)
        d.update(frame.f_locals)
    
        messages  = "Signal received. Stack trace:\n"
        messages += ''.join(traceback.format_stack(frame))
        self._logger.warn(messages)
    
    def listen(self):
        signal.signal(signal.SIGUSR1, self.log_stack_trace)

通过调用上面的 listen 方法(比如 new Debug(logger).listen()),就将一个可以接收 SIGUSR1 并打印方法栈的接收器注册到当前进程了。

那么怎么向进程发送信号呢?和 JVM 的方法类似,可以通过操作系统命令来发送:

kill -30 pid

这里的信号为什么是 30?这是因为 SIGUSR1 被当前操作系统定义成 30(请注意不同的操作系统这个映射表是可能不同的),这点可以通过 man signal 查看:

No Name Default Action Description
1 SIGHUP terminate process terminal line hangup
2 SIGINT terminate process interrupt program
3 SIGQUIT create core image quit program
4 SIGILL create core image illegal instruction
5 SIGTRAP create core image trace trap
6 SIGABRT create core image abort program (formerly SIGIOT)
7 SIGEMT create core image emulate instruction executed
8 SIGFPE create core image floating-point exception
9 SIGKILL terminate process kill program
10 SIGBUS create core image bus error
11 SIGSEGV create core image segmentation violation
12 SIGSYS create core image non-existent system call invoked
13 SIGPIPE terminate process write on a pipe with no reader
14 SIGALRM terminate process real-time timer expired
15 SIGTERM terminate process software termination signal
16 SIGURG discard signal urgent condition present on socket
17 SIGSTOP stop process stop (cannot be caught or ignored)
18 SIGTSTP stop process stop signal generated from keyboard
19 SIGCONT discard signal continue after stop
20 SIGCHLD discard signal child status has changed
21 SIGTTIN stop process background read attempted from control terminal
22 SIGTTOU stop process background write attempted to control terminal
23 SIGIO discard signal I/O is possible on a descriptor (see fcntl(2))
24 SIGXCPU terminate process cpu time limit exceeded (see setrlimit(2))
25 SIGXFSZ terminate process file size limit exceeded (see setrlimit(2))
26 SIGVTALRM terminate process virtual time alarm (see setitimer(2))
27 SIGPROF terminate process profiling timer alarm (see setitimer(2))
28 SIGWINCH discard signal Window size change
29 SIGINFO discard signal status request from keyboard
30 SIGUSR1 terminate process User defined signal 1
31 SIGUSR2 terminate process User defined signal 2

当然,也可以写一点点 python 脚本来发送这个信号:

import os, signal
os.kill($PID, signal.SIGUSR1)

原理是一样的。

strace

如果进程已经无响应了,或者上面的信号接收器没有注册,那么就要考虑别的方法来或者“进程在干什么”这件事情了。其中,一个有用的命令是 strace:

strace -p pid

比如,我自己写了一个测试脚本 t.py,使用 python 执行,然后调用 sleep,再给它发送一个 SIGUSR1 的消息,它打印方法栈并退出。这整个过程,我使用 strace 可以得到这样的结果:

strace -p 9157
strace: Process 9157 attached
select(0, NULL, NULL, NULL, {9999943, 62231}) = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGUSR1 {si_signo=SIGUSR1, si_code=SI_USER, si_pid=9273, si_uid=9007} ---
rt_sigreturn({mask=[]})                 = -1 EINTR (Interrupted system call)
stat("t.py", {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
open("t.py", O_RDONLY)                  = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f631e866000
read(3, "import traceback, signal, time\n "..., 8192) = 1281
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7f631e866000, 4096)            = 0
stat("t.py", {st_mode=S_IFREG|0644, st_size=1281, ...}) = 0
write(1, "Signal received. Stack trace:\n  "..., 134) = 134
write(1, "\n", 1)                       = 1
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f631e06f5d0}, {0x7f631e392680, [], SA_RESTORER, 0x7f631e06f5d0}, 8) = 0
rt_sigaction(SIGUSR1, {SIG_DFL, [], SA_RESTORER, 0x7f631e06f5d0}, {0x7f631e392680, [], SA_RESTORER, 0x7f631e06f5d0}, 8) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

可以看到从 strace attached 开始,到进程退出,所有重要的调用都被打印出来了。

在 iOS 下,没有 strace,但是可以使用类似的(更好的)命令 dtruss。

lsof

lsof 可以打印某进程打开的文件,而 Linux 下面一切都是文件,因此查看打开的文件列表有时可以获取很多额外的信息。比如,打开前面提到的这个测试进程:

lsof -p 16872
COMMAND   PID  USER   FD   TYPE DEVICE   SIZE/OFF     NODE NAME
Python  16872 xxx  cwd    DIR    1,5       2688  1113586 /Users/xxx
Python  16872 xxx  txt    REG    1,5      51744 10627527 /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
Python  16872 xxx  txt    REG    1,5      52768 10631046 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/_locale.so
Python  16872 xxx  txt    REG    1,5      65952 10631134 /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/lib-dynload/time.so
Python  16872 xxx  txt    REG    1,5     841440 10690598 /usr/lib/dyld
Python  16872 xxx  txt    REG    1,5 1170079744 10705794 /private/var/db/dyld/dyld_shared_cache_x86_64h
Python  16872 xxx    0u   CHR   16,2    0t39990      649 /dev/ttys002
Python  16872 xxx    1u   CHR   16,2    0t39990      649 /dev/ttys002
Python  16872 xxx    2u   CHR   16,2    0t39990      649 /dev/ttys002

它有几个参数很常用,比如-i,用来指定网络文件(如果是“-i: 端口号”这样的形式还可以指定端口)。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2Xpz0zB

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 谈谈 Ops(最终篇):工具和实践

除了主要内容——工具和实践,这篇文章也对“谈谈 Ops”系列做一个汇总,提供一个访问入口。之前几篇,从一个纯粹 dev 狭窄的视角,谈了谈自己对 Ops 的一些认识:

在往下继续以前,如果没有看过前面的文字,不妨移步阅读,因为上面的内容对下面的内容做了一定程度的铺垫。

现在在写的这一篇文字,我准备是最后一篇,主要谈论这样几个事情:一个是工具,另一个是实践。我依然还是从 dev 的视角,而不是从一个专业运维的视角来记叙。

工欲善其事,必先利其器。我在主要且通用的工具中挑了几个,和最佳实践放在一起介绍,并且按照功能和阶段来划分,而不执着于列出具体的工具名称。 其中最主要的阶段,包括开发阶段,集成测试阶段,以及线上部署维护阶段。 顺便也再强调一次,Ops 远不只有线上系统的维护。

Pipeline

把 Pipeline 放在最先讲,是因为它是集成下面各路神仙(工具)的核心,通过 pipeline,可以把一系列自动化的工具结合起来,而这一系列工具,往往从编译过程就开始,到部署后的验证执行结束。

Pipeline 最大的作用是对劳动力的解放,程序来控制代码从版本库到线上的行进流程,而非人。因此,对于那些一个 pipeline 上面设置了数个需要人工审批的暂停点,使得这样的事情失去了意义。这样的罪过往往来自于那些缺少技术背景的负责人,其下反映的是对流程的崇拜(前面关于流程和人的文章已经提到),而更进一步的原因是不懂技术,就无法相信代码,进而无法信任程序员,他们觉得,只有把生杀大权掌握在自己手里,才能得到对质量最好的控制。这样的问题从二十多年前的软件流程中就出现了,到现在愈演愈烈。我只能说,这无疑是一种悲哀。

我见过懂技术的老板,也见过不怎么懂技术的老板,还见过完全不懂技术的老板。但是最可怕的,是那种不太懂或完全不懂,却又非常想要插一手,对程序员在软件流程方面指手画脚的老板。为什么是流程?因为技术方面他们不懂,没法插手,却又觉得失去了掌控,只好搞流程了。

我曾经遇到过这样一件事情:程序有一个 bug,因为在一个判断中,状态集合中少放了一个枚举值,导致了一个严重的线上问题。后来,程序员修正了问题,老板和程序员有了这样的对话:

老板:“你怎么保证未来的发布不会有这样的问题?”

程序员:“我修正了啊。”

老板:“我怎么知道你修正了?”

程序员:“我发布了代码改动,我使用单元测试覆盖了改动。”

老板:“好,我相信你开发机的代码改动做了。可我怎么知道你发布到线上的版本没有问题?”

程序员:“……发布的代码就是我提交的啊。”

老板:“你怎么能保证代码从你提交到线上发布的过程中没有改动?”

程序员:“……(心中一千头草泥马奔腾而过)我可以到线上发布的 Python 包里面查看一下该行是不是已经得到修改。”

老板:“好。我们能不能在 pipeline 里面,添加这样一个步骤——执行部署的人到发布包里面去检查该行代码是不是正确的。”

程序员:“……(现在变成一万头了)不要把,这样一个额外的检查会浪费时间啊。”

老板:“检查一下需要多少时间?”

程序员:“5 分钟吧”

老板:“花费 5 分钟,避免一个严重的问题,难道不值得吗?”

程序员:“……(现在数不清多少头了)如果这一行要校对的话,为什么其它几十万行代码不用肉眼校对?”

老板:“就这一行需要。因为这一行代码曾经引发过严重问题,所以需要。”

程序员:“……”

如果你也见过这样的情形,不妨告诉我你的应对办法是什么。

依赖管理

以 Java 为例,有个搞笑的说法是“没有痛不欲生地处理过 Jar 包冲突的 Java 程序员不是真正的 Java 程序员”,一定程度上说明了依赖管理有多重要。尤其是茁壮发展的 Java 社区,副作用就是版本多如牛毛,质量良莠不齐,包和类的命名冲突简直是家常便饭。我用过几个依赖管理的工具,比如 Python 的 pip,比如 Java 的 Maven,但是最好的还是 Amazon 内部的那一个,很可惜没有开源。注意这里说的依赖,即便对于 Java 来说,也不一定是 Jar 包,可以是任何文件夹和文件。一个好的依赖管理的工具,有这么几点核心特性需要具备:

  1. 支持基于包和包组的依赖配置。目标软件可以依赖于配置的 Jar 包,而若干个 Jar 包也可以配置成一个组来简化依赖配置。
  2. 支持基于版本的递归依赖。比如 A 依赖于 B,B 依赖于 C,那么只需要在 A 的依赖文件中配置 B,C 就会被自动引入。
  3. 支持版本冲突的选择。比如 A 依赖于 B 和 C,B 依赖于 D 1.0,C 依赖于 D 2.0,那么通过配置可以选择在最终引入依赖的时候引入 D 1.0 还是 2.0。
  4. 支持不同环境的不同依赖配置,比如编译期的依赖,测试期的依赖和运行期的依赖都可能不一样。

当然,还有许许多多别的特性,比如支持冲突包的删除等等,只是没有那么核心。

自动化测试

代码检查、编译和单元测试(Unit Test)。这一步还属于代码层面的行为活动,代码库特定分支上的变动,触发这一行为,只有在这样的执行成功以后,后面的步骤才能得到机会运行。单元测试通常都是是 dev 写的,即便是在有独立的测试团队的环境中。因为单元测试重要的一个因素就是要保证它能够做到白盒覆盖。单元测试要求易于执行,由于需要反复执行和根据结果修改代码,快速的反馈是非常重要的,几十秒内必须得到结果。我见过有一些团队的单元测试跑一遍要十分钟以上,那么这种情况就要保证能够跑增量的测试,换言之,改动了什么内容,能够重跑改动的那一部分,而不是所有的测试集合。

集成测试(Integration Test)。这一步最主要的事情,就是自动部署代码到一个拟真的环境,之行端到端的测试。比如说,发布的产品是远程的 API,UT 关注的是功能单元,测试的对象是具体的类和方法;而在 IT 中,更关心暴露的远程接口,既包括功能,也包括性能。集成测试的成熟程度,往往是一个项目质量的一个非常好的体现。在某些团队中,集成测试通过几个不同的环境来完成,比如α环境、β环境、γ环境等等,依次递进,越来越接近生产环境。比如α环境是部署在开发机上的,而γ环境则是线上环境的拷贝,连数据库的数据都是从线上定期同步而来的。

冒烟测试(Smoke Testing)。冒烟测试最关心的不是功能的覆盖,而是对重要功能,或者核心功能的保障。到了这一步,通常在线上部署完成后,为了进一步确保它是一次成功的部署,需要有快速而易于执行的测试来覆盖核心测试用例。这就像每年的常规体检,不可能事无巨细地做各种各样侵入性强的检查,而是通过快速的几项,比如血常规、心跳、血压等等来执行核心的几项检查。在某些公司,冒烟测试还被称作“Sanity Test”,从字面意思也可以得知,测试的目的仅仅是保证系统“没有发疯”。除了功能上的快速冒烟覆盖,在某些系统中,性能是一个尤其重要的关注点,那么还会划分出 Soak Testing 这样的针对性能的测试来,当然,它对系统的影响可能较大,有时候不会部署在生产环境,而是在前面提到的镜像环境中。

部署工具

曾经使用过各种用于部署的工具,有开源的,也有内部开发的。这方面以前写过 Ant 脚本,在华为有内部工具;在 Amazon 也有一个内部工具,它几乎是我见过的这些个中,最强大,而且自动化程度最高的。部署工具我认为必须具备的功能包括:

  • 自动下载并同步指定版本的文件系统到环境中。这是最最基本的功能,没有这个谈不上部署工具。
  • 开发、测试、线上环境同质化。这指的是通过一定程度的抽象,无论软件部署到哪里,对程序员来说都是一样的,可以部署在开发机(本地)用于开发调试,可以部署到测试环境,也可以部署到线上环境。
  • 快速的本地覆盖和还原。这个功能非常有用。对于一个软件环境来说,可能 1000 个文件都不需要修改,但是又 3 个文件是当前我正在开发的文件,这些文件的修改需要及时同步到环境中去,以便得到快速验证。这个同步可能是本地的,也可能是远程的。比如我曾经把开发环境部署在云上,因为云机器的性能好,但是由于是远程,使用 GUI 起来并不友好,于是我采用的办法是在本地写代码,但是代码通过工具自动同步到云机器上。我也尝试过一些其他的同步场景,比如把代码 自动同步到本地虚拟机上
  • 环境差异 diff。这个功能也非常有用。开发人员很喜欢说的一句话是,“在本地没问题啊?”,因此如果这个工具可以快速比较两个环境中文件的不同,可以帮助找到环境差异,从而定位问题。

当然,还有其它很有用的功能,我这里只谈了一些印象深刻的。

监控工具

我工作过的三家公司,华为、Amazon,还是 Oracle,它们的监控工具各有特点,但做得都非常出色。且看如下的功能:

  • 多维度、分级别、可视化的数据统计和监控。核心性能的统计信息既包括应用的统计信息,包括存储,比如数据库的统计信息,还包括容器(比如 docker)或者是 host 机器本身的统计信息。监控信息的分级在数据量巨大的时候显得至关重要,信息量大而缺乏组织就是没有信息。通常,有一个主 dashboard,可以快速获知核心组件的健康信息,这个要求在一屏以内,以便可以一眼就得到。其它信息可以在不同的子 dashboard 中展开。
  • 基于监控信息的自动化操作。最常见的例子就是告警。CPU 过高了要告警、IO 过高了要告警、失败次数超过阈值要告警。使用监控工具根据这些信息可以很容易地配置合理的告警规则,要做一个完备的告警系统,规则可以非常复杂。告警和上面说的监控一样,也要分级。小问题自动创建低优先级的问题单,大问题创建高优先级的问题单,紧急问题电话、短信 page oncall。
  • 告警模块的系统化定义和重用。在上面说到这些复杂的需求的时候,如果一切都从头开始做无疑是非常耗时费力的。因而和软件代码需要组织和重构一样,告警的配置和规则也是。

对于其它的工具,比如日志工具,安全工具,审计工具,我这里不多叙述了。这并非是说它们不必须。

糟糕的实践

上面是我的理解,但是结合这些工具,我相信每个有追求的程序员,都对 Ops 的最佳实践有着自己的理解。于是,有一些实践在我看来,是非常糟糕的,它们包括:

  • SSH+命令/脚本。这大概是最糟糕的了,尤其是线上的运维,在实际操作中,一定是最好更相信工具,而不是人。如果没有工具,只能手工操作,只能使用命令+脚本来解决问题,于是各种吓人的误操作就成了催命符。你可以看看 《手滑的故事》,我相信很多人都经历过。最好的避免这样事情发生的方式是什么?限制权限?层层审批?都不是,最好的方式是自动化。人工命令和脚本的依赖程度和 Ops 的成熟度成逆相关。
  • 流程至上。这里我不是否认流程的作用,我的观点在 这篇文章 中已经说过了。其中一个最典型的操作就是堆人,发现问题了,就靠加人,增加一环审批来企图避免问题。
  • 英雄主义。这是很多公司的通病,一个写优质代码的工程师不会起眼,只有埋 bug 造灾难,再挺身而出力挽狂澜,从而拯救线上产品的“英雄”才受人景仰。正所谓,没有困难制造困难也要上。
  • 背锅侠。这和上面的英雄主义正好相反,却又相辅相成。找运维不规范操作背锅(可事实呢,考虑到复杂性、枯燥性等原因,几乎没法“规范”操作,人都是有偷懒和走捷径的本性的),找开发埋地雷,测试漏覆盖背锅。当场批评,事后追责。
  • 用户投诉驱动开发,线上事故驱动开发。这一系列通过糟糕的结果来反向推动的运维反馈开发的方式(其它各种奇葩的驱动开发方式,看 这里)。
  • 把研发的时间精力投入 ops。这是恶性循环最本质的一条, 没时间做好需求分析,没时间做好设计,没时间做好测试,没时间写好代码,什么都没时间,因为全都去 Ops 解线上问题去了 。结果呢,糟糕的上游造就了更糟糕的下游,问题频出,于是更多的人花更多的人去 ops。如此恶性循环……

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2Zv5beC

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 SwiftUI 的一些初步探索 (二)

我已经计划写一本关于 SwiftUI 和 Combine 编程的书籍,希望能通过一些实践案例帮助您快速上手 SwiftUI 及 Combine 响应式编程框架,掌握下一代客户端 UI 开发技术。现在这本书已经开始预售,预计能在 10 月左右完成。如果您对此有兴趣,可以查看 ObjC 中国的产品页面了解详情及购买。十分感谢!

上一篇继续对 SwiftUI 的教程进行一些解读。

教程 2 – Building Lists and Navigation

Section 4 – Step 2: 静态 List

var body: some View {
    List {
        LandmarkRow(landmark: landmarkData[0])
        LandmarkRow(landmark: landmarkData[1])
    }
}

这里的 ListHStack 或者 VStack 之类的容器很相似,接受一个 view builder 并采用 View DSL 的方式列举了两个 LandmarkRow。这种方式构建了对应着 UITableView 的静态 cell 的组织方式。

public init(content: () -> Content)

我们可以运行 app,并使用 Xcode 的 View Hierarchy 工具来观察 UI,结果可能会让你觉得很眼熟:

实际上在屏幕上绘制的 UpdateCoalesingTableView 是一个 UITableView 的子类,而两个 cell ListCoreCellHost 也是 UITableViewCell 的子类。对于 List 来说,SwiftUI 底层直接使用了成熟的 UITableView 的一套实现逻辑,而并非重新进行绘制。相比起来,像是 Text 或者 Image 这样的单一 ViewUIKit 层则全部统一由 DisplayList.ViewUpdater.Platform.CGDrawingView 这个 UIView 的子类进行绘制。

不过在使用 SwiftUI 时,我们首先需要做的就是跳出 UIKit 的思维方式,不应该去关心背后的绘制和实现。使用 UITableView 来表达 List 也许只是权宜之计,也许在未来也会被另外更高效的绘制方式取代。由于 SwiftUI 层只是 View 描述的数据抽象,因此和 React 的 Virtual DOM 以及 Flutter 的 Widget 一样,背后的具体绘制方式是完全解耦合,并且可以进行替换的。这为今后 SwiftUI 更进一步留出了足够的可能性。

Section 5 – Step 2: 动态 ListIdentifiable

List(landmarkData.identified(by: \.id)) { landmark in
    LandmarkRow(landmark: landmark)
}

除了静态方式以外,List 当然也可以接受动态方式的输入,这时使用的初始化方法和上面静态的情况不一样:

public struct List<Selection, Content> where Selection : SelectionManager, Content : View {
    public init<Data, RowContent>(
        _ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
        rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) 
    where 
        Content == ForEach<Data, Button<HStack<RowContent>>>, 
        Data : RandomAccessCollection, 
        RowContent : View, 
        Data.Element : Identifiable
        
    //...
}

这个初始化方法的约束比较多,我们一行行来看:

  • Content == ForEach<Data, Button<HStack<RowContent>>> 因为这个函数签名中并没有出现 ContentContent 仅只 List<Selection, Content> 的类型声明中有定义,所以在这与其说是一个约束,不如说是一个用来反向确定 List 实际类型的描述。现在让我们先将注意力放在更重要的地方,稍后会再多讲一些这个。
  • Data : RandomAccessCollection 这基本上等同于要求第一个输入参数是 Array
  • RowContent : View 对于构建每一行的 rowContent 来说,需要返回是 View 是很正常的事情。注意 rowContent 其实也是被 @ViewBuilder 标记的,因此你也可以把 LandmarkRow 的内容展开写进去。不过一般我们会更希望尽可能拆小 UI 部件,而不是把东西堆在一起。
  • Data.Element : Identifiable 要求 Data.Element (也就是数组元素的类型) 上存在一个可以辨别出某个实例的满足 Hashable 的 id。这个要求将在数据变更时快速定位到变化的数据所对应的 cell,并进行 UI 刷新。

关于 List 以及其他一些常见的基础 View,有一个比较有趣的事实。在下面的代码中,我们期望 List 的初始化方法生成的是某个类型的 View

var body: some View {
    List {
        //...
    }
}

但是你看遍 List 的文档,甚至是 Cmd + Click 到 SwiftUI 的 interface 中查找 View 相关的内容,都找不到 List : View 之类的声明。

难道是因为 SwiftUI 做了什么手脚,让本来没有满足 View 的类型都可以“充当”一个 View 吗?当然不是这样…如果你在运行时暂定 app 并用 lldb 打印一下 List 的类型信息,可以看到下面的下面的信息:

(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...

进一步,_UnaryView 的声明是:

protocol _UnaryView : View where Self.Body : _UnaryView {
}

SwiftUI 内部的一元视图 _UnaryView 协议虽然是满足 View 的,但它被隐藏起来了,而满足它的 List 虽然是 public 的,但是却可以把这个协议链的信息也作为内部信息隐藏起来。这是 Swift 内部框架的特权,第三方的开发者无法这样在在两个 public 的声明之间插入一个私有声明。

最后,SwiftUI 中当前 (Xcode 11 beta 1) 只有对应 UITableViewList,而没有 UICollectionView 对应的像是 Grid 这样的类型。现在想要实现类似效果的话,只能嵌套使用 VStackHStack。这是比较奇怪的,因为技术层面上应该和 table view 没有太多区别,大概是因为工期不太够?相信今后应该会补充上 Grid

教程 3 – Handling User Input

Section 3 – Step 2: @StateBinding

@State var showFavoritesOnly = true

var body: some View {
    NavigationView {
        List {
            Toggle(isOn: $showFavoritesOnly) {
                Text("Favorites only")
            }
    //...
            if !self.showFavoritesOnly || landmark.isFavorite {

这里出现了两个以前在 Swift 里没有的特性:@State$showFavoritesOnly

如果你 Cmd + Click 点到 State 的定义里面,可以看到它其实是一个特殊的 struct

@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible {

    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var value: Value { get nonmutating set }

    /// Returns a binding referencing the state value.
    public var binding: Binding<Value> { get }

    /// Produces the binding referencing this state value
    public var delegateValue: Binding<Value> { get }
}

@propertyWrapper 标注和上一篇中提到@_functionBuilder 类似,它修饰的 struct 可以变成一个新的修饰符并作用在其他代码上,来改变这些代码默认的行为。这里 @propertyWrapper 修饰的 State 被用做了 @State 修饰符,并用来修饰 View 中的 showFavoritesOnly 变量。

@_functionBuilder 负责按照规矩“重新构造”函数的作用不同,@propertyWrapper 的修饰符最终会作用在属性上,将属性“包裹”起来,以达到控制某个属性的读写行为的目的。如果将这部分代码“展开”,它实际上是这个样子的:

// @State var showFavoritesOnly = true
   var showFavoritesOnly = State(initialValue: true)
    
var body: some View {
    NavigationView {
        List {
//          Toggle(isOn: $showFavoritesOnly) {
            Toggle(isOn: showFavoritesOnly.binding) {
                Text("Favorites only")
            }
    //...
//          if !self.showFavoritesOnly || landmark.isFavorite {
            if !self.showFavoritesOnly.value || landmark.isFavorite {

我把变化之前的部分注释了一下,并且在后面一行写上了展开后的结果。可以看到 @State 只是声明 State struct 的一种简写方式而已。State 里对具体要如何读写属性的规则进行了定义。对于读取,非常简单,使用 showFavoritesOnly.value 就能拿到 State 中存储的实际值。而原代码中 $showFavoritesOnly 的写法也只不过是 showFavoritesOnly.binding 的简化。binding 将创建一个 showFavoritesOnly 的引用,并将它传递给 Toggle。再次强调,这个 binding 是一个引用类型,所以 Toggle 中对它的修改,会直接反应到当前 View 的 showFavoritesOnly 去设置它的 value。而 State 的 value didSet 将触发 body 的刷新,从而完成 State -> View 的绑定。

在 Xcode 11 beta 1 中,Swift 中使用的修饰符名字是 @propertyDelegate,不过在 WWDC 上 Apple 提到这个特性时把它叫做了 @propertyWrapper。根据可靠消息,在未来正式版中应该也会叫做 @propertyWrapper,所以大家在看各种资料的时候最好也建议一个简单的映射关系。

如果你想要了解更多关于 @propertyWrapper 的细节,可以看看相关的提案论坛讨论。比较有意思的细节是 Apple 在将相应的 PR merge 进了 master 以后又把这个提案的打回了“修改”的状态,而非直接接受。除了 @propertyWrapper 的名称修正以外,应该还会有一些其他的细节修改,但是已经公开的行为模式上应该不会太大变化了。

SwiftUI 中还有几个常见的 @ 开头的修饰,比如 @Binding@Environment@EnvironmentObject 等,原理上和 @State 都一样,只不过它们所对应的 struct 中定义读写方式有区别。它们共同构成了 SwiftUI 数据流的最基本的单元。对于 SwiftUI 的数据流,如果展开的话足够一整篇文章了。在这里还是十分建议看一看 Session 226 – Data Flow Through SwiftUI 的相关内容。

教程 5 – Animating Views and Transitions

Section 2 – Step 4: 两种动画的方式

在 SwiftUI 中,做动画变的十分简单。Apple 的教程里提供了两种动画的方式:

  1. 直接在 View 上使用 .animation modifier
  2. 使用 withAnimation { } 来控制某个 State,进而触发动画。

对于只需要对单个 View 做动画的时候,animation(_:) 要更方便一些,它和其他各类 modifier 并没有太大不同,返回的是一个包装了对象 View 和对应的动画类型的新的 Viewanimation(_:) 接受的参数 Animation 并不是直接定义 View 上的动画的数值内容的,它是描述的是动画所使用的时间曲线,动画的延迟等这些和 View 无关的东西。具体和 View 有关的,想要进行动画的数值方面的变更,由其他的诸如 rotationEffectscaleEffect 这样的 modifier 来描述。

在上面的 教程 5 – Section 1 – Step 5 里有这样一段代码:

Button(action: {
    self.showDetail.toggle()
}) {
    Image(systemName: "chevron.right.circle")
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil)
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring())
}

要注意,SwiftUI 的 modifier 是有顺序的。在我们调用 animation(_:) 时,SwiftUI 做的事情等效于是把之前的所有 modifier 检查一遍,然后找出所有满足 Animatable 协议的 view 上的数值变化,比如角度、位置、尺寸等,然后将这些变化打个包,创建一个事物 (Transaction) 并提交给底层渲染去做动画。在上面的代码中,.rotationEffect 后的 .animation(nil) 将 rotation 的动画提交,因为指定了 nil 所以这里没有实际的动画。在最后,.rotationEffect 已经被处理了,所以末行的 .animation(.spring()) 提交的只有 .scaleEffect

withAnimation { } 是一个顶层函数,在闭包内部,我们一般会触发某个 State 的变化,并让 View.body 进行重新计算:

Button(action: {
    withAnimation {
        self.showDetail.toggle()
    }
}) { 
  //...
}

如果需要,你也可以为它指定一个具体的 Animation

withAnimation(.basic()) {
    self.showDetail.toggle()
}

这个方法相当于把一个 animation 设置到 View 数值变化的 Transaction 上,并提交给底层渲染去做动画。从原理上来说,withAnimation 是统一控制单个的 Transaction,而针对不同 Viewanimation(_:) 调用则可能对应多个不同的 Transaction

教程 7 – Working with UI Controls

Section 4 – Step 2: 关于 View 的生命周期

ProfileEditor(profile: $draftProfile)
    .onDisappear {
        self.draftProfile = self.profile
    }

在 UIKit 开发时,我们经常会接触一些像是 viewDidLoadviewWillAppear 这样的生命周期的方法,并在里面进行一些配置。SwiftUI 里也有一部分这类生命周期的方法,比如 .onAppear.onDisappear,它们也被“统一”在了 modifier 这面大旗下。

但是相对于 UIKit 来说,SwiftUI 中能 hook 的生命周期方法比较少,而且相对要通用一些。本身在生命周期中做操作这种方式就和声明式的编程理念有些相悖,看上去就像是加上了一些命令式的 hack。我个人比较期待 ViewCombine 能再深度结合一些,把像是 self.draftProfile = self.profile 这类依赖生命周期的操作也用绑定的方式搞定。

相比于 .onAppear.onDisappear,更通用的事件响应 hook 是 .onReceive(_:perform:),它定义了一个可以响应目标 Publisher 的任意的 View,一旦订阅的 Publisher 发出新的事件时,onReceive 就将被调用。因为我们可以自行定义这些 publisher,所以它是完备的,这在把现有的 UIKit View 转换到 SwiftUI View 时会十分有用。

June 11, 2019 at 11:32AM via OneV’s Den http://bit.ly/2MMh8eb

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【喵神】 SwiftUI 的一些初步探索 (一)

总览

如果你想要入门 SwiftUI 的使用,那 Apple 这次给出的官方教程绝对给力。这个教程提供了非常详尽的步骤和说明,网页的交互也是一流,是觉得值得看和动手学习的参考。

不过,SwiftUI 中有一些值得注意的细节在教程里并没有太详细提及,也可能造成一些困惑。这篇文章以我的个人观点对教程的某些部分进行了补充说明,希望能在大家跟随教程学习 SwiftUI 的时候有点帮助。这篇文章的推荐阅读方式是,一边参照 SwiftUI 教程实际动手进行实现,一边在到达对应步骤时参照本文加深理解。在下面每段内容前我标注了对应的教程章节和链接,以供参考。

在开始学习 SwiftUI 之前,我们需要大致了解一个问题:为什么我们会需要一个新的 UI 框架。

为什么需要 SwiftUI

UIKit 面临的挑战

对于 Swift 开发者来说,昨天的 WWDC 19 首日 Keynote 和 Platforms State of the Union 上最引人注目的内容自然是 SwiftUI 的公布了。从 iOS SDK 2.0 开始,UIKit 已经伴随广大 iOS 开发者经历了接近十年的风风雨雨。UIKit 的思想继承了成熟的 AppKit 和 MVC,在初出时,为 iOS 开发者提供了良好的学习曲线。

UIKit 提供的是一套符合直觉的,基于控制流的命令式的编程方式。最主要的思想是在确保 View 或者 View Controller 生命周期以及用户交互时,相应的方法 (比如 viewDidLoad 或者某个 target-action 等) 能够被正确调用,从而构建用户界面和逻辑。不过,不管是从使用的便利性还是稳定性来说,UIKit 都面临着巨大的挑战。我个人勉强也能算是 iOS 开发的“老司机”了,但是「掉到 UIKit 的坑里」这件事,也几乎还是我每天的日常。UIKit 的基本思想要求 View Controller 承担绝大部分职责,它需要协调 model,view 以及用户交互。这带来了巨大的 side effect 以及大量的状态,如果没有妥善安置,它们将在 View Controller 中混杂在一起,同时作用于 view 或者逻辑,从而使状态管理愈发复杂,最后甚至不可维护而导致项目失败。不仅是作为开发者我们自己写的代码,UIKit 本身内部其实也经常受困于可变状态,各种奇怪的 bug 也频频出现。

声明式的界面开发方式

近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。最早的思想大概是来源于 Elm,之后这套方式被 ReactFlutter 采用,这一点上 SwiftUI 也几乎与它们一致。总结起来,这些 UI 框架都遵循以下步骤和原则:

  1. 使用各自的 DSL 来描述「UI 应该是什么样子」,而不是用一句句的代码来指导「要怎样构建 UI」。

    比如传统的 UIKit,我们会使用这样的代码来添加一个 “Hello World” 的标签,它负责“创建 label”,“设置文字”,“将其添加到 view 上”:

     func viewDidLoad() {
         super.viewDidLoad()
         let label = UILabel()
         label.text = "Hello World"
         view.addSubview(label)
         // 省略了布局的代码
     }
    

    而相对起来,使用 SwiftUI 我们只需要告诉 SDK 我们需要一个文字标签:

     var body: some View {
         Text("Hello World")
     }
    
  2. 接下来,框架内部读取这些 view 的声明,负责将它们以合适的方式绘制渲染。

    注意,这些 view 的声明只是纯数据结构的描述,而不是实际显示出来的视图,因此这些结构的创建和差分对比并不会带来太多性能损耗。相对来说,将描述性的语言进行渲染绘制的部分是最慢的,这部分工作将交由框架以黑盒的方式为我们完成。

  3. 如果 View 需要根据某个状态 (state) 进行改变,那我们将这个状态存储在变量中,并在声明 view 时使用它:

     @State var name: String = "Tom"
     var body: some View {
         Text("Hello \(name)")
     }
    

    关于代码细节可以先忽略,我们稍后会更多地解释这方面的内容。

  4. 状态发生改变时,框架重新调用声明部分的代码,计算出新的 view 声明,并和原来的 view 进行差分,之后框架负责对变更的部分进行高效的重新绘制。

SwiftUI 的思想也完全一样,而且实际处理也不外乎这几个步骤。使用描述方式开发,大幅减少了在 app 开发者层面上出现问题的机率。

一些细节解读

官方教程中对声明式 UI 的编程思想有深刻的体现。另外,SwiftUI 中也采用了非常多 Swift 5.1 的新特性,会让习惯了 Swift 4 或者 5 的开发者“耳目一新”。接下来,我会分几个话题,对官方教程的一些地方进行解释和探索。

教程 1 – Creating and Combining Views

Section 1 – Step 3: SwiftUI app 的启动

创建 app 之后第一件好奇的事情是,SwiftUI app 是怎么启动的。

教程示例 app 在 AppDelegate 中通过 application(_:configurationForConnecting:options) 返回了一个名为 “Default Configuration” 的 UISceneConfiguration 实例:

func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions) -> UISceneConfiguration
{
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

这个名字的 Configuration 在 Info.plist 的 “UIApplicationSceneManifest -> UISceneConfigurations” 中进行了定义,指定了 Scene Session Delegate 类为 $(PRODUCT_MODULE_NAME).SceneDelegate。这部分内容是 iOS 13 中新加入的通过 Scene 管理 app 生命周期的方式,以及多窗口支持部分所需要的代码。这部分不是我们今天的话题。在 app 完成启动后,控制权被交接给 SceneDelegate,它的 scene(_:willConnectTo:options:) 将会被调用,进行 UI 的配置:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions)
    {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: ContentView())
        self.window = window
        window.makeKeyAndVisible()
    }

这部分内容就是标准的 iOS app 启动流程了。UIHostingController 是一个 UIViewController 子类,它将负责接受一个 SwiftUI 的 View 描述并将其用 UIKit 进行渲染 (在 iOS 下的情况)。UIHostingController 就是一个普通的 UIViewController,因此完全可以做到将 SwiftUI 创建的界面一点点集成到已有的 UIKit app 中,而并不需要从头开始就是基于 SwiftUI 的构建。

由于 Swift ABI 已经稳定,SwiftUI 是一个搭载在用户 iOS 系统上的 Swift 框架。因此它的最低支持的版本是 iOS 13,可能想要在实际项目中使用,还需要等待一两年时间。

Section 1 – Step 4: 关于 some View

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

一眼看上去可能会对 some 比较陌生,为了将明白这件事,我们先从 View 说起。

View 是 SwiftUI 的一个最核心的协议,代表了一个屏幕上元素的描述。这个协议中含有一个 associatedtype:

public protocol View : _View {
    associatedtype Body : View
    var body: Self.Body { get }
}

这种带有 associatedtype 的协议不能作为类型来使用,而只能作为类型约束使用:

// Error
func createView() -> View {

}

// OK
func createView<T: View>() -> T {
    
}

这样一来,其实我们是不能写类似这种代码的:

// Error,含有 associatedtype 的 protocol View 只能作为类型约束使用
struct ContentView: View {
    var body: View {
        Text("Hello World")
    }
}

想要 Swift 帮助自动推断出 View.Body 的类型的话,我们需要明确地指出 body 的真正的类型。在这里,body 的实际类型是 Text

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

当然我们可以明确指定出 body 的类型,但是这带来一些麻烦:

  1. 每次修改 body 的返回时我们都需要手动去更改相应的类型。
  2. 新建一个 View 的时候,我们都需要去考虑会是什么类型。
  3. 其实我们只关心返回的是不是一个 View,而对实际上它是什么类型并不感兴趣。

some View 这种写法使用了 Swift 5.1 的 Opaque return types 特性。它向编译器作出保证,每次 body 得到的一定是某一个确定的,遵守 View 协议的类型,但是请编译器“网开一面”,不要再细究具体的类型。返回类型确定单一这个条件十分重要,比如,下面的代码也是无法通过的:


let someCondition: Bool

// Error: Function declares an opaque return type, 
// but the return statements in its body do not have 
// matching underlying types.
var body: some View {
    if someCondition {
        // 这个分支返回 Text
        return Text("Hello World")
    } else {
        // 这个分支返回 Button,和 if 分支的类型不统一
        return Button(action: {}) {
            Text("Tap me")
        }
    }
}

这是一个编译期间的特性,在保证 associatedtype protocol 的功能的前提下,使用 some 可以抹消具体的类型。这个特性用在 SwiftUI 上简化了书写难度,让不同 View 声明的语法上更加统一。

Section 2 – Step 1: 预览 SwiftUI

SwiftUI 的 Preview 是 Apple 用来对标 RN 或者 Flutter 的 Hot Reloading 的开发工具。由于 IBDesignable 的性能上的惨痛教训,而且得益于 SwiftUI 经由 UIKit 的跨 Apple 平台的特性,Apple 这次选择了直接在 macOS 上进行渲染。因此,你需要使用搭载有 SwiftUI.framework 的 macOS 10.15 才能够看到 Xcode Previews 界面。

Xcode 将对代码进行静态分析 (得益于 SwiftSyntax 框架),找到所有遵守 PreviewProvider 协议的类型进行预览渲染。另外,你可以为这些预览提供合适的数据,这甚至可以让整个界面开发流程不需要实际运行 app 就能进行。

笔者自己尝试下来,这套开发方式带来的效率提升相比 Hot Reloading 要更大。Hot Reloading 需要你有一个大致界面和准备相应数据,然后运行 app,停在要开发的界面,再进行调整。如果数据状态发生变化,你还需要 restart app 才能反应。SwiftUI 的 Preview 相比起来,不需要运行 app 并且可以提供任何的 dummy 数据,在开发效率上更胜一筹。

经过短短一天的使用,Option + Command + P 这个刷新 preview 的快捷键已经深入到我的肌肉记忆中了。

Section 3 – Step 5: 关于 ViewBuilder

创建 Stack 的语法很有趣:

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
}

一开始看起来好像我们给出了两个 Text,似乎是构成的是一个类似数组形式的 [View],但实际上并不是这么一回事。这里调用了 VStack 类型的初始化方法:

public struct VStack<Content> where Content : View {
    init(
        alignment: HorizontalAlignment = .center, 
        spacing: Length? = nil, 
        content: () -> Content)
}

前面的 alignmentspacing 没啥好说,最后一个 content 比较有意思。看签名的话,它是一个 () -> Content 类型,但是我们在创建这个 VStack 时所提供的代码只是简单列举了两个 Text,而并没有实际返回一个可用的 Content

这里使用了 Swift 5.1 的另一个新特性:Funtion builders。如果你实际观察 VStack这个初始化方法的签名,会发现 content 前面其实有一个 @ViewBuilder 标记:

init(
    alignment: HorizontalAlignment = .center, 
    spacing: Length? = nil, 
    @ViewBuilder content: () -> Content)

ViewBuilder 则是一个由 @_functionBuilder 进行标记的 struct:

@_functionBuilder public struct ViewBuilder { /* */ }

使用 @_functionBuilder 进行标记的类型 (这里的 ViewBuilder),可以被用来对其他内容进行标记 (这里用 @ViewBuildercontent 进行标记)。被用 function builder 标记过的 ViewBuilder 标记以后,content 这个输入的 function 在被使用前,会按照 ViewBuilder 中合适的 buildBlock 进行 build 后再使用。如果你阅读 ViewBuilder文档,会发现有很多接受不同个数参数的 buildBlock 方法,它们将负责把闭包中一一列举的 Text 和其他可能的 View 转换为一个 TupleView,并返回。由此,content 的签名 () -> Content 可以得到满足。

实际上构建这个 VStack 的代码会被转换为类似下面这样:

// 等效伪代码,不能实际编译。
VStack(alignment: .leading) { viewBuilder -> Content in
    let text1 = Text("Turtle Rock").font(.title)
    let text2 = Text("Joshua Tree National Park").font(.subheadline)
    return viewBuilder.buildBlock(text1, text2)
}

当然这种基于 funtion builder 的方式是有一定限制的。比如 ViewBuilder 就只实现了最多十个参数buildBlock,因此如果你在一个 VStack 中放超过十个 View 的话,编译器就会不太高兴。不过对于正常的 UI 构建,十个参数应该足够了。如果还不行的话,你也可以考虑直接使用 TupleView 来用多元组的方式合并 View

TupleView<(Text, Text)>(
    (Text("Hello"), Text("Hello"))
)

除了按顺序接受和构建 ViewbuildBlock 以外,ViewBuilder 还实现了两个特殊的方法:buildEitherbuildIf。它们分别对应 block 中的 if...else 的语法和 if 的语法。也就是说,你可以在 VStack 里写这样的代码:

var someCondition: Bool

VStack(alignment: .leading) {
    Text("Turtle Rock")
        .font(.title)
    Text("Joshua Tree National Park")
        .font(.subheadline)
    if someCondition {
        Text("Condition")
    } else {
        Text("Not Condition")
    }
}

其他的命令式的代码在 VStackcontent 闭包里是不被接受的,下面这样也不行:

VStack(alignment: .leading) {
    // let 语句无法通过 function builder 创建合适的输出
    let someCondition = model.condition
    if someCondition {
        Text("Condition")
    } else {
        Text("Not Condition")
    }
}

到目前为止,只有以下三种写法能被接受 (有可能随着 SwiftUI 的发展出现别的可接受写法):

  • 结果为 View 的语句
  • if 语句
  • if...else... 语句

Section 4 – Step 7: 链式调用修改 View 的属性

教程到这一步的话,相信大家已经对 SwiftUI 的超强表达能力有所感悟了。

var body: some View {
    Image("turtlerock")
        .clipShape(Circle())
        .overlay(
            Circle().stroke(Color.white, lineWidth: 4))
        .shadow(radius: 10)
}

可以试想一下,在 UIKit 中要动手撸一个这个效果的困难程度。我大概可以保证,99% 的开发者很难在不借助文档或者 copy paste 的前提下完成这些事情,但是在 SwiftUI 中简直信手拈来。在创建 View 之后,用链式调用的方式,可以将 View 转换为一个含有变更后内容的对象。这么说比较抽象,我们可以来看一个具体的例子。比如简化一下上面的代码:

let image: Image = Image("turtlerock")
let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)

image 通过一个 .shadow 的 modifier,modified 变量的类型将转变为 _ModifiedContent<Image, _ShadowEffect>。如果你查看 View 上的 shadow 的定义,它是这样的:

extension View {
    func shadow(
        color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), 
        radius: Length, x: Length = 0, y: Length = 0) 
    -> Self.Modified<_ShadowEffect>
}

ModifiedView 上的一个 typealias,在 struct Image: View 的实现里,我们有:

public typealias Modified<T> = _ModifiedContent<Self, T>

_ModifiedContent 是一个 SwiftUI 的私有类型,它存储了待变更的内容,以及用来实施变更的 Modifier

struct _ModifiedContent<Content, Modifier> {
    var content: Content
    var modifier: Modifier
}

Content 遵守 ViewModifier 遵守 ViewModifier 的情况下,_ModifiedContent 也将遵守 View,这是我们能够通过 View 的各个 modifier extension 进行链式调用的基础:

extension _ModifiedContent : _View 
    where Content : View, Modifier : ViewModifier 
{
}

shadow 的例子中,SwiftUI 内部会使用 _ShadowEffect 这个 ViewModifier,并把 image 自身和 _ShadowEffect 实例存放到 _ModifiedContent 里。不论是 image 还是 modifier,都只是对未来实际视图的描述,而不是直接对渲染进行的操作。在最终渲染前,ViewModifierbody(content: Self.Content) -> Self.Body 将被调用,以给出最终渲染层所需要的各个属性。

更具体来说,_ShadowEffect 是一个满足 EnvironmentalModifier 协议的类型,这个协议要求在使用前根据使用环境将自身解析为具体的 modifier。

其他的几个修改 View 属性的链式调用与 shadow 的原理几乎一致。

小结

上面是对 SwiftUI 教程的第一部分进行的一些说明,在之后的一篇文章里,我会对剩余的几个教程中有意思的部分再做些解释。

虽然公开还只有一天,但是 SwiftUI 已经经常被用来和 Flutter 等框架进行比较。试用下来,在 view 的描述表现力上和与 app 的结合方面,SwiftUI 要胜过 Flutter 和 Dart 的组合很多。Swift 虽然开源了,但是 Apple 对它的掌控并没有减弱。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,我们已经在本文中看到了一些例子,比如 Opaque return types 和 Function builder 等。在接下来对后面几个教程的解读中,我们还会看到更多这方面的内容。

另外,Apple 在背后使用 Combine.framework 这个响应式编程框架来对 SwiftUI.framework 进行驱动和数据绑定,相比于现有的 RxSwift/RxCocoa 或者是 ReactiveSwift 的方案来说,得到了语言和编译器层级的大力支持。如果有机会,我想我也会对这方面的内容进行一些探索和介绍。

June 04, 2019 at 02:32PM via OneV’s Den http://bit.ly/2X1KAAI

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 HTTPS 升级

昨晚花了几个钟头,把 blog 的 HTTP 升级成 HTTPS 了,虽然这件事做的晚了一点。为什么要升级,不是我说明的重点,想了解的朋友可以阅读 这篇文章 。我记录的是我升级的过程,踩到的坑。

备份

首先,文章中有许多以 http://www.raychase.net 开头的 URL,比如某些图片和链接,可以把它们改成 https 的,也可以全部改成相对路径,这样的适用性更广。

UPDATE xx_posts SET post_content = REPLACE(post-content, 'http://www.raychase.net', '/');

到浏览器里面访问看看,似乎没有什么问题。

安装

WordPress 管理台

在 Wordress 管理台的设置里面,把本站 URL 中的 http 替换成 https。

证书申请安装

certbot 申请证书,选好了代理和操作系统一个,照着 guide 操作。

wget https://dl.eff.org/certbot-auto
sudo mv certbot-auto /usr/local/bin/certbot-auto
sudo chown root /usr/local/bin/certbot-auto
sudo chmod 0755 /usr/local/bin/certbot-auto

接着自动安装,中途退出了:

sudo /usr/local/bin/certbot-auto --nginx
...
To use Certbot, packages from the EPEL repository need to be installed.
Enable the EPEL repository and try running Certbot again.

EPEL

既然是没有 EPEL,那就安装一个:

sudo yum install epel-release
Loaded plugins: fastestmirror
Setting up Install Process
Loading mirror speeds from cached hostfile
Error: Cannot retrieve metalink for repository: epel. Please verify its path and try again

失败了,从错误中看是 mirror 地址上没法访问到,于是研究了一下,修改 /etc/yum.repos.d/epel.repo 和 /etc/yum.repos.d/epel-testing.repo ,注释掉全部以 mirrorlist= 开始的行,再还原全部以 baseurl= 开始的行。这样就去原始地址,而不是镜像地址下载了。

果然就安装成功了。

Nginx 配置

于是继续刚才失败的操作:

sudo /usr/local/bin/certbot-auto --nginx

挂在了真正安装配置的过程中:

Error while running nginx -c /etc/nginx/nginx.conf -t.
nginx: [emerg] open() "/etc/nginx/nginx.conf" failed (2: No such file or directory)
nginx: configuration file /etc/nginx/nginx.conf test failed

原来是找不到 nginx 的配置文件,因为 nginx 安装的路径并非/etc 下面。于是就干脆建立一个软链接:

ln -s /usr/local/nginx/conf /etc/nginx

这一步过了,结果又挂在了证书 challenge 的过程(challenge 是域名验证的一环,在 这里 有介绍):

Performing the following challenges:
http-01 challenge for www.raychase.net
Waiting for verification...
Challenge failed for domain www.raychase.net

研究一下发现,它需要占用 80 端口,因为这个 challenge 的路径是 http://www.raychase.net/.well-known/acme-challenge/xxxxx,而网站应用已经占了 80,于是把 lnmp 先停掉,再操作。

终于提示成功了,注意到它在 nginx 的配置文件目录下的子目录 vhost 下面生成了一个增量配置文件:www.raychase.net.conf,里面配置了一些 ssl_cewrtificate 之类的 key 的路径,把它放到 nginx 的配置目录里面。

验证

命令行验证

尝试了一下 curl http://bit.ly/2YN6zZB 可以访问,于是就在 SSL Labs 可以验证证书的情况,结果提示失败。

接着在外网使用上述的 curl HTTPS 命令,但是却 timeout,可是 curl http://www.raychase.net,得到了正确的 301 重定向的响应。

首先怀疑 nginx,仔细研究了 nginx 的配置,没发现有什么问题。接着就怀疑防火墙,查看 /etc/sysconfig/iptables 发现可能是防火墙的问题。于是命令行添加规则:

iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT

果然,可以访问了。

再 curl http://www.raychase.net,得到了 301 重定向的响应。

浏览器验证

接着再到浏览器里面访问验证,大致没问题,可是,我注意到访问网站首页的时候,URL 左侧的小图标不是一般 HTTPS 的“锁”的图标,而是一个圈“i”的图标,点击以后可以看到“Your connection to this site is not fully secure”这样的文字。

原来是因为网页上包含了“mixed content”,即有指向当前站的 http 的链接。查看源代码,原来在网站中还有一些其它配置包含有当前站的 http 链接,逐一修复,果然,小锁图标出来了,而文字变成了“Connection is secure”。

改进

HTTP2

在 nginx 中配置打开 http2:

listen 443 ssl http2;

可是在修改完毕,重新加载 nginx 的时候,它提示 http2 不认识,于是就检查了,发现是 nginx 版本太老的原因。

于是使用 yum 来升级,之后提示已经是最新版本了,还是不支持。

原来 yum 的默认 repo 版本还是太老,必须要使用 nginx 自己的 repo。于是增加文件:

/etc/yum.repos.d/nginx.repo

写入:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=0
enabled=1

再重启 nginx 就好了。

证书 renew 自动化

证书每几个月就会过期,因此必须自动续上,而不是手动登上服务器来操作。我尝试了一下 renew 证书的过程:

/usr/local/bin/certbot-auto renew

发现又出现了前面 challenge 的过程。当时我停掉了 lnmp,可是我总不能为了续个证书停掉网站吧?于是考虑解决 challenge 的端口冲突问题。发现其实可以用 HTTP 的 80 端口,但是 challenge 的 URI 冲突才是问题的核心。这样问题就好解决了,把导致冲突的 URI 放进来就好了,在 nginx 里面配置:

location /.well-known/acme-challenge/.* {
    proxy_pass http://127.0.0.1:80;
}

接着配置 cron task,使得这个过程自动化:

crontab -e

加入:

0 0 25 * * /usr/local/bin/certbot-auto renew
10 0 25 * * /sbin/service nginx restart

昨天是 25 号,因此我设置成每天的 25 号来尝试 renew。如果证书还远没到期,它会提示“Cert not yet due for renewal”并会退出这个过程,因此这个命令可以安全地运行。

可是我并不知道运行结果啊,总不能每次都登陆上来,跑到/var/log/cron 去看日志吧?

其中一个解决办法是发 email,把运行输出发 email 给我。

Email 通知结果

于是先要配置 ssmtp,编辑/etc/ssmtp/ssmtp.conf:

root=xxx@gmail.com
mailhub=smtp.gmail.com:587
RewriteDomain=gmail.com
Hostname=xxx
FromLineOverride=YES
UseTLS=Yes
UseSTARTTLS=Yes
TLS_CA_File=/etc/pki/tls/certs/ca-bundle.crt
AuthUser=xxx@gmail.com
AuthPass=yyy
AuthMethod=LOGIN

接着把地址信息添加到/etc/ssmtp/revaliases。

我使用的是 Gmail 的 SMTP,为了让 gmail 允许这样的操作,还必须在账户选项的 security 里面,开启 less secure app access 的设置。

搞定以后尝试发一封邮件试试:

echo -n 'test' | sendmail -v raychase1986@gmail.com

没问题!那就可以配置 cron task 让它发邮件了,加上:

MAILTO=RayChase1986@gmail.com

这个变量加上以后,应该就可以自动发邮件了。

crontab 没有立即执行,从而验证的功能,于是为了立即验证,加了一行(每分钟都会执行一次):

* * * * * echo "test"

验证以后把这行删掉就好了。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2HBt6CJ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 不要让业务牵着鼻子走

这篇文章算是要和之前写的 《程序员懂业务有多重要?》“唱反调”了。

从工作开始,我就不断被灌输着一种业务至上的观点,无论在中国的公司,还是美国的公司,衡量一个决定或者一个需求的价值,都是在业务上有多大的帮助,都说 business impact 是什么。我从不怀疑单纯这样做的初衷,但是我质疑单纯这样做的结果。我觉得,即便是一个业务驱动为主的团队,在决策的时候,技术的占比,应当占据显著的地位。因而我说,不要被业务牵着鼻子走。继续把这一点发扬光大,我认为它对团队发展,对个人发展,都是如此。

曾经认为,这样的观点应该是公认的,但是我越来越发现,事实并不是这样。应该说几乎所有程序员都看到了业务上面的价值,都明白所谓“用技术创造业务价值”的意义,但是其中却有 很多人忘掉了技术的根基,技术是程序员吃饭和安家立命之本,它不只是在实施过程中的一门工具和一种途径,它本身就有着决定项目和产品走向的能力。

自己的经历

我把做的项目,或者写的代码,非常粗略地,可以分为两类:

  • 一种是基于现有代码设施,来写基于业务逻辑的实现,包括新的接口、新的条件分支等等,这种代码被称为“业务代码”;
  • 另一种则是拓展基础设施,实现新的可复用特性,本身不混入业务概念,不直接“变现”,这种代码被称为“非业务代码”。

看起来前者更偏向于业务,而后者更偏向于技术,但是话说回来,在许多场景中,这二者是很难彻底分离开的。有时,要用非业务代码实现某种可被不同业务重用的基础特性,却是由业务驱动的。应该说,硬要分类的话,通常我们遇到的大多数情况,都属于前者。

我在业务和技术的团队中都呆过。在 2008 年的时候,我首先加入的是华为的彩铃团队,写的是彩铃服务端的业务代码,扩展 SOAP 接口和存储过程算是业务为主的代码,但是重新实现的定时任务功能算是用实现业务无关的基础设施。后来在连续几年的和视频平台有关的项目中,做过一个比较小的 service,也做过一个比较大的 portal,专职做过性能优化,由于基本都是重头来做,因此业务和非业务代码都必须涉及,而考虑到我所在的基线团队,以为定制团队提供基础设施为主,因而非业务代码写得更多一点。

在 Amazon 呆的时间比较久,做的东西也比较宽泛,做过 data visualization,big data 的采集和处理,分布式计算,以及分布式的 workflow,还维护过一个 service(更多内容我 在这里 写过)。这些代码一半属于技术代码,而业务的部分主要由团队中的 scientists 和 data analysts 们精通并持续研究,工程师在多数情况下只负责提供好用的工具。(其实这一点分工很难做好,Amazon 是做得相对不错的, 介绍过分工 ,也 吐槽过工具

在 Oracle,所在的大 team 下我也算参与过三个小团队了,一个前端的团队,一个后端 service 的团队,再到现在稳定在这个 distributed workflow 的团队。前两者都属于业务代码远大过非业务代码的团队。事实上,在 OCI(Oracle Cloud Infrastructure)多数团队都是如此,几乎都是业务驱动,所谓的纯技术团队(比如做出 lib 或者 service 这样的基础设施来给其它业务团队使用)比较少。很荣幸的是,目前我在的团队就是如此,我们维护一个 distributed workflow 的 software stack,也维护基于它最主要的 service。

总体来看,最近几年和以前比,所在的团队和参与的项目要更加偏重技术一点,日常工作也相对更有趣一点。

项目的角度

业务驱动可以让你赚钱,但是技术支撑不足的业务驱动,也足以毁掉这个项目,甚至它所属的产品。专注于业务和技术的人眼光是很不同的。 专注于业务的人可以让项目的工作围绕着核心价值和“挣钱”这样直接的目标而工作,但是专注于技术的人才可以让项目踏实并具备长远的可持续发展性。

业务驱动可以让用户得到引导,需求得到满足,但是技术驱动才可以让产品具备稳定性和扩展性这些软件工程层面的要素,从而真正发展起来,而不是三天两头地救火。这也是一个项目,最好具备 TPM 和 Dev 这样两种不同角色的其中一个原因。

在被质问道 business impact 的时候,程序员们一定在心里对技术上的优劣有杆公平的秤。毕竟,有那么多人从业务的角度帮你思考价值的问题,却只有你们自己能从技术的角度考虑和评估。下次再有人质疑有什么业务影响的时候,程序员也可以说,无论 xxx 有没有业务影响,它有着 yyy 的技术影响。

团队的角度

我记得以前说过, 技术并不一定只体现在产品本身,时髦的、有趣的的技术带来的影响力,还能帮助吸引人才。这是技术对团队的影响之一,也恰恰是很多人忽视的一点。 听起来很功利是不是?但是现实就是如此,我记得当时在北京 office 的时候,我们给新员工做宣传说,来我们组吧,我们做大数据,我们做分布式计算,我们做机器学习。我看到有的人眼睛就亮了。事实上,对于这些新员工来说,他们可能并不了解,这些事情并不好做,很多情况下都是苦差事,并且,业务影响可能还不如老老实实做传统业务的团队。

但,那又如何?我知道很多程序员的想法是,不只是完成工作,还想要学到东西,并且向学到那些技术前沿的东西。你不能责怪他们不能立足根本,不能责怪他们好高骛远,这是一个技术人自然而然的追求。最起码,我能看到对方敢于打破舒适区的勇气。

反之,如果一个程序员说,我做什么都可以,我不在乎我加入的团队使用什么技术,做什么不是挣钱吃饭?我反而会觉得这样百适的风格可能意味着没有方向上的品鉴和偏好,没有技术上的追求,也没有学习的动力。我对这样的程序员加入团队,反而感到担忧。

我希望看到团队在技术上的追求和氛围,考虑扩展性、复用性和稳定性等各个方面,争论各种工程层面的解决方案,而不是用业务价值衡量一切。

个人的角度

技术上,需要精进,但是在技术上的投资,总体来说是不容易跑偏的。技术需要持续积累的过程。如果你发现自己每天写的代码无非就是加个接口,写一堆一堆的 if-else 面条式的代码,天天折腾在复杂的业务逻辑里面,而没有什么设计,没有什么技术问题的分析,那我觉得你大概应该需要寻求改变了。如果能在自己的组里面找适合自己的项目可以,如果不行,去寻求外部的团队,甚至其他公司,也是一个选择。

要明确的是,这些业务上的知识,只有少数能够积累下来,积累下来的部分,叫做领域知识。而绝大多数,都没有长期的价值,换言之,到了一个新环境,只有技术的东西是自己的,其他的多数都用不着了。这里不是说业务知识没有用,而是说,多数业务知识,对个人的发展,并不能提供持续的价值。有少部分,或者在确定了领域的基础上,当然是有用的。比如我有长期做 billing 的同事,就有着相当的业务积累,但是,这样的人才也会面临跳槽选择面过窄的问题。

有人说,技术上也一样啊,比如几年前的技术,到现在就淘汰了。MFC 淘汰了,CGI 淘汰了,WAP 淘汰了,我们现在用的技术,很快也要淘汰了,而体力跟不上年轻人,学习速度跟不上刚毕业的年轻人,是不是程序员也要被淘汰了?

我认为,技术是个复杂的事情,不是几个知识点,学会了就搞定一切了。这里有积累,并且技术在变化的过程中,绝大多数内容都是相通的,如果一个老程序员,学习某一项相关新技术的时间,或者掌握的深度,和新程序员一样,那么我会怀疑这个老程序员是不是靠谱。

技术上的积累,并不只是在解决实际问题的时候带来有价值的思路,更能够在学习新技术的时候,可以类比。 在和更年轻的程序员竞争的过程中,老程序员的优势,应当是经验、眼界,而不是体力、反应。这也是为什么不要只写那些纯业务代码的原因 ,同样是写码拿钱,还是很不一样的,写纯业务代码,技术上积累的经验和眼界会偏少,并且照猫画虎实现就可以了,容易废弃掉了人思考的习惯,并且,招个年轻人就可以替代掉你。

如果程序员的你发现自己一直在忙于填充业务代码,不做设计,不做架构,如果自己还不对技术世界那么敏感的话,那么要小心了。如果这都不算“搬砖”,那还有什么是“搬砖”?这样长久的工作,会把自己对于技术的热情慢慢消磨掉。

我们都知道要懂得业务变现的重要性,不要变成迂腐的 nerd,就仿佛那“回字有三种写法”一般,而且似乎这个社会都在这样思考;可是我却说,我们更要看到,只有技术,才是程序员继续充满价值存在的最重要的理由。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2X3pr6d

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 LeetCode 题目解答—— 第 416 到 460 题

从第 416 到第 460 题,跳过了需要付费的题目。付费的题目会单独放在一篇里面。

416 Partition Equal Subset Sum 40.0% Medium
417 Pacific Atlantic Water Flow 36.9% Medium
419 Battleships in a Board 65.2% Medium
420 Strong Password Checker 17.9% Hard
421 Maximum XOR of Two Numbers in an Array 50.5% Medium
423 Reconstruct Original Digits from English 45.4% Medium
424 Longest Repeating Character Replacement 43.8% Medium
427 Construct Quad Tree 55.0% Easy
429 N-ary Tree Level Order Traversal 58.5% Easy
430 Flatten a Multilevel Doubly Linked List 40.9% Medium
432 All O`one Data Structure 29.0% Hard
433 Minimum Genetic Mutation 37.5% Medium
434 Number of Segments in a String 36.7% Easy
435 Non-overlapping Intervals 41.4% Medium
436 Find Right Interval 42.4% Medium
437 Path Sum III 42.1% Easy
438 Find All Anagrams in a String 36.7% Easy
440 K-th Smallest in Lexicographical Order 26.3% Hard
441 Arranging Coins 37.6% Easy
442 Find All Duplicates in an Array 60.1% Medium
443 String Compression 37.0% Easy
445 Add Two Numbers II 49.5% Medium
446 Arithmetic Slices II – Subsequence 29.9% Hard
447 Number of Boomerangs 49.4% Easy
448 Find All Numbers Disappeared in an Array 52.9% Easy
449 Serialize and Deserialize BST 46.1% Medium
450 Delete Node in a BST 39.4% Medium
451 Sort Characters By Frequency 55.8% Medium
452 Minimum Number of Arrows to Burst Balloons 46.2% Medium
453 Minimum Moves to Equal Array Elements 49.1% Easy
454 4Sum II 50.4% Medium
455 Assign Cookies 48.3% Easy
456 132 Pattern 27.4% Medium
457 Circular Array Loop 27.5% Medium
458 Poor Pigs 45.3% Hard
459 Repeated Substring Pattern 39.7% Easy
460 LFU Cache 28.6% Hard

Partition Equal Subset Sum
【题目】Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:

  1. Each of the array element will not exceed 100.
  2. The array size will not exceed 200.

Example 1:

Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: [1, 2, 3, 5]
Output: false
Explanation: The array cannot be partitioned into equal sum subsets.

【解答】要划分数组成两个部分,这两个部分各自的和相等。
其实这道题可以问 k 个部分,两个部分是一个强化了的条件。整个数组的和(sum)是可以很容易得到的,那么分成两个部分,每个部分的和(sum/2)也就可以很容易得到了。于是这道题就变成了,能不能从数组中找出一些数,使之和为 sum/2?搜索求解即可。

求解期间做一点小优化,创建一个<index, <target, partitionable>> 这样的结构,表示源数组的下标 index 开始往后到底的这一段,能否存在一个子数组,使得和为 target。

class Solution {
    public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0)
            throw new IllegalArgumentException();
        
        int total = 0;
        for (int num : nums) {
            total += num;
        }
        
        if (total % 2 == 1)
            return false;
        
        int target = total / 2;
        // <index, <target, partitionable>>
        Map<Integer, Map<Integer, Boolean>> cache = new HashMap<>();
        
        return canPartition(nums, cache, target, 0);
    }
    
    private boolean canPartition(int[] nums, Map<Integer, Map<Integer, Boolean>> cache, int target, int index) {
        if (index == nums.length)
            return target == 0;
        
        if (!cache.containsKey(index)) {
            cache.put(index, new HashMap<>());
        }
        Map<Integer, Boolean> subMap = cache.get(index);
        if (subMap.containsKey(target))
            return subMap.get(target);
        
        // current is not selected
        boolean partitionable = canPartition(nums, cache, target, index + 1);
        subMap.put(target, partitionable);
        if (partitionable) {
            subMap.put(target, true);
            return true;
        }
        
        // current is selected
        if (nums[index] <= target) {
            partitionable = canPartition(nums, cache, target - nums[index], index + 1);
        }

        return partitionable;
    }
}

Pacific Atlantic Water Flow
【题目】Given an m x n matrix of non-negative integers representing the height of each unit cell in a continent, the “Pacific ocean” touches the left and top edges of the matrix and the “Atlantic ocean” touches the right and bottom edges.

Water can only flow in four directions (up, down, left, or right) from a cell to another one with height equal or lower.

Find the list of grid coordinates where water can flow to both the Pacific and Atlantic ocean.

Note:

  1. The order of returned grid coordinates does not matter.
  2. Both m and n are less than 150.

Example:

Given the following 5x5 matrix:

  Pacific ~   ~   ~   ~   ~ 
       ~  1   2   2   3  (5) *
       ~  3   2   3  (4) (4) *
       ~  2   4  (5)  3   1  *
       ~ (6) (7)  1   4   5  *
       ~ (5)  1   1   2   4  *
          *   *   *   *   * Atlantic

Return:

[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (positions with parentheses in above matrix).

【解答】太平洋在左上方,大西洋在右下方,要寻找二者的分界线。

太平洋从最左侧和最上方开始,认为数值不断变大就是可达的,寻找并标记所有可达区域。大西洋则是从最右侧和最下方开始,类似的标记。标记完毕以后找到二者重叠的部分,就是分界线。

class Solution {
    public List<int[]> pacificAtlantic(int[][] matrix) {
        if (matrix == null)
            throw new IllegalArgumentException();
        
        List<int[]> result = new ArrayList<>();
        if (matrix.length==0 || matrix[0].length==0)
            return result;
        
        // null: undetermined, true: reachable, false: unreachable
        Boolean[][] pacificReachable = new Boolean[matrix.length][matrix[0].length];
        Queue pacificQueue = new LinkedList<>();
        // top row
        for (int j=0; j<matrix[0].length; j++) {
            pacificReachable[0][j] = true;
            pacificQueue.offer(new Point(0, j));
        }
        // left column
        for (int i=0; i<matrix.length; i++) {
            pacificReachable[i][0] = true;
            pacificQueue.offer(new Point(i, 0));
        }
        // find all the nodes pacific water flow can reach
        iterate(matrix, pacificQueue, pacificReachable);
        
        Boolean[][] atlanticReachable = new Boolean[matrix.length][matrix[0].length];
        Queue atlanticQueue = new LinkedList<>();
        // bottom row
        for (int j=0; j<matrix[0].length; j++) {
            atlanticReachable[matrix.length-1][j] = true;
            atlanticQueue.offer(new Point(matrix.length-1, j));
        }
        // right column
        for (int i=0; i<matrix.length; i++) {
            atlanticReachable[i][matrix[0].length-1] = true;
            atlanticQueue.offer(new Point(i, matrix[0].length-1));
        }
        iterate(matrix, atlanticQueue, atlanticReachable);
        
        for (int i=0; i<matrix.length; i++) {
            for (int j=0; j<matrix[0].length; j++) {
                if (pacificReachable[i][j] == Boolean.TRUE && atlanticReachable[i][j] == Boolean.TRUE) {
                    result.add(new int[] {i, j});
                }
            }
        }
        
        return result;
    }
    
    private void iterate(int[][] matrix, Queue queue, Boolean[][] reachable) {
        while (!queue.isEmpty()) {
            Point p = queue.poll();
            
            if (this.mark(matrix, p.x, p.y, p.x-1, p.y, reachable))
                queue.add(new Point(p.x-1, p.y));
            if (this.mark(matrix, p.x, p.y, p.x, p.y-1, reachable))
                queue.add(new Point(p.x, p.y-1));
            if (this.mark(matrix, p.x, p.y, p.x+1, p.y, reachable))
                queue.add(new Point(p.x+1, p.y));
            if (this.mark(matrix, p.x, p.y, p.x, p.y+1, reachable))
                queue.add(new Point(p.x, p.y+1));
        }
    }
    
    private boolean mark(int[][] matrix, int x, int y, int nx, int ny, Boolean[][] reachable) {
        if (nx<0 || ny<0 || nx>=matrix.length || ny>=matrix[0].length || reachable[nx][ny] != null)
            return false;
        
        if (matrix[x][y] <= matrix[nx][ny]) {
            reachable[nx][ny] = true;
            return true;
        }
        
        return false;
    }
}

class Point {
    public int x;
    public int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Battleships in a Board
【题目】Given an 2D board, count how many battleships are in it. The battleships are represented with 'X's, empty slots are represented with '.'s. You may assume the following rules:

  • You receive a valid board, made of only battleships or empty slots.
  • Battleships can only be placed horizontally or vertically. In other words, they can only be made of the shape 1xN (1 row, N columns) or Nx1 (N rows, 1 column), where N can be of any size.
  • At least one horizontal or vertical cell separates between two battleships – there are no adjacent battleships.

Example:

X..X
...X
...X

In the above board there are 2 battleships.

Invalid Example:

...X
XXXX
...X

This is an invalid board that you will not receive – as battleships will always have a cell separating between them.

Follow up:
Could you do it in one-pass, using only O(1) extra memory and without modifying the value of the board?

【解答】要数有多少 battleship,并且要求使用 O(1) 的空间复杂度,还不能修改 board 上的数值。

一行一行遍历,每一行中从左往右遍历。对于每一个点,如果左侧和上方都不是 X,那就认为这是一艘新的 battleship。

class Solution {
    public int countBattleships(char[][] board) {
        int count = 0;
        
        for (int i=0; i<board.length; i++) {
            for (int j=0; j<board[0].length; j++) { if (board[i][j]=='.') continue; if (j>0 && board[i][j-1]=='X') continue;
                if (i>0 && board[i-1][j]=='X') continue;
                count++;
            }
        }
        
        return count;
    }
}

Strong Password Checker
【题目】A password is considered strong if below conditions are all met:

  1. It has at least 6 characters and at most 20 characters.
  2. It must contain at least one lowercase letter, at least one uppercase letter, and at least one digit.
  3. It must NOT contain three repeating characters in a row (“…aaa…” is weak, but “…aa…a…” is strong, assuming other conditions are met).

Write a function strongPasswordChecker(s), that takes a string s as input, and return the MINIMUM change required to make s a strong password. If s is already strong, return 0.

Insertion, deletion or replace of any one character are all considered as one change.

【解答】规则很容易理解:

  1. 6~20 个字符;
  2. 必须包含小写字符、大写字符和数字;
  3. 不能包含连续三个相同字符。

但是困难的地方在于,题目不是问一个字符串是否符合条件,而是要求怎样通过最少的增删改来使得密码符合规则。题目类似于求编辑距离,只不过编辑距离只有一头(源字符串)确定,另一头是不确定的。如果只有一个规则,也会简单一些,三个规则混到一起,互相影响,使得题目非常困难。

挣扎了一段时间,就去讨论区 看解答 去了:

class Solution {
    public int strongPasswordChecker(String s) {
        int res = 0, a = 1, A = 1, d = 1;
        char[] carr = s.toCharArray();
        int[] arr = new int[carr.length];

        for (int i = 0; i < arr.length;) {
            if (Character.isLowerCase(carr[i])) a = 0;
            if (Character.isUpperCase(carr[i])) A = 0;
            if (Character.isDigit(carr[i])) d = 0;

            int j = i;
            while (i < carr.length && carr[i] == carr[j]) i++;
            arr[j] = i - j;
        }

        int total_missing = (a + A + d);

        if (arr.length < 6) {
            res += total_missing + Math.max(0, 6 - (arr.length + total_missing));

        } else {
            int over_len = Math.max(arr.length - 20, 0), left_over = 0;
            res += over_len;

            for (int k = 1; k < 3; k++) {
                for (int i = 0; i < arr.length && over_len > 0; i++) {
                    if (arr[i] < 3 || arr[i] % 3 != (k - 1)) continue;
                    arr[i] -= Math.min(over_len, k);
                    over_len -= k;
                }
            }

            for (int i = 0; i < arr.length; i++) { if (arr[i] >= 3 && over_len > 0) {
                    int need = arr[i] - 2;
                    arr[i] -= over_len;
                    over_len -= need;
                }

                if (arr[i] >= 3) left_over += arr[i] / 3;
            }

            res += Math.max(total_missing, left_over);
        }

        return res;
    }
}

Maximum XOR of Two Numbers in an Array
【题目】Given a non-empty array of numbers, a0, a1, a2, … , an-1, where 0 ≤ ai < 231.

Find the maximum result of ai XOR aj, where 0 ≤ ij < n.

Could you do this in O(n) runtime?

Example:

Input: [3, 10, 5, 25, 2, 8]

Output: 28

Explanation: The maximum result is 5 ^ 25 = 28.

【解答】要在 O(n) 时间内找两个数 XOR 的最大值。

既然是 O(n) 时间,排序什么的就别想了。拿到手感觉没有什么思路,要去找全部的 XOR 结果暴力解法需要 n 平方的复杂度。再有一点,我觉得可以从高位往低位一位一位去分析,而每一位都是独立的,且优先级有着明确的区别。比如数值部分的最高位在 XOR 能得到 1,总共有 x 种组合,那么在分析次高位的时候,只需要考虑这 x 种中,寻找能够得到 XOR 结果为 1 的即可。但是思路再往下,又不太好继续了。

下面的思路借鉴自 讨论区 的一个解法。现在 Medium 的题目居然也需要看解答了,叹气。

class Solution {
    public int findMaximumXOR(int[] nums) {
        int max = 0, mask = 0;
        for (int i = 31; i >= 0; i--){
            // part 1:
            // prefix mask
            mask = mask | (1 << i);
            // prefix set
            Set set = new HashSet<>();
            for (int num : nums){
                set.add(num & mask);
            }
            
            // part 2:
            int tmp = max | (1 << i);
            for (int prefix : set){
                if(set.contains(tmp ^ prefix)) {
                    max = tmp;
                    break;
                }
            }
        }
        return max;
    }
}

我把思路整理了一下来说明上述代码。求解需要利用一个特性:若 a ^ b = c, a ^ c = b。循环体内的代码说明:

  • part 1:利用 mask 来取所有数的 prefix,第一次一个最高位,第二次为两个最高位,第三次为三个最高位…… 这些 prefix 组成一个 set。
  • part 2:现在假设第 n 次迭代所得到的最大值是 max,那么考虑第 n+1 次迭代:假设新确定的那一位是 1,那么设这个数为 tmp,然后把 tmp 和所有 prefix 进行 XOR 操作,如果得到的数还在这个 prefix set 里面,根据前面提到的特性,说明有两个 prefix 进行 XOR 以后可以得到这个 tmp,那么这个 tmp 就是新的最大值 max,否则这一位只能是 0,那么 max 不变。

Reconstruct Original Digits from English
【题目】Given a non-empty string containing an out-of-order English representation of digits 0-9, output the digits in ascending order.

Note:

  1. Input contains only lowercase English letters.
  2. Input is guaranteed to be valid and can be transformed to its original digits. That means invalid inputs such as “abc” or “zerone” are not permitted.
  3. Input length is less than 50,000.

Example 1:

Input: "owoztneoer"

Output: "012"

Example 2:

Input: "fviefuro"

Output: "45"

【解答】要从无序的英文字母串还原到数字。

考虑个数字的英文所包含字母的特性。比如说字母 z,就只可能在 zero 中出现,因此很容易就找到了 zero 的个数。有一些并不是 unique 的,比如 seven,每个字母都可能在别的英文数中出现,但是 s 只能再 six 和 seven 中出现,因此一旦知道了 six,把 s 的次数减去 six 的次数,就可以得到 seven 的次数了。

class Solution {
    public String originalDigits(String s) {
        // unique:
        // zero
        // 'z' => 0
        // two
        // 'w' => 2
        // four
        // 'u' => 4
        // six
        // 'x' => 6
        // eight
        // 'g' => 8
        
        // not unique:
        // seven = 's' - '6'
        // 's' => 7
        // five = 'v' - '7'
        // 'v' => 5
        // one = 'o' - '0' - '2' - '4'
        // 'o' => 1
        // three = 'h' - 8
        // 'h' => 3
        // nine = 'i' - '6' - '8' - '5'
        // 'i' => 9
        
        Map<Character, Integer> countMap = new HashMap<>();
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (!countMap.containsKey(ch))
                countMap.put(ch, 0);
            countMap.put(ch, countMap.get(ch) + 1);
        }
        
        int[] digits = new int[10];
        Integer count = countMap.get('z');
        if (count != null) {
            digits[0] = count;
        }
        count = countMap.get('w');
        if (count != null) {
            digits[2] = count;
        }
        count = countMap.get('u');
        if (count != null) {
            digits[4] = count;
        }
        count = countMap.get('x');
        if (count != null) {
            digits[6] = count;
        }
        count = countMap.get('g');
        if (count != null) {
            digits[8] = count;
        }
        
        count = countMap.get('s');
        if (count != null) {
            digits[7] = count - digits[6];
        }
        count = countMap.get('v');
        if (count != null) {
            digits[5] = count - digits[7];
        }
        count = countMap.get('o');
        if (count != null) {
            digits[1] = count - digits[0] - digits[2] - digits[4];
        }
        count = countMap.get('h');
        if (count != null) {
            digits[3] = count - digits[8];
        }
        count = countMap.get('i');
        if (count != null) {
            digits[9] = count - digits[6] - digits[8] - digits[5];
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i=0; i<digits.length; i++) {
            for (int j=0; j<digits[i]; j++)
                sb.append(i);
        }
        return sb.toString();
    }
}

Longest Repeating Character Replacement
【题目】Given a string that consists of only uppercase English letters, you can replace any letter in the string with another letter at most ktimes. Find the length of a longest substring containing all repeating letters you can get after performing the above operations.

Note:
Both the string’s length and k will not exceed 104.

Example 1:

Input:
s = "ABAB", k = 2

Output:
4

Explanation:
Replace the two 'A's with two 'B's or vice versa.

Example 2:

Input:
s = "AABABBA", k = 1

Output:
4

Explanation:
Replace the one 'A' in the middle with 'B' and form "AABBBBA".
The substring "BBBB" has the longest repeating letters, which is 4.

【解答】要求在字符串 s 中替换字符 k 次,得到最长的连续字符子串。

如果用回溯法或者动态规划复杂度比较高,而且不好优化。但是,把这个问题转化为一个 sliding window 的问题,一下就豁然开朗了:

  • 假设说有一个 sliding window,从左慢慢往右划,不断调整左右边界:左边吐出 char,右边吃进 char;
  • 这个 window 里面的不同 char 各有多少个,通过一个 map 来记录;
  • 这个 window 的左边界所对应的 char 作为所考察的 repeating char,在左边界固定的基础上不断右移右边界,记录 window 的最大值,如果其他 char 的数量超过了 k,那么需要右移左边界,吐出一个之前的 repeating char。

这类字符串寻找连续子串的问题,都可以考虑 sliding window 的方法。使用 sliding window 的一个好处有这么两个:

  1. 窗口内字符排列或数量增量变化。无论移动左侧还是右侧窗口边沿,都只变化一个字符。
  2. 窗口的左侧或者右侧固定,从而简化问题。像这道题就是总是考虑窗口左侧的 char 为 repeating char。
class Solution {
    public int characterReplacement(String s, int k) {
        if (s==null || k<0)
            throw new IllegalArgumentException();
        if (s.length()==0)
            return 0;
        
        int left=0, right=0;
        int max = 1;
        
        char[] counters = new char[26];
        counters[s.charAt(0) - 'A']++;
        
        while (right<s.length()) {
            int base = s.charAt(left) - 'A';
            int replacementCount = 0;
            for (int i=0; i<26; i++) {
                if (i!=base) {
                    replacementCount += counters[i];
                }
            }
            
            if (replacementCount>k) {
                counters[base]--;
                left++;
            } else {
                int newSize = right-left+1;
                max = Math.max(max, newSize);
                right++;
                if (right<s.length()) {
                    counters[s.charAt(right) - 'A']++;
                } else {
                    // special case: there are still change chances left
                    max = Math.max(max, newSize + k - replacementCount);
                }
            }
        }
        
        // never exceeds the length of s
        return Math.min(max, s.length());
    }
}

Construct Quad Tree
【题目】We want to use quad trees to store an N x N boolean grid. Each cell in the grid can only be true or false. The root node represents the whole grid. For each node, it will be subdivided into four children nodes until the values in the region it represents are all the same.

Each node has another two boolean attributes : isLeaf and valisLeaf is true if and only if the node is a leaf node. The val attribute for a leaf node contains the value of the region it represents.

Your task is to use a quad tree to represent a given grid. The following example may help you understand the problem better:

Given the 8 x 8 grid below, we want to construct the corresponding quad tree:

It can be divided according to the definition above:

 

The corresponding quad tree should be as following, where each node is represented as a (isLeaf, val) pair.

For the non-leaf nodes, val can be arbitrary, so it is represented as *.

Note:

  1. N is less than 1000 and guaranteened to be a power of 2.
  2. If you want to know more about the quad tree, you can refer to its wiki.

【解答】构造 Quad Tree。

没有太多可以说的。递归判断并合并节点,形成 quad tree。

class Solution {
    public Node construct(int[][] grid) {
        if (grid==null || grid.length==0 || grid.length!=grid[0].length)
            throw new IllegalArgumentException();
        
        return this.construct(grid, 0, 0, grid.length);
    }
    
    private Node construct(int[][] grid, int x, int y, int w) {
        if (w==1) {
            return new Node(grid[x][y]!=0, true, null, null, null, null);
        }
        
        Node topLeft = this.construct(grid, x, y, w/2);
        Node topRight = this.construct(grid, x, y+w/2, w/2);
        Node bottomLeft = this.construct(grid, x+w/2, y, w/2);
        Node bottomRight = this.construct(grid, x+w/2, y+w/2, w/2);
        
        // all leaves and equal, merge
        if (topLeft.isLeaf &&
            topRight.isLeaf &&
            bottomLeft.isLeaf &&
            bottomRight.isLeaf &&
            topLeft.val == topRight.val &&
            topLeft.val == bottomLeft.val &&
            topLeft.val == bottomRight.val
        ) {
            return new Node(topLeft.val, true, null, null, null, null);
        }
        
        // otherwise create a sub tree
        return new Node(false, false, topLeft, topRight, bottomLeft, bottomRight);
    }
}

N-ary Tree Level Order Traversal
【题目】

Given an n-ary tree, return the level order traversal of its nodes’ values. (ie, from left to right, level by level).

For example, given a 3-ary tree:

We should return its level order traversal:

[
     [1],
     [3,2,4],
     [5,6]
]

Note:

  1. The depth of the tree is at most 1000.
  2. The total number of nodes is at most 5000.

【解答】没有太多可以说的。循环内按层遍历就好。

class Solution {
    public List<List> levelOrder(Node root) {
        List<List> result = new ArrayList<>();
        if (root==null)
            return result;
        
        Queue level = new LinkedList<>();
        level.add(root);
        while (!level.isEmpty()) {
            Queue nextLevel = new LinkedList<>();
            List list = new ArrayList<>();
            for (Node node : level) {
                list.add(node.val);
                if (node.children != null)
                    nextLevel.addAll(node.children);
            }
            result.add(list);
            level = nextLevel;
        }
        
        return result;
    }
}

Flatten a Multilevel Doubly Linked List
【题目】You are given a doubly linked list which in addition to the next and previous pointers, it could have a child pointer, which may or may not point to a separate doubly linked list. These child lists may have one or more children of their own, and so on, to produce a multilevel data structure, as shown in the example below.

Flatten the list so that all the nodes appear in a single-level, doubly linked list. You are given the head of the first level of the list.

Example:

Input:
 1---2---3---4---5---6--NULL
         |
         7---8---9---10--NULL
             |
             11--12--NULL
Output:
1-2-3-7-8-11-12-9-10-4-5-6-NULL

Explanation for the above example:

Given the following multilevel doubly linked list:

We should return the following flattened doubly linked list:

【解答】要把多层的双向链表压平。
大致思路上应该说没有什么难的,但是细节处理的坑比较多。源链表节点的 next 始终要放到 stack 里面去,然后再看 child,如果 child 不为空,直接从 stack 里面取下一个节点;如果为空,则优先使用 child 为下一个节点。
在选中下一个节点之后,再重新链接的过程中,注意覆盖节点的所有 field,包括 next、prev、child。

class Solution {
    public Node flatten(Node head) {
        if (head==null)
            return null;
        
        Stack stack = new Stack<>();
        Node cur = head;
        while (true) {
            // always push the next node to stack
            if (cur.next != null)
                stack.push(cur.next);

            Node next = cur.child;
            if (next == null) {
                // make child as the next, but if child is null, pop the next from stack
                if (!stack.isEmpty()) {
                    next = stack.pop();
                } else {
                    cur.child = null;
                    cur.next = null;
                    break;
                }
            }
            
            // link current to next
            cur.child = null;
            next.prev = cur;
            cur.next = next;
            
            cur = next;
        }
        
        return head;
    }
}

All O`one Data Structure
【题目】Implement a data structure supporting the following operations:

  1. Inc(Key) – Inserts a new key with value 1. Or increments an existing key by 1. Key is guaranteed to be a non-empty string.
  2. Dec(Key) – If Key’s value is 1, remove it from the data structure. Otherwise decrements an existing key by 1. If the key does not exist, this function does nothing. Key is guaranteed to be a non-empty string.
  3. GetMaxKey() – Returns one of the keys with maximal value. If no element exists, return an empty string "".
  4. GetMinKey() – Returns one of the keys with minimal value. If no element exists, return an empty string "".

Challenge: Perform all these in O(1) time complexity.

【解答】实现一个数据结构,加一、减一,获取最大值的 key 和最小值的 key,都是 O(1) 的时间复杂度。

这类题目可以说遇到多次了,有一些通用思路:

  • 要 O(1),根据 key 去取 value 的,无非就两种数据结构,一个是 HashMap,一个是数组(下标访问)。
  • 如果有根据 key 取 value,那就需要从 key 到 value 的映射;如果有根据 value 取 key,那就需要 value 到 key 的映射。
  • 这道题看起来需要两者:
    • 根据 key 要获取 value 对象,从而进行 inc 和 dec 的操作;
    • 根据 value 的大小情况,来找到对象并返回相应的 key。
  • 要能 O(1) 得到最大值和最小值,肯定不能在需要的时候去现找,那就需要在平时维护一个有序列表,一头最大,一头最小。这个列表的大小比较实际只靠 value,具体这个 value 有多少个 key 对应并不重要。因而这个序号并不是 key 根据 value 的值实际的序号,而是互不重复的 value 进行比较得到的序号。
  • 在 inc 和 dec 的时候,由于变化只有 1,位置调整于是也只有 1。这就是为什么要根据互不重复的 value 来排序的原因,这样的情况一定能保证调整的幅度不超过 1。

有了这样的思路以后,建立一个 Item 元素,作为 value,里面需要存放前一个节点、后一个节点,实际取值,以及 key set。这里的前一个、后一个节点是用来保序用的。建立的 head 和 tail 是用于包住 value 形成的有序串,简化计算用的。

class Item {
    public int value;
    public Item next;
    public Item prev;
    public Set keys = new HashSet<>();;
}
class AllOne {
    
    private Map<String, Item> map = new HashMap<>();
    private Item head = new Item();
    private Item tail = new Item();
    

    /** Initialize your data structure here. */
    public AllOne() {
        this.head.next = this.tail;
        this.tail.prev = this.head;
        // dummy head and dummy tail
        this.head.value = Integer.MIN_VALUE;
        this.tail.value = Integer.MAX_VALUE;
    }
    
    private void link(Item left, Item right) {
        left.next = right;
        right.prev = left;
    } 
    
    /** Inserts a new key  with value 1. Or increments an existing key by 1. */
    public void inc(String key) {
        Item currentItem = this.map.get(key);
        if (currentItem != null) { // it's an existing key
            currentItem.keys.remove(key);
            int newValue = currentItem.value + 1;
            Item next = currentItem.next;
            if (next.value == newValue) {
                // the item for the newValue is already existed
                map.put(key, next);
                next.keys.add(key);
            } else {
                // there is no item for newValue, create one
                Item newItem = new Item();
                newItem.value = newValue;
                newItem.keys.add(key);
                
                this.link(currentItem, newItem);
                this.link(newItem, next);
                
                map.put(key, newItem);
            }
            
            // remove the node if there's no key mapped to its value
            if (currentItem.keys.isEmpty()) {
                this.link(currentItem.prev, currentItem.next);
            }
        } else { // it's a new key
            if (this.head.next.value == 1) {
                // the item for new key (value==1) is already existed
                this.head.next.keys.add(key);
                map.put(key, this.head.next);
            } else {
                // new item needed
                Item next = this.head.next;
                Item newItem = new Item();
                newItem.value = 1;
                newItem.keys.add(key);
                
                this.link(this.head, newItem);
                this.link(newItem, next);
                
                map.put(key, newItem);
            }
        }
    }
    
    /** Decrements an existing key by 1. If Key's value is 1, remove it from the data structure. */
    public void dec(String key) {
        Item currentItem = this.map.get(key);
        if (currentItem == null)
            return;
        
        Item prev = currentItem.prev;
        currentItem.keys.remove(key);
        // remove the item if no key mapped to it
        if (currentItem.keys.isEmpty()) {
            this.link(prev, currentItem.next);
        }

        if (currentItem.value == 1) {
            this.map.remove(key);
            return;
        }
        
        int newValue = currentItem.value - 1;
        // there's already an item existed for newValue
        if (prev.value == newValue) {
            prev.keys.add(key);
            this.map.put(key, prev);
            return;
        }
        
        // there is no item for newValue, create one
        Item newItem = new Item();
        newItem.value = newValue;
        newItem.keys.add(key);

        Item next = prev.next;
        this.link(prev, newItem);
        this.link(newItem, next);

        map.put(key, newItem);    
    }
    
    /** Returns one of the keys with maximal value. */
    public String getMaxKey() {
        if (this.head.next == this.tail)
            return "";
        return this.tail.prev.keys.iterator().next();
    }
    
    /** Returns one of the keys with Minimal value. */
    public String getMinKey() {
        if (this.head.next == this.tail)
            return "";
        return this.head.next.keys.iterator().next();
    }
}

Minimum Genetic Mutation
【题目】A gene string can be represented by an 8-character long string, with choices from "A""C""G""T".

Suppose we need to investigate about a mutation (mutation from “start” to “end”), where ONE mutation is defined as ONE single character changed in the gene string.

For example, "AACCGGTT" -> "AACCGGTA" is 1 mutation.

Also, there is a given gene “bank”, which records all the valid gene mutations. A gene must be in the bank to make it a valid gene string.

Now, given 3 things – start, end, bank, your task is to determine what is the minimum number of mutations needed to mutate from “start” to “end”. If there is no such a mutation, return -1.

Note:

  1. Starting point is assumed to be valid, so it might not be included in the bank.
  2. If multiple mutations are needed, all mutations during in the sequence must be valid.
  3. You may assume start and end string is not the same.

Example 1:

start: "AACCGGTT"
end:   "AACCGGTA"
bank: ["AACCGGTA"]

return: 1

Example 2:

start: "AACCGGTT"
end:   "AAACGGTA"
bank: ["AACCGGTA", "AACCGCTA", "AAACGGTA"]

return: 2

Example 3:

start: "AAAAACCC"
end:   "AACCCCCC"
bank: ["AAAACCCC", "AAACCCCC", "AACCCCCC"]

return: 3

【解答】维护一个当前考察的字符串集合和一个字典,每次都拿当前字符串去和字典里的所有候选字符串比较,如果只差 1,就把候选从字典里面拿出来,更新到当前考察的字符串集合中。直到发现解,或者字典为空,或者在最近一次比较中没有发现任何匹配,就表示算法已经结束。

class Solution {
    public int minMutation(String start, String end, String[] bank) {
        if (start==null || end==null || bank==null)
            throw new IllegalArgumentException();
        if (start.equals(end))
            return 0;
        if (start.length() != end.length() || bank.length==0)
            return -1;
        
        Set bankSet = new HashSet<>(Arrays.asList(bank));
        Set current = new HashSet<>();
        current.add(start);
        
        int count = 0;
        while (!current.isEmpty() && !bankSet.isEmpty()) {
            count++;
            Set newCurrent = new HashSet<>();
            for (String cur : current) {
                Set newBank = new HashSet<>();
                for (String b : bankSet) {
                    if (this.diffOne(cur, b)) {
                        if (b.equals(end))
                            return count;
                        else
                            newCurrent.add(b);
                    } else {
                        newBank.add(b);
                    }
                }
                bankSet = newBank;
            }
            current = newCurrent;
        }
        
        return -1;
    }
    
    private boolean diffOne(String left, String right) {
        if (left.length() != right.length())
            return false;
        
        boolean changed = false;
        for (int i=0; i<left.length(); i++) {
            if (left.charAt(i) != right.charAt(i)) {
                if (changed)
                    return false;
                else
                    changed = true;
            }
        }
        
        return true;
    }
}

Number of Segments in a String
【题目】Count the number of segments in a string, where a segment is defined to be a contiguous sequence of non-space characters.

Please note that the string does not contain any non-printable characters.

Example:

Input: "Hello, my name is John"
Output: 5

【解答】常规题。没有太多可说的,注意一些特殊 case 是否被覆盖到,比如首尾空格。

class Solution {
    public int countSegments(String s) {
        boolean isSpace = true;
        int count = 0;
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (ch!=' ' && isSpace) {
                isSpace = false;
            } else if (ch==' ' && !isSpace) {
                count++;
                isSpace = true;
            }
        }
        
        if (!isSpace)
            count++;
        
        return count;
    }
}

Non-overlapping Intervals
【题目】Given a collection of intervals, find the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.

Note:

  1. You may assume the interval’s end point is always bigger than its start point.
  2. Intervals like [1,2] and [2,3] have borders “touching” but they don’t overlap each other.

Example 1:

Input: [ [1,2], [2,3], [3,4], [1,3] ]

Output: 1

Explanation: [1,3] can be removed and the rest of intervals are non-overlapping.

Example 2:

Input: [ [1,2], [1,2], [1,2] ]

Output: 2

Explanation: You need to remove two [1,2] to make the rest of intervals non-overlapping.

Example 3:

Input: [ [1,2], [2,3] ]

Output: 0

Explanation: You don't need to remove any of the intervals since they're already non-overlapping.

【解答】要求去掉最少的 interval 使得剩余的 interval 无重叠。

一开始,觉得这道题应该比较简单,扫描线问题嘛。上来先排序,完毕之后从左往右扫一遍——这也算是常规思路了。于是我把 intervals 按照 start 排序,然后尝试从中拿掉某些。结果超时了,于是使用一个二维数组优化,内存使用又超了:

class Solution {
    public int eraseOverlapIntervals(Interval[] intervals) {
        Arrays.sort(intervals, new Comparator(){
            @Override
            public int compare(Interval left, Interval right) {
                if (left.start != right.start)
                    return left.start - right.start;
                // this is to make sure when start is same, we can always remove the left one and keep the right one as the right one is shorter
                return right.end - left.end;
            }
        });
        
        Integer[][] cache = new Integer[intervals.length][intervals.length];
        return this.cal(intervals, 0, -1, cache);
    }
    
    private int cal(Interval[] intervals, int index, int last, Integer[][] cache) {
        if (index >= intervals.length)
            return 0;
        
        if (last!=-1 && cache[index][last] != null)
            return cache[index][last];
        
        int count = 0;
        for (int i=index; i<intervals.length; i++) {
            Interval interval = intervals[i];
            
            if (last==-1) {
                last = i;
            } else if (intervals[last].start == interval.start) {
                count++;
                last = i;
            } else if (intervals[last].end <= interval.start) {
                // no overlapping
                last = i;
            } else {
                // it's the most complicated case as we don't know which should be removed: "last" or "i"
                int c1 = this.cal(intervals, i+1, last, cache);
                int c2 = this.cal(intervals, i+1, i, cache);
                count++;
                count += Math.min(c1, c2);
                break;
            }
        }
        
        cache[index][last] = count;
        return count;
    }
}

现在换一个角度,反过来想,如果是从零开始,找尽量多的无重叠 interval 呢?把搜索问题变成构造问题。

考虑排序,可根据什么排呢?二者是很不一样的。用 end 来排序,而不是 start:

  • 如果按照 start 排序,从左到右扫描的过程中,start 大或者小并不代表该 interval 是否应该被选中,因为需要考虑几个重叠 interval 之间覆盖的竞争关系。对右侧的 interval 来说,和不同的左侧 interval 产生重叠,但 start 更大不代表这个重叠范围更大或者更小。因此单纯按照 start 排序没有意义。
  • 而如果按照 end 排序,从小到大扫描的时候,只要发现重叠就只需要忽略后扫描到的,而根本不需要考虑 start 在哪里。因为对于右侧的 interval 来说,左侧元素的 start 在哪里不重要,重要的是 end 在哪里,end 才是决定了重叠部分右边界位置的因素。我们当然希望这个右边界越靠左越好,因此我们在扫描的过程中,尽可能留下先遇到的 end。
class Solution {
    public int eraseOverlapIntervals(Interval[] intervals) {
        Arrays.sort(intervals, new Comparator(){
            @Override
            public int compare(Interval left, Interval right) {
                return left.end - right.end;
            }
        });
        
        int count = 0;
        Interval last = null;
        for (Interval interval : intervals) {
            // intervals are sorted by "end"
            if (last==null || interval.start >= last.end) {
                last = interval;
                count++;
            }
        }
        
        return intervals.length - count;
    }
}

Find Right Interval
【题目】Given a set of intervals, for each of the interval i, check if there exists an interval j whose start point is bigger than or equal to the end point of the interval i, which can be called that j is on the “right” of i.

For any interval i, you need to store the minimum interval j’s index, which means that the interval j has the minimum start point to build the “right” relationship for interval i. If the interval j doesn’t exist, store -1 for the interval i. Finally, you need output the stored value of each interval as an array.

Note:

  1. You may assume the interval’s end point is always bigger than its start point.
  2. You may assume none of these intervals have the same start point.

Example 1:

Input: [ [1,2] ]

Output: [-1]

Explanation: There is only one interval in the collection, so it outputs -1.

Example 2:

Input: [ [3,4], [2,3], [1,2] ]

Output: [-1, 0, 1]

Explanation: There is no satisfied "right" interval for [3,4].
For [2,3], the interval [3,4] has minimum-"right" start point;
For [1,2], the interval [2,3] has minimum-"right" start point.

Example 3:

Input: [ [1,4], [2,3], [3,4] ]

Output: [-1, 2, -1]

Explanation: There is no satisfied "right" interval for [1,4] and [3,4].
For [2,3], the interval [3,4] has minimum-"right" start point.

【解答】对于每个 interval,要找有多少 interval 在它的右边。

肯定不能死算。仔细想一下,其实对每个 interval 我只关心它的右边界,和剩余所有 interval 的左边界。

如果它的右边界确定的时候,要找所有比它小的左边界,这就是一个典型的使用 TreeMap 的问题——key 是所有 interval 的左边界;value 是源数组中的 index。

class Solution {
    public int[] findRightInterval(Interval[] intervals) {
        if (intervals == null)
            throw new IllegalArgumentException();
        
        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i=0; i<intervals.length; i++) {
            Interval interval = intervals[i];
            map.put(interval.start, i);
        }
        
        int[] result = new int[intervals.length];
        for (int i=0; i<intervals.length; i++) {
            Interval interval = intervals[i];
            Map.Entry<Integer, Integer> entry = map.ceilingEntry(interval.end);
            if (entry==null)
                result[i] = -1;
            else
                result[i] = entry.getValue();
        }
        
        return result;
    }
}

Path Sum III
【题目】You are given a binary tree in which each node contains an integer value.

Find the number of paths that sum to a given value.

The path does not need to start or end at the root or a leaf, but it must go downwards (traveling only from parent nodes to child nodes).

The tree has no more than 1,000 nodes and the values are in the range -1,000,000 to 1,000,000.

Example:

root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8

      10
     /  \
    5   -3
   / \    \
  3   2   11
 / \   \
3  -2   1

Return 3. The paths that sum to 8 are:

1.  5 -> 3
2.  5 -> 2 -> 1
3. -3 -> 11

【解答】递归求解。但是在递归的过程中,不需要记录每个解分别是由那几个数累加起来的,但是需要记录都可以得到哪些和,不需要去重。因为不同的节点加起来可以得到同样的和,但是实际是算作不同的解。

class Solution {
    public int pathSum(TreeNode root, int sum) {
        if (root==null) return 0;
        return travel(root, new ArrayList<>(), sum);
    }
    
    private int travel(TreeNode root, List list, int sum) {
        if (root==null) return 0;
        
        int total = 0;
        if (root.val == sum) total++;
        
        List newList = new ArrayList<>();
        newList.add(root.val);
        for (int num : list) {
            int added = num + root.val;
            newList.add(added);
            if (added==sum) total++;
        }
        
        int left = travel(root.left, newList, sum);
        int right = travel(root.right, newList, sum);
        
        total += left;
        total += right;
        
        return total;
    }
}

Find All Anagrams in a String
【题目】Given a string s and a non-empty string p, find all the start indices of p‘s anagrams in s.

Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.

The order of output does not matter.

Example 1:

Input:
s: "cbaebabacd" p: "abc"

Output:
[0, 6]

Explanation:
The substring with start index = 0 is "cba", which is an anagram of "abc".
The substring with start index = 6 is "bac", which is an anagram of "abc".

Example 2:

Input:
s: "abab" p: "ab"

Output:
[0, 1, 2]

Explanation:
The substring with start index = 0 is "ab", which is an anagram of "ab".
The substring with start index = 1 is "ba", which is an anagram of "ab".
The substring with start index = 2 is "ab", which is an anagram of "ab".

【解答】要找成为 anagram 的子串,基本上最直接的方法就是使用滑动窗口了。而且是个固定大小的滑动窗口。我当时的做法不是特别好,虽然也解出来了,但没有充分利用固定大小这个特性,于是代码写得有些啰嗦。

class Solution {
    private void updateMap(Map<Character, Integer> charMap, char ch, int count) {
        if (count==0)
            charMap.remove(ch);
        else
            charMap.put(ch, count);
    }
    
    public List findAnagrams(String s, String p) {
        if (s==null || p==null || p.length()==0)
            throw new IllegalArgumentException();
        
        List result = new ArrayList<>();
        if (s.length()==0)
            return result;
        
        Map<Character, Integer> charMap = new HashMap<>();
        for (int i=0; i<p.length(); i++) {
            char ch = p.charAt(i);
            int count = charMap.getOrDefault(ch, 0);
            charMap.put(ch, ++count);
        }
        
        int left=0, right=0;
        while (right<s.length()) {
            // always load s[right] at the beginning of each iteration
            char cr = s.charAt(right);
            int count = charMap.getOrDefault(cr, 0) - 1;
            this.updateMap(charMap, cr, count);
            
            // anagram found
            if (charMap.isEmpty()) {
                result.add(left);
                // move both boundaries
                charMap.put(s.charAt(left), 1);
                left++;
                right++;
                continue;
            }
            
            // now try moving left only, and break the loop when:
            // the amount of cr is matched, or left moves beyond right (no matches found)
            while (charMap.getOrDefault(cr, 0)==-1 && left<=right) {
                char cl = s.charAt(left++);
                count = charMap.getOrDefault(cl, 0) + 1;
                this.updateMap(charMap, cl, count);
            }
            
            right++;
        }
        
        return result;
    }
}

K-th Smallest in Lexicographical Order
【题目】Given integers n and k, find the lexicographically k-th smallest integer in the range from 1 to n.

Note: 1 ≤ k ≤ n ≤ 109.

Example:

Input:
n: 13   k: 2

Output:
10

Explanation:
The lexicographical order is [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9], so the second smallest number is 10.

【解答】要找从 1 到 n 这连续的数中,第 k 小的那一个。

老实说,这一类题我是不太擅长做的。尝试了几种思路都没有发现特别好的解法。在讨论区我找到了我认为 最清晰的解法 ,而且这种解法对于类似的这种问题有一定启发意义。大致思路是使用引入十叉数,因为每一个数字的后面,最多可能有从 0 到 9 这十个可能。于是从根往叶子方向一层一层遍历,直到找到第 k 个数为止。

class Solution {
    public int findKthNumber(int n, int k) {
        int curr = 1;
        k = k - 1;
        while (k > 0) {
            int steps = calSteps(n, curr, curr + 1);
            if (steps <= k) {
                curr += 1;
                k -= steps;
            } else {
                curr *= 10;
                k -= 1;
            }
        }
        return curr;
    }
    
    /**
     * how many steps from n1 to n2
     */
    public int calSteps(int n, long n1, long n2) {
        int steps = 0;
        while (n1 <= n) {
            steps += Math.min(n + 1, n2) - n1;
            n1 *= 10;
            n2 *= 10;
        }
        return steps;
    }
}

说明,从根节点开始往下一层一次统计,curr 指向当前节点,如果 curr 和 curr+1 之间的 steps 小于 k,那 curr 就在当前层前进;否则,curr 下潜到下一层去,这种情况下 k 需要-1 因为原节点已经遍历过了。

关于 steps 的计算(calSteps):考虑边界情况,n2 和 n+1 二者中小的那个区减掉 n1,这里的+1 主要是考虑要把 n 这个数也算进去。

Arranging Coins
【题目】You have a total of n coins that you want to form in a staircase shape, where every k-th row must have exactly k coins.

Given n, find the total number of full staircase rows that can be formed.

n is a non-negative integer and fits within the range of a 32-bit signed integer.

Example 1:

n = 5

The coins can form the following rows:
¤
¤ ¤
¤ ¤

Because the 3rd row is incomplete, we return 2.

Example 2:

n = 8

The coins can form the following rows:
¤
¤ ¤
¤ ¤ ¤
¤ ¤

Because the 4th row is incomplete, we return 3.

【解答】要求硬币组成的楼梯形状的层数,不完整的层要舍弃。

我觉得是一个数学问题。第一层是有 1 枚硬币,第 x 层最多有 x 枚,等差数列,那么最多一共有 (1+x)*x/2 枚,这个数必须要大于等于 n,这是个一元二次方程,拿求根公式解一下就好。

class Solution {
    public int arrangeCoins(int n) {
        // (1 + x) * x / 2 >= n
        // so x^2 + x - 2n >= 0
        // [-b±(b^2-4ac)^(1/2)]/(2a)
        // avoid overflow
        double result = (-1 + Math.sqrt(1 - 4*(-2*(double)n))) / 2;
        return (int) result;
    }
}

Find All Duplicates in an Array
【题目】Given an array of integers, 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.

Find all the elements that appear twice in this array.

Could you do it without extra space and in O(n) runtime?

Example:

Input:
[4,3,2,7,8,2,3,1]

Output:
[2,3]

【解答】要从大小为 1~n 的长度为 n 数组中找出出现了两次的数,而且要 O(1) 的空间和 O(n) 的时间复杂度,这就意味着一般的搜索和排序之类方法的都可以靠边站了 。
利用这些数范围的特性,在找一个数的时候,把这个数 x 减去 1 作为一个 index,让数组在该 index 上的数取负数,如果发现该数已经是负数了,那就说明这个数 x 就是重复的数。

class Solution {
    public List findDuplicates(int[] nums) {
        if (null==nums)
            throw new IllegalArgumentException();
        
        // to save as much space as possible LinkedList is used, not ArrayList
        List result = new LinkedList<>();
        for (int i=0; i<nums.length; i++) {
            int index = Math.abs(nums[i]) - 1;
            if (nums[index]<0) { // if it's already <0 this is the second time to be hit
                result.add(index + 1);
            } else {
                nums[index] = -nums[index];
            }
        }
        
        return result;
    }
}

String Compression
【题目】Given an array of characters, compress it in-place.

The length after compression must always be smaller than or equal to the original array.

Every element of the array should be a character (not int) of length 1.

After you are done modifying the input array in-place, return the new length of the array.

Follow up:
Could you solve it using only O(1) extra space?

Example 1:

Input:
["a","a","b","b","c","c","c"]

Output:
Return 6, and the first 6 characters of the input array should be: ["a","2","b","2","c","3"]

Explanation:
"aa" is replaced by "a2". "bb" is replaced by "b2". "ccc" is replaced by "c3".

Example 2:

Input:
["a"]

Output:
Return 1, and the first 1 characters of the input array should be: ["a"]

Explanation:
Nothing is replaced.

Example 3:

Input:
["a","b","b","b","b","b","b","b","b","b","b","b","b"]

Output:
Return 4, and the first 4 characters of the input array should be: ["a","b","1","2"].

Explanation:
Since the character "a" does not repeat, it is not compressed. "bbbbbbbbbbbb" is replaced by "b12".
Notice each digit has it's own entry in the array.

Note:

  1. All characters have an ASCII value in [35, 126].
  2. 1 <= len(chars) <= 1000.

【解答】字符串压缩。快慢双指针方式求解。

class Solution {
    public int compress(char[] chars) {
        if (chars==null)
            throw new IllegalArgumentException();
        if (chars.length==0)
            return 0;
        
        // slow / fast: the star/end of the consecutive sub char array
        // recording: the pointer recording char + number
        int slow=0, fast=0, recording=0;
        while (slow<chars.length) {
            if (fast==chars.length || chars[fast]!=chars[slow]) {
                // recording always starts with the char
                chars[recording++] = chars[slow];
                
                if (fast-slow > 1) {
                    String num = "" + (fast-slow);
                    for (char n : num.toCharArray())
                        chars[recording++] = n;
                }
                slow = fast;
            }
            
            fast++;
        }
        
        return recording;
    }
}

Add Two Numbers II
【题目】You are given two non-empty linked lists representing two non-negative integers. The most significant digit comes first and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Follow up:
What if you cannot modify the input lists? In other words, reversing the lists is not allowed.

Example:

Input: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 8 -> 0 -> 7

【解答】两个用链表表示的数相加。常规操作,包括 fake/dummy head,进位符号的处理等等。

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        if (l1==null || l2==null)
            throw new IllegalArgumentException();
        
        Stack s1 = new Stack<>();
        Stack s2 = new Stack<>();
        
        ListNode cur = l1;
        while (cur!=null) {
            s1.push(cur);
            cur = cur.next;
        }
        cur = l2;
        while (cur!=null) {
            s2.push(cur);
            cur = cur.next;
        }
        
        Stack res = new Stack<>();
        boolean carry = false;
        while (!s1.isEmpty() || !s2.isEmpty()) {
            ListNode l = s1.isEmpty() ? null : s1.pop();
            ListNode r = s2.isEmpty() ? null : s2.pop();
            int lv = l==null ? 0 : l.val;
            int rv = r==null ? 0 : r.val;
            int sum = lv + rv;
            if (carry)
                sum++;
            
            if (sum>=10) {
                sum -= 10;
                carry = true;
            } else {
                carry = false;
            }
            
            res.push(new ListNode(sum));
        }
        if (carry)
            res.push(new ListNode(1));
        
        ListNode dummyHead = new ListNode(0);
        cur = dummyHead;
        while (!res.isEmpty()) {
            cur.next = res.pop();
            cur = cur.next;
        }
        
        return dummyHead.next;
    }
}

Arithmetic Slices II – Subsequence
【题目】A sequence of numbers is called arithmetic if it consists of at least three elements and if the difference between any two consecutive elements is the same.

For example, these are arithmetic sequences:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

The following sequence is not arithmetic.

1, 1, 2, 5, 7

A zero-indexed array A consisting of N numbers is given. A subsequence slice of that array is any sequence of integers (P0, P1, …, Pk) such that 0 ≤ P0 < P1 < … < Pk < N.

subsequence slice (P0, P1, …, Pk) of array A is called arithmetic if the sequence A[P0], A[P1], …, A[Pk-1], A[Pk] is arithmetic. In particular, this means that k ≥ 2.

The function should return the number of arithmetic subsequence slices in the array A.

The input contains N integers. Every integer is in the range of -231 and 231-1 and 0 ≤ N ≤ 1000. The output is guaranteed to be less than 231-1.

Example:

Input: [2, 4, 6, 8, 10]

Output: 7

Explanation:
All arithmetic subsequence slices are:
[2,4,6]
[4,6,8]
[6,8,10]
[2,4,6,8]
[4,6,8,10]
[2,4,6,8,10]
[2,6,10]

【解答】我最开始的思路是动态规划,划分成子问题。每个子问题是:

对于所有从 index 为 i 开始的 subsequence,在 interval 是 k 的情况下,解集用 c[i][k] 来表示,这个解集合我不需要知道具体内容,但我需要知道每个 subsequence 在包含特定个元素个数 p 的情况下个有多少种可能。那么对于一个新的 index j 且 j<i,考察 A[j] 和 A[i]:
A[j]-A[i] 为新的 interval,寻求解集 c[j][A[j]-A[i]]。看起来这个方法似乎是可以走得通的,但是明显无论从实现层面还是时间复杂度层面都过高了。为了缓存已得到的结果,我需要建立一个这样的数据结构来避开重复计算:Map<Integer, Map<Integer, Map<Integer, Integer>>>,含义是:<start_index, <interval, <sub_seq_len, count>>。因此这是一个三维的动态规划,我们一般只做一维到二维,而三维的话一般有更好的解决办法。[后记:其实第三维 sub_seq_len 应该是不需要的,当时的想法有点问题。]

下面的解答来自 讨论区 。简单思路:

  • 对于任意 0<=j<i<A.length, A[j] 和 A[i] 之间的差是 d,那么:map[i][d] 表示:以 A[i] 结尾的这些 subsequence 且元素两两差值为 d 的情况下,这样的这些 subsequence 有多少个。
  • 这样在每次迭代中,先对于当前的元素 A[i] 拿到当前的值 c1=map[i][d],再看它前面的元素 A[j],拿到它的值 c2=map[j][d],对于 map[i][d] 来说,新的值就是 c1+c2+1,这里面的“+1”是因为这个新的这些 subsequence:A[j],A[i];
  • 在考虑总数的时候,每次都把 c2 加进去,因为 c2 来自于前一个元素 A[j] 的统计,这些 subsequence 们最少也有两个元素,现在各自再加上一个 A[i] 就都成为了合法的结果(至少三个元素)。
class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        if (A==null)
            throw new IllegalArgumentException();
        
        int res = 0;
        Map<Integer, Integer>[] map = new Map[A.length];

        for (int i = 0; i < A.length; i++) {
            map[i] = new HashMap<>(i);

            for (int j = 0; j < i; j++) {
                long diff = (long)A[i] - A[j];
                if (diff <= Integer.MIN_VALUE || diff > Integer.MAX_VALUE) continue;

                int d = (int)diff;
                int c1 = map[i].getOrDefault(d, 0);
                int c2 = map[j].getOrDefault(d, 0);
                res += c2;
                map[i].put(d, c1 + c2 + 1);
            }
        }

        return res;
    }
}

Number of Boomerangs
【题目】Given n points in the plane that are all pairwise distinct, a “boomerang” is a tuple of points (i, j, k) such that the distance between i and j equals the distance between i and k (the order of the tuple matters).

Find the number of boomerangs. You may assume that n will be at most 500 and coordinates of points are all in the range [-10000, 10000] (inclusive).

Example:

Input:
[[0,0],[1,0],[2,0]]

Output:
2

Explanation:
The two boomerangs are [[1,0],[0,0],[2,0]] and [[1,0],[2,0],[0,0]]

【解答】i 到 j 的距离必须等于 i 到 k 的距离。

基本思路很简单,遍历每一个点,并以之为 i,再以它到所有其他点距离的平方为 key,放到 map 里面去,value 为统计该距离出现的次数。

之后对于 map 中的每组 entry,value 的个数就表示了在选定 i 的情况下,有多少组距离相等,如果它是 x 的话,那么 x*(x-1) 就意味着有多少种组合。(我在注释里面列出了排列组合的两个基本公式,也是它的本质由来)

class Solution {
    public int numberOfBoomerangs(int[][] points) {
        if (points==null)
            throw new IllegalArgumentException();
        
        int total = 0;
        for (int i=0; i<points.length; i++) {
            int[] point = points[i];
            // point is chosen as the center (first element of the tri-tuple)
            // key: distance^2, value: count
            Map<Integer, Integer> map = new HashMap<>();
            for (int j=0; j<points.length; j++) {
                int[] p = points[j];
                if (point==p)
                    continue;
                
                int distance = (int) (Math.pow(point[0]-p[0], 2) + Math.pow(point[1]-p[1], 2));
                int count = map.getOrDefault(distance, 0);
                map.put(distance, ++count);
            }
            
            for (int count : map.values())
                total += this.perm(count);
        }
        
        return total;
    }
    
    // Permutation: P(m, n) = n! / (n-m)!
    // Combination: C(m, n) = P(m, n) / m! = n! / ((n-m)!*m!)
    private int perm(int count) {
        // here m=2, so C(m, n) = n * (n-1);
        return count * (count-1);
    }
}

Find All Numbers Disappeared in an Array
【题目】Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.

Find all the elements of [1, n] inclusive that do not appear in this array.

Could you do it without extra space and in O(n) runtime? You may assume the returned list does not count as extra space.

Example:

Input:
[4,3,2,7,8,2,3,1]

Output:
[5,6]

【解答】应该算是常规题了。把每个数都尝试换到它应该去的位置,完毕之后,扫一遍看哪些位置上的数和 index 不匹配(index 应当等于 n-1)。

class Solution {
    public List findDisappearedNumbers(int[] nums) {
        if (nums==null)
            throw new IllegalArgumentException();
        
        int i=0;
        while (i<nums.length) {
            int currentVal = nums[i];
            int indexShouldBe = nums[i]-1;
            if (indexShouldBe != i) {
                if (nums[indexShouldBe] != currentVal) {
                    swap(nums, i, indexShouldBe);
                    continue;
                }
            }
            
            i++;
        }
        
        List result = new ArrayList<>();
        for (i=0; i<nums.length; i++) {
            if (nums[i]-1 != i)
                result.add(i+1);
        }
        
        return result;
    }
    
    private void swap(int[] nums, int i, int j) {
        if (i==j)
            return;
        
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

Serialize and Deserialize BST
【题目】Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.

Design an algorithm to serialize and deserialize a binary search tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary search tree can be serialized to a string and this string can be deserialized to the original tree structure.

The encoded string should be as compact as possible.

Note: Do not use class member/global/static variables to store states. Your serialize and deserialize algorithms should be stateless.

【解答】要把一棵 BST 序列化和反序列化。

应该说有很多做法。我的做法是把根放在序列化后字符串的头部,然后是递归得到的左子树串,再是右子树串。这样一来,反序列化的时候,字符串的第一个字符总是根,后面的字符只要比它小就是左子树,再往后就是右子树。

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root==null)
            return "";
        
        // pre-order traversal
        String result = "" + root.val;
        if (root.left!=null)
            result += "," + serialize(root.left);
        if (root.right!=null)
            result += "," + serialize(root.right);
        
        return result;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data.length()==0)
            return null;
        
        String[] arr = data.split(",");
        int[] tokens = new int[arr.length];
        for (int i=0; i<arr.length; i++)
            tokens[i] = Integer.parseInt(arr[i]);
        return deserialize(tokens, 0, tokens.length-1);
    }
    
    private TreeNode deserialize(int[] tokens, int start, int end) {
        TreeNode root = new TreeNode(tokens[start]);
        Integer left = null;
        Integer right = null;
        for (int i=start+1; i<=end; i++) {
            if (tokens[i]root.val && right==null) {
                right = i;
                break;
            }
        }
        
        if (left!=null)
            root.left = right==null ? deserialize(tokens, left, end) : deserialize(tokens, left, right-1);
        if (right!=null)
            root.right = deserialize(tokens, right, end);
        
        return root;
    }
}

Delete Node in a BST
【题目】Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST.

Basically, the deletion can be divided into two stages:

  1. Search for a node to remove.
  2. If the node is found, delete the node.

Note: Time complexity should be O(height of tree).

Example:

root = [5,3,6,2,4,null,7]
key = 3

    5
   / \
  3   6
 / \   \
2   4   7

Given key to delete is 3. So we find the node with value 3 and delete it.

One valid answer is [5,4,6,2,null,null,7], shown in the following BST.

    5
   / \
  4   6
 /     \
2       7

Another valid answer is [5,2,6,null,4,null,7].

    5
   / \
  2   6
   \   \
    4   7

【解答】要从一棵 BST 上面删除一个节点。分成几种情况考虑:

如果要删除的节点在左子树,递归调用左子树;

右子树同理;

最麻烦的是删除根节点,删掉了之后,如果原本有左子树,那就从左子树取最大的节点放到根上;反之,从右子树取最小的节点放到根上(不需要实际的节点替换,替换节点的数值即可)。

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root==null) return null;
        if (key<root.val) {
            root.left = this.deleteNode(root.left, key);
            return root;
        }
        if (key>root.val) {
            root.right = this.deleteNode(root.right, key);
            return root;
        }
        if (root.left==null && root.right==null) return null;
        if (root.left==null) return root.right;
        if (root.right==null) return root.left;
        
        TreeNode smallest = this.getSmallest(root.right);
        root.val = smallest.val;
        root.right = deleteNode(root.right, smallest.val);
        return root;
    }
    
    private TreeNode getSmallest(TreeNode node){
        while(node.left != null){
            node = node.left;
        }
        return node;
    }
}

Sort Characters By Frequency
【题目】Given a string, sort it in decreasing order based on the frequency of characters.

Example 1:

Input:
"tree"

Output:
"eert"

Explanation:
'e' appears twice while 'r' and 't' both appear once.
So 'e' must appear before both 'r' and 't'. Therefore "eetr" is also a valid answer.

Example 2:

Input:
"cccaaa"

Output:
"cccaaa"

Explanation:
Both 'c' and 'a' appear three times, so "aaaccc" is also a valid answer.
Note that "cacaca" is incorrect, as the same characters must be together.

Example 3:

Input:
"Aabb"

Output:
"bbAa"

Explanation:
"bbaA" is also a valid answer, but "Aabb" is incorrect.
Note that 'A' and 'a' are treated as two different characters.

【解答】按照字符的出现频率排序。常规题目,使用一个 map 来统计次数,然后实现比较接口来排序。

class Solution {
    public String frequencySort(String s) {
        if (s==null) throw new IllegalArgumentException();
        
        Map<Character, Item> map = new HashMap<>();
        List list = new ArrayList<>();
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (!map.containsKey(ch)) {
                Item item = new Item(ch, 1);
                map.put(ch, item);
                list.add(item);
            } else {
                Item item = map.get(ch);
                item.count = item.count + 1;
            }
        }
        
        Collections.sort(list);
        StringBuilder sb = new StringBuilder();
        for (Item item : list) {
            for (int i=1; i<=item.count; i++) {
                sb.append(item.ch);
            }
        }
        return sb.toString();
    }
}

class Item implements Comparable {
    public char ch;
    public int count;
    
    public Item(char ch, int count) {
        this.ch = ch;
        this.count = count;
    }
    
    @Override
    public int compareTo(Item that) {
        return that.count - this.count;
    }
}

Minimum Number of Arrows to Burst Balloons
【题目】There are a number of spherical balloons spread in two-dimensional space. For each balloon, provided input is the start and end coordinates of the horizontal diameter. Since it’s horizontal, y-coordinates don’t matter and hence the x-coordinates of start and end of the diameter suffice. Start is always smaller than end. There will be at most 104 balloons.

An arrow can be shot up exactly vertically from different points along the x-axis. A balloon with xstart and xend bursts by an arrow shot at x if xstart ≤ x ≤ xend. There is no limit to the number of arrows that can be shot. An arrow once shot keeps travelling up infinitely. The problem is to find the minimum number of arrows that must be shot to burst all balloons.

Example:

Input:
[[10,16], [2,8], [1,6], [7,12]]

Output:
2

Explanation:
One way is to shoot one arrow for example at x = 6 (bursting the balloons [2,8] and [1,6]) and another arrow at x = 11 (bursting the other two balloons).

【解答】要寻找看起来还是某种扫描线问题。在从左往右扫描的过程中,既然要尽量使得射出的箭少,那就要尽量到不得不射箭的时候再出手,即到最先出现的未打爆气球右边沿再射出箭,而这一箭要打爆其中所有碰到的气球。反复如此操作。(既然要考察右边沿,因此排序的时候必须按照 end,而不是 start 来排序。)

class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points==null)
            throw new IllegalArgumentException();
        
        List<Point> el = new ArrayList<>(points.length);
        for (int[] point : points) {
            Point p = new Point(point[0], point[1]);
            el.add(p);
        }
        
        Collections.sort(el, new Comparator<Point>() {
            @Override
            public int compare(Point left, Point right) {
                return left.end - right.end;
            }
        });
        
        int index = 0;
        int count = 0;
        Integer rangeStart = null;
        while (index<el.size()) {
            Point point = el.get(index);
            
            if (rangeStart==null || point.start>rangeStart) {
                rangeStart = point.end; // keep the range start as far as possible
                count++;
            }
            
            index++;
        }
        
        return count;
    }
}

class Point {
    public int start;
    public int end;
    public Point(int start, int end) {
        this.start = start;
        this.end = end;
    }
}

Minimum Moves to Equal Array Elements
【题目】Given a non-empty integer array of size n, find the minimum number of moves required to make all array elements equal, where a move is incrementing n – 1 elements by 1.

Example:

Input:
[1,2,3]

Output:
3

Explanation:
Only three moves are needed (remember each move increments two elements):

[1,2,3]  =>  [2,3,3]  =>  [3,4,3]  =>  [4,4,4]

【解答】每次只能移动 n-1 个元素且移动的方法是+1,要尽量让所有元素靠拢,那本质上其实意味着每次只能移动一个元素且移动方法是-1。这样,只需要找到其中的最小数,然后累加所有其他数和最小值的差值即可。顺便提一句,如果题目修改一下,移动的方法可以是+1 也可以是-1,那就不是找最小数,而是找中位数了。

class Solution {
    public int minMoves(int[] nums) {
        Integer min = null;
        for (int num : nums) {
            if (min==null || num<min)
                min = num;
        }
        
        int total = 0;
        for (int num : nums)
            total += (num-min);
        
        return total;
    }
}

4Sum II
【题目】Given four lists A, B, C, D of integer values, compute how many tuples (i, j, k, l) there are such that A[i] + B[j] + C[k] + D[l] is zero.

To make problem a bit easier, all A, B, C, D have same length of N where 0 ≤ N ≤ 500. All integers are in the range of -228 to 228 – 1 and the result is guaranteed to be at most 231 – 1.

Example:

Input:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

Output:
2

Explanation:
The two tuples are:
1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

【解答】四个数组,各取一个数,要求加起来为 0。似乎没有特别好的办法,死算肯定嵌套层数太多,时间复杂度可以到 n 的四次方。于是,折中的方法是,A 和 B 嵌套,找出两两配对所有的和的出现次数;再拿 C 和 D 嵌套,找出两两配对所有和的出现次数。再把这两个结果合并考察,找到加起来结果为 0 的组合。整体时间复杂度为 n 的平方,空间复杂度也为 n 的平方。

class Solution {
    public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
        if (A==null || B==null || C==null || D==null)
            throw new IllegalArgumentException();
        
        Map<Integer, Integer> sumMap = new HashMap<>();
        for (int a : A) {
            for (int b : B) {
                int count = sumMap.getOrDefault(a+b, 0);
                count++;
                sumMap.put(a+b, count);
            }
        }
        
        int total = 0;
        for (int c : C) {
            for (int d : D) {
                total += sumMap.getOrDefault(-(c+d), 0);
            }
        }
        
        return total;
    }
}

Assign Cookies
【题目】Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.

Note:
You may assume the greed factor is always positive.
You cannot assign more than one cookie to one child.

Example 1:

Input: [1,2,3], [1,1]

Output: 1

Explanation: You have 3 children and 2 cookies. The greed factors of 3 children are 1, 2, 3. 
And even though you have 2 cookies, since their size is both 1, you could only make the child whose greed factor is 1 content.
You need to output 1.

Example 2:

Input: [1,2], [1,2,3]

Output: 2

Explanation: You have 2 children and 3 cookies. The greed factors of 2 children are 1, 2. 
You have 3 cookies and their sizes are big enough to gratify all of the children, 
You need to output 2.

【解答】分饼干问题。

本质上还是一个贪心问题,把饼干根据大小排序,孩子根据贪心程度排序。两个指针分别从这两个数组的左侧开始往右扫描。只要能满足孩子,两个指针都分别往后移动,否则只移动指向饼干的指针。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        if (g==null || s==null)
            throw new IllegalArgumentException();
        if (g.length==0 || s.length==0)
            return 0;
        
        Arrays.sort(g);
        Arrays.sort(s);
        
        int gi = 0, si = 0;
        int count = 0;
        while (gi<g.length && si<s.length) {
            if (g[gi]<=s[si]) {
                count++;
                gi++;
                si++;
            } else {
                si++;
            }
        }
        
        return count;
    }
}

132 Pattern
【题目】Given a sequence of n integers a1, a2, …, an, a 132 pattern is a subsequence ai, aj, ak such that i < j < k and ai < ak < aj. Design an algorithm that takes a list of n numbers as input and checks whether there is a 132 pattern in the list.

Note: n will be less than 15,000.

Example 1:

Input: [1, 2, 3, 4]

Output: False

Explanation: There is no 132 pattern in the sequence.

Example 2:

Input: [3, 1, 4, 2]

Output: True

Explanation: There is a 132 pattern in the sequence: [1, 4, 2].

Example 3:

Input: [-1, 3, 2, 0]

Output: True

Explanation: There are three 132 patterns in the sequence: [-1, 3, 2], [-1, 3, 0] and [-1, 2, 0].

【解答】要寻找一串数中是否存在“小-大-中”这样的模式。

这道题看起来似乎不难,却让我思考了很长时间。最开始的想法是,看起来是一道引入双指针,创建几个 O(1) 的变量,从左到右简单扫描一遍,就可以得到结果的题目。后来越想越发现不是这样的,并且如果只使用 O(1) 的变量,似乎是无法解答的。因为对于这道题中 ai、aj、ak 三个变量的寻找,在拿到候选的 aj 的时候,ai 和 ak 使用不同的组合,aj 有可行和不可行这样不同的可能,因此为了遍历 ai 和 ak 不同的组合而形成的可能性,需要使用至少 O(n) 的空间来创建一个类似数组的结构。

这个空间就是使用 stack 来实现,而这个 stack 存放的是 ak 的候选。从右往左扫描,考虑到 i、j、k 三个变数,ai 在 aj 的左侧,aj 在 ak 的左侧,因此扫描遍历到的数:

  • 首先考虑能不能成为 ai,如果能,那就返回结果,因为 ai<ak,而 ak<aj 是已经存在了的;
  • 如果不能,那把它列为 aj 的候选,要求 aj 必须>ak;
  • 如果再不能,那就把它列为 ak 的候选,此处没有要求。
class Solution {
    public boolean find132pattern(int[] nums) {
        if (nums==null) throw new IllegalArgumentException();
        if (nums.length<3) return false;
        
        Integer num2 = null;
        Stack stack = new Stack<>();
        for (int i=nums.length-1; i>=0; i--) {
            int num1 = nums[i];
            // check the case if num1 can be ai -
            // if num1 is smaller than num2 we find the result
            if (num2!=null && num1<num2)
                return true;
            
            // so num1 can't be ai, let's check if num1 can be aj -
            // if so the numbers in the stack must be smaller than num1 because ak < aj
            // if there are multiple we only keep the last one because:
            // to match ai<ak<aj, since ak is smaller than aj, we want ak to be as largest as possible
            while (!stack.isEmpty() && stack.peek()<num1)
                num2 = stack.pop();
            
            // so num1 can't be aj either, let num1 be an ak candidate -
            stack.push(num1);
        }
        
        return false;
    }
}

Circular Array Loop
【题目】

You are given a circular array nums of positive and negative integers. If a number k at an index is positive, then move forward k steps. Conversely, if it’s negative (-k), move backward k steps. Since the array is circular, you may assume that the last element’s next element is the first element, and the first element’s previous element is the last element.

Determine if there is a loop (or a cycle) in nums. A cycle must start and end at the same index and the cycle’s length > 1. Furthermore, movements in a cycle must all follow a single direction. In other words, a cycle must not consist of both forward and backward movements.

 

Example 1:

Input: [2,-1,1,2,2]
Output: true
Explanation: There is a cycle, from index 0 -> 2 -> 3 -> 0. The cycle's length is 3.

Example 2:

Input: [-1,2]
Output: false
Explanation: The movement from index 1 -> 1 -> 1 ... is not a cycle, because the cycle's length is 1. By definition the cycle's length must be greater than 1.

Example 3:

Input: [-2,1,-1,-2,-2]
Output: false
Explanation: The movement from index 1 -> 2 -> 1 -> ... is not a cycle, because movement from index 1 -> 2 is a forward movement, but movement from index 2 -> 1 is a backward movement. All movements in a cycle must follow a single direction.

Note:

  1. -1000 ≤ nums[i] ≤ 1000
  2. nums[i] ≠ 0
  3. 1 ≤ nums.length ≤ 5000

Follow up:

Could you solve it in O(n) time complexity and O(1) extra space complexity?

【解答】我解是解出来了,但是属于基础解法,并没有做到上面 Follow Up 中要求的复杂度。思路是,每一个元素都尝试作为循环的开头,使用 set 来标记运行轨迹,同时也建立一个 boolean 型的变量来标记当前尝试是正向还是逆向。一旦发现遇到遍历过的元素,表示这次尝试失败;如果遇到起始元素,返回 true。

class Solution {
    public boolean circularArrayLoop(int[] nums) {
        if (nums==null) throw new IllegalArgumentException();
        if (nums.length==0 || nums.length==1) return false;
        
        for (int i=0; i<nums.length; i++) {
            Set<Integer> forwardIndices = new HashSet<>();
            Set<Integer> backwardIndices = new HashSet<>();
            Integer originalItem = i;
            boolean forward = nums[originalItem] > 0;
            if (nums[originalItem] == 0)
                continue;
            if (forward && forwardIndices.contains(originalItem))
                continue;
            if (!forward && backwardIndices.contains(originalItem))
                continue;
            
            if (this.getNextIndex(nums, i)==i) {
                forwardIndices.add(originalItem);
                backwardIndices.add(originalItem);
                continue;
            }
            
            Integer item = originalItem;
            if (forward)
                forwardIndices.add(originalItem);
            else
                backwardIndices.add(originalItem);
            
            while (true) {
                item = this.getNextIndex(nums, item);
                if (item.equals(originalItem))
                    return true;
                
                if (forward) {
                    if (nums[item]<=0 || forwardIndices.contains(item))
                        break;
                    forwardIndices.add(item);
                } else {
                    if (nums[item]>=0 || backwardIndices.contains(item))
                        break;
                    backwardIndices.add(item);
                }
            }
        }
        
        return false;
    }
    
    private int getNextIndex(int[] nums, int index) {
        index = nums[index] + index;
        while (index >= nums.length) {
            index -= nums.length;
        }
        while (index < 0) {
            index += nums.length;
        }
        return index;
    }
}

Poor Pigs
【题目】There are 1000 buckets, one and only one of them is poisonous, while the rest are filled with water. They all look identical. If a pig drinks the poison it will die within 15 minutes. What is the minimum amount of pigs you need to figure out which bucket is poisonous within one hour?

Answer this question, and write an algorithm for the general case.

General case:

If there are n buckets and a pig drinking poison will die within m minutes, how many pigs (x) you need to figure out the poisonous bucket within p minutes? There is exactly one bucket with poison.

Note:

  1. A pig can be allowed to drink simultaneously on as many buckets as one would like, and the feeding takes no time.
  2. After a pig has instantly finished drinking buckets, there has to be a cool down time of minutes. During this time, only observation is allowed and no feedings at all.
  3. Any given bucket can be sampled an infinite number of times (by an unlimited number of pigs).

【解答】1000 个桶,只有一个桶里面有毒。如果猪喝了毒药,15 分钟内死掉。问要在 1 小时内找到哪个桶,最少需要多少头猪。

这道题貌似是一道数学题,也许在很久以前在一本数学有关的书上见过。一个小时,如果把喝的水分成 5 部分,每部分都混合起来让猪喝,可以喝+观察 4 次,但是可以得出 5 部分的结果,因为 4 次如果死了分别可以代表该次喝的水有毒,如果没死则按照排除法,得知剩下那一部分有毒。一头猪的存活情况,代表了 5 种可能,这就像是 5 进制,那么两头猪就可以表示 5^5=25 种,所以题中例子要求的是 5^x >= 1000,求 x 的最小值。用这样的算法推广可以得到:

class Solution {
    public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
        long rounds = minutesToTest / minutesToDie + 1;
        // rounds ^ pigs >= buckets
        long pigs = 0;
        while (Math.pow(rounds, pigs)<buckets) {
            pigs++;
        }
        
        return (int) pigs;
    }
}

Repeated Substring Pattern
【题目】Given a non-empty string check if it can be constructed by taking a substring of it and appending multiple copies of the substring together. You may assume the given string consists of lowercase English letters only and its length will not exceed 10000.

Example 1:

Input: "abab"
Output: True
Explanation: It's the substring "ab" twice.

Example 2:

Input: "aba"
Output: False

Example 3:

Input: "abcabcabcabc"
Output: True
Explanation: It's the substring "abc" four times. (And the substring "abcabc" twice.)

【解答】要判断一个字符串是否有多个子字符串拼接而成。使用最直白的做法,考虑每一个字符为重复串开始的位置进行递归,如果字符串全部遍历完毕,而又没有多余的字符,就返回 true。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        if (s==null)
            throw new IllegalArgumentException();
        
        if (s.length()<2)
            return false;
        
        for (int len=1; len<=s.length()-len; len++) {
            if (s.length() % len == 0) {
                int start = len;
                boolean matched = true;
                while (start<s.length()) {
                    if (!this.check(s, len, start)) {
                        matched = false;
                        break;
                    }
                    start += len;
                }
                if (matched)
                    return true;
            }
        }
        
        return false;
    }
    
    private boolean check(String s, int len, int start) {
        int i=0;
        while (i<len) {
            if (s.charAt(i) != s.charAt(start + i))
                return false;
            i++;
        }
        return true;
    }
}

LFU Cache
【题目】Design and implement a data structure for Least Frequently Used (LFU) cache. It should support the following operations: get and put.

get(key) – Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) – Set or insert the value if the key is not already present. When the cache reaches its capacity, it should invalidate the least frequently used item before inserting a new item. For the purpose of this problem, when there is a tie (i.e., two or more keys that have the same frequency), the least recently used key would be evicted.

Follow up:
Could you do both operations in O(1) time complexity?

Example:

LFUCache cache = new LFUCache( 2 /* capacity */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // returns 1
cache.put(3, 3);    // evicts key 2
cache.get(2);       // returns -1 (not found)
cache.get(3);       // returns 3.
cache.put(4, 4);    // evicts key 1.
cache.get(1);       // returns -1 (not found)
cache.get(3);       // returns 3
cache.get(4);       // returns 4

【解答】LFU 的缓存设计。

和 LRU 类似,思路是使用一个普通 map 来处理 key-value 映射的问题,但是为了能够根据使用频率来淘汰元素,就引入了有序的链表,这样在访问的时候位置可能会发生邻近的移动。思路基本上没有什么特殊的,但是实现起来比较啰嗦。很遗憾下面这个解停在巨长的一个 test case,还不是超时,而是结果错误,这个 case 的步骤是如此之长,1000+的操作步骤,大概前 2/3 这段代码的执行是没问题的,之后不一致,我很难去分析原因了。

class LFUCache2 {
    private Map<Integer, Item> map = new HashMap<>();
    private Item dummyHead = new Item(0, 0, -1); // the list is sorted ascendingly
    private Item dummyTail = new Item(0, 0, Integer.MAX_VALUE);
    private int size = 0;
    private int capacity;
    
    public LFUCache2(int capacity) {
        if (capacity<0) throw new IllegalArgumentException();
        dummyHead.next = dummyTail;
        dummyTail.prev = dummyHead;
        this.capacity = capacity;
    }
    
    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        
        Item item = map.get(key);
        int returnValue = item.value;
        item.frequence = item.frequence + 1;
        while (item.next != null && item.next.frequence<=item.frequence) {
            this.swapWithNext(item);
            item = item.next;
        }
        
        return returnValue;
    }
    
    public void put(int key, int value) {
        if (capacity==0)
            return;
        
        if (map.containsKey(key)) {
            Item item = map.get(key);
            item.value = value;
            item.frequence = item.frequence + 1;
            
            this.moveForwardForSameFrequence(item);

            return;
        }
        
        if (size == this.capacity) {
            Item item = dummyHead.next;
            map.remove(item.key);
            
            item.key = key;
            item.value = value;
            item.frequence = 1;
            
            map.put(key, item);
            
            this.moveForwardForSameFrequence(item);
        } else {
            Item current = dummyHead.next;
            Item item = new Item(key, value, 1);
            
            dummyHead.next = item;
            current.prev = item;
            
            item.prev = dummyHead;
            item.next = current;
            
            this.moveForwardForSameFrequence(item);
            
            map.put(key, item);
            size++;
        }
    }
    
    private void moveForwardForSameFrequence(Item item) {
        while (item.next.frequence<=item.frequence) {
            this.swapWithNext(item);
        }
    }
    
    private void swapWithNext(Item l) {
        Item r = l.next;
        Item prev = l.prev;
        Item next = r.next;
        
        prev.next = r;
        next.prev = l;
        
        l.next = next;
        l.prev = r;
        
        r.next = l;
        r.prev = prev;
    }
}

class Item {
    public Integer key;
    public Integer value;
    public Integer frequence;
    public Item next;
    public Item prev;
    public Item(Integer key, Integer value, Integer frequence) {
        this.key = key;
        this.value = value;
        this.frequence = frequence;
        this.next = null;
        this.prev = null;
    }
}

我也看了 讨论区高票 的解法,对 LinkedHashSet 使用很合适,把根据访问频率挪位置等等操作都简化了。主要思路:

三个 HashMap:

  • 第一个是最常规的,存放 LFUCache 的 key 和 value;
  • 第二个存放 key 和频率(出现次数);
  • 那么余下的问题是怎么实现淘汰机制,即在有 map 以外的 key 到来的时候,找到最近最不使用的 key 来,于是第三个使用的是一个值为 LinkedHashSet 的 map,这个 map 的 key 是前面提到的出现次数,而把实际的外部看起来的 LFUCache 的 key 放到了这个 LinkedHashSet 里面。

在淘汰机制触发的时候,就看到 LinkedHashSet 优于普通 HashSet 的好处了——数据是存放在它内部的一个 LinkedList 里面的,因而是严格有序的。这个 list 上面所有的数,对应的 count 都是一致的,添加的时候总是在尾部添加,而淘汰的时候总是从头部淘汰,这就像是一个队列。那为什么不直接使用 LinkedList,而要使用 LinkedHashSet 呢?这就要用到 Set 的机制,当添加的元素已经在 Set 内存在了,就要把它挪到队尾,而不是添加重复元素。

除了以上三个 map,还需要一个 int min,用来记录最小的 count,因为淘汰首先要找到当前最小的 count,其次才在属于同一个 count 的 LinkedHashSet 里面找出需要淘汰的那个 key。

class LFUCache {
    HashMap<Integer, Integer> vals;
    HashMap<Integer, Integer> counts;
    HashMap<Integer, LinkedHashSet<Integer>> lists;
    int cap;
    int min = -1;
    public LFUCache(int capacity) {
        cap = capacity;
        vals = new HashMap<>();
        counts = new HashMap<>();
        lists = new HashMap<>();
        lists.put(1, new LinkedHashSet<>());
    }
    
    public int get(int key) {
        if(!vals.containsKey(key))
            return -1;
        int count = counts.get(key);
        counts.put(key, count+1);
        lists.get(count).remove(key);
        if(count==min && lists.get(count).size()==0)
            min++;
        if(!lists.containsKey(count+1))
            lists.put(count+1, new LinkedHashSet<>());
        lists.get(count+1).add(key);
        return vals.get(key);
    }
    
    public void put(int key, int value) {
        if(cap<=0)
            return;
        if(vals.containsKey(key)) {
            vals.put(key, value);
            get(key);
            return;
        } 
        if(vals.size() >= cap) {
            int evit = lists.get(min).iterator().next();
            lists.get(min).remove(evit);
            vals.remove(evit);
        }
        vals.put(key, value);
        counts.put(key, 1);
        min = 1;
        lists.get(1).add(key);
    }
}

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 http://bit.ly/2VWLVZj

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 HTTP API 认证授权术

我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说,用户的登录成功后,服务器会发一个登录凭证(又被叫作Token),就像你去访问某个公司,在前台被认证过合法后,这个公司的前台会给你的一个访客卡一样,之后,你在这个公司内去到哪都用这个访客卡来开门,而不再校验你是哪一个人。在计算机的世界里,这个登录凭证的相关数据会放在两种地方,一个地方在用户端,以Cookie的方式(一般不会放在浏览器的Local Storage,因为这很容易出现登录凭证被XSS攻击),另一个地方是放在服务器端,又叫Session的方式(SessonID存于Cookie)。

但是,这个世界还是比较复杂的,除了用户访问,还有用户委托的第三方的应用,还有企业和企业间的调用,这里,我想把业内常用的一些 API认证技术相对系统地总结归纳一下,这样可以让大家更为全面的了解这些技术。注意,这是一篇长文!

本篇文章会覆盖如下技术:

  • HTTP Basic
  • Digest Access
  • App Secret Key + HMAC
  • JWT – JSON Web Tokens
  • OAuth 1.0 – 3 legged & 2 legged
  • OAuth 2.0 – Authentication Code & Client Credential

HTTP Basic

HTTP Basic 是一个非常传统的API认证技术,也是一个比较简单的技术。这个技术也就是使用 usernamepassword 来进行登录。整个过程被定义在了 RFC 2617 中,也被描述在了 Wikipedia: Basic Access Authentication 词条中,同时也可以参看 MDN HTTP Authentication

其技术原理如下:

  1. usernamepassword 做成  username:password 的样子(用冒号分隔)
  2. 进行Base64编码。Base64("username:password") 得到一个字符串(如:把 haoel:coolshell 进行base64 后可以得到 aGFvZW86Y29vbHNoZWxsCg
  3. aGFvZW86Y29vbHNoZWxsCg放到HTTP头中 Authorization 字段中,形成 Authorization: Basic aGFvZW86Y29vbHNoZWxsCg,然后发送到服务端。
  4. 服务端如果没有在头里看到认证字段,则返回401错,以及一个个WWW-Authenticate: Basic Realm='HelloWorld' 之类的头要求客户端进行认证。之后如果没有认证通过,则返回一个401错。如果服务端认证通过,那么会返回200。

我们可以看到,使用Base64的目的无非就是为了把一些特殊的字符给搞掉,这样就可以放在HTTP协议里传输了。而这种方式的问题最大的问题就是把用户名和口令放在网络上传,所以,一般要配合TLS/SSL的安全加密方式来使用。我们可以看到 JIRA Cloud 的API认证支持HTTP Basic 这样的方式。

但我们还是要知道,这种把用户名和密码同时放在公网上传输的方式有点不太好,因为Base64不是加密协议,保是编码协议,所以就算是有HTTPS作为安全保护,给人的感觉还是不放心。

Digest Access

中文称“HTTP 摘要认证”,最初被定义在了 RFC 2069 文档中(后来被 RFC 2617 引入了一系列安全增强的选项;“保护质量”(qop)、随机数计数器由客户端增加、以及客户生成的随机数)。

其基本思路是,请求方把用户名口令和域做一个MD5 –  MD5(username:realm:password) 然后传给服务器,这样就不会在网上传用户名和口令了,但是,因为用户名和口令基本不会变,所以,这个MD5的字符串也是比较固定的,因此,这个认证过程在其中加入了两个事,一个是 nonce 另一个是 qop

  • 首先,调用方发起一个普通的HTTP请求。比如:GET /coolshell/admin/ HTTP/1.1
  • 服务端自然不能认证能过,服务端返回401错误,并且在HTTP头里的 WWW-Authenticate 包含如下信息:
 WWW-Authenticate: Digest realm="testrealm@host.com",
                        qop="auth,auth-int",
                        nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                        opaque="5ccc069c403ebaf9f0171e9517f40e41"
  • 其中的 nonce 为服务器端生成的随机数,然后,客户端做 HASH1=MD5(MD5(username:realm:password):nonce:cnonce) ,其中的 cnonce 为客户端生成的随机数,这样就可以使得整个MD5的结果是不一样的。
  • 如果 qop 中包含了 auth ,那么还得做  HASH2=MD5(method:digestURI) 其中的 method 就是HTTP的请求方法(GET/POST…),digestURI 是请求的URL。
  • 如果 qop 中包含了 auth-init ,那么,得做  HASH2=MD5(method:digestURI:MD5(entityBody)) 其中的 entityBody 就是HTTP请求的整个数据体。
  • 然后,得到 response = MD5(HASH1:nonce:nonceCount:cnonce:qop:HASH2) 如果没有 qopresponse = MD5(HA1:nonce:HA2)
  • 最后,我们的客户端对服务端发起如下请求—— 注意HTTP头的 Authorization: Digest ...
GET /dir/index.html HTTP/1.0
Host: localhost
Authorization: Digest username="Mufasa",
                     realm="testrealm@host.com",
                     nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                     uri="%2Fcoolshell%2Fadmin",
                     qop=auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="6629fae49393a05397450978507c4ef1",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

维基百科上的 Wikipedia: Digest access authentication 词条非常详细地描述了这个细节。

摘要认证这个方式会比之前的方式要好一些,因为没有在网上传递用户的密码,而只是把密码的MD5传送过去,相对会比较安全,而且,其并不需要是否TLS/SSL的安全链接。但是,别看这个算法这么复杂,最后你可以发现,整个过程其实关键是用户的password,这个password如果不够得杂,其实是可以被暴力破解的,而且,整个过程是非常容易受到中间人攻击——比如一个中间人告诉客户端需要一个

App Secret Key + HMAC

先说HMAC技术,这个东西来自于MAC – Message Authentication Code,是一种用于给消息签名的技术,也就是说,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。

 

(图片来自 Wikipedia – MAC 词条

我们再来说App ID,这个东西跟验证没有关系,只是用来区分,是谁来调用API的,就像我们每个人的身份证一样,只是用来标注不同的人,不是用来做身份认证的。与前面的不同之处是,这里,我们需要用App ID 来映射一个用于加密的密钥,这样一来,我们就可以在服务器端进行相关的管理,我们可以生成若干个密钥对(AppID, AppSecret),并可以有更细粒度的操作权限管理。

把AppID和HMAC用于API认证,目前来说,玩得最好最专业的应该是AWS了,我们可以通过S3的API请求签名文档看到AWS是怎么玩的。整个过程还是非常复杂的,可以通过下面的图片流程看个大概。基本上来说,分成如下几个步骤:

  1. 把HTTP的请求(方法、URI、查询字串、头、签名头,body)打个包叫 CanonicalRequest,作个SHA-256的签名,然后再做一个base16的编码
  2. 把上面的这个签名和签名算法 AWS4-HMAC-SHA256、时间戳、Scop,再打一个包,叫 StringToSign
  3. 准备签名,用 AWSSecretAccessKey来对日期签一个 DataKey,再用 DataKey 对要操作的Region签一个 DataRegionKey ,再对相关的服务签一个DataRegionServiceKey ,最后得到 SigningKey.
  4. 用第三步的 SigningKey来对第二步的 StringToSign 签名。

 

最后,发出HTTP Request时,在HTTP头的 Authorization字段中放入如下的信息:

Authorization: AWS4-HMAC-SHA256 
               Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, 
               SignedHeaders=content-type;host;x-amz-date, 
               Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7

 

其中的  AKIDEXAMPLE 是 AWS Access Key ID, 也就是所谓的 AppID,服务器端会根据这个AppID来查相关的 Secret Access Key,然后再验证签名。如果,你对这个过程有点没看懂的话,你可以读一读这篇文章——《Amazon S3 Rest API with curl》这篇文章里有好些代码,代码应该是最有细节也是最准确的了。

这种认证的方式好处在于,AppID和AppSecretKey,是由服务器的系统开出的,所以,是可以被管理的,AWS的IAM就是相关的管理,其管理了用户、权限和其对应的AppID和AppSecretKey。但是不好的地方在于,这个东西没有标准 ,所以,各家的实现很不一致。比如: Acquia 的 HMAC微信的签名算法 (这里,我们需要说明一下,微信的API没有遵循HTTP协议的标准,把认证信息放在HTTP 头的 Authorization 里,而是放在body里)

JWT – JSON Web Tokens

JWT是一个比较标准的认证解决方案,这个技术在Java圈里应该用的是非常普遍的。JWT签名也是一种MAC(Message Authentication Code)的方法。JWT的签名流程一般是下面这个样子:

  1. 用户使用用户名和口令到认证服务器上请求认证。
  2. 认证服务器验证用户名和口令后,以服务器端生成JWT Token,这个token的生成过程如下:
    • 认证服务器还会生成一个 Secret Key(密钥)
    • 对JWT Header和 JWT Payload分别求Base64。在Payload可能包括了用户的抽象ID和的过期时间。
    • 用密钥对JWT签名 HMAC-SHA256(SecertKey, Base64UrlEncode(JWT-Header)+'.'+Base64UrlEncode(JWT-Payload));
  3. 然后把 base64(header).base64(payload).signature 作为 JWT token返回客户端。
  4. 客户端使用JWT Token向应用服务器发送相关的请求。这个JWT Token就像一个临时用户权证一样。

当应用服务器收到请求后:

  1. 应用服务会检查 JWT  Token,确认签名是正确的。
  2. 然而,因为只有认证服务器有这个用户的Secret Key(密钥),所以,应用服务器得把JWT Token传给认证服务器。
  3. 认证服务器通过JWT Payload 解出用户的抽象ID,然后通过抽象ID查到登录时生成的Secret Key,然后再来检查一下签名。
  4. 认证服务器检查通过后,应用服务就可以认为这是合法请求了。

我们可以看以,上面的这个过程,是在认证服务器上为用户动态生成 Secret Key的,应用服务在验签的时候,需要到认证服务器上去签,这个过程增加了一些网络调用,所以,JWT除了支持HMAC-SHA256的算法外,还支持RSA的非对称加密的算法。

使用RSA非对称算法,在认证服务器这边放一个私钥,在应用服务器那边放一个公钥,认证服务器使用私钥加密,应用服务器使用公钥解密,这样一来,就不需要应用服务器向认证服务器请求了,但是,RSA是一个很慢的算法,所以,虽然你省了网络调用,但是却费了CPU,尤其是Header和Payload比较长的时候。所以,一种比较好的玩法是,如果我们把header 和 payload简单地做SHA256,这会很快,然后,我们用RSA加密这个SHA256出来的字符串,这样一来,RSA算法就比较快了,而我们也做到了使用RSA签名的目的。

最后,我们只需要使用一个机制在认证服务器和应用服务器之间定期地换一下公钥私钥对就好了。

这里强烈建议全文阅读 Anglar 大学的 《JSW:The Complete Guide to JSON Web Tokens

OAuth 1.0

OAuth也是一个API认证的协议,这个协议最初在2006年由Twitter的工程师在开发OpenID实现的时候和社交书签网站Ma.gnolia时发现,没有一种好的委托授权协议,后来在2007年成立了一个OAuth小组,知道这个消息后,Google员工也加入进来,并完善有善了这个协议,在2007年底发布草案,过一年后,在2008年将OAuth放进了IETF作进一步的标准化工作,最后在2010年4月,正式发布OAuth 1.0,即:RFC 5849 (这个RFC比起TCP的那些来说读起来还是很轻松的),不过,如果你想了解其前身的草案,可以读一下 OAuth Core 1.0 Revision A ,我在下面做个大概的描述。

根据RFC 5849,可以看到 OAuth 的出现,目的是为了,用户为了想使用一个第三方的网络打印服务来打印他在某网站上的照片,但是,用户不想把自己的用户名和口令交给那个第三方的网络打印服务,但又想让那个第三方的网络打印服务来访问自己的照片,为了解决这个授权的问题,OAuth这个协议就出来了。

  • 这个协议有三个角色:
    • User(照片所有者-用户)
    • Consumer(第三方照片打印服务)
    • Service Provider(照片存储服务)
  • 这个协义有三个阶段:
    • Consumer获取Request Token
    • Service Provider 认证用户并授权Consumer
    • Consumer获取Access Token调用API访问用户的照片

整个授权过程是这样的:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. 当 User 访问 Consumer 时,Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,使用HTTP GET 请求把 User 切到 Service Provide 的认证页上(其中带上Request Token),让用户输入他的用户和口令。
  5. Service Provider 认证 User 成功后,跳回 Consumer,并返回 Request Token (oauth_token)和 Verification Code(oauth_verifier
  6. 接下来就是签名请求,用Request Token 和 Verification Code 换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  7. 最后使用Access Token 访问用户授权访问的资源。

下图附上一个Yahoo!的流程图可以看到整个过程的相关细节。

因为上面这个流程有三方:User,Consumer 和 Service Provide,所以,又叫 3-legged flow,三脚流程。OAuth 1.0 也有不需要用户参与的,只有Consumer 和 Service Provider 的, 也就是 2-legged flow 两脚流程,其中省掉了用户认证的事。整个过程如下所示:

  1. Consumer(第三方照片打印服务)需要先上Service Provider获得开发的 Consumer Key 和 Consumer Secret
  2. Consumer 向 Service Provide 发起请求请求Request Token (需要对HTTP请求签名)
  3. Service Provide 验明 Consumer 是注册过的第三方服务商后,返回 Request Token(oauth_token)和 Request Token Secret (oauth_token_secret
  4. Consumer 收到 Request Token 后,直接换取 Access Token (oauth_token)和 Access Token Secret (oauth_token_secret)
  5. 最后使用Access Token 访问用户授权访问的资源。

最后,再来说一说OAuth中的签名。

  • 我们可以看到,有两个密钥,一个是Consumer注册Service Provider时由Provider颁发的 Consumer Secret,另一个是 Token Secret。
  • 签名密钥就是由这两具密钥拼接而成的,其中用 &作连接符。假设 Consumer Secret 为 j49sk3j29djd 而 Token Secret 为dh893hdasih9那个,签名密钥为:j49sk3j29djd&dh893hdasih9
  • 在请求Request/Access Token的时候需要对整个HTTP请求进行签名(使用HMAC-SHA1和HMAC-RSA1签名算法),请求头中需要包括一些OAuth需要的字段,如:
    • Consumer Key : 也就是所谓的AppID
    • Token: Request Token 或 Access Token
    • Signature Method :签名算法比如:HMAC-SHA1
    • Timestamp:过期时间
    • Nonce:随机字符串
    • Call Back:回调URL

下图是整个签名的示意图:

图片还是比较直观的,我就不多解释了。

OAuth 2.0

在前面,我们可以看到,从Digest Access, 到AppID+HMAC,再到JWT,再到OAuth 1.0,这些个API认证都是要向Client发一个密钥(或是用密码)然后用HASH或是RSA来签HTTP的请求,这其中有个主要的原因是,以前的HTTP是明文传输,所以,在传输过程中很容易被篡改,于是才搞出来一套的安全签名机制,所以,这些个认证的玩法是可以在HTTP明文协议下玩的。

这种使用签名方式大家可以看到是比较复杂的,所以,对于开发者来说,也是很不友好的,在组织签名的那些HTTP报文的时候,各种,URLEncode和Base64,还要对Query的参数进行排序,然后有的方法还要层层签名,非常容易出错,另外,这种认证的安全粒度比较粗,授权也比较单一,对于有终端用户参与的移动端来说也有点不够。所以,在2012年的时候,OAuth 2.0 的 RFC 6749 正式放出。

OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么 token secret 的密钥了,所以,OAuth 2.0是完全不同于1.0 的,也是不兼容的。目前,Facebook 的 Graph API 只支持OAuth 2.0协议,Google 和 Microsoft Azure 也支持Auth 2.0,国内的微信和支付宝也支持使用OAuth 2.0。

下面,我们来重点看一下OAuth 2.0的两个主要的Flow:

  • 一个是Authorization Code Flow, 这个是 3 legged 的
  • 一个是Client Credential Flow,这个是 2 legged 的。
Authorization Code Flow

Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。这个Flow也是OAuth 2.0四个Flow中我个人觉得最完整的一个Flow,其流程图如下所示。

 

下面是对这个流程的一个细节上的解释:

1)当用户(Resource Owner)访问第三方应用(Client)的时候,第三方应用会把用户带到认证服务器(Authorization Server)上去,主要请求的是 /authorize API,其中的请求方式如下所示。

https://login.authorization-server.com/authorize?
        client_id=6731de76-14a6-49ae-97bc-6eba6914391e
        &response_type=code
        &redirect_uri=http%3A%2F%2Fexample-client.com%2Fcallback%2F
        &scope=read
        &state=xcoiv98CoolShell3kch

其中:

    • client_id为第三方应用的App ID
    • response_type=code为告诉认证服务器,我要走Authorization Code Flow。
    • redirect_uri意思是我跳转回第三方应用的URL
    • scope意是相关的权限
    • state 是一个随机的字符串,主要用于防CSRF攻击。

2)当Authorization Server收到这个URL请求后,其会通过 client_id来检查 redirect_uriscope是否合法,如果合法,则弹出一个页面,让用户授权(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)。

3)当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以其中加入一个 Authorization Code。 如下所示:

https://example-client.com/callback?
        code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
        &state=xcoiv98CoolShell3kch

我们可以看到,

    • 请流动的链接是第 1)步中的 redirect_uri
    • 其中的 state 的值也和第 1)步的 state一样。

4)接下来,Client 就可以使用 Authorization Code 获得 Access Token。其需要向 Authorization Server 发出如下请求。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
 
code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
&grant_type=code
&redirect_uri=https%3A%2F%2Fexample-client.com%2Fcallback%2F
&client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&client_secret=JqQX2PNo9bpM0uEihUPzyrh

5)如果没什么问题,Authorization 会返回如下信息。

{
  "access_token": "iJKV1QiLCJhbGciOiJSUzI1NiI",
  "refresh_token": "1KaPlrEqdFSBzjqfTGAMxZGU",
  "token_type": "bearer",
  "expires": 3600,
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciO.eyJhdWQiOiIyZDRkM..."
}

其中,

    • access_token就是访问请求令牌了
    • refresh_token用于刷新 access_token
    • id_token 是JWT的token,其中一般会包含用户的OpenID

6)接下来就是用 Access Token 请求用户的资源了。

GET /v1/user/pictures
Host: https://example.resource.com

Authorization: Bearer iJKV1QiLCJhbGciOiJSUzI1NiI

 

 Client Credential Flow

Client Credential 是一个简化版的API认证,主要是用于认证服务器到服务器的调用,也就是没有用户参与的的认证流程。下面是相关的流程图。

这个过程非常简单,本质上就是Client用自己的 client_idclient_secret向Authorization Server 要一个 Access Token,然后使用Access Token访问相关的资源。

请求示例

POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=czZCaGRSa3F0Mzpn
&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw

返回示例

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create"
}

这里,容我多扯一句,微信公从平台的开发文档中,使用了OAuth 2.0 的 Client Credentials的方式(参看文档“微信公众号获取access token”),我截了个图如下所谓。我们可以看到,微信公众号使用的是GET方式的请求,把AppID和AppSecret放在了URL中,虽然这也符合OAuth 2.0,但是并不好,因为大多数网关代理会把整个URI请求记到日志中。我们只要脑补一下腾讯的网关的Access Log,里面的日志一定会有很多的各个用户的AppID和AppSecret……

 

小结

讲了这么多,我们来小结一下(下面的小结可能会有点散)

两个概念和三个术语
  • 区分两个概念:Authentication(认证) 和 Authorization (授权),前者是证明请求者是身份,就像身份证一样,后者是为了获得权限。身份是区别于别人的证明,而权限是证明自己的特权。Authentication为了证明操作的这个人就是他本人,需要提供密码、短信验证码,甚至人脸识别。Authorization 则是不需要在所有的请求都需要验人,是在经过Authorization后得到一个Token,这就是Authorization。就像护照和签证一样。
  • 区分三个概念:编码Base64Encode、签名HMAC、加密RSA。编码是为了更的传输,等同于明文,签名是为了信息不能被篡改,加密是为了不让别人看到是什么信息。
明白一些初衷
  • 使用复杂地HMAC哈希签名方式主要是应对当年没有TLS/SSL加密链路的情况。
  • JWT把 uid 放在 Token中目的是为了去掉状态,但不能让用户修改,所以需要签名。
  • OAuth 1.0区分了两个事,一个是第三方的Client,一个是真正的用户,其先拿Request Token,再换Access Token的方法主要是为了把第三方应用和用户区分开来。
  • 用户的Password是用户自己设置的,复杂度不可控,服务端颁发的Serect会很复杂,但主要目的是为了容易管理,可以随时注销掉。
  • OAuth 协议有比所有认证协议有更为灵活完善的配置,如果使用AppID/AppSecret签名的方式,又需要做到可以有不同的权限和可以随时注销,那么你得开发一个像AWS的IAM这样的账号和密钥对管理的系统。
相关的注意事项
  • 无论是哪种方式,我们都应该遵循HTTP的规范,把认证信息放在 Authorization HTTP 头中。
  • 不要使用GET的方式在URL中放入secret之类的东西,因为很多proxy或gateway的软件会把整个URL记在Access Log文件中。
  • 密钥Secret相当于Password,但他是用来加密的,最好不要在网络上传输,如果要传输,最好使用TLS/SSL的安全链路。
  • HMAC中无论是MD5还是SHA1/SHA2,其计算都是非常快的,RSA的非对称加密是比较耗CPU的,尤其是要加密的字符串很长的时候。
  • 最好不要在程序中hard code 你的 Secret,因为在github上有很多黑客的软件在监视各种Secret,千万小心!这类的东西应该放在你的配置系统或是部署系统中,在程序启动时设置在配置文件或是环境变量中。
  • 使用AppID/AppSecret,还是使用OAuth1.0a,还是OAuth2.0,还是使用JWT,我个人建议使用TLS/SSL下的OAuth 2.0。
  • 密钥是需要被管理的,管理就是可以新增可以撤销,可以设置账户和相关的权限。最好密钥是可以被自动更换的。
  • 认证授权服务器(Authorization Server)和应用服务器(App Server)最好分开。

(全文完)


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 May 09, 2019 at 09:37PM
from 酷 壳 – CoolShell http://bit.ly/2JdiUCG

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【四火】 LeetCode 题目解答—— 第 372 到 415 题

372 到 415 题,同级别的题目反正是越来越难。老规矩,跳过了那些付费题目。

372 35.5% Medium
373 33.3% Medium
374 38.9% Easy
375 37.3% Medium
376 37.0% Medium
377 43.7% Medium
378 48.6% Medium
380 42.1% Medium
381 31.6% Hard
382 48.8% Medium
383 49.4% Easy
384 49.6% Medium
385 31.6% Medium
386 45.0% Medium
387 49.4% Easy
388 38.9% Medium
389 52.8% Easy
390 43.3% Medium
391 28.0% Hard
392 46.3% Medium
393 35.5% Medium
394 44.1% Medium
395 38.0% Medium
396 34.9% Medium
397 31.1% Medium
398 49.1% Medium
399 46.9% Medium
400 30.2% Easy
401 45.1% Easy
402 26.2% Medium
403 35.6% Hard
404 48.7% Easy
405 41.7% Easy
406 59.1% Medium
407 38.9% Hard
409 47.6% Easy
410 41.9% Hard
412 59.0% Easy
413 55.4% Medium
414 28.7% Easy
415 43.2% Easy

Super Pow

【题目】Your task is to calculate ab mod 1337 where a is a positive integer and b is an extremely large positive integer given in the form of an array.

Example 1:

Input: a = 2, b = [3]
Output: 8

Example 2:

Input: a = 2, b = [1,0]
Output: 1024

【解答】

一开始很奇怪,想到最后也不是很清楚为什么这里选了一个 1337 来取余,也许就是随便选一个,要不然整个计算结果太大了。

这里有两个技巧来保证结果不要过大:

  • 自己定义的 pow 方法,在求 pow 的时候,拆成两部分,把 n 尽量等分,然后对 x 也取 1337 的余,再把结果乘起来,最后取 1337 的余。如果不这样拆在最后取余之前可能会得到一个大到溢出的结果。
  • 对于 b,题目已经用某种方式提示了——b 的表示方式就是一个数组,而不是一个数,这就意味着不能把它当做一个简单的数来直接处理。于是一位数一位数处理,从高到低,每算得一次结果,一样通过一个取余操作来保持结果足够小。

其中拆分的技巧在很多题目里都用到:pow(x, n) = pow(x, n/2) * pow(x, n-n/2)

class Solution {
    public int superPow(int a, int[] b) {
        long result = 1;
        for (int bi : b) {
            // from high to low, pow each digit and mod to keep the result small
            result = (pow(result, 10) * pow(a, bi)) % 1337;
        }
        return (int) result;
    }
    
    private long pow(long x, int n) {
        if (n == 0)
            return 1;
        if (n == 1)
            return x % 1337;
        // just a little trick to avoid too large result
        return pow(x % 1337, n / 2) * pow(x % 1337, n - n / 2) % 1337;
    }
}

Find K Pairs with Smallest Sums

【题目】You are given two integer arrays nums1 and nums2 sorted in ascending order and an integer k.

Define a pair (u,v) which consists of one element from the first array and one element from the second array.

Find the k pairs (u1,v1),(u2,v2) …(uk,vk) with the smallest sums.

Example 1:

Input: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
Output: [[1,2],[1,4],[1,6]] 
Explanation: The first 3 pairs are returned from the sequence: 
             [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

Example 2:

Input: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
Output: [1,1],[1,1]
Explanation: The first 2 pairs are returned from the sequence: 
             [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

Example 3:

Input: nums1 = [1,2], nums2 = [3], k = 3
Output: [1,3],[2,3]
Explanation: All possible pairs are returned from the sequence: [1,3],[2,3]

【解答】从 nums1 中取一个数,从 nums2 中取一个数,寻找两数之和的第 k 小。

一开始我尝试把所有数的组合都找出来,放到一个 TreeMap 里面去,取前 k 小。居然过了:

class Solution {
    public List<int[]> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        if (nums1==null || nums2==null || k<0)
            throw new IllegalArgumentException();
        
        List<int[]> res = new ArrayList<>(k);
        if (nums1.length==0 || nums2.length==0)
            return res;
        
        // tree map might not be the best solution - if the arrays are all large and k is small, it's a significant waste to sort and iterate all pairs
        TreeMap<Integer, List<int[]>> map = new TreeMap<>();
        
        for (int l : nums1) {
            for (int r : nums2) {
                List<int[]> list = map.getOrDefault(l+r, new ArrayList<>());
                list.add(new int[]{l, r});
                map.put(l+r, list);
            }
        }
        
        Iterator<Integer> iter = map.keySet().iterator();
        while (iter.hasNext() && k>0) {
            int sum = iter.next();
            List<int[]> list = map.get(sum);
            for (int[] pair : list) {
                res.add(pair);
                k--;
                if (k==0)
                    break;
            }
        }
        
        return res;
    }
}

显然这不是最好的办法。我还想过这样的办法:准备两个指针,一个 p1 在 num1,一个 p2 在 num2,然后每次移动指针的时候都考虑是移动 p1 还是移动 p2 可以得到更小的值,确定了以后,移动并把得到的新结果放到结果集中,直到结果集大小为 k。可是实现的时候发现其实这个方法是错误的,因为指针不能只往前走,在某些 case 下还要往后走。比如 num1=[0,1],num2=[0,2],k=4,用这样的方法最多只能得到大小为 3 的结果集,而实际是可以得到大小为 4 的结果集的。

如果我们以从小到大的方式来构造这个结果集,在取得某一个值 num1[i] 和 num2[j] 的时候,下一个是什么?如果需要确定下一个,需要考察什么?

  • 想到这里忽然又了头绪:
  • 如果 k 小于 nums1 的长度,我应该建立一个大小大致为 k 的堆;如果 k 大于 nums1 的长度,则建立一个大小为 nums1.length 的堆,无论哪种,这个堆的大小为 len。
  • 初始只需要考虑 len 个元素,且表示的是对于 nums1 的下标从 0 到 len-1 的情况下,nums2 的下标都为 0 的情况。
  • 每次 poll 出堆顶,即当前最小值 (nums1[i] + nums2[j]) 了之后,要找下一个,就把 (nums1[i]+nums2[j+1]) 放入堆。
  • 反复如上操作,直到取得了 k 个元素,或者元素取完了。
class Item implements Comparable<Item>{
    private int[] nums1;
    private int[] nums2;
    public int i;
    public int j;
    public Item(int i, int j, int[] nums1, int[] nums2) {
        this.nums1 = nums1;
        this.nums2 = nums2;
        this.i = i;
        this.j = j;
    }
    
    @Override
    public int compareTo(Item that) {
        return (nums1[i]+nums2[j]) - (that.nums1[that.i]+that.nums2[that.j]);
    }
}
class Solution {
    public List<int[]> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        if (nums1==null || nums2==null || k<0)
            throw new IllegalArgumentException();
        
        List<int[]> res = new ArrayList<>(k);
        if (nums1.length==0 || nums2.length==0)
            return res;
        
        int len = Math.min(nums1.length, k);
        PriorityQueue<Item> queue = new PriorityQueue<>(len);
        for (int i=0; i<len; i++) {
            queue.add(new Item(i, 0, nums1, nums2));
        }
        
        while(res.size()<k && !queue.isEmpty()) {
            Item item = queue.poll();
            res.add(new int[] {nums1[item.i], nums2[item.j]});
            if (item.j+1<nums2.length)
                queue.add(new Item(item.i, item.j+1, nums1, nums2));
        }
        
        return res;
    }
}

Guess Number Higher or Lower

【题目】We are playing the Guess Game. The game is as follows:

I pick a number from 1 to n. You have to guess which number I picked.

Every time you guess wrong, I’ll tell you whether the number is higher or lower.

You call a pre-defined API guess(int num) which returns 3 possible results (-11, or 0):

-1 : My number is lower
 1 : My number is higher
 0 : Congrats! You got it!

Example :

Input: n = 10, pick = 6
Output: 6

【解答】猜数。二分查找,当心溢出:

/* The guess API is defined in the parent class GuessGame.
   @param num, your guess
   @return -1 if my number is lower, 1 if my number is higher, otherwise return 0
      int guess(int num); */

public class Solution extends GuessGame {
    public int guessNumber(int n) {
        long start=1, end=n;
        while(true) {
            int mid = (int)((start+end)/2);
            int res = guess(mid);
            if (res==0)
                return mid;
            else if (res<0)
                end = mid-1;
            else
                start = mid+1;
        }
    }
}

Guess Number Higher or Lower II

【题目】We are playing the Guess Game. The game is as follows:

I pick a number from 1 to n. You have to guess which number I picked.

Every time you guess wrong, I’ll tell you whether the number I picked is higher or lower.

However, when you guess a particular number x, and you guess wrong, you pay $x. You win the game when you guess the number I picked.

Example:

n = 10, I pick 8.

First round:  You guess 5, I tell you that it's higher. You pay $5.
Second round: You guess 7, I tell you that it's higher. You pay $7.
Third round:  You guess 9, I tell you that it's lower. You pay $9.

Game over. 8 is the number I picked.

You end up paying $5 + $7 + $9 = $21.

Given a particular n ≥ 1, find out how much money you need to have to guarantee a win.

【解答】题目要求“guarantee a win”,这里面涉及不同策略的问题。但是,我们可以评估所有的策略,并取可以保证胜利的情况下最小的 money 数量。考虑三种情况分别递归计算,并且把中间计算结果缓存到一个二维数组中:猜中,猜少了,以及猜多了。

class Solution {
    public int getMoneyAmount(int n) {
        // cost[i][j] means to know the range is [i, j], what will the min cost be
        Integer[][] cost = new Integer[n+1][n+1];
        return cal(1, n, cost);
    }
    
    private int cal(int from, int to, Integer[][] cost) {
        if (from>=to)
            return 0;
        
        if (cost[from][to]!=null)
            return cost[from][to];
        
        int min = Integer.MAX_VALUE;
        for (int i=from; i<=to; i++) {
            // for each number i to guess, pick the max in all possible costs
            
            // case 1: target == i
            int total = i;
            // case 2: target < i
            total = Math.max(total, cal(from, i-1, cost)+i);
            // case 3: target > i
            total = Math.max(total, cal(i+1, to, cost)+i);
            
            // for all the guess strategies, pick the one with min cost
            min = Math.min(total, min);
        }
        
        cost[from][to] = min;
        return min;
    }
}

Wiggle Subsequence

【题目】A sequence of numbers is called a wiggle sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a wiggle sequence.

For example, [1,7,4,9,2,5] is a wiggle sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast, [1,4,7,2,5]and [1,7,4,5,5] are not wiggle sequences, the first because its first two differences are positive and the second because its last difference is zero.

Given a sequence of integers, return the length of the longest subsequence that is a wiggle sequence. A subsequence is obtained by deleting some number of elements (eventually, also zero) from the original sequence, leaving the remaining elements in their original order.

Example 1:

Input: [1,7,4,9,2,5]
Output: 6
Explanation: The entire sequence is a wiggle sequence.

Example 2:

Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
Explanation: There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].

Example 3:

Input: [1,2,3,4,5,6,7,8,9]
Output: 2

Follow up:
Can you do it in O(n) time?

【解答】要形成这种 wiggle 的序列,考虑两种情况,一种是先朝上(递增),第二种是县朝下(递减)的。

定义一个 wiggleMaxLength 重载方法,除了接受 nums 数组以外,还接受一个 start 表示从某一个下标开始,increased 表示上次是递增还是递减。方法返回从 start 开始的最长 wiggle 串的长度。

在一开始的时候,可以认为“上次”既可以是递增的,也可以是递减的,因此两种情况都要算。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums==null || nums.length==0) return 0;
        
        int inc = wiggleMaxLength(nums, 0, true);
        int dec = wiggleMaxLength(nums, 0, false);
        
        return Math.max(inc, dec);
    }
    
    private int wiggleMaxLength(int[] nums, int start, boolean increased) {
        if (start==nums.length)
            return 0;
        
        if (start==0) {
            return 1 + wiggleMaxLength(nums, 1, increased);
        } else {
            boolean monotonic = increased ? nums[start-1]<=nums[start] : nums[start-1]>=nums[start];
            if (monotonic) {
                return wiggleMaxLength(nums, start+1, increased);
            } else {
                return 1 + wiggleMaxLength(nums, start+1, !increased);
            }
        }
    }
}

Combination Sum IV

【题目】Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.

Example:

nums = [1, 2, 3]
target = 4

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.

Follow up:
What if negative numbers are allowed in the given array?
How does it change the problem?
What limitation we need to add to the question to allow negative numbers?

【解答】最终是求能得到特定值的组合个数,那么就可以使用一个 map 来存放这样的结果,key 是特定值,value 是组合个数。使用递归来遍历所有可能求解,由于有了前面的 map,重复计算可以避免。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        if (nums==null || nums.length==0 || target<=0)
            return 0;
        
        Map<Integer, Integer> dp = new HashMap<>();
        return com(nums, target, dp);
    }
    
    private int com(int[] nums, int target, Map<Integer, Integer> dp) {
        if (target<=0)
            return 0;
        
        if (dp.containsKey(target))
            return dp.get(target);
        
        int total = 0;
        for (int num : nums) {
            if (target==num) {
                total++;
                continue;
            } else if (num>target) {
                continue;
            } else {
                total += com(nums, target-num, dp);
            }
        }
        
        dp.put(target, total);
        return total;
    }
}

Kth Smallest Element in a Sorted Matrix

【题目】Given a n x n matrix where each of the rows and columns are sorted in ascending order, find the kth smallest element in the matrix.

Note that it is the kth smallest element in the sorted order, not the kth distinct element.

Example:

matrix = [
   [ 1,  5,  9],
   [10, 11, 13],
   [12, 13, 15]
],
k = 8,

return 13.

Note: 
You may assume k is always valid, 1 ≤ k ≤ n2.

【解答】从上到下和从左到右都是逐渐增大的。所有从左上角开始一层一层往右侧和下侧蔓延,通过一个 visited 数组或者一个 dedup set 来记录已经访问过的元素,同时使用一个堆来存放着最多 k 个的元素。每次蔓延实际都是检查当前边界元素的右侧和下方两个元素,这个蔓延的行为必须持续 k 次,这样理论上所有可能出现的 k 小都已经包括在内了,而每次操作因为带有一次 poll,因此 k 次循环之后 poll 到的也就是第 k 小的元素。

class Solution {
    public int kthSmallest(int[][] matrix, int k) {
        Item first = new Item(0, 0, matrix[0][0]);
        
        PriorityQueue<Item> heap = new PriorityQueue<>();
        heap.add(first);
        
        Set<Item> dedup = new HashSet<>();
        dedup.add(first);
        
        Item item = null;
        for (int p=0; p<k; p++) {
            item = heap.poll();
            int i = item.i;
            int j = item.j;
            
            if (item.i<matrix.length-1) {
                Item newItem = new Item(i+1, j, matrix[i+1][j]);
                if (dedup.add(newItem))
                    heap.add(newItem);
            }
            
            if (item.j<matrix[0].length-1) {
                Item newItem = new Item(i, j+1, matrix[i][j+1]);
                if (dedup.add(newItem))
                    heap.add(newItem);
            }
        }
        
        return item.val;
    }
}

class Item implements Comparable<Item>{
    int i;
    int j;
    int val;
    
    public Item(int i, int j, int val) {
        this.i = i;
        this.j = j;
        this.val = val;
    }
    
    @Override
    public int hashCode() {
        return i ^ j;
    }
    
    @Override
    public boolean equals(Object other) {
        Item that = ((Item) other);
        return this.i==that.i && this.j==that.j;
    }
    
    @Override
    public int compareTo(Item other) {
        return this.val - other.val;
    }
}

Insert Delete GetRandom O(1)

【题目】Design a data structure that supports all following operations in average O(1) time.

  1. insert(val): Inserts an item val to the set if not already present.
  2. remove(val): Removes an item val from the set if present.
  3. getRandom: Returns a random element from current set of elements. Each element must have the same probability of being returned.

Example:

// Init an empty set.
RandomizedSet randomSet = new RandomizedSet();

// Inserts 1 to the set. Returns true as 1 was inserted successfully.
randomSet.insert(1);

// Returns false as 2 does not exist in the set.
randomSet.remove(2);

// Inserts 2 to the set, returns true. Set now contains [1,2].
randomSet.insert(2);

// getRandom should return either 1 or 2 randomly.
randomSet.getRandom();

// Removes 1 from the set, returns true. Set now contains [2].
randomSet.remove(1);

// 2 was already in the set, so return false.
randomSet.insert(2);

// Since 2 is the only number in the set, getRandom always return 2.
randomSet.getRandom();

【解答】要能在常数时间内添加和删除任意数,不难想到使用 HashMap。要能够返回随机数,不难想到常规的 random 方法,给一个上限,每次调用 nextInt 方法。联合起来,要使得取得的随机数和里面的数的集合关联起来,并且这个上限每次在常数时间内调整,考虑使用双向 map:一个 map 是从 0~x 的连续整数映射到实际的数集合;另一个则是反过来。

class RandomizedSet {
    private Map<Integer, Integer> indexToValue = new HashMap<>();
    private Map<Integer, Integer> valueToIndex = new HashMap<>();
    private Random random = new Random(System.currentTimeMillis());

    /** Initialize your data structure here. */
    public RandomizedSet() {
    }
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    public boolean insert(int val) {
        Integer index = valueToIndex.get(val);
        if (index!=null)
            return false;
        
        // it's a new number, so the new index should be valueToIndex.size()
        valueToIndex.put(val, valueToIndex.size());
        indexToValue.put(indexToValue.size(), val);

        return true;
    }
    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    public boolean remove(int val) {
        Integer index = valueToIndex.get(val);
        if (index==null)
            return false;
        
        // the target is removed but there could be a hole in the middle
        valueToIndex.remove(val);
        indexToValue.remove(index);
        
        // if the end entry was removed, no adjustment needed
        if (index==indexToValue.size())
            return true;

        // else, move the end entry to fill the hole
        int valueToAdjust = indexToValue.get(indexToValue.size());
        int indexToAdjust = valueToIndex.get(valueToAdjust);

        valueToIndex.remove(valueToAdjust);
        indexToValue.remove(indexToAdjust);

        valueToIndex.put(valueToAdjust, index);
        indexToValue.put(index, valueToAdjust);
        
        return true;
    }
    
    /** Get a random element from the set. */
    public int getRandom() {
        int index = random.nextInt(indexToValue.size());
        return indexToValue.get(index);
    }
}

/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

Insert Delete GetRandom O(1) – Duplicates allowed

【题目】Design a data structure that supports all following operations in average O(1) time.

Note: Duplicate elements are allowed.

  1. insert(val): Inserts an item val to the collection.
  2. remove(val): Removes an item val from the collection if present.
  3. getRandom: Returns a random element from current collection of elements. The probability of each element being returned is linearly related to the number of same value the collection contains.

Example:

// Init an empty collection.
RandomizedCollection collection = new RandomizedCollection();

// Inserts 1 to the collection. Returns true as the collection did not contain 1.
collection.insert(1);

// Inserts another 1 to the collection. Returns false as the collection contained 1. Collection now contains [1,1].
collection.insert(1);

// Inserts 2 to the collection, returns true. Collection now contains [1,1,2].
collection.insert(2);

// getRandom should return 1 with the probability 2/3, and returns 2 with the probability 1/3.
collection.getRandom();

// Removes 1 from the collection, returns true. Collection now contains [1,2].
collection.remove(1);

// getRandom should return 1 and 2 both equally likely.
collection.getRandom();

【解答】思路和前面那道还是类似,但是因为这次允许有重复,所以这次在这两个 map 中有一个需要做出一定的改动,引入 index set。

class RandomizedCollection {
    private Map<Integer, Integer> indexToValue = new HashMap<>();
    private Map<Integer, Set<Integer>> valueToIndices = new HashMap<>();
    private Random random = new Random(System.currentTimeMillis());
    
    /** Initialize your data structure here. */
    public RandomizedCollection() {
        
    }
    
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    public boolean insert(int val) {
        int newIndex = indexToValue.size();
        indexToValue.put(newIndex, val);
        
        Set<Integer> indices = valueToIndices.getOrDefault(val, new HashSet<>());
        indices.add(newIndex);
        valueToIndices.put(val, indices);
        
        if (indices.size()==1) {
            return true;
        } else {
            return false;
        }
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    public boolean remove(int val) {
        Set<Integer> indices = valueToIndices.getOrDefault(val, new HashSet<>());
        if (indices.isEmpty()) {
            return false;
        }
        
        int indexRemoved = indices.iterator().next();
        indices.remove(indexRemoved);
        indexToValue.remove(indexRemoved);
        
        // if the end entry was removed, no adjustment needed
        if (indexRemoved==indexToValue.size()) {
            return true;
        }
        
        // else, move the end entry to fill the hole
        int indexToAdjust = indexToValue.size();
        int valueToAdjust = indexToValue.get(indexToAdjust);
        indexToValue.remove(indexToAdjust);
        indexToValue.put(indexRemoved, valueToAdjust);
        valueToIndices.get(valueToAdjust).remove(indexToAdjust);
        valueToIndices.get(valueToAdjust).add(indexRemoved);
        
        return true;
    }
    
    /** Get a random element from the collection. */
    public int getRandom() {
        int index = random.nextInt(indexToValue.size());
        return indexToValue.get(index);
    }
}

/**
 * Your RandomizedCollection object will be instantiated and called as such:
 * RandomizedCollection obj = new RandomizedCollection();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

Linked List Random Node

【题目】Given a singly linked list, return a random node’s value from the linked list. Each node must have the same probability of being chosen.

Follow up:
What if the linked list is extremely large and its length is unknown to you? Could you solve this efficiently without using extra space?

Example:

// Init a singly linked list [1,2,3].
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
Solution solution = new Solution(head);

// getRandom() should return either 1, 2, or 3 randomly. Each element should have equal probability of returning.
solution.getRandom();

【解答】本来一个思路是引入一个 map 来很快地访问到这个 list 上面的所有节点,但要不使用额外空间,就只有牺牲每次访问的时候的效率了:记录下 list 的长度,random 的时候生成要从 head 开始往前走的步数,从而找到要返回的值所在的节点。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    private Random random = new Random(System.currentTimeMillis());
    private int size;
    private ListNode head;
    
    /** @param head The linked list's head.
        Note that the head is guaranteed to be not null, so it contains at least one node. */
    public Solution(ListNode head) {
        this.head = head;
        while (head!=null) {
            this.size++;
            head = head.next;
        }
    }
    
    /** Returns a random node's value. */
    public int getRandom() {
        int step = random.nextInt(size);
        ListNode cur = head;
        for (int i=0; i<step; i++) {
            cur = cur.next;
        }
        return cur.val;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(head);
 * int param_1 = obj.getRandom();
 */

Ransom Note

【题目】Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.

Each letter in the magazine string can only be used once in your ransom note.

Note:
You may assume that both strings contain only lowercase letters.

canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true

【解答】没太多可说的,需要记录下每个字母出现的次数。

public class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        Map<Character, Integer> map = new HashMap<>();
        for (int i=0; i<magazine.length(); i++) {
            char ch = magazine.charAt(i);
            if (!map.containsKey(ch)) {
                map.put(ch, 0);
            }
            
            map.put(ch, map.get(ch)+1);
        }
        
        for (int i=0; i<ransomNote.length(); i++) {
            char ch = ransomNote.charAt(i);
            Integer count = map.get(ch);
            
            if (count==null || count==0)
                return false;
            
            map.put(ch, map.get(ch)-1);
        }
        
        return true;
    }
}

Shuffle an Array

【题目】Shuffle a set of numbers without duplicates.

Example:

// Init an array with set 1, 2, and 3.
int[] nums = {1,2,3};
Solution solution = new Solution(nums);

// Shuffle the array [1,2,3] and return its result. Any permutation of [1,2,3] must equally likely to be returned.
solution.shuffle();

// Resets the array back to its original configuration [1,2,3].
solution.reset();

// Returns the random shuffling of array [1,2,3].
solution.shuffle();

【解答】要求洗牌。每次都产生一个最大值为当前待选牌的数量的随机数作为下标,去这堆牌里面取,直到取完。

class Solution {
    private Random r = new Random();
    private int[] nums = null;
    
    public Solution(int[] nums) {
        this.nums = nums;
    }
    
    /** Resets the array to its original configuration and return it. */
    public int[] reset() {
        return this.nums;
    }
    
    /** Returns a random shuffling of the array. */
    public int[] shuffle() {
        List<Integer> list = new ArrayList<>(nums.length);
        for (int num : nums) {
            list.add(num);
        }
        
        int[] res = new int[nums.length];
        for (int i=nums.length-1; i>=0; i--) {
            int idx = r.nextInt(i+1);
            res[i] = list.remove(idx);
        }
        
        return res;
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int[] param_1 = obj.reset();
 * int[] param_2 = obj.shuffle();
 */

Mini Parser

【题目】Given a nested list of integers represented as a string, implement a parser to deserialize it.

Each element is either an integer, or a list — whose elements may also be integers or other lists.

Note: You may assume that the string is well-formed:

  • String is non-empty.
  • String does not contain white spaces.
  • String contains only digits 0-9[- ,].

Example 1:

Given s = "324",

You should return a NestedInteger object which contains a single integer 324.

Example 2:

Given s = "[123,[456,[789]]]",

Return a NestedInteger object containing a nested list with 2 elements:

1. An integer containing value 123.
2. A nested list containing two elements:
    i.  An integer containing value 456.
    ii. A nested list with one element:
         a. An integer containing value 789.

【解答】这种题目可以称之为“考操作”的题目,大致思路应该说没有什么难的,从左到右遍历,使用一个 stack 来处理括号。但是有一些细节需要注意,所以总的来说就是考操作:

  • 遇到数字要读取到底,负号的可能要考虑到;
  • 读完数字以后要检查移动后的下标是否还有意义(在界内);
  • 最后再处理进出栈。
/**
 * // This is the interface that allows for creating nested lists.
 * // You should not implement it, or speculate about its implementation
 * public interface NestedInteger {
 *     // Constructor initializes an empty nested list.
 *     public NestedInteger();
 *
 *     // Constructor initializes a single integer.
 *     public NestedInteger(int value);
 *
 *     // @return true if this NestedInteger holds a single integer, rather than a nested list.
 *     public boolean isInteger();
 *
 *     // @return the single integer that this NestedInteger holds, if it holds a single integer
 *     // Return null if this NestedInteger holds a nested list
 *     public Integer getInteger();
 *
 *     // Set this NestedInteger to hold a single integer.
 *     public void setInteger(int value);
 *
 *     // Set this NestedInteger to hold a nested list and adds a nested integer to it.
 *     public void add(NestedInteger ni);
 *
 *     // @return the nested list that this NestedInteger holds, if it holds a nested list
 *     // Return null if this NestedInteger holds a single integer
 *     public List<NestedInteger> getList();
 * }
 */
class Solution {
    public NestedInteger deserialize(String s) {
        NestedInteger root = null;
        NestedInteger cur = root;
        Stack<NestedInteger> stack = new Stack<>();
        
        int i = 0;
        while (i<s.length()) {
            // number
            String num = "";
            while (i<s.length() && (s.charAt(i)>='0' && s.charAt(i)<='9' || s.charAt(i)=='-')) {
                num += s.charAt(i);
                i++;
            }
            
            if (num.length()!=0) {
                NestedInteger numNI = new NestedInteger(Integer.parseInt(num));
                if (cur!=null) {
                    cur.add(numNI);
                } else {
                    cur = numNI;
                    root = cur;
                }
            }
            
            if (s.length()==i)
                break;
            
            char ch = s.charAt(i);
            if (ch=='[') {
                NestedInteger arrNI = new NestedInteger();
                if (cur!=null) {
                    cur.add(arrNI);
                    stack.push(cur);
                } else {
                    root = arrNI;
                }
                cur = arrNI;
            } else if (ch==']') {
                if (!stack.isEmpty())
                    cur = stack.pop();
            }
            
            i++;
        }
        
        return root;
    }
}

Lexicographical Numbers

【题目】Given an integer n, return 1 – n in lexicographical order.

For example, given 13, return: [1,10,11,12,13,2,3,4,5,6,7,8,9].

Please optimize your algorithm to use less time and space. The input size may be as large as 5,000,000.

【解答】要按照字典序找前 n 个数。开始想了想,似乎没有好的解法,就想排一排序取前面 n 个吧,果然超时了:

class Solution {
    // Time Limit ExceededMore Details 
    // Last executed input: 23489
    public List<Integer> lexicalOrder(int n) {
        List<Integer> list = new ArrayList<>(n);
        for (int i=1; i<=n; i++) {
            list.add(i);
        }
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer l, Integer r) {
                String ls = l + "";
                String rs = r + "";
                return ls.compareTo(rs);
            }
        });
        return list;
    }
}

在后来我猜逐渐理解,类似这种字典序的问题,有一个常用的思路,就是构造,而非穷举。有时候需要从左往右一位一位构造,这个构造可以表现为一棵 x 叉的树,有时候需要从这棵树的根开始一层一层往下计数。而这道题事实上还没那么麻烦,只需要从小到大尝试构造符合条件的数,操作 n 次即可。

class Solution {
    public List<Integer> lexicalOrder(int n) {
        int cur = 1;
        List<Integer> res = new ArrayList<>();
        for (int i=1; i<=n; i++) {
            res.add(cur);
            if (cur*10l<=n) { // avoid overflow
                cur = cur*10;
            } else if (cur%10!=9 && cur<n) { // can't add more digits, so increase the last digit
                cur++;
            } else { // can't add more digits, and can't change the last digit, have to shorten the number
                while ((cur/10)%10==9) { // if the second last digit is 9, remove last digit
                    cur/=10;
                }
                cur = cur/10 + 1; // remove the last digit and find the next number with the same length
            }
        }
        
        return res;
    }
}

First Unique Character in a String

【题目】Given a string, find the first non-repeating character in it and return it’s index. If it doesn’t exist, return -1.

Examples:

s = "leetcode"
return 0.

s = "loveleetcode",
return 2.

Note: You may assume the string contain only lowercase letters.

【解答】没有太多可说的,使用一个 map,key 是字母,value 是 index,该字母第二次及以后出现就置为特殊值-1:

public class Solution {
    public int firstUniqChar(String s) {
        Map<Character, Integer> map =  new HashMap<>(26);
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            if (!map.containsKey(ch)) {
                map.put(ch, i);
            } else {
                map.put(ch, -1);
            }
        }
        
        Integer min = -1;
        for (Integer pos : map.values()) {
            if (pos==null || pos.intValue()==-1)
                continue;
            
            if (min==-1 || pos.intValue()<min)
                min = pos.intValue();
        }
        
        return min;
    }
}

Longest Absolute File Path

【题目】Suppose we abstract our file system by a string in the following manner:

The string "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext" represents:

dir
    subdir1
    subdir2
        file.ext

The directory dir contains an empty sub-directory subdir1 and a sub-directory subdir2 containing a file file.ext.

The string "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"represents:

dir
    subdir1
        file1.ext
        subsubdir1
    subdir2
        subsubdir2
            file2.ext

The directory dir contains two sub-directories subdir1 and subdir2subdir1contains a file file1.ext and an empty second-level sub-directory subsubdir1subdir2 contains a second-level sub-directory subsubdir2 containing a file file2.ext.

We are interested in finding the longest (number of characters) absolute path to a file within our file system. For example, in the second example above, the longest absolute path is "dir/subdir2/subsubdir2/file2.ext", and its length is 32 (not including the double quotes).

Given a string representing the file system in the above format, return the length of the longest absolute path to file in the abstracted file system. If there is no file in the system, return 0.

Note:

  • The name of a file contains at least a . and an extension.
  • The name of a directory or sub-directory will not contain a ..

Time complexity required: O(n) where n is the size of the input string.

Notice that a/aa/aaa/file1.txt is not the longest file path, if there is another path aaaaaaaaaaaaaaaaaaaaa/sth.png.

【解答】寻找最长的路径。常规题目,从思考到写不困呢,但是考虑周全和思路清晰更重要。从左往右扫描:

  • 一开始就在 input 的最后增加一个换行,目的是简化逻辑,不需要在循环跳出以后做额外的处理;
  • 创建一个 nodes 的 list 用来扮演 stack 的角色,记录当前的路径;
  • tabs 是用来记录当前行的节点深度的。
class Solution {
    public int lengthLongestPath(String input) {
        if (input==null)
            return 0;
        
        input += '\n';
        int longest = 0;
        int tabs = 0;
        String current = "";
        List<String> nodes = new ArrayList<>();
        for (int i=0; i<input.length(); i++) {
            char ch = input.charAt(i);
            if (ch=='\n') {
                tabs = 0;
                String path = "";
                if (current.contains(".")) {
                    for (String node : nodes) {
                        path += (node + '/');
                    }
                    path += current;
                    
                    if (path.length()>longest)
                        longest = path.length();
                } else {
                    nodes.add(current);
                }
                current = "";
            } else if (ch=='\t') {
                tabs++;
            } else {
                if (nodes.size()>tabs) {
                    for (int j=tabs; j<nodes.size(); j++) {
                        nodes.remove(j);
                    }
                }
                
                current += ch;
            }
        }
        
        return longest;
    }
}

Find the Difference

【题目】Given two strings s and t which consist of only lowercase letters.

String t is generated by random shuffling string s and then add one more letter at a random position.

Find the letter that was added in t.

Example:

Input:
s = "abcd"
t = "abcde"

Output:
e

Explanation:
'e' is the letter that was added.

【解答】使用 map 或者长度为 26 的数组来记录字符出现的次数。

public class Solution {
    public char findTheDifference(String s, String t) {
        int[] chars = new int[26];
        for (int i=0; i<s.length(); i++) {
            int pos = s.charAt(i) - 'a';
            chars[pos]++;
        }
        
        for (int i=0; i<t.length(); i++) {
            int pos = t.charAt(i) - 'a';
            chars[pos]--;
        }
        
        for (int i=0; i<chars.length; i++) {
            if (chars[i] < 0)
                return (char)('a' + i);
        }
        
        return (char)0; // unreachable
    }
}

Elimination Game

【题目】There is a list of sorted integers from 1 to n. Starting from left to right, remove the first number and every other number afterward until you reach the end of the list.

Repeat the previous step again, but this time from right to left, remove the right most number and every other number from the remaining numbers.

We keep repeating the steps again, alternating left to right and right to left, until a single number remains.

Find the last number that remains starting with a list of length n.

Example:

Input:
n = 9,
1 2 3 4 5 6 7 8 9
2 4 6 8
2 6
6

Output:
6

【解答】这一类题看起来更像数学题,也许有偏向纯数学而非计算机算法的方法来解。

最显然的办法自然是按照题目提示的规则操作一遍,不要使用 ArrayList 也不要使用普通的 LinkedList,可以考虑使用双向链表,因为这里涉及到从前往后和从后往前的双向操作,但依然,在 n 比较大的时候需要创建一大堆元素,显得效率很低。

再仔细想想,为什么要创建那么长一个链表,再逐步删掉直到只剩一个元素?能不能在操作基本不变的基础上,假想一个链表?下面的做法就是这样:

  • 用 step 记录当前的步长,用 leftOrRight 记录当前的方向,rest 记录剩余元素的个数,first 记录当前剩余数中第一个的位置。
  • 在 rest 为偶数的时候,first 的位置不需要调整,但是如果是奇数,first 就要增加一个步长。
  • 每一遍循环都将 rest 的个数减半。

上面这个规则可以通过创建两个实际例子,一个偶数个数,一个奇数个数,来逐步摸索出来。比如下面上面这个 rest 减半的操作,需要考察实际当 rest 是偶数的时候,以及是奇数的时候两种情况,于是发现二者都可以简单地除以 2,不需要分类处理。

从一个实际数组,抽象到无数组,而只有几个关键参数的形式(因为这几个关键参数就可以完全表示出每次操作后余下的序列)——这是这类题最难的部分。

class Solution {
    public int lastRemaining(int n) {
        int rest = n;
        int step = 1;
        int first = 1; // the first number in the rest number series
        boolean leftOrRight = true;
        while (rest>1) {
            if (leftOrRight) {
                first += step;
            } else {
                if (rest%2==0) // even
                    ; // no change
                else // odd
                    first += step;
            }
            
            step *= 2;
            rest /= 2; // doesn't care rest is even or odd
            leftOrRight = !leftOrRight;
        }
        
        return first;
    }
}

稍微改进一下,本质没有区别:

class Solution {
    public int lastRemaining(int n) {
        int rest = n;
        int step = 1;
        int first = 1; // the first number in the rest number series
        boolean leftOrRight = true;
        while (rest>1) {
            if (leftOrRight || rest%2==1)
                first += step;
            
            step *= 2;
            rest /= 2; // doesn't care rest is even or odd
            leftOrRight = !leftOrRight;
        }
        
        return first;
    }
}

Perfect Rectangle

【题目】Given N axis-aligned rectangles where N > 0, determine if they all together form an exact cover of a rectangular region.

Each rectangle is represented as a bottom-left point and a top-right point. For example, a unit square is represented as [1,1,2,2]. (coordinate of bottom-left point is (1, 1) and top-right point is (2, 2)).

Example 1:

rectangles = [
  [1,1,3,3],
  [3,1,4,2],
  [3,2,4,4],
  [1,3,2,4],
  [2,3,3,4]
]

Return true. All 5 rectangles together form an exact cover of a rectangular region.

Example 2:

rectangles = [
  [1,1,2,3],
  [1,3,2,4],
  [3,1,4,2],
  [3,2,4,4]
]

Return false. Because there is a gap between the two rectangular regions.

Example 3:

rectangles = [
  [1,1,3,3],
  [3,1,4,2],
  [1,3,2,4],
  [3,2,4,4]
]

Return false. Because there is a gap in the top center.

Example 4:

rectangles = [
  [1,1,3,3],
  [3,1,4,2],
  [1,3,2,4],
  [2,2,4,4]
]

Return false. Because two of the rectangles overlap with each other.

【解答】要看其中的矩形们能否联合起来恰好形成一个矩形,没有重叠,没有遗漏。

最直接的做法,我引入一个标记是否覆盖过的图,第一次访问的时候就标记一下,第二次就返回失败,标记完毕以后再遍历一下,如果发现还有未覆盖的就返回失败,否则返回成功。思路简单,但是无论是时间复杂度还是空间复杂度都明显超出要求:

class Solution {
    private Integer rowMin = null;
    private Integer rowMax = null;
    private Integer colMin = null;
    private Integer colMax = null;
    private Map<Integer, Set<Integer>> graph = new HashMap<>();
    
    public boolean isRectangleCover(int[][] rectangles) {
        for (int[] row : rectangles) {
            for (int i=row[0]; i<row[2]; i++) {
                for (int j=row[1]; j<row[3]; j++) {
                    if (!mark(i, j))
                        return false;
                }
            }
        }
        
        return check();
    }
    
    private boolean mark(int i, int j) {
        if (rowMin==null || rowMin>i) rowMin = i;
        if (rowMax==null || rowMax<i) rowMax = i;
        if (colMin==null || colMin>j) colMin = j;
        if (colMax==null || colMax<j) colMax = j;
        
        Set<Integer> set = graph.getOrDefault(i, new HashSet<>());
        if (!set.add(j))
            return false;
        
        graph.put(i, set);
        return true;
    }
    
    private boolean check() {
        for (int i=rowMin; i<=rowMax; i++) {
            for (int j=colMin; j<=colMax; j++) {
                Set<Integer> set = graph.get(i);
                if (set==null)
                    return false;
                if (!set.contains(j))
                    return false;
            }
        }
        
        return true;
    }
}

也许还有一个类似方向的思路,就是用 TreeMap 来做,像是线段树一样,存放起始点和终结点,好处是省掉实际实打实的这个是否覆盖过的图,但问题是这会让代码很复杂,因为这个覆盖情况的判别不是一维的,而是二维的。

以下方法来自 讨论区 ,我认为非常巧妙。

如下两个条件满足的情况下,可以推出“Perfect Rectangle”的结论:

  1. 把所有子矩形的面积加起来,恰好等于四个边界点所围成的最大矩形面积
  2. 除去位于这个最大矩形的边上的部分,所有子矩形的边,都为偶数条

以上两个结论,如果只满足第一个,即只具备面积相等的条件,那么“最大矩形”内部同时还可能包含重叠和空缺;如果只满足第二个,可能出现的反例是,存在子矩形重叠且重叠导致的重复边为偶数。但是当两个结论都成立的时候,它们就构成了“Perfect Rectangle”的充分条件。

上面第一点要容易想到,因而第二点是题目解决最关键的部分。上面这两个结论,严格地可以通过数学方法推导出来。换言之,复杂度通过数学方法减小了。

具体实现:

  • 循环所有子矩形,累加所有面积;
  • 在判断两个点形成的线段是否有重复的方法上面,考虑每一条边线,通过用空格连接每一维实际的坐标,然后得到的字符串利用 HashSet 判别去重(算是取了个巧,比自定义一个表示线段的类要稍微简便一些);
  • 寻找到所有子矩形的最左点、最右点、最上点和最下点;
  • 上面的一通去重操作以后,应该只剩下恰好构成最终覆盖矩形的四个顶点了,其它的点应该都已经去重过程中湮灭掉了,于是在循环结束后判断一下这个 set 里面是否真的只剩它们。

复杂度上看,时间复杂度,避免了每一点的考察,只需要考察所给的图形,通常图形的个数和覆盖的点的个数差别巨大;空间复杂度,避免了建立整体矩形大小的标记数组,都获得了简化。

class Solution {
    public boolean isRectangleCover(int[][] rectangles) {
        if (rectangles.length == 0 || rectangles[0].length == 0) return false;

        int x1 = Integer.MAX_VALUE;
        int x2 = Integer.MIN_VALUE;
        int y1 = Integer.MAX_VALUE;
        int y2 = Integer.MIN_VALUE;
        
        HashSet<String> set = new HashSet<String>();
        int area = 0;
        
        for (int[] rect : rectangles) {
            x1 = Math.min(rect[0], x1);
            y1 = Math.min(rect[1], y1);
            x2 = Math.max(rect[2], x2);
            y2 = Math.max(rect[3], y2);
            
            area += (rect[2] - rect[0]) * (rect[3] - rect[1]);
            
            String s1 = rect[0] + " " + rect[1];
            String s2 = rect[0] + " " + rect[3];
            String s3 = rect[2] + " " + rect[3];
            String s4 = rect[2] + " " + rect[1];
            
            if (!set.add(s1)) set.remove(s1);
            if (!set.add(s2)) set.remove(s2);
            if (!set.add(s3)) set.remove(s3);
            if (!set.add(s4)) set.remove(s4);
        }
        
        if (!set.contains(x1 + " " + y1) || !set.contains(x1 + " " + y2) || !set.contains(x2 + " " + y1) || !set.contains(x2 + " " + y2) || set.size() != 4) return false;
        
        return area == (x2-x1) * (y2-y1);
    }
}

Is Subsequence

【题目】Given a string s and a string t, check if s is subsequence of t.

You may assume that there is only lower case English letters in both s and tt is potentially a very long (length ~= 500,000) string, and s is a short string (<=100).

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ace" is a subsequence of "abcde"while "aec" is not).

Example 1:
s = "abc"t = "ahbgdc"

Return true.

Example 2:
s = "axc"t = "ahbgdc"

Return false.

Follow up:
If there are lots of incoming S, say S1, S2, … , Sk where k >= 1B, and you want to check one by one to see if T has its subsequence. In this scenario, how would you change your code?

【解答】要判断 s 是不是 t 的子序列,我的第一反应是想大概是一道动态规划的题目,判断 s 的某一个字母 s[i] 是否在以 t[j..] 这个子串内,可是后来仔细分析以后发现,这个问题其实可以用贪心的策略来解决,这就要简单一些。

为 t 建立一个 map(实现的时候通过 array 达到一样的效果),key 是字符,value 是一个 HashSet,里面存放的是该字符出现的 index。这样每出现一个字符,就可以使用 ceiling 函数寻找所有可行的位置中最小的那一个。如果贪心可行,贪心的方法往往要比动态规划简单一些。

class Solution {
    public boolean isSubsequence(String s, String t) {
        // tree set array
        // (1) array index indicates the character
        // (2) indices of s are stored in its tree set
        TreeSet<Integer>[] positions = new TreeSet[26];
        
        // step 1: build tree set array
        for (int i=0; i<t.length(); i++) {
            char ch = t.charAt(i);
            int index = ch - 'a';
            
            if (positions[index]==null)
                positions[index] = new TreeSet<>();
            
            TreeSet<Integer> ts = positions[index];
            ts.add(i);
        }
        
        // step 2: search
        int lastPos = -1;
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            int index = ch - 'a';
            
            if (positions[index]==null)
                return false;
            
            Integer newPos = positions[index].ceiling(lastPos + 1);
            // +1 is important to indicate any position can only be used no more than once
            
            if (newPos==null)
                return false;
            
            lastPos = newPos;
        }
        
        return true;
    }
}

UTF-8 Validation

【题目】A character in UTF8 can be from 1 to 4 bytes long, subjected to the following rules:

  1. For 1-byte character, the first bit is a 0, followed by its unicode code.
  2. For n-bytes character, the first n-bits are all one’s, the n+1 bit is 0, followed by n-1 bytes with most significant 2 bits being 10.

This is how the UTF-8 encoding would work:

   Char. number range  |        UTF-8 octet sequence
      (hexadecimal)    |              (binary)
   --------------------+---------------------------------------------
   0000 0000-0000 007F | 0xxxxxxx
   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Given an array of integers representing the data, return whether it is a valid utf-8 encoding.

Note:
The input is an array of integers. Only the least significant 8 bits of each integer is used to store the data. This means each integer represents only 1 byte of data.

Example 1:

data = [197, 130, 1], which represents the octet sequence: 11000101 10000010 00000001.

Return true.
It is a valid utf-8 encoding for a 2-bytes character followed by a 1-byte character.

Example 2:

data = [235, 140, 4], which represented the octet sequence: 11101011 10001100 00000100.

Return false.
The first 3 bits are all one's and the 4th bit is 0 means it is a 3-bytes character.
The next byte is a continuation byte which starts with 10 and that's correct.
But the second continuation byte does not start with 10, so it is invalid.

【解答】要先把题意看懂,规则看懂。又是一道“考操作”的题目。

  • 从前往后扫描,定义一个 rest 来表示除了开头那八位,后面还有多少个八位来存放实际数据,rest 可以为 0、1、2 或 3,这取决于开头那八位的值;
  • 定义一个 getType 用来区分上面的几种情况,定义 TYPE_CONT 表示该八位以 10 开头,后面可以存放数据;
  • 于是,在 rest 为 0 的情况下,作为开头八位,getType 不能返回 TYPE_CONT;
  • 在 rest 不为 0 的情况下,getType 方法就必须返回 TYPE_CONT,其它都是错误的。
class Solution {
    /**
       TYPE=1: 0000 0000-0000 007F | 0xxxxxxx
       TYPE=2: 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
       TYPE=3: 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
       TYPE=4: 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
     */
    private static int TYPE_CONT = -1; // 10xxxxxx
    private static int TYPE_ERROR = 0;
    
    public boolean validUtf8(int[] data) {
        if (data==null || data.length==0)
            return false;
        
        Integer type = null;
        int rest = 0;
        for (int i=0; i<data.length; i++) {
            if (rest == 0) {
                type = getType(data[i]);
                
                if (type==TYPE_CONT || type==TYPE_ERROR)
                    return false;
                
                rest = type - 1;
            } else {
                rest--;
                if (getType(data[i]) != TYPE_CONT)
                    return false;
            }
        }
        
        return rest == 0;
    }
    
    private int getType(int num) {
        // only least significant 8 bits of each integer is used
        int mask = 1 << 7;
        
        if ((num&mask) == 0)
            return 1;
        
        mask = mask >>> 1;
        if ((num&mask) == 0)
            return TYPE_CONT;
        
        for (int i=2; i<=4; i++) {
            mask = mask >>> 1;
            if ((num&mask) == 0)
                return i;
        }

        return TYPE_ERROR;
    }
}

Decode String

【题目】Given an encoded string, return it’s decoded string.

The encoding rule is: k[encoded_string], where the encoded_string inside the square brackets is being repeated exactly k times. Note that k is guaranteed to be a positive integer.

You may assume that the input string is always valid; No extra white spaces, square brackets are well-formed, etc.

Furthermore, you may assume that the original data does not contain any digits and that digits are only for those repeat numbers, k. For example, there won’t be input like 3a or 2[4].

Examples:

s = "3[a]2[bc]", return "aaabcbc".
s = "3[a2]", return "accaccacc".
s = "2[abc]3[cd]ef", return "abcabccdcdcdef".

【解答】没有太多可说的,扫描+堆栈,这里面涉及到子字符串和重复次数。可以定义一个类包含这两项,然后使用一个堆栈,也可以偷个懒,使用两个堆栈,一个存放子字符串,一个存放重复次数。

class Solution {
    public String decodeString(String s) {
        Stack<Integer> times = new Stack<>();
        Stack<String> strs = new Stack<>();
        
        String cur = "";
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            
            if (ch>='0' && ch<='9') {
                int count = 0;
                while (ch>='0' && ch<='9') {
                    count = count*10 + (ch-'0');
                    i++;
                    ch = s.charAt(i);
                }
                
                times.push(count);
            }
            
            if (ch=='[') {
                strs.push(cur);
                cur = "";
            } else if (ch==']') {
                int count = times.pop();
                String newCur = "";
                for (int j=0; j<count; j++) {
                    newCur += cur;
                }
                cur = strs.pop() + newCur;
            } else {
                cur += ch;
            }
        }
        
        return cur;
    }
}

Longest Substring with At Least K Repeating Characters

【题目】Find the length of the longest substring T of a given string (consists of lowercase letters only) such that every character in T appears no less than k times.

Example 1:

Input:
s = "aaabb", k = 3

Output:
3

The longest substring is "aaa", as 'a' is repeated 3 times.

Example 2:

Input:
s = "ababbc", k = 2

Output:
5

The longest substring is "ababb", as 'a' is repeated 2 times and 'b' is repeated 3 times.

【解答】要找出最长的子串,其中的所有字符都至少重复了 k 次。

拿到题目以后我有这样几个思路:

  • 第一个思路是能不能建立一个动态规划模型,假设 p[i] 表示是从 s[i] 开始的最长的满足要求的子串的长度,但是这种思路很难找到问题和子问题之间联系的关系式,这条路似乎不好走。
  • 第二个思路是滑动窗口,控制一个 start 下标和一个 end 下标,各自都分别往右移动,窗口所框定的字符串满足题意。但是滑动窗口方法要求进窗口和出窗口的条件清晰,这里却并不:如果当前滑动窗口满足条件,end 下标右移,于是此时不满足了,我有两个选择,一个可以 start 前进,一个可以 end 前进,这两条路都可能是对的。
  • 第三个思路是,先考虑整个字符串,如果符合条件,皆大欢喜;如果不合条件,那么就从中寻找不合要求的字符,那么显然这个字符是不可能出现在符合条件的子串中的,于是字符的两边的串分别递归求解。

显然第三条思路想起来觉得靠谱:

class Solution {
    public int longestSubstring(String s, int k) {
        return longest(s, k, 0, s.length()-1);
    }
    
    private int longest(String s, int k, int start, int end) {
        if (end-start+1 < k)
            return 0;
        
        int[] letters = new int[26];
        for (int i=start; i<=end; i++) {
            char ch = s.charAt(i);
            letters[ch-'a']++;
        }
        
        for (int l=0; l<26; l++) {
            int count = letters[l];
            if (count==0 || count>=k)
                continue;
            
            // found incompliant letter
            for (int i=start; i<=end; i++) {
                if (s.charAt(i) ==(char)('a'+l)) {
                    int leftPart = longest(s, k, start, i-1);
                    int rightPart = longest(s, k, i+1, end);
                    return Math.max(leftPart, rightPart);
                }
            }
        }
        
        // compliant
        return end-start+1;
    }
}

虽然通过了,但复杂度还是偏高,最坏的时间复杂度在 n 的平方。讨论区有复杂度为 n 的 方法 供参考。

Rotate Function

【题目】Given an array of integers A and let n to be its length.

Assume Bk to be an array obtained by rotating the array A k positions clock-wise, we define a “rotation function” F on A as follow:

F(k) = 0 * Bk[0] + 1 * Bk[1] + ... + (n-1) * Bk[n-1].

Calculate the maximum value of F(0), F(1), ..., F(n-1).

Note:
n is guaranteed to be less than 105.

Example:

A = [4, 3, 2, 6]

F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26

So the maximum value of F(0), F(1), F(2), F(3) is F(3) = 26.

【解答】看起来就很像高中里面函数的递推关系式,事实上,也确实可以用那个时候解函数递推关系问题的办法来简化题目:

F(0) = 0*A[0] + 1*A[1] + 2*A[2] + … + (n-1)*A[n-1]
F(1) = 1*A[0] + 2*A[1] + 3*A[2] + … + 0*A[n-1]
= F(0) + sum – n*A[n-1]

F(k+1) = F(k) + sum – n*A[n-1-k]

于是就得到了 F(k+1) 和 F(k) 之间单纯的关系式,其中的 sum 我们可以在预先一次循环求得,利用它就可以把问题变得很简单:

class Solution {
    public int maxRotateFunction(int[] A) {
        if (A==null)
            throw new IllegalArgumentException();
        if (A.length==0)
            return 0;
        
        int sum = 0;
        int f0 = 0;
        for (int i=0; i<A.length; i++) {
            sum += A[i];
            f0 += i*A[i];
        }
        
        int last = f0;
        int max = last;
        for (int k=0; k<A.length-1; k++) {
            last = last + sum - A.length*A[A.length-1-k];
            max = Math.max(max, last);
        }
        
        return max;
    }
}

Integer Replacement

【题目】Given a positive integer n and you can do operations as follow:

  1. If n is even, replace n with n/2.
  2. If n is odd, you can replace n with either n + 1 or n - 1.

What is the minimum number of replacements needed for n to become 1?

Example 1:

Input:
8

Output:
3

Explanation:
8 -> 4 -> 2 -> 1

Example 2:

Input:
7

Output:
4

Explanation:
7 -> 8 -> 4 -> 2 -> 1
or
7 -> 6 -> 3 -> 2 -> 1

【解答】本质上是搜索类的问题,考虑 BSF 或者 DSF,对于这样预期步骤数量不明确的,以及回溯特征不明显的,通常都是 BSF。准备两个集合,一个用来存储当前 BSF 的边界,边界上的节点会参与到下一步的计算中;另一个用来存放已经访问过的节点,以避免重复运算。

class Solution {
    public int integerReplacement(int n) {
        // BFS
        Set<Long> visited = new HashSet<>();
        Set<Long> current = new HashSet<>();
        current.add(n+0l);
        return search(current, visited, 0);
    }
    
    private int search(Set<Long> current, Set<Long> visited, int depth) {
        if (current.contains(1l))
            return depth;
        
        visited.addAll(current);
        Set<Long> newCurrent = new HashSet<>();
        for (long cur : current) {
            if (cur%2 == 0) {
                if (visited.contains(cur/2))
                    continue;
                else
                    newCurrent.add(cur/2);
            } else {
                if (visited.contains(cur+1))
                    continue;
                else
                    newCurrent.add(cur+1);
                
                if (visited.contains(cur-1))
                    continue;
                else
                    newCurrent.add(cur-1);
            }
        }
        
        return search(newCurrent, visited, depth+1);
    }
}

Random Pick Index

【题目】Given an array of integers with possible duplicates, randomly output the index of a given target number. You can assume that the given target number must exist in the array.

Note:
The array size can be very large. Solution that uses too much extra space will not pass the judge.

Example:

int[] nums = new int[] {1,2,3,3,3};
Solution solution = new Solution(nums);

// pick(3) should return either index 2, 3, or 4 randomly. Each index should have equal probability of returning.
solution.pick(3);

// pick(1) should return 0. Since in the array only nums[0] is equal to 1.
solution.pick(1);

【解答】看起来就是要建立一个从具体数到 index 集合的映射,然后再在这个 index 集合里面使用 random 方法挑一个返回。不过题目说的不要使用“too much extra space”实在是不合适,怎样才算是“too much extra space”呢?

class Solution {
    private Map<Integer, List<Integer>> map = new HashMap<>();
    Random random = new Random();
    public Solution(int[] nums) {
        for (int i=0; i<nums.length; i++) {
            int num = nums[i];
            if (!map.containsKey(num))
                map.put(num, new ArrayList<>());
            
            map.get(num).add(i);
        }
    }
    
    public int pick(int target) {
        List<Integer> list = map.get(target);
        return list.get(random.nextInt(list.size()));
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int param_1 = obj.pick(target);
 */

Evaluate Division

【题目】Equations are given in the format A / B = k, where A and B are variables represented as strings, and k is a real number (floating point number). Given some queries, return the answers. If the answer does not exist, return -1.0.

Example:
Given a / b = 2.0, b / c = 3.0.
queries are: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? .
return [6.0, 0.5, -1.0, 1.0, -1.0 ].

The input is: vector<pair<string, string>> equations, vector<double>& values, vector<pair<string, string>> queries , where equations.size() == values.size(), and the values are positive. This represents the equations. Return vector<double>.

According to the example above:

equations = [ ["a", "b"], ["b", "c"] ],
values = [2.0, 3.0],
queries = [ ["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"] ].

The input is always valid. You may assume that evaluating the queries will result in no division by zero and there is no contradiction.

【解答】看起来让我想到了 Prolog 语言——给一堆规则,问一个问题,得到一个答案。

一开始还觉得题目似乎很新颖,没有什么思路,但是仔细研究这些规则发现,规则都是很简单的,都是除法,a/b=2.0 也就意味着 a/2.0=b,a 给一个参数 2.0 并执行除法操作,得到了 b。看起来似乎有了头绪——a 和 b 之间存在着联系,看起来可以抽象出一个双向图:a 通过权值为 2.0 的路径可以到达 b,而 b 通过权值为 1/2.0 的路径可以到达 a。

这样,当题目问起别的执行公式的时候,其实问的就是给定两个点有向的连通问题。

想到这一步,这道题可以说解出了一半,基本上就只剩操作的部分了。

具体实现上,

  • 定一个 Map<String, Set<Node>> graph,key 就是起始节点,Node 包含了终止节点和路径的权值,每一个已知公式都可以表示出两条关系,即双向路径;
  • 再定义一个标记是否访问过的 Map<String, Boolean> visited;
  • 执行的时候首先根据已知条件把 graph 构建起来,然后从给定问题的起点开始递归搜索,DFS 方式尝试寻找从起点到终点的路径,并把其中的每一段相乘。
class Solution {
    public double[] calcEquation(String[][] equations, double[] values, String[][] queries) {
        Map<String, Set<Node>> graph = new HashMap<>();
        Map<String, Boolean> visited = new HashMap<>();
        
        for (int i=0; i<equations.length; i++) {
            String start = equations[i][0];
            String end = equations[i][1];
            double val = values[i];
            
            visited.put(start, false);
            visited.put(end, false);
            
            if (!graph.containsKey(start))
                graph.put(start, new HashSet<>());
            graph.get(start).add(new Node(end, val));
            if (!graph.containsKey(end))
                graph.put(end, new HashSet<>());
            graph.get(end).add(new Node(start, 1/val));
        }
        
        double[] result = new double[queries.length];
        for (int i=0; i<queries.length; i++) {
            String start = queries[i][0];
            String end = queries[i][1];
            
            Double res;
            if (!visited.containsKey(start) || !visited.containsKey(end)) {
                res = null;
            } else {
                res = search(graph, visited, start, end);
            }
            
            if (res==null)
                res = -1.0;
            result[i] = res;
        }
        return result;
    }
    
    private Double search(Map<String, Set<Node>> graph, Map<String, Boolean> visited, String start, String end) {
        if (start.equals(end))
            return 1.0;
        
        if (visited.get(start))
            return null;

        for (Node node : graph.get(start)) {
            visited.put(start, true);
            Double next = search(graph, visited, node.end, end);
            visited.put(start, false);
            if (next!=null)
                return node.distance * next;
        }

        return null;
    }
}

class Node {
    public String end;
    public Double distance;
    public Node(String end, double distance) {
        this.end = end;
        this.distance = distance;
    }
}

Nth Digit

【题目】Find the nth digit of the infinite integer sequence 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …

Note:
n is positive and will fit within the range of a 32-bit signed integer (n < 231).

Example 1:

Input:
3

Output:
3

Example 2:

Input:
11

Output:
0

Explanation:
The 11th digit of the sequence 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... is a 0, which is part of the number 10.

【解答】要找到第 n 位数字是什么,这怎么可能只是 easy 难度?拿到题目的时候,心里有一个大致的思路,就是把这些数字挨个写出来,然后找第 n 位。但是实际上,并不需要真的把这些数写出来,而是根据数字排列的规律,把这些数字分成这样的多个部分:

1 ~ 9
10 ~ 99
100 ~ 999

这样分的原因是,一组一组可以逐渐线性增大,而且每一组内部的构成都是一致的(都具备相同的数字位数,这点便于计算):

  • 如果定义一个 totalNumber 表示当前组数字的数量,第一组是 1~9,一共 9 个;第二组 10~99 就是 totalNumber*10,因为第一组的每一个数字后面都可以跟一个数字,这个数字来自 0~9 中的任意一个。这个具备递进关系的分组是本题的关键。
  • 不断用增大的组数去累计并挑战 n,直到发现累计到最新的组所包含的数字超过 n,这是退出循环。因为所求的数字应当在当前组内。
  • 在已知当前组的起始元素和每个元素的长度(数字个数)的情况下,可以找到所求数字所在的元素(除法)和元素中的数字序号(取余)。
class Solution {
    public int findNthDigit(int n) {
        int startNumber = 1;
        int length = 1;
        long totalDigits = 9; // 1 ~ 9

        while (true) {
            if (n <= length * totalDigits)
                break;
            
            n -= length * totalDigits; // 1~9
            
            totalDigits *= 10; // 1~9 => adding 10 and 11~99
            startNumber *= 10; // 1 => 10
            length++;
        }
        
        // startNumber is 10^m, so "rest" starts from 0
        int rest = (n-1) / length;
        int digitNo = (n-1) % length;
        int locatedNumber = startNumber + rest;
        // e.g. startNumber=1000, n=5, length=4, so locatedNumber = 1000 + (5-1)/4 = 1001
        
        return ("" + locatedNumber).charAt(digitNo) - '0';
    }
}

Binary Watch

【题目】A binary watch has 4 LEDs on the top which represent the hours (0-11), and the 6 LEDs on the bottom represent the minutes (0-59).

Each LED represents a zero or one, with the least significant bit on the right.

For example, the above binary watch reads “3:25”.

Given a non-negative integer n which represents the number of LEDs that are currently on, return all possible times the watch could represent.

Example:

Input: n = 1
Return: ["1:00", "2:00", "4:00", "8:00", "0:01", "0:02", "0:04", "0:08", "0:16", "0:32"]

Note:

  • The order of output does not matter.
  • The hour must not contain a leading zero, for example “01:00” is not valid, it should be “1:00”.
  • The minute must be consist of two digits and may contain a leading zero, for example “10:2” is not valid, it should be “10:02”.

【解答】要寻找所有可能的时间。思路并不难找,但是要一次做对很有难度。

  • 首先定义一个 Time 类,包含小时和分钟,把时间抽象地表示出来,而且实现 Comparable 接口,便于排序以统一顺序。
  • 定义 hours 和 minutes 这两个标记访问情况的数组,接着回溯法去递归寻找所有表示的可能,每一个灯都有“亮”和“不亮”两种情况的考量。
  • 特别注意这次的回溯法有点特殊,是需要考虑两个核心目标模型,一个是 hour,一个是 minute。因此需要去重——先算了 hour 再算 minute,或者先算 minute 再算 hour,二者只能取其中之一。这是这道题的重点之一。
  • 有一些特殊情况需要考虑,比如 12:00,不应考虑,它应该用 0:00 来表示。
class Solution {
    public List<String> readBinaryWatch(int num) {
        // hour: 2^0 ~ 2^3
        // min: 2^0 ~ 2^5
        boolean[] hours = new boolean[4];
        boolean[] minutes = new boolean[6];

        List<Time> list = iterate(num, new Time(0, 0), 0, 0, hours, minutes);
        List<String> results = new ArrayList<>();
        for (Time time : list) {
            String min = "" + time.minute;
            if (time.minute < 10)
                min = '0' + min;

            results.add(time.hour + ":" + min);
        }
        Collections.sort(results);
        return results;
    }

    private List<Time> iterate(int num, Time time, int fromHourIndex, int fromMinIndex, boolean[] hours, boolean[] minutes) {
        List<Time> list = new ArrayList<>();
        if (num == 0) {
            list.add(time);
            return list;
        }

        for (int i = fromHourIndex; i < hours.length; i++) {
            if (hours[i])
                continue;

            int newHour = time.hour + (int) Math.pow(2, i);
            // 12:00 is not counted
            if (newHour >= 12)
                continue;
            hours[i] = true;
            list.addAll(iterate(num - 1, new Time(newHour, time.minute), i+1, fromMinIndex, hours, minutes));
            hours[i] = false;
        }

        for (int i = fromMinIndex; i < minutes.length; i++) {
            if (minutes[i])
                continue;

            int newMin = time.minute + (int) Math.pow(2, i);
            if (newMin>=60)
                continue;
            
            minutes[i] = true;
            // fromHourIndex = hours.length to avoid duplicates
            list.addAll(iterate(num - 1, new Time(time.hour, newMin), hours.length, i+1, hours, minutes));
            minutes[i] = false;
        }

        return list;
    }
}

class Time implements Comparable<Time> {
    public int hour;
    public int minute;

    public Time(int hour, int minute) {
        this.hour = hour;
        this.minute = minute;
    }

    @Override
    public int compareTo(Time that) {
        int hourDiff = this.hour - that.hour;
        if (hourDiff != 0)
            return hourDiff;

        int minDiff = this.minute - that.minute;
        return minDiff;
    }
}

Remove K Digits

【题目】Given a non-negative integer num represented as a string, remove k digits from the number so that the new number is the smallest possible.

Note:

  • The length of num is less than 10002 and will be ≥ k.
  • The given num does not contain any leading zero.

Example 1:

Input: num = "1432219", k = 3
Output: "1219"
Explanation: Remove the three digits 4, 3, and 2 to form the new number 1219 which is the smallest.

Example 2:

Input: num = "10200", k = 1
Output: "200"
Explanation: Remove the leading 1 and the number is 200. Note that the output must not contain leading zeroes.

Example 3:

Input: num = "10", k = 2
Output: "0"
Explanation: Remove all the digits from the number and it is left with nothing which is 0.

【解答】把问题思考清楚,写几个例子寻找思路:

  • 从左向右扫描,如果 num 中存在相邻的的两个数字且是递减的,即 num[i-1]>num[i],那么移除 num[i-1];
  • 如果不存在,删掉最后一个数字。

注意 leading zero 的处理。

class Solution {
    public String removeKdigits(String num, int k) {
        if (num==null || k<0 || k>num.length())
            throw new IllegalArgumentException();
        
        if (k == num.length())
            return "0";
        if (k == 0) {
            if (num.charAt(0) == '0')
                return removeKdigits(num.substring(1), 0);
            else
                return num;
        }
        
        for (int i=0; i<num.length(); i++) {
            if (i>0 && num.charAt(i-1)>num.charAt(i)) { // decreasing found
                return removeKdigits(num.substring(0, i-1) + num.substring(i), k-1);
            }
        }
        
        // no dcreasement found, remove the last digit
        return removeKdigits(num.substring(0, num.length()-1), k-1);
    }
}

Frog Jump

【题目】A frog is crossing a river. The river is divided into x units and at each unit there may or may not exist a stone. The frog can jump on a stone, but it must not jump into the water.

Given a list of stones’ positions (in units) in sorted ascending order, determine if the frog is able to cross the river by landing on the last stone. Initially, the frog is on the first stone and assume the first jump must be 1 unit.

If the frog’s last jump was k units, then its next jump must be either k – 1, k, or k + 1 units. Note that the frog can only jump in the forward direction.

Note:

  • The number of stones is ≥ 2 and is < 1,100.
  • Each stone’s position will be a non-negative integer < 231.
  • The first stone’s position is always 0.

Example 1:

[0,1,3,5,6,8,12,17]

There are a total of 8 stones.
The first stone at the 0th unit, second stone at the 1st unit,
third stone at the 3rd unit, and so on...
The last stone at the 17th unit.

Return true. The frog can jump to the last stone by jumping 
1 unit to the 2nd stone, then 2 units to the 3rd stone, then 
2 units to the 4th stone, then 3 units to the 6th stone, 
4 units to the 7th stone, and 5 units to the 8th stone.

Example 2:

[0,1,2,3,4,8,9,11]

Return false. There is no way to jump to the last stone as 
the gap between the 5th and 6th stone is too large.

【解答】判断从起点到终点是否可达的问题,而且是一维的,特殊的地方在于,每次可执行的步骤数是 k、k-1 或者 k+1。

看起来似乎不难,就是一个普通的回溯法问题。为了尽量避免重复计算,对于某个状态,无论是有解还是无解,都要把这个结果存起来。但是在逐步抽象模型建立起这个数据结构的时候发现似乎这条路是一条复杂的路,这个数据结构 map 类似于:

<stoneValue, <index, <jump, canCross>>>

  • 第一层是一个 HashMap,根据不同的 stoneValue 要找到石头以及相关数据;
  • 第二层是一个 TreeMap,key 是 index,因为找到的石头里面需要过滤掉当前和左侧的石头,因此需要用到 higherKey() 方法,即只考虑右侧的石头;
  • 第三层是一个 HashMap,jump 为 key;
  • 第四层是一个 Boolean,表示给定的 index 和 jump 的情况下,能否 cross。

即便可以做,这就显得很复杂了,map 很少有嵌套那么多层的。

在讨论区有一个简洁清晰的 解法 ,思路是:

建立一个 map:Map<Integer, Set<Integer>>,key 是 stoneValue,value 是从它开始所有可以进行的 jump,也就是说,这里不考虑 index。这个 map 在初始构造的时候,map[0] == [1],因为从最开始跳一步可以达到它右边的那个石头。除了它以外,其它的 set 都是空的,也就是说只是记录一下 stoneValue。

然后从左到右开始迭代,每一块石头取出来看,都要查看他的 set,即当前所有可以 jump 的可能性,只要没到最后一块石头,就把 jump 放到 set 里面去。

class Solution {
    public boolean canCross(int[] stones) {
        if (stones == null || stones.length == 0) {
            throw new IllegalArgumentException();
        }

        Map<Integer, Set<Integer>> map = new HashMap<Integer, Set<Integer>>(stones.length);
        map.put(0, new HashSet<Integer>());
        map.get(0).add(1);
        for (int i = 1; i < stones.length; i++) {
            map.put(stones[i], new HashSet<Integer>());
        }

        for (int stone : stones) {
            for (int step : map.get(stone)) {
                int reach = step + stone;
                if (reach == stones[stones.length - 1]) {
                    return true;
                }
                
                Set<Integer> set = map.get(reach);
                if (set != null) {
                    if (step - 1 > 0)
                        set.add(step - 1);
                    set.add(step);
                    set.add(step + 1);
                }
            }
        }

        return false;
    }
}

Sum of Left Leaves

【题目】Find the sum of all left leaves in a given binary tree.

Example:

    3
   / \
  9  20
    /  \
   15   7

There are two left leaves in the binary tree, with values 9 and 15 respectively. Return 24.

【解答】要求所有左叶子之和。这种二叉树的遍历问题,解法有一定套路,经常出现指针+栈的组合。像这道题,一个指向节点的指针,永远往左下方走;而栈则永远只存放右孩子。如果发现指针指向的节点没有左右孩子了,那就是找到左叶子了;如果左孩子为空,那就出栈一次。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if (root == null)
            return 0;
        
        int sum = 0;
        // always keep cur pointing to left child,
        // because right child would never be a left leave
        TreeNode cur = root.left;
        
        // always put right child to the queue
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        if (root.right != null)
            queue.add(root.right);
        
        while (cur != null || queue.peek() != null) {
            if (cur != null) {
                if (cur.left == null && cur.right == null) { // leaf
                    sum += cur.val;
                    cur = null;
                } else {
                    if (cur.right != null)
                        queue.add(cur.right);
                    cur = cur.left;
                }
            } else {
                TreeNode node = queue.poll();
                cur = node.left;
                if (node.right != null)
                    queue.add(node.right);
            }
        }
        
        return sum;
    }
}

Convert a Number to Hexadecimal

【题目】Given an integer, write an algorithm to convert it to hexadecimal. For negative integer, two’s complement method is used.

Note:

  1. All letters in hexadecimal (a-f) must be in lowercase.
  2. The hexadecimal string must not contain extra leading 0s. If the number is zero, it is represented by a single zero character '0'; otherwise, the first character in the hexadecimal string will not be the zero character.
  3. The given number is guaranteed to fit within the range of a 32-bit signed integer.
  4. You must not use any method provided by the library which converts/formats the number to hex directly.

Example 1:

Input:
26

Output:
"1a"

Example 2:

Input:
-1

Output:
"ffffffff"

【解答】转十六进制数。对于正数来说,没有什么难的。关键是负数,要使用补码的方法。如果是二进制,那就在非符号位上面取反加一,现在十六进制呢?其实可以一样操作,本身十六进制也是由 4 个二进制数联合构成的。如果要对十六进制数直接取反加一也可以,和二进制类似:二进制数 x 取反是 1-x,十六进制数 x 就是 15-x。加一的操作发生在个位。

class Solution {
    public String toHex(int num) {
        if (num==0)
            return "0";
        
        String result = "";
        long number = num + 0l;
        if (num<0)
            number = -number;
        
        while (number>0) {
            long quotient = number / 16;
            int mod = (int)(number % 16);
            result = toHexSingleDigit(mod) + result;
            number = quotient;
        }
        
        if (num<0)
            result = toComplement(result);
        return result;
    }
    
    private char toHexSingleDigit(int num) {
        if (num < 10)
            return (char)('0' + num);
        else
            return (char)('a' + (num-10));
    }
    
    private String toComplement(String s) {
        int len = s.length();
        for (int i=0; i<8-len; i++) {
            s = '0' + s;
        }
        
        String result = "";
        boolean carry = false;
        for (int i=7; i>=0; i--) {
            char ch = s.charAt(i);
            int num;
            if (ch >='0' && ch <= '9')
                num = ch - '0';
            else
                num = ch - 'a' + 10;
            
            num = 15 - num;
            if (i==7 || carry)
                num++;
            if (num == 16) {
                carry = true;
                num = num - 16;
            } else {
                carry = false;
            }
            
            result = toHexSingleDigit(num) + result;
        }
        return result;
    }
}

Queue Reconstruction by Height

【题目】Suppose you have a random list of people standing in a queue. Each person is described by a pair of integers (h, k), where h is the height of the person and kis the number of people in front of this person who have a height greater than or equal to h. Write an algorithm to reconstruct the queue.

Note:
The number of people is less than 1,100.

Example

Input:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

Output:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

【解答】排序题。这道题目如果拿到以后去考虑怎样实现 Comparable 接口或者实现一个 Comparator 比较类,然后调用 Collections.sort(),那就踏上不归路了。因为单纯地拿出任意两个数来,比如 [7,1] 和 [4,4],是无法判断谁在前面的,就因为这一点,一般的排序方式不再好用。

换一个思路,尽量先按照某个规则把整体顺序预置妥当,再利用插入排序的思想,逐渐构造出满足要求的序列来,这种方式在任意时间,构造的序列都是符合题意规则的。而且最重要的一点是,整个插入的过程,由于所取的元素是相等或者逐渐减小的,因此可以保证无论插入在结果集中的哪个位置,都对已有结果元素的合法性不产生任何影响。这个思路这个题目最关键的一点,剩下的都是“常规操作”。

首先按照身高从高到矮排序,如果高矮相等,按照第二个参数的大小,从小到大排序。排完序的链表作为源数据。

定义结果集 result,在循环中把确定的结果插入到 result 中去。由于源数据是按照高度有序的,因此每次从中取出的元素都是相比结果集中的的元素相等身高或者更矮一些,正是因为如此,根据它对元素的第二维上的要求,从左往右找到位置插进去,根本不用担心插入位置对于结果集中已有元素的影响。

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        if (people==null || people.length==0)
            return people;
        
        List<int[]> list = new LinkedList<>();
        for (int[] person : people) {
            list.add(person);
        }
        Collections.sort(list, new Comparator<int[]>() {
            @Override
            public int compare(int[] left, int[] right) {
                int diff = left[0] - right[0];
                if (diff!=0)
                    return -diff;
                else
                    return left[1] - right[1];
            }
        });
        
        List<int[]> result = new ArrayList<>();
        int start = 0;
        int end = 0;
        Integer height = null;
        while(start<list.size() && end<list.size()) {
            if (height==null) {
                height = list.get(end)[0];
                end++;
            } else {
                if (list.get(end)[0]==height) {
                    end++;
                } else {
                    insert(list, start, end, result);
                    start = end;
                    height = null;
                }
            }
        }
        
        insert(list, start, end, result);
        int[][] arr = new int[people.length][];
        for (int i=0; i<result.size(); i++) {
            arr[i] = result.get(i);
        }
        return arr;
    }
    
    // [start, end)
    private void insert(List<int[]> list, int start, int end, List<int[]> result) {
        while (start<end) {
            int[] item = list.get(start);
            result.add(item[1], item);
            start++;
        }
    }
}

Trapping Rain Water II

【题目】Given an m x n matrix of positive integers representing the height of each unit cell in a 2D elevation map, compute the volume of water it is able to trap after raining.

Note:

Both m and n are less than 110. The height of each unit cell is greater than 0 and is less than 20,000.

Example:

Given the following 3x6 height map:
[
  [1,4,3,1,3,2],
  [3,2,1,3,2,4],
  [2,3,3,2,3,1]
]

Return 4.

The above image represents the elevation map [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]] before the rain.

After the rain, water is trapped between the blocks. The total volume of water trapped is 4.

【解答】之前用双指针的方法解过一维的积雨水问题,现在是二维的。某种程度上二者有类似的思路。整个二维图形的最外圈是无法积水的,并且注意到,四个角上的元素,无论高度是多少,都对积水能力没有任何影响。当这个外圈固定下来,就可以从外圈上最矮的那一点 P 开始,考察它四周的位置,凡是没有考察过的新位置,都是考虑可以储水的,但是必须要比这个 P 点更矮;如果更高,那虽然这个点没法储水,但是从下一轮开始的循环中,它的高度潜在拔高了最矮的点 P。具体说:

  • 初始操作:
    • 建立一个 visitedMap(或者重用 heightMap)来标记该位置是否已经访问过;
    • 建立一个最小堆 heap 用来标记考察区域的外边界,把 heightMap 最靠外的这一圈放到 heap 里面去,同时更新 visitedMap(注意:heightMap 的四个角预先标记为“访问过”,且不放入 heap,因为它们不对存水产生任何影响)。
  •  循环操作:
    • 每次都从 heap 中取最小值(最小堆)p,并拿它的高度和当前的存水阈值比较,取较大的一个作为新的阈值;
    • 考察 p 的上下左右四个元素,如果没有访问过,就记录下它的存水量,标记访问过,并存入 heap;
    • 反复操作,直到 heap 为空。
class Solution {
    private Item load(int[][] heightMap, int x, int y) {
        if (x<0 || y<0 || x>=heightMap.length || y>=heightMap[0].length || heightMap[x][y]<0)
            return null;
        Item item = new Item(x, y, heightMap[x][y]);
        heightMap[x][y] = -1;
        return item;
    }
    
    public int trapRainWater(int[][] heightMap) {
        if (heightMap == null)
            throw new IllegalArgumentException();
        if (heightMap.length<=2 || heightMap[0].length<=2)
            return 0;
        
        int total = 0;
        Queue<Item> heap = new PriorityQueue<>();
        // top and bottom row
        for (int i=1; i<heightMap[0].length-1; i++) {
            heap.add(this.load(heightMap, 0, i));
            heap.add(this.load(heightMap, heightMap.length-1, i));
        }
        // left and right column
        for (int i=1; i<heightMap.length-1; i++) {
            heap.add(this.load(heightMap, i, 0));
            heap.add(this.load(heightMap, i, heightMap[0].length-1));
        }
        // mark the four corners visited
        heightMap[0][0] = -1;
        heightMap[0][heightMap[0].length-1] = -1;
        heightMap[heightMap.length-1][0] = -1;
        heightMap[heightMap.length-1][heightMap[0].length-1] = -1;
        
        int threshold = heap.peek().h;
        while (!heap.isEmpty()) {
            Item item = heap.poll();
            // the threshold would never get smaller
            threshold = Math.max(threshold, item.h);
            
            Item up = this.load(heightMap, item.x-1, item.y);
            Item down = this.load(heightMap, item.x+1, item.y);
            Item left = this.load(heightMap, item.x, item.y-1);
            Item right = this.load(heightMap, item.x, item.y+1);
            
            if (up != null) {
                if (up.h < threshold)
                    total += threshold - up.h;
                heap.offer(up);
            }
            if (down != null) {
                if (down.h < threshold)
                    total += threshold - down.h;
                heap.offer(down);
            }
            if (left != null) {
                if (left.h < threshold)
                    total += threshold - left.h;
                heap.offer(left);
            }
            if (right != null) {
                if (right.h < threshold)
                    total += threshold - right.h;
                heap.offer(right);
            }
        }
        
        return total;
    }
}

class Item implements Comparable<Item> {
    public int x;
    public int y;
    public int h;
    
    public Item(int x, int y, int h) {
        this.x = x;
        this.y = y;
        this.h = h;
    }
    
    @Override
    public String toString() {
        return "[" + this.x + ", " + this.y + "]: " + this.h;
    }
    
    @Override
    public int compareTo(Item other) {
        return this.h - other.h;
    }
}

Longest Palindrome

【题目】Given a string which consists of lowercase or uppercase letters, find the length of the longest palindromes that can be built with those letters.

This is case sensitive, for example "Aa" is not considered a palindrome here.

Note:
Assume the length of given string will not exceed 1,010.

Example:

Input:
"abccccdd"

Output:
7

Explanation:
One longest palindrome that can be built is "dccaccd", whose length is 7.

【解答】问能构建最长的回文串。对于偶数回文串,所有字符都是成对出现的;但是对于奇数回文串,允许有一个特殊字符出现奇数次。

class Solution {
    public int longestPalindrome(String s) {
        boolean[] chars = new boolean[52];
        int count = 0;
        for (int i=0; i<s.length(); i++) {
            char ch = s.charAt(i);
            int index;
            if (ch >= 'A' && ch <= 'Z') {
                index = ch - 'A' + 26;
            } else {
                index = ch - 'a';
            }
            if (chars[index]) {
                chars[index] = false;
                count += 2;
            } else {
                chars[index] = true;
            }
        }
        
        for (int i=0; i<52; i++) {
            if (chars[i])
                return count + 1;
        }
        return count;
    }
}

Split Array Largest Sum

【题目】Given an array which consists of non-negative integers and an integer m, you can split the array into m non-empty continuous subarrays. Write an algorithm to minimize the largest sum among these m subarrays.

Note:
If n is the length of array, assume the following constraints are satisfied:

  • 1 ≤ n ≤ 1000
  • 1 ≤ m ≤ min(50, n)

Examples:

Input:
nums = [7,2,5,10,8]
m = 2

Output:
18

Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [7,2,5] and [10,8],
where the largest sum among the two subarrays is only 18.

【解答】要把数组分成 m 份,并且让各份的和的最大值尽量小。

一开始我的反应是先尝试贪心这条路,但是发现不好走。因为很难找到一个特定的规则下结论说,符合条件了,这就是最优解的一部分。接着就想那退一步,能不能用动态规划,这条路依然不好走,因为原始问题和子问题的关系并不是清晰。

这样的情况下,似乎执着于分析问题本身的性质来寻找最优解的大致方向不正确。于是开始考虑能否反过来,从结论出发,去“猜”一个解,再反过来去验证它是不是真正的最优解。但是要猜,不能胡猜,这个最优解的下界是所有数中最大的一个,上界是所有数之和。接下去,借用二分查找的方式,找到这个最优解。

求和的时候为防止溢出,使用 long 代替 int。

class Solution {
    public int splitArray(int[] nums, int m) {
        long left = 0;
        long right = 0;
        for (int num : nums) {
            left = Math.max(left, num);
            right += num;
        }
        
        int largestSum = (int) right;
        while (left <= right) {
            long mid = (left+right) / 2;
            int subArrNum = m;
            long sum = 0;
            for (int num : nums) {
                // no need to continue the loop
                if (subArrNum == 0) {
                    break;
                }
                
                if (sum + num <= mid) {
                    sum += num;
                } else {
                    sum = num;
                    // a new sub array is required
                    subArrNum--;
                }
            }
            
            // make sure every time left or right moves
            if (subArrNum > 0) {
                right = mid - 1;
                largestSum = (int) mid;
            } else {
                left = mid + 1;
            }
        }
        
        return largestSum;
    }
}

Fizz Buzz

【题目】Write a program that outputs the string representation of numbers from 1 to n.

But for multiples of three it should output “Fizz” instead of the number and for the multiples of five output “Buzz”. For numbers which are multiples of both three and five output “FizzBuzz”.

Example:

n = 15,

Return:
[
    "1",
    "2",
    "Fizz",
    "4",
    "Buzz",
    "Fizz",
    "7",
    "8",
    "Fizz",
    "Buzz",
    "11",
    "Fizz",
    "13",
    "14",
    "FizzBuzz"
]

【解答】没有太多可说的,注意数字是从 1 开始,而不是 0.

class Solution {
    public List<String> fizzBuzz(int n) {
        List<String> list = new ArrayList<>();
        for (int i=0; i<n; i++) {
            int num = i+1;
            String s = null;
            if (num%3==0 && num%5==0)
                s = "FizzBuzz";
            else if (num%3==0)
                s = "Fizz";
            else if (num%5==0)
                s = "Buzz";
            else
                s = "" + num;
            
            list.add(s);
        }
        
        return list;
    }
}

Arithmetic Slices

【题目】A sequence of number is called arithmetic if it consists of at least three elements and if the difference between any two consecutive elements is the same.

For example, these are arithmetic sequence:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

The following sequence is not arithmetic.

1, 1, 2, 5, 7

A zero-indexed array A consisting of N numbers is given. A slice of that array is any pair of integers (P, Q) such that 0 <= P < Q < N.

A slice (P, Q) of array A is called arithmetic if the sequence:
A[P], A[p + 1], …, A[Q – 1], A[Q] is arithmetic. In particular, this means that P + 1 < Q.

The function should return the number of arithmetic slices in the array A.

Example:

A = [1, 2, 3, 4]

return: 3, for 3 arithmetic slices in A: [1, 2, 3], [2, 3, 4] and [1, 2, 3, 4] itself.

【解答】统计所有满足题目要求的子串的数量。理解题意后,发现只需要看连续两个数的差值,并且只关心同一个差值连续出现的次数,而根本不需要关心每个元素的值本身。发现这一点可以大大简化题目。

比如这样的差值数列,和连续出现的次数,以及结果的关系:

3, 3 => 2 times => result is 1

3, 3, 3 => 3 times => result is 2 + 1

3, 3, 3, 3 => 4 times => result is 3 + 2 + 1

… => x times => result is ((x-1) + 1) * (x-1) / 2

也就是说,如果给定一个长度为 x 的 arithmetic slice,它本身包含了 result is ((x-1) + 1) * (x-1) / 2 这么多个隐含的 arithmetic slice。所以,只需要寻找这样的连续子串,其中相邻两两元素的差值相等,它尽可能长,然后对于每个子串套用上面的公式即可。

class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        if (A==null)
            throw new IllegalArgumentException();
        if (A.length<3)
            return 0;
        
        List<Integer> diffs = new ArrayList<>();
        int diff = A[1] - A[0];
        int count = 1; // diff count
        int total = 0; // result
        for (int i=2; i<A.length; i++) {
            if (A[i] - A[i-1] == diff) {
                count++;
            } else {
                if (count > 1) {
                    total += ((count-1) + 1) * (count-1) / 2;
                }
                diff = A[i] - A[i-1];
                count = 1;
            }
        }
        
        if (count > 1) {
            total += ((count-1) + 1) * (count-1) / 2;
        }
        
        return total;
    }
}

Third Maximum Number

【题目】Given a non-empty array of integers, return the third maximum number in this array. If it does not exist, return the maximum number. The time complexity must be in O(n).

Example 1:

Input: [3, 2, 1]

Output: 1

Explanation: The third maximum is 1.

Example 2:

Input: [1, 2]

Output: 2

Explanation: The third maximum does not exist, so the maximum (2) is returned instead.

Example 3:

Input: [2, 2, 3, 1]

Output: 1

Explanation: Note that the third maximum here means the third maximum distinct number.
Both numbers with value 2 are both considered as second maximum.

【解答】返回第三大的数。注意去重。

class Solution {
    public int thirdMax(int[] nums) {
        if (nums == null || nums.length == 0)
            throw new IllegalArgumentException();
        
        Queue<Integer> heap = new PriorityQueue<>(nums.length, new Comparator<Integer>(){
            @Override
            public int compare(Integer left, Integer right) {
                // no dup so no need to cover right == left
                return right > left ? 1 : -1;
            }
        });
        
        for (int num : nums) {
            if (!heap.contains(num))
                heap.add(num);
        }
        
        if (heap.size() < 3)
            return heap.poll();
        
        int countDown = 3;
        int number = 0;
        while (countDown > 0) {
            number = heap.poll();
            countDown --;
        }
        
        return number;
    }
}

Add Strings

【题目】Given two non-negative integers num1 and num2 represented as string, return the sum of num1 and num2.

Note:

  1. The length of both num1 and num2 is < 5100.
  2. Both num1 and num2 contains only digits 0-9.
  3. Both num1 and num2 does not contain any leading zero.
  4. You must not use any built-in BigInteger library or convert the inputs to integer directly.

【解答】相加两个用 string 表示的数。

class Solution {
    public String addStrings(String num1, String num2) {
        int carry = 0;
        int idx = 0;
        String result = "";
        while (idx < num1.length() || idx < num2.length()) {
            int num = carry;
            if (idx < num1.length())
                num += num1.charAt(num1.length()-1-idx) - '0';
            if (idx < num2.length())
                num += num2.charAt(num2.length()-1-idx) - '0';
            
            if (num >= 10) {
                num -= 10;
                carry = 1;
            } else {
                carry = 0;
            }
            
            result = num + result;
            idx ++;
        }
        
        if (carry>0)
            result = carry + result;
        
        return result;
    }
}

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

via 四火的唠叨 https://ift.tt/2Fw4qJG

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 打造高效的工作环境 – Shell 篇

注:本文由雷俊(Javaer/Emacser)和我一起编辑,所以文章版权归雷俊与我共同所有,转载者必需注明出处和我们两位作者。原文最早发于酷壳微信公众号,后来我又做了一些修改,再发到博客这边。

程序员是一个很懒的群体,总想着能够让代码为自己干活,他们不断地把工作生活中的一些事情用代码自动化了,从而让整个社会的效率运作地越来越高。所以,程序员在准备去优化这个世界的时候,都会先要优化自己的工作环境,是所谓“工欲善其事,必先利其器”。

我们每个程序员都应该打造一套让自己更为高效的工作环境。那怕就是让你少输入一次命令,少按一次键,少在鼠标和键盘间切换一次,都会让程序员的工作变得更为的高效。所以,程序员一般需要一台性能比较好,不会因为开了太多的网页或程序就卡得不行的电脑,还要配备多个显示器,一个显示器写代码,一个查文档,一个测试运行结果,而不必在各种窗口来来回回的切换……在大量的窗口间切换经常会迷路,而且也容易出错(分不清线上或测试环境)……

除了硬件上的装备,软件上也是能够提升程序员生产力的地方,在软件层面提升程序员生产力的东西有一个很重要的事就是命令行和脚本,使用鼠标和图形界面则会大大降低程序员的生产力。酷壳以前也写过一些,如《你可能不知道的Shell》和《 应该知道的Linux技巧》,但是Unix/Linux Shell就是一个大宝库,怎么写也写不完,不然,怎么会有“Where is the Shell, there is a way”。

命令行

在不同的操作系统下,都有着很不错的命令行工具,比如 Mac 下的 Iterm2,Linux 下的原生命令行,如果你是在 Windows 下工作,问题也不大,因为 Windows 下现在有了 WSL。WSL 提供了一个由微软开发的Linux兼容的内核接口(不包含Linux内核代码),然后可以在其上运行GNU用户空间,例如 Ubuntu,openSUSE,SUSE Linux Enterprise Server,Debian和Kali Linux。这样的用户空间可能包含 Bash shell 和命令语言,使用本机 GNU/Linux 命令行工具(sed,awk 等),编程语言解释器(Ruby,Python 等),甚至是图形应用程序(使用主机端的X窗口系统)。

使用命令行可以完成所有日常的操作,新建文件夹(mkdir)、新建文件(touch)、移动(mv)、复制(cp)、删除(rm)等等。而且使用 Linux/Unix 命令行最好的方式是可以用 awksedgrepxargsfindsort 等等这样的命令,然后用管道把其串起来,就可以完成一个你想要的功能,尤其是一些简单的数据统计功能。这是Linux命令行不可比拟的优势。比如:

  • 查看连接你服务器 top10 用户端的 IP 地址:

netstat -nat | awk '{print $5}' | awk -F ':' '{print $1}' | sort | uniq -c | sort -rn | head -n 10

  • 查看一下你最常用的10个命令:

cat .bash_history | sort | uniq -c | sort -rn | head -n 10 (or cat .zhistory | sort | uniq -c | sort -rn | head -n 10

(注:awk 和 sed 是两大神器,所以,我以前的也有两篇文章来介绍它们——《awk简明教程》和《sed简明教程》,你可以前往一读)

在命令行中使用 alias 可以将使用频率很高命令或者比较复杂的命令合并成一个命令,或者修改原生的命令。

下面这几个命令,可能是你天天都在敲的。所以,你应该设置成 alias 来提高效率

alias nis="npm install --save "
alias svim='sudo vim'
alias mkcd='foo(){ mkdir -p "$1"; cd "$1" }; foo '
alias install='sudo apt get install'
alias update='sudo apt-get update; sudo apt-get upgrade'
alias ..="cd .."
alias ...="cd ..; cd .."
alias www='python -m SimpleHTTPServer 8000'
alias sock5='ssh -D 8080 -q -C -N -f user@your.server'

你还可以参考如下的一些文章,看看别人是怎么用好 alias 的

命令行中除了原生的命令之外,还有很多可以提升使用体验的工具。下面罗列一些很不错的命令,把原生的命令增强地很厉害:

  • fasd 增强了 cd 命令 。
  • bat 增强了 cat 命令 。如果你想要有语法高亮的 cat,可以试试 ccat 命令。
  • exa 增强了 ls 命令,如果你需要在很多目录上浏览各种文件 ,ranger 命令可以比 cd 和 cat 更有效率,甚至可以在你的终端预览图片。
  • fd 是一个比 find 更简单更快的命令,他还会自动地忽略掉一些你配置在 .gitignore 中的文件,以及 .git 下的文件。
  • fzf 会是一个很好用的文件搜索神器,其主要是搜索当前目录以下的文件,还可以使用 fzf --preview 'cat {}'边搜索文件边浏览内容。
  • grep 是一个上古神器,然而,ackag 和 rg 是更好的grep,和上面的 fd一样,在递归目录匹配的时候,会使用你配置在 .gitignore 中的规则。
  • rm 是一个危险的命令,尤其是各种 rm -rf …,所以,trash 是一个更好的删除命令。
  • man 命令是好读文档的命令,但是man的文档有时候太长了,所以,你可以试试 tldr 命令,把文档上的一些示例整出来给你看。
  • 如果你想要一个图示化的ping,你可以试试 prettyping 。
  • 如果你想搜索以前打过的命令,不要再用 Ctrl +R 了,你可以使用加强版的 hstr  。
  • htop  是 top 的一个加强版。然而,还有很多的各式各样的top,比如:用于看IO负载的 iotop,网络负载的 iftop, 以及把这些top都集成在一起的 atop
  • ncdu  比 du 好用多了用。另一个选择是 nnn
  • 如果你想把你的命令行操作建录制成一个 SVG 动图,那么你可以尝试使用 asciinema 和 svg-trem 。
  • httpie 是一个可以用来替代 curlwget 的 http 客户端,httpie 支持 json 和语法高亮,可以使用简单的语法进行 http 访问: http -v github.com
  • tmux 在需要经常登录远程服务器工作的时候会很有用,可以保持远程登录的会话,还可以在一个窗口中查看多个 shell 的状态。
  • Taskbook 是可以完全在命令行中使用的任务管理器 ,支持 ToDo 管理,还可以为每个任务加上优先级。
  • sshrc 是个神器,在你登录远程服务器的时候也能使用本机的 shell 的 rc 文件中的配置。
  • goaccess  这个是一个轻量级的分析统计日志文件的工具,主要是分析各种各样的 access log。

关于这些增加命令,主要是参考自下面的这些文章

  1. 10 Tools To Power Up Your Command Line
  2. 5 More Tools To Power Up Your Command Line (Part 2 Of Series)
  3. Power Up Your Command Line, Part 3
  4. Power Up Your Command Line
  5. Hacker Tools

Shell 和脚本

shell 是可以与计算机进行高效交互的文本接口。shell 提供了一套交互式的编程语言(脚本),shell的种类很多,比如 shbashzsh 等。

shell 的生命力很强,在各种高级编程语言大行其道的今天,很多的任务依然离不开 shell。比如可以使用 shell 来执行一些编译任务,或者做一些批处理任务,初始化数据、打包程序等等。

现在比较流行的是 zsh + oh-my-zsh + zsh-autosuggestions 的组合,你也可以试试看。其中 zsh 和 oh-my-zsh 算是常规操作了,但是 zsh-autosuggestions 特别有用,可以超级快速的帮你补全你输入过的命令,让命令行的操作更加高效。

另外,fish 也是另外一个牛逼的shell,比如:命令行自动完成(根据历史记录),命令行命令高亮,当你要输入命令行参数的时候,自动提示有哪些参数…… fish在很多地方也是用起来很爽的。和上面的 oh-my-zsh 有点不分伯仲了。

你也许会说,用 Python 脚本或 PHP 来写脚本会比 Shell 更好更没有 bug,但我要申辩一下:

  • 其一,如果你有一天要维护线上机器的时候,或是到了银行用户的系统(与外网完全隔离,而且服务器上没有安装 Python/PHP 或是他们的的高级库,那么,你只有 Shell 可以用了)。
  • 其二,而且,如果要跟命令行交互很多的话,Shell 是不二之选,试想一下,如果你要去 100 台远程的机器上查access.log 日志中有没有某个错误,完成这个工作你是用 PHP/Python 写脚本快还是用 Shell 写脚本快呢?

所以,我们还要学会只使用传统的grep/awk/sed等等这些POSIX的原生的系统默认安装的命令

当然,要写好一个脚本并不容易,下面有一些小模板供你参考:

处理命令行参数的一个样例

while [ "$1" != "" ]; do
    case $1 in
        -s  )   shift	
		SERVER=$1 ;;  
        -d  )   shift
		DATE=$1 ;;
	--paramter|p ) shift
		PARAMETER=$1;;
        -h|help  )   usage # function call
                exit ;;
        * )     usage # All other parameters
                exit 1
    esac
    shift
done 

命令行菜单的一个样例

#!/bin/bash
# Bash Menu Script Example

PS3='Please enter your choice: '
options=("Option 1" "Option 2" "Option 3" "Quit")
select opt in "${options[@]}"
do
    case $opt in
        "Option 1")
            echo "you chose choice 1"
            ;;
        "Option 2")
            echo "you chose choice 2"
            ;;
        "Option 3")
            echo "you chose choice $REPLY which is $opt"
            ;;
        "Quit")
            break
            ;;
        *) echo "invalid option $REPLY";;
    esac
done

颜色定义,你可以使用 echo -e "${Blu}blue ${Red}red ${RCol}etc...." 进行有颜色文本的输出

RCol='\e[0m'    # Text Reset

# Regular           Bold                Underline           High Intensity      BoldHigh Intens     Background          High Intensity Backgrounds
Bla='\e[0;30m';     BBla='\e[1;30m';    UBla='\e[4;30m';    IBla='\e[0;90m';    BIBla='\e[1;90m';   On_Bla='\e[40m';    On_IBla='\e[0;100m';
Red='\e[0;31m';     BRed='\e[1;31m';    URed='\e[4;31m';    IRed='\e[0;91m';    BIRed='\e[1;91m';   On_Red='\e[41m';    On_IRed='\e[0;101m';
Gre='\e[0;32m';     BGre='\e[1;32m';    UGre='\e[4;32m';    IGre='\e[0;92m';    BIGre='\e[1;92m';   On_Gre='\e[42m';    On_IGre='\e[0;102m';
Yel='\e[0;33m';     BYel='\e[1;33m';    UYel='\e[4;33m';    IYel='\e[0;93m';    BIYel='\e[1;93m';   On_Yel='\e[43m';    On_IYel='\e[0;103m';
Blu='\e[0;34m';     BBlu='\e[1;34m';    UBlu='\e[4;34m';    IBlu='\e[0;94m';    BIBlu='\e[1;94m';   On_Blu='\e[44m';    On_IBlu='\e[0;104m';
Pur='\e[0;35m';     BPur='\e[1;35m';    UPur='\e[4;35m';    IPur='\e[0;95m';    BIPur='\e[1;95m';   On_Pur='\e[45m';    On_IPur='\e[0;105m';
Cya='\e[0;36m';     BCya='\e[1;36m';    UCya='\e[4;36m';    ICya='\e[0;96m';    BICya='\e[1;96m';   On_Cya='\e[46m';    On_ICya='\e[0;106m';
Whi='\e[0;37m';     BWhi='\e[1;37m';    UWhi='\e[4;37m';    IWhi='\e[0;97m';    BIWhi='\e[1;97m';   On_Whi='\e[47m';    On_IWhi='\e[0;107m';

取当前运行脚本绝对路径的示例:(注:Linux下可以用 dirname $(readlink -f $0) )

FILE="$0"
while [[ -h ${FILE} ]]; do
    FILE="`readlink "${FILE}"`"
done
pushd "`dirname "${FILE}"`" &gt; /dev/null
DIR=`pwd -P`
popd &gt; /dev/null

如何在远程服务器运行一个本地脚本

#无参数
ssh user@server 'bash -s' &lt; local.script.sh

#有参数
ssh user@server ARG1="arg1" ARG2="arg2" 'bash -s' &lt; local_script.sh

如何检查一个命令是否存在,用 which 吗?最好不要用,因为很多操作系统的 which 命令没有设置退出状态码,这样你不知道是否是有那个命令。所以,你应该使用下面的方式。

# POSIX 兼容:
command -v &lt;the_command&gt;


# bash 环境:
hash &lt;the_command&gt; 
type &lt;the_command&gt;

# 示例:
gnudate() {
    if hash gdate 2&gt; /dev/null; then
        gdate "$@"
    else
        date "$@"
    fi
}

然后,如果要写出健壮性更好的脚本,下面是一些相关的技巧:

  • 使用 -e 参数,如:set -e 或是 #!/bin/sh -e,这样设置会让你的脚本出错就会停止运行,这样一来可以防止你的脚本在出错的情况下还在拼拿地干活停不下来。
  • 使用 -u 参数,如: set -eu,这意味着,如果你代码中有变量没有定义,就会退出。
  • 对一些变理,你可以使用默认值。如:${FOO:-'default'}
  • 处理你代码的退出码。这样方便你的脚本跟别的命令行或脚本集成。
  • 尽量不要使用 ; 来执行多个命令,而是使用 &&,这样会在出错的时候停止运行后续的命令。
  • 对于一些字符串变量,使用引号括起,避免其中有空格或是别的什么诡异字符。
  • 如果你的脚有参数,你需要检查脚本运行是否带了你想要的参数,或是,你的脚本可以在没有参数的情况下安全的运行。
  • 为你的脚本设置 -h 和 --help 来显示帮助信息。千万不要把这两个参数用做为的功能。
  • 使用 $() 而不是 “ 来获得命令行的输出,主要原因是易读。
  • 小心不同的平台,尤其是 MacOS 和 Linux 的跨平台。
  • 对于 rm -rf 这样的高危操作,需要检查后面的变量名是否为空,比如:rm -rf $MYDIDR/* 如果 $MYDIR为空,结果是灾难性的。
  • 考虑使用 “find/while” 而不是 “for/find”。如:for F in $(find . -type f) ; do echo $F; done 写成 find . -type f | while read F ; do echo $F ; done 不但可以容忍空格,而且还更快。
  • 防御式编程,在正式执行命令前,把相关的东西都检查好,比如,文件目录有没有存在。

你还可以使用ShellCheck 来帮助你检查你的脚本。

最后推荐一些 Shell 和脚本的参考资料。

各种有意思的命令拼装,一行命令走天涯:

下面是一些脚本集中营,你可以在里面淘到各种牛X的脚本:

甚至写脚本都可以使用框架:

Google的Shell脚本的代码规范:

最后,别忘了几个和shell有关的索引资源:

最后,如果你还有什么别的更好的玩的东西,欢迎在评论区留言,或是到 coolshellx/ariticles @ github 修改本文。

 


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 March 17, 2019 at 01:53PM
from 酷 壳 – CoolShell https://ift.tt/2W6efEU

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share

【酷壳】 谈谈我的“三观”

也许是人到了四十多了,敢写这么大的命题,我也醉了,不过,我还是想把我的想法记录下来,算是对我思考的一个snapshot,给未来的我看看,要么被未来的我打脸,要么打未来我的脸。无论怎么样,我觉得对我自己都很有意义。注意,这篇文章是长篇大论。

三观是世界观、人生观和价值观,

  • 世界观代表你是怎么看这个世界的。是左还是右,是激进还是保守,是理想还是现实,是乐观还是悲观……
  • 人生观代表你要想成为什么样的人。是成为有钱人,还是成为人生的体验者,是成为老师,还是成为行业专家,是成为有思想的人,还是成为,
  • 价值观则是你觉得什么对你来说更重要。是名是利,是过程还是结果,是付出还是索取,是国家还是自己,是家庭还是职业……

人的三观其实是会变的,回顾一下我的过去,我感觉我的三观至少有这么几比较明显的变化,学生时代、刚走上社会的年轻时代,三十岁后的时代,还有现在。估计都差不多。

  • 学生时代的三观更多的是学校给的,用各种标准答案给的,是又红又专的
  • 刚走上社会后发现完全不是这么一回事,但学生时代的三观根深蒂固,三观开始分裂,内心开始挣扎
  • 三十岁后,不如意的事越来越多,对社会越来越了解,一部分人屈从现实,一部分人不服输继续奋斗,而一部分人已展露才能,分裂的三观开始收敛
  • 四十岁时,经历过的事太多,发现留给自己的时间不多,世界太复杂,而还有好多事没做,从而变得与世无争,也变得更为地自我。

年轻的时候,抵制过日货,虽然没上过街,但是也激动过,一次是1999南斯拉夫大使馆被炸,一次是2005反日示威,以前,我也是一个爱国愤青。但是后来,有过各种机会出国长时间生活工作,加拿大、英国、美国、日本……随着自己的经历和眼界的开阔,自己的三观自己也随着有了很多的变化,发现有些事并不是自己一开始所认识的那样,而且还是截然相反的。我深深感觉到,要有一个好的世界观,你需要亲身去经历和体会这个世界,而不是听别人说。所以,当我看到身边的人情绪激动地要抵制这个国家,搞死那个民族的时候,我都会建议他去趟那个国家最好在在那个国家呆上一段时间,亲自感受一下。

再后来发现,要抵制的越来越多,小时候的美英帝国主义,然后是日本,再后面是法国、韩国、菲利宾、印度、德国、瑞典、加拿大……从小时候的台独到现在的港独、藏独、疆独……发现再这样下去,基本上来说,自己的人生也不用干别的事了……另外,随着自己的成长,越来越明白,抵制这个抵制那个只不过是幼稚和狭隘的爱国主义,真想强国,想别让他人看得起,就应该把时间和精力放在努力学习放在精益求精上,做出比他们更好的东西来。另外,感觉用对内的爱国主义解决对外的外交问题也有点驴唇不对马嘴,无非也就是转移一下内部的注意力罢了,另外还发现爱国主义还可以成为消费营销手段……不是我不爱国,是我觉得世道变复杂了,我只是一个普通的老百姓,能力有限,请不要赋予我那么大的使命,我只想在我的专业上精进,能力所能及地帮助身边的人,过一个简单纯粹安静友善的生活……

另外,为什么国与国之间硬要比个你高我低,硬要分个高下,硬要争出个输赢,我也不是太理解,世界都已经发展到全球化的阶段了,很多产品早就是你中有我,我中有你的情况了。举个例子,一部手机中的元件,可能来自全世界数十个国家,我们已经说不清楚一部手机是究竟是哪个国家生产的了。即然,整个世界都在以一种合作共赢全球化的姿态下运作,认准自己的位置,拥抱世界,持续向先进国家学习,互惠互利,不好吗?你可能会说,不是我们不想这样,是别人不容我们发展……老实说,大的层面我也感受不到,但就我在的互联网计算机行业方面,我觉得整个世界的开放性越来越好,开源项目空前地繁荣,世界上互联网文化也空前的开放,在计算机和互联网行业,我们享受了太多的开源和开放的红利,人家不开放,我们可能在很多领域还落后数十年。然而现在很多资源我们都访问不了,用个VPN也非法,你说是谁阻碍了发展?我只想能够流畅地访问互联网,让我的工作能够更有效率,然而,我在自己的家里却像做贼一样去学习新知识新技术,随时都有可能被抓进监狱……

随着自己的经历越多,发现这个世界越复杂,也发现自己越渺小,很多国家大事并不是我不关心,是我觉得那根本不是我这个平头老百姓可以操心的事,这个世界有这个世界运作的规律和方法,而还有很多事情超出了我能理解的范围,也超出了我能控制的范围,我关心不关心都一个样,这些大事都不会由我的意志所决定的。而所谓的关心,无非就是喊喊口号,跟人争论一下,试图改变其它老百姓的想法,然而,对事情的本身的帮助却没有多大意义。过上几天,生活照旧,人家该搞你还不是继续搞你,而你自己并不因为做这些事而过得更好。

我对国与国之间的关系的态度是,有礼有节,不卑不亢,对待外国人,有礼貌但也要有节气,既不卑躬屈膝,也不趾高气昂,整体上,我并不觉得我们比国外有多差,但我也不觉得我们比国外有多好,我们还在成长,还需要帮助和协作,四海之内皆兄弟,无论在哪个国家,在老百姓的世界里,哪有那么多矛盾。有机会多出去走走,多结交几个其它民族的朋友,你会觉得,在友善和包容的环境下,你的心情和生活可以更好

我现在更多关心的是和我生活相关的东西,比如:上网、教育、医疗、食品、治安、税务、旅游、收入、物价、个人权益、个人隐私……这些东西对我的影响会更大一些,也更值得关注,可以看到过去的几十年,我们国家已经有了长足的进步,这点也让我让感到很开心和自豪的,在一些地方也不输给强国。虽然依然有一些事的仍然没有达到我的预期,并也超出了我个人的控制范围无法改变,但整体都在变好。不过,未来的变数谁也不好说,我的安全感似乎还不足够,所以,我还是要继续努力,以便我可以有更多的选项。有选项总比没得选要好。所以,我想尽一切办法,努力让选项多起来,无法改变无法影响,那就只能提高自己有可选择的可能性。

另外,在网上与别人对一些事或观点的争论,我觉得越来越无聊,以前被怼了,一定要怼回去,现在不会了,视而不见,不是怕了,是因为,网络上的争论在我看来大多数都是些没有章法,逻辑混乱的争论。

  • 很多讨论不是说事,直接就是怼人骂人。随意就给人扣个帽子。
  • 非黑即白的划分,你说这个不是黑的,他们就把你划到白的那边。
  • 飘移观点,复杂化问题。东拉西扯,牵强附会,还扯出其它不相关的事来混淆。
  • 杠精很多,不关心你的整体观点,抓住一个小辫子大作文章。

很明显,与其花时间教育这些人,不如花时间提升自己,让自己变得更优秀,这样就有更高的可能性去接触更聪明更成功更高层次的人。因为,一方面,你改变不了他们,另外,改变他们对你自己也没什么意义,改变自己,提升自己,让自己成长才有意义。时间是宝贵的,那些人根本不值得花时间,应该花时间去结交更有素质更聪明的人,做更有价值的事。

美国总统富兰克林·罗斯福妻子埃莉诺·罗斯福(Eleanor Roosevelt)说过下面的一句话。

Great minds discuss ideas;
Average minds discuss events;
Small minds discuss people

把时间多放在一些想法上,对自己对社会都是有意义的,把时间放在八卦别人,说长到短,你也不可能改善自己的生活,你批评这个批评那个,看不上这个看不起那个,不会让你有成长,也不会提升你的影响力,你的影响力不是你对别人说长道短的能力,而是别人信赖你并希望得到你的帮助的现象。多交一些有想法的朋友,多把自己的想法付诸实践,那怕没有成功,你的人生也会比别人过得有意义。

如果你看过我以前的文章,你会看到一些吐槽性质的文章,而后面就再也没有了。另外,我也不再没有针对具体的某个人做出评价,因为人太复杂的了,经历的越多,你就会发现你很难评价人,与其花时间在评论人和事上,不如把时间花在做一些力所能及的事来改善自己或身边的环境。所以,我建议大家少一些对人的指责和批评,通过对一件事来引发你的思考,想一想有什么可以改善,有什么方法可以做得更好,有哪些是自己可以添砖加瓦的?你会发现,只要你坚持这么做,你个人的提升和对社会的价值会越来越大,而你的影响力也会越来越大

现在的我,即不是左派也不是右派,我不喜欢爱国主义,我也不喜欢崇洋媚外,我更多的时候是一个自由派,哪边我都不站,我站我自己。因为,生活在这样的一个时代,能让自己过好都是一些比较奢望的事了。

《教父》里有这样的人生观:第一步要努力实现自我价值,第二步要全力照顾好家人,第三步要尽可能帮助善良的人,第四步为族群发声,第五步为国家争荣誉。事实上作为男人,前两步成功,人生已算得上圆满,做到第三步堪称伟大,而随意颠倒次序的那些人,一般不值得信任。这也是古人的“修身齐家治国平天下”!所以,在你我准备要开始要“平天下”的时候,也得先想想,自己的生活有没有过好了,家人照顾好了么,身边有哪些力所能及的事是可以去改善的……

穷则独善其身,达则兼济天下。提升自己,实现自我,照顾好自己的家人,帮助身边的人。这已经很不错了!

什么样的人干什么样的事,什么样的阶段做什么样的选择,有人的说,选择比努力更重要的,我深以为然,而且,我觉得选择和决定,比努力更难,努力是认准了一个事后不停地发力,而要决定去认谁哪一个事则是令人彷徨和焦虑的(半途而废的人也很多)。面对人生,你每天都在作一个一个的决定,在做一个又一个的选择,有的决定大,有的决定小,你的人生的轨迹就是被这一个一个的决定和选择所走走出来的。

我在24岁放弃了一房子离开银行到小公司的时候,我就知道,人生的选择就是一个翘翘板,你要一头就没有另一头,选择是有代价的,你不选择的代价更大;选择是要冒险的,你不敢冒险的风险更大;选择是需要放弃的,因为无论怎么选你都要放弃什么。想想你老了以后,回头一看,好多事情在年轻的时候都不敢做,而你再也没有机会,你就知道不敢选择不敢冒险的代价有多大了。选择就是一种 trade-off,这世上根本不会有什么完美,只要你想做事,你有雄心壮志,你的人生就是一个坑接着一个坑,你所以做的就是找到你喜欢的方向跳坑。

所以, 你要想清楚你要什么,不要什么,而且还不能要得太多,这样你才好做选择。否则,你影响你的因子太多,决定不好做,也做不好。

就像最前面说的一样,你是激进派还是保守派,你是喜欢领导还是喜欢跟从,你是注重长期还是注重短期,你是注重过程还是注重结果……等等,你对这些东西的坚持和守护,成为了你的“三观”,而你的三观则影响着你的选择,而你的选择影响着你的人生。

下面是一些大家经常在说,可能也是大多数人关心的问题,就这些问题,我也谈谈我的价值取向。

挣钱。挣钱是一个大家都想做的事,但你得解决一个很核心的问题,那就是为什么别人愿意给你钱?对于挣钱的价值观从我大学毕业到现我就没怎么变过,那就是我更多关注的是怎么提高自己的能力,让自己值那个价钱,让别人愿意付钱。另外一方面,我发现,越是有能力的人,就越不计较一些短期得失,越计较短期得失的人往往都是很平庸的人。有能力的人不会关心自己的年终奖得拿多少,会不会晋升,他们更多的关心自己真正的实力有没有超过更多的人,更多的关注的是自己长远的成长,而不是一时的利益。聪明的人从来不关心眼前的得失,不会关心表面上的东西,他们更多关心的是长期利益,关心长期利益的人一定不是投机者,一定是投资者,投资会把自己的时间精力金钱投资在能让自己成长和提升的地方,那些让自己可以操更大的盘的地方,他们培养自己的领导力和影响力,而投机者在职场上会通过溜须拍马讨好领导,在学习上追求速成,在投资上使用跟随策略,在创业上甚至会不择手段,当风险来临时,投机者是几乎完全没有抗风险能力的,他们所谓的能力只不过因为形势好。

 

技术。对于计算机技术来说,要学的东西实在是太多,我并不害怕要学的东西很多,因为学习能力是一个好的工程师必需具备的事,我不惧怕困难和挑战。我觉得在语言和技术争论谁好谁坏是一种幼稚的表现, 没有完美的技术,Engineering 玩的是 Tradeoff。所以,我对没有完美的技术并不担心,但是我反而担心的是,当我们进入到一些公司后,这些公司会有一些技术上的沉淀也就是针对公司自己的专用技术,比如一些中间件,一些编程框架,lib库什么的。老实说,我比较害怕公司的专用技术,因为一旦失业,我建立在这些专用技术上的技能也会随之瓦解,有时候,我甚至害怕把我的技术建立在某一个平台上,小众的不用说了,大众的我也比较担扰,比如Windows或Unix/Linux上,因为一旦这个平台不流行或是被取代,那么我也会随之淘汰(过去的这20年已经发生过太多这样的事了)。为了应对这样的焦虑,我更愿意花时间在技术的原理和技术的本质上,这导致我需要了解各种各样的技术的设计方法,以及内在原理。所以,当国内的绝大多数程序员们更多的关注架构性能的今天,我则花更多的时间去了解编程范式,代码重构,软件设计,计算机系统原理,领域设计,工程方法……因为只有原理、本质和设计思想才可能让我不会被绑在某个专用技术或平台上,除非,我们人类的计算机这条路没走对。

 

职业。在过去20多年的职业生涯中,我从基层工程师做到管理,很多做技术的人都会转管理,但我却还是扎根技术,就算是在今天,还是会抠很多技术细节,包括写代码。因为我心里觉得,不写代码的人一定是做不好技术管理的,因为做技术管理有人要做技术决定,从不上手技术的人是做不好技术决定的,另一方面,我觉得管理是支持性的工作,不是产出性的工作,大多数的管理者无非是因为组织大了,所以需要管人管事,所以,必然要花大量的时间和精力处理各种问题,甚至办公室政治,然而,如果有一天失业了,大环境变得不好了,一个管理者和一个程序员要出去找工作,程序员会比管理者更能自食其力。所以,我并不觉得管理者这个职业有意思,我还是觉得程序员这个有创造性的职业更有趣。通常来说,管理者的技能力需要到公司和组织里才能展现,而有创造力的技能的人是可以自己独立的能力,所以,我觉得程序员的技能比管理者的技能能让我更稳定更自地活着。所以,我更喜欢“电影工作组”那样的团队和组织形式。

 

打工。对于打工,也就是加入一家公司工作,无论是在一家小公司还是一家大公司工作,都会有好的和不好的,任何公司都有其不完美的地方,这个需要承认。首先第一的肯定是完成公司交给你的任务,然后我会尽我所能在工作找到可以提高效率的地方进行改善。在推动公司/部门/团队在一技术和工程方面进步并不是一件很容易的事,因为进步是需要成本的,这成本并不一定是公司和团队愿意接授的,而另外,从客观规律上来说,一件事的进步一定是会有和现状有一些摩擦的。有的人害怕有摩擦而忍了,而我则不是,我觉得与别人的摩擦并不可怕,因为大家的目标都是基本一致的,只是做事的标准和方式不一样,这是可能沟通的,始终是会相互理解的。而如果你没有去推动一个事,我觉得对于公司对于我个人来说,都是一种对人生的浪费,敬业也好,激情也好,其就是体现在你是否愿意冒险去推动一件于公于私都有利的事,而不是成为一个“听话”、“随大流”、“懒政”的人,即耽误了公司也耽误了自己。所以,我更信仰的是《做正确的事情,等着被开除》,这些东西,可参看《我看绩效考核》,以及我在Gitchat上的一些问答

 

创业。前两天,有个小伙来跟我说,说他要离开BAT要去创业公司了,说在那些更自由一些,没有大公司的种种问题。我毫不犹豫地教育了他一下,我说,你选择这个创业公司的动机不对啊,你无非就是在逃避一些东西罢了,你把创业公司当做是一个避风港,这是不对的,创业公司的问题可能会更多,去创业公司的更好的心态是,这个创业公司在干的事业是不是你的事业?说白了,如果你是为了你的事业,为了解决个什么,为了改进个什么,那么,创业是适合你的,也只有在做自己事业的时候,你才能不惧困难,才会勇敢地面对一切那种想找一个安稳的避风港呆着的心态是不会让你平静地,你要知道世界本来就是不平静的,找了自己的归宿和目标才可能让你真正的平静。所以,在我现的创业团队,我不要求大家加班,我也不洗脑,对于加入的人,我会跟他讲各种问题和各种机遇,并一直在拷问他的“灵魂”,我们在做的事是不是你自己的事业诉求?可不可以更好?每个人都应该为自己的事业为自己的理想去活一次,追逐自己的事业和理想并不容易,需要有很大的付出,而也只有你心底里的那个理想值得这么大的付出……

 

客户。基于上述的价值观,在我现在创业的时候,我在面对客户的时候,也是一样的,我并不会完全的迁就于客户,我的一些银行客户和互联网客户应该体会到我的做的方式了,我并不觉得迁就用户,用户要什么我就应该给什么,用户想听什么,我就说什么,虽然这样可以省着精力,更圆滑,但这都不是我喜欢的,我更愿意鲜明地表达我的观点,并拉着用户跟我一起成长,因为我并不觉得完成客户的项目有成就感,我的成就感来自客户的成长。所以,面对客户有些做得不对有问题有隐患的地方,或是我有什么做错的事,我基本上都是直言不讳地说出来,因为我觉得这是对客户和对自己最大的尊重。我并不是在这里装,因为,我也想做一些更高级更有技术含量的事,所以,对于一些还达到的客户,我如果不把他们拉上来,我也对不起自己。

 

在我“不惑之年”形成了这些价值观体系,也许未来还会变,也许还不成熟,总之,我不愿跟大多数人一样,因为大多数人都是随遇而安随大流的,因为这样风险最小,而我想走一条属于自己的路,做真正的自己,就像我24岁从银行里出来时想的那样,我选择对了一个正确的专业(计算机科学),呆在了一个正确的年代(信息化革命),这样的“狗屎运”几百年不遇,如果我还患得患失,那我岂不辜负活在这样一个刺激的时代?!我所要做的就是在这个时代中做有价值的事就好了!这个时代真的是太好了!

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

陈皓 February 26, 2019 at 04:02PM
from 酷 壳 – CoolShell https://ift.tt/2U7TXdJ

VN:F [1.9.22_1171]
Rating: 0.0/5 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Share