├── images ├── nanchen12.jpg ├── nanchen15.jpg ├── nanchen30.jpg ├── nanchen50.jpg └── nanchen8.jpg ├── Android ├── 昨晚,我家地震了。.md ├── 每日一问:你了解 Java 虚拟机结构么?.md ├── 详细说一下 MeasureSpec.UNSPECIFIED.md ├── 每日一问:Android 中内存泄漏都有哪些注意点?.md ├── 每日一问:简述 View 的绘制流程.md ├── 每日一问:说说你对 LeakCanary 的了解.md ├── 每日一问:谈谈对 MeasureSpec 的理解.md ├── 每日一问:LayoutParams 你知道多少?.md └── Android 从零编写一个带标签的 TagTextView.md ├── kotlin ├── 分享一个 Kotlin 学习方式.md └── Better Kotlin.md ├── experience ├── 一眼就能望到头的人生,我想想都怕.md ├── 南尘的 2018,阅读本文大约需要一整年.md ├── 说说入职两天的感受.md ├── 说说过去一周的面试和想法.md └── 模拟面试分享.md ├── algorithm ├── 面试 11:Java 玩转归并排序.md ├── 面试 20:计算连续子数组的最大和(剑指 Offer 31 题).md ├── 面试 17:从上到下打印二叉树.md ├── 面试 4:避免用递归去解决斐波那契数列.md ├── 面试 19:输出数组中出现次数超过一半的数字(剑指 Offer 26 题).md ├── 面试 2:用 Java 逆序打印链表.md ├── 面试 8:面试常见的链表算法捷径(二).md ├── 面试 9:用 Java 实现冒泡排序.md ├── 面试 1:用 Java 实现一个 Singleton 模式.md ├── 面试 5:手写 Java 的 pow() 实现.md ├── 面试 15:顺时针从外往里打印数字.md ├── 面试 15:针对昨天的推文,有几句想说的.md ├── 面试 6:调整数组顺序使奇数位于偶数前面.md ├── 面试 7:面试常见的链表类算法捷径(一).md ├── 面试 16:栈的压入压出队列.md ├── 面试 10:Java 玩转选择排序和插入排序.md ├── 面试 18:复杂链表的复制(剑指 Offer 第 26 题).md ├── 面试 12:玩转 Java 快速排序.md ├── 面试 14:合并两个排序链表.md ├── 面试 3:查找旋转数组的最小数字.md └── 面试 13:基于排序算法的总结.md ├── others └── Git 如何遗弃已经 Push 的提交.md └── README.md /images/nanchen12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanchen2251/Blogs/HEAD/images/nanchen12.jpg -------------------------------------------------------------------------------- /images/nanchen15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanchen2251/Blogs/HEAD/images/nanchen15.jpg -------------------------------------------------------------------------------- /images/nanchen30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanchen2251/Blogs/HEAD/images/nanchen30.jpg -------------------------------------------------------------------------------- /images/nanchen50.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanchen2251/Blogs/HEAD/images/nanchen50.jpg -------------------------------------------------------------------------------- /images/nanchen8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanchen2251/Blogs/HEAD/images/nanchen8.jpg -------------------------------------------------------------------------------- /Android/昨晚,我家地震了。.md: -------------------------------------------------------------------------------- 1 | 嗨,朋友们,大家晚上好。很抱歉今天没能坚持给你们继续「每日一问」系列。原本是计划今天给大家讲讲多线程方面的知识的,但多次提笔,始终没能维持一个平静的思绪。 2 | 3 | 我觉得我是一个后知后觉的人,昨天晚上想必大家都知道,四川宜宾长宁县和珙县交界处发生了 6.1 级地震,发生的时候我还在家里健身,远在成都的我根本没有感觉,要不是室友提醒,我都不知道地震这回事。 4 | 5 | 随后,各种新闻爆料接踵而至,我觉得自己真是一个没心没肺的人,看到震中是我的家乡,我却觉得没啥大不了的,毕竟家里从小到大我经历的各种大大小小的地震应该不下 10 次,所以我非常平静地等洗澡完毕再给家里打电话。家人一切安好,没有任何亲人伤亡。 -------------------------------------------------------------------------------- /Android/每日一问:你了解 Java 虚拟机结构么?.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:你了解 Java 虚拟机结构么? 2 | 3 | 对于从事 C/C++ 程序员开发的小伙伴来说,在内存管理领域非常头疼,因为他们总是需要对每一个 `new` 操作去写配对的 `delete/free` 代码。而对于我们 Android 乃至 Java 程序员,却总是会因为虚拟机的自动内存管理机制而忽视内存管理的重要性。 4 | 5 | 经过前面简短的几篇纯 Android 问题,我想再给大家掺杂一点可能平时你并没有太多关注的东西。其实写这个的时候我相当纠结,因为对于大多数 Android 开发来说,他们会更加注重实质性的技能提升,而不是我今天将要讲的 Java 虚拟机结构。 6 | 7 | 但经过一系列的思想斗争,我还是打算把这个讲到底,为了不浪费大家太多的时间,我依然遵从「每日一问」系列的初衷,我们尽可能地精简文字,让每一个小知识点阅读时间都控制在 5 分钟以下。 8 | >Java 虚拟机结构作为一个面试高频考点,你完全可以当做你在复习面试知识,这样兴许你的学习的心态会好一些。 9 | 10 | ![](https://upload-images.jianshu.io/upload_images/3994917-eee9dd98cacca4ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 11 | 12 | 我还是不得不去网上摘下了这个图,我想唯有用这张图结合讲解起来才更加的通俗易懂。 13 | 14 | Java 虚拟机内存区域确实就由这几部分构成:方法区、虚拟机栈、本地方法栈、堆、程序计数器。 15 | 16 | #### 程序计数器 17 | 程序计数器是一个较小的内存空间,线程私有,**它是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。** 18 | 19 | #### 虚拟机栈 20 | 虚拟机栈和程序计数器一样,同样为线程所私有,并且生命周期和线程相同。。每个栈中的数据都是私有的,其他栈不允许访问,每个方法被执行的时候都会同时创建一个栈帧,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈从入栈到出栈的过程。**虚拟机栈主要存放各种编译期可知的基本数据类型和对象的引用。** 21 | 22 | #### 本地方法栈 23 | 本地方法栈与虚拟机栈发挥的作用非常相似,其主要区别是**虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地房发展则是为虚拟机用到的 Native 方法服务。** 24 | 25 | #### Java 堆 26 | **Java 堆是垃圾收集器管理的主要区域,主要用于存放对象的实例**,自然而然就成了 Java 虚拟机中管理内存最大的一块,并且它可以处于物理上不连续的内存空间中,Java 堆在虚拟机启动的时候就进行创建,并被所有线程所共享。 27 | 28 | #### 方法区 29 | 方法区和 Java 堆一样,是各个线程共享的内存区域,主要存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。这个区域的内存回收目标主要是针对常量池的回收和对类型的写在,较少发生垃圾收集行为。 30 | 31 | 上面对 Java 虚拟机结构进行了非常精简的讲解,大家可还对此清晰了一些?如果还是没有太透彻其实没有关系,多回顾几遍,最好能自己画一个图,在一边进行理解。明天我们再来讲一讲我们另外一个面试非常高频的考点:垃圾回收算法,一起来探究 Java 虚拟机到底是怎么来回收一个无用的对象的。 -------------------------------------------------------------------------------- /kotlin/分享一个 Kotlin 学习方式.md: -------------------------------------------------------------------------------- 1 | ## 分享一个 Kotlin 学习方式 2 | 3 | 2018 Google 开发者大会于今明两天在上海举办,想必不少开发者从四面八方都赶赴上海参会,毕竟 Google 爸爸搞的,那还是相当有价值和含金量的。 4 | 5 | 正如前面预言的一样,此次 Google 肯定是强烈推荐 Flutter 的,其实在国内外一些大厂,比如阿里、腾讯、Google 都在使用 Flutter 开发 App,阿里本身已经开源了跨平台开源框架 Weex,都在追随,也可见 Flutter 的魅力。 6 | 7 | 对于 Flutter 想必我的小伙伴们都知道,毕竟是 Google 爸爸的产物,这里就不多提了,除了 Flutter,此次 Google 大会也继续推荐了 Kotlin。 8 | 9 | 既 Kotlin 成为 Google 推荐的 Android 开发语言以来,它的市场份额一直在稳健上升,根据 Google 今天分享的开发者问卷调查显示,超过 40% 的开发者都已经选择使用 Kotlin,其中不乏抖音、咕咚这样的技术团队。 10 | 11 | ![](https://upload-images.jianshu.io/upload_images/3994917-6a64f4089edba77e.png) 12 | 13 | 之前本人也在公众号吐血推荐过一波 Kotlin,确实作为一个后知后觉的开发者来说,多少最开始还是对 Kotlin 有所抵触的,不过在真正接触以后,我发现我难以回头了。 14 | 15 | 不少小伙伴都问到到底怎么学习 Kotlin,之前其实给大家分享过一些资料。比如: 16 | 17 | - 官网的 Kotlin 中文中文站:https://www.kotlincn.net/docs/reference/ 18 | - GitHub 上开源的MindorksOpenSource:https://github.com/MindorksOpenSource/from-java-to-kotlin/blob/master/README-ZH.md 19 | - 非常适合 Android 开发者的 《Kotlin For Android Developers》一书:公众号后台回复「」可获取 PDF。 20 | - 非常不错的 From Java to Kotlin:https://fabiomsr.github.io/from-java-to-kotlin/ 21 | 22 | 真的太多了,这里就不一一例举了,上面的这些方式,无不就是学习语法,然后自己手动练习,或者是直接跟着书籍用 Kotlin 编写一个项目。 23 | 24 | 但今天我要推荐的是一种个人认为学习效率最高的方式,**在浏览器里面直接练习 Kotlin**。 25 | 26 | 主要是推荐这个网站:Try Kotlin:https://try.kotlinlang.org/ 27 | 28 | ![](https://upload-images.jianshu.io/upload_images/3994917-e150c2938e11cf74.png) 29 | 30 | 随便看一下,这个网站前面会用 Kotlin 给大家一些 Examples,后面会有 42 道练习题,编译器还可以自动给大家实时发现你代码中的语法问题。 31 | 32 | 细心的小伙伴还会发现,上面还可以支持把你的 Java 代码转换为 Kotlin,这和 Android Studio 3.0 以后自带的插件如出一辙,不得不说,这真是太棒了。 33 | 34 | > 呼叫 Try Kotlin 的作者给我广告费!!! 35 | 36 | 要说这个缺点,我觉得还真没找到,要真说,就排版略微不美观,并且是纯英文吧~ 37 | 38 | 啊,今天的文章很短,其实就是想把自己遇到的好东西,推荐给大家,喜欢的小伙伴去试试吧,兴许还有一些小惊喜在等着大家哟~ -------------------------------------------------------------------------------- /Android/详细说一下 MeasureSpec.UNSPECIFIED.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:详细说一下 MeasureSpec.UNSPECIFIED 2 | 3 | [前面的文章](https://www.jianshu.com/p/6cdbb418df46) 我留下了一个疑惑,那就是到底为什么 `NestedScrollView` 要把子 View 的测量模式强行设置为 `MeasureSpec.UNSPECIFIED` ,这不,在鸿洋的 "wanAndroid" 中,他再次提出了这样的问题: 4 | 5 | >MesureSpec.UNSPECIFIED 6 | >1. 这个模式什么时候会遇到? 7 | >2. 遇到后怎么处理? 8 | >3. 有什么注意事项? 9 | 10 | 下面摘自用户「陈小缘啦啦啦」的回答,我觉得回答的非常到位,特别在这里和大家分享一下。 11 | 12 | **`UNSPECIFID`,就是未指定的意思,在这个模式下父控件不会干涉子 View 想要多大的尺寸。** 13 | 那么,这个模式什么时候会`onMeasure()` 里遇到呢?其实是取决于它的父容器。 14 | 就拿最常用的 `RecyclerView` 做例子,在 `Item` 进行 `measure()` 时,如果列表可滚动,并且 `Item` 的宽或高设置了 `wrap_content` 的话,那么接下来,itemView 的 `onMeasure( )`方法的测量模式就会变成 `MeasureSpec.UNSPECIFIED`。 15 | 我们不妨打开 `RecyclerView` 源码,会在 `getChildMeasureSpec()` 方法里看到这么一句注释: 16 | > MATCH_PARENT can't be applied since we can scroll in this dimension, wrap instead using UNSPECIFIED. 17 | 18 | 它想表达的是:在可滚动的`ViewGroup`中,不应该限制 Item 的尺寸(如果是水平滚动,就不限制宽度),为什么呢? 因为是可以滚动的,就算 Item 有多宽,有多高,通过滚动也一样能看到滚动前被遮挡的部分。 19 | 20 | > 这里其实也就回答了我之前询问的 `NestedScrollView` 要强行设置 Item 为 UNSPECIFIED 的原因。 21 | 有同学可能会有疑问: 我设置 `wrap_content`,在 `onMeasure()` 中应该收到的是 `AT_MOST` 才对啊,为什么要强制变成 `UNSPECIFIED`? 22 | 23 | **这是因为考虑到 Item 的尺寸有可能超出这个可滚动的 `ViewGroup` 的尺寸,而在 `AT_MOST` 模式下,你的尺寸不能超出你所在的 `ViewGroup` 的尺寸,最多只能等于,所以用 `UNSPECIFIED`会更合适,这个模式下你想要多大就多大。** 24 | 25 | 那么,我们在自定义 View 的时候,在测量时发现是 `UNSPECIFIED` 模式时,应该怎么做呢? 26 | 27 | 这个就比较自由了,既然尺寸由自己决定,那么我可以写死为 50,也可以固定为 200。但还是建议结合实际需求来定义咯。 28 | 29 | 比如 `ImageView`,它的做法就是:有设置图片内容(drawable)的话,会直接使用这个 drawable 的尺寸,但不会超过指定的 `MaxWidth` 或 `MaxHeight`, 没有内容的话就是 0。而 `TextView` 处理 `UNSPECIFIED` 的方式,和 `AT_MOST` 是一样的。 30 | 31 | 当然了,这些尺寸都不一定等于最后 `layout` 出来的尺寸,因为最后决定子 `View` 位置和大小的,是在 `onLayout()` 方法中,在这里你完全可以无视这些尺寸,去 `layout()`成自己想要的样子。不过,一般不会这么做。 -------------------------------------------------------------------------------- /experience/一眼就能望到头的人生,我想想都怕.md: -------------------------------------------------------------------------------- 1 | ## 一眼就能望到头的人生,我想想都怕 2 | 3 | 又听见了一个很好的朋友想从公司离职。在决定告知他直属上级的时候,我们一起聊到很晚。一份还不错的工作,有着极佳的办公室氛围,还有一个团队灵魂一样的老大。辞职,意味着这一切可能都不复存在。 4 | 5 | 但他没有犹豫,只为了追寻自己的创业梦。 6 | 7 | 他也是我的同事,我曾试着找各种利处去挽留他,希望他再深思熟虑,毕竟在公司这个大家庭里,还有我,还有一些他舍不得的人。 8 | 9 | 他给我讲,身边的亲戚大多数都比自己的薪资更有说服力,每每谈到这个话题的时候,总觉得想找个地缝儿钻进去。 10 | 11 | 要只是这个问题,其实还好,因为他工作中非常努力,是一个非常上进并且优秀的人。 12 | 13 | 收入并不高,工作也比较累,还夹杂着无限的责任,对于无数人而言,这都是驱使他们离职的理由。 14 | 15 | 但朋友都不是因为这些,他最主要的原因,还是 **来自对自己的思考。** 16 | 17 | 他现在的工作大多数是比较重复的工作,虽然也有不少能学习到技能的地方,但依然是 **没有风景,没有旁逸斜出的情节,每一天,都像前一天的投影。** 18 | 19 | 一切的一切,都让他感到莫名恐慌。 20 | 21 | **他说他都快 30 岁了,每天做着这些工作,一眼就看到了自己的未来。**一览无余的余生,缺乏悬念的生命,这种感觉真他妈难受。你想,这个世界千变万化,**而自己却活成了一个标本。** 22 | 23 | 他说他不想做一个「标本」,他还有大把大把的梦想想去实现,即使创业失败,他也心安理得。 24 | 25 | 生活中并不缺乏对自己的薪资满怀抱怨的人,曾经我也是。我们这些工薪阶层,唯一的企盼就是老板能给自己多加点工资,好让自己能尽快过上更加富足的生活。 26 | 27 | 然而不少人只是停留在抱怨上,他们一边羡慕着别人的薪资,一边在心里默默质问自己的老大,为什么还不给自己涨工资? 28 | 29 | 我们 CEO 曾经说过一句话:**八卦是人类进步的阶梯。** 30 | 31 | 在《人类简史》这本书中,有句和 CEO 说的异曲同工的话。确实,在工作还是生活中,都有数不尽的八卦姿态。而**八卦也被称为人类语言最独特的地方,因为它可以传达一些根本不存在的事物的信息。** 32 | 33 | 在程序员的生活中,这个现象尤其明显。网上不少人就在整天逛着各种技术论坛,八卦着哪个哪个库的优势劣汰,但其实自己,根本就没有用心去思考过,只是人云亦云。 34 | 35 | 于是,就有不少人说:「哇,这个库 Star 这么多,一定超好用。哇,这个文章阅读量这么高,一定是学习的良品。」 36 | 37 | **各种给公司使用 Star 数多的库,这本身就是一种不负责任的体现。** 38 | 39 | 工作生活中,总会遇到这么一群得过且过的人,他们每天的工作似乎就是挑选自己能做的先做完,不能做的就一直放着,也不去思考应该怎么处理会更好,也不去让自己的同事知道自己的进度。 40 | 41 | 昨天在网上看到一则段子,讲述的是一个员工看到老板新提的豪车,问老板在工作中应该怎么办的时候。老板的回复着实让我惊讶,在宣扬了一堆努力工作的方法的时候,最后神结尾道,**公司多几位这样努力的员工,那明年我就能提一辆比这更加豪的豪车了。** 42 | 43 | 真是对现在社会的一种赤裸裸地讽刺。但我觉得段子的作者多半是不上进整天抱怨薪资的那类人吧。 44 | 45 | **我从来不相信哪个公司的老板会亏待那些真正努力为公司创造了价值的员工。** 46 | 47 | 如果你刚好是,那你应该好好质问一下自己:**当初是怎么瞎的眼,成为了这个公司的员工。** 48 | 49 | 最近在公众号新更新了一个系列,关于面试,看到一起学习的小伙伴连我之前发感悟文章的五分之一都不到,深感痛心。 50 | 51 | 难道南尘的朋友们,已经沦为了大多数都习惯过 **感悟时汹涌澎湃,然后再一如既往的生活** 了么? 52 | 53 | 没什么出众的文笔,我也就在这瞎比比而已,只是希望大家能有所感悟。 54 | 55 | **别过一眼就能看到未来的日子,别让自己的生活就像标本。** 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Android/每日一问:Android 中内存泄漏都有哪些注意点?.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:Android 中内存泄漏都有哪些注意点? 2 | 3 | 内存泄漏对每一位 Android 开发一定是司空见惯,大家或多或少都肯定有些许接触。大家都知道,每一个手机都有一定的承载上限,多处的内存泄漏堆积一定会堆积如山,最终出现内存爆炸 OOM。 4 | 5 | 而这,也是极有可能在 Android 面试中一道常见的开放题。 6 | 7 | 内存泄漏的根本原因是**一个长生命周期的对象持有了一个短生命周期的对象。**如果你对垃圾回收机制有所了解,我想这个问题基本难不住你,因为知道了原理,自然不会去触碰这些极易导致内存泄漏的雷区。 8 | 9 | > 该题重在积累,不需要死记硬背,自己多总结即可。 10 | 11 | ### 1. 长生命周期对象持有 Activity 12 | 13 | 这基本是最常见的内存泄漏了,比如 14 | 15 | - 内部类形式使用 Handler 同时发送延时消息,或者在 Handler 里面执行耗时任务,在任务还没完成的时候 Activity 需要销毁。这时候由于 Handler 持有 Activity 的强引用导致 Activity 无法被回收。 16 | - 同理内部类形式的使用 AsyncTask 执行耗时任务也会导致内存泄漏的发生。 17 | - 单例作为最长生命周期的对象,自然不应该持有 Activity 从而导致内存泄漏发生; 18 | 19 | 针对上面这种情况,基本不必多说了,不要使用内部类或者匿名内部类做这样的处理就好了,实际上 IDE 也会弹出警告,我想大家应该还是都知道采用静态内部类或者在销毁页面的时候使用相关方法移除处理的。 20 | 21 | > `Activity` 中匿名使用 `Handler` 实际上会导致 `Handler` 内部类持有外部类的引用,而 `SendMessage()` 的时候 `Message` 会持有 `Handler`,`enqueueMessage` 机制又会导致 `MeassageQueue` 持有 `Message`。所以当发送的是延迟消息那么 `Message` 并不会立即的遍历出来处理而是阻塞到对应的 `Message` 触发时间以后再处理。那么阻塞的这段时间中页面销毁一定会造成内存泄漏。 22 | 23 | ### 2. 各种注册操作没有对应的反注册 24 | 25 | 这一点基本不必多说,相信大家刚刚开始学习广播和 Service 的时候一定对此有所接触,然后就是比如我们常用的第三方框架 EventBus 也是一样的。平时使用的时候注意在对应的生命周期方法中进行反注册。 26 | 27 | ### 3. Bitmap 使用完没有注意 recycle() 28 | 29 | Bitmap 作为大对象,在使用完毕一定要注意调用 `recycle()` 进行回收。`TypedArray` 、`Cursor`、各种流同理,一定要在最后调用自己的回收关闭方法处理。 30 | 31 | ### 4. WebView 使用不当 32 | 33 | WebView 是非常常用的控件,但稍有不注意也会导致内存泄漏。内存泄漏的场景: 很多人使用 Webview 都喜欢采用布局引用方式, 这其实也是作为内存泄漏的一个隐患。当 Activity 被关闭时,Webview 不会被 GC 马上回收,而是提交给事务,进行队列处理,这样就造成了内存泄漏, 导致 Webview 无法及时回收。 34 | 35 | 目前所知的比较安全的方案是: 36 | 37 | - 在布局中动态添加 WebView。 38 | - 采用下面的方法。 39 | 40 | ```kotlin 41 | override fun onDestroy() { 42 | webView?.apply { 43 | val parent = parent 44 | if (parent is ViewGroup) { 45 | parent.removeView(this) 46 | } 47 | stopLoading() 48 | // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错 49 | settings.javaScriptEnabled = false 50 | clearHistory() 51 | removeAllViews() 52 | destroy() 53 | } 54 | } 55 | ``` 56 | 57 | ### 5. 循环引用 58 | 59 | 循环引用导致内存泄漏比较少见,正常来讲不会有人写出 A 持有 B,B 持有 C,C 又持有A 这样的代码,不过总还是需要注意。 60 | 61 | 总的来说,内存泄漏很常见,但检测方式也很多。我们的 Android Studio 自带的 Monitors 就可以帮我们找到大部分内存问题,当然我们也可以采用譬如 LeakCanary 这样的库去做检测。 62 | 63 | -------------------------------------------------------------------------------- /experience/南尘的 2018,阅读本文大约需要一整年.md: -------------------------------------------------------------------------------- 1 | ## 南尘的 2018,阅读本文大约需要一整年 2 | 3 | 嗨,陌生人,你是我的朋友吗?我是南尘,还是那个在网上习惯加个 2251 后缀的南尘。 4 | 5 | 你最近过的怎么样呀?应该还好吧。有没有好好工作,好好学习,好好生活呀? 6 | 7 | 距离南尘的上一篇原创,好像得有俩月了吧,好像,在 2018 年,南尘就一直在玩消失。 8 | 9 | 刚刚看了下 [GitHub](https://github.com/nanchen2251),过去的一年里,提交数量不到 50 次,虽然 Stars 和 Fllowers 都熙熙攘攘地长了一些,不过似乎确实好久没有产生什么新的内容啦。 10 | 11 | 在简书上看了一下自己的上一篇年终总结,花了一整年观看的 2017 年总结,早已悄然离开。2018 年,差点又悄然路过。 12 | 13 | 2018 年是一个让人恍惚的一年,各种裁员风波异军突起,大家有被影响到吗? 14 | 15 | 看起来好像每一年都是 Android 的寒冬的样子,但如果你的技术足够,即使你成了裁员潮下的亡命之徒,你会担心找不到下家吗? 16 | 17 | 好像并不会。做技术,从来都是和企业一样,优胜劣汰,你唯一能做的,只能是不断深入学习,让自己有一技之长。 18 | 19 | 好像有些许偏题了呢,不是说南尘的 2018 吗?怎么扯的渐行渐远啦。 20 | 21 | 2018 年,南尘被提拔为了「技术总监助理」,开始做着部分项目和部门的管理和推动工作,才真正体会到了管理者的辛酸。感觉自己一会又开始开发 Python,一会又开始运维,一会又开始测试,又一会开始打开 Axure 设计起了原型。 22 | 23 | 2018 年,带着对「致学」人文无限的不舍,为了追逐技术的深度,南尘终于跳槽啦。从头条到抖音,再到火山,心疼的是南尘均没有踏出成都。成都就像一座带着我无限感情的城市一样,始终不渝地牵扯着我。还好在秋天的 8 月,我和咕咚一拍即合,让咕咚成为了我 Android 路上的第二家企业。 24 | 25 | 咕咚,一个挺有诗意的名字,一直到现在,都远超我心中的期望。我是一个煽情并且容易被身边事情动容的人,而咕咚,不论是技术,还是人文,都遵从着把细节做到最好,我很开心能有这样一群上进的小伙伴,虽然我的环境总是独一无二地出事儿。 26 | 27 | 这里会有好多好多的人,在技术上帮助自己,比如一面就把我带上气氛的 **Blue**,比如早已声名在外的 **Hideeee**,还有一些我暂时还不知道他们网络昵称的大佬们,慢慢地,我才发现,原来最好的工作就是这样,薪资已经不是第一要素。 28 | 29 | 2018 年,总算在公众号开启了送书福利和红包福利,也总算有了文末广告的权利,推文却一天不如一天,南尘也一天比一天愧疚。 30 | 31 | 2018 年,我开始接触使用 Kotlin,在成为纯 Kotlin Android 开发后,进行了 [Better Kotlin](https://mp.weixin.qq.com/s/E83nfE2w8S9gE9WcQwv8Sg) 的技术分享,还分享了一个自己认为的 [较好 Kotlin 学习方式](https://mp.weixin.qq.com/s/hOLZEozcVp-aXyPwO0gQDA)。 32 | 33 | 2018 年,写了一堆关于 [Android 面试的题集](https://mp.weixin.qq.com/s/Ix6V6e3AS8nNcJ84TEbNCg),浑浑噩噩,多多少少,流流水水。 34 | 35 | 2018 年,当初写的那一套 [RxJava 2.x 入门教程](https://www.jianshu.com/p/0cd258eecf60) 阅读暴涨,其实并没有写的很深入,没想到却成了不少 Android 开发的 RxJava 入门教程,虽说离扔物线朱凯的 RxJava 教程还相差甚远,但至少对人有所帮助,所以甚是欣慰。 36 | 37 | 2018 年,在公众号上坚持了半年之久的 [模拟面试](https://mp.weixin.qq.com/s/WRdSmGxwDp-CpQcSlmJYlg) 从免费到收费,从一开始的无人问津到后面各种网站找我挂友链,到最后因为报名人数太多而使自己不堪重负被迫关停,直到现在,都还没有重新开启。 38 | 39 | 2018 年,没有再去 Android 巴士成都站担任讲师,因为深知 17 年的分享做的太粗浅,不想再一次浪费大家的时间。倒是去了偶像朱凯大人的 HenCoder Plus 瞎比比了一个半小时,也是把自己所有积累慷慨地分享成了 [文字](https://mp.weixin.qq.com/s/rP1vTJpoLlqj9jfj13WKeQ),哪怕只能帮到一个人。 40 | 41 | 2018 年,熙熙攘攘,[GitHub](https://github.com/nanchen2251) 上的 AiYa 系列又新增了一名成员 [AiYaScanner](https://github.com/nanchen2251/AiYaScanner),以 Zxing 和 Zbar 结合,只为极速扫码而生,希望大家多多众筹代码和提 bug,我想认真维护好这个库。 42 | 43 | 2018,浑浑噩噩,洋洋洒洒,不再想提及感情。 44 | 45 | ### 展望 2019 46 | 47 | 还是需要一如常态,展望一下 2019。 48 | 49 | 1. 希望能把模拟面试再重新开启一下,毕竟这是我在技术圈里以来,发现最有意义的事。 50 | 2. 还是希望自己能把架构方面挖一下吧,毕竟不想一直就做需求开发工程师。 51 | 3. 不敢奢求能发多少篇推文,但求每一篇都能带来无限制的价值。 52 | 4. 最最重要的还是,不管你在哪里,不管你对我是喜欢还是厌恶,都希望你能开心每一天~ 53 | 54 | -------------------------------------------------------------------------------- /experience/说说入职两天的感受.md: -------------------------------------------------------------------------------- 1 | ## 说说入职两日的感受 2 | 3 | 伙计们,做好准备吧,南尘最近一定不可能日更的,不过不保证后面还会像现在这样熟悉架构熟悉代码到极度困,然后就想到我亲爱的朋友们,然后再和你们吹会儿逼。 4 | 5 | 前面给大家讲过,选择了待遇相对偏低的咕咚,主要是因为一面的面试官,给了我很强的震撼力,让我如同找到了同路人:同样在为代码质量而疯狂努力。 6 | 7 | 今天,在他的指引下,总算对咕咚的架构有了较为深刻的理解。 8 | 9 | 果然,大一点的项目,总需要一个靠谱的架构,不然一定会面临各种各样的问题。 10 | 11 | 确实很刺激,这会儿公司还有一半多的员工在疯狂干着自己喜欢的事。但丝毫不会影响,南尘会是那个每天来的最早的人~ 12 | 13 | 今天看到致学发的关于我离职的文章,确实挺心酸的,不过好聚好散,还好我选择了咕咚这样一家还算注重技术的公司,我相信每一个致学人,也会饱含祝福。 14 | 15 | 对我来说,公司不在乎体量,我最在乎的还是团队对技术的饥渴,很幸运在这一点,咕咚让我足够满意! 16 | 17 | 咕咚强制采用 DataBinding && MVVM && ConstraintLayout 进行编写代码,之前也是一直没有去学习了解 ConstraintLayout 这个神奇的布局,今天一看,真心超赞,但使用文章我就不写了,鸿洋和郭霖已经把拖拽和直接手写代码的方式都讲的很清楚了,感兴趣的到他们的 CSDN 博客去仔细观摩观摩吧~ 18 | 19 | > 鸿洋的 ConstraintLayout 文章地址:https://blog.csdn.net/lmj623565791/article/details/78011599 20 | > 21 | > 郭霖的 ConstraintLayout 文章地址:https://blog.csdn.net/guolin_blog/article/details/53122387 22 | 23 | 对于 DataBinding && MVVM,可能小项目感觉不是很明显,但相对体量大一点的项目就真的太有价值了解学习了。这也难怪,咕咚和美团都在面试的时候问了我 MVVM。 24 | 25 | 然后,KotLin 的话,看了咕咚的代码,大概目前覆盖比例 15%,最近本宝宝也是好好学习了一波 Kotlin,只能说,自从 Google 开始推荐 Kotlin 后,我们就不得不学习了。 26 | 27 | > Kotlin 中文教程网站:https://www.kotlincn.net/docs/reference/ 28 | > 29 | > **强烈推荐书籍 《Kotlin for Android Developers》,目前中文版的 PDF 可在公众号后台回复 "kotlin" 获取,但更强烈推荐直接查看原作书籍!!!** 30 | 31 | 大概也没啥好说的,和我亲爱的朋友们交流了一下,感觉状态好了很多,我还是去默默做加班 dog 吧~ 32 | 33 | 额,好像忘了一件事,之前不少小伙伴留言问我面试答案。 34 | 35 | 在这里再说一下,面试这个东西,真的没有标准答案,不过我可以给大家简单讲一下思路,要是以后有了时间,再详细讲吧。 36 | 37 | #### RecyclerView 到底如何适配多种布局? 38 | 39 | 我看到问的最多的一个问题是,「RecyclerView 一个适配器如何适配多种布局」。 40 | 41 | 老实说,这个问题,我第一反应就是网上被人都写烂了的万能适配器,所以回答的就是根据不同的 Type 去设置 ViewHolder,毕竟我们通常设置 RecyclerView 的 Header 和 Footer 就是通过这样的方式来实现的。但这样的方式有一个非常严重的问题,就是其实根本就不万能,当我们遇到各种 Item 布局的时候,我们又得重新维护 ViewHolder,一旦这个布局方式多了起来,就会存在严重的维护问题。 42 | 43 | 那我们还能有怎样的思路来处理呢? 44 | 45 | 实际上,我们大多数,甚至是所有页面都可以用 RecyclerView 来实现,只是每一项的 Item 显示方式不一样而已。为了减少维护成本,我们显然不应该把判断是哪种 Type 的代码放在 RecyclerView 的「万能」适配器中。而应该把这个逻辑抽象成一个接口,然后让子类去自由发挥。然后在外面调用的时候,我们就只需要根据 model 的数据进行不一样的布局填充就可以了。 46 | 47 | 你可能会有点晕,其实我自己也一样,原谅我现在是从早上 7 点半一直干到现在的人,但我还是希望你能多看几遍。 48 | 49 | 好吧,看了好几遍了,还是一脸懵逼,姑且点到为止吧,时间关系,后面再做详细阐述。 50 | 51 | #### 上千个 Shape 文件如何维护的问题。 52 | 53 | 这是另外一个大家很关注的问题,在咕咚的面试中,提到了 CardView 不利好的一面,并阐述了自己面临成千上万个 Shape 文件无法统一维护管理的僵局。 54 | 55 | 这个题,其实我认为可能没有标准答案,只是面试官希望看你是否是一个喜欢并且善于思考的人吧。 56 | 57 | 一个开发人员稍微多一点的项目一定会遇到这样的难题,我们很难统筹所有经手这个项目的小伙伴都能认真先去看一遍别人 Shape 里面的实现,更多的时候会采用 CardView 或者自己新写一个 Shape 文件的方式。 58 | 59 | CardView 可能功能没有那么全面,而 Shape 可能面临维护难题。 60 | 61 | 这是很现实的问题,那到底怎么解决这个尴尬的窘境呢? 62 | 63 | 经过思考,我们似乎可以通过自定义一个 View,支持各种圆角和其他的 Shape 或者 CardView 具备的功能就好啦~ 64 | 65 | 可能有点投机取巧,不过至少说明我们很爱思考,哈哈。 66 | 67 | #### 写在最后 68 | 69 | 还有不少人问我 API 选择短连接而不是长连接的问题,我觉得这个问题,应该可以 Google 到吧,我就不想多提了。你可以思考一下,且不考虑客户端的性能问题,服务器接受 N 个来自客户端的长连接会怎样~巴拉巴拉~ 70 | 71 | 差不多了,这下要真的去加班 dog 了,要是大佬们看到错别字,还请见谅,直接指出。我已经检查了 3 遍了,但我这个状态,恐怕难以处理~ 72 | 73 | -------------------------------------------------------------------------------- /algorithm/面试 11:Java 玩转归并排序.md: -------------------------------------------------------------------------------- 1 | ## 面试 11:Java 玩转归并排序 2 | 3 | 前面讲了冒泡、选择、插入三种简单排序,时间复杂度都是 O(n²),今天,我们终于迎来了更高级的排序:**归并排序。** 4 | 5 | 虽然在这之前还有希尔排序和堆排序,但由于时间关系,我们这里就直接跳过,确实感兴趣的请直接 Google。 6 | 7 | ## 归并排序 8 | 9 | 我们总是可以将一个数组一分为二,然后二分为四,直到每一组只有两个元素,这可以理解为个递归的过程,然后将两个元素进行排序,之后再将两个元素为一组进行排序。直到所有的元素都排序完成。同样我们来看下边这个动图。 10 | 11 | ![图片来源于网络](https://user-gold-cdn.xitu.io/2018/3/1/161e0ae4c76803f3?imageslim) 12 | 13 | 归并排序算法是采用分治法的一个非常典型的应用,且各层分治递归可以同时进行。 14 | 15 | #### 归并算法的思想 16 | 17 | 归并算法其实可以分为递归法和迭代法(自底向上归并),两种实现对于最小集合的归并操作思想是一样的。区别在于如何划分数组,我们先介绍下算法最基本的操作: 18 | 19 | 1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列; 20 | 2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置; 21 | 3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; 22 | 4. 重复步骤 3 直到某一指针到达序列尾; 23 | 5. 将另一序列剩下的所有元素直接复制到合并序列尾。 24 | 25 | 我们来看看 Java 递归代码是怎么实现的: 26 | 27 | ```java 28 | public class Test09 { 29 | 30 | private static void swap(int[] arr, int i, int j) { 31 | int temp = arr[i]; 32 | arr[i] = arr[j]; 33 | arr[j] = temp; 34 | } 35 | 36 | private static void printArr(int[] arr) { 37 | for (int anArr : arr) { 38 | System.out.print(anArr + " "); 39 | } 40 | } 41 | 42 | private static void mergeSort(int[] arr) { 43 | if (arr == null) 44 | return; 45 | mergeSort(arr, 0, arr.length - 1); 46 | } 47 | 48 | private static void mergeSort(int[] arr, int start, int end) { 49 | if (start >= end) 50 | return; 51 | // 找出中间索引 52 | int mid = start + (end - start >> 1); 53 | // 对左边数组进行递归 54 | mergeSort(arr, start, mid); 55 | // 对右边数组进行递归 56 | mergeSort(arr, mid + 1, end); 57 | // 合并 58 | merge(arr, start, mid, end); 59 | } 60 | 61 | private static void merge(int[] arr, int start, int mid, int end) { 62 | // 先建立一个临时数组,用于存放排序后的数据 63 | int[] tmpArr = new int[arr.length]; 64 | 65 | int start1 = start, end1 = mid, start2 = mid + 1, end2 = end; 66 | // 创建一个下标 67 | int pos = start1; 68 | // 缓存左边数组的第一个元素的索引 69 | int tmp = start1; 70 | while (start1 <= end1 && start2 <= end2) { 71 | // 从两个数组中取出最小的放入临时数组 72 | if (arr[start1] <= arr[start2]) 73 | tmpArr[pos++] = arr[start1++]; 74 | else 75 | tmpArr[pos++] = arr[start2++]; 76 | } 77 | // 剩余部分依次放入临时数组,实际上下面两个 while 只会执行其中一个 78 | while (start1 <= end1) { 79 | tmpArr[pos++] = arr[start1++]; 80 | } 81 | while (start2 <= end2) { 82 | tmpArr[pos++] = arr[start2++]; 83 | } 84 | // 将临时数组中的内容拷贝回原来的数组中 85 | while (tmp <= end) { 86 | arr[tmp] = tmpArr[tmp++]; 87 | } 88 | 89 | } 90 | 91 | public static void main(String[] args) { 92 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 93 | mergeSort(arr); 94 | printArr(arr); 95 | } 96 | } 97 | ``` 98 | 99 | 归并排序算法总的时间复杂度是 O(nlogn),而且这是归并排序算法中最好、最坏、平均的时间性能。 100 | 101 | 而由于在归并排序过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时压入栈的数据占用的空间:n + logn,所以空间复杂度为 O(n)。 102 | 103 | #### 总结 104 | 105 | 归并排序虽然比较稳定,在时间上也是非常有效的,但是这种算法很消耗空间,一般来说只有在外部排序才会采用这个方法,但在内部排序不会用这种方法,而是用快速排序。明天,我们将带来排序算法中的王牌:快速排序。 106 | 107 | -------------------------------------------------------------------------------- /algorithm/面试 20:计算连续子数组的最大和(剑指 Offer 31 题).md: -------------------------------------------------------------------------------- 1 | # 面试 20:计算连续子数组的最大和(剑指 Offer 31 题) 2 | 3 | 我们上一次推文留下的题目来源于《剑指 Offer》第 31 题:计算连续子数组的最大和。 4 | 5 | > 面试题:输入一个整型数组,数组中有正数也有负数。数组中一个或多个整数形成一个子数组,求所有子数组的和的最大值,要求时间复杂度为 O(n)。 6 | > 比如输入 {1, -2, 3, 10, -4, 7, 2, -5},能产生子数组最大和的子数组为 {3,10,-4,7,2},最大和为 18。 7 | 8 | ## 准备测试用例 9 | 10 | 我们首先准备好测试用例,该题的测试用例也很简单。 11 | 12 | 1. 输入错误的值,比如空数组或者空指针,应该抛出异常; 13 | 2. 输入一个数组,数组包含正数和负数; 14 | 3. 输入一个数组,数组全是正数; 15 | 4. 输入一个数组,数组全是负数。 16 | 17 | ## 思考程序逻辑 18 | 19 | 看到本题,最直观的想法肯定是分出输入数组的所有子数组,并对它们一一求和,最后找到最大和对应的子数组。但一个长度为 n 的数组,总共都有 n(n+1)/2 个子数组,最快也需要 O(n²),所以显然不符合题目要求的 O(n) 算法。 20 | 21 | 我们再看题干,尝试从头到尾累加示例数组中的每个数字。 22 | 23 | 我们把和初始化和为 0。第一步加上第一个数字 1, 此时和为 1。接下来第二步加上数字 -2,和就变成了 -1。第三步加上数字 3。我们注意到由于此前累计的和是 -1 ,小于 0,那如果用 -1 加上 3 ,得到的和是 2 , 比 3 本身还小。也就是说从第一个数字开始的子数组的和会小于从第三个数字开始的子数组的和。因此我们不用考虑从第一个数字开始的子数组,之前累计的和也被抛弃。 24 | 25 | 我们从第三个数字重新开始累加,此时得到的和是 3 。接下来第四步加 10,得到和为 13 。第五步加上 -4, 和为 9。我们发现由于 -4 是一个负数,因此累加 -4 之后得到的和比原来的和还要小。因此我们要把之前得到的和 13 保存下来,它有可能是最大的子数组的和。第六步加上数字 7,9 加 7 的结果是 16,此时和比之前最大的和 13 还要大, 把最大的子数组的和由 13 更新为 16。第七步加上 2,累加得到的和为 18,同时我们也要更新最大子数组的和。第八步加上最后一个数字 -5,由于得到的和为 13 ,小于此前最大的和 18,因此最终最大的子数组的和为18 ,对应的子数组是 {3, 10, -4, 7, 2}。 26 | 27 | 我们还是用表格表示,可能会更加清晰。 28 | 29 | | 步骤 | 操作 | 累加的子数组和 | 最大的子数组和 | 30 | | ---- | ------------------- | -------------- | -------------- | 31 | | 1 | 加 1 | 1 | 1 | 32 | | 2 | 加 -2 | -1 | 1 | 33 | | 3 | 舍弃前面的 -1,加 3 | 3 | 3 | 34 | | 4 | 加 10 | 13 | 13 | 35 | | 5 | 加 -4 | 9 | 13 | 36 | | 6 | 加 7 | 16 | 16 | 37 | | 7 | 加 2 | 18 | 18 | 38 | | 8 | 加 -5 | 13 | 18 | 39 | 40 | 表格后确实清晰太多了,可见 **我们平时有想法不一定好写代码,但做了表格,思路绝对清晰的多**。当然你是大佬,完全是可以舍弃表格建思路的。 41 | 42 | 用上面的这种思路写代码就是: 43 | 44 | ```java 45 | public class Test20 { 46 | 47 | private static int findTheNumOfSubArray(int[] nums) { 48 | if (nums == null || nums.length == 0) 49 | throw new RuntimeException("the length of input must be large than 0!"); 50 | // 用 result 存放返回结果,即最大和 51 | int result = Integer.MIN_VALUE; 52 | // 用 sum 存放当前累加结果 53 | int sum = 0; 54 | for (int i = 0; i < nums.length; i++) { 55 | // 如果小于 0,则直接说明不可能是从前面开始的。不加之前的值,直接算当前值 56 | if (sum < 0) { 57 | sum = nums[i]; 58 | } else { 59 | // 如果大于 0,则相加 60 | sum += nums[i]; 61 | } 62 | // 如果添加后的值大于之前存放的最大值,则更新最大值 63 | if (sum > result) 64 | result = sum; 65 | } 66 | return result; 67 | } 68 | 69 | 70 | public static void main(String[] args) { 71 | int[] nums1 = {1, -2, 3, 10, -4, 7, 2, -5}; 72 | System.out.println(findTheNumOfSubArray(nums1)); 73 | int[] nums2 = {1,2,3,4,5}; 74 | System.out.println(findTheNumOfSubArray(nums2)); 75 | int[] nums3 = {-1,-2,-3,-4 -5}; 76 | System.out.println(findTheNumOfSubArray(nums3)); 77 | } 78 | } 79 | ``` 80 | 81 | 分别测试测试用例,测试通过。 82 | 83 | 最后再说点题外的吧,不少人现在还在找我「模拟面试」,该项活动已经截止很久啦,虽然本宝宝的菜单入口依然还在,那只能说明南尘等空闲一点会继续开放的,所以敬请期待洛~ 84 | 85 | 另外一点,南尘公号可能最近一段时间不会日更了,可能一星期,可能一个月,可能更长…但南尘不会辜负大家的陪伴~ -------------------------------------------------------------------------------- /algorithm/面试 17:从上到下打印二叉树.md: -------------------------------------------------------------------------------- 1 | # 面试 17:从上到下打印二叉树 2 | 3 | 在昨天的推文中,我们给大家讲了利用图表来构建自己的程序思路,在最后还给大家留下了一道拓展题,我们来看看大家有没有从中得到提升。 4 | 5 | > 面试题:从上到下打印二叉树的每个结点,同一层按照从左到右的顺序打印。例如数的结构如下: 6 | > 7 | > ​ 1 8 | > 2 3 9 | > 4 5 6 7 10 | > 11 | > 则依次打印 1、2、3、4、5、6、7 12 | 13 | ## 提前思考测试用例 14 | 15 | 依然是我们的第一步,提前想好我们的测试用例。 16 | 17 | 1. 传入一个空树,什么也不打印; 18 | 2. 传入题干上的树,打印正确结果; 19 | 3. 传入部分有子结点,部分没有子结点的非完全二叉树,打印正确结果。 20 | 21 | ## 提炼程序逻辑 22 | 23 | 这是一道二叉树的遍历问题,不同点在于它和我们平常所学习的前序遍历、中序遍历后后序遍历,看起来比较简单,但却很难抽象。 24 | 25 | 如题,我们希望打印的顺序是先打印根结点,然后再依次打印左结点和右结点,再继续打印左结点的左右结点,依次类推...... 26 | 27 | 我们知道,要打印一个结点的左右结点,根据二叉树的定义可得知,我们必须得先知道这个结点,所以我们一定得找到一个容器去装下它。 28 | 29 | 快速过一遍我们的容器类,我们比较容易知道队列可以极好地达到我们的要求,因为它的先进先出和我们这个需求非常吻合。 30 | 31 | 但比较遗憾的是,即使想到了这一点,有的小伙伴也容易犯迷糊,所以我们不得不用昨天所学习到的图表法进行思路地整理。 32 | 33 | | 步骤 | 操作 | 队列 | 34 | | ---- | -------------------------------- | ---------- | 35 | | 1 | 打印结点 1,入队左右结点 | 2、3 | 36 | | 2 | 打印结点 2,入队左右结点 | 3、4、5 | 37 | | 3 | 打印结点 3,入队左右结点 | 4、5、6、7 | 38 | | 4 | 打印结点 4,无子结点无需入队操作 | 5、6、7 | 39 | | 5 | 打印结点 5,无子结点无需入队操作 | 6、7 | 40 | | 6 | 打印结点 6,无子结点无需入队操作 | 7 | 41 | | 7 | 打印结点 7,无子结点无需入队操作 | | 42 | 43 | ## 编写代码 44 | 45 | 有了上面的思路,我们便可以开始编写代码了。 46 | 47 | ```java 48 | public class Test17 { 49 | 50 | private static class TreeNode { 51 | int data; 52 | TreeNode left; 53 | TreeNode right; 54 | 55 | TreeNode(int data) { 56 | this.data = data; 57 | } 58 | } 59 | 60 | private static void printOrder(TreeNode root) { 61 | if (root == null) 62 | return; 63 | Queue queue = new LinkedList<>(); 64 | // 先添加根结点 65 | queue.add(root); 66 | while (!queue.isEmpty()) { 67 | TreeNode node = queue.poll(); 68 | if (node != null) { 69 | System.out.print(node.data + ","); 70 | if (node.left != null) 71 | queue.offer(node.left); 72 | if (node.right != null) 73 | queue.offer(node.right); 74 | } 75 | } 76 | } 77 | 78 | public static void main(String[] args) { 79 | TreeNode root = new TreeNode(1); 80 | root.left = new TreeNode(2); 81 | root.right = new TreeNode(3); 82 | root.left.left = new TreeNode(4); 83 | root.left.right = new TreeNode(5); 84 | root.right.left = new TreeNode(6); 85 | root.right.right = new TreeNode(7); 86 | printOrder(root); 87 | System.out.println(); 88 | printOrder(null); 89 | root = new TreeNode(1); 90 | root.left = new TreeNode(2); 91 | root.left.left = new TreeNode(3); 92 | root.left.right = new TreeNode(4); 93 | root.left.right.left = new TreeNode(5); 94 | System.out.println(); 95 | printOrder(root); 96 | } 97 | } 98 | ``` 99 | 100 | 代码实现确实比较简单,唯一让大家疑惑的是我的出队列用的是 `poll` 而不是其他资料中用的 `remove`,进队列用的是 `offer` 而不是 `add`。 101 | 102 | >`offer` 和 `poll` 方法相对于 `add` 和 `remove` 方法更安全,它们是通过返回值来判断是否入队和出队成功的,而 `add` 和 `remove` 方法在错误的时候会报异常,两者各有优劣。 103 | 104 | ## 验证测试用例 105 | 106 | 代码写毕,开始验证我们的测试用例。 107 | 108 | 1. 传入异常值,直接返回,测试通过; 109 | 2. 传入单个结点,测试通过; 110 | 3. 传入非完全二叉树,测试通过; 111 | 4. 传入完全二叉树,测试通过。 112 | 113 | ## 课后习题 114 | 115 | 依然还是要放上下一次推文的习题讲解,下一题来自《剑指 Offer》 的面试题 26:复杂链表的复制 116 | 117 | > 请实现复杂链表的复制,在复杂链表中,每个结点除了 next 指针指向下一个结点外,还有一个 sibling 指向链表中的任意结点或者 NULL。比如下图就是一个含有 5 个结点的复杂链表。 118 | 119 | ![image-20180728122725439](/var/folders/6m/5yg4nys56t1dd5xpwk68cmbw0000gn/T/abnerworks.Typora/image-20180728122725439.png) -------------------------------------------------------------------------------- /algorithm/面试 4:避免用递归去解决斐波那契数列.md: -------------------------------------------------------------------------------- 1 | ## 面试:老师讲的递归解决斐波那契数列真的好吗 2 | 3 | 在搞「模拟面试」的日子,我发现大家普遍有个问题就是,感觉自己的能力总是到了瓶颈期,写了好几年代码,感觉只是会的框架比以前多了而已。去大公司面试,屡战屡败,问失败原因,大多数人的答案都是,在三面数据结构与算法的时候,直接就挂了。 4 | 5 | 而不少人表示,我数据结构与算法潜心修炼,把书都啃烂了,倒背如流,但每次一面试,咋就是不会呢? 6 | 7 | 归根结底,还是思维训练的问题,很多人知其然而不知其所以然,所以,南尘就尽量地贴近大家的常态化思维去帮助大家训练算法吧。 8 | 9 | 昨天已经给大家预告了,不知道小伙伴们下来有没有去自己尝试处理。但不管怎样,要想训练好算法,但听别人讲不去思考,是肯定没用的。好了废话不多说,进入正题! 10 | 11 | #### 来到今天的面试题 12 | 13 | > 面试题:一直青蛙一次可以跳上 1 级台阶,也可以跳上 2 级,求该青蛙跳上 n 级的台阶总共有多少种跳法。 14 | > 15 | > 题目来源于《剑指 Offer》 16 | 17 | 一看这道题,好像没啥思路,感觉和我们的数据结构和常用的算法好像一点都不沾边。 18 | 19 | 但这看起来就像一道数学题,而且似乎就是高考数学的倒数第一题,所以我们就用数学来做吧。 20 | 21 | 数学中有个方法叫「数学归纳法」,我们这里就可以巧妙用到。 22 | 23 | 1. 当 n = 1 时,青蛙有 1 种跳法; 24 | 2. 当 n = 2 时,青蛙可以选择一次跳 1 级,跳两次;也可以选择一次跳 2 级;青蛙有 2 种跳法; 25 | 3. 当 n = 3 时,青蛙可以选择 1-1-1,1-2,2-1,青蛙有 3 种跳法; 26 | 4. 当 n = 4 时,青蛙可以选择 1-1-1-1,1-1-2,1-2-1,2-1-1,2-2,青蛙有 5 种跳法; 27 | 5. 似乎能得到 f(3) = f(2) + f(1),f(4) = f(3) + f(2),这是 f(n) = f(n-1) + f(n-2) 的节奏?我们得用 n = 5 验证一下。 28 | 6. 当 n = 5 时,青蛙可以选择 1-1-1-1-1,1-1-1-2,1-1-2-1,1-2-1-1,2-1-1-1,1-2-2,2-1-2,2-2-1,青蛙有 8 种跳法,f(5) = f(4) + f(3) 成立。 29 | 30 | 这是最笨的方法了,得出了这确实就是一个典型的斐波那契数列,唯一不一样的地方就是 n =2 的时候并没有 f(2) = f(0) + f(1)。 31 | 32 | 稍微有点思维能力的可能更简单。 33 | 34 | 1. n = 1 ,青蛙有 1 种跳法; 35 | 2. n = 2 ,青蛙有 2 种跳法; 36 | 3. n = 3,青蛙在第 1 级可以跳 1 种,后面 2 级相当于 f(3-1) = f(2),还有一种就是先跳 2 级,然后后面 1 级有 f(3-2) = f(1) 种跳法,可以得出 f(3) = f(2) + f(1); 37 | 4. ... 38 | 5. 当取 n 时,青蛙在第一次跳 1 级,后面的相当于有 f(n-1) 种跳法;假设第一次跳 2 级,后面相当于有 f(n-2) 种跳法;故可以得出 f(n) = f(n-1) + f(n-2); 39 | 40 | 这样思考可能更不容易出错吧,这就是思维的提炼过程,可见我们高考常考的「数学归纳法」是多么地有用。 41 | 42 | 既然能分析出这是一道典型的斐波那契数列了,我想教科书都教给大家方法了,**不过一定要注意 n = 2 的时候,正常的斐波那契数列值应该是 1,而我们是 2。**大多数人肯定会写出下面的代码: 43 | 44 | ```java 45 | public class Test09 { 46 | 47 | private static int fn(int n) { 48 | if (n <= 0) 49 | return 0; 50 | if (n == 1) 51 | return 1; 52 | if (n == 2) 53 | return 2; 54 | else 55 | return fn(n - 1) + fn(n - 2); 56 | } 57 | 58 | public static void main(String[] args) { 59 | System.out.println(fn(1)); 60 | System.out.println(fn(2)); 61 | System.out.println(fn(3)); 62 | System.out.println(fn(4)); 63 | } 64 | } 65 | ``` 66 | 67 | 我们教科书上反复用这个问题来讲解递归函数,但并不能说明递归的解法是最适合这个题目的。当我们暗自窃喜完成了这道面试题的时候,或许面试官会告诉我们,上面的这种递归解法存在很严重的效率问题,并让我们分析其中的原因。 68 | 69 | 我们以求 fn(10) 为例,要想求得 fn(10),需要先求得 fn(9) 和 fn(8);同样,要求得 fn(9),需要先求得 fn(8) 和 fn(7)...... 70 | 71 | 这存在一个很大的问题,我们一定会去重复计算很多值,我们一定得想办法把这个计算好的值存放起来。 72 | 73 | #### 避免重复计算 74 | 75 | 既然我们找到了问题所在,那改进方法自然是信手拈来了。我们目前的算法是「从大到小」计算,而我们只需要反向「从小到大」计算就可以了。我们根据 fn(1) 和 fn(2) 计算出 fn(3),再根据 fn(2) 和 fn(3) 计算出 fn(4)...... 76 | 77 | 很容易理解,这样的算法思路时间复杂度是 O(n),实现代码如下: 78 | 79 | ```java 80 | public class Test09 { 81 | 82 | private static long fn(int n) { 83 | if (n <= 0) 84 | return 0; 85 | if (n == 1) 86 | return 1; 87 | if (n == 2) 88 | return 2; 89 | long prePre = 1, pre = 2; 90 | long result = 0; 91 | for (int i = 3; i <= n; i++) { 92 | result = prePre + pre; 93 | prePre = pre; 94 | pre = result; 95 | } 96 | return result; 97 | } 98 | 99 | public static void main(String[] args) { 100 | System.out.println(fn(1)); 101 | System.out.println(fn(3)); 102 | System.out.println(fn(50)); 103 | System.out.println(fn(100)); 104 | } 105 | } 106 | ``` 107 | 108 | 上面的代码,一定要注意做了一点小修改,我们把返回值悄悄地改成了 long ,因为我们并不能保证客户端是否会输入一个比较大的数字,比如:100,这样,如果返回值为 int,一定会因为超出了最大值而显示错误的,解决方案就是把值换为更大容量的 long。但有时候你会发现,long 的容量也不够,毕竟整型和长整型,它都会有最大显示值,在遇到这样的情况的时候。我们最好和面试官交流一下,是否处理这样的情况。如果一定要处理这样的情况,那么可能你就得用 String 来做显示处理了。 109 | 110 | 其实在《剑指 Offer》上还有时间复杂度为 O(logn) 的解法,但因为不够实用,我们这里也就不讲解了,主要还是我们解题的算法思路训练。如果真的很感兴趣的话,那就请移步《剑指 Offer》吧。反正你在公众号后台回复「剑指Offer」就可以拿到 PDF 版本的。 111 | 112 | #### 总结 113 | 114 | 今天的面试讲解就到这吧,大家一定要学会自己去独立思考,训练自己的思维。简单回顾一下我们本周所学习的内容,我们下周再见! 115 | 116 | -------------------------------------------------------------------------------- /Android/每日一问:简述 View 的绘制流程.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:简述 View 的绘制流程 2 | 3 | Android 开发中经常需要用一些自定义 View 去满足产品和设计的脑洞,所以 View 的绘制流程至关重要。网上目前有非常多这方面的资料,但最好的方式还是直接跟着源码进行解读,每日一问系列一直追求短平快,所以本文笔者尽量精简。 4 | 5 | 想必大多数 Android 开发都知道自定义 View 需要关注的几个方法:`onMeasure()`、`onLayout()` 和 `onDraw()`,这其实也是每个 View 至关重要的绘制流程。 6 | 7 | 基本绘制都是会从根视图 `ViewRoot` 的 `performTraversals()` 方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。`performTraversals()` 的核心代码如下: 8 | ```java 9 | private void performTraversals() { 10 | ... 11 | int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 12 | int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 13 | ... 14 | //执行测量流程 15 | performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 16 | ... 17 | //执行布局流程 18 | performLayout(lp, desiredWindowWidth, desiredWindowHeight); 19 | ... 20 | //执行绘制流程 21 | performDraw(); 22 | } 23 | ``` 24 | 25 | #### measure() 26 | ```java 27 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) 28 | ``` 29 | 每个 View 都有自己的大小,所以基本自定义 View 的时候都需要重写 `onMeasure()` 这个方法,以定制化我们的 View 的宽高。**如果不重写这个方法,我们通常会出现 `wrap_content` 和 `match_parent` 是一样的显示效果。**至于原因,其实一探源码便知。 30 | ```java 31 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 32 | setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 33 | getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 34 | } 35 | 36 | public static int getDefaultSize(int size, int measureSpec) { 37 | int result = size; 38 | int specMode = MeasureSpec.getMode(measureSpec); 39 | int specSize = MeasureSpec.getSize(measureSpec); 40 | 41 | switch (specMode) { 42 | case MeasureSpec.UNSPECIFIED: 43 | result = size; 44 | break; 45 | case MeasureSpec.AT_MOST: 46 | case MeasureSpec.EXACTLY: 47 | result = specSize; 48 | break; 49 | } 50 | return result; 51 | } 52 | 53 | protected int getSuggestedMinimumHeight() { 54 | return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); 55 | } 56 | ``` 57 | 可以看到,`View` 默认是会使用 `getDefaultSize()` 方法进行设置宽高的,在 `AT_MOST` 和 `EXACTLY` 两种情况下都会直接使用测量规格里面的尺寸。在 `UNSPECIFIED` 模式下会直接取`getSuggestedMinimumWidth()` 的返回值。 58 | >`getSuggestedMinimumWidth()` 会直接根据是否设置 `backgroud` 来进行计算,需要注意的是,直接设置 color 作为 `backgroud` 也会直接采用 `minXXX` 的值。 59 | 60 | 在 `ViewGroup` 中,并没有去重写 `View` 的 `onMeasure()` 方法,而这都需要它的子类根据自己的逻辑去实现,比如 `LinearLayout` 和 `RelativeLayout` 明显测量逻辑是不一样的。不过,`ViewGroup` 倒是提供了一个 `measureChildren()` 方法来依次遍历每个子 View 对其进行测量。 61 | 62 | 在经过 `onMeasure()` 操作后,`getMeasureWidth()` 和 `getMeasureHeight()` 方法就可以拿到正确的返回值了。 63 | 64 | >由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,如果 View 还没有测量完毕,那么获得的宽/高就是 0。所以在 `onCreate()`、`onStart()`、`onResume()` 中均无法正确得到某个 View 的宽高信息。可以通过在 `onWindowFocusChanged()` 判断获取到焦点后进行获取,或者使用 `view.post()` 方式。 65 | #### layout() 66 | ```java 67 | public void layout(int l, int t, int r, int b) 68 | ``` 69 | 我们可以重写的 `onLayout()` 方法主要作用是确定子 View 的显示位置,由于 View 已经是最小的层级,所以我们在自定义 View 的时候通常不需要管这个方法,而在自定义 ViewGroup 的时候就不得不注意这个方法了。 70 | 71 | 经过 `onLayout()` 流程后,我们的 `left`、`right`、`top`、`bottom` 得以赋值,所以这时候可以通过 `getWidth()` 和 `getHeight()` 方法来获取 View 的实际宽高了。 72 | >注意:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 `measure` 过程,而最终宽/高形成于 View 的 `layout` 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。在一些特殊的情况下则两者不相等: 73 | 74 | #### draw() 75 | ```java 76 | public void draw(Canvas canvas) 77 | ``` 78 | 绘制的流程也就是通过调用 View 的 `draw()` 方法实现的。`draw()` 方法里的逻辑看起来更清晰,我就不贴源码了。一般是遵循下面几个步骤: 79 | - 绘制背景 – `drawBackground()` 80 | - 绘制自己 – `onDraw()` 81 | - 绘制孩子 – `dispatchDraw()` 82 | - 绘制装饰 – `onDrawScrollbars()` 83 | 84 | 由于不同的控件都有自己不同的绘制实现,所以V iew 的 `onDraw()` 方法肯定是空方法。而 ViewGroup 由于需要照顾子 View 的绘制,所以肯定在 `dispatchDraw()` 方法里遍历调用了child的 `draw()` 方法。 85 | 86 | 参考: 87 | [Android View的绘制流程](https://jsonchao.github.io/2018/10/28/Android%20View%E7%9A%84%E7%BB%98%E5%88%B6%E6%B5%81%E7%A8%8B/) 88 | [https://blog.csdn.net/yisizhu/article/details/51527557](https://blog.csdn.net/yisizhu/article/details/51527557) -------------------------------------------------------------------------------- /algorithm/面试 19:输出数组中出现次数超过一半的数字(剑指 Offer 26 题).md: -------------------------------------------------------------------------------- 1 | # 面试 19:输出数组中出现次数超过一半的数字(剑指 Offer 26 题) 2 | 3 | 上一篇推文给大家留下的习题来自于《剑指 Offer》第 29 题:数组中超过一半的数字,不知道各位去思考了么? 4 | 5 | > 面试题:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字并输出。比如 {1,2,3,2,2,2,1} 中 2 的次数是 4,数组长度为 7,所以输出 2。要求不能修改输入的数组。 6 | 7 | ## 准备测试用例 8 | 9 | 这道题能思考到的测试用例比较简单。 10 | 11 | 1. 输入符合条件的数组,查看打印是否满足情况; 12 | 2. 输入不符合条件的数组,查看打印; 13 | 3. 输入只有一个元素的数组,查看打印; 14 | 4. 输入无效数组,查看打印; 15 | 16 | ## 思考程序逻辑 17 | 18 | 第二步便是我们的思考程序逻辑了,题目要求查找出现次数超过一半的数字。比较容易想到的思路是直接对数组排序,那中间那个值就是我们想要的值,但这样的想法明显排序后会对输入的数组顺序有影响,所以我们可能需要换一种思路。 19 | 20 | 再看一遍题干,我们不难思考到,我们是否可以对每个数字进行计数,最后返回计数次数最多的值。存储次数采用 map 做映射处理。 21 | 22 | ```java 23 | public class Test19 { 24 | private static int moreThanHalfNums(int[] nums) { 25 | if (nums == null || nums.length == 0) 26 | throw new RuntimeException("the length of array must be large than 0"); 27 | int len = nums.length; 28 | Map map = new HashMap<>(); 29 | 30 | for (int num : nums) { 31 | if (map.containsKey(num)) 32 | map.put(num, map.get(num) + 1); 33 | else 34 | map.put(num, 1); 35 | } 36 | int times = len / 2; 37 | // 查找 map 中 value 最大的值 38 | for (Entry entry : map.entrySet()) { 39 | if (entry.getValue() > times) 40 | return entry.getKey(); 41 | } 42 | throw new RuntimeException("invalid input!"); 43 | } 44 | 45 | public static void main(String[] args) { 46 | int[] nums1 = {1, 2, 3, 2, 2, 4, 2, 2, 5}; 47 | System.out.println(moreThanHalfNums(nums1)); 48 | int[] nums2 = {1}; 49 | System.out.println(moreThanHalfNums(nums2)); 50 | int[] nums3 = {2, 1, 2, 1, 2, 2, 3, 2, 1}; 51 | System.out.println(moreThanHalfNums(nums3)); 52 | int[] nums4 = {1, 2, 3, 4, 5}; 53 | System.out.println(moreThanHalfNums(nums4)); 54 | } 55 | } 56 | ``` 57 | 58 | 写毕后进行测试用例的验证,无不例外,目前都通过,于是我们把这样的代码解法递交给面试官。 59 | 60 | 面试官看了这样的算法,表示他更期待的是不使用任何辅存空间的算法。于是我们得换个角度思考。 61 | 62 | 数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现次数的和还要多。因此我们可以考虑在遍历数组的时候保存两个值: 一个是数组中的一个数字, 一个是次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加 1 ;如果下一个数字和我们之前保存的数不同,则次数减 1。如果次数为 0,我们需要保存下一个数字,并把次数设为 1 。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为 1 时对应的数字。 63 | 64 | 我们来看这样的思路用代码怎么实现。 65 | 66 | ```java 67 | public class Test19 { 68 | 69 | private static int moreThanHalfNums(int[] nums) { 70 | if (nums == null || nums.length == 0) 71 | throw new RuntimeException("the length of array must be large than 0"); 72 | int result = nums[0]; 73 | int times = 1; 74 | int len = nums.length; 75 | for (int i = 1; i < len; i++) { 76 | if (times == 0) { 77 | result = nums[i]; 78 | times = 1; 79 | } else if (result == nums[i]) 80 | times++; 81 | else 82 | times--; 83 | } 84 | times = 0; 85 | for (int num : nums) { 86 | if (num == result) 87 | times++; 88 | } 89 | if (times > len / 2) 90 | return result; 91 | throw new RuntimeException("invalid input!"); 92 | } 93 | 94 | public static void main(String[] args) { 95 | int[] nums1 = {1, 2, 3, 2, 2, 4, 2, 2, 5}; 96 | System.out.println(moreThanHalfNums(nums1)); 97 | int[] nums2 = {1}; 98 | System.out.println(moreThanHalfNums(nums2)); 99 | int[] nums3 = {2, 1, 2, 1, 2, 2, 3, 2, 1}; 100 | System.out.println(moreThanHalfNums(nums3)); 101 | int[] nums4 = {1, 2, 3, 4, 5}; 102 | System.out.println(moreThanHalfNums(nums4)); 103 | } 104 | } 105 | ``` 106 | 107 | 写毕后,验证测试用例,同样全部通过。 108 | 109 | 本题最后的思路,希望大家刻意去思考和记忆一下,因为也许变一下题意,这样的想法还可以用到。 110 | 111 | ## 课后习题 112 | 113 | 我们下一次推文的题目来源于《剑指 Offer》第 31 题:计算连续子数组的最大和。 114 | 115 | > 面试题:输入一个整型数组,数组中有正数也有负数。数组中一个或多个整数形成一个子数组,求所有子数组的和的最大值,要求时间复杂度为 O(n)。 116 | > 比如输入 {1, -2, 3, 10, -4, 7, 2, -5},能产生子数组最大和的子数组为 {3,10,-4,7,2},最大和为 18。 -------------------------------------------------------------------------------- /experience/说说过去一周的面试和想法.md: -------------------------------------------------------------------------------- 1 | # 2018 年 8 月面试 2 | 3 | 不少小伙伴还是在公众号私信问我上周怎么突然没有日更了,我也有在公众号中答应大家后面会给回复。好啦,现在还是简单说一下吧。 4 | 5 | 过去的一周中,我主要是出去找工作去啦~过去的一周,面了 4 家公司,从小到大都有,最终斩获了 3 个 offer。在经历了一番内心挣扎之后,我于周二上午决心选择了福利待遇相对较差一些的咕咚。 6 | 7 | 主要还是因为咕咚的面试官给我的印象非常深刻,我很高兴能够加入一家技术氛围如此淳厚的厂子。 8 | 9 | **所以,从此以后,你们可爱的南尘除了是一名致学人,还将成为一名咕咚范儿啦!!!** 10 | 11 | 相对较大的公司的话,面试题可能更加具有一定的参考性,所以南尘就姑且把上周面试的咕咚和美团面试题分享给大家,可能不全,有些问题可能忘了。 12 | 13 | ## 咕咚 14 | 15 | #### 一面(Android Leader) 16 | 17 | 1. 请举出你认为你在工作中挑战最大的事例; 18 | 19 | 2. RecyclerView 一个适配器如何适配多种布局,不考虑根据不同 Type 设置不同 ViewHolder; 20 | 21 | 3. 用过数据库么?如何防止数据库读写死锁? 22 | 23 | 可以考虑采用 ContentProvider && 单例实现。 24 | 25 | 4. CardView 真的好么?除了 CardView 还能用什么方式实现圆角等?假设我们有上千个 Shape 文件,如何维护? 26 | 27 | 5. 了解过 MVVM 么?它和 MVP 有什么差距? 28 | 29 | 6. 是否用过 DataBinding?ButterKnife 是怎么做到布局绑定的? 30 | 31 | 7. 使用过什么图片加载库,Glide 的源码设计哪里很微妙? 32 | 33 | 8. 知道 Linux 的线程间通信么? Android 为啥会采用 Binder?Binder 的机制又是怎样的? 34 | 35 | 9. 讲讲 AIDL。 36 | 37 | 10. 如何能保证随时随地都能拿到一个 Activity 的当前生命周期? 38 | 39 | 11. 会用 Kotlin 么? 40 | 41 | 可见 Kotlin 自从成为了谷歌首推语言后,确实还挺重要的,必须学习一下了。 42 | 43 | 12. Android 7.0 都适配了什么? 44 | 45 | 13. 你还有什么想问我的? 46 | 47 | 48 | 49 | #### 二面(移动端 Leader) 50 | 51 | 1. 讲讲 HTTPS 是怎么做加密的?讲讲非对称加密算法。 52 | 53 | 2. Android 8.0 都有些什么新特性? 54 | 55 | - 画中画; 56 | - 自适应 icon; 57 | - WebView 增强; 58 | - 通知分类别,增加圆点,通知延后,通知增加背景,同样增加历史,通知超时自动清除; 59 | - 自动填充信息(类似浏览器的选择保存的账号密码功能) 60 | - 后台执行增加限制(主要是服务和广播); 61 | - 蓝牙提升。支持蓝牙低功耗 5.0 标准; 62 | - 智能文本选择与智能共享; 63 | - WLAN 感知; 64 | - 取消屏幕纵横比限制; 65 | - 多显示器支持; 66 | - 可下载字体、XML 定义字体; 67 | - 自适应 TextView,统一布局边框; 68 | - 更丰富的色彩管理功能; 69 | - 可以声明应用类别。 70 | 71 | 3. 使用过 Gradle 的哪些功能。 72 | 73 | 4. RecyclerView 滑动卡顿,请分析原因,并提供解决方案。TraceView 都能看到哪些成分? 74 | 75 | 5. 操作系统里面的一个「虚拟内存」是指的什么? 76 | 77 | 虚拟内存指的是一个对内存和外存进行调度,只是从逻辑上扩充了内存,但实际上不存在的内存存储器。 78 | 79 | 原理是:基于局部性原理,在程序装入的时候,可以将程序的一部分装入内存,而在其余部分留在外存,就可启动程序执行;在程序执行时,当所访问的信息不在内存的时候,由操作系统所需要的部分调入内存,然后继续执行程序;操作系统再将内存中暂时不使用的内容换出到外存上,从而腾出空间存放将要调入内存的信息。 80 | 81 | 6. Android 是如何做的性能调优? 82 | 83 | 7. 讲讲适配器模式中适配器是干嘛的?在开发中都在哪里有用到? 84 | 85 | 8. 都用过哪些开源库? 86 | 87 | 9. 会用 KotLin 么? 88 | 89 | 10. 你还有什么想问我的? 90 | 91 | 92 | 93 | #### 三面:技术总监 94 | 95 | 1. 为什么大多数 API 会选择短连接而不是长连接? 96 | 2. 为什么会选择用 RxJava? 97 | 3. MVVM 模式到底有什么好处? 98 | 4. 详细讲讲 Android 8.0 新特性。 99 | 5. Kotlin 学习的怎么样了? 100 | 6. 你的职业规划是怎么样的? 101 | 7. 你有什么想问我的? 102 | 103 | 104 | 105 | #### 四面:HR 106 | 107 | 1. 你目前是在职还是已经离职? 108 | 2. 你为什么离职? 109 | 3. 对咕咚 APP 的了解? 110 | 4. 你是一个自律的人么? 111 | 5. 你的职业规划是怎么样的? 112 | 113 | 114 | 115 | #### 五面:CT0 116 | 117 | 1. 先做个自我介绍。 118 | 2. 你在原来公司发展挺好的,但你为什么离职? 119 | 3. 你们的 APP 采用蓝牙协议了么? 120 | 121 | 122 | 123 | ## 美团 124 | 125 | #### 一面(Android 技术) 126 | 127 | 1. 讲讲你工作开发的项目是干嘛的。 128 | 2. 手写快排; 129 | 3. 手写二分查找,并分析时间复杂度; 130 | 4. 讲讲 APK 是如何做瘦身的? 131 | 5. 说说你项目中挑战最大的一件事。 132 | 6. 讲讲 HTTPS 是如何做加密的,说下非对称加密算法; 133 | 7. 说一下 HTTP 协议请求头我们常用的 3 个字段; 134 | 8. 讲讲你这个图片压缩库是怎么做的? 135 | 9. 进程保活怎么做?进程拉活现在还可以做么? 136 | 10. 用过线程池么?讲讲 AsyncTask 的原理。 137 | 11. 讲讲 HashMap 的原理。 138 | 12. 讲讲 Android 如何做性能调优? 139 | 13. 你们是如何做 UI 的机型适配的? 140 | 14. 讲讲你们的多渠道打包是怎么做的? 141 | 15. bugly 是干嘛用的?Handler 怎么处理内存泄漏,除了使用弱引用。你还知道哪些地方需要注意内存泄漏? 142 | 16. Bitmap 使用需要注意哪些问题?Bitmap.recycle() 会立即回收么?什么时候会回收?如果没有地方使用这个 Bitmap,为什么垃圾回收不会直接回收它? 143 | 17. 如何存储一个大图,但显示在 UI 上的是小图。 144 | 18. 官方为什么会把 HttpClient 和 HttpUrlConnection 替换为 OkHttp 默认实现?它有什么好处? 145 | 19. 你的 GitHub 上都开源了些什么东西?都为哪些开源库贡献了源码?ImagePicker 为啥会出现有些图片拿不到? 146 | 20. 了解二叉树的遍历么?讲一讲他们。 147 | 21. 讲讲 SVN 和 Git 的差别,Git 的优势是什么。 148 | 22. 讲讲 MVVM,主要是为了解决 MVP 和 MVC 的什么问题? 149 | 23. equals() 和 hashCode() 的区别是什么?平时有重写过它们么?什么情况下会去重写。 150 | 24. 讲下为什么在 Android 下推荐使用 ArrayMap,相比 HashMap 它到底有什么优势? 151 | 152 | 153 | 154 | ## 说些其他的 155 | 156 | 我知道你会问我答案,实际上在互联网发达的今天,大概你是可以寻找到了。其实技术面试有时候并没有标准答案,很多时候就是一个互相交流的过程而已。 157 | 158 | 直到现在,美团一面过去快一周了,依然还没有得到传说中的复试通知,深感难受,不过目前都不重要了,还好有了心仪的咕咚最终的认可,也算满足啦。 159 | 160 | **不瞒你说,我对咕咚一直掺杂了很深厚的感情,能开发心爱的人常用的 APP,其实这本身就挺自豪的。** 161 | 162 | 针对个别小伙伴可能会询问我为什么放弃了薪资更好的 XXX 公司和 XXX 公司,其实结果很清楚了。相对来说,我觉得现在的自己,更需要一些志同道合的朋友,他们可以不算厉害,但至少特别努力! 163 | 164 | 其实之前也有一位读者问我,他拿到了百词斩和美团的 offer,薪资百词斩给的肯定更高,问我如何决策。一番思考后我也是让他选择了美团的,最终他确实去了美团,目前听闻还混得有模有样的,为他骄傲。 165 | 166 | 好啦,近期南尘在日更上可能还是会继续疏忽。毕竟暂时希望能快速融入咕咚大家庭,同时完整地交接好目前致学的工作,这才是目前最主要的。 167 | 168 | 给大家还是会一如既往推荐一些订阅号,建议大家还是简单看看,各取所需,这就犹如创业公司一样,他们对你而言可能会一文不值,但总会有那么一些有价值的号主们。比如南尘,哈哈。 169 | 170 | 当然,南尘还是会多加筛选,肯定给大家推荐一些很随便的订阅号的。然后,广告的话,大家也懂,南尘很少发,万一南尘发了,其实题目一般我会写的很明白,大家乐意地就点开看看,不乐意的忽略就好! 171 | 172 | 好了就这样啦,一晚上就给大家扯这么多,希望对你,不是打扰,而是那源源不断地一丁点儿收获吧~ 173 | 174 | -------------------------------------------------------------------------------- /algorithm/面试 2:用 Java 逆序打印链表.md: -------------------------------------------------------------------------------- 1 | ## 面试:用 Java 逆序打印链表 2 | 3 | 昨天的 Java 实现单例模式 中,我们的双重检验锁机制因为指令重排序问题而引入了 `volatile` 关键字,不少朋友问我,到底为啥要加 `volatile` 这个关键字呀,而它,到底又有什么神奇的作用呢? 4 | 5 | 对 `volatile` 这个关键字,在昨天的讲解中我们简单说了一下:被 `volatile` 修饰的共享变量,都会具有下面两个属性: 6 | 7 | - 保证不同线程对该变量操作的内存可见性。 8 | - 禁止指令重排序。 9 | 10 | 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。 11 | 12 | 可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到。 13 | 14 | 对于重排序,不熟悉的建议直接 Google 一下,这里也就不多提了。只需要记住,在多线程中操作一个共享变量的时候,一定要记住加上 `volatile` 修饰即可。 15 | 16 | 由于时间关系,我们还是得先进入今天的正题,对于 `volatile` 关键字,在要求并发编程能力的面试中还是很容易考察到的,后面我也会简单给大家讲解。 17 | 18 | #### 输入一个单链表的头结点,从尾到头打印出每个结点的值。 19 | 20 | 这是《剑指 Offer》上的第五道面试题,链表是经常在面试中考察的一种数据结构,所以推荐大家一定要掌握。对于链表不熟悉的小伙伴可一定要去《大话数据结构》好好补课哟~ 21 | 22 | > 《剑指 Offer》 PDF 版本在公众号后台回复「剑指Offer」即可获取。 23 | > 24 | > 《大话数据结构》PDF 版本在公众号后台回复「大话数据结构」即可获取。 25 | 26 | 我们的链表有很多,单链表,双向链表,环链表等。这里是最普通的单链表模式,我们一般会在数据存储区域存放数据,然后有一个指针指向下一个结点。虽然 Java 中没有指针这个概念,但 Java 的引用恰如其分的填补了这个问题。 27 | 28 | 看到这道题,我们往往会很快反应到每个结点都有 next 属性,所以要从头到尾输出很简单。于是我们自然而然就会想到先用一个 `while` 循环取出所有的结点存放到数组中,然后再通过逆序遍历这个数组,即可实现逆序打印单链表的结点值。 29 | 30 | 我们假定结点的数据为 int 型的。实现代码如下: 31 | 32 | ```java 33 | public class Test05 { 34 | public static class Node { 35 | int data; 36 | Node next; 37 | } 38 | 39 | public static void printLinkReverse(Node head) { 40 | ArrayList nodes = new ArrayList<>(); 41 | while (head != null) { 42 | nodes.add(head); 43 | head = head.next; 44 | } 45 | for (int i = nodes.size() - 1; i >= 0; i--) { 46 | System.out.print(nodes.get(i).data + " "); 47 | } 48 | } 49 | 50 | public static void main(String[] args) { 51 | Node head = new Node(); 52 | head.data = 1; 53 | head.next = new Node(); 54 | head.next.data = 2; 55 | head.next.next = new Node(); 56 | head.next.next.data = 3; 57 | head.next.next.next = new Node(); 58 | head.next.next.next.data = 4; 59 | head.next.next.next.next = new Node(); 60 | head.next.next.next.next.data = 5; 61 | printLinkReverse(head); 62 | } 63 | } 64 | ``` 65 | 66 | 这样的方式确实能实现逆序打印链表的数据,但明显用了整整两次循环,时间复杂度为 O(n²)。等等!逆序输出?似乎有这样一个数据结构可以完美解决这个问题,这个数据结构就是栈。 67 | 68 | 栈是一种「后进先出」的数据结构,用栈的原理更好能达到我们的要求,于是实现代码如下: 69 | 70 | ```java 71 | public class Test05 { 72 | public static class Node { 73 | int data; 74 | Node next; 75 | } 76 | 77 | public static void printLinkReverse(Node head) { 78 | Stack stack = new Stack<>(); 79 | while (head != null) { 80 | stack.push(head); 81 | head = head.next; 82 | } 83 | while (!stack.isEmpty()) { 84 | System.out.print(stack.pop().data + " "); 85 | } 86 | } 87 | 88 | public static void main(String[] args) { 89 | Node head = new Node(); 90 | head.data = 1; 91 | head.next = new Node(); 92 | head.next.data = 2; 93 | head.next.next = new Node(); 94 | head.next.next.data = 3; 95 | head.next.next.next = new Node(); 96 | head.next.next.next.data = 4; 97 | head.next.next.next.next = new Node(); 98 | head.next.next.next.next.data = 5; 99 | printLinkReverse(head); 100 | } 101 | } 102 | ``` 103 | 104 | 既然可以用栈来实现,我们也极容易想到递归也能解决这个问题,因为递归本质上也就是一个栈结构。要实现逆序输出链表,我们每访问一个结点的时候,我们先递归输出它后面的结点,再输出该结点本身,这样链表的输出结果自然也是反过来了。 105 | 106 | 代码如下: 107 | 108 | ```java 109 | public class Test05 { 110 | public static class Node { 111 | int data; 112 | Node next; 113 | } 114 | 115 | public static void printLinkReverse(Node head) { 116 | if (head != null) { 117 | printLinkReverse(head.next); 118 | System.out.print(head.data+" "); 119 | } 120 | } 121 | 122 | public static void main(String[] args) { 123 | Node head = new Node(); 124 | head.data = 1; 125 | head.next = new Node(); 126 | head.next.data = 2; 127 | head.next.next = new Node(); 128 | head.next.next.data = 3; 129 | head.next.next.next = new Node(); 130 | head.next.next.next.data = 4; 131 | head.next.next.next.next = new Node(); 132 | head.next.next.next.next.data = 5; 133 | printLinkReverse(head); 134 | } 135 | } 136 | ``` 137 | 138 | 虽然递归代码看起来确实很整洁,但有个问题:当链表非常长的时候,一定会导致函数调用的层级很深,从而有可能导致函数调用栈溢出。所以显示用栈基于循环实现的代码,健壮性还是要好一些的。 139 | 140 | 好了,今天的面试讲解就到这,我们明天再见! 141 | 142 | -------------------------------------------------------------------------------- /algorithm/面试 8:面试常见的链表算法捷径(二).md: -------------------------------------------------------------------------------- 1 | ## 面试 8:面试常见的链表算法捷径(二) 2 | 3 | 昨天在最后给大家留了拓展题,不知道大家有没有思考完成,其实南尘说有巨坑是吓大家的啦,实际上也没什么。我们来继续看看昨天这个拓展题。 4 | 5 | > 面试题:给定单链表的头结点,删除单链表的倒数第 k 个结点。 6 | > 7 | > 前面的文章见链接:面试 7:面试常见的链表算法捷径(一) 8 | 9 | 这个题和前面的文章中增加了一个操作,除了找出来这个结点,我们还要删除它。删除一个结点,想必大家必定也知道:**要想操作(添加、删除)单链表的某个结点,那我们还得知道这个节点的前一个节点。**所以我们要删除倒数第 k 个结点,就必须要找到倒数第 k+1 个结点。然后把倒数第 k+1 个元素的 next 变量 p.next 指向 p.next.next。 10 | 11 | 我们找到倒数第 k 个结点的时候,先让 fast 走了 k-1 步,然后再让 slow 变量和 fast 同步走,它们之间就会一直保持 k-1 的距离,所以当 fast 到链表尾结点的时候,slow 刚刚指向的是倒数第 k 个结点。 12 | 13 | 本题由于我们要知道倒数第 k+1 个结点,所以得让 fast 先走 k 步,待 fast 指向链表尾结点的时候,slow 正好指向倒数第 k+1 个结点。 14 | 15 | 我们简单思考一下临界值: 16 | 17 | 1. 当 k = 1 的时候,删除的值是尾结点。我们让 fast 先走 1 步,当 fast.next 为尾结点的时候,倒数第 k+1 个结点正好是我们的倒数第二个结点。我们删除 slow.next,并让slow.next 指向 slow.next.next = null,满足条件。 18 | 2. 当 k > len 的时候,我们要找的倒数第 k 个元素不存在,直接出错; 19 | 3. 当 1 < k < len 的时候,k 最大为 len-1 的时候,fast 移动 len-1 步,直接到达尾结点,此时,snow 指向头结点。删除倒数第 k 个元素,即删除正数第 2 个结点即可; 20 | 4. 当 k = len 的时候比较特殊,当 fast 移动 len 步的时候,已经指向了 fast.next = null,此时我们其实要删除的是头结点,直接返回 head.next 即可。 21 | 22 | 所以我们自然能得到这样的代码。 23 | 24 | ```java 25 | public class Test07 { 26 | public static class LinkNode { 27 | int data; 28 | LinkNode next; 29 | 30 | public LinkNode(int data) { 31 | this.data = data; 32 | } 33 | } 34 | 35 | private static LinkNode delTheSpecifiedReverse(LinkNode head, int k) { 36 | LinkNode slow = head; 37 | LinkNode fast = head; 38 | if (fast == null) { 39 | throw new RuntimeException("your linkNode is null"); 40 | } 41 | // 先让 fast 先走 k 步 42 | for (int i = 0; i < k; i++) { 43 | if (fast == null) { 44 | // 说明输入的 k 已经超过了链表长度,直接报错 45 | throw new RuntimeException("the value k is too large."); 46 | } 47 | fast = fast.next; 48 | } 49 | // fast == null ,说明已经到了尾结点后面的空区域,说明要删除的就是头结点。 50 | if (fast == null) { 51 | return head.next; 52 | } 53 | while (fast.next != null) { 54 | slow = slow.next; 55 | fast = fast.next; 56 | } 57 | slow.next = slow.next.next; 58 | return head; 59 | } 60 | 61 | public static void main(String[] args) { 62 | LinkNode head = new LinkNode(1); 63 | head.next = new LinkNode(2); 64 | head.next.next = new LinkNode(3); 65 | head.next.next.next = new LinkNode(4); 66 | head.next.next.next.next = new LinkNode(5); 67 | LinkNode node = delTheSpecifiedReverse(head, 3); 68 | while (node != null) { 69 | System.out.print(node.data + "->"); 70 | node = node.next; 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | 好了,我们解决了昨天文章中留下的拓展题,今天我们来看看我们链表都还有些怎样的考法。 77 | 78 | > 面试题:定义一个单链表,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。为了方便,我们链表的 data 采用整型。 79 | 80 | 这是一道反转链表的经典题,我们来屡一下思路:一个结点包含下一结点的引用,反转的意思就是要把原来指向下一结点的引用指向上一个结点。我们可以分为下面的步骤: 81 | 82 | 1. 找到当前要反转的结点的下一个结点,并用变量保存,因为下一次要反转的是它,如果我们不保存的话一定会因为前面已经反转,导致无法通过遍历得到这个结点; 83 | 2. 然后让当前结点的 next 引用指向上一个结点,上一个结点初始 null 因为头结点的反转后变成尾结点; 84 | 3. 当前要反转的结点变成下一个要比较元素的上一个结点,用变量保存; 85 | 4. 当前要比较的结点赋值为之前保存的未反转前的下一个结点; 86 | 5. 当前反转的结点为 null 的时候,保存的上一个结点即反转后的链表头结点。 87 | 88 | 用代码实现就是: 89 | 90 | ```java 91 | public class Test08 { 92 | 93 | private static class LinkNode { 94 | int data; 95 | LinkNode next; 96 | 97 | LinkNode(int data) { 98 | this.data = data; 99 | } 100 | } 101 | 102 | private static LinkNode reverseLink(LinkNode head) { 103 | // 上一个结点 104 | LinkNode nodePre = null; 105 | LinkNode next = null; 106 | LinkNode node = head; 107 | while (node != null) { 108 | // 先用 next 保存下一个要反转的结点,不然会导致链表断裂。 109 | next = node.next; 110 | // 再把现在结点的 next 引用指向上一个结点 111 | node.next = nodePre; 112 | // 把当前结点赋值给 nodePre 变量,以便于下一次赋值 113 | nodePre = node; 114 | // 向后遍历 115 | node = next; 116 | } 117 | return nodePre; 118 | } 119 | 120 | public static void main(String[] args) { 121 | LinkNode head = new LinkNode(1); 122 | head.next = new LinkNode(2); 123 | head.next.next = new LinkNode(3); 124 | head.next.next.next = new LinkNode(4); 125 | head.next.next.next.next = new LinkNode(5); 126 | LinkNode node = reverseLink(head); 127 | while (node != null) { 128 | System.out.print(node.data + "->"); 129 | node = node.next; 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | 链表可以考的可真多,相信不是小伙伴都和我一样,云里雾里了,那我们今天就讲到这里,后面还要继续考算法,你,打起精神,别睡着了。 -------------------------------------------------------------------------------- /algorithm/面试 9:用 Java 实现冒泡排序.md: -------------------------------------------------------------------------------- 1 | ## 面试 9:用 Java 实现冒泡排序 2 | 3 | 南尘的朋友们,新的一周好,原本打算继续讲链表考点算法的,这里姑且是卡一段。虽然在我们 Android 开发中,很少涉及到排序算法,因为基本官方都帮我们封装好了,但排序算法也是非常重要的,在面试中 **归并排序** 和 **快速排序** 一直为高频考点,但在学习它们之前,我们必须得先把三大基础算法学会,毕竟层层递进,方得始终嘛。 4 | 5 | ### 冒泡排序 6 | 7 | 冒泡排序恐怕是我们计算机专业课程上以第一个接触到的排序算法,也算是一种入门级的排序算法。它的基本思想是:**两两比较相邻记录的关键字,如何反序则交换,直到没有反序的记录为止。** 8 | 9 | #### 冒泡排序算法原理: 10 | 11 | 1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 12 | 2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 13 | 3. 针对所有的元素重复以上的步骤,除了最后一个。 14 | 4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 15 | 16 | 一次比较过程如图所示: 17 | 18 | ![图片来源于网络](https://user-gold-cdn.xitu.io/2018/3/1/161e0ae4c75fd077?imageslim) 19 | 20 | 我们通常容易想到最简单的实现代码: 21 | 22 | ```java 23 | public class Test09 { 24 | 25 | private static void swap(int[] arr, int i, int j) { 26 | int temp = arr[i]; 27 | arr[i] = arr[j]; 28 | arr[j] = temp; 29 | } 30 | 31 | private static void printArr(int[] arr) { 32 | for (int anArr : arr) { 33 | System.out.print(anArr + " "); 34 | } 35 | } 36 | 37 | private static void bubbleSort(int[] arr) { 38 | if (arr == null) 39 | return; 40 | for (int i = 0; i < arr.length - 1; i++) { 41 | for (int j = i + 1; j < arr.length; j++) { 42 | if (arr[i] > arr[j]) 43 | swap(arr, i, j); 44 | } 45 | } 46 | } 47 | 48 | public static void main(String[] args) { 49 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 50 | bubbleSort(arr); 51 | printArr(arr); 52 | } 53 | } 54 | ``` 55 | 56 | 严格地讲,上面的算法并不是冒泡排序,因为 **它完全不符合两两相邻比较。**它更应该是最最简单的就交换排序而已。它的思路是让每一个关键字,都和它后面的每一个关键字比较,如果大则交换,这样第一位置的关键字在一次循环后一定变成最小值。 57 | 58 | 我们不妨来看看正宗的冒泡排序算法。 59 | 60 | ```java 61 | public class Test09 { 62 | 63 | private static void swap(int[] arr, int i, int j) { 64 | int temp = arr[i]; 65 | arr[i] = arr[j]; 66 | arr[j] = temp; 67 | } 68 | 69 | private static void printArr(int[] arr) { 70 | for (int anArr : arr) { 71 | System.out.print(anArr + " "); 72 | } 73 | } 74 | 75 | private static void bubbleSort(int[] arr) { 76 | if (arr == null) 77 | return; 78 | for (int i = 0; i < arr.length - 1; i++) { 79 | for (int j = 1; j < arr.length - i; j++) { 80 | if (arr[j - 1] > arr[j]) { 81 | swap(arr, j - 1, j); 82 | } 83 | } 84 | } 85 | } 86 | 87 | public static void main(String[] args) { 88 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 89 | bubbleSort(arr); 90 | printArr(arr); 91 | } 92 | } 93 | ``` 94 | 95 | 上述代码是否完美了呢?答案是否定的,我们假设待排序的序列是 {2,1,3,4,5,6,7,8,9},也就是说,除了第一和第二个关键字需要交换外,别的都应该是正常的顺序,当 i = 1 时,交换了 2 和 1 的位置,此时已经有序,但是算法依然不依不挠地将 i = 2 到 9 以及每一个内循环都执行了一遍,尽管没有交换数据,但之后的大量比较还是大大的多余了。所以我们完全可以设置一个标记位 `isSort`,当我们比较一次后都没有交换,则代表数组已经有序了,此时直接退出循环即可。 96 | 97 | 既然思路已经确定,那代码自然是很信手拈来了。 98 | 99 | ```java 100 | public class Test09 { 101 | 102 | private static void swap(int[] arr, int i, int j) { 103 | int temp = arr[i]; 104 | arr[i] = arr[j]; 105 | arr[j] = temp; 106 | } 107 | 108 | private static void printArr(int[] arr) { 109 | for (int anArr : arr) { 110 | System.out.print(anArr + " "); 111 | } 112 | } 113 | 114 | private static void bubbleSort(int[] arr) { 115 | if (arr == null) 116 | return; 117 | // 定义一个标记 isSort,当其值为 true 的时候代表已经有序。 118 | boolean isSort; 119 | for (int i = 0; i < arr.length - 1; i++) { 120 | isSort = true; 121 | for (int j = 1; j < arr.length - i; j++) { 122 | if (arr[j - 1] > arr[j]) { 123 | swap(arr, j - 1, j); 124 | isSort = false; 125 | } 126 | } 127 | if (isSort) 128 | break; 129 | } 130 | } 131 | 132 | public static void main(String[] args) { 133 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 134 | bubbleSort(arr); 135 | printArr(arr); 136 | } 137 | } 138 | ``` 139 | 140 | Perfect 的代码,但冒泡排序在数组长度较大的时候,效率真的很低下,所以在实际生产中,我们也很少使用这种算法。 141 | 142 | #### 冒泡排序时间空间复杂度及算法稳定性分析 143 | 144 | 对于长度为 n 的数组,冒泡排序需要经过 n(n-1)/2 次比较,最坏的情况下,即数组本身是倒序的情况下,需要经过 n(n-1)/2 次交换,所以其 145 | 146 | > 冒泡排序的算法时间平均复杂度为 O(n²)。空间复杂度为 O(1)。 147 | 148 | 可以想象一下:如果两个相邻的元素相等是不会进行交换操作的,也就是两个相等元素的先后顺序是不会改变的。如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个元素相邻起来,最终也不会交换它俩的位置,所以相同元素经过排序后顺序并没有改变。 149 | 150 | 所以冒泡排序是一种稳定排序算法。所以冒泡排序是稳定排序。这也正是算法稳定性的定义: 151 | 152 | > **排序算法的稳定性:通俗地讲就是能保证排序前两个相等的数据其在序列中的先后位置顺序与排序后它们两个先后位置顺序相同。** 153 | 154 | **冒泡排序总结**: 155 | 156 | 1. 冒泡排序的算法时间平均复杂度为 O(n²)。 157 | 2. 空间复杂度为 O(1)。 158 | 3. 冒泡排序为稳定排序。 159 | 160 | 考虑到不少读者说每天代码量太多的问题,我们今天就只讲冒泡排序的 Java 实现,我们明天将带来三大简单排序的另外两种,**选择排序** 和 **插入排序**。 161 | 162 | 163 | 164 | 文章参考来源:https://juejin.im/post/5a96d6b15188255efc5f8bbd -------------------------------------------------------------------------------- /algorithm/面试 1:用 Java 实现一个 Singleton 模式.md: -------------------------------------------------------------------------------- 1 | ## 面试:用 Java 实现一个 Singleton 模式 2 | 3 | 面试系列更新后,终于迎来了我们的第一期,我们也将贴近《剑指 Offer》的题目给大家带来 Java 的讲解,个人还是非常推荐《剑指 Offer》作为面试必刷的书籍的,这不,再一次把这本书分享给大家,PDF 版本在公众号后台回复「剑指Offer」即可获取。 4 | 5 | 我们在面试中总会遇到不少设计模式的问题,而设计模式中的 Singleton 模式又是我们最容易出现的考题,大多数人可能在此前已经有充分的了解,但不少人仅仅是停留在比较浅显的层次,今天我们就结合《剑指 Offer》给大家带来更加深入的讲解。 6 | 7 | #### 题目:请用 Java 手写一个单例模式代码,希望尽可能考虑地全面。 8 | 9 | 不论是 Java 还是 Android 中单例模式肯定是我们经常用到的,所以这道题可能大多数人会第一时间想到饿汉式代码。 10 | 11 | ```java 12 | public class Singleton { 13 | private static final Singleton INSTANCE = new Singleton(); 14 | 15 | private Singleton() { 16 | } 17 | 18 | public static Singleton getInstance() { 19 | return INSTANCE; 20 | } 21 | } 22 | ``` 23 | 24 | 上面是典型的饿汉式写法,因为单例的实例被声明成 static 和 final 变量了,所以在第一次加载类到内存中时就会初始化,所以也不会存在多线程问题,但它的缺点非常显而易见,也经常为人诟病。这明显不是一种懒加载模式(lazy initialization),就因为它是 static 和 final 的,所以类会在加载后就被初始化,导致我们代码的健壮性很差,假如后面更改需求,希望在 `getInstance()` 之前调用某个方法给它设置参数,这个就明显不符合使用场景了,面试官极有可能在看到这个代码后觉得你就是一个只知道完成功能没有大局观的人。 25 | 26 | 当然还会有不少人直接采用我们的懒汉式代码,这样就解决了延展性和懒加载了。 27 | 28 | ```java 29 | public class Singleton { 30 | private static Singleton instance; 31 | 32 | private Singleton() { 33 | } 34 | 35 | public static Singleton getInstance() { 36 | if (instance == null) { 37 | instance = new Singleton(); 38 | } 39 | return instance; 40 | } 41 | 42 | } 43 | ``` 44 | 45 | 上述代码可能是大多数面试者的解法,包括教科书上也是这么教我们的,但这段代码却存在了一个致命的问题,那就是当多个线程并行调用 `getInstance()` 的时候,就会创建多个实例,这显然违背了面试官的意思。正好面试官加了一句希望尽可能考虑地全面,所以这样的代码肯定不能虏获面试官的芳心。 46 | 47 | 既然要线程安全,那我直接加锁呗。于是并有了下面的代码。他们也是懒汉式的,只不过线程安全了。 48 | 49 | ```java 50 | public class Singleton { 51 | private static Singleton instance; 52 | 53 | private Singleton() { 54 | } 55 | 56 | public static synchronized Singleton getInstance() { 57 | if (instance == null) { 58 | instance = new Singleton(); 59 | } 60 | return instance; 61 | } 62 | 63 | } 64 | ``` 65 | 66 | 这样的解法实现了线程安全,但它并不是那么高效,因为在任何时候只能有一个线程去调用 `getInstance()` 方法,但实际上加锁操作也是耗时的,我们应该尽量地避免使用它。所以自然就引出了双重检验锁。 67 | 68 | ```java 69 | public class Singleton { 70 | private static Singleton instance; 71 | 72 | private Singleton() { 73 | } 74 | 75 | public static Singleton getInstance() { 76 | if (instance == null) { 77 | synchronized (Singleton.class) { 78 | if (instance == null) { 79 | instance = new Singleton(); 80 | } 81 | } 82 | } 83 | return instance; 84 | } 85 | 86 | } 87 | ``` 88 | 89 | 这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。 90 | 91 | 1. 给 instance 分配内存 92 | 2. 调用 Singleton 的构造函数来初始化成员变量 93 | 3. 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 94 | 95 | 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。 96 | 97 | 我们只需要将 instance 变量声明成 volatile 就可以了。 98 | 99 | ```java 100 | public class Singleton { 101 | private volatile static Singleton instance; 102 | 103 | private Singleton() { 104 | } 105 | 106 | public static Singleton getInstance() { 107 | if (instance == null) { 108 | synchronized (Singleton.class) { 109 | if (instance == null) { 110 | instance = new Singleton(); 111 | } 112 | } 113 | } 114 | return instance; 115 | } 116 | 117 | } 118 | ``` 119 | 120 | 有些人认为使用 `volatile` 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 `volatile` 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 `volatile` 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。 121 | 122 | 但是特别注意在 Java 5 以前的版本使用了 `volatile` 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 `volatile` 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 `volatile`。 123 | 124 | 那么,有没有一种既有懒加载,又保证了线程安全,还简单的方法呢? 125 | 126 | 当然有,静态内部类,就是一种我们想要的方法。我们完全可以把 Singleton 实例放在一个静态内部类中,这样就避免了静态实例在 Singleton 类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。 127 | 128 | ```java 129 | public class Singleton { 130 | private static class Holder { 131 | private static Singleton INSTANCE = new Singleton(); 132 | } 133 | 134 | private Singleton() { 135 | } 136 | 137 | public static Singleton getInstance() { 138 | return Holder.INSTANCE; 139 | } 140 | } 141 | ``` 142 | 143 | 这是我比较推荐的解法,这种写法用 JVM 本身的机制保证了线程安全的问题,同时读取实例的时候也不会进行同步,没什么性能缺陷,还不依赖 JDK 版本。 144 | 145 | 虽说如此,但看《Effective Java》中第三点来说,还是有必要提醒一下:**享有特权的客户端可以借助 `AccessibleObject.setAccessible` 方法,通过反射机制来调用私有构造器。**如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。 146 | 147 | > 《Effective Java 中文版》PDF 在公众号后台回复「Effective Java」即可获取。 148 | 149 | #### 我们其实还有更简单的枚举单例。 150 | 151 | 用过枚举写单例的人都说:**用枚举写单例真是太简单了。**下面的这段代码就是声明枚举单例的通常做法。 152 | 153 | ```java 154 | public enum EasySingleton{ 155 | INSTANCE; 156 | } 157 | ``` 158 | 159 | 这是从 Java 1.5 发行版本后就可以实用的单例方法,我们可以通过 `EasySingleton.INSTANCE` 来访问实例,这比调用 `getInstance()` 方法简单多了。创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。 160 | 161 | ### 总结 162 | 163 | 一个总结肯定是必不可少的,上面也只是列举了我们常见的单例实现方式。当然也不完全,比如我们还可以用 static 代码块的方式实现懒汉式代码,但这里就不一一例举了。 164 | 165 | 就我个人而言,我还是比较推荐用静态内部类的方式使用单例模式,如果涉及到反序列化创建对象的话,不妨也试试枚举呗~ -------------------------------------------------------------------------------- /Android/每日一问:说说你对 LeakCanary 的了解.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:说说你对 LeakCanary 的了解 2 | 3 | 昨天的问题说到了关于 [内存泄漏需要注意的点](),在文章最后有说到 [LeakCanary]() 检测内存泄漏。实际上,我相信绝大多数人也知道甚至使用过这个库。 4 | 5 | > 这个系列通常来说如果发现了不错的资源,会选择直接截取部分拿过来,所以对于文章底部的参考链接一般都是非常不错的,可以直接去看哟~ 6 | 7 | #### LeakCanary 的基本工作流程是怎样的? 8 | 9 | LeakCanary 的使用方式非常简单,只需要在 build.gradle 里面直接写上依赖,并且在 Application 类里面做注册就可以了。 10 | 11 | > 当然,需要在 Application 里面注册这样的操作仅在大多数人接触的 1.x 版本,实际上 LeakCanary 现在已经升级到了 2.x 版本,代码侵入性更低,而且纯 Kotlin 写法。从 Google 各种 Demo 主推 Kotlin 以及各种主流库都在使用 Kotlin 编写来看可见 Kotlin 确实在 Android 开发中愈发重要,没使用的小伙伴必须得去学习一波了,目前我也是纯 Kotlin 做开发的。 12 | 13 | 对于工作原理我相信大家应该也是或多或少有一定了解,这里刚好有一张非常不错的流程图就直接借用过来了,另外他从源码角度理解 LeakCanary 的这篇文章也写的非常不错,感兴趣的点击文章底部的链接直达。 14 | 15 | ![](https://upload-images.jianshu.io/upload_images/6544890-9a37807f59f71b55.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/982/format/webp) 16 | 17 | #### 初次使用 LeakCanary 为什么没有 Icon 入口 18 | 19 | 我们常常在使用 LeakCanary 的时候会发现这样一个问题:最开始并没有出现 LeakCanary 的 Launcher icon,但当出现了内存泄漏警告的时候系统桌面就多了这么一个图标,一般情况下都是会非常好奇的。 20 | 21 | 从 1.x 的源码中就可以看出端倪。在 leakcanary-android 的 manifast 中,我们可以看到相关配置: 22 | 23 | ```xml 24 | 25 | 30 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | 我们可以看到 `DisplayLeakActivity` 被设置为了 Launcher,并设置上了对应的图标,所以我们使用 LeakCanary 会在系统桌面上生成 Icon 入口。但是 `DisplayLeakActivity` 的 `enable` 属性默认是 false,所以在桌面上是不会显示入口的。而在发生内存泄漏的时候,LeakCanary 会主动将 `enable` 属性置为 true。 52 | 53 | #### LeakCanary 2 都做了些什么 54 | 55 | 最近 LeakCanary 升级到了 2.x 版本,这是一次完全的重构,去除了 1.x release 环境下引用的空包 leakcanary-android-no-op。并且 Kotlin 语言覆盖高达 99.8%,也再也不需要在 Application 里面做类似下面的代码。 56 | 57 | ```java 58 | //com.example.leakcanary.ExampleApplication 59 | @Override 60 | public void onCreate() { 61 | super.onCreate(); 62 | if (LeakCanary.isInAnalyzerProcess(this)) { 63 | // This process is dedicated to LeakCanary for heap analysis. 64 | // You should not init your app in this process. 65 | return; 66 | } 67 | LeakCanary.install(this); 68 | } 69 | ``` 70 | 71 | 只需要在依赖里面添加这样的代码就可以了。 72 | 73 | ```groovy 74 | dependencies { 75 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2' 76 | } 77 | ``` 78 | 79 | 初次看到这样的操作,会觉得非常神奇,仔细阅读源码才回发现它竟然使用了一个骚操作:`ContentProvider`。 80 | 81 | 在 `leakcanary-leaksentry` 模块的 `AndroidManifest.xml `文件中可以看到: 82 | 83 | ```xml 84 | 85 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | ``` 99 | 100 | 再经过查看 `LeakSentryInstaller` 可以看到: 101 | 102 | ```kotlin 103 | package leakcanary.internal 104 | 105 | import android.app.Application 106 | import android.content.ContentProvider 107 | import android.content.ContentValues 108 | import android.database.Cursor 109 | import android.net.Uri 110 | import leakcanary.CanaryLog 111 | 112 | /** 113 | * Content providers are loaded before the application class is created. [LeakSentryInstaller] is 114 | * used to install [leaksentry.LeakSentry] on application start. 115 | */ 116 | internal class LeakSentryInstaller : ContentProvider() { 117 | 118 | override fun onCreate(): Boolean { 119 | CanaryLog.logger = DefaultCanaryLog() 120 | val application = context!!.applicationContext as Application 121 | InternalLeakSentry.install(application) 122 | return true 123 | } 124 | 125 | override fun query( 126 | uri: Uri, 127 | strings: Array?, 128 | s: String?, 129 | strings1: Array?, 130 | s1: String? 131 | ): Cursor? { 132 | return null 133 | } 134 | 135 | override fun getType(uri: Uri): String? { 136 | return null 137 | } 138 | 139 | override fun insert( 140 | uri: Uri, 141 | contentValues: ContentValues? 142 | ): Uri? { 143 | return null 144 | } 145 | 146 | override fun delete( 147 | uri: Uri, 148 | s: String?, 149 | strings: Array? 150 | ): Int { 151 | return 0 152 | } 153 | 154 | override fun update( 155 | uri: Uri, 156 | contentValues: ContentValues?, 157 | s: String?, 158 | strings: Array? 159 | ): Int { 160 | return 0 161 | } 162 | } 163 | ``` 164 | 165 | 确实是真的骚,我们都知道 `ContentProvider` 的 `onCreate()` 的调用时机介于 `Application` 的 `attachBaseContext()` 和 `onCreate()` 之间,LeakCanary 这么做,把 init 的逻辑放到库内部,让调用方完全不需要在 `Application` 里去进行初始化了,十分方便。这样下来既可以避免开发者忘记初始化导致一些错误,也可以让我们庞大的 `Application` 代码更加简洁。 166 | 167 | 参考: 168 | 169 | -------------------------------------------------------------------------------- /Android/每日一问:谈谈对 MeasureSpec 的理解.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:谈谈对 MeasureSpec 的理解 2 | 3 | 作为一名 Android 开发,正常情况下对 View 的绘制机制基本还是耳熟能详的,尤其对于经常需要自定义 View 实现一些特殊效果的同学。 4 | 5 | 网上也出现了大量的 Blog 讲 View 的 `onMeasure()`、`onLayout()`、`onDraw()` 等,虽然这是一个每个 Android 开发都应该知晓的东西,但这一系列实在是太多了,完全不符合咱们短平快的这个系列初衷。 6 | 7 | 那么,今天我们就来简单谈谈 `measure()` 过程中非常重要的 `MeasureSpec`。 8 | 9 | 对于绝大多数人来说,都是知道 `MeasureSpec` 是一个 32 位的 int 类型。并且取了最前面的两位代表 Mode,后 30 位代表大小 Size。 10 | 11 | 相比也非常清楚 `MeasureSpec` 有 3 种模式,它们分别是 `EXACTLY`、`AT_MOST` 和 `UNSPECIFIED`。 12 | >- 精确模式(MeasureSpec.EXACTLY):在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少,对应 `MATCH_PARENT` 和确定的值。 13 | >- 最大模式(MeasureSpec.AT_MOST):这个也就是父组件,能够给出的最大的空间,当前组件的长或宽最大只能为这么大,当然也可以比这个小。对应 `WRAP_CONETNT`。 14 | >- 未指定模式(MeasureSpec.UNSPECIFIED):这个就是说,当前组件,可以随便用空间,不受限制。 15 | 16 | 通常来说,我们在自定义 View 的时候会经常地接触到 `AT_MOST` 和 `EXACTLY`,我们通常会根据两种模式去定义自己的 View 大小,在 `wrap_content` 的时候使用自己计算或者设置的一个默认值。而更多的时候我们都会认为 `UNSPECIFIED` 这个模式被应用在系统源码中。具体就体现在 `NestedScrollView` 和 `ScrollView` 中。 17 | 18 | 我们看这样一个 XML 文件: 19 | ```xml 20 | 21 | 27 | 28 | 34 | 35 | 36 | 37 | ``` 38 | 在 `NestedScrollView` 里面写了一个充满屏幕高度的 `TextView`,为了更方便看效果,我们设置了一个背景颜色。但我们从 XML 预览中却会惊讶的发现不一样的情况。 39 | ![](https://upload-images.jianshu.io/upload_images/3994917-c47baa704e846909.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 40 | 41 | 我们所期望的是填充满屏幕的 `TextView`,但实际效果却和 `TextView` 设置高度为 `wrap_content` 如出一辙。 42 | 43 | 很明显,这一定是高度测量出现的问题,如果我们的父布局是 `LinearLayout`,很明显没有任何问题。所以问题一定出在了 `NestedScrollView` 的 `onMeasure()` 中。 44 | 45 | ```java 46 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 47 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 48 | if (this.mFillViewport) { 49 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 50 | if (heightMode != 0) { 51 | if (this.getChildCount() > 0) { 52 | View child = this.getChildAt(0); 53 | LayoutParams lp = (LayoutParams)child.getLayoutParams(); 54 | int childSize = child.getMeasuredHeight(); 55 | int parentSpace = this.getMeasuredHeight() - this.getPaddingTop() - this.getPaddingBottom() - lp.topMargin - lp.bottomMargin; 56 | if (childSize < parentSpace) { 57 | int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); 58 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, 1073741824); 59 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 60 | } 61 | } 62 | 63 | } 64 | } 65 | } 66 | ``` 67 | 由于我们并没有在外面设置 `mFillViewport` 这个属性,所以并不会进入到 if 条件中,我们来看看 `NestedScrollView` 的 super `FrameLayout` 的 `onMeasure()` 做了什么。 68 | ```java 69 | @Override 70 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 71 | int count = getChildCount(); 72 | 73 | final boolean measureMatchParentChildren = 74 | MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || 75 | MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; 76 | mMatchParentChildren.clear(); 77 | 78 | int maxHeight = 0; 79 | int maxWidth = 0; 80 | int childState = 0; 81 | 82 | for (int i = 0; i < count; i++) { 83 | final View child = getChildAt(i); 84 | if (mMeasureAllChildren || child.getVisibility() != GONE) { 85 | measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); 86 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 87 | maxWidth = Math.max(maxWidth, 88 | child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); 89 | maxHeight = Math.max(maxHeight, 90 | child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); 91 | childState = combineMeasuredStates(childState, child.getMeasuredState()); 92 | if (measureMatchParentChildren) { 93 | if (lp.width == LayoutParams.MATCH_PARENT || 94 | lp.height == LayoutParams.MATCH_PARENT) { 95 | mMatchParentChildren.add(child); 96 | } 97 | } 98 | } 99 | } 100 | 101 | // ignore something... 102 | } 103 | ``` 104 | 注意其中的关键方法 `measureChildWithMargins()`,这个方法在 `NestedScrollView` 中得到了完全重写。 105 | ```java 106 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { 107 | MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams(); 108 | int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); 109 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0); 110 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 111 | } 112 | ``` 113 | 我们看到其中有句非常关键的代码: 114 | ```java 115 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0); 116 | ``` 117 | `NestedScrollView` 直接无视了用户设置的 MODE,直接采用了 `UNSPECIFIED` 做处理。**经过测试发现,当我们重写 `NestedScrollView` 的这句代码,并且把 MODE 设置为 `EXACTLY` 的时候,我们得到了我们想要的效果,我已经查看 Google 的源码提交日志,并没有找到原因。** 118 | 119 | 我起初猜想是只有 `UNSPECIFIED` 才能实现滚动效果,但很遗憾并不是这样的。所以在这里抛出这个问题,希望有知情人士能一起讨论。 -------------------------------------------------------------------------------- /algorithm/面试 5:手写 Java 的 pow() 实现.md: -------------------------------------------------------------------------------- 1 | ## 面试 5:手写 Java 的 pow() 实现。 2 | 3 | 我们在处理一道编程面试题的时候,通常除了注意代码规范以外,千万要记得自己心中模拟一个单元测试。主要通过三方面来处理。 4 | 5 | - 功能性测试 6 | - 边界值测试 7 | - 负面性测试 8 | 9 | 不管如何,一定要保证自己代码考虑的全面,而不要简单地猜想用户的输入一定是正确的,只是去实现功能。通常你编写一个能接受住考验的代码,会让面试官对你刮目相看,你可以不厉害,但已经充分说明了你的靠谱。 10 | 11 | 今天我们的面试题目是: 12 | 13 | > **面试题:尝试实现 Java 的 Math.pow(double base,int exponent) 函数算法,计算 base 的 exponent 次方,不得使用库函数,同时不需要考虑大数问题。** 14 | > 15 | > > 面试题来源于《剑指 Offer》第 11 题,数字的整数次方。 16 | > > 17 | > > 不要介意 Java 真正的方法是 Math.pow(double var1,double var2)。 18 | 19 | 由于不需要考虑大数问题,不少小伙伴心中暗自窃喜,这题目也太简单了,给我撞上了,运气真好,于是直接写出下面的代码: 20 | 21 | ```java 22 | public class Test11 { 23 | 24 | private static double power(double base, int exponent) { 25 | double result = 1.0; 26 | for (int i = 0; i < exponent; i++) { 27 | result *= base; 28 | } 29 | return result; 30 | } 31 | 32 | public static void main(String[] args) { 33 | System.out.println(power(2, 2)); 34 | System.out.println(power(2, 4)); 35 | System.out.println(power(3, 1)); 36 | System.out.println(power(3, 0)); 37 | } 38 | } 39 | ``` 40 | 41 | 写的快自然是好事,如果正确的话会被面试官认为是思维敏捷。但如果考虑不周的话,恐怕就极容易被面试官认为是不靠谱的人了。在技术能力和靠谱度之间,大多数面试官更青睐于靠谱度。 42 | 43 | 我们上面确实做到了功能测试,但面试官可能会直接提示我们,假设我们的 `exponent` 输入一个负值,能得到正确值么? 44 | 45 | 跟着自己的代码走一遍,终于意识到了这个问题,当 `exponent` 为负数的时候,循环根本就进不去,无论输入的负数是什么,都会返回 1.0,这显然是不正确的算法。 46 | 47 | 我们在数学中学过,**给一个数值上负数次方,相当于给这个数值上整数次方再求倒数。** 48 | 49 | 意识到这点,我们修正一下代码。 50 | 51 | ```java 52 | public class Test11 { 53 | 54 | private static double power(double base, int exponent) { 55 | // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; 56 | // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 57 | double result = 1.0; 58 | // 处理负数次方情况 59 | boolean isNegetive = false; 60 | if (exponent < 0) { 61 | isNegetive = true; 62 | exponent = -exponent; 63 | } 64 | for (int i = 0; i < exponent; i++) { 65 | result *= base; 66 | } 67 | if (isNegetive) 68 | return 1 / result; 69 | return result; 70 | } 71 | 72 | public static void main(String[] args) { 73 | System.out.println(power(2, 2)); 74 | System.out.println(power(2, 4)); 75 | System.out.println(power(3, 1)); 76 | System.out.println(power(3, -1)); 77 | } 78 | } 79 | ``` 80 | 81 | 我们在代码中增加了一个判断是否为负数的 `isNegetive` 变量,当为负数的时候,我们就置为 true,并计算它的绝对值次幂,最后返回结果的时候返回它的倒数。 82 | 83 | 面试官看到这样的代码,可能就有点按捺不住内心的怒火了,不过由于你此前一直面试回答的较好,也打算再给你点机会,面试官提示你,当 `base` 传入 0,`exponent` 传入负数,会怎样? 84 | 85 | 瞬间发现了自己的问题,这不是犯了数学最常见的问题,给 0 求倒数么? 86 | 87 | > 虽然 Java 的 Math.pow() 方法也存在这个问题,但我们这里忽略不计。 88 | 89 | 于是马上更新代码。 90 | 91 | ```java 92 | public class Test11 { 93 | 94 | 95 | private static double power(double base, int exponent) { 96 | // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; 97 | // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 98 | double result = 1.0; 99 | // 处理底数为 0 的情况,底数为 0 其他任意次方结果都应该是 0 100 | if (base == 0) 101 | return 0.0; 102 | // 处理负数次方情况 103 | boolean isNegetive = false; 104 | if (exponent < 0) { 105 | isNegetive = true; 106 | exponent = -exponent; 107 | } 108 | for (int i = 0; i < exponent; i++) { 109 | result *= base; 110 | } 111 | if (isNegetive) 112 | return 1 / result; 113 | return result; 114 | } 115 | 116 | public static void main(String[] args) { 117 | System.out.println(power(2, 2)); 118 | System.out.println(power(2, 4)); 119 | System.out.println(power(3, 1)); 120 | System.out.println(power(0, -1)); 121 | } 122 | } 123 | ``` 124 | 125 | 有了上一次的经验,这次并不敢直接上交代码了,而是认真检查边界值和各种情况。检查 1 遍,2 遍,均没有发现问题,提交代码。 126 | 127 | > 计算机表示小数均有误差,这个在 Python 中尤其严重,但经数次测试,《剑指 Offer》中讲的双精度误差问题似乎在 Java 的 == 运算符中并不存在。如有问题,欢迎指正。 128 | 129 | 上面的代码基本还算整,健壮性也还不错,但面试官可能还想问问有没有更加优秀的算法。 130 | 131 | 仔细查看,确实似乎是有办法优化的,比如我们要求 `power(2,16)` 的值,我们只需要先求出 2 的 8 次方,再平方就可以了;以此类推,我们计算 2 的 8 次方的时候,可以先计算 2 的 4 次方,然后再做平方运算.....妙哉妙哉! 132 | 133 | 需要注意的是,如果我们的幂数为奇数的话,我们需要在最后再乘一次我们的底数。 134 | 135 | 我们尝试修改代码如下: 136 | 137 | ```java 138 | public class Test11 { 139 | private static double power(double base, int exponent) { 140 | // 因为除了 0 以外,任何数值的 0 次方都为 1,所以我们默认为 1.0; 141 | // 0 的 0 次方,在数学书是没有意义的,为了贴切,我们也默认为 1.0 142 | double result = 1.0; 143 | // 处理底数为 0 的情况,底数为 0 其他任意次方结果都应该是 0 144 | if (base == 0) 145 | return 0.0; 146 | // 处理负数次方情况 147 | boolean isNegetive = false; 148 | if (exponent < 0) { 149 | isNegetive = true; 150 | exponent = -exponent; 151 | } 152 | result = getTheResult(base, exponent); 153 | if (isNegetive) 154 | return 1 / result; 155 | return result; 156 | } 157 | 158 | private static double getTheResult(double base, int exponent) { 159 | // 如果指数为0,返回1 160 | if (exponent == 0) { 161 | return 1; 162 | } 163 | // 指数为1,返回底数 164 | if (exponent == 1) { 165 | return base; 166 | } 167 | // 递归求一半的值 168 | double result = getTheResult(base, exponent >> 1); 169 | // 求最终值,如果是奇数,还要乘一次底数 170 | result *= result; 171 | if ((exponent & 0x1) == 1) { 172 | result *= base; 173 | } 174 | return result; 175 | 176 | } 177 | 178 | public static void main(String[] args) { 179 | System.out.println(power(2, 2)); 180 | System.out.println(power(2, 4)); 181 | System.out.println(power(3, -1)); 182 | System.out.println(power(0.1, 2)); 183 | } 184 | } 185 | ``` 186 | 187 | 完美解决。 188 | 189 | 在提交代码的时候,还可以主动提示面试官,我们在上面用右移运算符代替了除以 2,用位与运算符代替了求余运算符 % 来判断是一个奇数还是一个偶数。让他知道我们对编程的细节真的很重视,这大概也就是细节决定成败吧。一两个细节的打动说不定就让面试官下定决心给我们发放 Offer 了。 190 | 191 | > **位运算的效率比乘除法及求余运算的效率要高的多**。 192 | > 193 | > 因为移位指令占 2 个机器周期,而乘除法指令占 4 个机器周期。从硬件上看,移位对硬件更容易实现,所以我们更优先用移位。 194 | 195 | 好了,今天我们的面试精讲就到这里,我们明天再见! 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /others/Git 如何遗弃已经 Push 的提交.md: -------------------------------------------------------------------------------- 1 | ## Git 如何遗弃已经 Push 的提交 2 | 3 | 题目看起来很像是提供解决方案的文章,但实际上我并不会给大家直接提供解决方案,我们追求的从来不应该是答案,而是探索的过程。当然,如果你只想查看答案的话,请直接拉到文章最底部。 4 | 5 | ### 写在前面 6 | 7 | 相信大家都知道,Git 相比于 SVN,优势不言而喻,以致于现在大多数公司的项目都在采用 Git 进行管理。作为一个开发人员,对 Git 的使用自然应该是得心应手。 8 | 9 | 如果你还不会使用 Git 的话,那我劝你还是不要声张,好好的去学习一番,再自己弄个实验项目走一下流程,以免遭到同事的鄙视。 10 | 11 | 每个公司都会有自己不一样的 Git 分支管理规范,特别是在开发人员较多的公司,Git 的分支管理规范就显得更加重要。前面比较出名的 Git Flow 分支管理策略相信不少人都已经了解了,不熟悉的当然也可以去看看:http://nvie.com/posts/a-successful-git-branching-model/ 12 | 13 | ![分支管理](https://upload-images.jianshu.io/upload_images/3994917-84b44e605cc6f095.png) 14 | 15 | Git Flow 管理方式把项目分为 5 条线,通常会是下面的管理方式。 16 | 17 | - Master:作为稳定主分支,长期有效。不可以在此分支进行任何提交,只能接受从 Hotfix 分支或者 Release 分支发起的 merge request,该分支上的每一个提交都对应一个 Tag。 18 | - Develop:开发主分支,长期有效。不可以在此分支上做任何提交,只接受从 Feature 分支发起的 merge request。所有的 Alpha Release 都应该在这个分支发布。 19 | - Feature:功能分支,生命周期为产品迭代周期,每个分支对应一期的需求。只可以从 Develop 分支进行 Kick Off。可以 merge Release 分支的代码,生命周期结束后,需要 merge 回 Develop 分支。**方式需要采用 merge request。** 20 | - Release:发布分支,声明周期从新需求的预发布到正式发布,每一个分支对应一个新版本的版本号。只可以从 Develop 分支 Kick Off。声明周期结束后,需要 Merge 回 Master 及 Develop 分支,方式同样需要采用 merge request。所有的 Beta Release 均需要在该分支发布。 21 | - Hotfix:热修复分支,生命周期对应一个或者多个需要紧急修复并上线的 Bug,每一个分支对应一个小版本号。只可以从 Master 分支进行 Kick Off。声明周期结束后,需要 merge 回 Master 分支和 Develop 分支,方式当然也是采用 merge request。 22 | 23 | 实际上,如果你熟悉 Git 的话,你会很快发现上面的管理方式会存在历史提交非常混乱的缺点,但觉得不失为一个 Git 分支管理的经典。实际上,我们可以用 rebase 去替换 merge 让 commit 看起来更加清晰。对 rebase 和 merge 的优劣对比这里暂不做讲解,感兴趣的可以直接 Google 搜索。 24 | 25 | 下面就给大家分享一下发生在咕咚项目的一次坑爹的 Git 体验。 26 | 27 | ### 从 git revert 说起 28 | 29 | 咕咚项目组并没有对开发者限制 Develop 分支和 Master 分支的权限,我们暂时并没有一个专门做代码 Review 和 PR 的角色,其实一定意义上也提现了团队对每个人的信任。 30 | 31 | 我们依然会基于 Develop 做开发主线,每个需求迭代期,团队成员会从 Develop 拉取自己的分支,并命名于 feture/XX,然后各自在自己的分支上进行开发。 32 | 33 | 由于大家开发业务上的不同,所以在需求开发完毕,整合代码到 Develop 分支的时候,一般不会出现太多冲突的情况。 34 | 35 | 而我这边交接一个需求时,采用 merge 的时候出现了一个奇怪的问题,我们姑且来重现一下事故现场。 36 | 37 | 首先使用 `git branch` 查看一下当前我们的本地分支。 38 | 39 | ![查看分支](https://upload-images.jianshu.io/upload_images/3994917-81a686501ae2afeb.png) 40 | 41 | 这里先简单提一下我们要做的操作。 42 | 43 | "feature8.28_buyGifts" 是我们同事的分支,基于 "release8.27.0" 拉取,而 "feature8.29.0_nanchen" 是我的分支,基于 "release8.28.0" 分支拉取,所以我这边的分支包含了最新的代码。 44 | 45 | 现在由于某些原因,我需要把同事的 "feature8.28_buyGifs" 分支代码合并到我的分支上,直接接手他的代码进行开发。 46 | 47 | > 就不要吐槽为啥不按照功能搞分支开发了,原因是因为他那边代码基本已经完成,现在只需要少量修改。 48 | 49 | 所以我们就采用 `git merge ` 命令进行 merge 操作。 50 | 51 | ![merge](https://upload-images.jianshu.io/upload_images/3994917-de4a932a54324665.png) 52 | 53 | 我们用 `git status` 更容易看明白冲突了什么。 54 | 55 | ![](https://upload-images.jianshu.io/upload_images/3994917-c91159f3787e8ca2.png) 56 | 57 | 可以看到,上面冲突的文件全是和同事开发的需求出现的冲突,所以出现这个冲突其实令人非常懊恼,因为是不可能有其他同事改动到这些文件的。 58 | 59 | 为了验证自己的想法,我们随意打开一个文件查看。这里就采用 `vim ` 查看第一个文件。 60 | 61 | ![](https://upload-images.jianshu.io/upload_images/3994917-f988d620cab43fbf.png) 62 | 63 | 正如我们所想,确实和同事编写的需求 `Presents` 类有关系,但看冲突内容就更一脸懵逼了,因为看起来,这应该是一个不会冲突的 merge。 64 | 65 | 于是赶紧使用 `git merge --abort` 撤销这次 merge。再在 "origin/feature8.29.0_nanchen" 查看我们刚刚的文件提交历史。 66 | 67 | ![](https://upload-images.jianshu.io/upload_images/3994917-7d0019d292a08f50.png) 68 | 69 | 可以很清晰的看到,确实是最近没有任何的修改记录。 70 | 71 | **一个 7 个月都没人动的文件,居然 merge 的时候发生了冲突!**这让我一脸懵逼。(手动黑人问号) 72 | 73 | 使用 `git lg` 查看一下该分支的提交历史,我们希望从中能得到某些思路。 74 | 75 | ![](https://upload-images.jianshu.io/upload_images/3994917-c82bc8c50bb0a83a.png) 76 | 77 | 注意其中红框中的 commit,我们这位同事之前想往 "release8.28.0" 合并他分支的代码,后面又因为某些原因,希望撤销这次提交,他采用了 revert 进行处理。**虽然 revert 对文件没有提交记录,但 Git 却认为我们在当前分支更改了这些文件,所以在我们 `git merge` 的时候,Git 认为这是一次冲突,并选择了告知我们。** 78 | 79 | 如若如我们所想,那我们只需要撤销这次 revert 操作即可。 80 | 81 | 我们当然知道,可以通过 reset 命令放弃这次提交,但这里后面已经有了非常多的 commit,显然我们这样是不行的,我们需要另辟蹊径。 82 | 83 | ### 解决方案? 84 | 85 | 最容易想到的大概就是直接在 merge 的时候解决冲突了,但通过一系列查看以后,我们发现文件改动量非常大,直接解决冲突并非易事。所以我们还是得 **想办法取消掉这次 revert 的 commit,再进行 merge**。 86 | 87 | 我们知道,代码回滚有三种方式:reset、checkout,还有我们的 revert。直观感受,我们应该在 reset 上想办法。 88 | 89 | 我们来看看 reset 有些怎样的操作方法。 90 | 91 | ![](https://upload-images.jianshu.io/upload_images/3994917-a6094e7c28580e87.png) 92 | 93 | 主要想给大家讲讲:--soft 和 --hard 的区别。 94 | 95 | 我们经常会用到 `git reset --hard ` 做「毁尸灭迹」的操作,常常爽到不能自已,因为这不仅可以回退到我们想要的版本,而且还「直接丢弃」了后面提交的代码,真正的「毁尸灭迹」级别的操作。 96 | 97 | 而另外一个 --soft 处理,实际上还具备点人性,虽然同样可以回退到我们想要的版本,但目标版本后面的提交都还会存放在 stage 区域中,以便后面找出证据。 98 | 99 | 说到这,似乎我们已经有了思路。 100 | 101 | 1. 使用 `git reset --soft ` 回退到 revert 操作的版本; 102 | 2. 使用 `git reset --hard ` 干掉那次 revert 提交; 103 | 3. 最后再把 stage 区域的所有改动汇聚成一个新的提交 commit 到我们的项目仓库中。 104 | 105 | **当然,细心的你一定会发现,在第 1 步操作后,我们还必须执行 `git stash` 命令把所有的改动存到暂存区,再在第 2 步操作后使用 `git stash pop` 命令取出来,直接进行第 2 步操作肯定还是会毁灭证据的。** 106 | 107 | ### 我们后面的提交不见了。 108 | 109 | 这样似乎可以解决我们的问题,不过有个弊端:**我们后面那么多的提交被合并成一个提交了,以后我们就没办法看到了,万一...** 110 | 111 | 不少小伙伴会想到进阶方案: 112 | 113 | 1. 对 "feature8.29.0_nanchen" 的最新代码 checkout -b 一个分支 feature_copy; 114 | 2. 然后使用 `git checkout feature8.29.0_nanchen` 回到我们的分支; 115 | 3. 然后直接对当前分支 reset 到 revert 的前一个 commit 后,我们采用 cherry-pick 方式进行傻瓜式改写便可以把历史重写了。(谁说的我们不能改写历史?) 116 | 117 | ### 改写历史? 118 | 119 | 改写历史?等等,好像还有一个操作:rebase。 120 | 121 | rebase 是 Git 的一个神奇的命令,前面我也说了,总会有人不喜欢 merge 之后历史的分叉,这种分叉再汇合后会让结构看起来非常混乱,以致于无法管理。如果你不喜欢 commit 历史出现分叉,那 rebase 绝对是你的救星。 122 | 123 | 改写历史是 rebase 与生俱来的能力。我们可以用 `git rebase -i ` 进行历史的改写。 124 | 125 | 我们试试看在我们的项目中直接使用 `git rebase -i ` 会怎样。 126 | 127 | ![](https://upload-images.jianshu.io/upload_images/3994917-1205e5e0223414bf.png) 128 | 129 | 我们会拿到分支后面的提交历史,并且前面还有一个 Commands。我们可以从提示中看到,上面全写的 pick 就是代表保持这个提交的意思,edit 代表编辑此次提交... 130 | 131 | 我们希望删除此次 revert 这次提交,那当然我们最关心的就是 drop 了,**甚至我们可以更加简单粗暴:直接删掉这一行**。 132 | 133 | 然后我们便开始处理了。 134 | 135 | ![](https://upload-images.jianshu.io/upload_images/3994917-f9d22eba92500ea3.png) 136 | 137 | 过程中可能会出现冲突,我们只需要解决就好。 138 | 139 | 解决掉冲突后,再使用 `git add ` 把它们 merge 进去。 140 | 141 | ![](https://upload-images.jianshu.io/upload_images/3994917-2032e702850f10ac.png) 142 | 143 | oh,我们看到我们已经 rebase 成功了。我们再使用 `git lg` 查看一下提交历史。 144 | 145 | ![](https://upload-images.jianshu.io/upload_images/3994917-1980d1692a078c41.png) 146 | 147 | **我们成功改写了历史!** 148 | 149 | 历史改写结束,我们还要做我们最开始想做的事情,进行 merge 操作。 150 | 151 | ![](https://upload-images.jianshu.io/upload_images/3994917-30868d63436765e2.png) 152 | 153 | 可以看到,这次我们 merge 确实如我们预期的不再发生冲突,方案亲测有效! 154 | 155 | ### 写在最后 156 | 157 | 写了这么多,想必大家对解决方案也算比较清楚了。我们主要便是采用 `git rebase -i <>` 操作进入到 commit 历史编辑页面,然后进行历史改写处理! -------------------------------------------------------------------------------- /algorithm/面试 15:顺时针从外往里打印数字.md: -------------------------------------------------------------------------------- 1 | ## 面试 15:顺时针从外往里打印数字 2 | 3 | > 题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入: 4 | > {{1,2,3}, 5 | > {4,5,6}, 6 | > {7,8,9}} 7 | > 则依次打印数字为 1、2、3、6、9、8、7、4、5 8 | 9 | 这是昨天最后给大家留下的题目,相信大家也有去思考如何处理这道题目了。 10 | 11 | 初看这个题目,比较容易理解,也无需牵扯到数据结构或者高级的算法,看起来问题比较简单,但实际上解决起来且并没有想象中的容易。 12 | 13 | 大家极有可能想到循环嵌套的方式,套用几个 for 循环就可以啦。 14 | 15 | > 1. 首先打印第 1 行,然后第一个 for 循环从第一列打印到最后一列。 16 | > 2. 到最后一列,开始向下打印,为了防止重复打印第一行最后一列的数字,所以应该从第二行开始打印; 17 | > 3. 上面步骤 2 到底的时候,再在最后一行从倒数第二列开始往前打印一直到第一列; 18 | > 4. 用步骤 3 到最后一行第一列的时候再往上打印,第一行第一列由于步骤 1 已经打印过,所以这次只需要从倒数第二行第一列开始打印到顺数第二行第一列即可; 19 | > 5. 然后里面其实是一样的,不难看出里面其实就是对一个更小的矩阵重复上面的步骤 1 到步骤 4; 20 | > 6. 由于之前说了一定注意边界值,所以我们再步骤 1 之前严格注意一下传入矩阵为 null 的情况。 21 | 22 | 思路想好了,所以开始下笔写起代码: 23 | 24 | ```java 25 | public class Test15 { 26 | 27 | private static void print(int[][] nums) { 28 | if (nums == null) 29 | return; 30 | 31 | int rows = nums.length; 32 | int columns = nums[0].length; 33 | // 因为一次循环后 里面的矩阵会少 2 行,所以我们步长应该设置为 2 34 | // 因为一次循环后 里面的矩阵会少 2 行,所以我们步长应该设置为 2 35 | for (int i = 0; i <= rows/2 || i <= columns/2; i++) { 36 | // 向右打印,i 代表第 i 行,用 j 代表列,从 0 到 列数-1-2*i 37 | for (int j = i; j < columns - 2 * i; j++) { 38 | System.out.print(nums[i][j] + ","); 39 | } 40 | // 向下打印,j 代表行,列固定为最后一列-i*2 41 | for (int j = i + 1; j < rows - 2 * i; j++) { 42 | System.out.print(nums[j][rows - 1 - 2 * i] + ","); 43 | } 44 | // 向左打印,j 代表列,行固定为最后一列-i*2 45 | for (int j = rows - 2 - 2 * i; j >= 2 * i; j--) { 46 | System.out.print(nums[rows - 1 - 2 * i][j] + ","); 47 | } 48 | // 向上打印,j 代表行,列固定为第一列 +i*2 49 | for (int j = rows - 2 - 2 * i; j > 2 * i; j++) { 50 | System.out.print(nums[j][2 * i] + ","); 51 | } 52 | } 53 | } 54 | 55 | public static void main(String[] args) { 56 | int[][] nums = {{1, 2, 3}, 57 | {4,5,6}, 58 | {7,8,9}}; 59 | print(nums); 60 | } 61 | ``` 62 | 63 | 上面的代码可能大家会觉得看的很绕,实际上我也很晕,在这种很晕的情况下通常是极易出现问题的。不信?不妨我们分析来看看。 64 | 65 | > 1. 首先我们做了 null 的输入值判断,挺好的,这没问题; 66 | > 2. 然后我们做了一个循环,输出看成一个环一个环的输出,因为输出完成一个环后总会少 2 行和 2 列,最后一次输出例外,所以我们给出步长为 2 ,并且中间的判断采用 || 而不是 &&,这里也没啥问题; 67 | > 3. 我们直接代入题干中的例子试一试。 68 | > 4. rows = 3,columns = 3,最外层循环会进行 2 次,符合条件; 69 | > 5. 进入第一次循环,第一次打印向右,j 从 0 一直递增到 2 循环 3 次,打印出 1, 2, 3,没问题; 70 | > 6. 进入第二次循环,本次循环我们希望打印 6,9;我们从 i + 1 列开始,一直到最后一列,正确,没问题; 71 | > 7. 进入第三次循环,测试没问题,可以正常打印 8,7; 72 | > 8. 进入第四次循环,测试没问题,可以正常打印 4; 73 | > 9. 最外层循环进入第二次,此时 i = 1, i < 1,出现错误。额,这里循环结束条件应该 i <= columns - 2 * i 74 | > 10. .... 75 | 76 | 不知道小伙伴有没有被绕晕,反正我已经云里雾里了,我是谁?我在哪? 77 | 78 | 各种试,会发现坑还不少,其实上面贴的这个代码已经是经过上面这样走流程走了好几次修正的,但特别无奈,这个坑始终填不满。 79 | 80 | 有时候,不得不说,其实能有上面这般思考的小伙伴已经很优秀了,但在算法上还是欠了点火候。在面试中,我们当然希望竭尽全力完成健壮性很棒,又能实现功能的代码,但不得不说,人都有思维愚钝的时候,有时候就是怎么也弄不出来。 81 | 82 | 我们在解题前,其实不妨通过画图或者其他的方式先和面试官交流自己的思路,虽然他不会告诉你这样做对与否。但这其实就形成了一种非常好的沟通方式,当然也是展现你沟通能力的一种体现! 83 | 84 | 前面的思路其实没毛病,只是即使我们得到了正解,但这样的一连串代码,别说面试官,你自己可能都看的头大。 85 | 86 | 我们确实可以用这样先打印矩阵最外层环,打印完后把里面的再当做一个环,重复外面的情况打印。环的打印次数上面也提了,限制结束的条件就是环数 <= 行数的二分之一 && 环数 <= 列数的 二分之一。 87 | 88 | 所以我们极易得到这样的代码: 89 | 90 | ```java 91 | private static void print(int[][] nums) { 92 | if (nums == null) 93 | return; 94 | int rows = nums.length; 95 | int columns = nums[0].length; 96 | for (int i = 0; i <= rows / 2 && i <= columns / 2; i++) { 97 | printRing(nums, i, rows, columns); 98 | } 99 | } 100 | ``` 101 | 102 | 我们着重是需要编写 `printRing(nums,i)` 的代码。 103 | 104 | 仔细分析,我们打印一圈实际上就分为四步: 105 | 106 | 1. 从左到右打印一行; 107 | 2. 从上到下打印一列; 108 | 3. 从右到左打印一行; 109 | 4. 从下到上打印一列; 110 | 111 | 不过值得注意的是,最后一圈有可能退化为只有一行,只有一列,甚至只有 1 个数字,因此这样的打印并不需要 4 步。下图是几个退化的例子,他们打印一圈分别只需要 3 步、2 步 甚至 1 步。 112 | 113 | ![剑指 Offer](/var/folders/6m/5yg4nys56t1dd5xpwk68cmbw0000gn/T/abnerworks.Typora/image-20180726171125124.png) 114 | 115 | 因此我们需要仔细分析打印时每一步的前提条件。 116 | 117 | - 第一步总是需要的,不管你是一个数字,还是只有一行。 118 | - 如果只有一行,那就不用第二步了,所以第二步能进去的条件是终止的行号大于起始的行号; 119 | - 如果刚刚两行并且大于两列,则可进行第三步打印; 120 | - 要想进行第四步的话,除了终止列号大于起始行号以外,还得至少有三行。 121 | 122 | 此外,依然得额外地注意:数组的下标是从 0 开始的,所以尾坐标总是得减 1 ,并且每进行一次循环,尾列和尾行的坐标总是得减去 1。 123 | 124 | 所以,完整的代码就奉上了: 125 | 126 | ```java 127 | public class Test15 { 128 | 129 | private static void print(int[][] nums) { 130 | if (nums == null) 131 | return; 132 | int rows = nums.length; 133 | int columns = nums[0].length; 134 | for (int i = 0; i <= rows / 2 && i <= columns / 2; i++) { 135 | printRing(nums, i, rows, columns); 136 | } 137 | } 138 | 139 | private static void printRing(int[][] nums, int start, int rows, int columns) { 140 | // 设置两个变量,endRow 代表当前环尾行坐标;endCol 代表当前环尾列坐标; 141 | int endRow = rows - 1 - start; 142 | int endCol = columns - 1 - start; 143 | 144 | // 第一步:打印第一行,行不变列变,列从起到尾 145 | for (int i = start; i <= endCol; i++) { 146 | System.out.print(nums[start][i] + ","); 147 | } 148 | // 假设有多行才需要打印第二步 149 | if (endRow > start) { 150 | // 第二步,打印尾列,行变列不变,需要注意的是尾列第一行已经打印过 151 | for (int i = start + 1; i <= endRow; i++) { 152 | System.out.print(nums[i][endCol] + ","); 153 | } 154 | } 155 | // 至少两行并且 2 列才会有第三步逆序打印 156 | if (endCol > start && endRow > start) { 157 | // 第三步,打印尾行,行不变,列变。需要注意尾行最后一列第二步已经打印 158 | for (int i = endCol - 1; i >= start; i--) { 159 | System.out.print(nums[endRow][i] + ","); 160 | } 161 | } 162 | // 至少大于 2 行 并且大于等于 2 列才会有第四步打印 163 | if (endRow > start && endCol - 1 > start) { 164 | // 第四步,打印首列,行变,列不变。需要注意尾行和首行的都打印过 165 | for (int i = endRow - 1; i >= start + 1; i--) { 166 | System.out.print(nums[i][start] + ","); 167 | } 168 | } 169 | } 170 | 171 | public static void main(String[] args) { 172 | int[][] nums = {{1, 2, 3, 4}, 173 | {5, 6, 7, 8}, 174 | {9, 10, 11, 12}}; 175 | print(nums); 176 | } 177 | } 178 | ``` 179 | 180 | 用自己准备的测试用例输入测试,没有问题,通过。 181 | 182 | 上面的代码中用两个变量 `endRow` 和 `endCol` 以及画图完美地解决了我们思路混乱并且代码难以看明白的问题。「**其实不用吐槽判断方法有重复的情况,我们都是为了看起来思路更加清晰。**」 183 | 184 | 只看不练,很明显这样的题是容易被绕进去的,思路其实我们很好想到,但实现出来完全是另外一回事,所以大家不妨再去动手试试吧~ 185 | 186 | 紧张之余,还是要留下明天的习题,记得提前思考和动手练习哟~ 187 | 188 | > 面试题:输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如:压入序列为{1,2,3,4,5},那{4,5,3,2,1} 就是该栈的弹出顺序,而{4,3,5,1,2} 明显就不符合要求; -------------------------------------------------------------------------------- /algorithm/面试 15:针对昨天的推文,有几句想说的.md: -------------------------------------------------------------------------------- 1 | ## 针对昨天的推文,有几句想说的 2 | 3 | 又一次周末推文,原本是想周一推的,但是这样的错误让我实在无法等待到周一了。 4 | 5 | 在昨天的推文 [面试 15:顺时针从外往里打印数字](https://mp.weixin.qq.com/s/nzUTmCuIaSgtpag5S1RQ3w) 中,我出现了明显的「勘误」,额不,这不是「勘误」,这是明显的算法错误。我们来看我们的算法题目,题目来自《剑指 Offer》第 20 题。 6 | 7 | > 题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入: 8 | > {{1,2,3}, 9 | > {4,5,6}, 10 | > {7,8,9}} 11 | > 则依次打印数字为 1、2、3、6、9、8、7、4、5 12 | 13 | 我们最后给大家总结的代码是这样的。 14 | 15 | ```java 16 | public class Test15 { 17 | 18 | private static void print(int[][] nums) { 19 | if (nums == null) 20 | return; 21 | int rows = nums.length; 22 | int columns = nums[0].length; 23 | for (int i = 0; i <= rows / 2 && i <= columns / 2; i++) { 24 | printRing(nums, i, rows, columns); 25 | } 26 | } 27 | 28 | private static void printRing(int[][] nums, int start, int rows, int columns) { 29 | // 设置两个变量,endRow 代表当前环尾行坐标;endCol 代表当前环尾列坐标; 30 | int endRow = rows - 1 - start; 31 | int endCol = columns - 1 - start; 32 | 33 | // 第一步:打印第一行,行不变列变,列从起到尾 34 | for (int i = start; i <= endCol; i++) { 35 | System.out.print(nums[start][i] + ","); 36 | } 37 | // 假设有多行才需要打印第二步 38 | if (endRow > start) { 39 | // 第二步,打印尾列,行变列不变,需要注意的是尾列第一行已经打印过 40 | for (int i = start + 1; i <= endRow; i++) { 41 | System.out.print(nums[i][endCol] + ","); 42 | } 43 | } 44 | // 至少两行并且 2 列才会有第三步逆序打印 45 | if (endCol > start && endRow > start) { 46 | // 第三步,打印尾行,行不变,列变。需要注意尾行最后一列第二步已经打印 47 | for (int i = endCol - 1; i >= start; i--) { 48 | System.out.print(nums[endRow][i] + ","); 49 | } 50 | } 51 | // 至少大于 2 行 并且大于等于 2 列才会有第四步打印 52 | if (endRow > start && endCol - 1 > start) { 53 | // 第四步,打印首列,行变,列不变。需要注意尾行和首行的都打印过 54 | for (int i = endRow - 1; i >= start + 1; i--) { 55 | System.out.print(nums[i][start] + ","); 56 | } 57 | } 58 | } 59 | 60 | public static void main(String[] args) { 61 | int[][] nums = {{1, 2, 3, 4}, 62 | {5, 6, 7, 8}, 63 | {9, 10, 11, 12}}; 64 | print(nums); 65 | } 66 | } 67 | ``` 68 | 69 | 实际上细心的小伙伴会发现这并不是 AC 的代码,也有一名小伙伴在推文后面留言要测试用例(暂不清楚他是否发现了问题),在文后留言中,我发现了一名叫「wants温拿」的小伙伴这样留言到。 70 | 71 | > 你好,你试过数组是{{1, 2, 3, 4},{5, 6, 7, 8}}这种情况吗?会有重复打印的情况。 72 | 73 | 我立马停下手中的工作,放弃了午休,用这个测试用例代入查看代码。 74 | 75 | 确实存在重复打印的问题,当我们输入 {{1,2,3,4},{5,6,7,8}} 的时候,我们控制台得到的输出是:1,2,3,4,8,7,6,5,6,7, 76 | 77 | 从这里我们可以看出,完整的测试用例是多么重要,**所以以后我们的推文都会加重对测试用例的投入时间。** 78 | 79 | 出现了这样的重复,其实不少人一眼便能看出是 for 循环出了问题,然而南尘却非常愚钝,我竟然第一感觉是 `printRing()` 方法的最后一个 if 出现了问题。 80 | 81 | 确实发现了 if 的判断出了一点差错,我们最后一个 if 希望判断的是 **至少大于 2 行,并且最少 2 列才可以打印。** 82 | 83 | 而我们的代码中给出的 if 却是 **至少大于 2 列,并且最少 2 行才可以打印。** 84 | 85 | 这里明显会出现当我们的输入是 2 列 n 行( n >= 2 ) 的时候,不会执行第四个 for 循环的。 86 | 87 | 发现了这个问题,但无论是分析还是运行,这都不是造成我们 **重复打印的原因**。 88 | 89 | 我们看看为什么会出现这样的情况,倘若我们是用 IDE 来处理的话,直接 Debug 一下就会发现我们是 `for (int i = 0; i <= rows / 2 && i <= columns / 2; i++)` 这个 for 循环的问题。用我们「wants温拿」小伙伴的测试用例就可以看到,原本我们期望的是 for 循环只需要执行 1 次便退出,而目前的代码会进行两次循环,所以导致了我们的重复输出。 90 | 91 | > 正常现场面试是手写的,我们可没那么幸运可以利用 IDE 进行 Debug,这里为了时间快就姑且快速处理。 92 | 93 | 知道了是 for 循环的问题,我们自然就知道怎么处理了。实际上在我们上篇文章的分析中,我们已经得到了一个信息:那就是第一次循环是从 nums\[0][0] 开始打印,第二次循环就是从 nums\[1][1] 打印……第 n 次循环就是从 nums\[n][n] 开始打印。而我们现在就需要看看什么时候才应该有第二次循环。毫无疑问,至少得有 3 行 3 列。进行第三次循环至少得有 5 行 5 列,第四次循环至少得有 7 行 7 列 ..... 第 n 次循环,至少得有 2 * n -1 行 2 * n - 1 列。所以我们不难得到,正确的循环进行条件应该为: 94 | 95 | ```java 96 | 2 * (i + 1) - 1 <= rows && 2 * (i + 1) - 1 <= columns 97 | ``` 98 | 99 | 简化后得到: 100 | 101 | ```java 102 | 2 * i + 1 <= rows && 2 * i + 1 <= columns 103 | ``` 104 | 105 | 顺着这样的思路,我们肯定直接用 while 循环更加贴近我们的思路,所以自然得到代码为: 106 | 107 | ```java 108 | public class Test15 { 109 | 110 | private static void print(int[][] nums) { 111 | if (nums == null) 112 | return; 113 | int rows = nums.length; 114 | int columns = nums[0].length; 115 | int i = 0; 116 | while (2 * i + 1 <= rows && 2 * i + 1 <= columns) { 117 | printRing(nums, i, rows, columns); 118 | ++i; 119 | } 120 | } 121 | 122 | private static void printRing(int[][] nums, int start, int rows, int columns) { 123 | // 设置两个变量,endRow 代表当前环尾行坐标;endCol 代表当前环尾列坐标; 124 | int endRow = rows - 1 - start; 125 | int endCol = columns - 1 - start; 126 | 127 | // 第一步:打印第一行,行不变列变,列从起到尾 128 | for (int i = start; i <= endCol; i++) { 129 | System.out.print(nums[start][i] + ","); 130 | } 131 | // 假设有多行才需要打印第二步 132 | if (endRow > start) { 133 | // 第二步,打印尾列,行变列不变,需要注意的是尾列第一行已经打印过 134 | for (int i = start + 1; i <= endRow; i++) { 135 | System.out.print(nums[i][endCol] + ","); 136 | } 137 | } 138 | // 至少两行并且 2 列才会有第三步逆序打印 139 | if (endCol > start && endRow > start) { 140 | // 第三步,打印尾行,行不变,列变。需要注意尾行最后一列第二步已经打印 141 | for (int i = endCol - 1; i >= start; i--) { 142 | System.out.print(nums[endRow][i] + ","); 143 | } 144 | } 145 | // 至少大于 2 行 并且大于等于 2 列才会有第四步打印 146 | if (endCol > start && endRow - 1 > start) { 147 | // 第四步,打印首列,行变,列不变。需要注意尾行和首行的都打印过 148 | for (int i = endRow - 1; i >= start + 1; i--) { 149 | System.out.print(nums[i][start] + ","); 150 | } 151 | } 152 | } 153 | 154 | public static void main(String[] args) { 155 | int[][] nums = {{1, 2, 3, 4}, 156 | {5, 6, 7, 8}}; 157 | print(nums); 158 | } 159 | } 160 | ``` 161 | 162 | 代码写毕,我们还是得拿出我们的测试用例进行验证: 163 | 164 | - 输入 null 或者 空数组,什么也不输出,满足条件; 165 | - 输入 1 个数字的,比如 {{1}},输出 1 满足条件; 166 | - 输入 1 行 x 列的,比如 {{1,2,3,4,5,6,7,8}},输出满足条件; 167 | - 输入 x 行 1 列的,比如 {{1},{2},{3},{4}},输出满足条件; 168 | - 输入 3 行 2 列的,输出满足条件; 169 | - 输入 2 行 3 列的,输出满足条件; 170 | 171 | 这次应该是能 AC 了,总结一下:**完整的测试用例很重要,完整的测试用例很重要,完整的测试用例很重要!** 172 | 173 | 重要的话说了三遍了,当然也印证了一句话,南尘本来就是比较菜的,普普通通的一个人,所以各位叫我「大佬」那还真是承受不起啦~ 174 | 175 | 不过从这样的错误中,你我一起成长,岂不妙哉? 176 | 177 | 最近也有不少小伙伴在问我怎么学习算法以及为什么花这么多时间学习算法,其实在这里也很明了了,学习算法每个人的目的不一样,我主要觉得一个人应该足够靠谱,从程序思维都能感悟出来了。 178 | 179 | 对于怎么学习算法,大家可以看到我这是啃 《剑指 Offer》,用 Java 实现,并且不看答案,提前思考并直接上手编写代码的。不过我还差了一步,那就是自己思考后再去看看作者的代码。很明显,这里我只要去看作者的答案,自然就不会在小伙伴「wants温拿」指出了问题才明白的。 180 | 181 | 最后,为了强调大家一起学习,对于本次找出问题的「wants温拿」同学也准备了个小红包以示心意,请「wants温拿」小伙伴,在后台回复「加群」获取我的微信添加方式,然后找到我领取你的小红包哟~ 182 | 183 | 另外,希望其他小伙伴后面也认真跟进南尘的文章,发现这样的问题的,最好能自己找出为什么出错的原因就更好的。 184 | 185 | 好了南尘的朋友们,周六愉快,周末愉快!爱你们~ -------------------------------------------------------------------------------- /algorithm/面试 6:调整数组顺序使奇数位于偶数前面.md: -------------------------------------------------------------------------------- 1 | ## 面试:调整数组顺序使奇数位于偶数前面 2 | 3 | > 面试题:输入一个整型数组,实现一个函数来调整该数组中的数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分,**希望时间复杂度尽量小。** 4 | 5 | 看到这道题,想必大多数人都是能一下就想到从头到尾扫描一遍数组,然后遇到奇数就移动到最前面,~~遇到偶数就移动到最后面~~的思路,于是便有了下面的代码。 6 | 7 | > 注:**《剑指 Offer》上面的 「遇到奇数移动到最前面,遇到偶数也移动到最后面」其实只需要做其中一种即可。** 8 | 9 | ```java 10 | public class Test14 { 11 | 12 | private static int[] reOrderArray(int[] arr) { 13 | for (int i = 0; i < arr.length; i++) { 14 | // 遇到奇数就放到最前面 15 | if (Math.abs(arr[i]) % 2 == 1) { 16 | int temp = arr[i]; 17 | // 先把 i 前面的都向后移动一个位置 18 | for (int j = i; j > 0; j--) { 19 | arr[j] = arr[j - 1]; 20 | } 21 | arr[0] = temp; 22 | } 23 | } 24 | return arr; 25 | } 26 | 27 | public static void main(String[] args) { 28 | int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; 29 | arr = reOrderArray(arr); 30 | for (int i = 0; i < arr.length; i++) { 31 | System.out.print(arr[i] + " "); 32 | } 33 | System.out.println(); 34 | 35 | int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; 36 | arr1 = reOrderArray(arr1); 37 | for (int i = 0; i < arr1.length; i++) { 38 | System.out.print(arr1[i] + " "); 39 | } 40 | System.out.println(); 41 | 42 | int[] arr2 = {2, 4, 6, 8, 10}; 43 | arr2 = reOrderArray(arr2); 44 | for (int i = 0; i < arr2.length; i++) { 45 | System.out.print(arr2[i] + " "); 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | 上面的代码固然能达到功能,但时间复杂度上完全不能恭维。每找到一个奇数,我们总是要去移动不少个位置的数。 52 | 53 | 等等。 54 | 55 | 我们上面算法最大的问题在于移动,我们能否不做这个移动呢? 56 | 57 | 当然是可以的。题目要求所有奇数都应该在偶数前面,所以我们应该只需要维护两个下标值,让一个下标值从前往后遍历,另外一个下标值从后往前遍历,**当发现第一个下标值对应到偶数,第二个下标值对应到奇数的时候,我们就直接对调两个值。**直到第一个下标到了第二个下标的后面的时候退出循环。 58 | 59 | 我们有了这样的想法,可以先拿一个例子在心中走一遍,如果没有问题再写代码,这样也可以让面试官知道,我们并不是那种上来就开始写代码不考虑全面的程序员。 60 | 61 | 1. 假定输入的数组是 {1,2,3,4,5}; 62 | 2. 设定 odd = 0,代表第一个下标;even = arr.length = 4; 63 | 3. 从前往后移动第一个下标 odd,直到它等于偶数,即当 odd = 1 的时候,我们停止移动; 64 | 4. 再从后往前移动下标 even,直到它等于奇数,即当 even = 4 的时候,我们停止移动; 65 | 5. 满足 arr[odd] 为偶数,arr[even] 为奇数,我们对调两个值,得到新数组 {1,5,3,4,2}; 66 | 6. 继续循环,此时 odd = 3,even = 2,不满足 odd < even 的条件,退出循环,得到的数组符合条件; 67 | 68 | 心中默走一遍没问题后,开始手写代码: 69 | 70 | ```java 71 | public class Test14 { 72 | 73 | private static int[] reOrderArray(int[] arr) { 74 | int odd = 0, even = arr.length - 1; 75 | // 循环结束条件为 odd >= even 76 | while (odd < even) { 77 | // 第一个下标为偶数的时候停止 78 | while (odd < even && Math.abs(arr[odd]) % 2 != 0) { 79 | odd++; 80 | } 81 | // 第二个下标为奇数的时候停止 82 | while (odd < even && Math.abs(arr[even]) % 2 == 0) { 83 | even--; 84 | } 85 | 86 | // 找到后对调两个值 87 | int temp = arr[odd]; 88 | arr[odd] = arr[even]; 89 | arr[even] = temp; 90 | 91 | } 92 | return arr; 93 | } 94 | 95 | public static void main(String[] args) { 96 | int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; 97 | arr = reOrderArray(arr); 98 | for (int i = 0; i < arr.length; i++) { 99 | System.out.print(arr[i] + " "); 100 | } 101 | System.out.println(); 102 | 103 | int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; 104 | arr1 = reOrderArray(arr1); 105 | for (int i = 0; i < arr1.length; i++) { 106 | System.out.print(arr1[i] + " "); 107 | } 108 | System.out.println(); 109 | 110 | int[] arr2 = {2, 4, 6, 8, 10}; 111 | arr2 = reOrderArray(arr2); 112 | for (int i = 0; i < arr2.length; i++) { 113 | System.out.print(arr2[i] + " "); 114 | } 115 | System.out.println(); 116 | } 117 | } 118 | ``` 119 | 120 | #### 扩展性更好的代码,能秒杀 Offer 121 | 122 | 如果是面试应届毕业生或者工作时间不长的程序员,面试官可能会满意前面的代码,但如果应聘者申请的是资深 的开发岗位,那面试官可能会接着问几个问题。 123 | 124 | >- 面试官:如果把题目改成把数组中的数组按照大小分为两部分,所有的负数都在非负整数的前面,该怎么做? 125 | >- 应聘者:这很简单,可以重新定义一个函数,在新的函数里,只要修改第二个和第三个 while 循环里面的判断条件就好了。 126 | >- 面试官:如果再把题目改改,变成把数组中的数分为两部分,能被 3 整除的数都在不能被 3 整除的数的前面,怎么办? 127 | >- 应聘者:我们还是可以定义一个新的函数,在这个函数中...... 128 | >- 面试官:(打断应聘者的话),难道就没有更好的方法? 129 | 130 | 这个时候应聘者应该要反应过来,面试官期待我们能提供的不仅仅是解决一个问题的办法,而是解决一系列同类型问题的通用方法。我们在做解法的时候不能只想着解决当前的问题就好。在《大话设计模式》中,讲解了一个非常有意思的事情就是大鸟让小菜做商场促销活动的时候,各种改变需求,把小菜绕的云里雾里。 131 | 132 | > 《大话设计模式》PDF 版本可以在公众号后台回复「大话设计模式」即可获取。 133 | 134 | 是呀,哪有不变的需求,需求不变,我们哪来那么多活干呀?不过要是,我们事先就做了这样的准备,省下来的时间那不是正好又可以去玩一盘吃鸡洛? 135 | 136 | 回到面试官新提出的两个问题来,我们其实新的函数都只需要更改第二个和第三个 while 循环里面的判断条件,而其它都是不需要动的。 137 | 138 | ```java 139 | public class Test14 { 140 | 141 | interface ICheck { 142 | boolean function(int n); 143 | } 144 | 145 | public static class OrderEven implements ICheck { 146 | @Override 147 | public boolean function(int n) { 148 | return n % 2 == 0; 149 | } 150 | } 151 | 152 | private static int[] reOrderArray(int[] arr, ICheck iCheck) { 153 | int odd = 0, even = arr.length - 1; 154 | // 循环结束条件为 odd >= even 155 | while (odd < even) { 156 | // 第一个下标为偶数的时候停止 157 | while (odd < even && !iCheck.function(arr[odd])) { 158 | odd++; 159 | } 160 | // 第二个下标为奇数的时候停止 161 | while (odd < even && iCheck.function(arr[even])) { 162 | even--; 163 | } 164 | 165 | // 找到后对调两个值 166 | int temp = arr[odd]; 167 | arr[odd] = arr[even]; 168 | arr[even] = temp; 169 | 170 | } 171 | return arr; 172 | } 173 | 174 | public static void main(String[] args) { 175 | OrderEven even = new OrderEven(); 176 | int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; 177 | arr = reOrderArray(arr,even); 178 | for (int i = 0; i < arr.length; i++) { 179 | System.out.print(arr[i] + " "); 180 | } 181 | System.out.println(); 182 | 183 | int[] arr1 = {2, 4, 6, 8, 1, 3, 5, 7, 9}; 184 | arr1 = reOrderArray(arr1,even); 185 | for (int i = 0; i < arr1.length; i++) { 186 | System.out.print(arr1[i] + " "); 187 | } 188 | System.out.println(); 189 | 190 | int[] arr2 = {2, 4, 6, 8, 10}; 191 | arr2 = reOrderArray(arr2,even); 192 | for (int i = 0; i < arr2.length; i++) { 193 | System.out.print(arr2[i] + " "); 194 | } 195 | System.out.println(); 196 | 197 | } 198 | } 199 | ``` 200 | 201 | > **写这玩意儿的时候,我内心是拒绝的,由于 Java 没有 Python 一样方便的函数指针,我想了想只想到了用接口方式来处理。要是有其他实现方式的希望大家能在评论区留言~** 202 | 203 | 好了,今天的面试讲解,就先到这儿吧。 204 | 205 | -------------------------------------------------------------------------------- /algorithm/面试 7:面试常见的链表类算法捷径(一).md: -------------------------------------------------------------------------------- 1 | ## 面试 7:面试常见的链表类算法捷径 2 | 3 | 链表是我们数据结构面试中比较容易出错的问题,所以很多面试官总喜欢在这上面下功夫,为了避免出错,我们最好先进行全面的分析。在实际软件开发周期中,设计的时间通常不会比编码的时间短,在面试的时候我们不要着急于写代码,而是一开始仔细分析和设计,这将给面试官留下一个很好的印象。 4 | 5 | **与其很快写出一段千疮百孔的代码,不容仔细分析后再写出健壮性无敌的程序。** 6 | 7 | > 面试题:输入一个单链表的头结点,返回它的中间元素。为了方便,元素值用整型表示。 8 | 9 | 当应聘者看到这道题的时候,内心一阵狂喜,怎么给自己遇到了这么简单的题。拿起笔就开始写,先遍历整个链表,拿到链表的长度 len,再次遍历链表,位于 len/2 的元素就是链表的中间元素。 10 | 11 | 所以这个题最重要的点就是拿到链表的长度 len。而拿到这个 len 也比较简单,只需要遍历前设定一个 count 值,遍历的时候 count++ ,第一次遍历结束,就拿到单链表的长度 len 了。 12 | 13 | 于是我们很快写出了这样的代码: 14 | 15 | ```java 16 | public class Test15 { 17 | public static class LinkNode { 18 | int data; 19 | LinkNode next; 20 | 21 | public LinkNode(int data) { 22 | this.data = data; 23 | } 24 | } 25 | 26 | private static int getTheMid(LinkNode head) { 27 | int count = 0; 28 | LinkNode node = head; 29 | while (head != null) { 30 | head = head.next; 31 | count++; 32 | } 33 | for (int i = 0; i < count / 2; i++) { 34 | node = node.next; 35 | } 36 | return node.data; 37 | } 38 | 39 | public static void main(String[] args) { 40 | LinkNode head = new LinkNode(1); 41 | head.next = new LinkNode(2); 42 | head.next.next = new LinkNode(3); 43 | head.next.next.next = new LinkNode(4); 44 | head.next.next.next.next = new LinkNode(5); 45 | System.out.println(getTheMid(head)); 46 | } 47 | } 48 | ``` 49 | 50 | 面试官看到这个代码的时候,他告诉我们上面代码循环了两次,但是他期待的只有一次。 51 | 52 | 于是我们绞尽脑汁,突然想到了网上介绍过的一个概念:**快慢指针法**。 53 | 54 | 假设我们设置两个变量 slow、fast 起始都指向单链表的头结点当中,然后依次向后面移动,fast 的移动速度是 slow 的 2 倍。这样当 fast 指向末尾节点的时候,slow 就正好在正中间了。 55 | 56 | 想清楚这个思路后,我们很快就能写出如下代码: 57 | 58 | ```java 59 | public class Test15 { 60 | public static class LinkNode { 61 | int data; 62 | LinkNode next; 63 | 64 | public LinkNode(int data) { 65 | this.data = data; 66 | } 67 | } 68 | 69 | private static int getTheMid(LinkNode head) { 70 | LinkNode slow = head; 71 | LinkNode fast = head; 72 | while (fast != null && fast.next != null) { 73 | fast = fast.next.next; 74 | slow = slow.next; 75 | } 76 | return slow.data; 77 | } 78 | 79 | public static void main(String[] args) { 80 | LinkNode head = new LinkNode(1); 81 | head.next = new LinkNode(2); 82 | head.next.next = new LinkNode(3); 83 | head.next.next.next = new LinkNode(4); 84 | head.next.next.next.next = new LinkNode(5); 85 | System.out.println(getTheMid(head)); 86 | } 87 | } 88 | ``` 89 | 90 | #### 快慢指针法举一反三 91 | 92 | **快慢指针法** 确实在链表类面试题中特别好用,我们不妨在这里举一反三,对原题稍微修改一下,其实也可以实现。 93 | 94 | > 面试题:给定一个单链表的头结点,判断这个链表是否是循环链表。 95 | 96 | 和前面的问题一样,我们只需要定义两个变量 slow,fast,同时从链表的头结点出发,fast 每次走链表,而 slow 每次只走一步。如果走得快的指针追上了走得慢的指针,那么链表就是环形(循环)链表。如果走得快的指针走到了链表的末尾(fast.next 指向 null)都没有追上走得慢的指针,那么链表就不是环形链表。 97 | 98 | 有了这样的思路,实现代码那还不是分分钟的事儿。 99 | 100 | ```java 101 | public class Test15 { 102 | public static class LinkNode { 103 | int data; 104 | LinkNode next; 105 | 106 | public LinkNode(int data) { 107 | this.data = data; 108 | } 109 | } 110 | 111 | private static boolean isRingLink(LinkNode head) { 112 | LinkNode slow = head; 113 | LinkNode fast = head; 114 | while (slow != null && fast != null && fast.next != null) { 115 | if (slow == fast || fast.next = slow) { 116 | return true; 117 | } 118 | fast = fast.next.next; 119 | slow = slow.next; 120 | } 121 | return false; 122 | } 123 | 124 | public static void main(String[] args) { 125 | LinkNode head = new LinkNode(1); 126 | head.next = new LinkNode(2); 127 | head.next.next = new LinkNode(3); 128 | head.next.next.next = new LinkNode(4); 129 | head.next.next.next.next = new LinkNode(5); 130 | System.out.println(isRingLink(head)); 131 | head.next.next.next.next.next = head; 132 | System.out.println(isRingLink(head)); 133 | } 134 | } 135 | ``` 136 | 137 | 确实有意思,**快慢指针法** 再一次利用它的优势巧妙解决了我们的问题。 138 | 139 | #### 快慢指针法的延展 140 | 141 | 我们上面讲解的「快慢指针法」均是一个变量走 1 步,一个变量走 n 步。我们其实还可以拓展它。这个「快慢」并不是说一定要同时遍历。 142 | 143 | 比如《剑指Offer》中的第 15 道面试题,就运用到了「快慢指针法」的延展。 144 | 145 | > 面试题:输入一个单链表的头结点,输出该链表中倒数第 k 个节点的值。 146 | 147 | 初一看这个似乎并不像我们前面学习到的「快慢指针法」的考察。所以大多数人就迷糊了,进入到常规化思考。依然还是设置一个整型变量 count,然后每次循环的时候 count++,拿到链表的长度 n。那么倒数第 k 个节点也就是顺数第 n-k+1 个结点。所以我们只需要在拿到长度 n 后再进行一次 n-k+1 次循环就可以拿到这个倒数第 k 个节点的值了。 148 | 149 | 但面试官显然不会太满意这个臃肿的解法,他依然希望我们一次循环就能搞定这个事。 150 | 151 | 为了实现只遍历一次链表就能找到倒数第 k 个结点,我们依然可以定义两个遍历 slow 和 fast。我们让 fast 变量先往前遍历 k-1 步,slow 保持不动。从第 k 步开始,slow 变量也跟着 fast 变量从链表的头结点开始遍历。由于两个变量指向的结点距离始终保持在 k-1,那么当 fast 变量到达链表的尾结点的时候,slow 变量指向的结点正好是我们所需要的倒数第 k 个结点。 152 | 153 | 我们依然可以在心中默认一遍代码: 154 | 155 | 1. 假设输入的链表是:1->2->3->4->5; 156 | 2. 现在我们要求倒数第三个结点的值,即顺数第 3 个结点,它的值为 3; 157 | 3. 定义两个变量 slow、fast,它们均指向结点 1; 158 | 4. 先让 fast 向前走 k-1 即 2 步,这时候 fast 指向了第 3 个结点,它的值是 3; 159 | 5. 现在 fast 和 slow 同步向右移动; 160 | 6. fast 再经过了 2 步到达了链表尾结点;fast 正好指向了第 3 个结点,这显然是符合我们的猜想的。 161 | 162 | 在心中默走了一遍代码后,我们显然很容易写出下面的代码。 163 | 164 | ```java 165 | public class Test15 { 166 | public static class LinkNode { 167 | int data; 168 | LinkNode next; 169 | 170 | public LinkNode(int data) { 171 | this.data = data; 172 | } 173 | } 174 | 175 | private static int getSpecifiedNodeReverse(LinkNode head, int k) { 176 | LinkNode slow = head; 177 | LinkNode fast = head; 178 | if (fast == null) { 179 | throw new RuntimeException("your linkNode is null"); 180 | } 181 | // 先让 fast 先走 k-1 步 182 | for (int i = 0; i < k - 1; i++) { 183 | if (fast.next == null) { 184 | // 说明输入的 k 已经超过了链表长度,直接报错 185 | throw new RuntimeException("the value k is too large."); 186 | } 187 | fast = fast.next; 188 | 189 | } 190 | while (fast.next != null) { 191 | slow = slow.next; 192 | fast = fast.next; 193 | } 194 | return slow.data; 195 | } 196 | 197 | public static void main(String[] args) { 198 | LinkNode head = new LinkNode(1); 199 | head.next = new LinkNode(2); 200 | head.next.next = new LinkNode(3); 201 | head.next.next.next = new LinkNode(4); 202 | head.next.next.next.next = new LinkNode(5); 203 | System.out.println(getSpecifiedNodeReverse(head, 3)); 204 | System.out.println(getSpecifiedNodeReverse(null, 1)); 205 | } 206 | } 207 | ``` 208 | 209 | #### 总结 210 | 211 | 链表类面试题,真是可以玩出五花八门,当我们用一个变量遍历链表不能解决问题的时候,我们可以尝试用两个变量来遍历链表,可以让其中一个变量遍历的速度快一些,比如一次走两步,或者是走若干步。我们在遇到这类面试的时候,千万不要自乱阵脚,学会理性分析问题。 212 | 213 | 原本是想给我的小伙伴说再见了,但唯恐大家还没学到真本事,所以在这里再留一个拓展题。 214 | 215 | > 面试题:给定一个单链表的头结点,删除倒数第 k 个结点。 216 | 217 | 哈哈,和上面的题目仅仅只是把获得它的值变成了删除,不少小伙伴肯定都偷着乐了,但南尘还是先提醒大家,不要太得意忘形哟~ 218 | 219 | 好啦,咱们明天再见啦~ 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /Android/每日一问:LayoutParams 你知道多少?.md: -------------------------------------------------------------------------------- 1 | ## 每日一问:LayoutParams 你知道多少? 2 | 3 | 前面的文章中着重讲解了 View 的测量流程。其中我提到了一句非常重要的话:**View 的测量匡高是由父控件的 `MeasureSpec` 和 View 自身的 `LayoutParams 共同决定的。**我们在前面的 [每日一问:谈谈对 MeasureSpec 的理解](https://www.jianshu.com/p/6cdbb418df46) 把 MeasureSpec 的重点进行了讲解,其实另外一个 LayoutParams 同样是非常非常重要。 4 | 5 | #### 从概念讲起 6 | `LayoutParams`,顾名思义,就是布局参数。而且大多数人对此都是司空见惯,我们 XML 文件里面的每一个 View 都会接触到 `layout_xxx` 这样的属性,这实际上就是对布局参数的描述。大概大家也就清楚了,`layout_` 这样开头的东西都不属于 View,而是控制具体显示在哪里。 7 | 8 | #### LayoutParams 都有哪些初始化方法 9 | 通常来说,我们都会把我们的控件放在 XML 文件中,即使我们有时候需要对屏幕做比较「取巧」的适配,会直接通过 `View.getLayoutParams()` 这样的方法获取 `LayoutParams` 的实例,但我们接触的少并不代表它的初始化方法不重要。 10 | 11 | > 实际上,用代码写出来的 View 加载效率要比在 XML 中加载快上大约 1 倍。只是在如今手机配置都比较高的情况下,我们常常忽略了这种方式。 12 | 13 | 我们来看看 `ViewGroup.LayoutParams` 到底有哪些构造方法。 14 | ```java 15 | public LayoutParams(Context c, AttributeSet attrs) { 16 | TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout); 17 | setBaseAttributes(a, 18 | R.styleable.ViewGroup_Layout_layout_width, 19 | R.styleable.ViewGroup_Layout_layout_height); 20 | a.recycle(); 21 | } 22 | 23 | public LayoutParams(int width, int height) { 24 | this.width = width; 25 | this.height = height; 26 | } 27 | 28 | public LayoutParams(LayoutParams source) { 29 | this.width = source.width; 30 | this.height = source.height; 31 | } 32 | 33 | LayoutParams() { } 34 | ``` 35 | 36 | #### MarginLayoutParams 37 | 除去最后一个放给 `MarginLayoutParams` 做处理的方法外,我们在 `ViewGroup` 中还有 3 个构造方法。他们分别负责给 XML 处理、直接让用户指定宽高、还有类似集合的 `addAll()` 这样的方式的赋值方法。 38 | 39 | 实际上,`ViewGroup` 的子类的 `LayoutParams` 类拥有更多的构造方法,感兴趣的自己翻阅源码查看。在这里我想更加强调一下我上面提到的 `MarginLayoutParams`。 40 | 41 | `MarginLayoutParams` 继承于 `ViewGroup.LayoutParams`。 42 | 43 | ```java 44 | public static class MarginLayoutParams extends ViewGroup.LayoutParams { 45 | @ViewDebug.ExportedProperty(category = "layout") 46 | public int leftMargin; 47 | 48 | @ViewDebug.ExportedProperty(category = "layout") 49 | public int topMargin; 50 | 51 | @ViewDebug.ExportedProperty(category = "layout") 52 | public int rightMargin; 53 | 54 | @ViewDebug.ExportedProperty(category = "layout") 55 | public int bottomMargin; 56 | 57 | @ViewDebug.ExportedProperty(category = "layout") 58 | private int startMargin = DEFAULT_MARGIN_RELATIVE; 59 | 60 | @ViewDebug.ExportedProperty(category = "layout") 61 | private int endMargin = DEFAULT_MARGIN_RELATIVE; 62 | 63 | public MarginLayoutParams(Context c, AttributeSet attrs) { 64 | super(); 65 | TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); 66 | setBaseAttributes(a, 67 | R.styleable.ViewGroup_MarginLayout_layout_width, 68 | R.styleable.ViewGroup_MarginLayout_layout_height); 69 | 70 | int margin = a.getDimensionPixelSize( 71 | com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); 72 | if (margin >= 0) { 73 | leftMargin = margin; 74 | topMargin = margin; 75 | rightMargin= margin; 76 | bottomMargin = margin; 77 | } else { 78 | int horizontalMargin = a.getDimensionPixelSize( 79 | R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1); 80 | // ... something 81 | } 82 | // ... something 83 | } 84 | } 85 | ``` 86 | 一看代码,自然就清楚了,为什么我们以前会发现在 XML 布局里, `layout_margin` 属性的值会覆盖 `layout_marginLeft` 与 `layout_marginRight` 等属性的值。 87 | 88 | >实际上,事实上,绝大部分容器控件都是直接继承 `ViewGroup.MarginLayoutParams` 而非 `ViewGroup.LayoutParams`。所以我们再自定义 `LayoutParams` 的时候记得继承 `ViewGroup.MarginLayoutParams` 。 89 | 90 | #### 在代码里面使用 LayoutParams 91 | 前面介绍了 `LayoutParams` 的几种构造方法,我们下面以 `LinearLayout.LayoutParams` 来看看几种简单的使用方式。 92 | ```kotlin 93 | val textView1 = TextView(this) 94 | textView1.text = "不指定 LayoutParams" 95 | layout.addView(textView1) 96 | 97 | val textView2 = TextView(this) 98 | textView2.text = "手动指定 LayoutParams" 99 | textView2.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT) 100 | layout.addView(textView2) 101 | 102 | val textView3 = TextView(this) 103 | textView3.text = "手动传递 LayoutParams" 104 | textView3.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams(100, 100)) 105 | layout.addView(textView3) 106 | ``` 107 | 我们看看 `addView()` 都做了什么。 108 | ```java 109 | public void addView(View child) { 110 | addView(child, -1); 111 | } 112 | 113 | public void addView(View child, int index) { 114 | if (child == null) { 115 | throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); 116 | } 117 | LayoutParams params = child.getLayoutParams(); 118 | if (params == null) { 119 | params = generateDefaultLayoutParams(); 120 | if (params == null) { 121 | throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); 122 | } 123 | } 124 | addView(child, index, params); 125 | } 126 | 127 | @Override 128 | protected LayoutParams generateDefaultLayoutParams() { 129 | if (mOrientation == HORIZONTAL) { 130 | return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 131 | } else if (mOrientation == VERTICAL) { 132 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 133 | } 134 | return null; 135 | } 136 | 137 | public void addView(View child, int index, LayoutParams params) { 138 | if (DBG) { 139 | System.out.println(this + " addView"); 140 | } 141 | if (child == null) { 142 | throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); 143 | } 144 | requestLayout(); 145 | invalidate(true); 146 | addViewInner(child, index, params, false); 147 | } 148 | 149 | private void addViewInner(View child, int index, LayoutParams params, 150 | boolean preventRequestLayout) { 151 | 152 | // ... 153 | 154 | if (!checkLayoutParams(params)) { 155 | params = generateLayoutParams(params); 156 | } 157 | 158 | // ... 159 | } 160 | 161 | @Override 162 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 163 | return p instanceof LinearLayout.LayoutParams; 164 | } 165 | ``` 166 | 看起来 `ViewGroup` 真是煞费苦心,如果我们没有给 View 设置 `LayoutParams`,则系统会帮我们根据 `orientation` 设置默认的 `LayoutParams`。甚至是我们即使在 `addView()` 之前设置了错误的 `LayoutParams` 值,系统也会我们帮我们进行纠正。 167 | >虽然系统已经做的足够完善,帮我们各种矫正错误,但在 `addView()` 之后,我们还强行设置错误的 `LayoutParams`,那还是一定会报 `ClassCastException` 的。 168 | 169 | ![](https://upload-images.jianshu.io/upload_images/3994917-00024485ee8afac5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 170 | 171 | `LayoutParams` 很重要,每一名 Android 开发都应该尽力地去掌握,只有弄清楚了系统的编写方式,应对上面类似简书的流式布局才能更好处理。 172 | > 实际上 Google 出的 [FlexboxLayout](https://github.com/google/flexbox-layout) 已经做的相当完美。 173 | > 当然如果使用的 `RecyclerView`,还可以自己写一个 `FlowLayoutManager` 进行处理。 174 | 175 | 原文较多地参考自:[https://blog.csdn.net/yisizhu/article/details/51582622](https://blog.csdn.net/yisizhu/article/details/51582622) -------------------------------------------------------------------------------- /algorithm/面试 16:栈的压入压出队列.md: -------------------------------------------------------------------------------- 1 | ## 面试 16:栈的压入压出队列 2 | 3 | 我们今天继续来看看周五留下的习题: 4 | 5 | > 面试题:输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如:压入序列为{1,2,3,4,5},那{4,5,3,2,1} 就是该栈的弹出顺序,而{4,3,5,1,2} 明显就不符合要求; 6 | 7 | 这道题还是比较容易想到思路,很直观的想法就是建立一个辅助栈,把输入的第一个序列中的数字依次压入该辅助栈,并按照第二个序列的顺序依次从该栈中弹出数字。 8 | 9 | ## 提前想好测试用例 10 | 11 | 一样是老方法,我们先准备测试用例: 12 | 13 | - 传入两个 null,或者 1 个 null,或者空数组,此时应该不符合要求; 14 | - 传入两个不相等的数组,应该直接不符合要求; 15 | - 分别传入题干的示意值,他们应该分别满足和不满足要求; 16 | - 传入单个数字,选择一组满足要求的和一组不满足要求的; 17 | 18 | ## 思考程序逻辑 19 | 20 | **判断一个序列是不是栈的弹出序列的规律:如果下一个弹出的数字刚好是栈顶数字,那么直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。** 21 | 22 | 然而有的小伙伴还是容易被绕晕,这时候不妨我们可以直接作一个表格来模拟他们的压栈出栈过程,数据就采用我们题设中的数据吧~ 23 | 24 | > 用作图和模拟数据更容易给面试官一个你是个喜欢思考的好同事哟~ 25 | 26 | 首先看看我们正确的数据。 27 | 28 | | 判断操作 | 操作 | 栈 | 弹出数字 | 29 | | :---------------------------------------------- | ---- | ---------- | -------- | 30 | | 栈没数据,push 数组还有数据、压入 | 压入 | 1 | | 31 | | 栈顶是 1,不等于pop[0],push 数组还有数据、压入 | 压入 | 1、2 | | 32 | | 栈顶是 2,不等于pop[0],push 数组还有数据、压入 | 压入 | 1、2、3 | | 33 | | 栈顶是 3,不等于pop[0],push 数组还有数据、压入 | 压入 | 1、2、3、4 | | 34 | | 栈顶是 4,等于pop[0],弹出数字 4 | 弹出 | 1、2、3 | 4 | 35 | | 栈顶是 3,不等于pop[1],push 数组还有数据、压入 | 压入 | 1、2、3、5 | | 36 | | 栈顶是 5,等于pop[1],弹出数字 5 | 弹出 | 1、2、3 | 5 | 37 | | 栈顶是 3,等于pop[2],弹出数字 3 | 弹出 | 1、2 | 3 | 38 | | 栈顶是 2,等于pop[3],弹出数字 2 | 弹出 | 1 | 2 | 39 | | 栈顶是 1,等于pop[4],弹出数字 1 | 弹出 | | 1 | 40 | 41 | 实际上我们在草稿纸上并不需要做这么标准的表格,只要能表现意思即可。 42 | 43 | 我们仔细观察可以得知,我们判断压入还是弹出甚至是得出确定结论的标准是,先看当前栈顶元素和弹出的数字是否相等,如果相等则直接弹出;如果不相等则直接看看压入数组中还有没有元素,如果有则直接压入到辅助栈;如果已经没有数据则代表第二个序列不是第一个序列的弹出栈。 44 | 45 | ## 编写程序代码 46 | 47 | 实际上我们心中已经大概知道怎么写了。 48 | 49 | ```java 50 | private static boolean isPushStack(int[] push, int[] pop) { 51 | if (push == null || pop == null || pop.length != push.length) 52 | return false; 53 | Stack stack = new Stack<>(); 54 | int j = 0; 55 | for (int i = 0; i < pop.length; i++) { 56 | // 第一步判断栈顶元素是否和 pop[i] 相等 57 | if (!stack.isEmpty() && pop[i] == stack.peek()) { 58 | // 如果相等则直接 pop() 59 | stack.pop(); 60 | } else { 61 | // 栈顶和 pop[i] 不相等,则判断 push 数组还有没有数据 62 | // 如果 push 数组没数据了,栈顶元素又不等于 pop[i],则说明不符合要求 63 | if (j == push.length) 64 | return false; 65 | while (j < push.length) { 66 | // 如果还有数据,则直接 push 67 | stack.push(push[j]); 68 | ++j; 69 | // push 后继续判断栈顶元素是否和 pop[i] 相等; 70 | if (pop[i] == stack.peek()) { 71 | // 如果相等则弹出栈,并且推出内层循环 72 | stack.pop(); 73 | break; 74 | } 75 | } 76 | } 77 | } 78 | return true; 79 | } 80 | ``` 81 | 82 | ## 验证测试用例 83 | 84 | 写毕代码后,我们得用自己事先准备的测试用例测试一下。 85 | 86 | - 测试 1 和测试 2 我们已经考虑到了,这样的情况直接在功能之前就判断,不符合条件的直接返回 false,测试通过。 87 | 88 | - 传入{1,2,3,4,5} 和 {4,5,3,2,1}: 89 | 90 | 1. 进入循环,i = 0,pop[i] = 4,直接进入 else 语句,开始 push 数据,一直 push 到 j = 3。 91 | 2. 此时栈内元素为 {1,2,3,4},push 里面还剩下 {5}。因为 pop[0] 等于栈顶,所以进入 if 语句,弹出 4,退出 while 循环; 92 | 3. i = 1,栈内元素为{1,2,3},栈顶元素不等于 pop[1] = 5,进入 else 语句。push 数组还有数据,直接 push,结束后 j = 5,栈内元素为 {1,2,3,5},栈顶刚好等于 pop[1],故弹出数字 5,退出 while 循环; 93 | 4. i = 2,栈内元素为{1,2,3},栈顶元素刚刚等于 pop[2] ,弹出数字 3; 94 | 5. i = 3,栈内元素为 {1,2},栈顶元素刚等于 pop[3],弹出数字 2; 95 | 6. i = 4,栈内元素为 {1},栈顶元素刚刚等于 pop[4],弹出数字 1; 96 | 7. for 循环能直接执行结束,返回 ture,测试通过。 97 | 98 | - 传入 {1,2,3,4,5} 和 {4,3,5,1,2}: 99 | 100 | 1. 进入循环,i = 0,pop[i] = 4,由于同上,所以直接进入到上面的步骤 3; 101 | 2. 此时 i = 1,栈内元素为 {1,2,3},因为栈顶元素等于 pop[1],弹出数字 3; 102 | 3. i = 2,栈内元素为 {1,2},栈顶元素不等于 pop[2],进入 else 语句,此时 push 数组还有元素 {5},所以进入 while 循环。push 后栈内元素为 {1,2,5},栈顶元素等于 pop[2],所以弹出数字 5,退出 while 循环; 103 | 4. i = 3,栈内元素为{1,2},pop[3] = 1,和栈顶元素不相等,所以进入 else 语句,由于 push 里面已经没有了元素,所以直接返回 false,测试通过。 104 | 105 | - 传入 {1} 和 {2}: 106 | 107 | 进入循环,i = 0,pop[i] = 2,进入 else 语句,不相等,直接进入 while 循环,push 后栈内元素为 {1},栈顶元素和 pop[i] 不相等,此时 j = 1,不符合 while 循环条件。循环结束,外循环也结束,返回 true。**测试不通过**。 108 | 109 | ## 修复程序逻辑 110 | 111 | 所以我们现在应该着重处理一下单个数字的情况,分析后明显可以得到,我们要判断这种情况只需要再判断结束 for 循环后栈内是否还有元素和 push 里面还是否有元素即可。 112 | 113 | 所以在最后增加一个条件判断即可。 114 | 115 | ```java 116 | public class Test16 { 117 | 118 | 119 | private static boolean isPushStack(int[] push, int[] pop) { 120 | if (push == null || pop == null || pop.length != push.length) 121 | return false; 122 | Stack stack = new Stack<>(); 123 | int j = 0; 124 | for (int i = 0; i < pop.length; i++) { 125 | // 第一步判断栈顶元素是否和 pop[i] 相等 126 | if (!stack.isEmpty() && pop[i] == stack.peek()) { 127 | // 如果相等则直接 pop() 128 | stack.pop(); 129 | } else { 130 | // 栈顶和 pop[i] 不相等,则判断 push 数组还有没有数据 131 | // 如果 push 数组没数据了,栈顶元素又不等于 pop[i],则说明不符合要求 132 | if (j == push.length) 133 | return false; 134 | while (j < push.length) { 135 | // 如果还有数据,则直接 push 136 | stack.push(push[j]); 137 | ++j; 138 | // push 后继续判断栈顶元素是否和 pop[i] 相等; 139 | if (pop[i] == stack.peek()) { 140 | // 如果相等则弹出栈,并且推出内层循环 141 | stack.pop(); 142 | break; 143 | } 144 | } 145 | } 146 | } 147 | // 增加判断 148 | if (!stack.isEmpty() && j == push.length) 149 | return false; 150 | return true; 151 | } 152 | 153 | public static void main(String[] args) { 154 | int[] push = {1, 2, 3, 4, 5}; 155 | int[] pop1 = {4, 5, 3, 2, 1}; 156 | int[] pop2 = {3, 5, 4, 2, 1}; 157 | int[] pop3 = {4, 3, 5, 1, 2}; 158 | int[] pop4 = {3, 5, 4, 1, 2}; 159 | System.out.println(isPushStack(push, pop1)); 160 | System.out.println(isPushStack(push, pop2)); 161 | System.out.println(isPushStack(push, pop3)); 162 | System.out.println(isPushStack(push, pop4)); 163 | int[] push1 = {1}; 164 | int[] pop5 = {2}; 165 | System.out.println(isPushStack(push1, pop5)); 166 | int[] push2 = {1}; 167 | int[] pop6 = {1}; 168 | System.out.println(isPushStack(push2, pop6)); 169 | } 170 | } 171 | ``` 172 | 173 | 上面在代码逻辑上并没有做多少操作,所以我们只需要再传入 {1} 和 {1} 测试就可以了。 174 | 175 | 直接进入到 while 循环,push 后栈内元素为 {1},因为栈顶元素刚刚等于 pop[0],所以推出数字 1。此后栈内无元素,所以直接返回 true。 176 | 177 | ## 总结 178 | 179 | 我亲爱的小伙伴想必也一定在上面的分析中收获到东西了吧,这也是南尘给大家的箴言。 180 | 181 | - 在思路不是很清晰的时候画表或者画图来处理; 182 | - 在验证测试用例的时候,一定从简单的开始,比如上面,我们其实更加建议先验证单个数字的情况。 183 | 184 | ## 拓展延伸 185 | 186 | 本次学习的方法将非常有效,不信大家可以试试下明天的拓展题。 187 | 188 | > 面试题:从上到下打印二叉树的每个结点,同一层按照从左到右的顺序打印。例如数的结构如下: 189 | > 190 | > ​ 1 191 | > 2 3 192 | > 4 5 6 7 193 | > 194 | > 则依次打印 1、2、3、4、5、6、7 195 | 196 | -------------------------------------------------------------------------------- /algorithm/面试 10:Java 玩转选择排序和插入排序.md: -------------------------------------------------------------------------------- 1 | ## 面试 10:Java 玩转选择排序和插入排序 2 | 3 | 昨天给大家讲解了 [Java 玩转冒泡排序](https://mp.weixin.qq.com/s/WFojXY4Ectfc4brNDp3SKg),大家一定觉得并没有什么难度吧,不知道大佬们玩转了吗?不知道大家有没有多加思考,实际上在我们最后的一种思路上,还可以再继续改进。 4 | 5 | 我们先看看昨天最终版本的代码。 6 | 7 | ```java 8 | public class Test09 { 9 | 10 | private static void swap(int[] arr, int i, int j) { 11 | int temp = arr[i]; 12 | arr[i] = arr[j]; 13 | arr[j] = temp; 14 | } 15 | 16 | private static void printArr(int[] arr) { 17 | for (int anArr : arr) { 18 | System.out.print(anArr + " "); 19 | } 20 | } 21 | 22 | private static void bubbleSort(int[] arr) { 23 | if (arr == null) 24 | return; 25 | // 定义一个标记 isSort,当其值为 true 的时候代表已经有序。 26 | boolean isSort; 27 | for (int i = 0; i < arr.length - 1; i++) { 28 | isSort = true; 29 | for (int j = 1; j < arr.length - i; j++) { 30 | if (arr[j - 1] > arr[j]) { 31 | swap(arr, j - 1, j); 32 | isSort = false; 33 | } 34 | } 35 | if (isSort) 36 | break; 37 | } 38 | } 39 | 40 | public static void main(String[] args) { 41 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 42 | bubbleSort(arr); 43 | printArr(arr); 44 | } 45 | } 46 | ``` 47 | 48 | 我们用一个 boolean 变量 `isSort` 来判断是否已经排序完成,当一整趟遍历都没有发生数据交换的时候,说明已经排序完成,直接 break 退出循环即可。 49 | 50 | 我们试想一下这样的场景:**假设有 100 个数字的数组,仅仅前 10 个无序,后面 90 个均有序并且都大于前面 10 个数字。** 51 | 52 | 我们采用上面的终极算法可以明显看到,第一趟排序后,最后发生交换的位置必定大于 10,且这个位置之后的数据必定已经有序了,但我们还是会去做徒劳的 90 次遍历,而且我们还要遍历 10 次! 53 | 54 | 显然我们可以找到这样的思路,**在第一次排序后,就记住最后发生交换的位置,第二次只要从数组头部遍历到这个位置就 OK 了。** 55 | 56 | 我们不妨直接看看代码实现: 57 | 58 | ```java 59 | public class Test09 { 60 | 61 | private static void swap(int[] arr, int i, int j) { 62 | int temp = arr[i]; 63 | arr[i] = arr[j]; 64 | arr[j] = temp; 65 | } 66 | 67 | private static void printArr(int[] arr) { 68 | for (int anArr : arr) { 69 | System.out.print(anArr + " "); 70 | } 71 | } 72 | 73 | private static void bubbleSort(int[] arr) { 74 | if (arr == null) 75 | return; 76 | int flag = arr.length; 77 | int k; 78 | for (int i = 0; i < arr.length - 1; i++) { 79 | k = flag; 80 | flag = 0; 81 | for (int j = 1; j < k; j++) { 82 | if (arr[j - 1] > arr[j]) { 83 | swap(arr, j - 1, j); 84 | flag = j; 85 | } 86 | } 87 | if (flag == 0) 88 | break; 89 | } 90 | } 91 | 92 | public static void main(String[] args) { 93 | int[] arr = {6, 4, 1, 2, 3, 5, 7, 8, 9}; 94 | bubbleSort(arr); 95 | printArr(arr); 96 | } 97 | } 98 | ``` 99 | 100 | 其实算法也就那么一回事儿,用心去理解它的原理,理解后,无论是用哪种语言实现起来都是非常简单的。那我们今天就来看看另外两种排序,选择排序和插入排序。 101 | 102 | ## 选择排序 103 | 104 | **选择排序**(Selection sort)是一种简单直观的排序算法。选择排序之所以叫选择排序就是在一次遍历过程中找到最小元素的角标位置,然后把它放到数组的首端。我们排序过程都是在寻找剩余数组中的最小元素,所以就叫做选择排序。 105 | 106 | 它的思想如下: 107 | 108 | 1. 从待排序序列中,找到关键字最小的元素;起始假定第一个元素为最小 109 | 2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换; 110 | 3. 从余下的 N - 1 个元素中,找出关键字最小的元素,重复1,2步,直到排序结束。 111 | 112 | ![图片来源于网络](https://user-gold-cdn.xitu.io/2018/3/1/161e0ae4c72cb1b0?imageslim) 113 | 114 | 选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n 个元素的表进行排序总共进行至多 n - 1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 115 | 116 | 我们来看看用 Java 是怎么实现的。 117 | 118 | ```java 119 | public class Test09 { 120 | 121 | private static void swap(int[] arr, int i, int j) { 122 | int temp = arr[i]; 123 | arr[i] = arr[j]; 124 | arr[j] = temp; 125 | } 126 | 127 | private static void printArr(int[] arr) { 128 | for (int anArr : arr) { 129 | System.out.print(anArr + " "); 130 | } 131 | } 132 | 133 | private static void selectSort(int[] arr) { 134 | if (arr == null) 135 | return; 136 | int i, j, min, len = arr.length; 137 | for (i = 0; i < len - 1; i++) { 138 | min = i; // 未排序的序列中最小元素的下标 139 | for (j = i + 1; j < len; j++) { 140 | //在未排序元素中继续寻找最小元素,并保存其下标 141 | if (arr[min] > arr[j]) { 142 | min = j; 143 | } 144 | } 145 | if (min != i) 146 | swap(arr, min, i); 147 | } 148 | } 149 | 150 | public static void main(String[] args) { 151 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 152 | selectSort(arr); 153 | printArr(arr); 154 | } 155 | } 156 | ``` 157 | 158 | 上述 java 代码可以看出我们除了交换元素并未开辟额外的空间,所以额外的空间复杂度为 O(1)。 159 | 160 | 对于时间复杂度而言,选择排序序冒泡排序一样都需要遍历 n(n-1)/2 次,但是相对于冒泡排序来说每次遍历只需要交换一次元素,这对于计算机执行来说有一定的优化。但是选择排序也是名副其实的慢性子,即使是有序数组,也需要进行 n(n-1)/2 次比较,所以其时间复杂度为 O(n²)。 161 | 162 | 即便无论如何也要进行 n(n-1)/2 次比较,选择排序仍是不稳定的排序算法,我们举一个例子如:序列 5 8 5 2 9, 我们知道第一趟选择第 1 个元素 5 会与 2 进行交换,那么原序列中两个 5 的相对先后顺序也就被破坏了。 163 | 164 | **选择排序总结:** 165 | 166 | 1. 选择排序的算法时间平均复杂度为O(n²)。 167 | 2. 选择排序空间复杂度为 O(1)。 168 | 3. 选择排序为不稳定排序。 169 | 170 | ## 插入排序 171 | 172 | 对于插入排序,大部分资料都是使用扑克牌整理作为例子来引入的,我们打牌都是一张一张摸牌的,每摸到一张牌就会跟手里所有的牌比较来选择合适的位置插入这张牌,这也就是直接插入排序的中心思想,我们先来看下动图: 173 | 174 | ![图片来源于网络](https://user-gold-cdn.xitu.io/2018/3/1/161e0ae55a54d8ca?imageslim) 175 | 176 | 相信大家看完动图以后大概知道了插入排序的实现思路了。那么我们就来说下插入排序的思想。 177 | 178 | #### 插入排序的思想 179 | 180 | 1. 从第一个元素开始,该元素可以认为已经被排序 181 | 2. 取出下一个元素,在已经排序的元素序列中从后向前扫描 182 | 3. 如果该元素(已排序)大于新元素,将该元素移到下一位置 183 | 4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置 184 | 5. 将新元素插入到该位置后 185 | 6. 重复步骤 2~5 186 | 187 | 理解上述思想其实并不难,我们来看看用 Java 怎么实现: 188 | 189 | ```java 190 | public class Test09 { 191 | 192 | private static void swap(int[] arr, int i, int j) { 193 | int temp = arr[i]; 194 | arr[i] = arr[j]; 195 | arr[j] = temp; 196 | } 197 | 198 | private static void printArr(int[] arr) { 199 | for (int anArr : arr) { 200 | System.out.print(anArr + " "); 201 | } 202 | } 203 | 204 | private static void insertionSort(int[] arr) { 205 | if (arr == null) 206 | return; 207 | int j; 208 | int temp; 209 | for (int i = 1; i < arr.length; i++) { 210 | // 设置哨兵,拿出待插入的值 211 | temp = arr[i]; 212 | j = i; 213 | // 然后寻找正确插入的位置 214 | while (j > 0 && arr[j - 1] > temp) { 215 | arr[j] = arr[j - 1]; 216 | j--; 217 | } 218 | arr[j] = temp; 219 | } 220 | } 221 | 222 | public static void main(String[] args) { 223 | int[] arr = {6, 4, 2, 1, 8, 3, 7, 9, 5}; 224 | insertionSort(arr); 225 | printArr(arr); 226 | } 227 | } 228 | ``` 229 | 230 | #### 插入排序的时间复杂度和空间复杂度分析 231 | 232 | 对于插入的时间复杂度和空间复杂度,通过代码就可以看出跟选择和冒泡来说没什么区别同属于 O(n²) 级别的时间复杂度算法 ,只是遍历方式有原来的 n n-1 n-2 ... 1,变成了 1 2 3 ... n 了。最终得到时间复杂度都是 n(n-1)/2。 233 | 234 | 对于稳定性来说,插入排序和冒泡一样,并不会改变原有的元素之间的顺序,如果遇见一个与插入元素相等的,那么把待插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序仍是排好序后的顺序,所以插入排序是稳定的。 235 | 236 | 对于插入排序这里说一个非常重要的一点就是:由于这个算法可以提前终止内层比较( arr[j-1] > arr[j])所以这个排序算法很有用!因此对于一些 NlogN 级别的算法,后边的归并和快速都属于这个级别的,算法来说对于 n 小于一定级别的时候(Array.sort 中使用的是47)都可以用插入算法来优化,另外对于近乎有序的数组来说这个提前终止的方式就显得更加又有优势了。 237 | 238 | **插入排序总结:** 239 | 240 | 1. 插入排序的算法时间平均复杂度为O(n²)。 241 | 2. 插入排序空间复杂度为 O(1)。 242 | 3. 插入排序为稳定排序。 243 | 4. 插入排序对于近乎有序的数组来说效率更高,插入排序可用来优化高级排序算法 244 | 245 | 到现在,我们的三种简单排序就告一段落了,下面我们将直接进入 **归并排序** 和 **快速排序** 的讲解。这两个算法也是面试上的常客了,所以你准备好了么? 246 | 247 | 文章参考来源:https://juejin.im/post/5a96d6b15188255efc5f8bbd 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /algorithm/面试 18:复杂链表的复制(剑指 Offer 第 26 题).md: -------------------------------------------------------------------------------- 1 | ## 面试 18:复杂链表的复制(剑指 Offer 第 26 题) 2 | 3 | 在上一篇推文中,我们留下的习题是来自《剑指 Offer》 的面试题 26:复杂链表的复制。 4 | 5 | > 请实现复杂链表的复制,在复杂链表中,每个结点除了 next 指针指向下一个结点外,还有一个 sibling 指向链表中的任意结点或者 NULL。比如下图就是一个含有 5 个结点的复杂链表。 6 | 7 | ![image-20180728122908757](/var/folders/6m/5yg4nys56t1dd5xpwk68cmbw0000gn/T/abnerworks.Typora/image-20180728122908757.png) 8 | 9 | ## 提前想好测试用例 10 | 11 | 依旧是我们熟悉的第一步,先想好我们的测试用例: 12 | 13 | 1. 输入一个 null ,期望什么也不输出; 14 | 2. 输入一个结点,sibling 指向自身,期望打印符合题干的值; 15 | 3. 输入多个结点,部分 sibling 指向 null,期望打印符合题干的值。 16 | 17 | ## 思考程序逻辑 18 | 19 | 测试用例思考完毕,自然是开始思考我们的测试逻辑了,在思考的过程中,我们不妨尝试和面试官进行沟通,这样可以避免我们走不少弯路,而且也容易给面试官留下一个善于思考和沟通的好印象。 20 | 21 | 极易想到的逻辑是,**我们先复制我们传统的单链表,然后再遍历单链表,复制 sibling 的指向。** 22 | 23 | 假设链表中有个结点 A,A 的 sibling 指向结点 B,这个 B 可能在 A 前面也可能在 A 后面,所以我们唯一的办法只有从头结点开始遍历。对于一个含有 n 个结点的链表,由于定位每个结点的 sibling 都需要从链表头结点开始经过 O(n) 步才能找到,因此这种方法的时间复杂度是 O(n²)。 24 | 25 | 当我们告知面试官我们这样的思路的时候,面试官告诉我们,他期待的并不是这样的算法,这样的算法时间复杂度也太高了,希望能有更加简单的方式。 26 | 27 | 得到了面试官的诉求,我们再来看看我们前面的想法时间都花在哪儿去了。 28 | 29 | 很明显,我们上面的想法在定位 sibling 指向上面花了大量的时间,我们可以尝试在这上面进行优化。我们还是分为两步:第一步仍然是先复制原始链表上的每个结点 N 创建 N1,然后把这些创建出来的结点用 next 连接起来。同时我们把 的配对信息放在一个哈希表中。第二步是设置复制链表的 sibling 指向,如果原始链表中有 N 指向 S,那么我们的复制链表中必然存在 N1 指向 S1 。由于有了哈希表,我们可以用 O(1) 的时间,根据 S 找到 S1。 30 | 31 | 这样的方法降低了时间成本,我们高兴地与面试官分享我们的想法,却被面试官指出,这样的想法虽然把时间复杂度降低到了 O(n),但却由于哈希表的存在,需要 O(n) 的空间,而他所期望的方法是不占用任何辅助空间的。 32 | 33 | 接下来我们再换一下思路,不用辅助空间,我们却要用更少的实际解决 sibling 的指向问题。 34 | 35 | 我们前面似乎对于指向都采用过两个指针的方法,这里似乎可以用类似的处理方式处理。 36 | 37 | 我们不妨利用原有链表对每个结点 N 在后面直接在后面创建 N1,这样相当于我们扩长原始链表长度为现有链表的 2 倍,奇数位置的结点连接起来是原始链表,偶数位置的结点连接起来就是我们的复制链表。 38 | 39 | ## 开始编写代码 40 | 41 | 我们先完成第一部分的代码。根据原始链表的每个结点 N ,创建 N1,并把 N 的 next 指向 N1,N1 的 next 指向 N 的 next。 42 | 43 | ```java 44 | private static void cloneNodes(Node head) { 45 | Node node = null; 46 | while (head != null) { 47 | // 先新建结点 48 | node = new Node(head.data); 49 | // 再把head 的 next 指向 node 的 next 50 | node.next = head.next; 51 | // 然后把 node 作为 head 的 next 52 | head.next = node; 53 | // 最后遍历条件 54 | head = node.next; 55 | } 56 | } 57 | ``` 58 | 59 | 上面完成了复制结点,下面我们需要编写 sibling 的指向复制。 60 | 61 | 我们的思想是:当 N 执行 S,那么 N1 就应该指向 S1,即 N.next.sibling = N.sibling.next; 62 | 63 | ```java 64 | private static void connectNodes(Node head) { 65 | while (head != null) { 66 | if (head.sibling != null) { 67 | //如果 当前结点的 sibling 不为 null,那就把它后面的复制结点指向当前sibling指向的下一个结点 68 | head.next.sibling = head.sibling.next; 69 | } 70 | // 遍历 71 | head = head.next.next; 72 | } 73 | } 74 | ``` 75 | 76 | 最后我们只需要拿出原本的链表(奇数)和复制的链表(偶数)即可。 77 | 78 | ```java 79 | private static Node reconnectList(Node head) { 80 | if (head == null) 81 | return null; 82 | // 用于存放复制链表的头结点 83 | Node cloneHead = head.next; 84 | // 用于记录当前处理的结点 85 | Node temp = cloneHead; 86 | // head 的 next 还是要指向原本的 head.next 87 | // 实际上现在由于复制后,应该是 head.next.next,即cloneHead.next 88 | head.next = cloneHead.next; 89 | // 指向新的被复制结点 90 | head = head.next; 91 | while (head != null) { 92 | // temp 代表的是复制结点 93 | // 先进行赋值 94 | temp.next = head.next; 95 | // 赋值结束应该给 next 指向的结点赋值 96 | temp = temp.next; 97 | // head 的下一个结点应该指向被赋值的下一个结点 98 | head.next = temp.next; 99 | head = temp.next; 100 | } 101 | return cloneHead; 102 | } 103 | ``` 104 | 105 | 合并后的最终代码就是: 106 | 107 | ```java 108 | public class Test18 { 109 | 110 | private static class Node { 111 | int data; 112 | Node next; 113 | Node sibling; 114 | 115 | Node(int data) { 116 | this.data = data; 117 | } 118 | } 119 | 120 | private static Node complexListNode(Node head) { 121 | if (head == null) 122 | return null; 123 | // 第一步,复制结点,并用 next 连接 124 | cloneNodes(head); 125 | // 第二步,把 sibling 也复制起来 126 | connectNodes(head); 127 | // 第三步,返回偶数结点,连接起来就是复制的链表 128 | return reconnectList(head); 129 | } 130 | 131 | private static void cloneNodes(Node head) { 132 | Node node = null; 133 | while (head != null) { 134 | // 先新建结点 135 | node = new Node(head.data); 136 | // 再把head 的 next 指向 node 的 next 137 | node.next = head.next; 138 | // 然后把 node 作为 head 的 next 139 | head.next = node; 140 | // 最后遍历条件 141 | head = node.next; 142 | } 143 | } 144 | 145 | private static void connectNodes(Node head) { 146 | while (head != null) { 147 | if (head.sibling != null) { 148 | // 如果 当前结点的 sibling 不为 null,那就把它后面的复制结点指向当前sibling指向的下一个结点 149 | head.next.sibling = head.sibling.next; 150 | } 151 | // 遍历 152 | head = head.next.next; 153 | } 154 | } 155 | 156 | private static Node reconnectList(Node head) { 157 | if (head == null) 158 | return null; 159 | // 用于存放复制链表的头结点 160 | Node cloneHead = head.next; 161 | // 用于记录当前处理的结点 162 | Node cloneNode = cloneHead; 163 | // head 的 next 还是要指向原本的 head.next 164 | // 实际上现在由于复制后,应该是 head.next.next,即cloneHead.next 165 | head.next = cloneHead.next; 166 | // 因为我们第一个结点已经拆分了,所以需要指向新的被复制结点才可以开始循环 167 | head = head.next; 168 | while (head != null) { 169 | // cloneNode 代表的是复制结点 170 | // 先进行赋值 171 | cloneNode.next = head.next; 172 | // 赋值结束应该给 next 指向的结点赋值 173 | cloneNode = cloneNode.next; 174 | // head 的下一个结点应该指向被赋值的下一个结点 175 | head.next = cloneNode.next; 176 | head = cloneNode.next; 177 | } 178 | return cloneHead; 179 | } 180 | 181 | 182 | public static void main(String[] args) { 183 | Node head1 = new Node(1); 184 | Node node2 = new Node(2); 185 | Node node3 = new Node(3); 186 | Node node4 = new Node(4); 187 | Node node5 = new Node(5); 188 | head1.next = node2; 189 | node2.next = node3; 190 | node3.next = node4; 191 | node4.next = node5; 192 | node5.next = null; 193 | head1.sibling = node4; 194 | node2.sibling = null; 195 | node3.sibling = node5; 196 | node4.sibling = node2; 197 | node5.sibling = head1; 198 | 199 | print(head1); 200 | Node root = complexListNode(head1); 201 | System.out.println(); 202 | print(head1); 203 | print(root); 204 | System.out.println(); 205 | System.out.println(isSameLink(head1, root)); 206 | } 207 | 208 | private static boolean isSameLink(Node head, Node root) { 209 | while (head != null && root != null) { 210 | if (head == root) { 211 | head = head.next; 212 | root = root.next; 213 | } else { 214 | return false; 215 | } 216 | } 217 | return head == null && root == null; 218 | } 219 | 220 | private static void print(Node head) { 221 | Node temp = head; 222 | while (head != null) { 223 | System.out.print(head.data + "->"); 224 | head = head.next; 225 | } 226 | System.out.println("null"); 227 | while (temp != null) { 228 | System.out.println(temp.data + "=>" + (temp.sibling == null ? "null" : temp.sibling.data)); 229 | temp = temp.next; 230 | } 231 | } 232 | } 233 | ``` 234 | 235 | ## 验证测试用例 236 | 237 | 写毕代码,我们验证我们的测试用例。 238 | 239 | 1. 输入一个 null ,也不会输出,测试通过; 240 | 2. 输入一个结点,sibling 指向自身,测试通过; 241 | 3. 输入多个结点,部分 sibling 指向 null,测试通过。 242 | 243 | ## 课后习题 244 | 245 | 下一次推文的习题来自于《剑指 Offer》第 29 题:数组中超过一半的数字 246 | 247 | > 面试题:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字并输出。比如 {1,2,3,2,2,2,1} 中 2 的次数是 4,数组长度为 7,所以输出 2。要求不能修改输入的数组。 248 | 249 | -------------------------------------------------------------------------------- /algorithm/面试 12:玩转 Java 快速排序.md: -------------------------------------------------------------------------------- 1 | ## 面试 12:玩转 Java 快速排序 2 | 3 | 终于轮到我们排序算法中的王牌登场了。 4 | 5 | 快速排序由于排序效率在同为 O(nlogn) 的几种排序方法中效率最高,因此经常被采用。再加上快速排序思想——分治法也确实非常实用,所以 **在各大厂的面试习题中,快排总是最耀眼的那个**。要是你会的排序算法中没有快速排序,我想你还是偷偷去学好它,再去向大厂砸简历。 6 | 7 | 事实上,在我们的诸多高级语言中,都能找到它的某种实现版本,那我们 Java 自然不能在此缺席。 8 | 9 | 总的来说,默写排序代码是南尘非常不推荐的,撇开快排的代码不是那么容易默写,即使你能默写快排代码,也总会因为面试官稍微的变种面试导致你惶恐不安。 10 | 11 | 所以我们的面试系列自然不能少了这位王牌选手。 12 | 13 | ![图片来自于维基百科](https://upload.wikimedia.org/wikipedia/commons/6/6a/Sorting_quicksort_anim.gif) 14 | 15 | ## 基本思想 16 | 17 | 快速排序使用分治法策略来把一个序列分为两个子序列,基本步骤为: 18 | 19 | 1. 先从序列中取出一个数作为基准数; 20 | 2. 分区过程:将把这个数大的数全部放到它的右边,小于或者等于它的数全放到它的左边; 21 | 3. 递归地对左右子序列进行不走2,直到各区间只有一个数。 22 | 23 | ![图片来自于网络](https://upload.wikimedia.org/wikipedia/commons/thumb/8/84/Partition_example.svg/200px-Partition_example.svg.png) 24 | 25 | 虽然快排算法的策略是分治法,但分治法这三个字显然无法很好的概括快排的全部不走,因此借用 CSDN 神人 MoreWindows 的定义说明为:**挖坑填数 + 分治法**。 26 | 27 | 似乎还是不太好理解,我们这里就直接借用 MoreWindows 大佬的例子说明。 28 | 29 | 以一个数组作为示例,取区间第一个数为基准数。 30 | 31 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 32 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 33 | | 72 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 48 | 85 | 34 | 35 | 初始时,i = 0; j = 9; temp = a[i] = 72 36 | 37 | 由于已经将 a[0] 中的数保存到 temp 中,可以理解成在数组 a[0] 上挖了个坑,可以将其它数据填充到这来。 38 | 39 | 从 j 开始向前找一个比 temp 小或等于 temp 的数。当 j = 8,符合条件,将 a[8] 挖出再填到上一个坑 a[0] 中。 40 | 41 | a[0] = a[8]; i++; 这样一个坑 a[0] 就被搞定了,但又形成了一个新坑 a[8],这怎么办了?简单,再找数字来填 a[8] 这个坑。这次从i开始向后找一个大于 temp 的数,当 i = 3,符合条件,将 a[3] 挖出再填到上一个坑中 a[8] = a[3]; j--; 42 | 43 | 数组变为: 44 | 45 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 46 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 47 | | 48 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 88 | 85 | 48 | 49 | i = 3; j = 7; temp = 72 50 | 51 | 再重复上面的步骤,**先从后向前找,再从前向后找**。 52 | 53 | 从 j 开始向前找,当 j = 5,符合条件,将 a[5] 挖出填到上一个坑中,a[3] = a[5]; i++; 54 | 55 | 从i开始向后找,当 i = 5 时,由于 i==j 退出。 56 | 57 | 此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将 temp 填入 a[5]。 58 | 59 | 数组变为: 60 | 61 | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 62 | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | 63 | | 48 | 6 | 57 | 42 | 60 | 72 | 83 | 73 | 88 | 85 | 64 | 65 | 可以**看出 a[5] 前面的数字都小于它,a[5] 后面的数字都大于它**。因此再对 a[0…4] 和 a[6…9] 这二个子区间**重复**上述步骤就可以了。 66 | 67 | 对挖坑填数进行总结 68 | 69 | 1.i = L; j = R; 将基准数挖出形成第一个坑 a[i]。 70 | 71 | 2.j-- 由后向前找比它小的数,找到后挖出此数填前一个坑 a[i] 中。 72 | 73 | 3.i++ 由前向后找比它大的数,找到后也挖出此数填到前一个坑 a[j] 中。 74 | 75 | 4.再重复执行 2,3 二步,直到 i==j,将基准数填入 a[i] 中。 76 | 77 | 有了这样的分析,我们明显能写出下面的代码: 78 | 79 | ```java 80 | public class Test09 { 81 | 82 | private static void printArr(int[] arr) { 83 | for (int anArr : arr) { 84 | System.out.print(anArr + " "); 85 | } 86 | } 87 | 88 | private static int partition(int[] arr, int left, int right) { 89 | int temp = arr[left]; 90 | while (right > left) { 91 | // 先判断基准数和后面的数依次比较 92 | while (temp <= arr[right] && left < right) { 93 | --right; 94 | } 95 | // 当基准数大于了 arr[right],则填坑 96 | if (left < right) { 97 | arr[left] = arr[right]; 98 | ++left; 99 | } 100 | // 现在是 arr[right] 需要填坑了 101 | while (temp >= arr[left] && left < right) { 102 | ++left; 103 | } 104 | if (left < right) { 105 | arr[right] = arr[left]; 106 | --right; 107 | } 108 | } 109 | arr[left] = temp; 110 | return left; 111 | } 112 | 113 | private static void quickSort(int[] arr, int left, int right) { 114 | if (arr == null || left >= right || arr.length <= 1) 115 | return; 116 | int mid = partition(arr, left, right); 117 | quickSort(arr, left, mid); 118 | quickSort(arr, mid + 1, right); 119 | } 120 | 121 | 122 | public static void main(String[] args) { 123 | int[] arr = {6, 4, 3, 2, 7, 9, 1, 8, 5}; 124 | quickSort(arr, 0, arr.length - 1); 125 | printArr(arr); 126 | } 127 | } 128 | ``` 129 | 130 | 我们不妨尝试来对这个算法进行一下时间复杂度的分析: 131 | 132 | - 最好情况 133 | 134 | 在最好的情况下,每次我们进行一次分区,我们会把一个序列刚好分为几近相等的两个子序列,这个情况也我们每次递归调用的是时候也就刚好处理一半大小的子序列。这看起来其实就是一个完全二叉树,树的深度为 O(logn),所以我们需要做 O(logn) 次嵌套调用。但是在同一层次结构的两个程序调用中,不会处理为原来数列的相同部分。因此,程序调用的每一层次结构总共全部需要 O(n) 的时间。所以这个算法在最好情况下的时间复杂度为 O(nlogn)。 135 | 136 | 事实上,我们并不需要如此精确的分区:即使我们每个基准值把元素分开为 99% 在一边和 1% 在另一边。调用的深度仍然限制在 100logn,所以全部运行时间依然是 O(nlogn)。 137 | 138 | - 最坏情况 139 | 140 | 事实上,我们总不能保证上面的理想情况。试想一下,假设每次分区后都出现子序列的长度一个为 1 一个为 n-1,那真是糟糕透顶。这一定会导致我们的表达式变成: 141 | 142 | T(n) = O(n) + T(1) + T(n-1) = O(n) + T(n-1) 143 | 144 | 这和插入排序和选择排序的关系式真是如出一辙,所以我们的最坏情况是 O(n²)。 145 | 146 | ## 找到更好的基准数 147 | 148 | 上面对时间复杂度进行了简要分析,可见我们的时间复杂度和我们的基准数的选择密不可分。基准数选好了,把序列每次都能分为几近相等的两份,我们的快排就跟着吃香喝辣;但一旦选择的基准数很差,那我们的快排也就跟着穷困潦倒。 149 | 150 | 所以大家就各显神通,出现了各种选择基准数的方式。 151 | 152 | - 固定基准数 153 | 154 | 上面的那种算法,就是一种固定基准数的方式。如果输入的序列是随机的,处理时间还相对比较能接受。但如果数组已经有序,用上面的方式显然非常不好,因为每次划分都只能使待排序序列长度减一。这真是糟糕透了,快排沦为冒泡排序,时间复杂度为 O(n²)。因此,使用第一个元素作为基准数是非常糟糕的,我们应该立即放弃这种想法。 155 | 156 | - 随机基准数 157 | 158 | 这是一种相对安全的策略。由于基准数的位置是随机的,那么产生的分割也不会总是出现劣质的分割。但在数组所有数字完全相等的时候,仍然会是最坏情况。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到 O(nlogn) 的期望时间复杂度。 159 | 160 | - 三数取中 161 | 162 | 虽然随机基准数方法选取方式减少了出现不好分割的几率,但是最坏情况下还是 O(n²)。为了缓解这个尴尬的气氛,就引入了「三数取中」这样的基准数选取方式。 163 | 164 | ## 三数取中法实现 165 | 166 | 我们不妨来分析一下「三数取中」这个方式。我们最佳的划分是将待排序的序列氛围等长的子序列,最佳的状态我们可以使用序列中间的值,也就是第 n/2 个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约 5% 的比较次数。 167 | 168 | 我们来看看代码是怎么实现的。 169 | 170 | ```java 171 | public class Test09 { 172 | 173 | private static void swap(int[] arr, int i, int j) { 174 | int temp = arr[i]; 175 | arr[i] = arr[j]; 176 | arr[j] = temp; 177 | } 178 | 179 | private static void printArr(int[] arr) { 180 | for (int anArr : arr) { 181 | System.out.print(anArr + " "); 182 | } 183 | } 184 | 185 | private static int partition(int[] arr, int left, int right) { 186 | // 采用三数中值分割法 187 | int mid = left + (right - left) / 2; 188 | // 保证左端较小 189 | if (arr[left] > arr[right]) 190 | swap(arr, left, right); 191 | // 保证中间较小 192 | if (arr[mid] > arr[right]) 193 | swap(arr, mid, right); 194 | // 保证中间最小,左右最大 195 | if (arr[mid] > arr[left]) 196 | swap(arr, left, mid); 197 | int pivot = arr[left]; 198 | while (right > left) { 199 | // 先判断基准数和后面的数依次比较 200 | while (pivot <= arr[right] && left < right) { 201 | --right; 202 | } 203 | // 当基准数大于了 arr[right],则填坑 204 | if (left < right) { 205 | arr[left] = arr[right]; 206 | ++left; 207 | } 208 | // 现在是 arr[right] 需要填坑了 209 | while (pivot >= arr[left] && left < right) { 210 | ++left; 211 | } 212 | if (left < right) { 213 | arr[right] = arr[left]; 214 | --right; 215 | } 216 | } 217 | arr[left] = pivot; 218 | return left; 219 | } 220 | 221 | private static void quickSort(int[] arr, int left, int right) { 222 | if (arr == null || left >= right || arr.length <= 1) 223 | return; 224 | int mid = partition(arr, left, right); 225 | quickSort(arr, left, mid); 226 | quickSort(arr, mid + 1, right); 227 | } 228 | 229 | 230 | public static void main(String[] args) { 231 | int[] arr = {6, 4, 3, 2, 7, 9, 1, 8, 5}; 232 | quickSort(arr, 0, arr.length - 1); 233 | printArr(arr); 234 | } 235 | } 236 | ``` 237 | 238 | 由于篇幅关系,今天我们的讲解暂且就到这里。 239 | 240 | 话说 Java 官方是怎么实现的呢?我们明天不妨直接到 JDK 里面一探究竟。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanchen's blogs 2 | 这里主要是对自己的原创推文进行一个集锦,但没有包括所有,更多博客推文请移步我的博客园(早期推文)和简书以及掘金平台,当然 CSDN 和其他一些网站也有部分推文,感兴趣的可以搜索查看。 3 | 4 | #### 每日一问系列请[点击这里](https://github.com/nanchen2251/Blogs/tree/master/Android): 5 | 6 | ### 算法系列 7 | 1. [用 Java 实现一个 Singleton 模式](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%201%EF%BC%9A%E7%94%A8%20Java%20%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%20Singleton%20%E6%A8%A1%E5%BC%8F.md) 8 | 2. [用 Java 逆序打印链表](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%202%EF%BC%9A%E7%94%A8%20Java%20%E9%80%86%E5%BA%8F%E6%89%93%E5%8D%B0%E9%93%BE%E8%A1%A8.md) 9 | 3. [查找旋转数组的最小数字](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%203%EF%BC%9A%E6%9F%A5%E6%89%BE%E6%97%8B%E8%BD%AC%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%B0%8F%E6%95%B0%E5%AD%97.md) 10 | 4. [避免用递归去解决斐波那契数列](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%204%EF%BC%9A%E9%81%BF%E5%85%8D%E7%94%A8%E9%80%92%E5%BD%92%E5%8E%BB%E8%A7%A3%E5%86%B3%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97.md) 11 | 5. [手写 Java 的 pow() 实现](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%205%EF%BC%9A%E6%89%8B%E5%86%99%20Java%20%E7%9A%84%20pow()%20%E5%AE%9E%E7%8E%B0.md) 12 | 6. [调整数组顺序使奇数位于偶数前面](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%206%EF%BC%9A%E8%B0%83%E6%95%B4%E6%95%B0%E7%BB%84%E9%A1%BA%E5%BA%8F%E4%BD%BF%E5%A5%87%E6%95%B0%E4%BD%8D%E4%BA%8E%E5%81%B6%E6%95%B0%E5%89%8D%E9%9D%A2.md) 13 | 7. [面试常见的链表类算法捷径(一)](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%207%EF%BC%9A%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%A7%81%E7%9A%84%E9%93%BE%E8%A1%A8%E7%B1%BB%E7%AE%97%E6%B3%95%E6%8D%B7%E5%BE%84(%E4%B8%80).md) 14 | 8. [面试常见的链表算法捷径(二)](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%208%EF%BC%9A%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%A7%81%E7%9A%84%E9%93%BE%E8%A1%A8%E7%AE%97%E6%B3%95%E6%8D%B7%E5%BE%84(%E4%BA%8C).md) 15 | 9. [用 Java 实现冒泡排序](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%209%EF%BC%9A%E7%94%A8%20Java%20%E5%AE%9E%E7%8E%B0%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F.md) 16 | 10. [Java 玩转选择排序和插入排序](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2010%EF%BC%9AJava%20%E7%8E%A9%E8%BD%AC%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F%E5%92%8C%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F.md) 17 | 11. [Java 玩转归并排序](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2011%EF%BC%9AJava%20%E7%8E%A9%E8%BD%AC%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F.md) 18 | 12. [玩转 Java 快速排序](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2012%EF%BC%9A%E7%8E%A9%E8%BD%AC%20Java%20%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F.md) 19 | 13. [基于排序算法的总结](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2013%EF%BC%9A%E5%9F%BA%E4%BA%8E%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E7%9A%84%E6%80%BB%E7%BB%93.md) 20 | 14. [合并两个排序链表](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2014%EF%BC%9A%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%8E%92%E5%BA%8F%E9%93%BE%E8%A1%A8.md) 21 | 15. [合并两个排序链表勘误纠正](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2015%EF%BC%9A%E9%92%88%E5%AF%B9%E6%98%A8%E5%A4%A9%E7%9A%84%E6%8E%A8%E6%96%87%EF%BC%8C%E6%9C%89%E5%87%A0%E5%8F%A5%E6%83%B3%E8%AF%B4%E7%9A%84.md) 22 | 16. [顺时针从外往里打印数字](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2015%EF%BC%9A%E9%A1%BA%E6%97%B6%E9%92%88%E4%BB%8E%E5%A4%96%E5%BE%80%E9%87%8C%E6%89%93%E5%8D%B0%E6%95%B0%E5%AD%97.md) 23 | 17. [栈的压入压出队列](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2016%EF%BC%9A%E6%A0%88%E7%9A%84%E5%8E%8B%E5%85%A5%E5%8E%8B%E5%87%BA%E9%98%9F%E5%88%97.md) 24 | 18. [从上到下打印二叉树](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2017%EF%BC%9A%E4%BB%8E%E4%B8%8A%E5%88%B0%E4%B8%8B%E6%89%93%E5%8D%B0%E4%BA%8C%E5%8F%89%E6%A0%91.md) 25 | 19. [复杂链表的复制](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2018%EF%BC%9A%E5%A4%8D%E6%9D%82%E9%93%BE%E8%A1%A8%E7%9A%84%E5%A4%8D%E5%88%B6%EF%BC%88%E5%89%91%E6%8C%87%20Offer%20%E7%AC%AC%2026%20%E9%A2%98%EF%BC%89.md) 26 | 20. [输出数组中出现次数超过一半的数字](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2019%EF%BC%9A%E8%BE%93%E5%87%BA%E6%95%B0%E7%BB%84%E4%B8%AD%E5%87%BA%E7%8E%B0%E6%AC%A1%E6%95%B0%E8%B6%85%E8%BF%87%E4%B8%80%E5%8D%8A%E7%9A%84%E6%95%B0%E5%AD%97%EF%BC%88%E5%89%91%E6%8C%87%20Offer%2026%20%E9%A2%98%EF%BC%89.md) 27 | 21. [计算连续子数组的最大和](https://github.com/nanchen2251/Blogs/blob/master/algorithm/%E9%9D%A2%E8%AF%95%2020%EF%BC%9A%E8%AE%A1%E7%AE%97%E8%BF%9E%E7%BB%AD%E5%AD%90%E6%95%B0%E7%BB%84%E7%9A%84%E6%9C%80%E5%A4%A7%E5%92%8C%EF%BC%88%E5%89%91%E6%8C%87%20Offer%2031%20%E9%A2%98%EF%BC%89.md) 28 | 29 | ### 感悟心得系列 30 | 1. [给扔物线朱凯 HenCoder Plus 的一次分享](https://github.com/nanchen2251/Blogs/blob/master/experience/%E6%A8%A1%E6%8B%9F%E9%9D%A2%E8%AF%95%E5%88%86%E4%BA%AB.md) 31 | 2. [说说过去一周的面试和想法](https://github.com/nanchen2251/Blogs/blob/master/experience/%E8%AF%B4%E8%AF%B4%E8%BF%87%E5%8E%BB%E4%B8%80%E5%91%A8%E7%9A%84%E9%9D%A2%E8%AF%95%E5%92%8C%E6%83%B3%E6%B3%95.md) 32 | 3. [说说入职咕咚两日的感受](https://github.com/nanchen2251/Blogs/blob/master/experience/%E8%AF%B4%E8%AF%B4%E5%85%A5%E8%81%8C%E4%B8%A4%E5%A4%A9%E7%9A%84%E6%84%9F%E5%8F%97.md) 33 | 4. [一眼就能望到头的人生,我想想都怕](https://github.com/nanchen2251/Blogs/blob/master/experience/%E4%B8%80%E7%9C%BC%E5%B0%B1%E8%83%BD%E6%9C%9B%E5%88%B0%E5%A4%B4%E7%9A%84%E4%BA%BA%E7%94%9F%EF%BC%8C%E6%88%91%E6%83%B3%E6%83%B3%E9%83%BD%E6%80%95.md) 34 | 5. [南尘的 2018,阅读本文大约需要一整年](https://github.com/nanchen2251/Blogs/blob/master/experience/%E5%8D%97%E5%B0%98%E7%9A%84%202018%EF%BC%8C%E9%98%85%E8%AF%BB%E6%9C%AC%E6%96%87%E5%A4%A7%E7%BA%A6%E9%9C%80%E8%A6%81%E4%B8%80%E6%95%B4%E5%B9%B4.md) 35 | 36 | ### RxJava 37 | 1. [这可能是最好的 RxJava 2.x 入门教程(完结版)](http://www.jianshu.com/p/0cd258eecf60) 38 | 2. [这可能是最好的 RxJava 2.x 入门教程(一)](http://www.jianshu.com/p/a93c79e9f689) 39 | 3. [这可能是最好的 RxJava 2.x 入门教程(二)](http://www.jianshu.com/p/b39afa92807e) 40 | 4. [这可能是最好的 RxJava 2.x 入门教程(三)](http://www.jianshu.com/p/e9c79eacc8e3) 41 | 5. [这可能是最好的 RxJava 2.x 入门教程(四)](http://www.jianshu.com/p/c08bfc58f4b6) 42 | 6. [这可能是最好的 RxJava 2.x 入门教程(五)](http://www.jianshu.com/p/81fac37430dd) 43 | 44 | ### Kotlin 相关系列 45 | 1. [To Better Kotlin](https://github.com/nanchen2251/Blogs/blob/master/kotlin/Better%20Kotlin.md) 46 | 2. [分享一个 Kotlin 学习方式](https://github.com/nanchen2251/Blogs/blob/master/kotlin/%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%AA%20Kotlin%20%E5%AD%A6%E4%B9%A0%E6%96%B9%E5%BC%8F.md) 47 | 48 | ### Other 49 | 1. [Git 如何遗弃已经 Push 的提交](https://github.com/nanchen2251/Blogs/blob/master/others/Git%20%E5%A6%82%E4%BD%95%E9%81%97%E5%BC%83%E5%B7%B2%E7%BB%8F%20Push%20%E7%9A%84%E6%8F%90%E4%BA%A4.md) 50 | 51 | ### 关于作者 52 |    南尘
53 |    四川成都
54 |    [其它开源](https://github.com/nanchen2251/)
55 |    [个人博客](https://nanchen2251.github.io/)
56 |    [简书](http://www.jianshu.com/u/f690947ed5a6)
57 |    [博客园](http://www.cnblogs.com/liushilin/)
58 | [掘金](https://juejin.im/user/593f78bada2f60006738d641)
59 |    欢迎投稿(关注)我的唯一公众号,公众号搜索 nanchen 或者扫描下方二维码:
60 |    ![](https://github.com/nanchen2251/Blogs/blob/master/images/nanchen12.jpg) 61 | 62 | #### 有码走遍天下 无码寸步难行(引自网络) 63 | 64 | > 1024 - 梦想,永不止步! 65 | 爱编程 不爱Bug 66 | 爱加班 不爱黑眼圈 67 | 固执 但不偏执 68 | 疯狂 但不疯癫 69 | 生活里的菜鸟 70 | 工作中的大神 71 | 身怀宝藏,一心憧憬星辰大海 72 | 追求极致,目标始于高山之巅 73 | 一群怀揣好奇,梦想改变世界的孩子 74 | 一群追日逐浪,正在改变世界的极客 75 | 你们用最美的语言,诠释着科技的力量 76 | 你们用极速的创新,引领着时代的变迁 77 | 78 | ------至所有正在努力奋斗的程序猿们!加油!! 79 | ​ 80 | ## Licenses 81 | ``` 82 | Copyright 2019 nanchen(刘世麟) 83 | 84 | Licensed under the Apache License, Version 2.0 (the "License"); 85 | you may not use this file except in compliance with the License. 86 | You may obtain a copy of the License at 87 | 88 | http://www.apache.org/licenses/LICENSE-2.0 89 | 90 | Unless required by applicable law or agreed to in writing, software 91 | distributed under the License is distributed on an "AS IS" BASIS, 92 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 93 | See the License for the specific language governing permissions and 94 | limitations under the License. 95 | ``` 96 | -------------------------------------------------------------------------------- /algorithm/面试 14:合并两个排序链表.md: -------------------------------------------------------------------------------- 1 | ## 面试 14:合并两个排序链表 2 | 3 | 终于又回到了我们的算法习题讲解了。南尘发现最近文章阅读量明显比以前少了不少,就上门请教小伙伴原因。他们都说作为一名 Android 应用开发工程师,实在是在工作中没有接触到算法。做技术这个东西,学习了还是得练,不练过几天一定会忘掉。 4 | 5 | 不过想必大家读南尘的文章也是深有所感,基本都是站在一个极其普通的程序员角度思考的,层次感也不会突如其来。所以,大家还望多多思考呀,算法这个东西是练出来的,不是看出来的。 6 | 7 | 不过呢,不喜欢算法系列推文的小伙伴也大可不必担心,在算法之后的板块就是 Java 基础啦。 8 | 9 | 有句话说的好,**面试造航母,入职拧螺丝。**实际上也是这样,面试官极难通过简单的面试了解到你这个人的能力,而手写算法却是最适合看出一个人写代码的习惯和程序思维的,这也是大公司以及不少小公司慢慢转向算法面试的一个原因吧~ 10 | 11 | 好了,话不多说,我们直接来看今天的面试题。 12 | 13 | > 面试题:输入两个递增排序的单链表,data 域为 int 型值。合并这两个链表,并使新链表中的结点也是按照递增排序的。例如链表 A:1->3->5,链表 B:2->4,那它们合并后就是 1->2->3->4->5 14 | 15 | 看到这样的题,我们一定要学会先在心中想好测试用例,再思考程序逻辑。写完程序逻辑后,再把自己事先想好的测试用例测试通过后再交给面试官。事实上,面试官也是事先准备了测试用例的。 16 | 17 | 而对于测试用例,就一定要注意好之前南尘给大家讲的边界值。只有能通过边界值、错误值的程序,才拥有足够的健壮性。 18 | 19 | 我们回到题干,输入的是两个递增排序的单链表,我们需要合并它,得到的新链表也是递增排序的。在心中不难拥有这样的思路。 20 | 21 | 1. 假设单链表 A:1->3,单链表 B:2->4->5; 22 | 2. 先比较 A、B 链表的头结点,这里 1 < 2,所以把 1 作为新链表的头结点; 23 | 3. 1 从 A 链表脱离,3 成为了 A 链表的头结点; 24 | 4. 再进行第二步的比较,3 > 2,把 B 链表的头结点值 2 接到新链表上,得到 1->2; 25 | 5. 一直执行类似 2~4 的步骤,直到 A 链表脱离完,新链表是 1->2->3 ,B 链表是 4->5,A 链表已经为 null; 26 | 6. 此时直接把 B 链表全部接到新链表上,得到最后结构 1->2->3->4->5; 27 | 7. 我们不得不想到边界值和错误值,假设输入的 A 链表为 null,则新链表直接为 B。B 链表为 null,新链表为 A。 A、B 都为 null,则新链表也为 null。 28 | 29 | 我们既然是重复的步骤,那我们一定首先能想到递归的思路。我们极容易得到下面的代码。 30 | 31 | ```java 32 | public class Test14 { 33 | public static class Node { 34 | int data; 35 | Node next; 36 | 37 | Node(int data) { 38 | this.data = data; 39 | } 40 | } 41 | 42 | private static Node merge(Node head1, Node head2) { 43 | // 如果链表 1 为 null ,新链表直接为 2 链表; 44 | if (head1 == null) 45 | return head2; 46 | // 如果链表 2 为 null,则新链表直接为 1 链表 47 | if (head2 == null) 48 | return head1; 49 | Node head = null; 50 | // 假设链表 1 的头结点值小于等于链表 2;则直接把链表 1 的头结点赋值为新链表,并递归新的 1 链表 51 | if (head1.data <= head2.data) { 52 | head = head1; 53 | head.next = merge(head1.next, head2); 54 | } else { 55 | // 否则,对链表 2 执行同样的操作,并把脱离的值赋值上去 56 | head = head2; 57 | head.next = merge(head1, head2.next); 58 | } 59 | return head; 60 | } 61 | 62 | public static void main(String[] args) { 63 | Node head1 = new Node(1); 64 | head1.next = new Node(3); 65 | head1.next.next = new Node(5); 66 | head1.next.next.next = new Node((7)); 67 | 68 | Node head2 = new Node(2); 69 | head2.next = new Node(4); 70 | head2.next.next = new Node(6); 71 | head2.next.next.next = new Node(8); 72 | 73 | Node head = merge(head1, head2); 74 | while (head != null) { 75 | System.out.print(head.data + "->"); 76 | head = head.next; 77 | } 78 | System.out.print("null"); 79 | } 80 | } 81 | ``` 82 | 83 | 写出了代码后,自然不能忘了用事先想好的测试用例去测试一遍流程,这里测试是没有问题的。 84 | 85 | 事实上,这也是《剑指 Offer》的标准解法。递归解法有个好处,就是比较容易想到,但这应该是建立在建模能力比较强的基础之上。但往往不少人会觉得递归特别绕,容易把人绕晕,而且在空间利用率上一直都表现不好,有的面试官就是不能接受递归的代码,所以本题我们更加推荐的是迭代的方式处理。 86 | 87 | ## 迭代处理 88 | 89 | 回到我们前面的思路中,上面我们用的方式是不断「脱落」,然后把暴露出来的结点当做一个新的头结点来处理,相信不少小伙伴不怎么能接受这个思路。那我们换个更好理解的方式,我们就直接用我们链表常用的指针移动的方式来处理,我们看行不行。 90 | 91 | 所以我们开始直接编写。 92 | 93 | ```java 94 | private static Node merge(Node head1, Node head2) { 95 | if (head1 == null) 96 | return head2; 97 | if (head2 == null) 98 | return head1; 99 | // 上面的不用说,就是处理传入值为 null 的情况 100 | Node head = null; 101 | // 当两个链表都不为空就可以比较大小来确定接哪个 102 | while (head1 != null && head2 != null) { 103 | if (head1.data <= head2.data) { 104 | head = head1; 105 | head1 = head1.next; 106 | } else { 107 | head = head2; 108 | head2 = head2.next; 109 | } 110 | } 111 | // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后 112 | if (head1 != null) 113 | head.next = head1; 114 | // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点 115 | if (head2 != null) 116 | head.next = head2; 117 | return head; 118 | } 119 | ``` 120 | 121 | 同样写完后,拿出我们的测试用例开始测试,首先肯定是功能测试。 122 | 123 | 1. 假定我们的链表 A 为:1->3,链表 B 为: 2->4->5; 124 | 2. 都不为空进入 while 循环,因为 1 < 2,执行 head = head1,所以新链表为:**1->3->null**;head1 = head1.next,指针后移; 125 | 3. 依然满足 whild 循环条件,因为 3 > 2,所以新链表为,head = head2 ;**等等,这里出了问题,我根本没接到前面放的 head1 的后面,所以这样的赋值明显是不对的。** 126 | 127 | 功能测试就出了问题,我们当然得思考如何修改,正常来说,我们希望新链表的 1.next = 2,所以我们肯定不能直接用 head = head2 这样的表达式来进行赋值。 128 | 129 | 我们一定的有个类似 head.next = head2,然后用类似 head = head.next 这样的方式向后遍历才是正确的。所以我们修改一下代码: 130 | 131 | ```java 132 | private static Node merge(Node head1, Node head2) { 133 | if (head1 == null) 134 | return head2; 135 | if (head2 == null) 136 | return head1; 137 | // 上面的不用说,就是处理传入值为 null 的情况 138 | // 为了下面的 head.next,所以我们首先肯定得初始化一个 Node 139 | Node head = new Node(); 140 | // 当两个链表都不为空就可以比较大小来确定接哪个 141 | while (head1 != null && head2 != null) { 142 | if (head1.data <= head2.data) { 143 | head.next = head1; 144 | head1 = head1.next; 145 | } else { 146 | head.next = head2; 147 | head2 = head2.next; 148 | } 149 | head = head.next; 150 | } 151 | // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后 152 | if (head1 != null) 153 | head.next = head1; 154 | // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点 155 | if (head2 != null) 156 | head.next = head2; 157 | return head; 158 | } 159 | ``` 160 | 161 | 再次进行功能测试: 162 | 163 | 1. 假定 A:1->3,B:2->4->5,先初始化 head,最开始肯定两个都不为空的,所以直接比较; 164 | 2. 因为 1 \< 2 , 所以新链表为 **0->1->3->null**,链表 A 指针后移;head = head.next,所以新的 head 值为 1; 165 | 3. 继续循环,因为 3 > 2,所以得到新的 head.next = head2, 即有 1->2; 166 | 4. 以此类推,最终得到 head 一直得到的历史为 1,2,3。此时链表 A 已经遍历完,链表 B 的指针指向 4; 167 | 5. 退出 while 循环,满足 head2 != null,接到 head 后面, head.next = head2,此时的 head2 里面还剩下 4->5->null; 168 | 6. 返回 head。**等等,这里还是有问题,我们一直在让新链表的指针后移,这样返回出来的就是尾结点了。但我们想要的答案必须是头结点才可以遍历;** 169 | 170 | 所以我们必须在 while 循环前面放头结点的值。**需要注意的是,由于我们要用到 head.next,所以我们最后真正的头结点实际上是 head.next。** 171 | 172 | 完整代码如下: 173 | 174 | ```java 175 | public class Test14 { 176 | public static class Node { 177 | int data; 178 | Node next; 179 | 180 | Node(int data) { 181 | this.data = data; 182 | } 183 | 184 | Node() { 185 | } 186 | } 187 | 188 | private static Node merge(Node head1, Node head2) { 189 | if (head1 == null) 190 | return head2; 191 | if (head2 == null) 192 | return head1; 193 | // 上面的不用说,就是处理传入值为 null 的情况 194 | // 为了下面的 head.next,所以我们首先肯定得初始化一个 Node 195 | Node head = new Node(); 196 | Node temp = head; 197 | // 当两个链表都不为空就可以比较大小来确定接哪个 198 | while (head1 != null && head2 != null) { 199 | if (head1.data <= head2.data) { 200 | head.next = head1; 201 | head1 = head1.next; 202 | } else { 203 | head.next = head2; 204 | head2 = head2.next; 205 | } 206 | head = head.next; 207 | } 208 | // 如果第一个链表的元素未处理完毕,则把剩余的链表接到最后一个链表后 209 | if (head1 != null) 210 | head.next = head1; 211 | // 同理,如果第二个链表的元素未处理完毕,就把剩余的链表接到新链表的尾结点 212 | if (head2 != null) 213 | head.next = head2; 214 | return temp.next; 215 | } 216 | 217 | public static void main(String[] args) { 218 | Node head1 = new Node(1); 219 | head1.next = new Node(3); 220 | head1.next.next = new Node(5); 221 | head1.next.next.next = new Node((7)); 222 | 223 | Node head2 = new Node(2); 224 | head2.next = new Node(4); 225 | head2.next.next = new Node(6); 226 | head2.next.next.next = new Node(8); 227 | 228 | Node head = merge(head1, head2); 229 | while (head != null) { 230 | System.out.print(head.data + "->"); 231 | head = head.next; 232 | } 233 | System.out.print("null"); 234 | } 235 | } 236 | ``` 237 | 238 | 写完后,还是得过一遍测试用例。 239 | 240 | 1. 当传入的 head1 或者 head2 为 null 的时候,直接返回另外一条链表,都为 null ,就返回 null; 241 | 2. 当传入 A:1->3,B:2->4->5,上面已经例证,符合条件; 242 | 3. 当传入相等值:A:1->3->5,B:1->2->4,符合条件; 243 | 4. 当传入两个链表均只有一个结点的,A:1,B:2,符合条件。 244 | 245 | 基本还是没毛病,这时候我们就可以交上自己的答卷啦~ 246 | 247 | 基本步骤很清晰:**思考测试用例 => 思考程序逻辑 => 拿测试用例进去验证 => 发现问题直接更改 => 测试用例验证,注意边界值 => 测试通过。** 248 | 249 | 如果保持上面这样的步骤,实在想不明白,在纸上画画思路,相信即使你没有做到 100% 正确,你给面试官的感觉也是足够靠谱的。 250 | 251 | 好啦,今天的面试题就先到这里,不知道大家有没有被南尘绕晕呀,绕晕了没事儿,多体验几次就好了。 252 | 253 | 还是要提醒大家:**千万要自己去练习,看懂不思考,这样的学习效率是极低的!** 254 | 255 | 好啦,明天的习题我们先放一下,别忘了先思考思考,并动手练练~ 256 | 257 | > 题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入: 258 | > {{1,2,3}, 259 | > {4,5,6}, 260 | > {7,8,9}} 261 | > 则依次打印数字为 1、2、3、6、9、8、7、4、5 -------------------------------------------------------------------------------- /algorithm/面试 3:查找旋转数组的最小数字.md: -------------------------------------------------------------------------------- 1 | ### 面试:查找旋转数组的最小数字 2 | 3 | 在算法面试中,面试官总是喜欢围绕链表、排序、二叉树、二分查找来做文章,而大多数人都可以跟着专业的书籍来做到倒背如流。而面试官并不希望招收的是一位记忆功底很好,但不会活学活用的程序员。所以学会数学建模和分析问题,并用合理的算法或数据结构来解决问题相当重要。 4 | 5 | #### 面试题:打印出旋转数组的最小数字 6 | 7 | > 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3,4,5,1,2} 为数组 {1,2,3,4,5} 的一个旋转,该数组的最小值为 1。 8 | 9 | 要想实现这个需求很简单,我们只需要遍历一遍数组,找到最小的值后直接退出循环。代码实现如下: 10 | 11 | ```java 12 | public class Test08 { 13 | 14 | public static int getTheMin(int nums[]) { 15 | if (nums == null || nums.length == 0) { 16 | throw new RuntimeException("input error!"); 17 | } 18 | int result = nums[0]; 19 | for (int i = 0; i < nums.length - 1; i++) { 20 | if (nums[i + 1] < nums[i]) { 21 | result = nums[i + 1]; 22 | break; 23 | } 24 | } 25 | return result; 26 | } 27 | 28 | public static void main(String[] args) { 29 | // 典型输入,单调升序的数组的一个旋转 30 | int[] array1 = {3, 4, 5, 1, 2}; 31 | System.out.println(getTheMin(array1)); 32 | 33 | // 有重复数字,并且重复的数字刚好的最小的数字 34 | int[] array2 = {3, 4, 5, 1, 1, 2}; 35 | System.out.println(getTheMin(array2)); 36 | 37 | // 有重复数字,但重复的数字不是第一个数字和最后一个数字 38 | int[] array3 = {3, 4, 5, 1, 2, 2}; 39 | System.out.println(getTheMin(array3)); 40 | 41 | // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 42 | int[] array4 = {1, 0, 1, 1, 1}; 43 | System.out.println(getTheMin(array4)); 44 | 45 | // 单调升序数组,旋转0个元素,也就是单调升序数组本身 46 | int[] array5 = {1, 2, 3, 4, 5}; 47 | System.out.println(getTheMin(array5)); 48 | 49 | // 数组中只有一个数字 50 | int[] array6 = {2}; 51 | System.out.println(getTheMin(array6)); 52 | 53 | // 数组中数字都相同 54 | int[] array7 = {1, 1, 1, 1, 1, 1, 1}; 55 | System.out.println(getTheMin(array7)); 56 | } 57 | } 58 | ``` 59 | 60 | 打印结果没什么毛病。不过这样的方法显然不是最优的,我们看看有没有办法找出更加优质的方法处理。 61 | 62 | 有序,还要查找? 63 | 64 | 找到这两个关键字,我们不免会想到我们的二分查找法,但不少小伙伴肯定会问,我们这个数组旋转后已经不是一个真正的有序数组了,不过倒像是两个递增的数组组合而成的,我们可以这样思考。 65 | 66 | 我们可以设定两个下标 low 和 high,并设定 mid = (low + high)/2,我们自然就可以找到数组中间的元素 array[mid],如果中间的元素位于前面的递增数组,那么它应该大于或者等于 low 下标对应的元素,此时数组中最小的元素应该位于该元素的后面,我们可以把 low 下标指向该中间元素,这样可以缩小查找的范围。 67 | 68 | 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于 high 下标对应的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们就可以把 high 下标更新到中位数的下标,这样也可以缩小查找的范围,移动之后的 high 下标对应的元素仍然在后面的递增子数组中。 69 | 70 | 不管是更新 low 还是 high,我们的查找范围都会缩小为原来的一半,接下来我们再用更新的下标去重复新一轮的查找。直到最后两个下标相邻,也就是我们的循环结束条件。 71 | 72 | 说了一堆,似乎已经绕的云里雾里了,我们不妨就拿题干中的这个输入来模拟验证一下我们的算法。 73 | 74 | 1. input:{3,4,5,1,2} 75 | 2. 此时 low = 0,high = 4,mid = 2,对应的值分别是:num[low] = 3,num[high] = 2,num[mid] = 5 76 | 3. 由于 num[mid] > num[low],所以 num[mid] 应该是在左边的递增子数组中。 77 | 4. 更新 low = mid = 2,num[low] = 5,mid = (low+high)/2 = 3,num[mid] = 1; 78 | 5. high - low ≠ 1 ,继续更新 79 | 6. 由于 num[mid] < num[high],所以断定 num[mid] = 1 位于右边的自增子数组中; 80 | 7. 更新 high = mid = 3,由于 high - mid = 1,所以结束循环,得到最小值 num[high] = 1; 81 | 82 | 我们再来看看 Java 中如何用代码实现这个思路: 83 | 84 | ```java 85 | public class Test08 { 86 | 87 | public static int getTheMin(int nums[]) { 88 | if (nums == null || nums.length == 0) { 89 | throw new RuntimeException("input error!"); 90 | } 91 | // 如果只有一个元素,直接返回 92 | if (nums.length == 1) 93 | return nums[0]; 94 | int result = nums[0]; 95 | int low = 0, high = nums.length - 1; 96 | int mid; 97 | // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 98 | while (nums[low] >= nums[high]) { 99 | // 确保循环结束条件 100 | if (high - low == 1) { 101 | return nums[high]; 102 | } 103 | // 取中间位置 104 | mid = (low + high) / 2; 105 | // 代表中间元素在左边递增子数组 106 | if (nums[mid] >= nums[low]) { 107 | low = mid; 108 | } else { 109 | high = mid; 110 | } 111 | } 112 | return result; 113 | } 114 | 115 | public static void main(String[] args) { 116 | // 典型输入,单调升序的数组的一个旋转 117 | int[] array1 = {3, 4, 5, 1, 2}; 118 | System.out.println(getTheMin(array1)); 119 | 120 | // 有重复数字,并且重复的数字刚好的最小的数字 121 | int[] array2 = {3, 4, 5, 1, 1, 2}; 122 | System.out.println(getTheMin(array2)); 123 | 124 | // 有重复数字,但重复的数字不是第一个数字和最后一个数字 125 | int[] array3 = {3, 4, 5, 1, 2, 2}; 126 | System.out.println(getTheMin(array3)); 127 | 128 | // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 129 | int[] array4 = {1, 0, 1, 1, 1}; 130 | System.out.println(getTheMin(array4)); 131 | 132 | // 单调升序数组,旋转0个元素,也就是单调升序数组本身 133 | int[] array5 = {1, 2, 3, 4, 5}; 134 | System.out.println(getTheMin(array5)); 135 | 136 | // 数组中只有一个数字 137 | int[] array6 = {2}; 138 | System.out.println(getTheMin(array6)); 139 | 140 | // 数组中数字都相同 141 | int[] array7 = {1, 1, 1, 1, 1, 1, 1}; 142 | System.out.println(getTheMin(array7)); 143 | 144 | // 特殊的不知道如何移动 145 | int[] array8 = {1, 0, 1, 1, 1}; 146 | System.out.println(getTheMin(array8)); 147 | } 148 | } 149 | ``` 150 | 151 | 前面我们提到在旋转数组中,由于是把递增排序数组的前面的若干个数字搬到数组后面,因为第一个数字总是大于或者等于最后一个数字,而还有一种特殊情况是移动了 0 个元素,即数组本身,也是它自己的旋转数组。这种情况本身数组就是有序的了,所以我们只需要返回第一个元素就好了,这也是为什么我先给 result 赋值为 nums[0] 的原因。 152 | 153 | 上述代码就完美了吗?我们通过测试用例并没有达到我们的要求,我们具体看看 array8 这个输入。先模拟计算机运行分析一下: 154 | 155 | 1. low = 0, high = 4, mid = 2, nums[low] = 1, nums[high] = 1,nums[mid] = 1; 156 | 2. 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 157 | 3. 所以更新 high = mid = 2,mid = (low+high)/2 = 1; 158 | 4. nums[low] = 1,nums[mid] = 1,nums[high] = 1; 159 | 5. high - low ≠ 1,继续循环; 160 | 6. 由于 nums[mid] >= nums[low],故认定 nums[mid] = 1 在左边递增子数组中; 161 | 7. 所以更新 high = mid = 1,由于 high - low = 1,故退出循环,得到 result = 1; 162 | 163 | 但我们一眼了然,明显我们的最小值不是 1 ,而是 0 ,所以当 array[low]、array[mid]、array[high] 相等的时候,我们的程序并不知道应该如何移动,按照目前的移动方式就默认 array[mid] 在左边递增子数组了,这显然是不负责任的做法。 164 | 165 | 我们修正一下代码: 166 | 167 | ```java 168 | public class Test08 { 169 | 170 | public static int getTheMin(int nums[]) { 171 | if (nums == null || nums.length == 0) { 172 | throw new RuntimeException("input error!"); 173 | } 174 | // 如果只有一个元素,直接返回 175 | if (nums.length == 1) 176 | return nums[0]; 177 | int result = nums[0]; 178 | int low = 0, high = nums.length - 1; 179 | int mid = low; 180 | // 确保 low 下标对应的值在左边的递增子数组,high 对应的值在右边递增子数组 181 | while (nums[low] >= nums[high]) { 182 | // 确保循环结束条件 183 | if (high - low == 1) { 184 | return nums[high]; 185 | } 186 | // 取中间位置 187 | mid = (low + high) / 2; 188 | // 三值相等的特殊情况,则需要从头到尾查找最小的值 189 | if (nums[mid] == nums[low] && nums[mid] == nums[high]) { 190 | return midInorder(nums, low, high); 191 | } 192 | // 代表中间元素在左边递增子数组 193 | if (nums[mid] >= nums[low]) { 194 | low = mid; 195 | } else { 196 | high = mid; 197 | } 198 | } 199 | return result; 200 | } 201 | 202 | /** 203 | * 查找数组中的最小值 204 | * 205 | * @param nums 数组 206 | * @param start 数组开始位置 207 | * @param end 数组结束位置 208 | * @return 找到的最小的数字 209 | */ 210 | public static int midInorder(int[] nums, int start, int end) { 211 | int result = nums[start]; 212 | for (int i = start + 1; i <= end; i++) { 213 | if (result > nums[i]) 214 | result = nums[i]; 215 | } 216 | return result; 217 | } 218 | 219 | public static void main(String[] args) { 220 | // 典型输入,单调升序的数组的一个旋转 221 | int[] array1 = {3, 4, 5, 1, 2}; 222 | System.out.println(getTheMin(array1)); 223 | 224 | // 有重复数字,并且重复的数字刚好的最小的数字 225 | int[] array2 = {3, 4, 5, 1, 1, 2}; 226 | System.out.println(getTheMin(array2)); 227 | 228 | // 有重复数字,但重复的数字不是第一个数字和最后一个数字 229 | int[] array3 = {3, 4, 5, 1, 2, 2}; 230 | System.out.println(getTheMin(array3)); 231 | 232 | // 有重复的数字,并且重复的数字刚好是第一个数字和最后一个数字 233 | int[] array4 = {1, 0, 1, 1, 1}; 234 | System.out.println(getTheMin(array4)); 235 | 236 | // 单调升序数组,旋转0个元素,也就是单调升序数组本身 237 | int[] array5 = {1, 2, 3, 4, 5}; 238 | System.out.println(getTheMin(array5)); 239 | 240 | // 数组中只有一个数字 241 | int[] array6 = {2}; 242 | System.out.println(getTheMin(array6)); 243 | 244 | // 数组中数字都相同 245 | int[] array7 = {1, 1, 1, 1, 1, 1, 1}; 246 | System.out.println(getTheMin(array7)); 247 | 248 | // 特殊的不知道如何移动 249 | int[] array8 = {1, 0, 1, 1, 1}; 250 | System.out.println(getTheMin(array8)); 251 | 252 | } 253 | } 254 | ``` 255 | 256 | 我们再用完善的测试用例放进去,测试通过。 257 | 258 | #### 总结 259 | 260 | 本题其实考察的点挺多的,实际上就是考察对二分查找的灵活运用,不少小伙伴死记硬背二分查找必须遵从有序,而没有学会这个二分查找的思想,这样会导致只能想到循环查找最小值了。 261 | 262 | 不少小伙伴在面试中表态,Android 原生态基本都封装了常用算法,对面试这些无作用的算法表示抗议,其实这是相当愚蠢的。我们不求死记硬背算法的实现,但求学习到其中巧妙的思想。只有不断地提升自己的思维能力,才能助自己收获更好的职业发展。 263 | 264 | 这也大概是大家一直到处叫大佬,埋怨自己工资总是跟不上别人的一方面原因吧。 -------------------------------------------------------------------------------- /algorithm/面试 13:基于排序算法的总结.md: -------------------------------------------------------------------------------- 1 | ## 面试 13:基于排序算法的总结 2 | 3 | 浑浑噩噩,我们前面已经讲解了冒泡、插入、选择、归并、快排 5 种排序算法,其他的由于时间关系,我们就不一一例举了。 4 | 5 | 说到排序,不得不想到我们 JDK 中自带的 `Arrays.sort()` 和 `Collections.sort()` 方法。这两个方法基本算是 Java 中排序王牌了。在我们常用的方式中,`Arrays.sort()` 接受一个数组型参数,而 `Collections.sort()` 接受一个 List 集合参数。抛开它们可以接受多种类型的参数,我们且看看平时排序用的较多的 `Arrays.sort()` 是如何实现的。 6 | 7 | ```java 8 | /** 9 | * Sorts the specified array into ascending numerical order. 10 | * 11 | *

Implementation note: The sorting algorithm is a Dual-Pivot Quicksort 12 | * by Vladimir Yaroslavskiy, Jon Bentley, and Joshua Bloch. This algorithm 13 | * offers O(n log(n)) performance on many data sets that cause other 14 | * quicksorts to degrade to quadratic performance, and is typically 15 | * faster than traditional (one-pivot) Quicksort implementations. 16 | * 17 | * @param a the array to be sorted 18 | */ 19 | public static void sort(int[] a) { 20 | DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0); 21 | } 22 | ``` 23 | 24 | 可以看到,在 JDK 里面把排序交给 `DualPivotQuicksort` 类进行全全处理,该类使用的是一种叫做 「双轴快速排序」的算法。从注释中我们可以看出,该排序方法默认是升序排序的,并且同样提供 O(nlogn) 的时间复杂度。 25 | 26 | 我们不妨进到 `DualPivotQuicksort.sort()` 查看代码。 27 | 28 | ```java 29 | /** 30 | * Sorts the specified range of the array using the given 31 | * workspace array slice if possible for merging 32 | * 33 | * @param a the array to be sorted 34 | * @param left the index of the first element, inclusive, to be sorted 35 | * @param right the index of the last element, inclusive, to be sorted 36 | * @param work a workspace array (slice) 37 | * @param workBase origin of usable space in work array 38 | * @param workLen usable size of work array 39 | */ 40 | static void sort(int[] a, int left, int right, 41 | int[] work, int workBase, int workLen) { 42 | // Use Quicksort on small arrays 43 | if (right - left < QUICKSORT_THRESHOLD) { 44 | sort(a, left, right, true); 45 | return; 46 | } 47 | 48 | /* 49 | * Index run[i] is the start of i-th run 50 | * (ascending or descending sequence). 51 | */ 52 | int[] run = new int[MAX_RUN_COUNT + 1]; 53 | int count = 0; run[0] = left; 54 | 55 | // Check if the array is nearly sorted 56 | for (int k = left; k < right; run[count] = k) { 57 | if (a[k] < a[k + 1]) { // ascending 58 | while (++k <= right && a[k - 1] <= a[k]); 59 | } else if (a[k] > a[k + 1]) { // descending 60 | while (++k <= right && a[k - 1] >= a[k]); 61 | for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) { 62 | int t = a[lo]; a[lo] = a[hi]; a[hi] = t; 63 | } 64 | } else { // equal 65 | for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) { 66 | if (--m == 0) { 67 | sort(a, left, right, true); 68 | return; 69 | } 70 | } 71 | } 72 | 73 | /* 74 | * The array is not highly structured, 75 | * use Quicksort instead of merge sort. 76 | */ 77 | if (++count == MAX_RUN_COUNT) { 78 | sort(a, left, right, true); 79 | return; 80 | } 81 | } 82 | 83 | // Check special cases 84 | // Implementation note: variable "right" is increased by 1. 85 | if (run[count] == right++) { // The last run contains one element 86 | run[++count] = right; 87 | } else if (count == 1) { // The array is already sorted 88 | return; 89 | } 90 | 91 | // Determine alternation base for merge 92 | byte odd = 0; 93 | for (int n = 1; (n <<= 1) < count; odd ^= 1); 94 | 95 | // Use or create temporary array b for merging 96 | int[] b; // temp array; alternates with a 97 | int ao, bo; // array offsets from 'left' 98 | int blen = right - left; // space needed for b 99 | if (work == null || workLen < blen || workBase + blen > work.length) { 100 | work = new int[blen]; 101 | workBase = 0; 102 | } 103 | if (odd == 0) { 104 | System.arraycopy(a, left, work, workBase, blen); 105 | b = a; 106 | bo = 0; 107 | a = work; 108 | ao = workBase - left; 109 | } else { 110 | b = work; 111 | ao = 0; 112 | bo = workBase - left; 113 | } 114 | 115 | // Merging 116 | for (int last; count > 1; count = last) { 117 | for (int k = (last = 0) + 2; k <= count; k += 2) { 118 | int hi = run[k], mi = run[k - 1]; 119 | for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) { 120 | if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) { 121 | b[i + bo] = a[p++ + ao]; 122 | } else { 123 | b[i + bo] = a[q++ + ao]; 124 | } 125 | } 126 | run[++last] = hi; 127 | } 128 | if ((count & 1) != 0) { 129 | for (int i = right, lo = run[count - 1]; --i >= lo; 130 | b[i + bo] = a[i + ao] 131 | ); 132 | run[++last] = right; 133 | } 134 | int[] t = a; a = b; b = t; 135 | int o = ao; ao = bo; bo = o; 136 | } 137 | } 138 | ``` 139 | 140 | 注意上面的代码,可以看到当我们传入的数组长度小于常量 `QUICKSORT_THRESHOLD` 时,我们直接采用双轴快速排序。从定义中我们可以看到这个常量值为 286。 141 | 142 | ```java 143 | /** 144 | * If the length of an array to be sorted is less than this 145 | * constant, Quicksort is used in preference to merge sort. 146 | */ 147 | private static final int QUICKSORT_THRESHOLD = 286; 148 | ``` 149 | 150 | 对于「双轴快速排序」,受于篇幅关系,这里就不细讲了,感兴趣的可以 Google 一下。 151 | 152 | 在 JDK 的 `sort()` 方法中,实际上还做了一次判断。当传入的数组长度小于常量 `INSERTION_SORT_THRESHOLD` 值时,将直接采用插入排序。 153 | 154 | ```java 155 | /** 156 | * Sorts the specified range of the array by Dual-Pivot Quicksort. 157 | * 158 | * @param a the array to be sorted 159 | * @param left the index of the first element, inclusive, to be sorted 160 | * @param right the index of the last element, inclusive, to be sorted 161 | * @param leftmost indicates if this part is the leftmost in the range 162 | */ 163 | private static void sort(int[] a, int left, int right, boolean leftmost) { 164 | int length = right - left + 1; 165 | 166 | // Use insertion sort on tiny arrays 167 | if (length < INSERTION_SORT_THRESHOLD) { 168 | if (leftmost) { 169 | /* 170 | * Traditional (without sentinel) insertion sort, 171 | * optimized for server VM, is used in case of 172 | * the leftmost part. 173 | */ 174 | for (int i = left, j = i; i < right; j = ++i) { 175 | int ai = a[i + 1]; 176 | while (ai < a[j]) { 177 | a[j + 1] = a[j]; 178 | if (j-- == left) { 179 | break; 180 | } 181 | } 182 | a[j + 1] = ai; 183 | } 184 | } else { 185 | /* 186 | * Skip the longest ascending sequence. 187 | */ 188 | do { 189 | if (left >= right) { 190 | return; 191 | } 192 | } while (a[++left] >= a[left - 1]); 193 | 194 | /* 195 | * Every element from adjoining part plays the role 196 | * of sentinel, therefore this allows us to avoid the 197 | * left range check on each iteration. Moreover, we use 198 | * the more optimized algorithm, so called pair insertion 199 | * sort, which is faster (in the context of Quicksort) 200 | * than traditional implementation of insertion sort. 201 | */ 202 | for (int k = left; ++left <= right; k = ++left) { 203 | int a1 = a[k], a2 = a[left]; 204 | 205 | if (a1 < a2) { 206 | a2 = a1; a1 = a[left]; 207 | } 208 | while (a1 < a[--k]) { 209 | a[k + 2] = a[k]; 210 | } 211 | a[++k + 1] = a1; 212 | 213 | while (a2 < a[--k]) { 214 | a[k + 1] = a[k]; 215 | } 216 | a[k + 1] = a2; 217 | } 218 | int last = a[right]; 219 | 220 | while (last < a[--right]) { 221 | a[right + 1] = a[right]; 222 | } 223 | a[right + 1] = last; 224 | } 225 | return; 226 | } 227 | // 源码太多,在此省略 228 | } 229 | ``` 230 | 231 | 由于插入排序并不需要遍历整个数组,整个排序算法的核心性能消耗是移位,所以在数组长度较小的时候,插入排序的效果是非常好的。 232 | 233 | 可见我们的 JDK 排序利用了多个排序算法的各种优良属性,避免出现我们前面讲到的各种糟糕情况。所以我们不得不在此对各种算法进行一定的总结。 234 | 235 | ## 总结 236 | 237 | 相信从前面的文章中大家也有了自己对排序算法的认知,目前并没有十全十美的排序算法,有优点就会有缺点,即使是快速排序法,也只是在整体性能上优越,它也存在排序不稳定、需要大量辅助空间、对少量数据排序无优势等不足。因此我们必须从多个角度剖析一下它们的优劣。 238 | 239 | 为了省时间,我们这里就直接搬运《大话数据结构》中的总结了。 240 | 241 | > 希尔排序和堆排序,在南尘的文章中没有讲,感兴趣的直接 Google。 242 | 243 | ![图片来源于网络](/var/folders/6m/5yg4nys56t1dd5xpwk68cmbw0000gn/T/abnerworks.Typora/image-20180717175945035.png) 244 | 245 | 从算法的简单性来看,我们将 7 种算法分为两类: 246 | 247 | - 简单算法:冒油、简单选择、直接插入。 248 | - 改进算法:希尔、堆、归并、快速。 249 | 250 | 从平均情况来看,显然最后 3 种改进算法要胜过希尔排序,并远远胜过前 3 种简单算法 。 251 | 252 | 从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑 4 种复杂的改进算法。 253 | 254 | 从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。 255 | 256 | 从这三组时间复杂度的数据对比中,我们可以得出这样一个认识: 257 | 258 | > 堆排序和归并排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化的天才,心情好时表现极佳,碰到较糟糕环境会变得差强人意。但是他们如果都来比赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。 259 | > 260 | > 从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是 O(1),如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。 261 | > 262 | > 从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排序是个好算法。 263 | > 264 | > 从待排序记录的个数上来说,待排序的个数 n 越小,采用简单排序方法越合适。 反之, n 越大,采用改进排序方法越合适。这也就是为什么在 JDK 中对快速排序优化时, 增加了一个阀值 47,低于阀值时换作直接插入排序的原因。 265 | 266 | 好了,本篇文章有点状态迷糊,大家且将就看看,我们下周再见。 -------------------------------------------------------------------------------- /kotlin/Better Kotlin.md: -------------------------------------------------------------------------------- 1 | ## Better Kotlin 2 | 3 | 转眼间使用 Kotlin 已经有两个月了,时间不长,我也算搭上了 Google 宣布 Kotlin 作为官方支持语言的一波末班车。可能大家早已从纯 Java 开发 Android 转为了混合使用开发甚至是 Kotlin 开发,那你转向 Kotlin 的初衷又是什么呢? 4 | 5 | 对于我,很简单,只是因为一句话:「Google 爸爸都推荐的语言,我们没理由不用!」 6 | 7 | Kotlin 有着诸多的特性,比如空指针安全、方法扩展、支持函数式编程、丰富的语法糖等。这些特性使得 Kotlin 的代码比 Java 简洁优雅许多,提高了代码的可读性和可维护性,节省了开发时间,提高了开发效率,但同样作为 Kotlin 使用者的你,我相信你一定也有不少小建议和小技巧,一直想迫不及待地分享给大家。 8 | 9 | **那就给你一个机会,愿你把你的黑科技悄悄留言在本文下方!**截止到明天早上 9 点,点赞最多的找我有小奖励哟~ 10 | 11 | ### 我想给大家的一些小建议 12 | 13 | 这么有趣的活动,那我作为一名两个月的 Kotlin 开发,自然也应该来这个活动凑凑热闹。 14 | 15 | #### 1. 避免使用 IDE 自带的插件转换 Java 代码 16 | 17 | 想必 IDE 里面的插件 "Covert Java File To Kotlin File" 早已被大家熟知,要是不知道的小伙伴,赶紧写个 Java 文件,尝试点击 Android Studio 工具栏的 Code 下面的 "Convert Java File To Kotlin File",看看都有什么小妙用。 18 | 19 | 这也是南尘最开始喜欢使用的方式,没有技术却有一颗装 ✘ 的内心,直接写成 Java 文件,再直接一键转换为 Kotlin。甚至宝宝想告诉你,我 GitHub 上 1k Star 的 [AiYaGilr](https://github.com/nanchen2251/AiYaGirl) 项目的 Kotlin 分支,也是这样而来。但真是踩了不少的坑。 20 | 21 | 这样的方式足够地快,但却会出现很多很多的 ```!!```,这是由于 Kotlin 的 null safety 特性。这是 Kotlin 在 Android 开发中的很牛逼的一大特性,想必不少小伙伴都被此 Android 的 ```NullPointException``` 困扰许久。我们直接转换 Java 文件造成的各种 ```!!``` ,其实也就意味着你可能存在潜在的未处理的 ```KotlinNullPointException```。 22 | 23 | #### 2. 尽量地使用 val 24 | 25 | `val` 是线程安全的,并且不需要担心 null 的问题,我们自然应该尽可能地使用它。 26 | 27 | 比如我们常用的 Android 解析的服务器数据,我们应该为自己的 data class 设置为 `val`,因为它本身就不应该是可写的。 28 | 29 | 当我第一次使用 Kotlin 的时候,我以为` val` 和 `var` 的区别在于` val` 代表不可变,而 `var` 代表是可变的。但事实比这更加微妙:**val 不代表不可变,val 意味着只读。**。这意味着你不允许明确声明为 `val`,它就不能保证它是不可变的。 30 | 31 | 对于普通变量来说,「不可变」和「只读」之间并没什么区别,因为你没办法复写一个 `val` 变量,所以在此时却是是不可变的。但在 class 的成员变量中,「只读」和「不可变」的区别就大了。 32 | 33 | 在 Kotlin 的类中,val 和 var 是用于表示属性是否有 getter/setter: 34 | 35 | - var:同时有 getter 和 setter。 36 | - val:只有 getter。 37 | 38 | 这里是可以通过自定义 getter 函数来返回不同的值: 39 | 40 | ```kotlin 41 | class Person(val birthDay: DateTime) { 42 | val age: Int 43 | get() = yearsBetween(birthDay, DateTime.now()) 44 | } 45 | ``` 46 | 47 | 可以看到,虽然没有方法来设置 age 的值,但会随着当前日期的变化而变化。 48 | 49 | 这种情况下,我建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代: 50 | 51 | ```kotlin 52 | class Person(val birthDay: DateTime) { 53 | fun age(): Int = yearsBetween(birthDay, DateTime.now()) 54 | } 55 | ``` 56 | 57 | 这也是 [Kotlin 代码约定 ](https://link.zhihu.com/?target=https%3A//kotlinlang.org/docs/reference/coding-conventions.html%23functions-vs-properties)中所提到的,当具有下面列举的特点时使用属性,不然更推荐使用函数: 58 | 59 | - 不会抛出异常。 60 | - 具有 O(1) 的复杂度。 61 | - 计算时的消耗很少。 62 | - 同时多次调用有相同的返回值。 63 | 64 | 因此上面提到的,自定义 getter 方法并随着当前时间的不同而返回不同的值违反了最后一条原则。大家也要尽量的避免这种情况。 65 | 66 | #### 3. 你真的应该好好注意一下伴生对象 67 | 68 | 伴生对象通过在类中使用 `companion object` 来创建,用来替代静态成员,类似于 Java 中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。 69 | 70 | 比如下面的这段代码: 71 | 72 | ```kotlin 73 | class CompanionKotlin { 74 | companion object { 75 | val DATA = "CompanionKotlin_DATA" 76 | } 77 | 78 | fun getData(): String = DATA 79 | } 80 | ``` 81 | 82 | 挺简洁地一段代码。但将这段简洁的 Kotlin 代码转换为等同的 Java 代码后,却显的晦涩难懂。 83 | 84 | ```java 85 | public final class CompanionKotlin { 86 | @NotNull 87 | private static final String DATA = "CompanionKotlin_DATA"; 88 | public static final CompanionKotlin.Companion Companion = new CompanionKotlin.Companion((DefaultConstructorMarker)null); 89 | 90 | @NotNull 91 | public final String getData() { 92 | return DATA; 93 | } 94 | // ... 95 | public static final class Companion { 96 | @NotNull 97 | public final String getDATA() { 98 | return CompanionKotlin.DATA; 99 | } 100 | 101 | private Companion() { 102 | } 103 | 104 | // $FF: synthetic method 105 | public Companion(DefaultConstructorMarker $constructor_marker) { 106 | this(); 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | 与 Java 直接读取一个常量不同,Kotlin 访问一个伴生对象的私有常量字段需要经过以下方法: 113 | 114 | - 调用伴生对象的静态方法 115 | - 调用伴生对象的实例方法 116 | - 调用主类的静态方法 117 | - 读取主类中的静态字段 118 | 119 | 为了访问一个常量,而多花费调用4个方法的开销,这样的 Kotlin 代码无疑是低效的。 120 | 121 | 我们可以通过以下解决方法来减少生成的字节码: 122 | 123 | 1. 对于基本类型和字符串,可以使用 `const` 关键字将常量声明为编译时常量。 124 | 2. 对于公共字段,可以使用 `@JvmField` 注解。 125 | 3. 对于其他类型的常量,最好在它们自己的主类对象而不是伴生对象中来存储公共的全局常量。 126 | 127 | #### 4. @JvmStatic、@JvmFiled 和 object 还有这种故事? 128 | 129 | 我们在 Kotlin 中发现了 `object` 这个东西,我以前就一直对这个东西很好奇,不知道这是个什么玩意儿。 130 | 131 | > object ?难道又一个对象? 132 | 133 | 之前有人写过这样的代码,表示很不解,一个接口类型的成员变量,访问外部类的成员变量 name。这不是理所应当的么? 134 | 135 | ```kotlin 136 | interface Runnable { 137 | fun run() 138 | } 139 | 140 | class Test { 141 | private val name: String = "nanchen" 142 | 143 | object impl : Runnable { 144 | override fun run() { 145 | // 这里编译器会报红报错。对 name 146 | println(name) 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | 即使查看 Kotlin 官方文档,也有这样一段描述: 153 | 154 | > Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations. 155 | 156 | 核心意思是:Kotlin 使用 object 代替 Java 匿名内部类实现。 157 | 158 | 很明显,即便如此,这里的访问应该也是合情合理的。从匿名内部类中访问成员变量在 Java 语言中是完全允许的。 159 | 160 | 这个问题很有意思,解答这个我们需要生成 Java 字节码,再反编译成 Java 看看具体生成的代码是什么。 161 | 162 | ```java 163 | public final class Test { 164 | private final String name = "nanchen"; 165 | public static final class impl implements Runnable { 166 | public static final Test.impl INSTANCE; 167 | 168 | public void run() { 169 | } 170 | 171 | static { 172 | Test.impl var0 = new Test.impl(); 173 | INSTANCE = var0; 174 | } 175 | } 176 | } 177 | 178 | public interface Runnable { 179 | void run(); 180 | } 181 | ``` 182 | 183 | 静态内部类!确实,Java 中静态内部类是不允许访问外部类的成员变量的。但,说好的 object 代替的是 Java 的匿名内部类呢?那这里为啥是静态内部类。 184 | 185 | 这里一定要注意,如果你只是这样声明了一个object,Kotlin认为你是需要一个静态内部类。而如果你用一个变量去接收object表达式,Kotlin认为你需要一个匿名内部类对象。 186 | 187 | 因此,这个类应该这样改进: 188 | 189 | ```kotlin 190 | interface Runnable { 191 | fun run() 192 | } 193 | 194 | class Test { 195 | private val name: String = "nanchen" 196 | 197 | private val impl = object : Runnable { 198 | override fun run() { 199 | println(name) 200 | } 201 | } 202 | } 203 | ``` 204 | 205 | 为了避免出现这个问题,谨记一个原则:如果 object 只是声明,它代表一个静态内部类。如果用变量接收 object 表达式,它代表一个匿名内部类对象。 206 | 207 | 讲到这,自然也就知道了 Kotlin 对 object 的三个作用: 208 | 209 | - 简化生成静态内部类 210 | - 生成匿名内部类对象 211 | - 生成单例对象 212 | 213 | 咳咳,说了那么多,到底和 @JvmStatic 和 @JvmField 有啥关系呢? 214 | 215 | 实际上,目前我们大多数的 Android 项目都是 Java 和 Kotlin 混编的,包括我们的项目在内也是如此。所以我们总是免不了 Java 和 Kotlin 互调的情况。我们可能经常会在代码中这样编写: 216 | 217 | ```kotlin 218 | object Test1 { 219 | val NAME = "nanchen" 220 | fun getAge() = 18 221 | } 222 | ``` 223 | 224 | 在 Java 中会调用是这样的: 225 | 226 | ```java 227 | System.out.println("name:"+Test1.INSTANCE.getNAME()+",age:"+Test1.INSTANCE.getAge()); 228 | ``` 229 | 230 | 作为强迫症重度患者的我,自然是无法接受上面这样奇怪的代码。所以我强烈建议大家在 object 和 companion object 中分别为变量和方法增加上 @JvmField 和 @JvmStatic 注解。 231 | 232 | ```kotlin 233 | object Test1 { 234 | @JvmField 235 | val NAME = "nanchen" 236 | @JvmStatic 237 | fun getAge() = 18 238 | } 239 | ``` 240 | 241 | 这样外面 Java 调用起来就好看多了。 242 | 243 | ### 5. by lazy 和 lateinit 相爱相杀 244 | 245 | 在 Android 开发中,我们经常会有不少的成员变量需要在 onCreate() 中对其进行初始化,特别是我们在 XML 中使用的各种控件,而 Kotlin 要求声明成员变量的时候默认需要为它声明一个初始值。这时候就会出现不少的下面这样的代码。 246 | 247 | ```kotlin 248 | private var textView:TextView? = null 249 | ``` 250 | 251 | 迫于压力,我们不能不为这些 View 加上 ? 代表它们可以为空,然后为它们赋值为 null。实际上,我们在使用中一点都不希望它们为空。这样造成的后果就是,我们每次要使用它的时候都必须去先判断它不为空。这样无用的代码,无疑是浪费了我们的工作时间。 252 | 253 | 好在 Kotlin 推出了 lateinit 关键字:延迟加载。这样我们可以先绕过 kotlin 的强制要求,在后面使用的时候,再也不需要先判断它是否为空了。但要注意,访问未初始化的 *lateinit* 属性会导致*UninitializedPropertyAccessException*。 254 | 255 | 并且 *lateinit* 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样: 256 | 257 | ```kotlin 258 | private var mNumber: Int by Delegates.notNull() 259 | ``` 260 | 261 | 当然,我们还可以使用 let 函数来进行上面的这种情况,但无疑都是画蛇添足的。 262 | 263 | 我们前面说了,在一些明知是只读不可写不可变的变量,我们尽可能地用 val 去修饰它。而 lateinit 仅仅能修饰 var 变量,所以 by lazy 懒加载,是时候表演真正的技术了。 264 | 265 | 对于很多不可变的变量,比如上个页面通过 bundle 传递过来的用于该页面请求网络的参数,比如 MVP 架构开发中的 Presenter,我们都应该用 by lazy 关键字去初始化它。 266 | 267 | `lazy()` 委托属性可以用于只读属性的惰性加载,但是在使用 `lazy()` 时经常被忽视的地方就是有一个可选的model参数: 268 | 269 | - LazyThreadSafetyMode.SYNCHRONIZED:初始化属性时会有双重锁检查,保证该值只在一个线程中计算,并且所有线程会得到相同的值。 270 | - LazyThreadSafetyMode.PUBLICATION:多个线程会同时执行,初始化属性的函数会被多次调用,但是只有第一个返回的值被当做委托属性的值。 271 | - LazyThreadSafetyMode.NONE:没有双重锁检查,不应该用在多线程下。 272 | 273 | `lazy()` 默认情况下会指定 `LazyThreadSafetyMode.SYNCHRONIZED`,这可能会造成不必要线程安全的开销,应该根据实际情况,指定合适的model来避免不需要的同步锁。 274 | 275 | ### 6.注意 Kotlin 中的 for 循环 276 | 277 | Kotlin提供了 `downTo`、`step`、`until`、`reversed` 等函数来帮助开发者更简单的使用 For 循环,如果单一的使用这些函数确实是方便简洁又高效,但要是将其中两个结合呢?比如下面这样: 278 | 279 | ```kotlin 280 | class A { 281 | fun loop() { 282 | for (i in 10 downTo 0 step 3) { 283 | println(i) 284 | } 285 | } 286 | } 287 | ``` 288 | 289 | 上面使用了 downTo 和 step 两个关键字,我们看看 Java 是怎样实现的。 290 | 291 | ```java 292 | public final class A { 293 | public final void loop() { 294 | IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3); 295 | int i = var10000.getFirst(); 296 | int var2 = var10000.getLast(); 297 | int var3 = var10000.getStep(); 298 | if (var3 > 0) { 299 | if (i > var2) { 300 | return; 301 | } 302 | } else if (i < var2) { 303 | return; 304 | } 305 | 306 | while(true) { 307 | System.out.println(i); 308 | if (i == var2) { 309 | return; 310 | } 311 | 312 | i += var3; 313 | } 314 | } 315 | } 316 | ``` 317 | 318 | 毫无疑问:`IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3);` 一行代码就创建了两个 `IntProgression` 临时对象,增加了额外的开销。 319 | 320 | ### 7. 注意 Kotlin 的可空和不可空 321 | 322 | 最近闹了一个笑话,在项目中需要写一个上传跳绳数据的功能。于是有了下面的代码。 323 | 324 | ```java 325 | public interface ISkipService { 326 | /** 327 | * 上传用户跳绳数据 328 | */ 329 | @POST("v2/rope/upload_jump_data") 330 | Observable> uploadJumpData(@Field("data") List data); 331 | } 332 | ``` 333 | 334 | 写毕上面的接口,我们再到 ViewModel 中进行网络请求。 335 | 336 | ```java 337 | private List list = new ArrayList<>(); 338 | 339 | public void uploadClick() { 340 | mNavigator.showProgressDialog(); 341 | list.add(bean); 342 | RetrofitManager.create(ISkipService.class) 343 | .uploadJumpData(list) 344 | .compose(RetrofitUtil.schedulersAndGetData()) 345 | .subscribe(new BaseSubscriber() { 346 | @Override 347 | protected void onSuccess(Object data) { 348 | mNavigator.hideProgressDialog(); 349 | mNavigator.uploadDataSuccess(); 350 | // 点击上传成功,删除数据库 351 | deleteDataFromDB(); 352 | } 353 | 354 | @Override 355 | protected void onFail(ErrorBean errorBean) { 356 | super.onFail(errorBean); 357 | mNavigator.hideProgressDialog(); 358 | mNavigator.uploadDataFailed(errorBean.error_description); 359 | } 360 | }); 361 | } 362 | ``` 363 | 364 | 运行其实并没有什么问题。但由于某些原因,当我把上面的 ISkipService 类修改为了 Kotlin 实现,却发生了崩溃,从代码上暂时没看出问题。 365 | 366 | ```kotlin 367 | interface ISkipService { 368 | /** 369 | * 上传用户跳绳数据 370 | */ 371 | @POST("v2/rope/upload_jump_data") 372 | fun uploadJumpData(@Field("data") data: List): Observable> 373 | } 374 | ``` 375 | 376 | 但确实就是崩溃了。仔细一看,发现 Java 编写这个接口的时候,会被认为这个参数 "data" 对应的 "value" 是可以为 null 的,而改为 Kotlin 后,由于 Kotlin 默认不为空的机制,所以需要的参数是一个不可以为 null 的 List 集合。而我们的 ViewModel 中使用的 Java 代码,由于 Java 认为我们的 List 是可以为 null 的,所以导致了类型不匹配的崩溃。 377 | 378 | 找到了原因,解决方案也就很简单,在 Kotlin 接口中允许参数 data 为 null 或者直接在调用点加上 @NotNull 注解即可。 379 | 380 | ### 写在最后 381 | 382 | 真是想继续写呀,但参考了不少的资料,大家如果觉得有意思可以尽情地参见原文。 383 | 384 | > 参考链接: 385 | > https://blog.danlew.net/2017/05/30/mutable-vals-in-kotlin/ 386 | > https://juejin.im/post/5ad18d705188255c5668ddf0 387 | > https://tech.meituan.com/Kotlin_code_inspect.html 388 | 389 | 390 | 391 | 392 | 393 | -------------------------------------------------------------------------------- /Android/Android 从零编写一个带标签的 TagTextView.md: -------------------------------------------------------------------------------- 1 | 最近公司的项目升级到了 9.x,随之而来的就是一大波的更新,其中有个比较明显的改变就是很多板块都出了一个带标签的设计图,如下: 2 | ![](https://upload-images.jianshu.io/upload_images/3994917-5e4b79ae3fc5da00.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 3 | ![](https://upload-images.jianshu.io/upload_images/3994917-28c02d01fb147887.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | ### 怎么实现 6 | 看到这个,大多数小伙伴都能想到这就是一个简单的图文混排,不由得会想到鸿洋大佬的图文并排控件 [MixtureTextView](https://github.com/hongyangAndroid/MixtureTextView),或者自己写一个也不麻烦,只需要利用 shape 背景文件结合 `SpannableString` 即可。 7 | 8 | 确实如此,利用 `SpannableString` 确实是最方便快捷的方式,但稍不注意这里可能会踩坑。 9 | 10 | ```kotlin 11 | private fun convertViewToBitmap(view: View): Bitmap { 12 | view.isDrawingCacheEnabled = true 13 | view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) 14 | view.layout(0, 0, view.measuredWidth, view.measuredHeight) 15 | view.buildDrawingCache() 16 | val bitmap = view.drawingCache 17 | view.isDrawingCacheEnabled = false 18 | view.destroyDrawingCache() 19 | return bitmap 20 | } 21 | 22 | fun setTagText(style: Int, content: String) { 23 | val view = LayoutInflater.from(context).inflate(R.layout.layout_codoon_tag_textview, null) 24 | val tagView = view.findViewById(R.id.tvName) 25 | val tag = when (style) { 26 | STYLE_NONE -> { 27 | "" 28 | } 29 | STYLE_CODOON -> { 30 | tagView.setStrokeColor(R.color.tag_color_codoon.toColorRes()) 31 | tagView.setTextColor(R.color.tag_color_codoon.toColorRes()) 32 | "自营" 33 | } 34 | STYLE_JD -> { 35 | tagView.setStrokeColor(R.color.tag_color_jd.toColorRes()) 36 | tagView.setTextColor(R.color.tag_color_jd.toColorRes()) 37 | "京东" 38 | } 39 | STYLE_TM -> { 40 | tagView.setStrokeColor(R.color.tag_color_tm.toColorRes()) 41 | tagView.setTextColor(R.color.tag_color_tm.toColorRes()) 42 | "天猫" 43 | } 44 | STYLE_PDD -> { 45 | tagView.setStrokeColor(R.color.tag_color_pdd.toColorRes()) 46 | tagView.setTextColor(R.color.tag_color_pdd.toColorRes()) 47 | "拼多多" 48 | } 49 | STYLE_TB -> { 50 | tagView.setStrokeColor(R.color.tag_color_tb.toColorRes()) 51 | tagView.setTextColor(R.color.tag_color_tb.toColorRes()) 52 | "淘宝" 53 | } 54 | else -> { 55 | "" 56 | } 57 | } 58 | val spannableString = SpannableString("$tag$content") 59 | val bitmap = convertViewToBitmap(view) 60 | val drawable = BitmapDrawable(resources, bitmap) 61 | drawable.setBounds(0, 0, tagView.width, tagView.height) 62 | spannableString.setSpan(CenterImageSpan(drawable), 0, tag.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) 63 | text = spannableString 64 | gravity = Gravity.CENTER_VERTICAL 65 | } 66 | 67 | companion object { 68 | const val STYLE_NONE = 0 69 | const val STYLE_JD = 1 70 | const val STYLE_TB = 2 71 | const val STYLE_CODOON = 3 72 | const val STYLE_PDD = 4 73 | const val STYLE_TM = 5 74 | } 75 | ``` 76 | 77 | xml 文件的样式就不必在这里贴了,很简单,就是一个带 shape 背景的 TextView,不过由于 shape 文件的极难维护性,在我们的项目中统一采用的是自定义 View 来实现这些圆角等效果。 78 | >详细参考作者 blog:[Android 项目中 shape 标签的整理和思考](https://xiaozhuanlan.com/topic/3205781694) 79 | 80 | 圆角 shape 等效果不是我们在这里主要讨论的东西,我们来看这个代码,思路也是很清晰简洁:首先利用 `LayoutInflater` 返回一个 `View`,然后对这个 `View` 经过一系列判断逻辑确认里面的显示文案和描边颜色等处理。然后通过 `View` 的 `buildDrawingCache()` 的方法生成一个 Bitmap 供 `SpannableString` 使用,然后再把 `spannableString` 设置给 `textView` 即可。 81 | 82 | ### 一些注意点 83 | 其中有个细节需要注意的是,利用 `LayoutInflater` 生成的 `View` 并没有经过 `measure()` 和 `layout()` 方法的洗礼,所以一定没对它的 `width` 和 `height` 等属性赋值。 84 | 85 | 所以我们在 `buildDrawingCache()` 前做了至关重要的两步操作: 86 | ```kotlin 87 | view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) 88 | view.layout(0, 0, view.measuredWidth, view.measuredHeight) 89 | ``` 90 | 91 | 从 `buildDrawingCache()` 源码中我们可以看到,这个方法并不是一定会返回到正确的 `Bitmap`,在我们的 `View` 的 `CacheSize` 大小超过了某写设备的默认值的时候,可能会返回 null。 92 | > 系统给我了我们的默认最大的 `DrawingCacheSize` 为屏幕宽高乘积的 4 倍。 93 | 94 | 由于我们这里的 View 是极小的,所以暂时没有出现返回 null 的情况。 95 | 96 | 尽管上面的代码经过测试,基本上能在大部分机型上满足需求。但本着被标记 `@Deprecated` 的过时方法,我们坚决不用的思想,我们需要对生成 `Bitmap` 的方法进行小范围改造。 97 | 98 | 在最新的 SDK 中,我们发现 `View` 的 `buildDrawingCache()` 等一系列方法都已经被标记了 `@Deprecated` 。 99 | ```java 100 | /** 101 | *

Calling this method is equivalent to calling buildDrawingCache(false).

102 | * 103 | * @see #buildDrawingCache(boolean) 104 | * 105 | * @deprecated The view drawing cache was largely made obsolete with the introduction of 106 | * hardware-accelerated rendering in API 11. With hardware-acceleration, intermediate cache 107 | * layers are largely unnecessary and can easily result in a net loss in performance due to the 108 | * cost of creating and updating the layer. In the rare cases where caching layers are useful, 109 | * such as for alpha animations, {@link #setLayerType(int, Paint)} handles this with hardware 110 | * rendering. For software-rendered snapshots of a small part of the View hierarchy or 111 | * individual Views it is recommended to create a {@link Canvas} from either a {@link Bitmap} or 112 | * {@link android.graphics.Picture} and call {@link #draw(Canvas)} on the View. However these 113 | * software-rendered usages are discouraged and have compatibility issues with hardware-only 114 | * rendering features such as {@link android.graphics.Bitmap.Config#HARDWARE Config.HARDWARE} 115 | * bitmaps, real-time shadows, and outline clipping. For screenshots of the UI for feedback 116 | * reports or unit testing the {@link PixelCopy} API is recommended. 117 | */ 118 | @Deprecated 119 | public void buildDrawingCache() { 120 | buildDrawingCache(false); 121 | } 122 | ``` 123 | 从官方注释中我们发现,**使用视图渲染已经过时,硬件加速后中间缓存很多程度上都是不必要的,而且很容易导致性能的净损失。** 124 | 125 | 所以我们采用 `Canvas` 进行简单改造一下: 126 | ```kotlin 127 | private fun convertViewToBitmap(view: View): Bitmap? { 128 | view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) 129 | view.layout(0, 0, view.measuredWidth, view.measuredHeight) 130 | val bitmap = Bitmap.createBitmap(view.measuredWidth, view.measuredHeight, Bitmap.Config.ARGB_4444) 131 | val canvas = Canvas(bitmap) 132 | canvas.drawColor(Color.WHITE) 133 | view.draw(canvas) 134 | return bitmap 135 | } 136 | ``` 137 | ### 突如其来的崩溃 138 | perfect,但很不幸,在上 4.x 某手机上测试的时候,发生了一个空指针崩溃。 139 | ![](https://upload-images.jianshu.io/upload_images/3994917-cdf2996bff53f732.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 140 | 一看日志,发现我们在执行 `view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))` 这句代码的时候抛出了系统层源码的 bug。 141 | 142 | 进入源码发现在 `RelativeLayout` 的 `onMeasure()` 中有这样一段代码。 143 | ```java 144 | if (isWrapContentWidth) { 145 | // Width already has left padding in it since it was calculated by looking at 146 | // the right of each child view 147 | width += mPaddingRight; 148 | 149 | if (mLayoutParams != null && mLayoutParams.width >= 0) { 150 | width = Math.max(width, mLayoutParams.width); 151 | } 152 | 153 | width = Math.max(width, getSuggestedMinimumWidth()); 154 | width = resolveSize(width, widthMeasureSpec); 155 | // ... 156 | } 157 | } 158 | ``` 159 | 看起来没有任何问题,但对比 4.3 的源码,发现了一点端倪。 160 | ```java 161 | if (mLayoutParams.width >= 0) { 162 | width = Math.max(width, mLayoutParams.width); 163 | } 164 | ``` 165 | 原来空指针报的是这个 `layoutParams`。 166 | 再看看我们 `inflate()` 的代码。 167 | ```kotlin 168 | val view = LayoutInflater.from(context).inflate(R.layout.layout_codoon_tag_textview, null) 169 | ``` 170 | 对任何一位 Android 开发来讲,都是最熟悉的代码了,意思很简单,从 xml 中实例化 `View` 视图,但是父视图为 null,所以从 xml 文件实例化的 `View` 视图没办法 `attach` 到 `View` 层次树中,所以导致了 `layoutParams` 这个参数为 null。 171 | 既然找到了原因,那么解决方案也就非常简单了。 172 | 只需要在 `inflate()` 后,再设置一下 `params` 就可以了。 173 | ```kotlin 174 | view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) 175 | ``` 176 | 177 | 至此,基本已经实现,主要逻辑代码为: 178 | ```kotlin 179 | /** 180 | * 电商专用的 TagTextView 181 | * 后面可以拓展直接设置颜色和样式的其他风格 182 | * 183 | * Author: nanchen 184 | * Email: liusl@codoon.com 185 | * Date: 2019/5/7 10:43 186 | */ 187 | class CodoonTagTextView @JvmOverloads constructor( 188 | context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 189 | ) : AppCompatTextView(context, attrs, defStyleAttr) { 190 | 191 | private var tagTvSize: Float = 0f 192 | 193 | init { 194 | val array = context.obtainStyledAttributes(attrs, R.styleable.CodoonTagTextView) 195 | val style = array.getInt(R.styleable.CodoonTagTextView_codoon_tag_style, 0) 196 | val content = array.getString(R.styleable.CodoonTagTextView_codoon_tag_content) 197 | tagTvSize = array.getDimension(R.styleable.CodoonTagTextView_codoon_tag_tv_size, 0f) 198 | content?.apply { 199 | setTagText(style, this) 200 | } 201 | array.recycle() 202 | } 203 | 204 | private fun convertViewToBitmap(view: View): Bitmap? { 205 | // view.isDrawingCacheEnabled = true 206 | view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)) 207 | view.layout(0, 0, view.measuredWidth, view.measuredHeight) 208 | // view.buildDrawingCache() 209 | // val bitmap = view.drawingCache 210 | // view.isDrawingCacheEnabled = false 211 | // view.destroyDrawingCache() 212 | val bitmap = Bitmap.createBitmap(view.measuredWidth, view.measuredHeight, Bitmap.Config.ARGB_4444) 213 | val canvas = Canvas(bitmap) 214 | canvas.drawColor(Color.WHITE) 215 | view.draw(canvas) 216 | return bitmap 217 | } 218 | 219 | fun setTagText(style: Int, content: String) { 220 | val view = LayoutInflater.from(context).inflate(R.layout.layout_codoon_tag_textview, null) 221 | view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) 222 | val tagView = view.findViewById(R.id.tvName) 223 | val tag = when (style) { 224 | STYLE_NONE -> { 225 | "" 226 | } 227 | STYLE_CODOON -> { 228 | tagView.setStrokeColor(R.color.tag_color_codoon.toColorRes()) 229 | tagView.setTextColor(R.color.tag_color_codoon.toColorRes()) 230 | "自营" 231 | } 232 | STYLE_JD -> { 233 | tagView.setStrokeColor(R.color.tag_color_jd.toColorRes()) 234 | tagView.setTextColor(R.color.tag_color_jd.toColorRes()) 235 | "京东" 236 | } 237 | STYLE_TM -> { 238 | tagView.setStrokeColor(R.color.tag_color_tm.toColorRes()) 239 | tagView.setTextColor(R.color.tag_color_tm.toColorRes()) 240 | "天猫" 241 | } 242 | STYLE_PDD -> { 243 | tagView.setStrokeColor(R.color.tag_color_pdd.toColorRes()) 244 | tagView.setTextColor(R.color.tag_color_pdd.toColorRes()) 245 | "拼多多" 246 | } 247 | STYLE_TB -> { 248 | tagView.setStrokeColor(R.color.tag_color_tb.toColorRes()) 249 | tagView.setTextColor(R.color.tag_color_tb.toColorRes()) 250 | "淘宝" 251 | } 252 | else -> { 253 | "" 254 | } 255 | } 256 | if (tag.isNotEmpty()) { 257 | tagView.text = tag 258 | if (tagTvSize != 0f) { 259 | tagView.textSize = tagTvSize.toDpF() 260 | } 261 | // if (tagHeight != 0f) { 262 | // val params = tagView.layoutParams 263 | // params.height = tagHeight.toInt() 264 | // tagView.layoutParams = params 265 | // } 266 | } 267 | val spannableString = SpannableString("$tag$content") 268 | val bitmap = convertViewToBitmap(view) 269 | bitmap?.apply { 270 | val drawable = BitmapDrawable(resources, bitmap) 271 | drawable.setBounds(0, 0, tagView.width, tagView.height) 272 | spannableString.setSpan(CenterImageSpan(drawable), 0, tag.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) 273 | } 274 | text = spannableString 275 | gravity = Gravity.CENTER_VERTICAL 276 | } 277 | 278 | companion object { 279 | const val STYLE_NONE = 0 // 不加 280 | const val STYLE_JD = 1 // 京东 281 | const val STYLE_TB = 2 // 淘宝 282 | const val STYLE_CODOON = 3 // 自营 283 | const val STYLE_PDD = 4 // 拼多多 284 | const val STYLE_TM = 5 // 天猫 285 | } 286 | } 287 | 288 | ``` -------------------------------------------------------------------------------- /experience/模拟面试分享.md: -------------------------------------------------------------------------------- 1 | 我先自我介绍一下,我叫刘世麟,在网上呢,我的名字是南尘或者说,是南尘2251。 2 | 3 | 在找工作之前,我们总会经历写简历,准备面试知识,再到面试的过程。但我们总会发现,即使我们准备许久,感觉自我良好。但在面试的时候,却总感觉使出浑身解数也无济于事。实际上,我们的面试是需要实战的,我们刷再多的题,不实战也是无济于事。然而,我们通常面试一次,算上花在路上的时间,我们至少得要好几个小时。最重要的是,这个时间还基本都是花在了工作日上。 4 | 5 | 此外,我们即使花上几个小时,面完之后却会发现,我们能得到的有价值的东西却需要深挖细掘才能形成精华。我们很难得到面试官对自己真正的反馈,大多数情况下被拒绝的时候面试官给的原因都极其委婉。 6 | 7 | 为了解决这样尴尬的窘境,我在去年11月在公众号上推出了「模拟面试」这项活动。用工作之余的时间帮助那些真正渴望希望面试却又不希望浪费工作时间的读者。 8 | 9 | 在为期 4 个多月的活动中,我面试了 118 位读者,其中包括小米、硅谷、美团的一些在职员工。在活动中,我与他们共同成长,一起进步,从自我介绍,到面试过程,再到面试反馈。读者从我这里得到了有价值的评价,我从读者那里得到了非常有价值的数据反馈。 10 | 11 | 我只是抱着一种尝试的态度,希望小范围进行,但没想到有这种情况的读者极多,我采取的排队机制已经完全不能让读者得到及时反馈了,加上我自己的时间分配问题,我很快就停止了这项活动。 12 | 13 | 虽然做的时间不长,但却非常有意义,所以,今天,我和凯哥一拍即合,来到了这里,给大家做一下分享。我并不是什么面霸,也不是什么大佬,仅仅是凯哥的迷弟而已。 14 | 15 | 周三呢,我跟着大家一起听了课,我发现听课的同学不少我认识的哈,还有在我这里进行过模拟面试的人,不得不说呀,做 Android 的人很多,但圈子里面上进的人,转来转去还是这些人。 16 | 17 | 下面,我给大家简单地分享一下我做这个活动后的一些心得。在分享之前呢,我想先送大家一句话:你能否通过面试往往不是靠**技术**,但你能否拿高工资绝对是靠**技术**。我们的求职面试,决定因素确实不是只靠技术,你的简历、面试表现,通通都会影响着你的评级。 18 | 19 | 几个月的「模拟面试」下来,我发现大家的问题五花八门,总结下来就是这四个问题: 20 | 21 | - 不知道怎么写简历 22 | - 不知道怎么做自我介绍 23 | - 不清楚自己的定位(优势) 24 | - 不知道怎么准备面试 25 | 26 | 我先说一下简历呀。虽然我只面了 118 位读者,但收到的简历却远不止这点,对于明显抱着来尝试浪费其他人时间的,我从来都是直接打回去的。但我面试的人中,简历写的足够让我满意的人,也是凤毛麟角。大多数人的简历都是写的非常的乱,连基本的排版整洁都谈不上。 27 | 28 | ### 简历的本质 29 | 30 | 我经常会收到来自读者的同一个问题,「南尘呀,你能不能给我一个好的简历的模板?」 31 | 32 | 这个问题让我十分为难,如果我说「其实不存在好与不好的模板,主要看气质」。你可能会很失望,觉得我可能在浪费你的时间。如果我说有,并且挑了一份我认为还不错的模板给你,可能你也会很失望,会怀疑,好的简历模板是这个样子的吗? 33 | 34 | 这就好比凯哥在上海开发者大会上讲的一样,「图片上传怎么做,你服务器怎么给你要求的你就怎么上传呀」。 35 | 36 | 实际上我们并没有逗你,要得到一个「好」的简历模板,其实并不困难,而且根本就不需要向别人要。**你只需要问一下自己,什么是「好」,并把「好」这个很缥缈的词量化出来,你的简历就是好简历。** 37 | 38 | 不过,在你量化「好」这个词之前,我们需要先解决一个问题:「简历的本质是什么?」 39 | 40 | 可能每个人的理解并不一定相同,不过这并不是很重要。如果你有不同于我的思考,那也不代表你和我之间有一个人是错的,我们暂时可以这么认为:绚丽或者简约的模板其实都不是简历的本质。 41 | 42 | 简历的本质只有一个:**向别人说明清楚你是谁,你擅长做什么。** 43 | 44 | 你可能会发现,其实这两个问题都是同一个问题:**你的定位是怎样的?** 45 | 46 | - 问题 1:「你是谁」? 47 | 48 | 可能你觉得自己很清楚了,你是张三,你是李四。但如果让你用三个词,可以是名动、形容词、动词或者成语都可以,只用三个词来概括一下「你是谁」,你能做到吗?这就是你对自己的定位。 49 | 50 | - 问题 2:「你擅长做什么?」 51 | 52 | 如果我能说「什么都不擅长就是最好的擅长」,可能大家都会拍手称快吧。正如问很多人你的兴趣是什么一样,有很多人都表示没有什么兴趣。然而,你真的没有擅长的事情吗?可能只是因为你没有发现而已。这是你对自己能力的定位。 53 | 54 | ### 如何对自己进行更好的定位 55 | 56 | 如果你真正把问题定位好了,其实解决问题并不难。然而,难就难在对自己进行定位往往很难做到精准,正如苏轼的名句:「不识庐山真面目,只缘身在此山中」。 57 | 58 | 即然是定位,那么,我们用生活中的一个例子来说明吧。你的手机上的 GPS 定位是怎么做到「精准」定位的? 59 | 60 | 不是靠手机自身就行的吧,它需要通过在天上的至少 4 颗卫星才能精确定位自己的空间位置。也就是一个简单的道理: 61 | 62 | **你要定位自己,是需要别人来做参照物的。** 63 | 64 | 那么如果你是应届毕业生,你需要在简历中和你的参照物(基本上是和你同一届毕业的人)做个对比较。如果你已工作多年,那么和同样工作几年的人或者应届毕业生,你也需要和他们做一个比较。 65 | 66 | 其实,你也可以从另一个角度理解,要定位自己,也可以参考一下别人眼中的自己是怎么样的,别人往往更能发现你的优点和缺点,而且一般比你自己评估的要准确。 67 | 68 | 此外,你还可以用事物来做参照。比如为了表达你擅长 Android 视频直播技术,很多人喜欢直接在简历上加上一个「自我评价」,然后写上「我擅长 Android 视频直播技术」,你觉得是个什么效果呢?像不像一个人说他很幽默,但别人和他聊天从来没笑过? 69 | 70 | 比较好的表达一个人幽默的方法应该是直接说段子,那么表达一个人擅长某个技术的方法呢?「段子」留给大家自己去写吧。 71 | 72 | ### 简历的加分项和减分项 73 | 74 | 在面试后和他们分享,我才知道,他们其实并不是说没有认真去做,而是确实不知道简历上到底应该放哪些东西,或者说编写的顺序是怎样的,大多数人都是直接使用比如拉勾网呀、BOSS 直聘呀,这些招聘网站的模板来编写的。这就会导致,我们的简历上出现了一大堆没有任何价值,别人也不关心的东西。所以,我在这里再次申明呀:**千万不要去用招聘网站的模板来做你的简历。** 75 | 76 | 有哪些好一点的简历加分项呢? 77 | 78 | - 黄金两页原则:最好简历控制在 A4 两页; 79 | - PDF 原则:简历最好为 PDF,这样可以解决不同系统上打开格式混乱或者乱码的问题; 80 | - 在最顶部写清楚个人信息:比如姓名,电话,年龄,毕业院校/专业,Blog 和 GitHub 比较有内容的也可以直接放在最前面。最好一行两个信息,不要一行只写一个信息,导致右半部全部空白浪费。 81 | 82 | ### 不知道怎么写项目经历 83 | 84 | 从简历上来看,大家还有一个常见的问题是,对于项目经历,写的非常粗糙,大多数人都是写的:「我用了什么第三方库,实现了什么功能。」「我在项目中,使用什么技术,做了什么什么。」甚至是,一个项目下来,三句话就被完全介绍完了。对于这点,我还专门咨询了我们的 HR,他们对于这种技术简历,会怎么处理。我得到的回答是:「除非是非常厉害的公司或者项目,不然都会直接 Pass 的。」 85 | 86 | 那到底怎么写项目经历呢?在网上,盛传着一种法则:STAR (Situation Task Action Result)法则。 87 | 88 | - S(情景):这可以是让你参与这个项目,或者是解决问题的背景; 89 | - T(任务):只需要在简历上写清楚自己的职责; 90 | - A(策略):如何实现,是否遇到困难,如何解决的。 91 | - R(结果):取得的成果,**作为程序员,一定用数字说话。** 92 | 93 | 前面说了,在编写项目经历上,大多数人写的真的非常粗糙,基本都是使用了什么技术,什么库。甚至有人仅仅只是用过,也写了一个「精通」,实际上这是非常不好的,即使拿到了面试资格,也很容易在面试中翻车。因为大多数面试官面试的策略都分两步: 94 | 95 | - 就你简历上提及的点问问题,了解你对简历上技术的掌握程度; 96 | - 就公司处理好的或者还没有处理的难题,问问你的想法。 97 | 98 | 所以,简历上出现的内容,一定要是自己能答上的,仅仅是使用过的东西,不要写在最前面。 99 | 100 | ### 到底写不写自我评价? 101 | 102 | 还有一个比较受争议的问题,就是到底写不写自我评价。可能是受招聘网站模板简历的影响,我发现 80% 的面试者都写了这一栏。其实无可厚非,但大多数人直接放在第二个位置,我很好奇这样做的缘由。最重要的是,深入一看,内容居然都是一些乱七八糟,面试官并不关心的内容。 103 | 104 | 先给大家截取一个反面教材: 105 | 106 | - 为人友善,诚实谦虚,勤奋,能吃苦耐劳,有耐心; 107 | - 有团队意识,能和同事和谐相处,虚心接受别人的建议; 108 | - 责任心强,善于沟通,具有良好的团队合作精神; 109 | - 专业扎实,具有较强的钻研精神和学习能力; 110 | - 性格比较乐观外向,喜欢打篮球和羽毛球。 111 | 112 | 当我看到这个自我评价的时候,就觉得比较冠冕堂皇,可能面试者确实就是这么好的人,但是,从何取证呢?所以建议大家不要在简历中写一堆废话,简历就是求职的敲门砖,不要把没有价值的东西放在上面。 113 | 114 | 正确的关注点应该是: 115 | 116 | - 定位:主攻 Android,不依赖别人,希望全栈; 117 | - 态度:高效完成=>追求完美=>互相提升; 118 | - 困难:Google && Stack Over Flow,阅读文档源码; 119 | - 视野:GitHub? 掘金? 简书? CSDN? 120 | - 优劣势:热爱技术,心态沟通。非科班劣势(算法) 121 | 122 | 上面并不是模板,只是着重的一些点。好的自我评价应该写清楚自己的定位和方向,以及平时自己的一些生活和习惯。非科班出身的通常会被打上算法不够出众的标签,但我们完全可以从其他方面表现自己的优势。 123 | 124 | ### 简历总结 125 | 126 | 总的来说,简历就是求职的敲门砖。 127 | 128 | - 它不需要精美的外表,但求整洁和规范,使用 PDF,控制在两页左右; 129 | - 它不求你样样精通,但求你对上面的内容胸有成竹; 130 | - 只写有价值的内容,不写无意义东西。 131 | - 对于不一样的公司,简历尽量不要只写一个。 132 | - 经历一定要写出亮点,如果你没有亮点,那现在就行动起来去创造一两个亮点,让自己一两年后不再重复这样的烦恼。 133 | 134 | ### 面试 135 | 136 | 说完了简历,我们再来谈谈面试。无数次面试让我深刻的明白一个道理:**作为程序员,我们的能力都是建立在技术基础上的,技术不达标,其他的能力会显得很缥缈。**但决定我们是否通过面试,除了我们得有足够的技术能力以外,还得有不错的表达和沟通能力。 137 | 138 | 我进行了 118 次模拟面试,却发现了一个非常尴尬的结论,那就是:**没有人会做自我介绍。** 139 | 140 | 基本总结下来大家的自我介绍是这样的: 141 | 142 | - 简单介绍下自己的个人基本信息,就已经不知所措; 143 | - 说两三句就终止; 144 | - 把简历上的内容背诵一遍; 145 | 146 | 重点我「模拟面试」的方式仅仅是通过微信语音,还没有现场的氛围压抑感,我难以想象要是在现场面试他们会发挥成怎样。我一度以为他们是表达能力不行,或者说是怯场紧张,所以我一直在调整面试气氛。但到讲技术的时候,我发现他们的问题并不是出在表达能力。 147 | 148 | ### 如何自我介绍 149 | 150 | 必须要说一下自我介绍啊。任何的面试,都会有自我介绍这个环节,这是大家给面试官的第一印象,真的非常重要。一个好的自我介绍,能改善面试气氛,让自己接下来发挥更好,面试官也面试得更爽。 151 | 152 | 那我们自我介绍应该说什么呢?我建议大家说自己的亮点闪光点,这些东西最好是简历上亮点的详细介绍,时间控制在一分钟左右,再重复一遍:**不要背简历!!!** 153 | 154 | 我先给大家来个范例哈,注意一下,这是没有什么技术闪光点的自我介绍。 155 | 156 | > 面试官您好,我是刘世麟,非常荣幸能参加贵公司的面试,下面我简单介绍一下我的个人情况:我从实习到现在一直在 XX 公司工作,从事 Android 开发,凭借良好的工作能力和沟通能力,连续两年蝉联「优秀员工」称号,在今年初被公司内聘为技术总监助理,协助技术总监开展部门管理和项目推动工作。在工作之外,我喜欢编写技术博客和在 GitHub 上贡献开源代码,目前在 GitHub 上总共拥有 7k 左右的 Star,数篇技术博客也有数十万阅读。我非常地热爱移动开发,早已久仰贵团队对技术的看重,所以希望今天自己面试有好的表现,未来能有幸与您共事。 157 | 158 | 很简单的一个自我介绍,这些内容在简历上都很少提及,其中,我抓住要点,强调了我自己的工作能力和沟通能力。蝉联优秀员工,被内聘为技术总监助理,都说明了自己在工作中的优异表现。在技术博客和 GitHub 上的表现,说面了我的软实力比较强,非常地热爱学习, 通过数字性的展示,让面试官有了一个比较直观的良好感受。 159 | 160 | 大家在面试之前都可以在笔记本上面写下来,然后自己对着墙壁,模拟介绍几次,这样下来你在面试中进行自我介绍的时候往往就能够得心应手。 161 | 162 | ### 技术面试 163 | 164 | 在自我介绍之后,一般都会开始进行技术面试,基本上你的技术等级都会在技术面试环节敲定。那面试官如何在短时间内判断你的技术等级呢? 165 | 166 | 这里先借用凯哥之前在知乎上的一个回答,我觉得非常有意思。 167 | 168 | ![](https://upload-images.jianshu.io/upload_images/3994917-1fdcf28ba40cd607.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 169 | 170 | 从这里,我想大家已经可以得到结论啦。通常面试官在开始面试的时候,都会针对你的简历对你进行大概的判断。因为每个人的经历不同,擅长的方向也是千差万别,所以都不会紧抓自己擅长的方向问,而会选择对你简历上提及的内容进行挖掘。 171 | 172 | 比如你说你擅长使用 RecyclerView,那你知道如何处理 RecyclerView 的嵌套滚动么?那你知道如何处理 ViewPager 和 RecyclerView 嵌套的时候出现的焦点问题么?假设检查 RecyclerView 各种设置没问题后,数据却展示不出来,你能猜想哪些原因么?假设只能用一个 RecyclerView,不用分 Type,让你实现一个复杂布局,你能想到一些方法或思路么? 173 | 174 | 我们经常会在面试前刷很多的面试题,准备很久,但我们真正到面试的时候,却总是被面试官虐的体无完肤。这说明了一个问题,我们平时准备的东西,平时如果没有深入理解的,是很难在面试中正常发挥的,所以,这还需要一个沉淀的过程。所以大家参加 HenCoder Plus 跟着凯哥搞清楚细节,是非常有价值的。 175 | 176 | 一些的情况说明一个结论:**细节 => 技术**。 177 | 178 | 但我想说,应该是 **细节 + 深度 => 技术。** 179 | 180 | 前面我们有说到,在简历上我们可以使用 STAR 法则编写我们的履历。实际上,我们在编写简历的时候,就已经可以思考自己面试中可能被问到的问题啦。呈现在简历上的是遵循 STAR 法则的精简版内容,实际上面试中,我们给到的应该是详细版。不过我认为在面试中应该是 START 法则,我在后面加了一个 T,这个 T 是什么呢?Thinking。 181 | 182 | 不会总结的程序员不是好程序员,大家知道,我在工作之余写了不少 Blog,实际上就是一个总结的过程,我认为这样的方式,让我成长非常迅速。实际上,我们在面试中完全可以展现自己的总结能力,让面试官看到自己的亮点。 183 | 184 | 我们来看看面试中,我们如何利用好 START 法则。 185 | 186 | - S(Situation):在简历上我们呈现的是项目的背景,但在面试中,我们还应该就项目的细节进行更加详细的讲解。 187 | - T(Task):我们在简历上主要是编写自己在该项目中承担的职责,但在面试中,除了说明自己的职责,建议带上团队的整体任务,展现出无论何时,你都很在乎你的团队。 188 | - A(Action):在简历上我们展现的是应对问题采取的策略和具体方法,在面试中,除了进行详细说明以外,还应该说清楚自己和团队成员的分工,一定需要记住的是:**不能否认团队的价值。** 189 | - R(Result):产生的结果,这一点在面试中和简历上可以基本保持一致。 190 | - T(Thinking):这一个词是我自己添加的,我觉得一个好的介绍还应该举一反三,总结这个事情哪里做的好,哪里做的不好,接下来如何去避免这个问题,以及可以复用在哪些场景。 191 | 192 | ### 简历之外的技术面试 193 | 194 | 除了上面提及的,面试官会对简历上进行深挖细掘以外,通常面试官还会问一些其他的。比如对于中级和初级工程师,一般会问一些 Java 基础和 Android 基础,比如什么 HashMap 的内部结构,Hash 碰撞处理方式呀,还有 JVM 类加载过程呀,垃圾回收算法呀,启动模式呀,Handler 原理呀,Android 的事件分发机制呀,Activity 的生命周期呀等等。这些问题好像网上都已经司空见惯,很多人都选择了直接去背诵面试题答案。 195 | 196 | 我是非常不赞同背诵答案这种做法的,人的记忆本来就是有限的,你的脑袋就只能装这么多,况且网上的博客基本出处都差不多,很多博客并没有深入到细节里面。现在的面试官也越来越聪明,知道如何辨别面试者是真会还是假会。 197 | 198 | 比如上面的 Activity 生命周期,可能网上都会写,额是,onCreate() => onStart() => onResume() => onPause() => onStop() => onDestory()。但实际上,背的了这个流程,不一定能灵活应用起来。比如面试官问到,锁屏会依次调用哪些生命周期,面试者不一定知道。有些 Blog 可能写的比较仔细,会给大家列上一个表,展示这些可能的问题,比如 锁屏是 onPause() => onStop(),Activity 从不可见到可见的调用方法是 onStart(),onStop() 是完全不可见的时候调用,所以自然而然调起 Dialog 的时候走的生命周期是 onPause() 而不是 onStop()。甚至有的 Blog 直接教大家背诵。完整生存期是 onCreate() 一直走到 onDestroy(),可见生存期是 onStart() 到 onStop(),前台生存期是 onPause() 到 onPause()。 199 | 200 | 可能这样的问题早已经被大家司空见惯,但实际上,面试官早就不会这样直接问了。基本采用的方式是给你一个场景,看你能否正确的处理,比如 Activity A启动 Activity B 后,A 真的一定会调用 `onStop()` 么?毕竟我们平时做需求,也是用自己已有的知识组织起来解决需求的。 201 | 202 | ### 如何准备面试 203 | 204 | 说了这么多面试技巧,那我们还得有个非常重要的过程:准备面试,大多数人会选择去看各种面经,刷各种面试题。虽然这样确实会有一定的作用,但我认为是低效的。首先,大多数的面经,都没有一个深入讲解的答案。第二,刷的题,大多数和求职公司的面试不匹配。目前看来,只有刷算法题在应对算法类面试的时候成功率较高,基本应用类面试,作用都微乎其微。 205 | 206 | 这就让我们必须谈到了另外一个话题:如何准备面试? 207 | 208 | 经过我多次试验发现,技术面试的面试官问的知识,80% 会来自于你的简历,所以你至少提前除了准备自我介绍,还应该认真针对简历上的每个技术点,思考一下可能出现的面试题,并想一想如何去应对它。 209 | 210 | 除了一些特别基础的机制原理问题,比如 Activity 的生命周期呀、Handler 机制等,其他问题都是允许面试者答错的。其实面试官并不会奢望你能够完整无缺地答好每一个知识点,有时候判断一个面试者是否适合面试官所在公司提供的开发岗位,往往看的是面试者在回答问题中体现出来的「编码之外的能力」。所以,不要想着背诵面试题和知识点,那样无疑是低效的,并且这样得来的知识,你以后也基本不可能用到。 211 | 212 | 此外,针对不一样的工作岗位,准备的面试内容也不应该一样。相较于中小型企业,大厂会问的知识面更广,比如会问不少的算法和计算机网络等基础知识,而一般的中小型企业却对这块不那么看重,他们更期望的是能迅速上手的人。也就是说,他们需求的并不是一个技术多么厉害的人,而是一个合适的人。对于初级和中级工程师,面试官会更看重基础知识,对于高级甚至资深工程师,会对多线程编程,自定义View,架构能力,产品观有更高的要求。 213 | 214 | 其实「二八原则」在好多地方都发挥着作用,在 Android 开发中,我认为也是一样的。作为一个 Android 开发,你也许只会用到 Android 开发知识的 20%,另外的 80% 你其实学了也不一定会用。 215 | 216 | 而面试官其实也一样,他有可能也只掌握了 20% 的知识,而且一个面试也不会有足够多的时间给你展示你全部的知识,而往往只会注意开发中最常遇到的 20%。但大体上来说,这 20% 比较重要的知识点,一般都是大家需要重视和答对的。我之前在公众号上写了一个面试专题,现在在公众号底部也还有一个导航。虽然后面夭折了,但写的内容基本都是每一位 Android 开发都需要重视和答对的。 217 | 218 | 俗话说「英雄不问出处」,前提很明显了你得是「英雄」。如果别人不知道你是「英雄」,那么势必会通过你的学历、公司和项目经验来判断你是否合适。毕业的学校和任职过的公司,包括你跳槽的频繁度等都会对你的评分有一定的影响。 219 | 220 | 但其实最重要做判断的根据还是你的项目经验。所以对于你从事过的项目及你在这些项目中的职责和作用,你应该有一个清晰的描叙。 221 | 222 | 对于项目经验丰富但是项目的类型单一的人,如项目中清一色的「资讯」类应用,那么你应该表现你具备独立开发和处理各方面问题的能力,而且最好在平时你就要有意识的避免进入到这种境地当中。对于「一个经验用十年」的人,面试官其实也很难分辨出他在其他的方面是否也能做得一样好,如果你不能在公司层面避免陷入到这种情况,那么你还是应该尝试同一个项目中的不同方面,或者自己做一些和当前公司不相关的项目、开源库等。 223 | 224 | 但其实有很多人的问题在于,项目经验并不丰富,而且有些人工作了很多年,但有可能其中的几年都在维护一个项目,简历上往往用一句话就把这几年的事情说完了。但我认为,并不是我们在这几年中没有做什么有价值的事,而是我们没有把这些事情记录和总结,并做一个深入的思考和扩展。想想吧,总会有的,把事情想到了还要对这个主题做一下扩展,你总结出来的东西才更有深意。 225 | 226 | 对于面试者来说,往往觉得面试就是回答对面试官的问题,但从面试官的角度来看,面试其实就是要做一件事情:「如何区分面试者」。简单的说,就是把你和面试官面过的(或即将面试的)的人区分开来,并给你打上几个签标,简单点可以是「不错」、「合适」、「犹豫」、「肯定不行」。复杂点的,可能会把你的某些能力列出来,比如学习能力强、协作能力差,然后再和其他人放在一起综合考虑。每个项目都有不同的特点,所以每次的侧重能力考察也会不一样。 227 | 228 | 所以,有时候你通过了一家公司的面试,也不需要太得意了,可能并不是你有多厉害,仅仅只是你正好是这个时间段里性价比较高的那个。当然,如果你被淘汰了,也不需要妄自菲薄,也许只是因为在这个时间段有个比你更高性价比的人也来这家公司面试了。 229 | 230 | ### 如何准备与 HR 的面试 231 | 232 | HR 通常会问你一些离职原因和职业规划,对于离职原因的阐述,我想大家应该都很清楚了,不要否认老东家的价值,不要否认老东家的价值,可以结合现在面试公司来说出自己的展望。比如我之前面试美团的时候,我是这样回答的,**因为美团是一个非常重视移动端业务的互联网平台,而我之前公司由于公司战略的变化,现在业务像提供服务转型而弱化了移动端,作为一个深爱着移动互联网的人,我渴望加入美团这样的团队。** 233 | 234 | 而除了说离职原因,我们还极可能遇到的诸如「说说你的优缺点」、「你最擅长什么」、「你在项目提供的最有价值的作用是什么」等等这类问题。这类问题在我前不久的面试中,其实技术面都会提到。其实,反过来看就很简单了,这些问题归根到底就是「你和别人的区别在哪里」。面试官的任务是要把你和别人做区别,你自己也需要把自己和别人做区别,回答「不知道、好像没什么这样的话,其本上会给减分。 235 | 236 | 我遇到过最难的「吹牛」面试题就是: 237 | 238 | > 说说你和其他程序员相比你更出色的地方,为什么我们要录用你? 239 | 240 | 这是我遇到过的最难的面试题,哲学家苏格拉底说过:「人最难的就是认识自己」。这句话一点都不假,我们可能经常会惯性地觉得自己比其他程序员厉害,但真要说厉害在哪里,这真难说出来。每个人都有和别人不一样的地方,在面试前一定要想想一些正面的积极的地方,然后自己总结一下,最好给你周围的同事、朋友说一下,看他们是否认同你的看法。最后你会发现给别人说事情时,最好的方式是说一些案例故事,虽然你要说的可能只是一个简单的点(比如你抗压能力强),但你也可以用讲故事的方式讲出来(在某次事件中你在怎样的压力下完成工作的)。 241 | 242 | ### 如何准备和 BOSS 的面试 243 | 244 | 在我进行模拟面试中,我清晰地记得我有一位读者,在一天晚上 11 点过,慌乱地找到我,希望我能给他做一下模拟面试,因为第二天他就要和自己心仪已久公司的 CEO 进行终面。这位小伙伴已经经过了三轮技术面和 HR 面了,而且是自己非常渴望加入的公司,所以也是慌得不行。我强忍睡意,和他进行了模拟面试,额,严格意义上说,这只能算一次交流。 245 | 246 | 我简单了解了下他的情况,他的音视频开发经验非常丰富,而自己渴望加入的公司也是微视频领域的。所以我认为他没什么好担心的,经过了整整 3 轮的技术面试,说明他的技术实力已经得到了公司的认可,BOSS 面最多只是随便聊聊,谈谈「人生和理想」。 247 | 248 | > 大家切不可小看这个「随便聊聊」,这个「聊聊」可以很容易看出你的思维能力和对事物的看法,而且这些方面是你短期很难改变的特质。 249 | 250 | 他应聘的职位只是高级开发工程师,并不是管理和技术负责人的角色,对方并不会太在意他的管理能力和领导能力,所以后面的面试大可轻松应对,但还是要简单准备一下,了解下公司情况,态度上不卑不亢。 251 | 252 | ### 如何和 HR 谈薪资 253 | 254 | 中国人都很喜欢打听别人的收入,收入对于我们这个社会传统来说并不算隐私,但是对本公司或者同行业的人我一般都会选择隐瞒。HR 或者公司的制度都会明文规定不许在公司内打听员工的工资和奖金,为什么呢?大家心知肚明,别人比自己低了,别人难过;别人比咱高了,自己得难过吧。人都会认为自己的能力高于平均水平,对公司的贡献肯定比身边的某某多,但一但得知对方的工资比自己高,那就容易打破自己的心理平衡。 255 | 256 | 身体不平衡容易生病,心理不平衡容易出事。 257 | 258 | 所以薪资这个东西一直以来都是一个敏感话题。在讲这个之前,我想先提醒大家,选择工作的时候一定不要只看薪资福利,而应该看重更加长远的价值。 259 | 260 | 前段时间,我也经历了找工作,一共面了 4 个公司吧, 拿到了 3 个 offer,但我最后就选择了薪资最低的公司,尽管薪资第一的公司一年可以多不少收入。直到现在,我一点都不后悔。因为我看中的是同事们的学习能力和提升,现在的公司每隔一周都会又一次技术分享,分享的内容,经常都让我瞠目结舌。 261 | 262 | 好像扯远啦,我们终究还是绕不开和 HR 谈薪资的过程,我们总是期望着在加入自己心仪公司的前提下,还可以摇到尽可能高的薪资。下面结合我的经验给大家几点建议: 263 | 264 | - 增加自己的筹码 265 | 266 | 也就是你在技术面试和管理者的面试过程中表现较好,这时候你可以获得较高的评级,这时候 HR 给到你的幅度也会大一些。所以在面试前应该进行充分的准备,如果你是别人犹豫的对象,刚好放你通过,你不来也行的话,HR一般情况下是不会对你让步的。所以总得来说,好像是废话,但就是真理,你还得好好准备提升自己的能力,以便于在面试中获得更好的评价。 267 | 268 | - 在关键位置上有人 269 | 270 | 这一点非常重要,有自己人在你要面试的公司内部,你可以获知他们很多情况,比如这次招聘是否紧急,什么叫坐地起价,就是别人没有你有。有自己人在你才能知道有没有其他比你合适的候选者,没有的话,你才有资格坐地起价。 271 | 272 | 如果一个公司找了很久都找不到合适的人,项目已经迫在眉睫,这个时候你出现了。但是你并不知道你是他们千辛万苦找到的,他们不要你就有可能将项目至于风险中可能会有更大的损失,这个时候你是可以要一个比较高的价位的。但没有自己人告诉你这个状态的话,一般你还是会从自身出发,可能觉得自己的表现并不算特别好,会患得患失,脸皮薄的话当然不敢狮子大开口(或者你不明白你为什么在这个时候在这个公司值高价)。 273 | 274 | 如果没有相关资讯了解对方公司内部对人员的需求情况的话,很多时候你靠的是运气,就是看你前后的面试者(候选人)和你的性价比结果。如果那个时期,只有你一个候选人,那么你的要求不过份,一般都可以满足。当然,HR还是会打击你一下压压价,别人的工作就是要控制人力成本嘛。所以你认为能力不如你的人工资却比你高,一点也不奇怪,你们进入公司的时机不一样。而且就算你认为对方水平不如你的,那只是你个人的看法,他的水平可能真不如你,但在公司的层面,你们可能被划分在同一级别。所以不要太在意,最好就不要去打听。 275 | 276 | - 不要让对方给你定价格 277 | 278 | 之前不少读者问我,在 HR 问到期望薪资的时候,自己可否反问对方 HR 自己值多少钱。假如我是 HR,我肯定是非常反感这种行为的,我询问我们家 HR 后(平时和 HR 私下关系很好),得到的结果如出一辙。后面我发现网上竟然还有文章说到,可以委婉地把问题抛回给 HR,比如「我相信贵公司会给我一个心仪的价位。」 279 | 280 | 我个人是非常反对这样的做法的。大多数公司会给到招聘网站上挂的薪资范围,而且普遍会比较接近平均值以下。所以当 HR 问你期望薪资的时候,你除了要预估自己在之前技术面试中的表现,还应该注意一下公司给的薪资范围。一般情况下不要直接给出公司给的最高值(自己能力足够优秀的例外),基本比自己预期高出 15% 较为合适,比如自己心里期望是 18k,实际上自己觉得 16k 也可以加入,这时候可以说自己期望是 20k 如果实在是没有底气,也可以附加一句, 19k 也可以,相信公司不会埋没自己。 281 | 282 | ### 你还有什么想问的? 283 | 284 | 经常遇到这样的面试官,当他吧唧吧唧问完你问题后,突然就停下了,但感觉又意犹未尽,所以往往会把对话的主动权交给你,让你来提问。 285 | 286 | > 我的问题问完了,你有什么要问的吗? 287 | 288 | 可能这时对于你,“要问”还是“不要问”是个问题。如果要问,那么要问什么样的问题呢?只要你不是太傻太天真,你的内心会坚信面试其实还没结束,并不是什么问题都合适问的,如奖金、加班费这些你特别关心的。 289 | 290 | 面试官的这个问题,是有意问的一种开放性问题,以此来了解你这个人的关注点;还是仅仅是因为面试确实无问题可问了,但又不想太直接结束面试,所以就顺便问问? 291 | 292 | 这真的是个问题。 293 | 294 | 不过我们不一定非要去揣测面试官的用意,我们回到自身的需要。 295 | 296 | 如果你也没什么问题想问,那么可以委婉的告诉面试官自己没什么问题要问。 297 | 298 | 如:「通过一些朋友和渠道,其实我对贵公司的一些文化和愿景都还比较了解,所以我暂时也没有什么想问的,我也很希望能加入到这样一个环境中。」 299 | 300 | 那问什么呢? 301 | 302 | 如果要问,那问题就多了。 303 | 304 | > “项目常加班吗?有加班费吗?” 305 | > “有出国旅游吗?” 306 | > “在这个团队中的个人提升空间怎样?” 307 | > ...... 308 | 309 | 其实,并不是说上面的这些问题不好,或者不能问。只是,我们问题之前应该思考一下,问什么样的问题即可以了解到想要的信息,又是眼前这个面试官最合适回答的。即我们要让这个问题问出去后的对话能成为有效的沟通,而且这个问题是我们关注的,并且这个问题是对方比较有发言权的。 310 | 311 | 如关于加班费的问题,其实你问 HR 或者在里面上班的朋友会更清楚些。 312 | 313 | **技术面试官** 314 | 在提出问题前,我们要先看一下现在这个面试官是处在公司的什么位置。如果他也是一个开发人员,在对你做技术面试,那不妨聊聊团队的一些技术栈方面的问题。 315 | 316 | 如:“你们的团队在采用敏捷开发的方式吗?” 317 | 然后和面试官聊聊敏捷,分享一些各自的经验,方便双方进一步的了解。 318 | 319 | 并不是所有的公司都会用敏捷,那我们可以问一些更开放性的问题,如:「在你们的项目中遇到技术障碍了,公司有什么机制去应对吗?」可以就此看看这个公司是否重视技术,有没有一些技术提升和交流的传统。 320 | 321 | **管理类面试官** 322 | 如果面试官是管理职位的,那么可以问问团队组成;假设你能加入的话,会分配在哪个team,team中有没有带你的人或让你得到进步的模式;或者了解一下他对团队目前状态的看法,是否有什么变化他想引入团队或组织的。 323 | 324 | 也就是向管理类面试官提问,你可以问一些对团队现状和未来预期(目标)相关的一些问题,这些问题会让你提前知道,进入这家公司后你应该往哪个方向去努力。 325 | 326 | **HR** 327 | 公司文化什么的 HR 一般会主动向你介绍,薪酬和福利不清楚的地方也可以继续沟通。 328 | 329 | 简单说,问自己需想要得到答案的问题,而且要针对不同的面试官问对方比较“擅长”回答的问题。 330 | 331 | > 管理者:问战略 332 | > 技术人员:问战术 333 | > HR 行政人员:问后勤 334 | 335 | ### 离面试不到 24 小时,怎么办? 336 | 337 | 大多数面试一般都会有 3 天以上的时间让我们准备,不过我们时常还是会遇到临时安排的面试,给你准备的时间不足 24 个小时,让人措手不及,这个时候我们该准备些什么呢?当然,有些同学可能是从有很多天时间拖延到只剩一天,才下定决心要准备一下。 338 | 339 | 那假如我们就只剩下一天的时间了,怎么办?不少小伙伴呀,会越来越慌,越来越慌,不断地去看一些其他的面试点,生怕哪一个点没有看到。最后呢,在面试的时候,发现自己全都忘光啦,而且在面试的时候,发现自己前面没答好,极容易影响自己后面的发挥,对吧。 340 | 341 | 其实时间越近啦,反而我们不应该再去看一些新的面试知识,放平心态,不需要准备什么。 342 | 343 | 不需要准备什么?你可能会说,南尘,你在逗我?怎么可以平静到什么都不去准备呢? 344 | 345 | 确实还是需要准备一些东西。 346 | 347 | 那到底准备啥? 348 | 349 | - 自我介绍 350 | 351 | 第一个是自我介绍,刚刚前面说了我们要怎么自我介绍,到底要说多少句,但我没有说到底该什么时候准备。正常来说,在面试前一天准备这个,是最好的,但不要死记硬背。 352 | 353 | - 项目经验介绍 354 | 355 | 你比较熟悉的项目是什么?你在工作中遇到的最大困难是什么?以及你最终是如何解决的?这个项目让你得到了什么成长? 356 | 357 | - 自己的定位 358 | 359 | 一般在面试中,还会问到的优势和劣势,比如说:「你的有点是什么」?「你最擅长的事情是什么」? 360 | 361 | 不过但凡是你对自己有一个比较清晰的定位,哪怕你这个定位是错的,你也可以以不变应万变,拿出纸和笔记录下你自己的优劣势,并附上相应的案例。 362 | 363 | - 尽可能了解公司 364 | 365 | 要了解公司什么呢?肯定不是他的什么福利呀什么的,你先得知道对方想要什么样的人,可以先看招聘网上的职位简介。这个一般还是不够准确,如果有条件,可以问问里面工作的人,或者直接问他们 HR,如果录用你的话,会让你做什么。当你清楚了对方想招什么样的人,你会更加清楚自己应该怎么做。 366 | 367 | 再来应该了解面试公司的产品,猜测他们可能会遇到哪些问题。对方招你去是要实现产品和解决问题的,比如大型的 APP 应用,可能会涉及组件化方案和各种性能问题,而小型的应用可能会更加看重你的快速开发能力。 368 | 369 | - 如果有条件,还可以准备一下 Java 的基础和 Android 的基础,没什么好说的,Android 基础可以直接去看我的面试系列。 370 | 371 | - 不需要准备的。 372 | 373 | 一些你现在还没有掌握的技术点,准备它们的收益不是很大。既然你还未掌握,现在再看一遍还是难以理解透彻,可能还会出现你认为自己答对了,面试官却认为你南辕北辙的情况。 374 | 375 | 比如:算法、设计模式、OpenGL 等,这时候看并不利于你记忆和应付面试官可能换一个角度来问你。 376 | 377 | 这样的题或者说技能,是很重要的,但在 24小 时内你只能接受这个现实。如果你其他都准备好了,也可以从现在开始投入时间在这些方面,但不要想着马上就能用上。 378 | 379 | 在最后一刻,请再看一遍自己投递给这家公司的简历,如实按简历上的回答,保证你的诚信。如果你的说法和简历上不相符,对你的影响是很大的。 380 | 381 | ### 总结 382 | 383 | 今天大概就讲到这样,我们来做个总结。首先是我们的简历总结。 384 | 385 | #### 针对简历 386 | 387 | - 简历上的所有内容都应该是你最有价值的东西。 388 | 389 | 第一句话非常重要,我在这里再说一遍啊。**简历上的所有内容都应该是你最有价值的东西。**这上面不应该带任何乱七八糟毫无价值的东西哈,比如你喜欢打游戏,喜欢摄影,这跟我们程序员没什么太大关系哈。 390 | 391 | - 黄金两页,PDF 格式,个人信息一行两个避免留白 392 | 393 | 我们的简历简历都采用 PDF 格式,防止别人打开出现各种格式问题。页数最好控制在两页,项目个数控制在 3~5 个,项目数目太多的,注意筛选,每个项目宁可写更详细也不要写太多的项目,免除外包项目嫌疑。最开始的个人信息栏最好一行写两个信息,不要一行只写一个,浪费右上角大部分的好展位。 394 | 395 | - 简历上每个栏目的顺序 396 | 397 | 简历上每个栏目的顺序最好遵循:个人信息 => 工作经历 => 项目经历 => 个人评价。社招的同学也建议自己能有几个练手项目,可以增加校内经历在第二个栏目。 398 | 399 | - 利用好 STAR 法则 400 | 401 | 在写简历的项目介绍的时候,最好采用 STAR 法则写清楚项目发生背景、自己的职责任务、自己的处理方案和遇到困难如何解决,最后用数字来量化结果。 402 | 403 | - 拒绝太官方的自我评价 404 | 405 | 自我评价可以写,但不应该太多,一般 4 ~ 5 条适宜,内容不要太官方,最好能体现自己的定位,态度,遇到困难时的方案,自己平时的技术交流平台,以及自己的优劣势。一般自己对自己评价容易太主观,这时候把自己的想法告诉你的朋友,看看他们是否和你的看法一致,一般而言,其他人的看法会更加具有客观性。 406 | 407 | - 不要只准备一份简历 408 | 409 | 对不同的公司应该准备不一样的简历,实际上每次的修改也不大,但不要一味迎合对方的 JD 要求去改写自己的简历,自己的内容还是应该做到心中有数。 410 | 411 | #### 针对面试 412 | 413 | - 提前准备自我介绍 414 | 415 | 自我介绍是面试的第一个环节,而且在每一轮面试都会存在。针对不一样的面试官,自我介绍应该侧重点不一样,但大体思路一致。不可完全背诵简历上有的内容,也不可随便说几句就完事儿。正确的思路应该是自己的一个全面总结,包含自己的工作中印象深刻的难题解决过程(START 法则),一半表现自己的技术硬实力和工作表现,另一半表现自己的软实力,包括但不限于沟通,学习和其他能力。 416 | 417 | 针对自我介绍,最好是提前在纸上写下来,并且对着墙多练习几遍。 418 | 419 | - 如何准备技术面试 420 | 421 | 技术面试一般分两个方向。 422 | 423 | 第一个方向是简历上的内容,一般会针对简历上提及的内容进行深层次地追问,以确保简历上的内容属实,并且很容易通过细节判断技术深度。所以在写简历的时候,就应该猜测面试官可能面试到的问题。对简历上出现的一些框架,最好针对它的疑问点进行一定的准备。比如你说你擅长使用 RxJava,那你得知道 RxJava 1 和 2 的区别联系吧,给你一个实际场景,你得知道什么时候用 map、flatMap、zip、skipWhile 等这些操作符吧,你还得知道背压吧,以及 RxJava 2 到底是怎么去应对处理的吧? 424 | 425 | 第二个方向是简历之外的内容提问。这个得先看你面试的职级,比如中级和初级开发,你得明白四大组件的基本生命周期吧,你得明白 Java 的基本基础吧,你得明白基本的自定义 View 吧。对于高级和资深,你得好好准备一下多线程、复杂自定义 View 以及动画,得知道多点触控这些吧。还有一些就是面试官所在的公司迫切希望解决或者是他们之前的问题后面得到解决的内容了。 426 | 427 | 还有一个是需要看一下你面试的公司,稍大的公司会更加在乎你的基础水平和代码质量,所以会对你的技术深度和技术广度有更高的要去,而小型甚至外包公司会更在乎你的开发速度,和你的抗压能力。 428 | 429 | 总的来说,准备再多的面试题都不如先把基础问题弄清楚,弄明白,再把你简历上提及的技术点都先想清楚,搞明白再去面试。 430 | 431 | - 如何应对吹牛题 432 | 433 | HR 的吹牛题不可避免,而且现在吹牛题除了 HR,甚至还有一些总监、经理等都会问到。基本吹牛题都是什么离职原因呀,职业规划呀,什么你遇到过最大的挑战呀,还有你的优势是什么,为什么我们要录用你呀这类的。这些问题还是应该事先准备的。和自我介绍应该,同样是应该写在笔记本上,然后自己多次揣摩,可能你觉得自己准备很好的东西,你写出来自己都知道了。 434 | 435 | 还有一个基本不可避免的吹牛题是:「你还有什么想问我的」?这个问题,针对不一样的人问的内容要不一样,对于管理者,可以问公司战略;对于技术人员,可以问公司的项目流程;对于行政 HR 人员,可以问福利和公司文化。如果确实没啥好问的,就委婉地表示自己之前通过其他渠道已经了解了自己想知道的,切忌不要直接说没有。 436 | 437 | - 如何和 HR 谈薪资 438 | 439 | 和 HR 谈薪资是一个必不可少的过程。如果手里已经有 offer 或者前面感觉自己面试表现不错,建议谈的更加有底气。一般要价比自己期望的高 15%,不要说范围价格,不要说范围价格!虽然我知道你心里比预期低 10% 也可以接受。 440 | 441 | - 手里有多个 Offer 怎么选 442 | 443 | 面对多个 Offer,大多数都在同一个城市,这时候需要把眼光看的更加长远,**一定要把眼光看得更加长远!**选择更有发展潜力的公司,而不要一味地追求薪资。这一块我其实深有感触,所以前段时间,我放弃了高我现在薪资一半的公司,选择了现在的公司,其实原因很简单,一面的面试官把我点燃了。 444 | 445 | 还有一点是,一定要注意距离,最好优先选择距离家近的公司,租房的可以把家搬过去。每天花在地铁上的时间,不如在公司多做点事儿。 446 | 447 | 对于校招的同学,肯定会有些 Offer 来自不同城市。不考虑到家距离的,建议优先北京、上海、深圳、成都。再是广州、杭州和一些其他城市。 448 | 449 | ### 没有技术深度的烦恼 450 | 451 | 当我们是初级工程师的时候,最希望的就是有丰富的项目经验,好把自己苍白干瘪的简历填的炫丽饱满。然而随着时间的积累,简历上的项目是挺「饱满」的了,但我们只看「外表」的行为造成了自己另一个困境:看似很资深,其实又没有做过什么有难度的事情,工作了十年可能只是 1 年的工作经验用了 9 次。 452 | 453 | 我之前就面过一位从 09 年开始就做 Android 的人,我算算啊,到现在应该是快干了 10 年了。光项目,简历上都写了 10 多个,整整 4 页的简历。我们抛开简历没有对项目经历进行精挑细选的毛病,我仔细阅读简历之后,发现简历中没有任何深入的地方。虽然写的很有技术,但却只是在使用 API 的程度而已,有些解决问题的方式很有技巧,但还不成体系。 454 | 455 | 这位读者待过 4 家公司,其中两家都是知名互联网企业。但假设我是公司的面试官,我可能会对他表示遗憾,心疼他没有选择更加深入的研究和拓展。 456 | 457 | 有知名互联网企业做背书,有将近 10 年的开发经验,但我总感觉还差了点什么。 458 | 459 | 假如我是公司的 Leader,我会觉得这样的一位面试者,当然会比一般的 Android 求职者技术更好,但性价比确实太低了。 460 | 461 | 没有技术深度是 Android 程序员的一种常态。因为很多工作,很多人从事的项目并不需要多少技术深度,即使你有深度,你也有可能发现用不上,对于大多数人,合乎理性的做法不是去追求技术深度,而是够用,能满足需求就可以了。 462 | 463 | 但转到个人的话就不一样了,在技术上你需要够用,但是在某方面上你需要有一定的深度,以突出你自己的学习理解和运用的能力,而且这个能力是要有成功案例来背书。 464 | 465 | 特别是当你成为一个资深的工程师的时候,很多公司并不希望你还是那样平庸,没有深度。虽然你会纳闷,我就算有深度你们也不一定用得上呀?然而到了这个级别的人需求量并不像初中级开发那么多,公司更理性和稳妥的做法是选择有深度的人,不是吗? --------------------------------------------------------------------------------