├── Git └── 最近从0学习Git,总结了这份Git命令宝典.md ├── Java ├── Java并发包中最重要的几个同步类,你敢不学?.md ├── 一句话撸完重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁.md ├── 并发的核心:CAS是什么?Java8是如何优化CAS的?.md ├── 求求你规范下你的代码风格.md ├── 线程安全(上):彻底搞懂volatile关键字.md └── 线程安全(下):彻底搞懂synchronized(从偏向锁到重量级锁).md ├── MySQL ├── 我去,这两个小技巧,让我的SQL语句不仅躲了坑,还提升了1000倍.md ├── 腾讯面试:一条SQL语句执行得很慢的原因有哪些?.md └── 面试小知识:MySQL索引相关.md ├── README.md ├── 个人经历与感想 ├── 2020第一篇原创:我是如何让自己变的更加优秀的.md ├── 写公众号15个月以来,这一路上的学习与收获.md ├── 我的2019.md ├── 玩公众号写文章一年多以来,我经历了被喷被拉黑被赞美,我酸了.md ├── 秋招结束了,普普通通,我的三年大学.md └── 说一说我最近的日常,学习与思考.md ├── 写给在校生的经验总结 ├── 历经两个月的秋招,结束了,谈谈春秋招中一些重要的知识点吧.md ├── 嗯,春招两次腾讯面试都挂二面,分享下我失败+傻傻的面试经历.md ├── 学了四年编程,这些优质的学习工具,网站与资料,推荐给你们.md ├── 想了很久,这是一份适合普通大众的学习路线.md ├── 有必要说一说即将到来的春招(经历+重要性+如何准备).md ├── 核心整理:那些让你起飞的计算机基础知识:学什么,怎么学?.md └── 讲一讲当时春秋招时做过的项目以及推荐几个项目+面试视频.md ├── 学操作系统 └── 记一次面试:进程之间究竟有哪些通信方式?如何通信?.md ├── 学数据结构 ├── 二叉堆是什么鬼?.md ├── 以后有面试官问你跳跃表,你就把这篇文章扔给他.md ├── 堆排序是什么鬼?.md ├── 漫画:什么是avl树?.md ├── 腾讯面试题:有了二叉查找树,平衡树为啥还需要红黑树?.md └── 高频面试:什么是B树?为啥文件索引要用B树而不用二叉查找树?.md ├── 学算法 ├── 再现校招算法面试现场 │ ├── 一道阿里笔试题:我是如何用一行代码解决约瑟夫环问题的.md │ ├── 前缀和的应用,从一道网易笔试题说起.md │ ├── 只用2GB内存从20亿,40亿,80亿个整数中找到出现次数最多的数.md │ ├── 记一次手撕算法面试:字节跳动的面试官把我四连击了.md │ ├── 记一次阿里面试:面试挂在了LRU缓存算法设计上.md │ ├── 记一道字节跳动的算法面试题:变形的链表反转.md │ ├── 面试被虐:记一次shopee算法面试题:最小栈的最优解.md │ └── 面试被虐:说说游戏中的敏感词过滤是如何实现的?.md ├── 学习算法经验分享 │ ├── leetcode刷500道题,笔试:面试稳过吗?.md │ ├── 作为一个小白,算法该如何学习?.md │ └── 程序员必须掌握的算法有哪些?.md ├── 学二分查找 │ ├── 两道看似简单的算法题.md │ ├── 二分查找你确定真的会?生活中还能用来设计骗局?.md │ └── 二分法题型小结.md ├── 学动态规划 │ ├── 动态规划很难?DP连刷40道题,我总结出了这些套路.md │ ├── 动态规划的优化.md │ ├── 训练1:详解三道一维的动态规划算法题.md │ └── 训练2:详解leetcode221题:最大正方形.md ├── 学字符串匹配算法 │ ├── 图解字符串匹配KMP算法.md │ └── 字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?.md ├── 学递归 │ ├── 为什么你学不会递归?告别递归,谈谈我的一些经验.md │ ├── 训练1:在两个长度相等的排序数组中找到上中位数.md │ ├── 训练2:求两个有序数组的第K小数.md │ └── 训练3:求两个有序数组的中位数(论思维转换的重要性).md ├── 必学排序算法 │ ├── 别翻了,程序员必学十大经典排序算法,看这篇就够了.md │ ├── 漫画:为什么说O(n)复杂度的基数排序没有快速排序快?.md │ ├── 漫画:外部排序:如果用2GB内存给20亿个整数排序?(其实这也是一个常考面试题).md │ └── 漫画:求求你不要再问我快速排序了.md ├── 必学算法思维与技巧 │ ├── 位运算装逼指南.md │ ├── 分享一道解法巧妙的算法题.md │ ├── 寻找缺失的整数.md │ ├── 帅地给你总结了这份高频地算法解题技巧,助你更快速着解题!.md │ ├── 最求极致:我是如何把easy级别的算法题做成hard级别的.md │ ├── 牛逼!一行代码居然能解决这么多曾经困扰我半天的算法题.md │ ├── 算法数据结构中有哪些奇技淫巧?.md │ ├── 阶乘很简单?说实话,这几道阶乘相关面试题你还真不一定懂.md │ └── 面试官,求求你不要问我这么简单但又刁难的算法题了.md ├── 搞定二叉树 │ ├── 二叉搜索树的后序遍历序列.md │ ├── 二叉树的中序遍历(非递归版).md │ ├── 二叉树的先序遍历(非递归版).md │ ├── 二叉树的后序遍历(非递归版).md │ ├── 二叉树的子结构.md │ ├── 二叉树的构建.md │ ├── 二叉树的镜像.md │ ├── 从上往下打印二叉树.md │ └── 重建二叉树.md └── 搞定链表 │ ├── 链表训练1:删除单链表的第K个节点.md │ ├── 链表训练2:删除单链表的中间节点.md │ ├── 链表训练3:如何优雅着反转单链表.md │ ├── 链表训练4:环形单链表约瑟夫问题.md │ ├── 链表训练5:三种方法带你优雅判断回文链表.md │ ├── 链表训练6:将单向链表按某值划分成左边小,中间相等,右边大的形式.md │ ├── 链表训练7:复制含有随机指针节点的链表.md │ ├── 链表训练8:将单链表的每K个节点之间逆序.md │ └── 链表训练9:将搜索二叉树转换成双向链表.md ├── 学计算机网络 ├── 一文读懂一台计算机是如何把数据发送给另一台计算机的.md ├── 什么是TCP流量控制.md ├── 什么是广播路由算法?如何解决广播风暴?.md ├── 什么是拥塞控制?.md ├── 关于三次握手与四次挥手面试官想考我们什么?.md ├── 图解:两天完全陌生的主机是如何办到数据的正确交付的?.md ├── 数字签名是什么.md ├── 漫话:什么是https?这应该是全网把https讲的最好的一篇文章了.md ├── 电脑的ip是怎么来的?我又没有配置过.md └── 电路交换与分组交换的区别.md └── 看过的优质书籍推荐 ├── 大学四年,看过的优质书籍推荐.md └── 算法与计算机基础,有哪些值得阅读的书籍?.md /Java/一句话撸完重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁.md: -------------------------------------------------------------------------------- 1 | 重量级锁?自旋锁?自适应自旋锁?轻量级锁?偏向锁?悲观锁?乐观锁?执行一个方法咋这么辛苦,到处都是锁。 2 | 3 | 今天这篇文章,给大家普及下这些锁究竟是啥,他们的由来,他们之间有啥关系,有啥区别。 4 | 5 | #### 重量级锁 6 | 7 | 如果你学过多线程,那么你肯定知道**锁**这个东西,至于为什么需要锁,我就不给你普及了,就当做你是已经懂的了。 8 | 9 | 我们知道,我们要进入一个**同步、线程安全**的方法时,是需要先获得这个方法的锁的,退出这个方法时,则会释放锁。如果获取不到这个锁的话,意味着有别的线程在执行这个方法,这时我们就会**马上进入阻塞的状态**,等待那个持有锁的线程释放锁,然后再把我们从**阻塞的状态唤醒**,我们再去获取这个方法的锁。 10 | 11 | 这种获取不到锁就**马上**进入阻塞状态的锁,我们称之为**重量级锁**。 12 | 13 | #### 自旋锁 14 | 15 | 我们知道,线程从**运行态**进入**阻塞态**这个过程,是非常耗时的,因为不仅需要保存线程此时的执行状态,上下文等数据,还涉及到**用户态**到**内核态**的转换。当然,把线程从阻塞态唤醒也是一样,也是非常消耗时间的。 16 | 17 | 刚才我说线程拿不到锁,就会**马上**进入阻塞状态,然而现实是,它虽然这一刻拿不到锁,可能在下 0.0001 秒,就有其他线程把这个锁释放了。如果它慢0.0001秒来拿这个锁的话,可能就可以顺利拿到了,不需要经历**阻塞/唤醒**这个花时间的过程了。 18 | 19 | 然而重量级锁就是这么坑,它就是不肯等待一下,一拿不到就是要马上进入阻塞状态。为了解决这个问题,我们引入了另外一种愿意等待一段时间的锁 --- **自旋锁**。 20 | 21 | 自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。怎么等呢?这个就类似于线程在那里做**空循环**,如果循环一定的次数还拿不到锁,那么它才会进入阻塞的状态。 22 | 23 | 至于是循环等待几次,这个是可以人为指定一个数字的。 24 | 25 | #### 自适应自旋锁 26 | 27 | 上面我们说的自旋锁,每个线程循环等待的次数都是一样的,例如我设置为 100次的话,那么线程在空循环 100 次之后还没拿到锁,就会进入阻塞状态了。 28 | 29 | 而自适应自旋锁就牛逼了,它不需要我们人为指定循环几次,它自己本身会进行判断要循环几次,而且每个线程可能循环的次数也是不一样的。而之所以这样做,主要是我们觉得,如果一个线程在不久前拿到过这个锁,或者它之前经常拿到过这个锁,那么我们认为**它再次拿到锁的几率非常大**,所以循环的次数会多一些。 30 | 31 | 而如果有些线程从来就没有拿到过这个锁,或者说,平时很少拿到,那么我们认为,它再次拿到的概率是比较小的,所以我们就让它循环的次数少一些。因为你在那里做空循环是很消耗 CPU 的。 32 | 33 | 所以这种能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为**自适应自旋锁**。 34 | 35 | #### 轻量级锁 36 | 37 | 上面我们介绍的三种锁:重量级、自旋锁和自适应自旋锁,他们都有一个特点,就是进入一个方法的时候,就会加上锁,退出一个方法的时候,也就释放对应的锁。 38 | 39 | 之所以要加锁,是因为他们害怕自己在这个方法执行的时候,被别人偷偷进来了,所以只能加锁,防止其他线程进来。这就相当于,每次离开自己的房间,都要锁上门,人回来了再把锁解开。 40 | 41 | 这实在是太麻烦了,如果根本就没有线程来和他们竞争锁,那他们不是白白上锁了?要知道,**加锁**这个过程是需要操作系统这个大佬来帮忙的,是很消耗时间的,。为了解决这种**动不动就加锁**带来的开销,轻量级锁出现了。 42 | 43 | 轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要**做一个标记**就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用**CAS**机制,把这个方法的状态标记为**已经有人在执行**,退出这个方法时,在把这个状态改为了**没有人在执行**了。 44 | 45 | > 之所以要用CAS机制来改变状态,是因为我们对这个状态的改变,不是一个原子性操作,所以需要CAS机制来保证操作的原子性。不知道CAS的可以看这篇文章:[并发的核心:CAS 是什么?Java8是如何优化 CAS 的?](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247485301&idx=1&sn=c433c39a741c941a4d38d8944704b342&chksm=ce404ca1f937c5b79e2922051b36f76f9d7d669442071a3cc8489116f422331095d45cb42523&token=960773791&lang=zh_CN#rd)。 46 | 47 | 显然,**比起加锁操作,这个采用CAS来改变状态的操作,花销就小多了。** 48 | 49 | 然而可能会说,没人来竞争的这种想法,那是你说的而已,那如果万一有人来竞争说呢?也就是说,当一个线程来执行一个方法的时候,方法里面已经有人在执行了。 50 | 51 | 如果真的遇到了竞争,我们就会认为**轻量级锁**已经不适合了,我们就会把轻量级锁升级为重量级锁了。 52 | 53 | 所以轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是**错开时间**来获取锁的情况。 54 | 55 | #### 偏向锁 56 | 57 | 偏向锁就更加牛逼了,我们已经觉得轻量级锁已经够**轻**,然而偏向锁更加**省事**,偏向锁认为,你轻量级锁每次进入一个方法都需要用CAS来改变状态,退出也需要改变,多麻烦。 58 | 59 | 偏向锁认为,其实对于一个方法,是很少有两个线程来执行的,搞来搞去,其实也就一个线程在执行这个方法而已,相当于**单线程**的情况,居然是单线程,那就没必要加锁了。 60 | 61 | 不过毕竟实际情况的多线程,单线程只是自己认为的而已了,所以呢,偏向锁进入一个方法的时候是这样处理的:如果这个方法没有人进来过,那么一个线程首次进入这个方法的时候,会采用CAS机制,把这个方法标记为有人在执行了,和轻量级锁加锁有点类似,并且也会把该线程的 ID 也记录进去,相当于记录了哪个线程在执行。 62 | 63 | 然后,但这个线程退出这个方法的时候,它不会改变这个方法的状态,而是直接退出来,懒的去改,因为它认为除了自己这个线程之外,其他线程并不会来执行这个方法。 64 | 65 | 然后当这个线程想要再次进入这个方法的时候,会判断一下这个方法的状态,如果这个方法已经被标记为**有人在执行**了,并且线程的ID是自己,那么它就直接进入这个方法执行,啥也不用做 66 | 67 | 你看,多方便,第一次进入需要CAS机制来设置,以后进出就啥也不用干了,直接进入退出。 68 | 69 | 然而,现实总是残酷的,毕竟实际情况还是多线程,所以万一有其他线程来进入这个方法呢?如果真的出现这种情况,其他线程一看这个方法的ID不是自己,这个时候说明,至少有两个线程要来执行这个方法论,这意味着**偏向锁已经不适用了**,这个时候就会从偏向锁升级为轻量级锁。 70 | 71 | 所以呢,偏向锁适用于那种,始终只有一个线程在执行一个方法的情况哦。 72 | 73 | > 这里我作下说明,为了方便大家理解,我在将轻量级锁和偏向锁的时候,其实是简化了很多的,不然的话会涉及到**对象的内部结构、布局**,我觉得把那些扯出来,你们可能要晕了,所以我大致讲了他们的原理。 74 | 75 | ####悲观锁和乐观锁 76 | 77 | 最开始我们说的三种锁,重量级锁、自旋锁和自适应自旋锁,进入方法之前,就一定要先加一个锁,这种我们为称之为**悲观锁**。悲观锁总认为,如果不事先加锁的话,就会出事,这种想法确实悲观了点,这估计就是**悲观锁**的来源了。 78 | 79 | 而乐观锁却相反,认为不加锁也没事,我们可以先不加锁,如果出现了冲突,我们在想办法解决,例如 CAS 机制,上面说的轻量级锁,就是乐观锁的。不会马上加锁,而是等待真的出现了冲突,在想办法解决。不知道 CAS 机制的,可以看我之前写的这篇文章哦:[并发的核心:CAS 是什么?Java8是如何优化 CAS 的?](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247485301&idx=1&sn=c433c39a741c941a4d38d8944704b342&chksm=ce404ca1f937c5b79e2922051b36f76f9d7d669442071a3cc8489116f422331095d45cb42523&token=960773791&lang=zh_CN#rd)。 80 | 81 | #### 总结 82 | 83 | 到这里也大致写完了,简单介绍普及了一下,重点的大家要理解他们的由来,原理。每一种锁都有他们的应用以及各自的优缺点,如果有机会,我再给大家说说他们各自的应用场景,优缺点,这个面试的时候,好像也会被经常到,今天先写到这里勒。 84 | 85 | 大家可以说说这些锁的优缺点哦,例如与重量级锁相比,自旋锁容量导致什么问题的发生?悲观锁和乐观锁的比较呢?大家也可以评论区说说勒,这些是一定要搞懂的哦。 86 | 87 | 88 | 89 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 90 | 91 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /Java/并发的核心:CAS是什么?Java8是如何优化CAS的?.md: -------------------------------------------------------------------------------- 1 | 大家可能都听说说 Java 中的并发包,如果想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,因为 CAS 可以说是并发包的底层实现原理。 2 | 3 | 今天就带大家读懂 CAS 是如何保证操作的原子性的,以及 Java8 对 CAS 进行了哪些优化。 4 | 5 | #### synchronized:大材小用 6 | 7 | 我们先来看几行代码: 8 | 9 | ```java 10 | public class CASTest { 11 | static int i = 0; 12 | 13 | public static void increment() { 14 | i++; 15 | } 16 | } 17 | ``` 18 | 19 | 假如有100个线程同时调用 increment() 方法对 i 进行自增操作,i 的结果会是 100 吗? 20 | 21 | 学会多线程的同学应该都知道,这个方法是线程不安全的,由于 i++ 不是一个**原子操作**,所以是很难得到 100 的。 22 | 23 | > 这里稍微解释下为啥会得不到 100(知道的可直接跳过), i++ 这个操作,计算机需要分成三步来执行。 24 | 1、读取 i 的值。 25 | 2、把 i 加 1. 26 | 3、把 最终 i 的结果写入内存之中。所以,假如线程 A 读取了 i 的值为 i = 0,这个时候线程 B 也读取了 i 的值 i = 0。接着 A把 i 加 1,然后写入内存,此时 i = 1。紧接着,B也把 i 加 1,此时线程B中的 i = 1,然后线程 B 把 i 写入内存,此时内存中的 i = 1。也就是说,线程 A, B 都对 i 进行了自增,但最终的结果却是 1,不是 2. 27 | 28 | 那该怎么办呢?解决的策略一般都是给这个方法加个锁,如下 29 | 30 | ```java 31 | public class CASTest { 32 | static int i = 0; 33 | 34 | public synchronized static void increment() { 35 | i++; 36 | } 37 | } 38 | ``` 39 | 加了 synchronized 之后,就最多只能有一个线程能够进入这个 increment() 方法了。这样,就不会出现线程不安全了。不懂 synchronized 的可以看我这篇文章:[彻底搞懂synchronized(从偏向锁到重量级锁)](https://mp.weixin.qq.com/s/qDvd8MYAzBXOsWgzwIbNMA) 40 | 41 | 然而,一个简简单单的自增操作,就加了 synchronized 进行同步,好像有点大材小用的感觉,加了 synchronized 关键词之后,当有很多线程去竞争 increment 这个方法的时候,拿不到锁的方法是会被**阻塞**在方法外面的,最后再来唤醒他们,而阻塞/唤醒这些操作,是非常消耗时间的。 42 | 43 | > 这里可能有人会说,synchronized 到了JDK1.6之后不是做了很多优化吗?是的,确实做了很多优化,增加了偏向锁、轻量级锁等, 但是,就算增加了这些,当很多线程来竞争的时候,开销依然很多,不信你看我另外一篇文章的介绍:[彻底搞懂synchronized(从偏向锁到重量级锁)](https://mp.weixin.qq.com/s/qDvd8MYAzBXOsWgzwIbNMA) 44 | 45 | #### CAS :这种小事交给我 46 | 47 | 那有没有其他方法来代替 synchronized 对方法的加锁,并且保证 increment() 方法是线程安全呢? 48 | 49 | 大家看一下,如果我采用下面这种方式,能否保证 increment 是线程安全的呢?步骤如下: 50 | 51 | 1、线程从内存中读取 i 的值,假如此时 i 的值为 0,我们把这个值称为 k 吧,即此时 k = 0。 52 | 53 | 2、令 j = k + 1。 54 | 55 | 3、用 k 的值与内存中i的值相比,如果相等,这意味着没有其他线程修改过 i 的值,我们就把 j(此时为1) 的值写入内存;如果不相等(意味着i的值被其他线程修改过),我们就不把j的值写入内存,而是重新跳回步骤 1,继续这三个操作。 56 | 57 | 翻译成代码的话就是这样: 58 | 59 | ```java 60 | public static void increment() { 61 | do{ 62 | int k = i; 63 | int j = k + 1; 64 | }while (compareAndSet(i, k, j)) 65 | } 66 | ``` 67 | 如果你去模拟一下,就会发现,这样写是线程安全的。 68 | 69 | 这里可能有人会说,第三步的 compareAndSet 这个操作不仅要读取内存,还干了比较、写入内存等操作,,,这一步本身就是线程不安全的啊? 70 | 71 | 如果你能想到这个,说明你是真的有去思考、模拟这个过程,不过我想要告诉你的是,这个 compareAndSet 操作,他其实只对应操作系统的**一条硬件操作指令**,尽管看似有很多操作在里面,但操作系统能够保证他是原子执行的。 72 | 73 | 对于一条英文单词很长的指令,我们都喜欢用它的简称来称呼他,所以,我们就把 compareAndSet 称为 **CAS** 吧。 74 | 75 | 所以,采用 CAS 这种机制的写法也是线程安全的,通过这种方式,可以说是不存在锁的竞争,也不存在阻塞等事情的发生,可以让程序执行的更好。 76 | 77 | 在 Java 中,也是提供了这种 CAS 的原子类,例如: 78 | 79 | 1. AtomicBoolean 80 | 2. AtomicInteger 81 | 3. AtomicLong 82 | 4. AtomicReference 83 | 84 | 具体如何使用呢?我就以上面那个例子进行改版吧,代码如下: 85 | 86 | ```java 87 | public class CASTest { 88 | static AtomicInteger i = new AtomicInteger(0); 89 | 90 | public static void increment() { 91 | // 自增 1并返回之后的结果 92 | i.incrementAndGet(); 93 | } 94 | } 95 | ``` 96 | 97 | #### CAS:谁偷偷更改了我的值 98 | 99 | 虽然这种 CAS 的机制能够保证increment() 方法,但依然有一些问题,例如,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。而这,就是我们平常说的**ABA问题**。 100 | 101 | 对于基本类型的值来说,这种把**数字改变了在改回原来的值**是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。 102 | 103 | #### 来个版本控制吧 104 | 105 | 为了解决这个 ABA 的问题,我们可以引入版本控制,例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。 106 | 107 | #### Java8 对 CAS 的优化。 108 | 109 | 由于采用这种 CAS 机制是没有对方法进行加锁的,所以,所有的线程都可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i 的值老是被修改了,所以线程又到回到第一步继续重头再来。 110 | 111 | 而这就会导致一个问题:由于线程太密集了,太多人想要修改 i 的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源。 112 | 113 | 为了解决这个问题,Java8 引入了一个 cell[] 数组,它的工作机制是这样的:假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。 114 | 115 | 但是,如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操作。 116 | 117 | 当然,我这里只是举个例子来说明 Java8 对 CAS 优化的大致原理,具体的大家有兴趣可以去看源码,或者去搜索对应的文章哦。 118 | 119 | #### 总结 120 | 121 | 理解 CAS 的原理还是非常重要的,它是 AQS 的基石,而 AQS 又是并发框架的基石,接下来有时间的话,还会写一篇 AQS 的文章。 122 | 123 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 124 | 125 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /Java/求求你规范下你的代码风格.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 代码风格并不影响程序的运行,也不会给你的程序带来潜在的危险。但一段好的代码风格,能让人阅读起来特别舒服,特别的阅读别人代码的时候,有些人的代码可以说是完全不注意空格、缩进、大小写的,看起来特别不舒服。 4 | 5 | 给人好的印象,从一段代码风格开始。今天我总结了几个最常用到的代码规范、可以说你每时每刻都会接触到的了,也比较简单,如果你平时没有按这些规范写的话,建议慢慢改过来。 6 | 7 | #### 1. 类名 8 | 9 | 类名采用**大驼峰**的命名形式,所谓大驼峰就是首字母大写,例如UpperCameCase。 10 | 11 | 抽象类命名使用 Abstract 或 Base 开头;异常类使用 Exception 结尾;测试类命名以测试的类名开始,以 Test 结尾。 12 | 13 | 枚举类名带上 Enum 作为后缀,**枚举成员**名称需要大写,**单词间**用下画线隔开。 14 | 15 | #### 2. 包名 16 | 17 | 包名统一使用小写,**点分隔符**之间有且仅有一个自然语义的英语单词。一般单词使用单数形式,但是类名如果有复数含义的话,则可以使用复数形式。 18 | 19 | #### 3. 变量 20 | 21 | 变量可分为**不可变量(常量)**和**可变变量**。 22 | 23 | **(1). 常量** 24 | 25 | 在 Java 中,常量一般指 final 关键字修饰的变量。 26 | 27 | 1、**全局常量**和**类内常量**的命名采用字母全部大写,单词之间加**下画线**的方式。 28 | 29 | 所谓全局常量指的是**类的公开静态属性**,使用 public static final 修饰;类内常量指的是**私有静态属性**,使用 private static final 修饰。 30 | 31 | 2、**局部常量**则采用**小驼峰**的形式。所谓局部常量指的是方法内的常量。 32 | 33 | 34 | 35 | 例子展示: 36 | 37 | ```java 38 | public class Constant { 39 | //全局常量 40 | public static final String MY_NAME = "shuaidi"; 41 | //类内常量 42 | private static final String MY_SEX = "男"; 43 | 44 | public void tt(){ 45 | //局部常量 46 | final String myName = "shuaidi"; 47 | } 48 | } 49 | ``` 50 | 51 | **(2). 可变变量** 52 | 53 | 可变变量一般常用**小驼峰**的命名形式,如 myName ,小驼峰和大驼峰的区别就是,小驼峰首字母小写,而大驼峰首字母大写。不过需要注意的是,针对**布尔类型**的变量,在命名的时候,不要用 **is** 做前缀,否则部分框架在解析的时候会引起序列化错误。 54 | 55 | 例如标识是否删除的成员变量 Boolean isDeleted, 它的 getter 方法也是 isDeleted(),框架在反向解析的时候,会误认为对应的属性名称为 deleted,从而引起错误。 56 | 57 | #### 4. 空格 58 | 59 | 我发现很多人在写代码的时候,在运算符、赋值、参数等之间很少使用**空格**来隔开各种元素之间的距离,例如 60 | 61 | ```java 62 | //错误例子示范,注:里面的代码之间没啥联系,都是随意给出的。 63 | int a=1 64 | int b=a==1?1:2; 65 | if(a==1&&b==2){ 66 | print(a,b);//调用打印函数 67 | }else{ 68 | 69 | } 70 | public static void print(int a,int b){ 71 | System.out.printf(a+b); 72 | } 73 | ``` 74 | 75 | 像上面的这个例子中,就是几乎没用到空格的,代码看起来很紧,反正我看起来是很不舒服,特别的当代码很多的时候。 76 | 77 | 下面是我给的关于加空格的几点建议: 78 | 79 | 1、二目、三目运算符的左右两边都应该加一个空格。 80 | 81 | 2、注释的双斜线与注释内容之间有且仅有一个空格。 82 | 83 | 3、方法参数在定义和传入参数时,多个参数逗号后边都应该加空格。 84 | 85 | 4、如果大括号为空,则简洁地写成{}即可,大括号中间无须换行和加空格。 86 | 87 | 5、左右小括号与括号内部的相邻字符之间不要出现空格。 88 | 89 | 6、左大括号前需要加空格。 90 | 91 | 所以,修改后如下: 92 | 93 | ```java 94 | int a = 1; 95 | int b = a == 1 ? 1 : 2; 96 | if(a == 1 && b == 2) { 97 | print(a, b); 98 | } else {} 99 | 100 | public static void print(int a, int b) { 101 | System.out.printf(a + b);// 调用打印函数 102 | } 103 | ``` 104 | 105 | 这样看起来舒服多了,特别是在括号内参数多的的时候。 106 | 107 | #### 5. 控制语句 108 | 109 | 控制语句可以说是最容易出现 bug 的地方,所以代码风格的约束极为重要,而不是天马行空地乱跳。因此,控制语句必须遵循如下约定: 110 | 111 | 1、在 if, for, while, do-while 等语句中必须使用大括号,即使只有一行代码,也应该加上大括号。例如: 112 | 113 | ```java 114 | int sum = 0; 115 | for(int i = 0; i < 10; i++) { 116 | sum += i;// 尽管只有一行/ 117 | } 118 | ``` 119 | 120 | 2、在条件表达式中不允许出现赋值操作,也不允许在判断表达式中出现复杂的所及组合。例如: 121 | 122 | ```java 123 | //复杂的多级组合 124 | if((file.open(fileName, "w") != null) && (...) && (...)) { 125 | dosomething(); 126 | } 127 | ``` 128 | 129 | 争取的做法应该是将复杂的多级运算赋值给一个具有业务含义的布尔变量。例如: 130 | 131 | ```java 132 | boolean existed = (file.open(fileName, "w") != null) && (...) && (...); 133 | 134 | if(existed) { 135 | dosomething(); 136 | } 137 | 138 | ``` 139 | 140 | 3、多层嵌套不能超过三层。 141 | 142 | #### 6. 关于缩进与空格 143 | 144 | 一个缩进的距离等于四个空格的距离,但究竟是要使用 Tab 缩进来调距离还是用四个空格代替一个缩进来调距离,这个貌似存在争议,有些大佬建议用 Tab 键,有些大佬建议用空格。我在《码出高效Java开发手册》里,本书的作者是推荐**四个空格缩进**,禁止使用Tab键。 145 | 146 | 当然,你在使用IDE的时候,当你换行时,很多编辑器是会帮你自动缩进的,大多数IDE都是默认四个空格来缩进。 147 | 148 | 不过很多 IDE 工具提供了 Tab 键与空格之间的快速转换设置。例如对于 IDEA 这个工具,要设置 Tab 键为四个空格时,可以在设置那里勾选 Use tab character(setting->editor->Code Style->选择你想编辑的语言);而在 Eclipse 中,就得勾选 Insert spaces for tabs。 149 | 150 | 上面讲的这几点,可以说是无时无刻都在和他们打交道的了,其他比较少接触的我就不列出来了。你平时有木按照这种风格来写呢? 151 | 152 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 153 | 154 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /MySQL/我去,这两个小技巧,让我的SQL语句不仅躲了坑,还提升了1000倍.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 本次来讲解与 SQL 查询有关的两个小知识点,掌握这些知识点,能够让你避免踩坑以及提高查询效率。 4 | 5 | #### 1、允许字段的值为 null,往往会引发灾难 6 | 7 | 首先,先准备点数据,后面好演示 8 | 9 | ```sql 10 | create table animal( 11 | id int, 12 | name char(20), 13 | index(id) 14 | )engine=innodb; 15 | ``` 16 | > index(id) 表示给 id 这个字段创建索引,并且 id 和 name 都允许为 null。 17 | 18 | 接着插入4条数据,其中最后一条数据的 id 为。 19 | ```sql 20 | insert into animal(id, name) values(1, '猫'); 21 | insert into animal(id, name) values(2, '狗'); 22 | insert into animal(id, name) values(3, '猪'); 23 | insert into animal(id, name) values(null, '无名动物'); 24 | ``` 25 | 此时表中的数据为 26 | 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be43930d59448b?w=375&h=174&f=png&s=9357) 29 | 30 | 这时我们查询表中 id != 1 的动物有哪些 31 | 32 | ```sql 33 | select * from animal where id != 1; 34 | ``` 35 | 结果如下: 36 | 37 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be43ae96f571ad?w=520&h=143&f=png&s=7962) 38 | 39 | 此时我们只找到了两行数据,按道理应该是三行的,**但是 id = null 的这一行居然没有被匹配到,**,可能大家听说过,null 与任何 40 | 其他值都不相等,按道理 null != 1 是成立的话,然而现实很残酷,它就是不会被匹配到。 41 | 42 | 所以,**坚决不允许字段的值为 null,否则可能会出现与预期不符合的结果。** 43 | 44 | 反正我之前有踩过这个坑,不知道大家踩过木有? 45 | 46 | 但是万一有人设置了允许为 null 值怎么办?如果真的这样的话,对于 != 的查找,后面可以多加一个 **or id is null** 的子句(注意,是 is null,不是 = null,因为 id = null 也不会匹配到值为 null 的行)。即 47 | ```sql 48 | select * from animal where id != 1 or id is null; 49 | ``` 50 | 结果如下: 51 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be442d7f9f0616?w=641&h=164&f=png&s=10533) 52 | 53 | #### 2、尽可能用 union 来代替 or 54 | 55 | (1)、刚才我们给 id 这个字段建立了索引,如果我们来进行等值操作的话,一般会走索引操作,不信你看: 56 | 57 | ```sql 58 | explain select * from animal where id = 1; 59 | ``` 60 | 结果如下: 61 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be4e52ca640708?w=1183&h=117&f=png&s=17181) 62 | 63 | 通过执行计划可以看见,id 上的等值查找能够走索引查询(估计在你的意料之中),其中 64 | 65 | type = ref :表示走非唯一索引 66 | rows = 1 :预测扫描一行 67 | 68 | (2)、那 id is null 会走索引吗?答是会的,如图 69 | 70 | ```sql 71 | explain select * from animal where id is null; 72 | ``` 73 | 74 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be4e675140fa01?w=1330&h=118&f=png&s=17592) 75 | 其中 76 | 77 | type = ref :表示走非唯一索引 78 | rows = 1 :预测扫描一行 79 | 80 | (3)、那么问题来了,那如果我们要找出 id = 1 或者 id = null 的动物,我们可能会用 or 语句来连接,即 81 | ```sql 82 | select * from animal where id = 1 or id is null; 83 | ``` 84 | **那么这条语句会走索引吗?** 85 | 86 | 有没有走索引,看执行计划就知道了,如图 87 | 88 | ```sql 89 | explain select * from animal where id = 1 or id is null; 90 | ``` 91 | 92 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be4e79c7d26811?w=1219&h=127&f=png&s=17327) 93 | 94 | 其中: 95 | 96 | ref = ALL:表示全表扫描 97 | rows = 4 :预测扫描4行(而我们整个表就只有4行记录) 98 | 99 | 通过执行计划可以看出,使用 or 是很有可能不走索引的,这将会大大降低查询的速率,所以一般不建议使用 or 子句来连接条件。 100 | 101 | **那么该如何解决?** 102 | 103 | 其实可以用 **union** 来取代 or,即如下: 104 | 105 | ```sql 106 | select * from animal where id = 1 union select * from animal where id is null. 107 | ``` 108 | 109 | ![](https://user-gold-cdn.xitu.io/2019/7/12/16be4eeabaabc9a0?w=1379&h=160&f=png&s=26167) 110 | 111 | 此时就会分别走两次索引,找出所有 id = 1 和 所有 id = null 的行,然后再用一个临时表来存放最终的结果,最后再扫描临时表。 112 | 113 | #### 3、总结 114 | 115 | 1、定义表的时候,尽量不允许字段值为 null,可以用 default 设置默认值。 116 | 117 | 2、尽量用 union 来代替 or,避免查询没有走索引。 118 | 119 | 3、注意,用 id = null 的等值查询,也是不会匹配到值为 null 的行的,而是应该用 id is null。 120 | 121 | 也欢迎大家说一说自己踩过的坑。 122 | 123 | 124 | 125 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 126 | 127 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /MySQL/腾讯面试:一条SQL语句执行得很慢的原因有哪些?.md: -------------------------------------------------------------------------------- 1 | 说实话,这个问题可以涉及到 MySQL 的很多核心知识,可以扯出一大堆,就像要考你计算机网络的知识时,问你“输入URL回车之后,究竟发生了什么”一样,看看你能说出多少了。 2 | 3 | 之前腾讯面试的实话,也问到这个问题了,不过答的很不好,之前没去想过相关原因,导致一时之间扯不出来。所以今天,我带大家来详细扯一下有哪些原因,相信你看完之后一定会有所收获,不然你打我。 4 | 5 | #### 开始装逼:分类讨论 6 | 7 | 一条 SQL 语句执行的很慢,那是每次执行都很慢呢?还是大多数情况下是正常的,偶尔出现很慢呢?所以我觉得,我们还得分以下两种情况来讨论。 8 | 9 | 1、大多数情况是正常的,只是偶尔会出现很慢的情况。 10 | 11 | 2、在数据量不变的情况下,这条SQL语句一直以来都执行的很慢。 12 | 13 | 14 | 15 | 针对这两种情况,我们来分析下可能是哪些原因导致的。 16 | 17 | #### 针对偶尔很慢的情况 18 | 19 | 一条 SQL 大多数情况正常,偶尔才能出现很慢的情况,针对这种情况,我觉得这条SQL语句的书写本身是没什么问题的,而是其他原因导致的,那会是什么原因呢? 20 | 21 | ##### 数据库在刷新脏页我也无奈啊 22 | 23 | 当我们要往数据库插入一条数据、或者要更新一条数据的时候,我们知道数据库会在**内存**中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到**磁盘**中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到**磁盘**中去。 24 | 25 | 不过,redo log 里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候 redo log 很快就会被写满了,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中去的,而这个时候,**就会导致我们平时正常的SQL语句突然执行的很慢**,所以说,数据库在在同步数据到磁盘的时候,就有可能导致我们的SQL语句执行的很慢了。 26 | 27 | ##### 拿不到锁我能怎么办 28 | 29 | 这个就比较容易想到了,我们要执行的这条语句,刚好这条语句涉及到的**表**,别人在用,并且加锁了,我们拿不到锁,只能慢慢等待别人释放锁了。或者,表没有加锁,但要使用到的某个一行被加锁了,这个时候,我也没办法啊。 30 | 31 | 如果要判断是否真的在等待锁,我们可以用 **show processlist**这个命令来查看当前的状态哦,这里我要提醒一下,有些命令最好记录一下,反正,我被问了好几个命令,都不知道怎么写,呵呵。 32 | 33 | 下来我们来访分析下第二种情况,我觉得第二种情况的分析才是最重要的 34 | 35 | #### 针对一直都这么慢的情况 36 | 37 | 如果在数据量一样大的情况下,这条 SQL 语句每次都执行的这么慢,那就就要好好考虑下你的 SQL 书写了,下面我们来分析下哪些原因会导致我们的 SQL 语句执行的很不理想。 38 | 39 | 我们先来假设我们有一个表,表里有下面两个字段,分别是主键 id,和两个普通字段 c 和 d。 40 | 41 | ```java 42 | mysql> CREATE TABLE `t` ( 43 | `id` int(11) NOT NULL, 44 | `c` int(11) DEFAULT NULL, 45 | `d` int(11) DEFAULT NULL, 46 | PRIMARY KEY (`id`) 47 | ) ENGINE=InnoDB; 48 | 49 | ``` 50 | 51 | ##### 扎心了,没用到索引 52 | 53 | 没有用上索引,我觉得这个原因是很多人都能想到的,例如你要查询这条语句 54 | 55 | ``` 56 | select * from t where 100 这里我声明一下,系统判断是否走索引,扫描行数的预测其实只是原因之一,这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。 132 | 133 | 134 | 不过呢,我们有时候也可以通过强制走索引的方式来查询,例如 135 | 136 | ``` 137 | select * from t force index(a) where c < 100 and c < 100000; 138 | ``` 139 | 140 | 我们也可以通过 141 | 142 | ``` 143 | show index from t; 144 | ``` 145 | 来查询索引的基数和实际是否符合,如果和实际很不符合的话,我们可以重新来统计索引的基数,可以用这条命令 146 | 147 | ``` 148 | analyze table t; 149 | ``` 150 | 来重新统计分析。 151 | 152 | **既然会预测错索引的基数,这也意味着,当我们的查询语句有多个索引的时候,系统有可能也会选错索引哦**,这也可能是 SQL 执行的很慢的一个原因。 153 | 154 | 好吧,就先扯这么多了,你到时候能扯出这么多,我觉得已经很棒了,下面做一个总结。 155 | 156 | ### 总结 157 | 158 | 以上是我的总结与理解,最后一个部分,我怕很多人不大懂**数据库居然会选错索引**,所以我详细解释了一下,下面我对以上做一个总结。 159 | 160 | 一个 SQL 执行的很慢,我们要分两种情况讨论: 161 | 162 | 1、大多数情况下很正常,偶尔很慢,则有如下原因 163 | 164 | (1)、数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。 165 | 166 | (2)、执行的时候,遇到锁,如表锁、行锁。 167 | 168 | 2、这条 SQL 语句一直执行的很慢,则有如下原因。 169 | 170 | (1)、没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。 171 | 172 | (2)、数据库选错了索引。 173 | 174 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 175 | 176 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /MySQL/面试小知识:MySQL索引相关.md: -------------------------------------------------------------------------------- 1 | MySQL 索引你真的懂吗?这几道题带你了解索引的几个重要知识点 2 | 3 | #### 1. 什么是最左前缀原则? 4 | 5 | > 以下回答全部是基于MySQL的InnoDB引擎 6 | 7 | 例如对于下面这一张表 8 | 9 | 10 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e49131416762c?w=358&h=286&f=png&s=14375) 11 | 12 | 如果我们按照 name 字段来建立索引的话,采用B+树的结构,大概的索引结构如下 13 | 14 | 15 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e498c9031dfd0?w=501&h=273&f=png&s=14295) 16 | 17 | 如果我们要进行模糊查找,查找name 以“张"开头的所有人的ID,即 sql 语句为 18 | 19 | ``` 20 | select ID from table where name like '张%' 21 | ``` 22 | 23 | 由于在B+树结构的索引中,索引项是按照索引定义里面出现的字段顺序排序的,索引在查找的时候,可以快速定位到 ID 为 100的张一,然后**直接向右遍历**所有**张**开头的人,直到条件不满足为止。 24 | 25 | 也就是说,我们找到第一个满足条件的人之后,直接向右遍历就可以了,由于索引是有序的,所有满足条件的人都会聚集在一起。 26 | 27 | 而这种定位到最左边,然后向右遍历寻找,就是我们所说的**最左前缀原则**。 28 | 29 | #### 2. 为什么用 B+ 树做索引而不用哈希表做索引? 30 | 31 | 1、哈希表是把索引字段映射成对应的哈希码然后再存放在对应的位置,这样的话,如果我们要进行模糊查找的话,显然哈希表这种结构是不支持的,只能遍历这个表。而B+树则可以通过最左前缀原则快速找到对应的数据。 32 | 33 | 2、如果我们要进行范围查找,例如查找ID为100 ~ 400的人,哈希表同样不支持,只能遍历全表。 34 | 35 | 3、索引字段通过哈希映射成哈希码,如果很多字段都刚好映射到相同值的哈希码的话,那么形成的索引结构将会是一条很长的**链表**,这样的话,查找的时间就会大大增加。 36 | 37 | #### 3. 主键索引和非主键索引有什么区别? 38 | 39 | 例如对于下面这个表(其实就是上面的表中增加了一个k字段),且ID是主键。 40 | 41 | 42 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e4c132ab5f5fe?w=459&h=435&f=png&s=19461) 43 | 44 | 主键索引和非主键索引的示意图如下: 45 | 46 | 47 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e4ce8c5874c85?w=1022&h=373&f=png&s=31736) 48 | 49 | 其中R代表一整行的值。 50 | 51 | 从图中不难看出,主键索引和非主键索引的区别是:非主键索引的叶子节点存放的是**主键的值**,而主键索引的叶子节点存放的是**整行数据**,其中非主键索引也被称为**二级索引**,而主键索引也被称为**聚簇索引**。 52 | 53 | 根据这两种结构我们来进行下查询,看看他们在查询上有什么区别。 54 | 55 | 1、如果查询语句是 select * from table where ID = 100,即主键查询的方式,则只需要搜索 ID 这棵 B+树。 56 | 57 | 2、如果查询语句是 select * from table where k = 1,即非主键的查询方式,则先搜索k索引树,得到ID=100,再到ID索引树搜索一次,这个过程也被称为回表。 58 | 59 | 现在,知道他们的区别了吧? 60 | 61 | #### 4. 为什么建议使用主键自增的索引? 62 | 63 | 对于这颗主键索引的树 64 | 65 | 66 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e4dc9269f4823?w=544&h=335&f=png&s=18257) 67 | 68 | 如果我们插入 ID = 650 的一行数据,那么直接在最右边插入就可以了 69 | 70 | 71 | ![](https://user-gold-cdn.xitu.io/2019/2/13/168e4de29fd71de6?w=563&h=364&f=png&s=19774) 72 | 73 | 但是如果插入的是 ID = 350 的一行数据,由于 B+ 树是有序的,那么需要将下面的叶子节点进行移动,腾出位置来插入 ID = 350 的数据,这样就会比较消耗时间,如果刚好 R4 所在的数据页已经满了,需要进行**页分裂**操作,这样会更加糟糕。 74 | 75 | 但是,如果我们的主键是自增的,每次插入的 ID 都会比前面的大,那么我们每次只需要在后面插入就行, 不需要移动位置、分裂等操作,这样可以提高性能。也就是为什么建议使用主键自增的索引。 76 | 77 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 78 | 79 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /个人经历与感想/2020第一篇原创:我是如何让自己变的更加优秀的.md: -------------------------------------------------------------------------------- 1 | 这是 2020 年公众号的第一篇原创,文章的标题本来是要命名为『如何让自己变的更加优秀』,这看起来有点像鸡汤文? 2 | 3 | 说实话,我这个人基本不写鸡汤文,公众号发文几百篇,基本没有写过一篇鸡汤文,更多的是谈自己的**真实经历**,**真实感受**,所以有些文章可能在你们看来是鸡汤文,但是却是真实发生在自己的身上,不然我是不写的。形如这些文章: 4 | 5 | 1、我建议你这样度过自己的大学 6 | 2、给大学生的几点建议等等。 7 | 8 | 这种文章说的话,基本属于通用文,对谁都合适,但是又对谁都不合适。 9 | 10 | 今天这篇文章,**是有感而发**。最近不是年终了吗?这两天看了挺多人的年终总结,觉得有必要写一篇鸡汤文,同时也把这篇鸡汤文送给 2020 年的自己。 11 | 12 | **那么如何让自己变的更加优秀?** 13 | 14 | 2019 年给我的感受就是:**多接触优秀的人,多接触正能量的人,远离负能量的圈子**。 15 | 16 | #### 0、多接触优秀的人,真的可以影响你 17 | 18 | 说实话,我玩公众号有个非常大的收获,就是认识了一群优秀的人,他们的优秀,让我也逐渐变的优秀起来(当然,我并没有说自己多优秀,只是觉得自己比以前优秀了)。 19 | 20 | 以前,我从来没觉得自己能够写文章挣钱,但是后面我认识了一群人,说实话,他们并没有多优秀,也不算聪明,他们也是普普通通,不过我们都在一个群里,后来他们开始玩公众号,那时候我才大二,不过把他们的公众号都关注了,等到大二暑假那会,发现他们的公众号玩的不错,还听到他们说挣了一些钱。 21 | 22 | 我可能比较势利,听到**钱**,我突然也想着自己是否也能像他们一样,也来去玩一玩,挣几个零花钱,想着他们可以,我觉得我可能也是可以的。于是,我走上了公众号这条路,刚开始是被他们远远甩在乡村路口,不过我时刻关注他们,他们的一举一动也经常激励着我,我也在偷偷模仿他们。 23 | 24 | 现在,我之前关注的那批人,都取得了不错的战绩,而我也因为玩公众号,收获了非常多。他们对我最大的影响估计就是:**看到他们可以,我觉得我可能也可以;看到他们再折腾,我也有了折腾的目标**。 25 | 26 | 也就是说,这种优秀的圈子,并不是每个人多聪明,每个人都非常牛逼,相反,是大家的背景都挺普通,大家都一直在探索,也会带动我去探索。不得不说,在一个正能量的圈子,都的很重要。 27 | 28 | 例如,本来今天想要赖床不想去练车的,自己有种在逃避的感觉。然而在知识星球看到大家的年终总结,顿时浑身充满了能量,觉得自己不能这样逃避下去了,马上起床,早上开着摩托车跑去练车了,练完车回来,感觉特别舒服。 29 | 30 | #### 1、正能量的圈子,真的可以大大的激励你 31 | 32 | 说实话,我不大喜欢负能量的人,负能量的圈子。例如有些人天天哭天喊地,天天说生活太难了,天天抱怨生活等等。你去认真做一件事,他们可能还会嘲笑你,搞的自己本来想认真去干的,结果在他们的影响下,自己也敷衍了事得了。我说的这些,觉得不是鸡汤,自己也确实发生过这样的事情。 33 | 34 | 前阵子我写了一一篇文章[普普通通,我的三年大学]()。这篇文章激励的很多人,也有挺多人来加我,说自己不能再这样下去了,说自己也想进大厂,然后也开始努力学习了,有些人还经常来问我推荐书籍,看到我的文章激励了别人,我还是挺开心的,因为确实,很多人在大学都在玩,就算你现在的奋战之后没有进入理想的公司,那么比起以前,你也是更加优秀了,学到更多了。 35 | 36 | 我举个例子,如果你是三本院校的(这里不是看不起三本院校之类的哈,因为确实存在这样的情况),你想要认真学习,想要进大厂,可能你的同学会笑笑不说话,你可能会孤身一人在奋战。但是说实话,三本,二本进大厂的,还真的不少,**实际比例可能挺少,但是我在身边的圈子,真的见过不少**,并且还有挺多人是在同一个圈子的,大家都很正能量,也都失败过很多次,但是你看看我,我看看你,并且也有挺多过来人在鼓励他们,大家一下子又充满能力,继续干了。 37 | 38 | 例如,我昨天在一个知识星球,就看到一个三本的大三的年终总结了,这个人我也经常有和他聊天,偶尔鼓励他,最后取得了不错的战绩,截图给大家看 39 | 40 | ![](https://user-gold-cdn.xitu.io/2020/1/2/16f65050e36b169f?w=592&h=448&f=png&s=146198) 41 | 42 | ![](https://user-gold-cdn.xitu.io/2020/1/2/16f6505de4c2b5be?w=606&h=176&f=png&s=74224) 43 | 44 | 如果你是三本的,本来你也有一颗奋斗的心,但是如果你身边好多负能量的,可能你会觉得进大厂太难了而放弃。不过如果你周围有挺多优秀且正能量的人,你可能并不会放弃,而且会充满能力,一直干下去,并且后面还有曾经奋斗过的人在鼓励你。那么你,觉得可能比之前更加优秀。 45 | 46 | 我觉得,这位同学能够拿到大厂 offer,真的离不开这个正能量的圈子, 当然,最重要的,还是他自己的努力,感觉自己有希望实现自己的目标,学习起来真的会很香。他投了 109 份简历,可能有些人 投了几十分简历被刷,估计都在**奉劝**自己的学弟学妹说大公司多难多难,我们三本学历 0 机会的了。最后这位同学也夸了我一下,也截个图 47 | 48 | ![](https://user-gold-cdn.xitu.io/2020/1/2/16f650d8d0eddf1a?w=626&h=214&f=png&s=81379) 49 | 50 | 大家面试的时候,记得多看我汇总的文章,我比较**势利**,很多真的都是面试有考到。 51 | 52 | 在这样一个正能量的圈子里,和一群人优秀的人在一起,自己真的也会变优秀起来。 53 | 54 | #### 2、我比以前更加敢于尝试了 55 | 56 | 我举个例子:如果在以前,你说关于**理财、保险**相关的,我可能会给你一个**白眼**,因为我没钱,学习这东西也没啥用,而且感觉这东西害人。但是现在不同了,看到大家有时候都在尝试这东西,而且大家也都不是很有钱,更多的是在**学习**,而不是想着挣钱,算是为自己以后做准备,顺便探索下这方面的水。 57 | 58 | 但是现在的我,业余时间也会学习学习,因为我的观念变了,有了更多自己的想法,感觉接触下这东西或许以后对我有帮助。而且我可以告诉你,一旦你去认真接触一样东西,你会学习到很多其他知识。 59 | 60 | > 当然,我这里并不是推荐大家去学,而是举一个发生在自己身上的例子 61 | 62 | 这样子说吧,如果你周围的人都在尝试、折腾各种东西,你本来没有什么 idea 的,但是看到大家都在尝试这,尝试那,你也会慢慢有自己的 idea。 63 | 64 | #### 3、那么如何找到这样的圈子? 65 | 66 | **那么问题来了,道理我都懂,但是我身边很难找到这样的圈子,咋办?** 67 | 68 | 可能有些人觉得接下来我会打广告,介绍某个圈子。然而我想告诉你的是,这样的圈子哪有那么容易找。。。。。。 69 | 70 | 不过我想说的就是,这样的圈子,周围往往很难找,更多的是存在于网上,因为这种圈子往往就是由一批志同道合的人组合起来的。所以需要你自己去探索,去寻找适合你的圈子。 71 | 72 | 当然,我也想过建议一个知识星球,把大家汇聚起来,不过,我觉得目前的我,还不行,目前的我还太菜了,等我以后强大点了,再来创建吧。 73 | 74 | #### 4、想删文的总结 75 | 76 | 这应该是我第一次写这种类型的文章,昨晚本来有好多话想和大家说的,不过写到文章发现好难写啊,本来想要写的有感染力点的,发现写不出来。搞的不想把这篇文章发出去,,,,, 77 | 78 | 写这篇文章可以说是很走心的吧,主要就是想要告诉你们: 79 | 80 | 1、要相信自己、多去尝试,或许你真的可以的。 81 | 82 | 2、多接触优秀的人,远离负能量的人,或许,你真的可以变的更加优秀。 83 | 84 | 如果你觉得你身边没有这样的人,那么请记住,还有我,你们的博主-帅地。我觉得我还是挺正能量滴,当然,如果你有加我微信好友,你会发现我的朋友圈发的一点也不正能力,,,,可能是,我比较热爱生活,朋友圈更多的是反应我的真实生活,平平凡凡,普普通通。 85 | 86 | 不过后面可能会创建一些微信群,把有志同道合的人聚集在一起。之前、现在也有挺多人叫我创建的,不过我还不知道怎么维护好这些群,,,,,所以一直迟迟没有建。后面创建了,再拉筛选那些想要让自己变的更加优秀的人进去,**相信一群志同道合的人在一起,真的可以让自己变的更加优秀**! 87 | 88 | 89 | 90 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 91 | 92 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /个人经历与感想/我的2019.md: -------------------------------------------------------------------------------- 1 | 今天是 2019 年的最后一天,对于我来说,2019 年可以说是我高考进入大学以来,最重要的一年了。这一年,也是我收获最多的一年,其中最重要的收获应该就是『找工作』和『运营公众号』以及『挣到了人生的第一个10万』了。 2 | 3 | #### 工作 4 | 5 | 去年的这个时候,说实话,我还不知道我在干嘛,因为我对即将到来的春招和秋招也不大了解,最后是鼓起勇气,啥也没复习就参加了春招的几场面试,不过都失败了,春招大概面试了三四家公司,不过都以失败告终,顿时感觉自己好菜啊。不过但说实话,我并没有多低落,而是好好复习,完善自己的知识体系 6 | ![](https://img-blog.csdnimg.cn/20191230191535752.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 7 | 到了 8 月初就开始投递简历的,当时我正在成都实习(准确着说是实训,学校组织的),不过说实话,在实训的这段时间,基本是天天在摸鱼,自己学习自己的。 8 | 9 | > 如果你想要这份导图,可以在我的微信公众号**帅地玩编程**那回复**思维导图**,即可获取 10 | 11 | 最后很幸运,在秋招提前批面试中,拿到了腾讯后端开发的 offer。 12 | ![](https://img-blog.csdnimg.cn/20191230192439935.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 13 | 14 | 15 | 工作地点在深圳,可以说公司和工作地点都是自己挺喜欢的,所以当时 9 月初就早早结束自己的秋招了,开启了**冲浪**模式。 16 | 17 | 找工作,可以说是我大学生涯中非常重要的一件事情了,作为一个双非本科生,能够拿到大厂 offer,真的很开心,同学也觉得自己很幸运,可以说,2019,为我找工作这件大事画了一个圆满的句话。 18 | 19 | #### 运营公众号 20 | 21 | 2018 年的这个时候,我的公众号大概有 6000 多粉丝吧,这 6000 多粉丝来的非常不容易,可以说完全是靠我的文章吸引过来的,当时写的文章,还是有非常多好评的,哈哈,吹了一波自己的文章。 22 | 23 | 当时很想自己能够有 10000 的粉丝,这样我就可以发个朋友圈,配上文字:**从 0 到 1,在技术之外,迈出了重要的一步**。 24 | 25 | 不得不承认,我还是个挺容易满足的**俗人**,同时也挺喜欢偶尔装装逼,满足下自己的虚荣心,当然,在很多事情上,自认为还是很有自知自明滴。 26 | 27 | 到了 2019 年 2 月,我的粉丝成功突破 10000,不过当时并没有发朋友圈,对自己的要求高了,想多一个 0 在发,,,,,截止到目前,我的粉丝已经 60000 多了 28 | ![](https://img-blog.csdnimg.cn/20191230193605225.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 29 | 当时在大概 3 月份的时候,经历了春招的惨败,当时我也在做一个选择,**要不要停更公众号,全力备战秋招呢?**,想着等结束了秋招,在好好运营公众号,因为不得不说,运营公众号真的很花时间,很让人分心。就算你不写文章,只要发了文章,就会花掉不少的时间,因为**寻找文章,排版,看留言,回复留言,有人对文章提出疑惑我得进行回答,看看今天增加了多少粉丝,看看文章的阅读量怎么样等等**,任何一件事都会让人分心,没有运营过公众号的,可能没有这种体会。 30 | 31 | 不过很庆幸,在选择了继续把公众号做下去,不过从 3月 ~ 10 月这段时间,涨粉比较少,大概在 10 月份的时候,我的粉丝大概在 30000 多左右,这段时间一边忙着找工作,一边运营公众号,虽然 7个月的时间涨了 2万多,不过我挺满意了。不过从 10 月到 12 月感觉开了挂,粉丝从 3w 多涨到 6w 多。可以说,每个粉丝都在自己辛苦引流来了,没有花过一分钱去推广。 32 | 33 | 很幸运,这一年下来,公众号的粉丝翻了 10 倍,这 6 w 多粉丝在大佬们看来可能不足一提,但对于我来说,我还是非常开心的,因为我也不是全职在搞这个,而且**技术号**的受众也比较少。 34 | 35 | 当然,对于目前的我来说,学习技术,提升自己的硬实力,仍然是我的主要任务,我还期待自己工作几年,工资翻一翻。 36 | 37 | #### 挣到了人生的第一个 10 w 38 | 39 | 从 2019 年开始,我就不再向家里人要钱了,基本实现了经济独立的。当时真的很开心,我爸妈估计也挺开心,以前每个月我哥都会给我寄 1000 元左右,供我当生活费,我觉得自己用钱还是比较省滴,我爸妈是干农活滴,有加我微信好友的,估计也经常看我在朋友圈晒番薯,玉米之类的 40 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191230201051893.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 41 | 不过在 7 月份的时候,我把用了四年的电脑扔了,真的被这电脑给气到了,同时用自己挣的钱买了人生的第一台 mac。 42 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191230201343109.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 43 | 不得不说,mac 刚开始不习惯,用一阵子后,发现是真香。 44 | 45 | 在八月底的时候,那时候我还在成都实习,不过我妈生病了需要动手术,当时还是挺担心的,但是距离又这么远,最后是没回去,然后给我爸寄了 5000 元,让他们别担心钱的事情。这个可以说是我人生第一次给家里寄钱,看到自己还没毕业、还没工作的儿子给家里寄了钱,我爸当时也是挺开心的。 46 | 47 | 到了 2019 年 10 月底的时候,2019 年这一年,我算了下,累计下来自己已经挣了 10w 元了,当时也写了一篇文章封面为『人生的第一个 10 万』的文章 48 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191230210957135.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 49 | 对于我来说,最大的收获并不是这 10w,而是这件事让我对未来更加有信心了,有了更多的探索方向,而且也让我更加有动力去学习去探索更多事物。当然,对于现在的我来说,提升硬技术才是根本,但也不是全部,只能说我会一边主学技术,附加发展自己的副业,探索更多的可能性吧。 50 | 51 | #### 总结 52 | 53 | 2019 年这一年,收获颇多,但也即将成为过去式;2020 年,对于我来说,是一个全新的开始,这一年,我也将步入职场,探索更多的可能性,同时我也将我的公众号名称从『苦逼的码农』改为『帅地玩编程』,也算是全新开始的一个标志。 54 | 55 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 56 | 57 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /个人经历与感想/说一说我最近的日常,学习与思考.md: -------------------------------------------------------------------------------- 1 | 实不相瞒,我已经在家待了有两个月了,主要是前阵子去爬山,不小心摔了一下,导致肩脱臼了,所以就在家养伤了。在这里也顺便提醒各位,去玩,一定要注意安全!!!当然,我相信大家只有尝试点苦头才会真的去注意的了。 2 | 3 | 虽然是在家,很多人说我好悠闲,过起了滋润的生活。其实不然,无论我是在学校、工作还是在家,感觉自己都是有非常多的事情做,特别是这段时间在家,一点也不悠闲,除了**时间自由**,估计也不比在公司搬砖的各位闲,毕竟年轻,有时间还是要多学一点,现在的学习,应该说是为了以后能够悠闲一点,年轻应该是我现在最大的资本了,,,,。最近主要在做的事情主要有**学习专业知识**、**阅读与写作**、**英语**。 4 | 5 | #### 专业知识 6 | 7 | 在计算机基础知识理论方面,我算掌握的还行,例如算法、计算机网络、数据库、操作系统等等,注意,我说的是**理论**,当时学这些,很大程度上也是为了**校招**,不过说实话,我觉得学习这些底层基础,对我的影响还是非常非常大的,我也认为每个程序员都应该至少学一下这些基础知识,我的公众号也在不断着提供这些知识供大家学习。 8 | 9 | 不过秋招过后,马上就要步入**职场工作**了,而这些知识的远远不够了,在工作上实际用到的可能会比较少,我认为那些基础的学习,更多的是**潜在性**的影响,不是说没用到就是没用,更合适的说法可能是,处处不在处处在。 10 | 11 | 其实我还不知道进了公司之后,自己需要负责的任务,也不清楚要学习哪些内容好,居然不清楚自己未来会用到什么技能,那我就先学那些**必须掌握的技能**,例如 **SQL + Git + Linux**,虽然这三个技能我之前都学过,但是并没有系统学过,很多都是断断续续,很多原理也不是很清楚,所以我最近是**从零学习了这些知识**,前几天我也发过一篇 Git 命令总结的文章:[最近从 0 学习Git,详细分类总结了这份 Git 命令宝典](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247486712&idx=1&sn=dde6760eb20a0669d06f7fb880f01682&chksm=ce40472cf937ce3ab17eeea01f79d3f22e11021c03f58c747170df82bc58844af0be382366cc&token=806726781&lang=zh_CN#rd) 12 | 13 | 我这个人有一个特点,就是各种疑问多,例如对于 git,执行了那些命令之后发生了什么,git 是怎么做到版本控制的,本地仓库和远程仓库是怎么一一对应起来的,等等,我都想把这些本质的东西搞懂,这样以后我遇到问题,或者面对别人的询问等,我也可以快速着知道问题所在,当然,说起话来也会更加自信。 14 | 15 | 所以,我从 0 学习了这些知识,而 sql + git +linux,我赋予了最大的优先级来学习,因为我不清楚未来工作会用到哪些,但我知道这些是每个程序员都应该掌握的。 16 | 17 | 这里我也经常和各大在校的学生说,特别是那些不知道学啥的在校生,如果你不知道学啥,不知道未来从事什么样的岗位,那么你可以把那些**必学**的知识学了,哪些是必学的?像数据结构,算法,计算机网络等等,可以看我公众号之前的文章去,基本都有说到这些知识以及推荐的书籍。 18 | 19 | 我之前是学习 Java 技术栈的,不过我大概率是要转型 Go 语言,当时面试的时候面试官没有问过一句 Java 的,最后的提问环节也有叫我可以去学一学 Go,所以为入职能够更快着上手任务,我最近也在学习 Go,买了《Go语言程序设计》和《Go Web》这两本书。 20 | 21 | 不过我这个人有个特点,就是入门的时候喜欢**刷视频入门**。感觉这样可以避免一些坑(例如环境配置,软件推荐啥的),让自己更快着入门,所以我也在慕课网付费买了一门课程。说实话,要是在以前,能不花钱的绝对不花钱,花多少时间寻找我都愿意。 22 | 23 | 不过现在的我,能少花时间的尽量少花时间,多花几十块钱,一两百我倒不在意,重点是能够**花最少的时间学习到最多的东西**,因为如果把花在寻找资源的时间折算下来,可能直接付费更加便宜。当然,这里只是我**目前**的想法,随着时间的转移,阅历的提升,可能每个人的想法也都会在不停着改变。 24 | 25 | 总体来说就是,多花时间学习专业技能,是我当前最主要的任务,这也是为了以后能够少花时间去学习,为了以后能够更加悠闲,现在辛苦点,问题不大,毕竟年轻。 26 | 27 | #### 阅读与写作 28 | 29 | 虽然专业技能的学习很重要,但我也花了挺多时间在阅读与写作上,当然,这里指的阅读,一般是指非技术书籍,写作则指技术文章与非技术文章的写作,例如你们现在在阅读的这篇文章就是非技术文章了。 30 | 31 | 我觉得,除了技术,还是有非常多值得学习的东西的,**我们的世界里,不应该只有 Coding,还有 xxxxx**。至于 xxxxx 是什么?这个就看你自己的选择了,那我就说一说我最近的 xxxxx。 32 | 33 | 以前我是很喜欢看那种什么修仙、都市啥啥的小说的,你们懂的,主角不但有很多美女喜欢,还经常越级干对手,爽啊!这种感觉属于**爽文**,不过在我现在这个阶段,是不看这些了,更多的是看能够提升自己的**认知、思维**相关书籍,还有就是看一些看了之后能够**让自己更加热爱生活,让自己的心态更加乐观**的书籍,说白了就是让自己活的更好。 34 | 35 | 我举个例子,有时候我们总是为了某一些事焦虑不安,我们知道焦虑是没有任何帮助的,也改变不了什么,实际上我们也想平常心,也想不受这些小事影响,从而去做正确的事情,因为在焦虑、莫名紧张的过程中,我们会啥也不想做,做啥都效率好低,感觉时间过的好慢,内心好煎熬。 36 | 37 | 然而事实是:**想要不焦虑,我做不到**,因为控制不了自己的内心,**有些事情,只有经历多了,才能坦然相对**,不过我觉得看书,是可以加速**坦然相对**这个过程的,而且,我觉得阅读一些书籍,真的可以让我的内心更加强大,遇事更加冷静,总之,**阅读,真的可以慢慢改变一个人**。 38 | 39 | 可能有人会问我,有没有书籍推荐的?说实话,没看过的书籍我是不敢随便推荐的,我目前也看的不多,可能暂时不敢给大家推荐,不过寻找一本合适自己的书,也是很不容易的吧,以后看到喜欢的非技术书籍,我会推荐给大家,今天这篇文章,不是专门说阅读这个事情的,就先不介绍了。 40 | 41 | 其实我不断的阅读输入,一个很重要的原因也是为了以后能够更好的**输出**,也就是**写作**,肚子里啥也没有,掌握再多的写作技巧也是没用的,而且我觉得,写作也是一个人人都值得掌握的能力,相当于,专业技术是硬能力,而写作算是一个软能力。这里顺便推荐下我的小号『**我是帅地**』,主要就是写这些非技术的东西了,包括推荐非技术书籍等等,不过目前小号还没有写过文章,想看我瞎扯的可以关注 42 | 43 | 总之,对于目前的我来说,学习技术放第一位,不过也会花些时间来阅读与写作。 44 | 45 | #### 英语 46 | 47 | 实不相瞒,我英语四级只考了 429 分,折算成 100 分制的话相当于 60.5 分,要是多做错一道选择题,我估计就没过 4 级了,然后 6 级考了两三次,一次比一次低分,最后我觉悟了,六级是不可能过的了,还是省了几十块钱来去买几包辣条吃,压压惊。 48 | 49 | 可以说,高中是我英语水平的巅峰时期,大学之后,英语一年不如一年。对于一个技术人员,能够阅读英文技术书籍、技术文档,我觉得还是非常重要的,如果不是在外企之类的,可能**听**和**说**倒不是很重要。说实话,我听和说,只能呵呵,我四六级考试,从来不带耳机做题的,都是自己猜测听力的答案,而且我可以告诉你,每次在听力这部分,我盲猜每次都拿了 100+ 分数,我是根据几道选择题之间的一些关系来推测答案了,,我感觉,我听了听力,可能 100 分都拿不到,,, 50 | 51 | 相比大家都知道英语很重要,可是,就是很难学,,,我之前为了学习英语,也买过水滴阅读啥的课程,不过太难坚持了,,总之就是每次都坚持不下去,不过,我还是想至少把**阅读**这方面搞起来。 52 | 53 | 以前经常失败,我觉得主要是缺乏**驱动力**,例如买水滴阅读课程的时候,我选择了阅读一本小说,不过小说的剧情我完全没兴趣,,这应该是导致我失败的一个大原因,如果有一个强大的驱动力,我想我还是可以坚持下去的,所以我最近一直在寻找驱动力,最后发现**公众号**对我的驱动力挺大,不如来去翻译一些国外的技术文章,然后发到公众号上,这应该挺有驱动力。 54 | 55 | 不过寻找文章也是个挺花时间的事情,所以我干脆就来去买个课程吧,最终我是决定买一个国外的**算法**课程,这个课程我看别人推荐的,反正是一个大佬转行到计算机,靠这个拿了谷歌的实习 offer(听说谷歌非常注重编码能力),我看了课程的目录还挺不错,有点像 leetcode 的分类刷题总结,不过挺贵的,,,好像70~80美元。 56 | 57 | 到时候我来去翻译了,我就把自己学到的同步到公众号,当做一个系列算法来更新,相信你跟着我学完这个系列,肯定也会挺牛逼,希望大家也积极来看我的系列算法文章,这样我可能会翻译的更加有动力,,,,当然,不会马上更新,还得过阵子才来去翻译。 58 | 59 | 总之,对我来说,驱动力还是非常重要的,每次学习一些东西,我都会尽可能去寻找自己的驱动力,希望大家对于想学,又坚持不了的东西,也可以试着去寻找驱动力来学习。 60 | 61 | #### 总结 62 | 63 | 这篇文章主要分享了我最近的学习以及自己的一些学习方法,对于在校生,寒假也来了,我希望在这个寒假中,你能够让自己在年后开学后,感觉自己更加强大了,对于在工作的,基础很差的,我也希望你不要太焦虑,可以制定下计划,一本书一本书阅读,如果喜欢写博客记录起来,那就更好了,当然,也要根据自己的时间的决定。 64 | 65 | 虽然这段时间天天一个人呆着家里,但是一点也不会无聊,过的很充实,对于我来说,**无聊,从来都是不存在的事情**,**学习,无时无刻都在执行**,当然,我觉得打游戏,看剧也可以是一种学习,,,,所以,我的无时无刻学习,包括打游戏,看剧。 66 | 67 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 68 | 69 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /写给在校生的经验总结/历经两个月的秋招,结束了,谈谈春秋招中一些重要的知识点吧.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 历经两个月的秋招总算是结束了,从七月份开始复习秋招相关知识,到八月多开始笔试、面试,到九月下旬的秋招结束,在笔试面试的这两个月里,还是挺累的。这篇文章就说说秋招这段时间的收获以及给对于明年要参加秋招的同学的一些建议吧。 4 | 5 | #### 一、最后去的公司 6 | 7 | 对于我来说,这次秋招算是满意的吧,找到了想去的**城市(深圳)以及公司(腾讯)**,我投的岗位都是**后端开发**。在之前春招找实习的时候,人生的第一次献给了腾讯,那时候没啥面试经验,感觉傻傻的,没看过的可以看我之前写过的文章[嗯,春招两次腾讯面试都挂二面了,分享下我失败+傻傻的面试经历](https://mp.weixin.qq.com/s/Vr9TGgT6qpKjm9S5EkePuw)。 8 | 9 | 在秋招,腾讯也是我第一家面试的公司,感觉还是挺有缘的,8 月 14 号接到了腾讯面试官的面试预约,当时突然有点后悔,感觉自己应该晚一点投,因为腾讯的提前批是 9 月 12 号才结束,正式批 9 月 26 号开始。感觉当时还有很多没复习,想晚一点再面试。**后来,我才知道,我错了,真的是越早投越好,千万别等到正式批或者提前批即将结束才投,那个时候投,真的会错过很多机会**(至于为什么,后面会说)。17 号开始了秋招的第一场面试,到 8 月底面完了所有流程,9 月下旬出才收到面试结果。下面谈谈这次秋招的感受吧。 10 | 11 | #### 二、关于我 12 | 13 | 可能没看过之前我的文章的,很多人还不知道我。这里我简单介绍我的背景吧。 14 | 15 | 我今年大四,大一学的专业是**木材科学与工程**,后面转专业到**软件工程**,老家是广东的某个 5 线城市,在广州这边读大学,当然,是某个个**双非大学**,至于是哪个?学校里有养**神兽**的就是了。 16 | 17 | 可能看我文章的读者中,很多人觉得我很厉害,说实话,其实我还是挺菜的,在校期间没有参加过任何比赛,没有拿过任何奖金,扎心了(凉了,大神的形象暴露了)。所以这次能够拿到大厂的 offer,我觉得得归功于我之前对**计算机基础知识**以及**算法**学习。想拿大厂 offer,**基础知识 + 算法**必须重视。 18 | 19 | #### 三、基础 + 算法 + 项目 20 | 21 | **1、关于基础知识** 22 | 23 | 秋招的竞争还是非常激烈的,如果你想要在秋招中拿到满意的 offer,那么从现实开始,就要把**计算机基础**(操作系统、数据库、计算机网络、Linux)、**算法**学好,特别是算法,不容易临时抱佛系,是一个长期积累的过程。 24 | 25 | 对于大厂,比起项目,它更加主要你的基础能力是否扎实吧。记得腾讯一面的时候,面试官就**哈希表**这个问题问了我有二十分钟,从刚开始让我用 C 语言来设计一个哈希表,后面问我如何设计 hash 哈希,怎么样设计更高效,怎么样设计能够最大程度减少碰撞,是否要动态扩容等等。一系列问题,我都按照自己的理解回答了,有些引用 redis 、 hashmap,并且我都举了一些例子。这个问题回答之后,感觉面试官有些惊讶,问我是否研究过 redis 这些框架的源码等。感觉这个问题回答之后,面试官对我更加感兴趣了,那场面试问了 90 分钟,基本把所有基础知识都问了。 26 | 27 | 所以我觉得,对于秋招,理解常见数据结构的相关设计,为什么要这么设计,实在是太重要了,可能很多人都知道链表、树、哈希表等,但被深入一问,可能就不懂,不知道为什么要这么设计了。 28 | 29 | 这次秋招,被问的最多的就是操作系统、计算机网络、MySQL了,虽然我面试的是 Java 工程师,但是很多公司并没有问我 Java 相关知识(ಥ_ಥ),不过这和一个公司的技术栈相关吧,像我面试的 腾讯,字节跳动,shopee,小米等,公司的主要开发语言不是 Java,所以这几个公司的面试,一个 Java 相关的知识点都没有问过我,反正我是哭了。不过这并不影响我的回答,因为这些计算机基础知识,我很早就在准备了。 30 | 31 | 所以对于要参加面试的同学,千万别把自己吊死在某个语言上,语言只是一门工具,而应该多花一些时间在一些**通用的知识**上,例如 **sql + Linux + 算法 + 操作系统 + 计算机网络**。 32 | 33 | 当然,如果公司的主要语言是 Java 的,还是会问很多 Java 相关知识的,例如我面试京东,蘑菇街,阿里的时候,就问了很多 Java 的知识,像京东,蘑菇街,cvte 就没问过我计算机网络、操作系统这些知识。 34 | 35 | 所以说,不同公司,侧重点还是不大同的,但是,对于 BAT 这些大公司,**基础知识 + 算法** 是必问的。 36 | 37 | **2、关于算法** 38 | 39 | 如果算法学的差,会错过非常多非常多的面试机会,会很难过**笔试**这一关,秋招的笔试,反正我一直被虐,感觉笔试的难度还是很大的,自己一个人做笔试,想要全 a,还是非常难的。笔试题目一般是**选择题** + **编程题**,但有些公司没有选择题,全是编程题(例如腾讯,字节跳动,拼多多,网易等,这里指的是提前批哈,正式批的好多我没去参加)。不过无论是否有选择题,编程题做的差,就凉了,一般编程题占**60%**的分值。 40 | 41 | 反正我有挺多笔试环节就挂了的,有些我编程题全 A了,然而并没有收到面试通知,估计是我简历没啥亮点吧。 42 | 43 | 有人说,leetcode 的前 500 道题刷了,笔试稳吗?说实话,还真的不稳,得看你的掌握程度,像 leetcode 那些题,一看就知道是什么题型,应该用哪种算法。而笔试题完全不一样,很灵活,可能是多种算法的结合。而且,有时候题意还得看十几分钟才看懂要我们干嘛。不像 leetcode,就几十个字,简单明了。反正 leetcode 中挺多 hard 级别的题我都会做,不过笔试的难度有些并没有 hard 高,却做不出来。因为时间也是挺紧的….大概一道题只有 30 分钟的时间给你做吧。 44 | 45 | 所以,那些经常刷 leetcode 的,我的建议是,千万别图刷题的算法,而是应该彻底搞懂这道题的算法思想,力求**最优解**,之前我也写过相关的文章[我是如何学习数据结构与算法的?](https://mp.weixin.qq.com/s/1iD8lG1TfJTe1YbzmhQJ1A)。 46 | 47 | 对于,还有一点,建议大家在刷题的时候,直接在网页那里打代码,别跑到 IDE 里写了,因为面试手撕代码的时候,并不会给你 IDE 写,而是在笔记本手撕算法,如果你不熟悉的话,估计代码会经常写过,而且排版可能也会很乱。反正我春招面试阿里的时候,让我在笔记本做算法题,我哭了,调用库函数的时候,方法名啥的全忘了怎么写,而且代码也老是写错。因为平时在 idea 会提示,在笔记本没提示,特别不习惯。 48 | 49 | **3、关于项目** 50 | 51 | 基础、算法很重要,进大厂缺一不可。那么对于一个参加秋招的学生来说,项目重要吗? 52 | 53 | 答是**非常重要**,我秋招最大的弱点是项目经验不好,这也让我在很多公司直接一面就凉了。我自己没有脚踏实地着去做一个项目,都是看视频速成的,而且自己也没有好好跟着视频打代码,自己尝试去做一个项目,和跟着视频去做,还是有所不一样的,毕竟跟着视频,很多东西不是自己想的,所以不深刻。 54 | 55 | 我看的项目视频感觉还是挺不错的,是**牛客网**左神讲的,只是我比较懒,偷工减料,没有好好写代码,也没有去拓展这个项目。如果自己看完视频,好好去拓展、完善的话,我的项目经验,就不会那么差了。 56 | 57 | 记得蘑菇街一面的时候,面试官一上来就让我讲项目,然后我就讲牛客网学的哪个项目,面试官让我讲**线上**的项目,别讲**练手**的项目,我哭了,因为我没有线上的项目,因为我的暑假实习,实际上就是去培训,并不像其他人去公司实习,可以参与到完整的项目流程。这个时候,我就随便说了培训期间水的一个项目(几天时间快速水的),然后我就被面试官怼死了,,,然后就没有然后了,一面挂。 58 | 59 | 然后节点 cvte 面试的时候,一面二面全程怼项目,全是我的弱项,我也哭了。可以说,秋招我最大的弱点是项目,多次被怼告诉我,**秋招,一定要有一个项目,这个项目不需要多高端,但需要你真正动手做过,研究过**。 60 | 61 | 所以说,项目非常重要,可以打打增加面试的成功率,特别是中小型公司。当然,我觉得对于有些大厂,没项目,也一样能进,因为有些公司并不看重你的项目,例如我面试过的腾讯,字节跳动,shopee,小米等,基本没怎么问项目(可能对我的项目不感兴趣,哈哈)。当然,有个项目更好,只是并非必备条件。 62 | 63 | 项目该如何准备?我觉得可以跟着学校的老师做,或者自己看视频做,但是,一定要自己打代码,并且进行拓展,**注意,一定要进行拓展,不然可能会坑了你也不一定。** 64 | 65 | **4、总结** 66 | 67 | 所以我觉得,只要把基础打好,算法学扎实,并且弄些项目经验,进大厂的机会还是很大的,大家不用怕被卡学历,很多互**联网大厂**,还是大部分不卡学历的,只有你能过得了笔试,基本都能获得面试的机会,面试就是凭实力说话了。当然,对于一些公司,还是挺卡学历的,例如一些国企之类的。 68 | 69 | 一篇文章也写不了那么多,后面我也会分享自己在秋招中收获的经验的经验等。 70 | 71 | #### 四、关注我,助你搞懂面试必考点 72 | 73 | 我已经有两个月没写文章了,十月份,一定好好写文章,主要写**计算机基础知识**(计算机网络,操作系统,Linux,MySQL)和**算法**。看过我文章的都知道,我写的文章,一定是你在百度查找不到的,可能很多知识点你都有看过相应的文章了,例如 B 树,B+ 树等等,但是,我居然要写,就一定不会和百度出来的那些一样,千篇一律。而是会从自己的理解出发,助你更好着搞好某个知识点,让你知其所以然。 74 | 75 | 说时候,基础知识的面试,面来面试,高频的面试点无非就那几十个,但是,对于同一个知识点,不同人的回答,效果相差巨大。例如百问不厌的高频面试题:**进程间的通信方式有那些**,这个问题我被问吐了。有些人可能是这样回答的: 76 | 77 | 进程的通信有 6 种,分别是:管道、消息队列、共享内存、信号量、socket,信号。面试官一问你他们的区别,有哪些应用,为什么需要这些的时候,你可能就不懂了,给人的感觉就是:你不是真的理解,而是**背**的。 78 | 79 | 而我在回答的时候,是从最常见的应用说起,从管道,层层递进,一个一个引出来。也就是说,当你真正理解了之后,是不需要记忆的,基本可以推出来。关于进程间的通信方式,我也写过对应的文章:[记一次面试:进程之间究竟有哪些通信方式? ---- 告别死记硬背](https://mp.weixin.qq.com/s/5CbYGrylSKx1JwtOiW3aOQ) 80 | 81 | 所以,在之后,我会用心写好每一个知识点,保存通俗易懂,让你知其然,知其所以然。大家敬请期待。 82 | 83 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 84 | 85 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /写给在校生的经验总结/核心整理:那些让你起飞的计算机基础知识:学什么,怎么学?.md: -------------------------------------------------------------------------------- 1 | 我之前里的文章,写的大部分都是与**计算机基础知识**相关的,这些基础知识,就像我们的内功,如果在未来想要走的更远,这些内功是必须要修炼的。框架千变万化,而这些通用的底层知识,却是几乎不变的,了解了这些知识,可以帮助我们更快着学习一门知识,更加懂得计算机的运行机制。当然,在面试中也经常会被问到,特别是对于应届生,对于春秋招,也可以看看我前阵子写过的文章[历经两个月,我的秋招之路结束了!](https://mp.weixin.qq.com/s/HiDp0UWM2hy_riERh53w8Q)。也有读者经常问的**计算机基础知识究竟是指啥?学习顺序?推荐书籍?** 2 | 3 | 我公众号的读者学生以及非科班的应该挺多的,所以我今天这篇文章就写一写,我学过的计算机基础知识,看过的书以及我学过的顺序 4 | 5 | > 当然,以下是我个人的一些经验,并且学过的一些知识,仅供参考,也欢迎大家进行补充 6 | 7 | #### 一、计算机网络 8 | 9 | 在我们用的程序中,99% 都离不开网络,作为一个程序员,我觉得了解计算机网络是必须的,在大学的课程中,一般也都会开设这一门课。 10 | 11 | 在我学习这门课之前,我就特别好奇,一台电脑是怎么把消息发给另外一台电脑的呢?例如: 12 | 13 | 1、两台电脑啥线路也没有相连,怎么就能把消息发送给他呢? 14 | 15 | 2、世界上的电脑那么多,咋就能找到那台特点的电脑呢?有人说我们可以 MAC 或者 IP 来唯一标识啊,可是,我就有点疑惑了,世界那么大,电脑那么多,有了这个标识,我们该怎么找到他呢?遍历所有电脑? 16 | 17 | 3、多个程序同时发消息给一台电脑,电脑是如何准确把这些消息拿给这些不同程序的呢? 18 | 19 | 4、发送的消息丢失了怎么办? 20 | 21 | 总之,一大堆疑问,看了计算机网络之后,才豁然开朗。自己也写了一篇评价不错的文章:[一文读懂一台计算机是如何把数据发送给另一台计算机的](https://mp.weixin.qq.com/s/Y3-CM6EiIX9saXn4U9yu1w) 22 | 23 | 所以这里,我是强烈建议大家学一下的,在面试中,计算机网络也是高频考点,这里我大致总结一下一些必学协议以及面试高频考点: 24 | 25 | 1、http协议,包括:封装格式,常见响应码,不同版本的区别,常见请求方法,存在哪些安全隐患,啥是无状态协议等。 26 | 27 | 2、https协议:http 是明文传输,https 是加密安全的,需要知道 https 是如何加密的、数字证书如何形成,啥的对称加密、非对称加密。 28 | 29 | 3、TCP协议:三次握手、四次挥手、如何保证可靠传输、流量控制、拥塞控制。 30 | 31 | 4、UDP:这个大致了解即可,好像内容比较少 32 | 33 | 5、DNS、ICMP、ARP、DHCP(我就不一个一个写了) 34 | 35 | 上面写的这些协议,我觉得是比较重要的,特别是在面试中。我公众号文章也写了好几篇这些协议的,大家可以去**计算机基础**那个模块找。 36 | 37 | 对于新手推荐视频:可以看韩老师讲的视频,在哔哩哔哩搜索**韩老师**就可以找到 韩老师讲搞笑《计算机网络原理》。 38 | 39 | 推荐书籍:《计算机网络:自顶向下》、《图解http》 40 | 41 | 我是先看了视频,在看这两本书的(文末我会给出电子版)。 42 | 43 | > 大家也可以关注我的微信公众号:**苦逼的码农**,第一时间获取我的文章以及一些资料 44 | 45 | 46 | 47 | #### 二、操作系统 48 | 49 | 操作系统也是一门非常重要的知识,在面试中也是问的非常多(当然,看公司,有些公司技术栈是 Java 的,可能问的比较少)。对于操作系统,要学的也挺多,例如: 50 | 51 | 啥是进程,啥是线程,他们的本质区别?我们运行一个程序时,数据放在哪里?代码放在哪里?咋就还要分堆和栈?线程切换时是上下文是啥意思? 52 | 53 | 虚拟地址是什么鬼东西?线程需要那么多种状态干啥子?什么是乐观锁、悲观锁?死锁是怎么造成的?解决死锁的策略有哪些?等等 54 | 55 | > 有人说学操作系统太他妈枯燥了,确实挺枯燥,不过说实话,我还是学的挺有意思的,感觉可以学到很多种策略,一种比一种好,每次看完都是:我去,咋我就想不到呢。我觉得,对于前期,我们需要多参考别人的策略,看多了,有了一定的基础,慢慢形成自己的策略,总之,看这些书,不单要掌握这些知识点,更重要的是一种思维逻辑的提升 56 | 57 | 对于操作系统,我总结了下面一些比较核心,面试相对容易被考到的。 58 | 59 | 1、进程的通信方式(我写过一篇很不错的文章:[记一次面试:进程之间究竟有哪些通信方式? ---- 告别死记硬背](https://mp.weixin.qq.com/s/5CbYGrylSKx1JwtOiW3aOQ)) 60 | 61 | 2、进程、线程究竟是由什么组成的?有哪些数据? 62 | 63 | 3、内存管理,包括:虚拟内存(重点)、分页、分段、分页系统地址映射、内存置换算法(重点)。 64 | 65 | 4、死锁的处理策略(死锁预防、死锁检测与恢复、死锁避免) 66 | 67 | 5、进程调度算法 68 | 69 | 6、磁盘寻道算法 70 | 71 | 上面说的这些,我认为是比较重要的,如果你没学过,我相信学了之后,你可以学到很多东西,知识点只是其中之一。 72 | 73 | 推荐视频:这个我没看过视频,所以想学的,我推荐去**中国mooc大学**找各大高校的课,也可以去国外找对应的课。 74 | 75 | 推荐书籍:我看过的书籍是《操作系统—精髓与设计原理(第八版)》,不过大佬们都推荐《深入理解计算机操作系统(原书第三版)》,我看过目录,感觉还不错,这里也推荐这一本。不过对于零基础的,我建议可以先看一本专门给小白看的书:《程序是如何跑起来的》。 76 | 77 | > 操作系统的学习,还是挺枯燥的,不过,只有把最难的啃过去,才能变的更加强大 78 | 79 | #### 三、数据库(这里我用 MySQL) 80 | 81 | 在大学的课程里,一般都会开设一门数据库的课程,不过这门数据库是没有针对某一种数据库语言的(例如 MySQL、Oracle)。不过我这里只讲 MySQL的学习,**别问为什么,问就是我逃了二十分之十九的课**。 82 | 83 | 把MySQL学好,还是特别重要的,千万不能停留在会用的层面上,而是应该要了解一下原理,特别是对于要**面试的同学**,会问挺多原理,我每次被问到 MySQL 我都会信心大增,因为我虽然不大好写 SQL,但是,知道挺多原理,记得腾讯、shopee面试时,面完 MySQL,面试官好像对我刮目相看了。好了,不吹了,说这些也是强大 MySQL 的重要性。下面就说我学过的一些知识以及推荐的学习资料吧。 84 | 85 | 对于 MySQL,需要学的还挺多的,例如, 86 | 87 | 1、一条 sql 语句是如何执行的?进行更新时又是怎么处理的? 88 | 89 | 2、索引是如何实现的?多种引擎的实现区别?聚族索引,非聚族索引,二级索引,唯一索引、最左匹配原则等等(非常重要) 90 | 91 | 3、事务相关:例如事务的隔离是如何实现的?事务是如何保证原子性?不同的事务看到的数据怎么就不一样了?难道每个事务都拷贝一份视图?MVCC 的实现原理(重要)等等。 92 | 93 | 4、各种锁相关,例如表锁,行锁,间隙锁,共享锁,排他锁。这些锁的出现主要是用来解决哪些问题?(重要) 94 | 95 | 5、日志相关:redolog,binlog,undolog,这些日志的实现原理,为了解决怎么问题?日志也是非常重要的吧,面试也问的挺多。 96 | 97 | 6、数据库的主从备份、如何保证数据不丢失、如何保证高可用等等。 98 | 99 | 还有一些常用命令也要知道。 100 | 101 | 我觉得,只要你了解了以上的原理,那么对数据库调优的帮助是非常大的,上面除了第六点,其他五点,在应届生的面试中,极其高频。 102 | 103 | 推荐书籍:连 sql 都不会写的,推荐《SQL必知必会》,接着推荐《MySQL技术内幕:InnoDB存储引擎》。 104 | 105 | 这里我必须推荐下极客时间的一个专栏:《MySQL实战45讲》,讲的非常好,看完应付面试,我觉得够了,我每次面试 MySQL 几乎都加分,离不开这个专栏。如何你想要购买,可以在我的公众号回复**『数据库』**,我会给你发对应的购买链接(注意,这个可不是广告哈,大家买不买看自己) 106 | 107 | #### 四、数据结构与算法 108 | 109 | 数据结构与算法,我就不想多说了,看我文章的都知道,我写的文章 80% 是数据结构与算法相关的,重要性不用说。我秋招最大的优势估计就是**数据结构与算法的掌握了**。上面三门课程的学习,基本也都是离不开数据结构的,对于如何学习数据结构与算法,我觉得可以在写一篇文章了,所以数据结构与算法的学习,我这里不写了,可以关注我的文章,我明天会写一篇与**算法**相关的。 110 | 111 | 112 | 113 | > 论面试,我觉得 **操作系统+计算机网络+数据库 + 算法** 这三个是问的最多的,所以我写的比较详细,对于学习计算机基础,不为了面试的话,我觉得下面的也及其重要。我分出来说,是为了那些要急着面试的人,可以重点学习下上面这四个。 114 | 115 | #### 五、汇编 116 | 117 | 我觉得,如何有时间,学习下汇编是必须的,学习了汇编,能够更好着帮助我们知道计算机是如何处理程序代码的,例如寄存器和内存是如何使用的?循环、函数调用、数组是如何实现的?地址是怎么一回事?等等。 118 | 119 | 很多二进制代码是可以反编译成汇编的,如何你会汇编,那么可以帮助我们更好着去理解一些东西。所以这里建议大家学习下汇编,并且要动手写一些程序。 120 | 121 | 122 | 123 | 对于汇编的资料,我可能没啥好推荐的,自己看的不多。看过两本书,对于入门的,我建议看 王爽的那本书《汇编语言(第三版)》,不过这本只适合入门,如果想继续,可以看《汇编程序设计》。 124 | 125 | #### 六、编译原理 126 | 127 | 说实话,编译原理还挺难,反正我觉得很难,不过有时间我觉得可以学学,学了这个你可以知道我们的编译器如何分析我们的代码的,例如词法分析,语法分析,语义分析等等。当然,你未来可能会自己写个特定分析代码的编译器也不一定,这个时候,就更加需要学了。 128 | 129 | 对于学习的资料,我觉得可以看视频 + 书。视频的话中国 mooc 大学搜索即可,书的话,说时候,我也看的不多,只看过学校指定的教材,所以这里给不了多少建议,自己当当自行搜索,哪本热门卖哪本勒。 130 | 131 | #### 总结 132 | 133 | 暂时先介绍这么多吧,说实话,学了这些,不单单是多学了一门知识,更重要的是可以提升你的罗辑思维,给你带来更多的 idea。在之后我的公众号里,我也是主要写计算机基础 + 算法。而这些,是值得每一个程序员去学习的,无论你是什么岗位。而且知识知识学了之后,你去学习其他知识,我相信可以上手的更快滴。 134 | 135 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 136 | 137 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /学操作系统/记一次面试:进程之间究竟有哪些通信方式?如何通信?.md: -------------------------------------------------------------------------------- 1 | 有一次面试的时候,被问到进程之间有哪些通信方式,不过由于之前没深入思考且整理过,说的并不好。想必大家也都知道进程有哪些通信方式,可是我猜很多人都是靠着”背“来记忆的,所以今天的这篇文章,讲给大家详细着讲解他们是如何通信的,让大家尽量能够理解他们之间的区别、优缺点等,这样的话,以后面试官让你举例子,你也能够顺手拈来。 2 | 3 | #### 1、管道 4 | 5 | 我们来看一条 Linux 的语句 6 | ``` 7 | netstat -tulnp | grep 8080 8 | ``` 9 | 学过 Linux 命名的估计都懂这条语句的含义,其中”|“是**管道**的意思,它的作用就是把前一条命令的输出作为后一条命令的输入。在这里就是把 netstat -tulnp 的输出结果作为 grep 8080 这条命令的输入。如果两个进程要进行通信的话,就可以用这种**管道**来进行通信了,并且我们可以知道这条**竖线**是没有名字的,所以我们把这种通信方式称之为**匿名管道**。 10 | 11 | 并且这种通信方式是**单向**的,只能把第一个命令的输出作为第二个命令的输入,如果进程之间想要互相通信的话,那么需要创建两个管道。 12 | 13 | 居然有匿名管道,那也意味着有**命名**管道,下面我们来创建一个命名管道。 14 | 15 | ``` 16 | mkfifo test 17 | 18 | ``` 19 | 这条命令创建了一个名字为 test 的命名管道。 20 | 21 | 接下来我们用一个进程向这个管道里面写数据,然后有另外一个进程把里面的数据读出来。 22 | 23 | ``` 24 | echo "this is a pipe" > test // 写数据 25 | ``` 26 | 这个时候管道的内容没有被读出的话,那么这个命令就会一直停在这里,只有当另外一个进程把 test 里面的内容读出来的时候这条命令才会结束。接下来我们用另外一个进程来读取 27 | ``` 28 | cat < test // 读数据 29 | ``` 30 | 我们可以看到,test 里面的数据被读取出来了。上一条命令也执行结束了。 31 | 32 | 从上面的例子可以看出,管道的通知机制类似于**缓存**,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是**单向传输的。** 33 | 34 | 这种通信方式有什么缺点呢?显然,这种通信方式**效率低下**,你看,a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。 35 | 36 | 所以管道不适合频繁通信的进程。当然,他也有它的优点,例如比较简单,能够保证我们的数据已经真的被其他进程拿走了。我们平时用 Linux 的时候,也算是经常用。 37 | 38 | #### 2、消息队列 39 | 40 | 那我们能不能把进程的数据放在某个内存之后就马上让进程返回呢?无需等待其他进程来取就返回呢? 41 | 42 | 答是可以的,我们可以用**消息队列**的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的 43 | 消息队列里取出来。同理,b 进程要个 a 进程发送消息也是一样。这种通信方式也类似于**缓存**吧。 44 | 45 | 这种通信方式有缺点吗?答是有的,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味**发送消息(拷贝)**这个过程需要花很多时间来读内存。 46 | 47 | 哪有没有什么解决方案呢?答是有的,请继续往下看。 48 | 49 | #### 3、共享内存 50 | 51 | **共享内存**这个通信方式就可以很好着解决**拷贝**所消耗的时间了。 52 | 53 | 这个可能有人会问了,每个进程不是有自己的独立内存吗?两个进程怎么就可以共享一块内存了? 54 | 55 | 我们都知道,系统加载一个进程的时候,分配给进程的内存并不是**实际物理内存**,而是**虚拟内存空间**。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了**内存共享**机制了。 56 | 57 | #### 4、信号量 58 | 59 | 共享内存最大的问题是什么?没错,就是多进程竞争内存的问题,就像类似于我们平时说的**线程安全**问题。如何解决这个问题?这个时候我们的**信号量**就上场了。 60 | 61 | 信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问**内存1**的时候,我们就把信号量的值设为 0,然后进程b 也要来访问**内存1**的时候,看到信号量的值为 0 就知道已经有进程在访问**内存1**了,这个时候进程 b 就会访问不了**内存1**。所以说,信号量也是进程之间的一种通信方式。 62 | 63 | #### 5、Socket 64 | 65 | 上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗? 66 | 67 | 答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。 68 | 69 | #### 总结 70 | 71 | 所以,进程之间的通信方式有: 72 | 73 | 1、管道 74 | 75 | 2、消息队列 76 | 77 | 3、共享内存 78 | 79 | 4、信号量 80 | 81 | 5、Socket 82 | 83 | 讲到这里也就完结了,之前我看进程之间的通信方式的时候,也算是死记硬背,并没有去理解他们之间的关系,优缺点,为什么会有这种通信方式。所以最近花点时间去研究了一下, 84 | 整理了这篇文章,相信看完这篇文章,你就可以更好着理解各种通信方式的由来的。 85 | 86 | 唠叨一下,最近有点对不住各位,好久没写原创文章了。有点偷懒,哈哈。不过呢,我接下来会好好写文章的了,希望大家多多支持。 87 | 88 | 89 | 90 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 91 | 92 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /学数据结构/堆排序是什么鬼?.md: -------------------------------------------------------------------------------- 1 | 排序算法相必大家都见过很多种,例如快速排序、归并排序、冒泡排序等等。今天,我们就来**简单**讲讲**堆排序**。 2 | 3 | #### 用辅助数组来实现堆排序算法 4 | 5 | 假如给你一个二叉堆,根据二叉堆的特性,你会怎么使用二叉堆来实现堆排序呢? 6 | 7 | 我们都知道,二叉堆有一个很特殊的节点 --- **堆顶**,堆顶要嘛是所有节点的**最大元素**,要嘛是**最小元素**,这主要取决于这个二叉堆是**最小堆**还是**最大堆**。 8 | 9 | 今天,我们暂且选择以**最小堆**来作为例子。 10 | 11 | 基于堆顶这个特点,我们就可以来实现我们的堆排序了。 12 | 13 | 大家看下面一个例子: 14 | 15 | 16 | 对于一个如图有10个节点元素的二叉堆: 17 | 18 | 19 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620d696deed420?w=576&h=370&f=png&s=17473) 20 | 21 | 22 | 23 | 我们把堆顶这个节点删除,然后把删除的节点放在一个辅助数组help里。 24 | 25 | 26 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620d6caff392bf?w=763&h=510&f=png&s=23802) 27 | 28 | 显然,这个被删除的节点,是堆中最小的节点。接下来,我们继续删除二叉堆的堆顶,然后把删除的元素还是存放在help数组里。 29 | 30 | 31 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620da1c2830fb7?w=772&h=501&f=png&s=23624) 32 | 33 | 34 | 显然,第二次删除的节点,是原始二叉堆中的第二小节点。 35 | 36 | 继续删除 37 | 38 | 39 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620da867c14793?w=746&h=498&f=png&s=22644) 40 | 41 | 42 | 继续连续6次删除堆顶,把删除的节点一次放入help数组。 43 | 44 | 45 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620daa7383fd94?w=808&h=517&f=png&s=24876) 46 | 47 | 48 | 二叉堆中只剩最后一个节点了,这个节点同时也是原始二叉堆中的最大节点,把这个节点继续删除了,还是放入help数组里。 49 | 50 | 51 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620dabcaa009c5?w=780&h=516&f=png&s=24496) 52 | 53 | 此时,二叉堆的元素被删除光了,观察一下help数组。这是一个**有序的数组**,实际上,通过从二叉堆的堆顶逐个取出最小值,存放在另一个辅助的数组里,当二叉堆被取光之时,我们就完成了一次堆排序了。 54 | 55 | #### 其实无需辅助数组 56 | 57 | 58 | 在上面的堆排序过程中,我们使用了一个辅助数组help。可事实上,我们真的需要辅助数组吗? 59 | 60 | 61 | 上篇讲二叉堆的时候,我们说过。二叉堆在实现的时候,是采取**数组**的形式来存储的。 62 | 63 | 从二叉堆中删除一个元素,为了充分利用空间,其实我们是可以把删除的元素直接存放在二叉堆的最后一个元素那里的。例如: 64 | 65 | 66 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620db129fea47a?w=612&h=402&f=png&s=18016) 67 | 68 | 删除堆顶,把删除的元素放在最后一个元素。 69 | 70 | 71 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620db4364ec6c9?w=611&h=376&f=png&s=18594) 72 | 73 | 继续删除,把删除的元素放在最后第二个位置 74 | 75 | 76 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620dc1696e75af?w=619&h=369&f=png&s=18704) 77 | 78 | 继续删除,把删除的元素放在最后第三个位置 79 | 80 | 81 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620dc9e39a8541?w=664&h=378&f=png&s=18189) 82 | 83 | 以此类推.... 84 | 85 | 86 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620dcbfb96059f?w=663&h=378&f=png&s=18975) 87 | 88 | 这样,对于一个含有n个元素的二叉堆,经过n-1(不用删除n次)次删除之后,这个数组就是一个有序数组了。 89 | 90 | 91 | ![](https://user-gold-cdn.xitu.io/2018/9/28/16620dce4bff3378?w=669&h=382&f=png&s=18889) 92 | 93 | 94 | 95 | 所以,给你一个无序的数组,我们需要把这个数组构建成**二叉堆**,然后在通过堆顶逐个删除的方式来实现堆排序。 96 | 97 | 其实,也不算是删除了,相当于是把堆顶的元素与堆尾部在交换位置,然后在通过下沉的方式,把二叉树恢复成二叉堆。 98 | 99 | 代码如下: 100 | 101 | ```java 102 | public class HeapSort { 103 | /** 104 | * 下沉操作,执行删除操作相当于把最后 105 | * * 一个元素赋给根元素之后,然后对根元素执行下沉操作 106 | * @param arr 107 | * @param parent 要下沉元素的下标 108 | * @param length 数组长度 109 | */ 110 | public static int[] downAdjust(int[] arr, int parent, int length) { 111 | //临时保证要下沉的元素 112 | int temp = arr[parent]; 113 | //定位左孩子节点位置 114 | int child = 2 * parent + 1; 115 | //开始下沉 116 | while (child < length) { 117 | //如果右孩子节点比左孩子小,则定位到右孩子 118 | if (child + 1 < length && arr[child] > arr[child + 1]) { 119 | child++; 120 | } 121 | //如果父节点比孩子节点小或等于,则下沉结束 122 | if (temp <= arr[child]) 123 | break; 124 | //单向赋值 125 | arr[parent] = arr[child]; 126 | parent = child; 127 | child = 2 * parent + 1; 128 | } 129 | arr[parent] = temp; 130 | return arr; 131 | } 132 | 133 | //堆排序 134 | public static int[] heapSort(int[] arr, int length) { 135 | //构建二叉堆 136 | for (int i = (length - 2) / 2; i >= 0; i--) { 137 | arr = downAdjust(arr, i, length); 138 | } 139 | //进行堆排序 140 | for (int i = length - 1; i >= 1; i--) { 141 | //把堆顶的元素与最后一个元素交换 142 | int temp = arr[i]; 143 | arr[i] = arr[0]; 144 | arr[0] = temp; 145 | //下沉调整 146 | arr = downAdjust(arr, 0, i); 147 | } 148 | return arr; 149 | } 150 | //测试 151 | public static void main(String[] args) { 152 | int[] arr = new int[]{1, 3, 5,2, 0,10,6}; 153 | System.out.println(Arrays.toString(arr)); 154 | arr = heapSort(arr, arr.length); 155 | System.out.println(Arrays.toString(arr)); 156 | } 157 | } 158 | 159 | ``` 160 | 161 | 162 | 对于堆的时间复杂度,我就直接给出了,有兴趣的可以自己推理下,还是不难的。堆的时间复杂度是 O (nlogn)。空间复杂度是 O(1)。 163 | 164 | 这里可能大家会问,堆排序的时间复杂度是O (nlogn),像快速排序,归并排序的时间复杂度也是 O(nlogn),那我在使用的时候该如何选择呢? 165 | 166 | > 这里说明一下:快速排序是平均复杂度 O(logn),实际上,快速排序的最坏时间复杂度是O(n^2。),而像归并排序,堆排序,都稳定在O(nlogn) 167 | 168 | 169 | 我给出一个问题,例如给你一个拥有n个元素的无序数组,要你找出第 k 个大的数,那么你会选择哪种排序呢? 170 | 171 | 显然在这个问题中,选用堆排序是最好的,我们不用把数组全部排序,只需要排序到前k个数就可以了。至于代码如何实现,这个我就不给代码了,大家可以动手敲一敲。 172 | 173 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 174 | 175 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 176 | 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /学数据结构/腾讯面试题:有了二叉查找树,平衡树为啥还需要红黑树?.md: -------------------------------------------------------------------------------- 1 | 红黑树算是很难的一种数据结构吧,一般很少考察**插入**、**删除**等具体操作步骤,如果遇到要你手写红黑树的面试官,就直接**告辞**吧。所以,更多是会考察你对红黑树的理解程度,考察的最多的估计就是**为什么有了二查找查找树/平衡树还需要红黑树**这个问题了,今天,你只需要花一分钟的时间,就知道怎么回答这个问题了。 2 | 3 | #### 1、二叉查找树的缺点 4 | 5 | 二叉查找树,相信大家都接触过,二叉查找树的特点就是**左子树的节点值比父亲节点小,而右子树的节点值比父亲节点大**,如图 6 | 7 | 8 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b4161f747bade1?w=1071&h=544&f=png&s=177182) 9 | 基于二叉查找树的这种特点,我们在查找某个节点的时候,可以采取类似于**二分查找**的思想,快速找到某个节点。n 个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logn)。 10 | 11 | 之所以说是**正常情况下**,是因为二叉查找树有可能出现一种极端的情况,例如 12 | 13 | 14 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b41629590b19fe?w=822&h=656&f=png&s=102114) 15 | 这种情况也是满足**二叉查找树**的条件,然而,此时的二叉查找树已经近似退化为一条链表,这样的二叉查找树的查找时间复杂度顿时变成了 O(n),可想而知,我们必须不能让这种情况发生,为了解决这个问题,于是我们引申出了**平衡二叉树**。 16 | 17 | #### 2、平衡二叉树 18 | 19 | 平衡二叉树就是为了解决二叉查找树退化成一颗链表而诞生了,平衡树具有如下特点 20 | 21 | 1、具有二叉查找树的全部特性。 22 | 23 | 2、每个节点的左子树和右子树的高度差至多等于1。 24 | 25 | 例如:图一就是一颗平衡树了,而图二则不是(节点右边标的是这个节点的高度) 26 | 27 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b41648d3e684ec?w=648&h=323&f=png&s=47379)。 28 | 29 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b4164b238b1494?w=719&h=424&f=png&s=60720) 30 | 对于图二,因为节点9的左孩子高度为2,而右孩子高度为0。他们之间的差值超过1了。 31 | 32 | 平衡树基于这种特点就可以保证不会出现大量节点偏向于一边的情况了。关于平衡树如何构建、插入、删除、左旋、右旋等操作这里不在说明,具体可以看我之前写的一篇文章:[【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。](https://mp.weixin.qq.com/s/dYP5-fM22BgM3viWg4V44A) 33 | 34 | 35 | 36 | 于是,通过平衡树,我们解决了二叉查找树的缺点。对于有 n 个节点的平衡树,最坏的查找时间复杂度也为 O(logn)。 37 | 38 | #### 为什么有了平衡树还需要红黑树? 39 | 40 | 虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求**每个节点的左子树和右子树的高度差至多等于1**,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过**左旋**和**右旋**来进行调整,使之再次成为一颗符合要求的平衡树。 41 | 42 | 显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了**红黑树**,红黑树具有如下特点: 43 | 44 | 1、具有二叉查找树的特点。 45 | 46 | 2、根节点是黑色的; 47 | 48 | 3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据。 49 | 50 | 4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的。 51 | 52 | 5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。 53 | 54 | 例如下面的图片(注意,图片中黑色的、空的叶子节点没有画出)(图片来自极客时间) 55 | 56 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b4186cd8d2bbd8?w=1142&h=473&f=png&s=118284) 57 | 58 | 正是由于红黑树的这种特点,使得它能够在最坏情况下,也能在 O(logn) 的时间复杂度查找到某个节点。至于为什么就能够保证时间复杂度为 O(logn),我这里就不细讲了,后面的文章可能会讲。 59 | 60 | 不过,与平衡树不同的是,红黑树在插入、删除等操作,**不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整**,这也是我们为什么大多数情况下使用红黑树的原因。 61 | 62 | 不过,如果你要说,单单在查找方面的效率的话,平衡树比红黑树快。 63 | 64 | 所以,我们也可以说,**红黑树是一种不大严格的平衡树**。也可以说是一个折中发方案。 65 | 66 | 如果我上面讲的,你都懂,都能够在面试中说出来,应该是足够的了。我当时就是这么回答的。 67 | 68 | #### 总结 69 | 70 | 所以,最后的答案是,平衡树是为了解决二叉查找树退化为链表的情况,而红黑树是为了解决平衡树在插入、删除等操作需要频繁调整的情况。 71 | 72 | 不过,红黑树还有挺多其他的知识点可以考,例如红黑树有哪些应用场景?向集合容器中 HashMap,TreeMap 等,内部结构就用到了红黑树了。还有构建一棵节点个数为 n 的红黑树,时间复杂度是多少?红黑树与哈希表在不同应该场景的选择?红黑树有哪些性质?红黑树各种操作的时间复杂度是多少? 73 | 74 | 如果你把这些都弄懂了,应该就差不多可以的了,后面有时间的话,我给大家详细讲一下这些题,这里最好是要求理解,而不是死记硬背。 75 | 76 | 77 | 78 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 79 | 80 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /学数据结构/高频面试:什么是B树?为啥文件索引要用B树而不用二叉查找树?.md: -------------------------------------------------------------------------------- 1 | #### 一、面试被怼 2 | 3 | 面试官:你知道文件索引、数据库索引一般用什么数据结构来存储吗? 4 | 5 | 小秋:知道啊,一般都是用**树形结构**来存储的。 6 | 7 | 面试官:可以说说为啥用树形结构来存储吗? 8 | 9 | 小秋:树形结构例如想 B 树,B+ 树,二叉查找树都是有序的,所以查询效率很高,可以再 O(logn) 的时间复杂度查找到目标数据。 10 | 11 | 面试官:那可以问问文件索引,例如数据库索引一般用哪种树形结构吗? 12 | 13 | 小秋:大部分用 B+ 树,少部分用 B 树。(B和B+树太他么复杂了,幸好背了下面试题,嘻嘻) 14 | 15 | 面试官:想问下为什呀要用 B 树而不用二叉查找树啊?或者为啥不用哈希表啊?哈希表的查找速度也很快呀。 16 | 17 | 小秋:哈希表虽然能够再 O(1) 查找到目标数据,不过如果我们要进行**模糊查找**的话,却只能遍历所有数据,并且如果出现了极端情况,哈希表冲突的元素太多,也会导致线性时间的查找效率的。 18 | 19 | 面试官:那为啥不用二叉查找树呢? 20 | 21 | 小秋:这个…..其实我也不知道,当时是再某个面试题中看到的答案的,嘻嘻。 22 | 23 | 面试官:那你可以回去等通知了…. 24 | 25 | #### 二、为啥不用二叉查找树呢? 26 | 27 | 小秋被怼后有点沮丧,跑过来问帅地关于 B 树的一些知识以及心中的疑问。 28 | 29 | 小秋:今天去面试,面试问我,为啥文件索引要用 B 树而不用二叉查找树,然后我想了下,感觉如果这是一颗比较平衡的二叉查找树的话,那么查找效率是非常快的,难度 B 树还能比它更快吗? 30 | 31 | 帅地:确实,如果是查找效率(即比较次数)的话,实际上二叉树可以说是最快的了,但是,我们的文件索引是存放在**磁盘**上的,所以我们不仅要考虑查找效率,还要考虑**磁盘的寻址加载次数**哦,而这也是我们为什么要用 B 树的原因。 32 | 33 | 小秋:难道二叉查找树会导致磁盘的加载次数更多吗?可以给我详细讲讲吗? 34 | 35 | 帅地:可以呀,不过听懂了,觉得我讲的不错,你要记得给我**多点赞,转发**哦。 36 | 37 | 小秋:绝对没问题。 38 | 39 | #### 三、什么是 B 树? 40 | 41 | 帅地:要讲懂这个问题,我们先来了解一下什么是 B 树,其实,B 树和二叉查找树一样,都是**树**,**B 树**相当于是一棵**多叉查找树**,对于一棵 m 阶的 B 树具有如下特性: 42 | 43 | 1、根节点至少有两个孩子。 44 | 45 | 2、每个中间节点都包含 k - 1 个元素和 k 个孩子,其中 m/2 <= k <= m。 46 | 47 | 3、每一个叶子节点都包含 k - 1 个元素,其中 m/2 <= k <= m。 48 | 49 | 4、所有的叶子节点都位于同一侧。 50 | 51 | 5、每个节点中的元素从小到大排列,节点当中的 k - 1 个元素正好是 k 个孩子包含的元素的值域划分。 52 | 53 | 小秋:我去,这么复杂,鬼才记得住,我还是选择不学了,呜呜。 54 | 55 | 帅地:你别着急,这些规则我也记不住,只是让你大致知道一些这些规则,一般情况下,我们并不需要把它的规则完全背起来滴。为了加深理解,我给你举个 B 树的例子吧。例如: 56 | 57 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4abadbacb9d4?w=902&h=534&f=png&s=107021) 58 | 59 | 图中是一棵m = 3 的 3 阶 B 树,可以看出,树中有些节点是有**多个元素**的,并且和二叉查找树一样,左节点的所有元素的值都比父亲元素小。例如对于(3, 7)这个节点。两个元素把这个节点分割成**三个值域**,即可以有 3 个孩子。2 相当于 3 的左孩子节点,而 (4,6)相当于 3 的右孩子,同时也是 7 的左孩子,而 9 是 7 的右孩子。 60 | 61 | 和二叉查找树还是很相似滴,都是有序,且左孩子小,右孩子大,只是 B 树的一个节点可以有**多个元素**,并且有多个分支。而这些分支以及元素的数量规则,可以从上面的五个规则中查找哈。说实话,我也懒的记那些规则,只知道个大概以及 B 树的应用即可。 62 | 63 | > 文章来源于微信公众:『苦逼的码农』,更多文章可搜索关注 64 | 65 | #### 四、小秋的疑惑 66 | 67 | 小秋:我知道了,不过这种多叉的树,真的可以比二叉查找树效率更好吗,我怎么觉得不可以呢? 68 | 69 | 帅地:那你可以说说哦。 70 | 71 | 小秋:例如,上面的 B 树有 11 个元素,按照这 11 个元素,我们建立一个如下的二叉查找树,如图(我去,这个图话了好长时间,ppt 画的) 72 | 73 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4bce373a8032?w=986&h=602&f=png&s=159334) 74 | 75 | 76 | **下面我们来进行查询效率比较** 77 | 78 | 79 | **1、在 B 树中的查找次数**。 80 | 现在假如我们要查询元素 9,对于 B 树,我们需要进行**4次比较**,例如: 81 | 第一次比较: 10 比较,比 10 小,所以再 10 的左孩子找。 82 | 83 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4c1c993947bb?w=898&h=464&f=png&s=97862) 84 | 85 | 第二、三次比较:和 3 比较,比 3 大,这个时候我们还得和 7 比较。 86 | 87 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4c42c80d36ba?w=846&h=566&f=png&s=109649) 88 | 89 | 第四次比较:和 9 比较,相等,找到目标树,返回。 90 | 91 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4c4cbe7ae6d1?w=820&h=536&f=png&s=108179) 92 | 93 | 所以最终的结果需要 4 次比较。 94 | 95 | **2、在二叉树的比较结果** 96 | 97 | 为了节省篇幅,我就不逐个比较了,相信你也一眼就看出来了,也是需要 4 次比较。如图 98 | 99 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4c75aa82ecc6?w=994&h=524&f=png&s=157933) 100 | 101 | 小秋:同样都是四次比较,而且,B 树的每一个节点,如果存放的元素比较多,那么 B 树的比较次数会更多,为什么就说 B 的效率比 二叉查找树快呢? 102 | 103 | #### 五、解决疑惑 104 | 105 | 帅地:确实,如果单单从比较次数看的话,二叉查找树确实不比 B 树差,不过你忽略了一个很重要的点,那就是**磁盘的寻址加载次数**。 106 | 107 | 我们知道,在把磁盘里的数据加载到内存中的时候,是以**页**为单位来加载的,而我们也知道,**节点与节点之间的数据是不连续的**,所以不同的节点,很有可能分布在不同的**磁盘页**中。所以对于上面的二叉查找树,我们可能需要进行 4 次寻址加载,如图: 108 | 109 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4d19aefd2920?w=992&h=536&f=png&s=170885) 110 | 111 | 而对于 B 树,由于 B 树的每一个节点,可以存放多个元素,所以磁盘寻址加载的次数会比较少,例如上面的例子中,用 B 树的话,只需要加载 3 次,如图: 112 | 113 | ![](https://user-gold-cdn.xitu.io/2019/10/16/16dd4d567dabef7f?w=826&h=522&f=png&s=115650) 114 | 115 | 我们都知道,在内存的运算速度是非常快的,至少比磁盘的寻址加载速度,快了几百倍,而我们进行数值比较的时候,是在内存中进行的,虽然 B 树的比较次数可能比二叉查找树多,但是磁盘操作次数少,所以总体来说,还是 B 树快的多,这也是为什么我们用使用 B 树来存储的原因。 116 | 117 | 小秋:原来这样啊,以前一直蒙在鼓里。 118 | 119 | > 文章来源于微信公众:『苦逼的码农』,更多文章可搜索关注 120 | 121 | 帅地:不知道你发现没有,实际上磁盘的加载次数,基本上是和树的**高度**相关联的,高度越高,加载次数越多,越矮,加载次数越少。所以对于这种文件索引的存储,我们一般会选择**矮胖**的树形结构。例如有 1000 个元素,如果是二叉查找树的话,高度可能高达 10 层,而如果用 10 阶 B 树的话,只需要三四层即可。 122 | 123 | 小秋:终于搞懂了,不过我还有个疑问,大部分文件索引或者数据库索引都是用 B+ 树的,而只有小部分才用 B 树,可以问下为什要用 B+ 树而不用 B 树吗?还有,B 树还有其他的应用吗? 124 | 125 | 帅地:B 树处于会用在少部分的文件索引(数据库索引)外,应用的最多的就是**文件系统**了。至于为什呀要用 B 树而不用 B+ 树,为什么数据库索引大部分用 B+ 树而不用 B 树,我们下节再讲了。 126 | 127 | 小秋:那我期待着。 128 | 129 | 帅地:如果觉得有收获,可以帮忙多多转发,点赞,分享哦,这也是我写文章的动力来源。 130 | 131 | #### 总结 132 | 133 | 关于 B 树和 B+ 树,在面试的过程中,还是问的挺多滴,特别是问到数据库的时候,基本会问索引,进而问到 B+ 树,从而也会扯到 B 树。所以掌握着两种树的应用以及原理,是非常重要的,虽然他们的规则很复杂,但是如果是应付面试等,其实不需要背那些规则,只需要知道大概以及知道他们的原理即可。 134 | 135 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 136 | 137 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /学算法/再现校招算法面试现场/一道阿里笔试题:我是如何用一行代码解决约瑟夫环问题的.md: -------------------------------------------------------------------------------- 1 | 约瑟夫环问题算是很经典的题了,估计大家都听说过,然后我就在一次笔试中遇到了,下面我就用 3 种方法来详细讲解一下这道题,最后一种方法学了之后保证让你可以让你装逼。 2 | 3 | > 问题描述:编号为 1-N 的 N 个士兵围坐在一起形成一个圆圈,从编号为 1 的士兵开始依次报数(1,2,3...这样依次报),数到 m 的 士兵会被杀死出列,之后的士兵再从 1 开始报数。直到最后剩下一士兵,求这个士兵的编号。 4 | 5 | #### 1、方法一:数组 6 | 7 | 在大一第一次遇到这个题的时候,我是用数组做的,我猜绝大多数人也都知道怎么做。方法是这样的: 8 | 9 | 用一个数组来存放 1,2,3 ... n 这 n 个编号,如图(这里我们假设n = 6, m = 3) 10 | 11 | ![](https://user-gold-cdn.xitu.io/2019/7/9/16bd6ca7d460c9ec?w=589&h=115&f=png&s=4292) 12 | 然后不停着遍历数组,对于被选中的编号,我们就做一个标记,例如编号 arr[2] = 3 被选中了,那么我们可以做一个标记,例如让 arr[2] = -1,来表示 arr[2] 存放的编号已经出局的了。 13 | 14 | ![](https://user-gold-cdn.xitu.io/2019/7/9/16bd6cbeb8d77991?w=567&h=242&f=png&s=9985) 15 | 16 | 然后就按照这种方法,不停着遍历数组,不停着做标记,直到数组中只有一个元素是非 -1 的,这样,剩下的那个元素就是我们要找的元素了。我演示一下吧: 17 | 18 | ![](https://user-gold-cdn.xitu.io/2019/7/9/16bd6d147c98dc83?w=685&h=699&f=png&s=32046) 19 | 20 | 这种方法简单吗?思路简单,但是编码却没那么简单,临界条件特别多,每次遍历到数组最后一个元素的时候,还得重新设置下标为 0,并且遍历的时候还得判断该元素时候是否是 -1。感兴趣的可以动手写一下代码,用这种数组的方式做,千万不要觉得很简单,编码这个过程还是挺考验人的。 21 | 22 | 这种做法的时间复杂度是 O(n * m), 空间复杂度是 O(n); 23 | 24 | #### 2、方法二:环形链表 25 | 26 | 学过链表的人,估计都会用链表来处理约瑟夫环问题,用链表来处理其实和上面处理的思路差不多,只是用链表来处理的时候,对于被选中的编号,不再是**做标记**,而是**直接移除**,因为从链表移除一个元素的时间复杂度很低,为 O(1)。当然,上面数组的方法你也可以采用移除的方式,不过数组移除的时间复杂度为 O(n)。所以采用链表的解决方法如下: 27 | 28 | 1、先创建一个环形链表来存放元素: 29 | 30 | ![](https://user-gold-cdn.xitu.io/2019/7/9/16bd6e1dc843b877?w=638&h=191&f=png&s=8000) 31 | 32 | 2、然后一边遍历链表一遍删除,直到链表只剩下一个节点,我这里就不全部演示了 33 | 34 | 35 | ![](https://user-gold-cdn.xitu.io/2019/7/9/16bd6e390dcb24a6?w=701&h=412&f=png&s=19574) 36 | 37 | 代码如下: 38 | ```java 39 | // 定义链表节点 40 | class Node{ 41 | int date; 42 | Node next; 43 | 44 | public Node(int date) { 45 | this.date = date; 46 | } 47 | } 48 | ``` 49 | 核心代码 50 | ```java 51 | public static int solve(int n, int m) { 52 | if(m == 1 || n < 2) 53 | return n; 54 | // 创建环形链表 55 | Node head = createLinkedList(n); 56 | // 遍历删除 57 | int count = 1; 58 | Node cur = head; 59 | Node pre = null;//前驱节点 60 | while (head.next != head) { 61 | // 删除节点 62 | if (count == m) { 63 | count = 1; 64 | pre.next = cur.next; 65 | cur = pre.next; 66 | } else { 67 | count++; 68 | pre = cur; 69 | cur = cur.next; 70 | } 71 | } 72 | return head.date; 73 | } 74 | 75 | static Node createLinkedList(int n) { 76 | Node head = new Node(1); 77 | Node next = head; 78 | for (int i = 2; i <= n; i++) { 79 | Node tmp = new Node(i); 80 | next.next = tmp; 81 | next = next.next; 82 | } 83 | // 头尾串联 84 | next.next = head; 85 | return head; 86 | } 87 | ``` 88 | 89 | 这种方法估计是最多人用的,时间复杂度为 O(n * m),空间复杂度是 O(n)。 90 | 91 | 还有更好的方法吗?答有,请往下看 92 | 93 | #### 方法三:递归 94 | 95 | 其实这道题还可以用递归来解决,递归是思路是**每次我们删除了某一个士兵之后,我们就对这些士兵重新编号,然后我们的难点就是找出删除前和删除后士兵编号的映射关系**。 96 | 97 | 我们定义递归函数 f(n,m) 的返回结果是存活士兵的编号,显然当 n = 1 时,f(n, m) = 1。假如我们能够找出 f(n,m) 和 f(n-1,m) 之间的关系的话,我们就可以用递归的方式来解决了。我们假设人员数为 n, 报数到 m 的人就自杀。则刚开始的编号为 98 | 99 | … 100 | 1 101 | ... 102 | m - 2 103 | 104 | m - 1 105 | 106 | m 107 | 108 | m + 1 109 | 110 | m + 2 111 | ... 112 | n 113 | … 114 | 115 | 进行了一次删除之后,删除了编号为 m 的节点。删除之后,就只剩下 n - 1 个节点了,删除前和删除之后的编号转换关系为: 116 | 117 | 删除前 --- 删除后 118 | 119 | … --- … 120 | 121 | m - 2 --- n - 2 122 | 123 | m - 1 --- n - 1 124 | 125 | m ---- 无(因为编号被删除了) 126 | 127 | m + 1 --- 1(因为下次就从这里报数了) 128 | 129 | m + 2 ---- 2 130 | 131 | … ---- … 132 | 133 | 新的环中只有 n - 1 个节点。且删除前编号为 m + 1, m + 2, m + 3 的节点成了删除后编号为 1, 2, 3 的节点。 134 | 135 | 假设 old 为删除之前的节点编号, new 为删除了一个节点之后的编号,则 old 与 new 之间的关系为 old = (new + m - 1) % n + 1。 136 | 137 | > 注:有些人可能会疑惑为什么不是 old = (new + m ) % n 呢?主要是因为编号是从 1 开始的,而不是从 0 开始的。如果 new + m == n的话,会导致最后的计算结果为 old = 0。所以 old = (new + m - 1) % n + 1. 138 | 这样,我们就得出 f(n, m) 与 f(n - 1, m)之间的关系了,而 f(1, m) = 1.所以我们可以采用递归的方式来做。代码如下: 139 | 140 | ```java 141 | int f(int n, int m){ 142 | if(n == 1) return n; 143 | return (f(n - 1, m) + m - 1) % n + 1; 144 | } 145 | ``` 146 | 我去,两行代码搞定,而且时间复杂度是 O(n),空间复杂度是O(1),牛逼!那如果你想跟别人说,我想一行代码解决约瑟夫问题呢?答是没问题的,如下: 147 | ```java 148 | int f(int n, int m){ 149 | return n == 1 ? n : (f(n - 1, m) + m - 1) % n + 1; 150 | } 151 | ``` 152 | 卧槽,以后面试官让你手写约瑟夫问题,你就扔这一行代码给它。 153 | 154 | #### 总结 155 | 156 | 不过那次笔试时,并没有用递归的方法做,而是用链表的方式做,,,,,那时,不知道原来还能用一行代码搞定的,,,,欢迎各位大佬提供半行代码搞定的方法! 157 | 158 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 159 | 160 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /学算法/再现校招算法面试现场/前缀和的应用,从一道网易笔试题说起.md: -------------------------------------------------------------------------------- 1 | 8月3号参加了网易提前批的笔试,笔试时间 120 分钟,然后有 10 道选择题(20分), 4 道编程题(80分), 2 道主观题(20分)。可以说你编程题凉了那就基本凉了,其他做的再好也没有。所以时刻保持刷题还是很有必要。 2 | 3 | 4 | 5 | 这次网易的笔试题还是挺难,四道题都用到了不同的思想,可能你看了题目,然后看了别人的解析会感觉,咦,挺简单的,但是身处考场可能就完全不一样了,基本每道题给你的时间只有 20 分钟,而且还要我们自己处理**输入、输出**,由于平时大家刷 Leetcode 的时候都是自己给出个**方法**就可以了,无需考虑**输入、输出**,所以有些人对输入输出不是很熟悉,调试花了不少时间,所以我这里建议你一定要把标准**输入、输出**弄熟悉。 6 | 7 | 今天我要将的这道题是网易 8 月 3 号研发岗笔试的**第一题**,这道题涉及到**前缀和**的应用,所以我想借这道题给大家讲一讲前缀和相关的一些知识,以后大家遇到这道题,就可以快速秒杀了。 8 | 9 | #### 问题描述 10 | 11 | 下面我描述下这道题,不过我给的描述是**简化版**的,实际上再做笔试题的时候,每道题的描述都**巨长**,一般都会根据实际场景来给出问题的,有些人可能阅读了十几分钟,然后不知道自己要干嘛,我这里给出最简化的版本。 12 | 13 | 14 | 15 | > 有一个班级有 n 个人,给出 n 个元素,第 i 个元素代表 第 i 位同学的考试成绩,接下进行 m 次询问,每次询问给出一个数值 t ,表示第 t 个同学,然后需要我们输出第 t 个同学的成绩超过班级百分之几的人,百分数 p 可以这样算:p = (不超过第 t 个同学分数的人数 ) / n * 100%。输出的时候保留到小数点后 6 位,并且需要四舍五入。 16 | > 17 | > 输入描述:第一行输入两个数 n 和 m,两个数以空格隔开,表示 n 个同学和 m 次询问。第二行输入 n 个数值 ni,表示每个同学的分数,第三行输入 m 个数值mi,表示每次询问是询问第几个同学。(注意,这里 2<=n,m<=100000,0<=ni<=150,1<=mi<=n) 18 | > 19 | > 输出描述:输出 m 行,每一行输出一个百分数 p,代表超过班级百分之几的人。 20 | > 21 | > 示例1: 22 | > 23 | > 输入 : 24 | > 25 | > 3 2 26 | > 27 | > 50 60 70 28 | > 29 | > 1 2 30 | > 31 | > 输出 32 | > 33 | > 33.333333% 34 | > 35 | > 66.666667% 36 | 37 | 38 | 39 | 第一题大致是这样,不过不是和原题完全一样哈,再输入和输出有小许区别,以为你具体的输入输出我忘了。 40 | 41 | #### 解答 42 | 43 | 44 | 45 | 那么这道题难吗?说实话不难,不过你可以先自己再脑子里想想怎么做比较好,或许在考场上 20 分钟你还真不一定做的出来。 46 | 47 | 有些人说,这还不简单,每次询问的时候,我都遍历一下所有人的成绩,这样的花时间复杂度是 O(n * m),显然,如果你这样做,那么是一定通不过的,一定会超时。一般暴力法能够通过 20% ~ 30% 的测试用力,如果一道题 20 分的花,能拿到 4~6 分。如果你实在没思路,那么暴力也是个不错的选择。 48 | 49 | 50 | 51 | **1、二分法** 52 | 53 | 这道题我是用二分法做的,就是先对所有人的成绩进行排序,不过排序的时候我们需要开一个新的数组来存储。然后每次查询的时候可以通过二分查找进行匹配,由于用到了排序,需要花 O(nlogn) 的时间,m 次查询花的时间大致为 O(mlogn)。所以平均时间复杂度可以算是 O((m+n)logn)。这个时间复杂度通过所有测试用例,代码如下(没学过java的也能看懂): 54 | 55 | ```java 56 | public class Main { 57 | // 主函数,相当于c语言的main方法 58 | public static void main(String[] args){ 59 | Scanner in = new Scanner(System.in); 60 | int m = in.nextInt(); 61 | int n = in.nextInt(); 62 | // 存放成绩的数组 63 | int[] a = new int[n]; 64 | // 存放排序过后的成绩 65 | int[] b = new int[n]; 66 | // 输入 n 个人的成绩 67 | for(int i = 0; i < n; i++){ 68 | a[i] = in.nextInt(); 69 | b[i] = a[i]; 70 | } 71 | // 排序 72 | Arrays.sort(b); 73 | // 进行查询 74 | for(int j = 0; j < m; j++){ 75 | // 输入是要查询第几个同学 76 | int index = in.nextInt(); 77 | // 把这个同学的分数拿出来 78 | int fen = a[index - 1]; 79 | // 通过二分查找是排在第几位 80 | int sum = binarySearch(b, fen) - 1; 81 | double t = sum * 1.0 / n * 100; 82 | System.out.printf("%.6f\n", t); 83 | 84 | } 85 | } 86 | 87 | private static int binarySearch(int[] b, int fen){ 88 | int l = 0; 89 | int r = b.length - 1; 90 | while(l <= r){ 91 | int mid = l + (r - l) / 2; 92 | if(b[mid] > fen){ 93 | r --; 94 | }else if(b[mid] < fen){ 95 | l++; 96 | }else{ 97 | // 由于存在分数相同的人,所以还要往后查找 98 | while(mid < b.length && b[mid] == fen){ 99 | mid++; 100 | } 101 | return mid; 102 | } 103 | } 104 | return 0; 105 | } 106 | } 107 | ``` 108 | 109 | #### 前缀和 110 | 111 | 不过这道题更好的做法是采用**前缀和**来做。题设中每个同学的分数不超过 150,不小于 0,那么我们可以用一个数组 arr,然后让 arr[i] 表示分数不超过 i 的人数。通过这种方式,我们可以把时间复杂度控制再 O(n+m)。直接看代码吧,会更好理解(这里我就不写输入的代码了,用a[]存放成绩,m[]存放m次询问): 112 | 113 | ```java 114 | void f(int a[], int m[]){ 115 | int n = a.length; 116 | int[] arr = new int[151]; 117 | //先统计分数为 i 的有多少人 118 | for(int i = 0; i < n; i++){ 119 | arr[a[i]]++; 120 | } 121 | // 接着构造前缀和 122 | for(int i = 1; i < 151; i++){ 123 | arr[i] = arr[i] + arr[i-1]; 124 | } 125 | // 进行 m 次询问 126 | for(int i = 0; i < m.length; i++){ 127 | // 取出成绩 128 | int score = a[m[i]]; 129 | // 有多少人的成绩不超过 score的 130 | int sum = arr[score]; 131 | System.out.printf("%.6f\n", sum * 1.0 / n * 100 ); 132 | } 133 | } 134 | ``` 135 | 136 | 这种方法就叫做**前缀和**,用这种方法简单了很多。当然前缀和还有其他应用,例如: 137 | 138 | 如果给你一个数组 arr[],然后进行 m 次询问,每次询问输入两个数 i,j(i <= j)。要求你输出 arr[i] ~ arr[j] 这个区间的和。这个时候就可以使用前缀和的方式了。 139 | 140 | 141 | 142 | 前缀和看起来还是挺简单的,不过再做题中,或许会有意想不到的作用,例如这次的笔试,所以今天给大家讲解一下。 143 | 144 | 145 | 146 | 最后我再问你一个和前缀和类似的问题,给你一串长度为n的数列a1,a2,a3......an,要求对a[i]~a[j]进行m次操作: 147 | 148 | 操作一:将a[L]~a[R]内的元素都加上P 149 | 150 | 操作二:将a[L]~a[R]内的元素都减去P 151 | 152 | 最后再给出一个询问求a[i]-a[j]内的元素之和。 153 | 154 | 155 | 156 | 这个你会怎么做呢?这个时候就涉及到**差分**的知识了,关于这个知识的应用,我会在后面的文章讲。大家也可以模仿前缀和的思路,想想这道题怎么做。 157 | 158 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 159 | 160 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /学算法/再现校招算法面试现场/只用2GB内存从20亿,40亿,80亿个整数中找到出现次数最多的数.md: -------------------------------------------------------------------------------- 1 | 这几天小秋去面试了,不过最近小秋学习了不少和**位算法**相关文章,对于算法题还是有点信心的,,,,于是,发现了如下对话。 2 | 3 | #### 20亿级别 4 | 5 | 面试官:如果我给你 2GB 的内存,并且给你 20 亿个 int 型整数,让你来找出次数出现最多的数,你会怎么做? 6 | 7 | 小秋:(嗯?怎么感觉和之前的那道判断一个数是否出现在这 40 亿个整数中有点一样?可是,如果还是采用 bitmap 算法的话,好像无法统计一个数出现的次数,只能判断一个数是否存在),我可以采用哈希表来统计,把这个数作为 key,把这个数出现的次数作为 value,之后我再遍历哈希表哪个数出现最多的次数最多就可以了。 8 | 9 | 面试官:你可以算下你这个方法需要花费多少内存吗? 10 | 11 | 小秋:key 和 value 都是 int 型整数,一个 int 型占用 4B 的内存,所以哈希表的一条记录需要占用 8B,最坏的情况下,这 20 亿个数都是不同的数,大概会占用 16GB 的内存。 12 | 13 | 面试官:你的分析是对的,然而我给你的只有 2GB 内存。 14 | 15 | 小秋:(感觉这道题有点相似,不过不知为啥,没啥思路,这下凉凉),目前没有更好的方法。 16 | 17 | 面试官:按照你那个方法的话,最多只能记录大概 2 亿多条不同的记录,2 亿多条不同的记录,大概是 1.6GB 的内存。 18 | 19 | 小秋:(嗯?面试官说这话是在提示我?)我有点思路了,我可以把这 20 亿个数存放在不同的文件,然后再来筛选。 20 | 21 | 面试题:可以具体说说吗? 22 | 23 | 小秋:刚才你说,我的那个方法,最多只能记录大概 2 亿多条的不同记录,那么我可以把这 20 亿个数映射到不同的文件中去,例如,数值在 0 至 2亿之间的存放在文件1中,数值在2亿至4亿之间的存放在文件2中....,由于 int 型整数大概有 42 亿个不同的数,所以我可以把他们映射到 21 个文件中去,如图 24 | 25 | 26 | ![](https://user-gold-cdn.xitu.io/2019/5/23/16ae563d903c3755?w=1122&h=306&f=png&s=23796) 27 | 28 | 显然,相同的数一定会在同一个文件中,我们这个时候就可以用我的那个方法,统计每个文件中出现次数最多的数,然后再从这些数中再次选出最多的数,就可以了。 29 | 30 | 面试官:嗯,这个方法确实不错,不过,如果我给的这 20 亿个数数值比较集中的话,例如都处于 1~20000000 之间,那么你都会把他们全部映射到同一个文件中,你有优化思路吗? 31 | 32 | 小秋:那我可以先把每个数先做**哈希函数映射**,根据哈希函数得到的哈希值,再把他们存放到对应的文件中,如果哈希函数设计到好的话,那么这些数就会分布的比较平均。(关于哈希函数的设计,我就不说了,我这只是提供一种思路) 33 | 34 | #### 40亿级别 35 | 36 | 面试官:那如果我把 20 亿个数加到 40 亿个数呢? 37 | 38 | 小秋:(这还不简单,映射到42个文件呗)那我可以加大文件的数量啊。 39 | 40 | 面试官:那如果我给的这 40 亿个数中数值都是一样的,那么你的哈希表中,某个 key 的 value 存放的数值就会是 40 亿,然而 int 的最大数值是 21 亿左右,那么就会出现溢出,你该怎么办? 41 | 42 | 小秋:(那我把 int 改为 long 不就得了,虽然会占用更多的内存,那我可以把文件分多几份呗,不过,这应该不是面试官想要的答案),我可以把 value 初始值赋值为 **负21亿**,这样,如果 value 的数值是 21 亿的话,就代表某个 key 出现了 42 亿次了。 43 | 44 | > 这里说明下,文件还是 21 个就够了,因为 21 个文件就可以把每个文件的数值种类控制在 2亿种了,也就是说,哈希表存放的记录还是不会超过 2 亿中。 45 | 46 | #### 80亿级别 47 | 48 | 面试官:反应挺快哈,那我如果把 40 亿增加到 80 亿呢? 49 | 50 | 小秋:(我靠,这变本加厉啊).........我知道了,我可以一边遍历一遍判断啊,如果我在统计的过程中,发现某个 key 出现的次数超过了 40 亿次,那么,就不可能再有另外一个 key 出现的次数比它多了,那我直接把这个 key 返回就搞定了。 51 | 52 | 面试官:行,此次面试到此结束,回去等通知吧。 53 | 54 | #### 总结 55 | 56 | 今天这篇文章主要讲了大数据处理相关的一些问题,后面可能还会给大家找一些类似,但处理方式不同的题勒. 57 | 58 | 59 | 60 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 61 | 62 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /学算法/再现校招算法面试现场/记一道字节跳动的算法面试题:变形的链表反转.md: -------------------------------------------------------------------------------- 1 | 前几天有个朋友去面试字节跳动,面试官问了他一道**链表**相关的算法题,不过他一时之间没做出来,就来问了我一下,感觉这道题还不错,拿来讲一讲。 2 | 3 | #### 题目 4 | 5 | 这其实是一道**变形**的链表反转题,大致描述如下 6 | 7 | 给定一个单链表的头节点 head,实现一个调整单链表的函数,使得每K个节点之间为一组进行逆序,**并且从链表的尾部开始组起**,头部剩余节点数量不够一组的不需要逆序。(不能使用队列或者栈作为辅助) 8 | 9 | 例如: 10 | 链表:1->2->3->4->5->6->7->8->null, K = 3。那么 6->7->8,3->4->5,1->2各位一组。调整后:1->2->5->4->3->8->7->6->null。其中 1,2不调整,因为不够一组。 11 | 12 | #### 解答 13 | 14 | 这道题的难点在于,是从链表的尾部开始组起的,而不是从链表的**头部**,如果是头部的话,那我们还是比较容易做的,因为你可以遍历链表,每遍历 k 个就拆分为一组来逆序。但是从尾部的话就不一样了,因为是单链表,不能往后遍历组起。不过这道题肯定是用递归比较好做,对递归不大懂的建议看我之前写的一篇文章[为什么你学不会递归?告别递归,谈谈我的一些经验](https://mp.weixin.qq.com/s/mJ_jZZoak7uhItNgnfmZvQ),这篇文章写了关于递归的一些套路。 15 | 16 | #### 先做一道类似的反转题 17 | 18 | 在做这道题之前,我们不仿先来看看**如果从头部开始组起的话**,应该怎么做呢?例如:链表:1->2->3->4->5->6->7->8->null, K = 3。调整后:3->2->1->6->5->4->7->8->null。其中 7,8不调整,因为不够一组。 19 | 20 | 对于这道题,如果你不知道怎么逆序一个单链表,那么可以看一下我之前写的[如何优雅着反转单链表](https://mp.weixin.qq.com/s/WNO3KNhS6oU7rUvCNEGw8g) 21 | 22 | 这道题我们可以用递归来实现,假设方法reverseKNode()的功能是将单链表的每K个节点之间逆序(从**头部**开始组起的哦);reverse()方法的功能是将一个单链表逆序。 23 | 24 | 那么对于下面的这个单链表,其中 K = 3。 25 | 26 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c50b6d583aff?w=961&h=139&f=png&s=33464) 27 | 我们把前K个节点与后面的节点分割出来: 28 | 29 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c5103118f605?w=708&h=301&f=png&s=36577) 30 | temp指向的剩余的链表,可以说是原问题的一个子问题。我们可以调用reverseKNode()方法将temp指向的链表每K个节点之间进行逆序。再调用reverse()方法把head指向的那3个节点进行逆序,结果如下: 31 | 32 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c51c7cb59ea6?w=653&h=261&f=png&s=37303) 33 | > 再次声明,如果对这个递归看不大懂的,建议看下我那篇递归的文章 34 | 35 | 接着,我们只需要把这两部分给连接起来就可以了。最后的结果如下: 36 | 37 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c52146f04cdd?w=1004&h=135&f=png&s=32839) 38 | 39 | 代码如下: 40 | ```java 41 | //k个为一组逆序 42 | public ListNode reverseKGroup(ListNode head, int k) { 43 | ListNode temp = head; 44 | for (int i = 1; i < k && temp != null; i++) { 45 | temp = temp.next; 46 | } 47 | //判断节点的数量是否能够凑成一组 48 | if(temp == null) 49 | return head; 50 | 51 | ListNode t2 = temp.next; 52 | temp.next = null; 53 | //把当前的组进行逆序 54 | ListNode newHead = reverseList(head); 55 | //把之后的节点进行分组逆序 56 | ListNode newTemp = reverseKGroup(t2, k); 57 | // 把两部分连接起来 58 | head.next = newTemp; 59 | 60 | return newHead; 61 | } 62 | 63 | //逆序单链表 64 | private static ListNode reverseList(ListNode head) { 65 | if(head == null || head.next == null) 66 | return head; 67 | ListNode result = reverseList(head.next); 68 | head.next.next = head; 69 | head.next = null; 70 | return result; 71 | } 72 | ``` 73 | 74 | #### 回到本题 75 | 76 | 这两道题可以说是及其相似的了,只是一道从头部开始组起,这道从头部开始组起的,也是 leetcode 的第 25 题。而面试的时候,经常会进行变形,例如这道字节跳动的题,它变成从**尾部**开始组起,可能你一时之间就不知道该怎么弄了。当然,可能有人一下子就反应出来,把他秒杀了。 77 | 78 | 其实这道题很好做滴,你只需要先把单链表进行一次**逆序**,逆序之后就能转化为**从头部开始组起**了,然后按照我上面的解法,处理完之后,把结果**再次逆序**即搞定。两次逆序相当于没逆序。 79 | 80 | 例如对于链表(其中 K = 3) 81 | 82 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c50b6d583aff?w=961&h=139&f=png&s=33464) 83 | 我们把它从尾部开始组起,每 K 个节点为一组进行逆序。步骤如下 84 | 85 | 1、先进行逆序 86 | 87 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c6204a4c255c?w=1108&h=196&f=png&s=47200) 88 | 逆序之后就可以把问题转化为从**头部**开始组起,每 K 个节点为一组进行逆序。 89 | 90 | 2、处理后的结果如下 91 | 92 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c639443500a8?w=1102&h=172&f=png&s=43544) 93 | 94 | 3、接着在把结果**逆序**一次,结果如下 95 | 96 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c6532fbfafb0?w=1104&h=186&f=png&s=44935) 97 | 98 | 代码如下 99 | ```java 100 | public ListNode solve(ListNode head, int k) { 101 | // 调用逆序函数 102 | head = reverse(head); 103 | // 调用每 k 个为一组的逆序函数(从头部开始组起) 104 | head = reverseKGroup(head, k); 105 | // 在逆序一次 106 | head = reverse(head); 107 | return head; 108 | 109 | } 110 | ``` 111 | 类似于这种需要先进行逆序的还要两个链表相加,这道题字节跳动的笔试题也有出过,如下图的第二题 112 | 113 | 114 | ![](https://user-gold-cdn.xitu.io/2019/8/7/16c6c6fc42fb280d?w=1080&h=1440&f=png&s=553267) 115 | 116 | 这道题就需要先把两个链表逆序,再节点间相加,最后在合并了。 117 | 118 | #### 总结 119 | 120 | 关于链表的算法题,在面试的时候听说是挺常考的,大家可以多注意注意,遇到不错的链表算法题,也欢迎扔给我勒。 121 | 122 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 123 | 124 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /学算法/再现校招算法面试现场/面试被虐:说说游戏中的敏感词过滤是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 小秋今天去面试了,面试官问了一个与敏感词过滤算法相关的问题,然而小秋对敏感词过滤算法一点也没听说过。于是,有了下下事情的发生..... 2 | 3 | #### 面试官开怼 4 | 5 | 面试官:玩过王者荣耀吧?了解过**敏感词过滤吗?**,例如在游戏里,如果我们发送“你在干嘛?麻痹演员啊你?”,由于“麻痹”是一个敏感词,所以当你把聊天发出来之后,我们会用“\*\*”来代表“麻痹”这次词,所以发送出来的聊天会变成这样:“你在干嘛?**演员啊你?”。 6 | 7 | 小秋:听说过啊,在各大社区也经常看到,例如评论一个问题等,一些粗话经常被过滤掉了。 8 | 9 | 面试官:嗯,如果我给你一段文字,以及给你一些需要过滤的敏感词,你会怎么来实现这个敏感词过滤的算法呢?例如我给你一段字符串“abcdefghi",以及三个敏感词"de", "bca", "bcf"。 10 | 11 | 小秋:(敏感词过来算法??不就是字符串匹配吗?)我可以通过字符串匹配算法,例如在字符串”abcdefghi"在查找是否存在字串“de",如果找到了就把”de“用"**"代替。通过三次匹配之后,接变成这样了:“abc ** fghi"。 12 | 13 | 面试官:可以说说你采用哪种字符串匹配算法吗? 14 | 15 | 小秋:最简单的方法就是采用两个for循环保留求解了,不过每次匹配的都时间复杂度为O(n*m),我可以采用 KMP 字符串匹配算法,这样时间复杂度是 O(m+n)。 16 | 17 | > n 表示字符串的长度,m 表示每个敏感词的长度。 18 | 19 | 面试官:这是一个方法,对于敏感词过滤,你还有其他方法吗? 20 | 21 | 小秋:(其他方法?说实话,我也觉得不是采用这种 KMP 算法来匹配的了,可是,之前也没去了解过敏感词,这下要凉)对敏感词过来之前也没了解过,暂时没想到其他方法。 22 | 23 | #### trie 树 24 | 25 | 面试官:了解过 trie 树吗? 26 | 27 | 小秋:(嘿嘿,数据结构这方法,我得争气点)了解过,我还用代码实现过。 28 | 29 | 面试官:可以说说它的特点吗? 30 | 31 | 小秋:trie 树也称为字典树、单词查找树,最大的特点就是共享**字符串的公共前缀**来达到节省空间的目的了。例如,字符串 "abc"和"abd"构成的 trie 树如下: 32 | 33 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a9220f8cb8b0f8?w=471&h=336&f=png&s=17359) 34 | 35 | trie 树的**根节点**不存任何数据,每**整个**个分支代表一个完整的字符串。像 abc 和 abd 有公共前缀 ab,所以我们可以共享节点 ab。如果再插入 abf,则变成这样: 36 | 37 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a92247a12117dc?w=475&h=348&f=png&s=19729) 38 | 39 | 如果我再插入 bc,则是这样(bc 和其他三个字符串没有公共前缀) 40 | 41 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a922651588ed2e?w=489&h=342&f=png&s=23915)。 42 | 43 | 面试官:那如果再插入 "ab" 这个字符串呢? 44 | 45 | 小秋:差点说了,每个分支的内部可能也含有完整的字符串,所以我们可以对于那些是某个字符串结尾的节点做一个**标记**,例如 abc, abd,abf 都包含了字符串 ab,所以我们可以在节点 b 这里做一个标记。如下(我用红色作为标记): 46 | 47 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a922b03ec436fa?w=432&h=370&f=png&s=23677) 48 | 49 | 50 | 面试官:可以说说 trie 树有哪些应用吗? 51 | 52 | 小秋:trie 最大的特点就是利用了字符串的公共前缀,像我们有时候在百度、谷歌输入某个关键字的时候,它会给我们列举出很多相关的信息 53 | 54 | 55 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a92311b4dfcdf2?w=984&h=236&f=png&s=25030) 56 | 57 | 这种就是通过 trie 树来实现的。 58 | 59 | 小秋:(嗯? trie 又称为单词查找树,好像可以用 trie 来实现刚才的敏感词匹配?面试官无缘无故提 trie 树难道别有用意?) 60 | 61 | 面试官:刚才的敏感词过滤,其实也可以采用 trie 来实现,你知道怎么实现吗? 62 | 63 | #### trie 树来实现敏感词过滤 64 | 65 | 小秋:(果然,面试官真是个好人啊,直接提示了,要是还不知道怎么实现,那不真凉?)我想想........我知道了,我可以这样来实现: 66 | 67 | 先把你给我的三个敏感词:"de", "bca", "bcf" 建立一颗 trie 树,如下: 68 | 69 | 70 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a923f595a6fae5?w=411&h=349&f=png&s=21076) 71 | 72 | 接着我们可以采用三个指针来遍历,我直接用上面你给你例子来演示吧。 73 | 74 | 1、首先指针 p1 指向 root,指针 p2 和 p3 指向字符串第一个字符 75 | 76 | 77 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a9241249414d30?w=950&h=509&f=png&s=34588) 78 | 79 | 2、然后从字符串的 a 开始,检测有没有以 a 作为前缀的敏感词,直接判断 p1 的孩子节点中是否有 a 这个节点就可以了,显然这里没有。接着把指针 p2 和 p3 向右移动一格。 80 | 81 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a9243c3fe2e11c?w=914&h=405&f=png&s=32150) 82 | 83 | 3、然后从字符串 b 开始查找,看看是否有以 b 作为前缀的字符串,p1 的孩子节点中有 b,这时,**我们把 p1 指向节点 b,p2 向右移动一格,不过,p3不动。** 84 | 85 | 86 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a92475d7f0be9d?w=926&h=413&f=png&s=32744) 87 | 88 | 4、判断 p1 的孩子节点中是否存在 p2 指向的字符c,显然有。我们把 p1 指向节点 c,p2 向右移动一格,p3不动。 89 | 90 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a92496237eb3cf?w=919&h=412&f=png&s=32450) 91 | 92 | 5、判断 p1 的孩子节点中是否存在 p2 指向的字符d,这里没有。这意味着,**不存在以字符b作为前缀的敏感词**。这时我们把p2和p3都移向字符c,p1 还是还原到最开始指向 root。 93 | 94 | 95 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a924bda72bba94?w=1019&h=400&f=png&s=33486) 96 | 97 | 6、和前面的步骤一样,判断有没以 c 作为前缀的字符串,显然这里没有,所以把 p2 和 p3 移到字符 d。 98 | 99 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a924cf1a5d0009?w=980&h=483&f=png&s=35107) 100 | 101 | 7、然后从字符串 d 开始查找,看看是否有以 d 作为前缀的字符串,p1 的孩子节点中有 d,这时,**我们把 p1 指向节点 d,p2 向右移动一格,不过,p3和刚才一样不动。**(看到这里,我猜你已经懂了) 102 | 103 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a924e6fa468db5?w=1046&h=375&f=png&s=33005) 104 | 105 | 8、判断 p1 的孩子节点中是否存在 p2 指向的字符e,显然有。我们把 p1 指向节点 e,**并且,这里e是最后一个节点了,查找结束,所以存在敏感词de**,即 p3 和 p2 这个区间指向的就是敏感词了,把 p2 和 p3 指向的区间那些字符替换成 *。并且把 p2 和 p3 移向字符 f。如下: 106 | 107 | ![](https://user-gold-cdn.xitu.io/2019/5/7/16a9251b45b50014?w=954&h=422&f=png&s=33004) 108 | 109 | 9、接着还是重复同样的步骤,知道 p3 指向最后一个字符。 110 | 111 | #### 复杂度分析 112 | 113 | 面试官:可以说说时间复杂度吗? 114 | 115 | 小秋:如果敏感词的长度为 m,则每个敏感词的查找时间复杂度是 O(m),字符串的长度为 n,我们需要遍历 n 遍,所以敏感词查找这个过程的时间复杂度是 O(n * m)。如果有 t 个敏感词的话,构建 trie 树的时间复杂度是 O(t * m)。 116 | 117 | > 这里我说明一下,在实际的应用中,构建 trie 树的时间复杂度我觉得可以忽略,因为 trie 树我们可以在一开始就构建了,以后可以无数次重复利用的了。而刚才的 kmp 算法时间复杂度是 t *(m+n),不过kmp需要维护 next 数组比较费空间,而且在实际情况中,敏感词的数量 t 是比较大,而 n 反而比较小的吧。 118 | 119 | 10、如果让你来 构建 trie 树,你会用什么数据结构来实现? 120 | 121 | 小秋:我一般使用 Java,我会采用 HashMap 来实现,因为一个节点的字节点个数未知,采用 HashMap 可以动态拓展,而且可以在 O(1) 复杂度内判断某个子节点是否存在。 122 | 123 | 面试官:嗯,回去等通知吧。 124 | 125 | #### 总结 126 | 127 | 今天主要将了 trie 树以及 trie 树的一些应用,还要就是如何通过 trie 树来实现敏感词的过滤,至于代码的实现,我这里就不给出了,在实现的时候,为了防止这种”麻 痹"或者“麻¥痹”等,我们也要对特殊字符进行过滤等,有兴趣的可以去实现一波。 128 | 129 | 今天也是第一次尝试采用这种对话的方式来写文章,可能写的没有平常的好,不过我会慢慢改进,希望大家多多支持。 130 | 131 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 132 | 133 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /学算法/学习算法经验分享/leetcode刷500道题,笔试:面试稳过吗?.md: -------------------------------------------------------------------------------- 1 | 想要学习算法、应付笔试或者应付面试手撕算法题,相信大部分人都会去刷 Leetcode,有读者问?如果我在 leetcode 坚持刷它个 500 道题,以后笔试/面试稳吗? 2 | 3 | 这里我说下我的个人看法,我认为**不稳**。下面说说为啥不稳以及算法题应该如何刷、如何学才比较好,当然,也会推荐自己学过的资料。 4 | 5 | #### 一、先说说笔试题 6 | 7 | 在刷 leetcode 的时候,你会发现,每道题的题意都很短,你只需要花十几秒的时间,就知道这道题是要你干嘛了,并且每道题所用道的算法思想都很明确,动态规划、递归、二分查找等,你可能很快就知道该用哪种方法,只是你不懂如何把代码写出来而已。 8 | 9 | 而在笔试中是完全不一样的,在笔试中,大部分题目都是情景题,可能读懂个题目都需要花不少时间,偶尔还会遇到不大知道题目要我们干嘛,而且有时间限制,估计每道题给你分配的时间是 30 分钟。这里我随便扔一道题给大家看看(Shopee去年的真题) 10 | 11 | ![](https://user-gold-cdn.xitu.io/2019/10/7/16da48168b4475c1?w=1296&h=1498&f=png&s=283292) 12 | 13 | 14 | 15 | 并且你可能不容易看出来,这些道题该用什么方法好,有可能是多种方法的结合(当然,不是指这道题哈)。 16 | 17 | 也就是说,在 leetcode 中,hard 级别的题做的出来,而在笔试中 medium 级别的,由于时间、心态等因素的影响。你可能还做不出来,当然,大佬除外。下面说一说题型的一些题型以及如何学习算法会好应付点。 18 | 19 | **在笔试中,我认为主要有如下几种题型:** 20 | 21 | **1、基本数据结构的考察**:这类题我觉得是比较简单的,主要考场基本数据结构的操作,例如二叉树的层序遍历,链表的逆序等,当然,它不会直接告诉你,让你来逆序或者遍历。例如 22 | 23 | ![](https://user-gold-cdn.xitu.io/2019/10/7/16da505fbfca6148?w=1994&h=150&f=png&s=31494) 24 | 25 | **2、某种算法思想的掌握**:这类题你掌握了某种算法思想,就会比较容易,如果不懂,那就凉凉了。例如动态规划、回溯、枚举、深度/广度、贪心、二分等。其中,我觉得**动态规划**考的挺多,还要就是 **回溯+深度/广度**。例如 26 | 27 | ![](https://user-gold-cdn.xitu.io/2019/10/7/16da506ffd21a0cb?w=1970&h=266&f=png&s=87056)所以,常见算法思想,一定要掌握。 28 | 29 | **3、边界条件的考察**:这类型的题,估计你一看就有思路,知道该怎么做,但是,它的边界条件特别多,需要分很多种情况来讨论,特别容易出错,有时候会让人陷进去,越做越复杂,这类题主要考场你的思维严谨程度。例如 30 | 31 | ![](https://user-gold-cdn.xitu.io/2019/10/7/16da4974771171f7?w=1822&h=170&f=png&s=44113) 32 | 33 | **4、找规律、数学公式**:这类型的题,主要是根据数据之间的一些关系,来找一些规律,进而推出他们的通用公式,就像我们高中时,找数列的同项一样。例如 34 | 35 | ![](https://user-gold-cdn.xitu.io/2019/10/7/16da4957cf8c312e?w=1300&h=252&f=png&s=57881) 36 | 37 | #### 二、应该如何刷题?如何学习? 38 | 39 | 上面说了笔试题的一些情况,也说了主要考察的一些题型。针对这些题型,我觉得在刷题的时候,你要做好下面几件事。 40 | 41 | ##### 1、分类归纳/总结 42 | 43 | 归纳?总结?估计大部分都知道归纳、总结这么一回事,但是,有没有去实践我就不知道了。 44 | 45 | **(1)、数组和相关题型** 46 | 47 | 对于算法题,还是有很多种题型需要去总结的,如果你懂这个题型,以后遇到类似的题,相信很快就能做出来的。有哪些题型可以总结呢?答是非常多:例如 48 | 49 | > (1)、给你一个非负数的数组,求最大子数组和的长度 50 | 51 | 这算是一个题型,关于这个题型,有很多种变形、拓展,这里建议一起归纳总结,例如 52 | 53 | > (2)、刚才给的数组是非负数的,现在变一下,给的数组是可正可负。 54 | 55 | 还能继续拓展吗?答是可以的,例如 56 | 57 | > (3)、给你个矩阵(即二维数组),求最大子矩阵和的面积 58 | 59 | 还有吗?有,例如刚才是求最大和,现在我改成求最大乘积。 60 | 61 | 我举上面这些例子,就是想告诉你,对于前期的学习,我建议**分类刷题,总结题型**,像我上面举的这些例子,在笔试/面试中还是比较常见的,如果你懂得对应的方法,就可以秒杀了,因为这类题,没啥边界或者规律。例如我刚才距离的**Shopee的零食柜**那道题,实际上就是数组切割题型,相当于给你一个数组,让你切割 n 下,那么可以把数组切割成 n + 1 个子数组,怎么样切割,才能让最大子数组的和最小? 62 | 63 | 关于题型的,还是很多的,我这里无法一一给你列举,只能靠你刷题的过程中,进行分类、总结。**不过我可以给你推荐一些资料**,后面推荐哈。下面我在说一些题型吧。 64 | 65 | **(2)、基本数据结构操作相关题型** 66 | 67 | 刚才我说了,笔试题的考察,有一类题是基本数据结构的考场,而且,这类题在面试中,也是高频考点,在笔试中,倒不是很高频。对于这类题,我觉得你愿意去总结,那么以后遇到,问题不大。例如 68 | 69 | **链表的各种操作**:逆序(部分逆序、按某种条件逆序)、判断是否有环,环的入口节点、删除指定节点等。 70 | 71 | **二叉树的各种操作**:各种非递归的遍历操作(前中后、层)、二叉树的公共祖先、根据前中后的遍历结果来重构二叉树等等。 72 | 73 | **队列、栈相关操作**:最小栈、来队列来实现栈等。 74 | 75 | **(3)、字符串相关问题** 76 | 77 | 不得不说,字符串相关问题,估计考的最高频,而且,我可以告诉你,**对于字符串相关问题,90% 可以用动态规划来解决**。反正对于字符串问题,我一般想法就是能否套用动态规划,字符串问题有点多,不过你有时间,建议总结。例如:通配符的匹配、最长公共子串、最小编辑代价、最长回文串等等。大部分都是用动态规划,而且,我觉得解法都差不多,所以强烈建议专门花一段时间来做、总结、归纳。后面我也会写这方面的算法文章,敬请期待。 78 | 79 | ##### 2、多思考/动手,提高自己的思维完整性/灵敏性 80 | 81 | **(1)、边界、找规律题型** 82 | 83 | 刚才我说有一类题型是边界特别多的,对于这类题,我觉得不好总结,这类题考察你逻辑是否严谨,能否化繁为简。这里我建议多做几道,做的时候,多自己思考,**千万不要觉得自己知道思路,自己怎么写,只是情况太多,懒的写,直接看别人的答案**,这样子,这道题你做了价值不大,因为这类题就是考察你思维完整性的,最好是自己做,可能你用了 十几个 if 语句,没关系。接着你可以把你的 if 语句进行化简,查找他们的共同点。最后你可以看大佬们的做法,你的收获会更大! 84 | 85 | 对了,也千万别急着动手写,应该想一想可行性,不然你容易陷入无底深渊。 86 | 87 | 对于找规律的题型也是一样,这类题最后别急着看答案,应该多思考,多做几道,做多了,你的思维会越来越灵敏,以后看到这类型的题,可以很快有思路。 88 | 89 | 所以,对于这种边界、规律题,个人感觉总结的价值不是特别大,更多的是多思考,多动手。 90 | 91 | > 注意:每道题,我们都要追求最优解哈,别觉得 ac 了就完事了。 92 | 93 | #### 三、我看过的一些资料 94 | 95 | 上面说了那么多,可能有人是**道理我都懂,可我还是学不会**,说实话,学习的方法有很多,每个人的学习方法也都不一样,我这里也只是提供一种参考。但是,无论什么方法,你不去动手执行,那么,一切都是**空话**。 96 | 97 | 这里我推荐一些我看过的书,感觉挺不错。 98 | 99 | > 文中涉及到的书籍以及视频,在我的微信公众号:**帅地玩编程**,回复『**算法学习**』即可获取 100 | 101 | **1、书籍推荐** 102 | 103 | 刚才我说了很多种题型,对于按题型刷题总结,首推《程序员代码面试指南:IT名企算法与数据结构题目最优解》,这本书真的挺不错,大部分题型都总结了,而且每个专题有十几二十道,这里建议大家买本来学习。 104 | 105 | 还要一本我大一看的,感觉也挺不错,叫做《挑战程序设计大赛》,不过这本比较适合不急着面试的吧,这本不像上面那一本,专门来总结各种题型应付面试。 106 | 107 | 《编程之美》、《编程珠玑》也建议看,这两本我觉得比较有趣,不是说让你一直刷题一直刷题,这两本你可以买来看看,会给你带来一些思路,这两本我是只看,没动手打代码。 108 | 109 | Leetcode 刷题的时候,也是可以分题型刷滴,所以也可以去 leetcode 刷题,不过刷题的时候,我这里有个建议,就是别在本地 IDE 写代码,直接在网页端写就行了。因为面试的时候,一般就让你在记事本写代码,不会给你 IDE。如果你不习惯,估计很容易写错代码,而且,有些库函数你也把名字忘记了。网页端其实也是挺方便的,也会有一些代码提示。 110 | 111 | > 对于,对于连各种算法思想、数据结构都还不懂的同学,上面的数据不大合适哈,推荐我看过的两本书《数据结构与算法分析 — C 语言描述版》、《算法设计与分析基础》(这本代码实现是用伪代码的)。 112 | 113 | **2、视频推荐** 114 | 115 | 说时候,我视频看的不多,对于算法的学习,特别是刷题,我是不大习惯看视频,如果你想看视频,我觉得牛客网的算法视频还不错吧,我没过几集,分初级班和进阶班。其他的我也没看过,所以这里可以推荐的不多。 116 | 117 | #### 四、总结 118 | 119 | 回到标题,leetcode 刷 500 道题稳吗?说实话,你能坚持刷 500 道题,说明你的能力还是挺强的,但是对于笔试,我觉得不一定稳,得看你怎么做,例如是否追求最优解,是否进行总结归纳,还是说你只是暴力 ac 了之后就不理了,或者不敢**跳出舒适区**,老是做那些比本来就比较擅长的题目,而遇到自己弱的题目,马上就看答案了。而且我说了,有些题是找规律或者边界很多的,这类题需要你多思考、动手,不是说我多刷几道就可以了。 120 | 121 | 总之,对于刷题,千万别追求数量! 122 | 123 | 上面的做题方法,不一定适合每一个人,只是我自己的学习以及建议,供大家参考。想要获取上面那些资料的,可以在我的公众号 **帅地玩编程** 回复『**算法学习**』即可获取。 124 | 125 | 今天是国庆最后一天,大家也玩够了,所以接下来,就要好好学习了,先把自己的硬实力提升起来。在后面,我也会多写一些算法题,例如动态规划,回溯,递归等。 126 | 127 | 128 | 129 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 130 | 131 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /学算法/学习算法经验分享/程序员必须掌握的算法有哪些?.md: -------------------------------------------------------------------------------- 1 | 由于我之前一直强调数据结构以及算法学习的重要性,所以就有一些读者经常问我,**数据结构与算法应该要学习到哪个程度呢?**,说实话,这个问题我不知道要怎么回答你,主要取决于你想学习到哪些程度,不过针对这个问题,我稍微总结一下我学过的算法知识点,以及我觉得值得学习的算法。这些算法与数据结构的学习大多数是**零散**的,并没有一本把他们全部覆盖的书籍。下面是我觉得值得学习的一些算法以及数据结构,当然,我也会整理一些看过不错的文章给大家。大家也可以留言区补充。 2 | 3 | #### 一、算法最最基础 4 | 5 | 1、时间复杂度 6 | 7 | 2、空间复杂度 8 | 9 | 一般最先接触的就是时间复杂度和空间复杂度的学习了,这两个概念以及如何计算,是必须学的,也是必须最先学的,主要有最大复杂度、平均复杂度等,直接通过博客搜索学习即可。 10 | 11 | 文章推荐: 12 | 13 | [算法分析神器—时间复杂度](https://mp.weixin.qq.com/s/070nYGokM96aorZn6MZTDA) 14 | 15 | #### 二、基础数据结构 16 | 17 | **1、线性表** 18 | 19 | - 列表(必学) 20 | - 链表(必学) 21 | - 跳跃表(知道原理,应用,最后自己实现一遍) 22 | - 并查集(建议结合刷题学习) 23 | 24 | 不用说,链表、列表必须,不过重点是链表。 25 | 26 | [三分钟基础数据结构:如何轻松手写链表?]( https://mp.weixin.qq.com/s/hKjkITbCRcnZBafpjiwWJA) 27 | 28 | [以后有面试官问你「跳跃表」,你就把这篇文章扔给他](https://mp.weixin.qq.com/s/AGPCfFg7bEiCsa5zNeCi4A) 29 | 30 | **2、栈与队列** 31 | 32 | - 栈(必学) 33 | - 队列(必学) 34 | - 优先队列、堆(必学) 35 | - 多级反馈队列(原理与应用) 36 | 37 | 特别是优先队列,再刷题的时候,还是经常用到的,队列与栈,是最基本的数据结构,必学。可以通过博客来学习。相关文章: 38 | 39 | [三分钟基础知识:什么是栈?](https://mp.weixin.qq.com/s/6DMLl_EksTqSqWyE2FkEug) 40 | 41 | [二叉堆是什么鬼?](https://mp.weixin.qq.com/s/TKRtF2dAtH7VuNs-FC4awA) 42 | 43 | [【算法与数据结构】堆排序是什么鬼?](https://mp.weixin.qq.com/s/B0ImTjuQJiR7ahRzBpslcg) 44 | 45 | **3、哈希表**(必学) 46 | 47 | - 碰撞解决方法:开放定址法、链地址法、再次哈希法、建立公共溢出区(必学) 48 | - 布隆过滤器(原理与应用) 49 | 50 | 哈希表相关的,推荐通过博客来学习,推荐文章: 51 | 52 | [Hash冲突之开放地址法](https://mp.weixin.qq.com/s/SddKKeTK6Hpk9Q5R8NmZKA) 53 | 54 | 55 | 56 | **4、树** 57 | 58 | - 二叉树:各种遍历(递归与非递归)(必学) 59 | - 哈夫曼树与编码(原理与应用) 60 | - AVL树(必学) 61 | - B 树与 B+ 树(原理与应用) 62 | - 前缀树(原理与应用) 63 | - 红黑树(原理与应用) 64 | - 线段树(原理与应用) 65 | 66 | 树相关是知识还是挺多的,建议看书,可以看《算法第四版》。相关文章: 67 | 68 | [高频面试题:什么是B树?为啥文件索引要用B树而不用二叉查找树?](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247486101&idx=1&sn=980f6dfb7643a9ff4f5a661d4a496046&chksm=ce404141f937c85750232523583435e97f3965a3761fa327e5d79e2b720dfced1a1dfc731d3b&token=1321503479&lang=zh_CN#rd) 69 | 70 | [【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。](https://mp.weixin.qq.com/s/dYP5-fM22BgM3viWg4V44A) 71 | 72 | [腾讯面试题:有了二叉查找树、平衡树为啥还需要红黑树?](https://mp.weixin.qq.com/s/p_fEMMNjlnPbbwY9dDQMAQ) 73 | 74 | [【面试被虐】游戏中的敏感词过滤是如何实现的?](https://mp.weixin.qq.com/s/ZYtU4v9y2KMLT0d2X_MIZQ) 75 | 76 | **5、数组** 77 | 78 | - 树状数组 79 | - 矩阵(必学) 80 | 81 | 树状数组其实我也没学过,,,, 82 | 83 | #### 三、各种常见算法 84 | 85 | **1、十大排序算法** 86 | 87 | - 简单排序:插入排序、选择排序、冒泡排序(必学) 88 | - 分治排序:快速排序、归并排序(必学,快速排序还要关注中轴的选取方式) 89 | - 分配排序:桶排序、基数排序 90 | - 树状排序:堆排序(必学) 91 | - 其他:计数排序(必学)、希尔排序 92 | 93 | 对于十大算法的学习,假如你不大懂的话,那么我还是挺推荐你去看书的,因为看了书,你可能不仅仅知道这个算法怎么写,还能知道他是怎么来的。推荐书籍是《算法第四版》,这本书讲的很详细,而且配了很多图演示,还是挺好懂的。 94 | 95 | 推荐文章: 96 | 97 | [必学十大经典排序算法,看这篇就够了(附完整代码/动图/优质文章)(修订版)](https://mp.weixin.qq.com/s/IAZnN00i65Ad3BicZy5kzQ) 98 | 99 | **2、图论算法** 100 | 101 | - 图的表示:邻接矩阵和邻接表 102 | - 遍历算法:深度搜索和广度搜索(必学) 103 | - 最短路径算法:Floyd,Dijkstra(必学) 104 | - 最小生成树算法:Prim,Kruskal(必学) 105 | - 实际常用算法:关键路径、拓扑排序(原理与应用) 106 | - 二分图匹配:配对、匈牙利算法(原理与应用) 107 | - 拓展:中心性算法、社区发现算法(原理与应用) 108 | 109 | 图还是比较难的,不过我觉得图涉及到的挺多算法都是挺实用的,例如最短路径的计算等,图相关的,我这里还是建议看书的,可以看《算法第四版》。 110 | 111 | [漫画:什么是 “图”?(修订版)](https://mp.weixin.qq.com/s/4JEHZWanGtsQHYrZ0MDq7Q) 112 | 113 | [漫画:深度优先遍历 和 广度优先遍历](https://mp.weixin.qq.com/s/WA5hQXkcACIarcdVnRnuiw) 114 | 115 | [漫画:图的 “最短路径” 问题](https://mp.weixin.qq.com/s/gjjrsj95X4w7QdWBlAKnaA) 116 | 117 | [漫画:Dijkstra 算法的优化](https://mp.weixin.qq.com/s/ALQntqQJkdWf4RbPaGOOhg) 118 | 119 | [漫画:图的 “多源” 最短路径](https://mp.weixin.qq.com/s/qnPSzv_xWSZN0VpdUgwvMg) 120 | > 更多算法的学习,欢迎关注我的公众号『**苦逼的码农**』 121 | 122 | **3、搜索与回溯算法** 123 | 124 | - 贪心算法(必学) 125 | - 启发式搜索算法:A*寻路算法(了解) 126 | - 地图着色算法、N 皇后问题、最优加工顺序 127 | - 旅行商问题 128 | 129 | 这方便的只是都是一些算法相关的,我觉得如果可以,都学一下。像贪心算法的思想,就必须学的了。建议通过刷题来学习,leetcode 直接专题刷。 130 | 131 | **4、动态规划** 132 | 133 | - 树形DP:01背包问题 134 | - 线性DP:最长公共子序列、最长公共子串 135 | - 区间DP:矩阵最大值(和以及积) 136 | - 数位DP:数字游戏 137 | - 状态压缩DP:旅行商 138 | 139 | 我觉得动态规划是最难的一个算法思想了,记得当初第一次接触动态规划的时候,是看01背包问题的,看了好久都不大懂,懵懵懂懂,后面懂了基本思想,可是做题下不了手,但是看的懂答案。一气之下,再leetcdoe专题连续刷了几十道,才掌握了动态规划的**套路**,也有了自己的一套模板。不过说实话,动态规划,是考的真他妈多,学习算法、刷题,一定要掌握。这里建议先了解动态规划是什么,之后 leetcode 专题刷,反正就一般上面这几种题型。后面有时间,我也写一下我学到的**套路**,有点类似于我之前写的递归那样,算是一种经验。也就是我做题时的模板,不过感觉得写七八个小时,,,,,有时间就写。之前写的递归文章:[为什么你学不会递归?告别递归,谈谈我的一些经验](https://mp.weixin.qq.com/s/mJ_jZZoak7uhItNgnfmZvQ) 140 | 141 | **5、字符匹配算法** 142 | 143 | - 正则表达式 144 | - 模式匹配:KMP、Boyer-Moore 145 | 146 | 我写过两篇字符串匹配的文章,感觉还不错,看了这两篇文章,我觉得你就差不多懂 kmp 和 Boyer-Moore 了。 147 | 148 | [字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?](https://mp.weixin.qq.com/s/7IZTuLrPSuxvFRqsv5PiXQ) 149 | 150 | **6、流相关算法** 151 | 152 | - 最大流:最短增广路、Dinic 算法 153 | - 最大流最小割:最大收益问题、方格取数问题 154 | - 最小费用最大流:最小费用路、消遣 155 | 156 | 这方面的一些算法,我也只了解过一些,感兴趣的可以学习下。 157 | 158 | #### 总结 159 | 160 | 对于上面设计到的算法,我都提供了感觉还不错的文章,建议大家收藏,然后可以利用零碎的时间进行阅读,有些人可能会觉得上面的算法太多,说实话,我觉得不多,特别是对于在校生的,上面涉及到的算法可以不用很懂,但至少得了解。至于书籍的话,如果你连基本数据结构都还不懂的,建议看《数据结构与算法》相关书籍,例如《大话数据结构》、《数据结构与算法分析》。如果你有一定的基础,例如知道链表,栈,队列,那么可以看《算法第四版》,不过这本书是用 Java 实现的,不过我觉得你只要学过 C,那么可以看的懂。 161 | 162 | 这些算法的学习,虽然你觉得学了没有什么用,但还是那些话,它对你的影响是潜意识的,它可以给你打下很深厚的基础内功,如果你想走的更远,那么我推荐学习,标注**必学**的,那么我觉得,你是真的需要抽时间来学习下,标注**原理与应用**的,代表你可以不知道怎么用代码实现,但是必得知道它的实现原理以及应用,更多算法的学习,可以持续关注我的微信公众号勒。 163 | 164 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 165 | 166 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /学算法/学二分查找/二分法题型小结.md: -------------------------------------------------------------------------------- 1 | 在刷题的过程中,二分法用的还是挺多的,有时候超时了往往是你没有用上二分法,今天我就来稍微总结下用的最多的三种**二分法搜索**。 2 | 3 | #### 一、搜索和目标值相等的数 4 | 5 | 这一类是最简单的,例如对于数组 arr = {1, 2, 5, 6, 9},要我们搜索返回目标数 target = 6,这个时候我们需要返回 6 的下标 i = 3。代码如下 6 | ```java 7 | int binarySearch(int[] arr, int target){ 8 | int left = 0, right = arr.length - 1; 9 | while (left <= right) { 10 | int mid = left + (right - left) / 2; 11 | if (arr[mid] == target) return mid; 12 | else if (arr[mid] < target) left = mid + 1; 13 | else return mid; 14 | } 15 | return -1; 16 | } 17 | ``` 18 | 不过这个有一个需要注意的点,就是很多人在求 mid 的时候,会这样写 19 | ``` 20 | mid = (left + right) / 2; 21 | ``` 22 | 其实这样写是有点小问题的,**因为 left + right 有可能导致数值溢出**,从而 mid 的计算就错误了,所以经常这样写的小伙伴要注意了,严谨的写法应该是这样写: 23 | ``` 24 | mid = left + (right - left) / 2; 25 | ``` 26 | 27 | #### 二、 查找第一个不小于目标值的数 28 | 29 | 查找第一个不小于目标的数,我觉得这类出现的频率是最高的,而且要注意,目标数并不一定就出现在数组中。 30 | 31 | 例如对于数组 arr = {1, 2, 5, 6, 9},目标数 target = 6,那么我们要返回下标 i = 4 。 32 | 33 | 又如 arr = {0, 1, 2, 2, 2, 3},target = 2。我们要返回 i = 2。 34 | 35 | 代码如下 36 | ```java 37 | int binarySearch(int[] arr, int target){ 38 | int left = 0, right = arr.length - 1; 39 | while (left <= right) { 40 | int mid = left + (right - left) / 2; 41 | if (arr[mid] < target) { 42 | left = mid + 1; 43 | }else{ 44 | right = mid; 45 | } 46 | } 47 | return right; 48 | } 49 | ``` 50 | 在代码上,和第一种最大的区别就是 right = mid - 1 变成 right = mid 了,至于为啥?自行脑补,随便找我上面举的例子模拟一下就行了。 51 | 52 | 对了,还有一种和**查找第一个不小于目标值的数**类似的题,就是**找最后一个小于目标值的数**。这很简单,直接返回 right - 1 就可以了。 53 | 54 | #### 三、查找第一个大于目标值的数 55 | 56 | 查找第一个大于目标的数,我觉得这类出现的频率也是最高的,而且也要注意,目标数并不一定就出现在数组中。 57 | 58 | 例如对于数组 arr = {1, 2, 5, 6, 9},目标数 target = 6,那么我们要返回下标 i = 5 。 59 | 60 | 又如 arr = {0, 1, 2, 2, 2, 3},target = 2。我们要返回 i = 5。 61 | 62 | 我们先来看下代码 63 | 64 | ```java 65 | int binarySearch(int[] arr, int target){ 66 | int left = 0, right = arr.length - 1; 67 | while (left <= right) { 68 | int mid = left + (right - left) / 2; 69 | if (arr[mid] <= target) { 70 | left = mid + 1; 71 | }else{ 72 | right = mid; 73 | } 74 | } 75 | return right; 76 | } 77 | ``` 78 | 有木觉得和第二种类型的代码太像了,只需要在 if 语句里吧 arr[mid] < target 改成 arr[mid] <= target 即可。至于为啥?因为**第一个不小于**和**第一个大于等于**是同一个意思,但是这道题是**第一个大于**,不包括**等于**这种情况啊。所以把 < 改为 <= 即可。 79 | 80 | #### 总结 81 | 82 | 其实对于二分法的查找,有很多种类型,不过,代码的差别都不大,就是有可能大于改成大于等于。 left/right = mid + 1 变成 left/right = mid。所以有时也很容易出错,特别是脑子乱了的时候,所以我这里建议,**选择一种自己喜欢的代码风格**,然后每次做题的时候,的用你心中的那份模板代码。 83 | 84 | 当然,我上面说的这三种 还是比较简单的,还有一些比较难的,就是 target 可能是动态更新的,而且 left 和 right 的更新也比较复杂,不过对于这种,在 leetcode 一般都是 hard 级别的题了。所以大家可以先把上面这三种掌握起来,不过 你在做题时,并不会有直接说是哪种题型的话,甚至你都没有考虑到可以使用二分法来做,所以平时做题的时候可以多留意。 85 | 86 | 87 | 88 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 89 | 90 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /学算法/学字符串匹配算法/图解字符串匹配KMP算法.md: -------------------------------------------------------------------------------- 1 | > 这篇其实不是玩写的,是来自于阮一峰的网络日记,我觉得写的很好,所以转载过来,这里感谢阮一峰老师的创作 2 | 3 | #### 前言 4 | 5 | 字符串匹配是计算机的基本任务之一。 6 | 7 | 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"? 8 | 9 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182129867.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 10 | 许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。 11 | 12 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182251757.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 13 | 这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。 14 | 15 | #### 二、图解KMP算法 16 | 17 | 1、 18 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020030418252614.png) 19 | 首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。 20 | 21 | 2、 22 | 23 | 24 | 25 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182556909.png) 26 | 27 | 28 | 29 | 因为B与A不匹配,搜索词再往后移。 30 | 31 | 3、 32 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182625455.png) 33 | 34 | 就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。 35 | 36 | 4、 37 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182644289.png) 38 | 接着比较字符串和搜索词的下一个字符,还是相同。 39 | 40 | 5、 41 | 42 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182703921.png) 43 | 直到字符串有一个字符,与搜索词对应的字符不相同为止。 44 | 45 | 6、 46 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182723523.png) 47 | 48 | 这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。 49 | 50 | 7、 51 | 52 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182739814.png) 53 | 一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。 54 | 55 | 8、 56 | 57 | 58 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182755617.png) 59 | 怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。 60 | 61 | 9、 62 | 63 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182824375.png) 64 | 已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数: 65 | 66 | 67 | 68 | > 移动位数 = 已匹配的字符数 - 对应的部分匹配值 69 | 70 | 71 | 72 | 因为 6 - 2 等于4,所以将搜索词向后移动4位。 73 | 74 | 10、 75 | 76 | 77 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182840902.png) 78 | 79 | 80 | 81 | 82 | 因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。 83 | 84 | 11、 85 | 86 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182909214.png) 87 | 88 | 因为空格与A不匹配,继续后移一位。 89 | 90 | 12、 91 | 92 | 93 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182918699.png) 94 | 95 | 逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。 96 | 97 | 13、 98 | 99 | 100 | 101 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304182938159.png) 102 | 103 | 104 | 105 | 逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了 106 | 107 | #### 三、部分匹配值 108 | 109 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304183008819.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 110 | 111 | 下面介绍《部分匹配表》是如何产生的。 112 | 113 | 114 | 115 | 首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。 116 | 117 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200304183020664.png) 118 | "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例, 119 | 120 | 121 | - "A"的前缀和后缀都为空集,共有元素的长度为0; 122 | 123 | 124 | 125 | - "AB"的前缀为[A],后缀为[B],共有元素的长度为0; 126 | 127 | 128 | 129 | - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0; 130 | 131 | 132 | 133 | - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0; 134 | 135 | 136 | 137 | - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1; 138 | 139 | 140 | 141 | - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2; 142 | 143 | 144 | 145 | - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。 146 | 147 | "部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。 148 | 149 | #### 四、几点说明 150 | 151 | 152 | 1、可能有些人会有这样的疑问: 153 | 154 | 155 | 156 | 如果 157 | 158 | 已匹配的字符数 = 0 159 | 160 | 同时 161 | 162 | 对应的部分匹配值 = 0. 163 | 164 | 又 165 | 166 | 移动位数 = 已匹配的字符数 - 对应的部分匹配值 = 0 - 0 = 0。这个时候移动的位数为0,那不是永远无法移动? 167 | 168 | 169 | 170 | 解答:如果第一个字符就不匹配,搜索词直接比较下一个字符,不用考虑《部分匹配表》。 171 | 172 | 173 | 174 | 2、这个部分匹配表的值,相当于我们代码实现中的next数组的值。 175 | 176 | 177 | 178 | 3、我不给出代码实现了,希望大家能根据这个思路,不看别人的代码实现一遍,之后你也可以手写kmp字符匹配算法了。 179 | 180 | 181 | 182 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 183 | 184 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /学算法/学字符串匹配算法/字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?.md: -------------------------------------------------------------------------------- 1 | 关于字符串匹配算法有很多,之前我有讲过一篇 KMP 匹配算法:[图解字符串匹配 KMP 算法](https://mp.weixin.qq.com/s/8hL0z0-9adByWk-hVro7PA),不懂 kmp 的建议看下,写的还不错,这个算法虽然很牛逼,但在实际中用的并不是特别多。至于选择哪一种字符串匹配算法,在不同的场景有不同的选择。 2 | 3 | 在我们平时文档里的字符查找里 4 | 5 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c82ecb65505f?w=748&h=323&f=png&s=20953) 6 | 采用的就是 Boyer-Moore 匹配算法了,简称**BM**算法。这个算法也是有一定的难度,不过今天,我选用一个例子,带大家读懂这个**字符串匹配 BM 算法**,看完这篇文章,保证你能够掌握这个算法的思想。 7 | 8 | 首先我先给出一个字符串和一个模式串 9 | 10 | 11 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c95fa5740bb2?w=623&h=207&f=png&s=11752) 12 | 13 | 接下来我们要在字符串中查找有没有和模式串匹配的字串,步骤如下: 14 | 15 | #### 坏字符 16 | 17 | 1、 18 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c982b8550148?w=808&h=283&f=png&s=10904) 19 | 和其他的匹配算法不同,BM 匹配算法,是从模式串的**尾部**开始匹配的,所以我们把字符串和模式串的**尾部**对齐。 20 | 21 | 显然,从图中我们可以发现,s 和 e 并不匹配。这时我们把“s” 称之为**坏字符**,即代表不匹配的字符。而且我们可以发现,s 和模式串中的任意一个字符都不匹配,所以这时,我们可以直接把模式串移动到 s 的后面。 22 | 23 | 2、 24 | 25 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c992874dab45?w=777&h=234&f=png&s=10074) 26 | 27 | 从图中可以看出,此时 p 和 e 不匹配,所以 p 是一个**坏字符**,不过,我们可以发现 “p” 包含在模式串中 28 | 29 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c9b9b91c0ce7?w=358&h=229&f=png&s=4664) 30 | 31 | 所以,我们不能像第一步那样,把模式串直接全部移到 p 的后面去,而是移动两位,让两个 p 对齐。 32 | 33 | 4、 34 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3c9d1a6d6ad62?w=750&h=170&f=png&s=8558) 35 | 36 | 那么问题来了,**当我们碰到碰到坏字符的时候,该移动几位呢?** 37 | 下面我和大家讲一下这个问题,首先我们要算出**模式串**中两个字符的下标。这两个字符分别是 38 | 39 | (1)模式串中与坏字符对应的那个字符的下标,在我们上面那个例子中,就是 e。 40 | 41 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3ca24df6e549c?w=742&h=301&f=png&s=18298) 42 | 显然,这个 e 的下标是 6(从0开始算起)。我们用变量 t1 来代表这个字符的下标吧。 43 | 44 | (2)坏字符在模式串中的下标,在我们上面那个例子中,坏字符在模式串中的下标为 4,我们用变量 t2 来代表这个下标,如图 45 | 46 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3ca759d3afe3b?w=734&h=279&f=png&s=14976) 47 | 48 | 找出这两个字符的下标之后,我们就可以计算移动的位数了 49 | 50 | **移动的位数 = t1 - t2。** 51 | 52 | 例如上面的例子步骤 2 中 t1 = 6, t2 = 4,所以移动了 t1 - t2 = 2 位。 53 | 54 | **(1)这个时候可能有人会问了,那如果模式串中有多个 p 呢?** 55 | 56 | 答是如果有多个,我们只计算最右边的那个(当然是移动的位数越少越安全了) 57 | 58 | **(2)可能又有人会问,那如果模式串中并不存在坏字符呢?例如步骤1** 59 | 60 | 答是如果不存在的话,我们把 t2 赋值为 -1,即 t2 = -1。所以我们步骤 1 中移动了 t1 - t2 = 6 - (-1) = 7 位。 61 | 62 | 好了,现在我们已经解决了**遇到坏字符之后,应该移动多少位的问题**了。 63 | 64 | #### 好后缀 65 | 66 | 我们继续匹配 67 | 68 | 5、 69 | 70 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cba8ca1e977c?w=729&h=207&f=png&s=9101) 71 | 匹配,所以继续匹配前面的字符 72 | 73 | 6、 74 | 75 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cbb43b3ed33e?w=774&h=226&f=png&s=9674) 76 | 匹配,继续匹配前面的字符 77 | 78 | 7、 79 | 80 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cbbe1648642a?w=720&h=230&f=png&s=9416) 81 | 匹配,继续匹配前面的字符 82 | 83 | 8、 84 | 85 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cbc4a86af530?w=724&h=234&f=png&s=9558) 86 | 匹配,继续匹配前面的字符 87 | 88 | 9、 89 | 90 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cbd17b892f22?w=742&h=242&f=png&s=9808) 91 | 遇到坏字符 i,按照我们前面的规则,可以计算出 t1 = 2(就是a的下标)。t2 = -1(因为模式串不存在坏字符)。所以移动的位数是 t1 - t2 = 3。 92 | 93 | 但是,我想问一下,这是最好的移动方式吗?有没有更好的移动方法呢?接下来我就和大家介绍一种更好的方法,这种方法就是根据**好后缀**来移动位数。首先我们先介绍下啥的**好后缀**。 94 | 95 | 在上面的例子中,我们发现 "mple" 是能够成功匹配的 96 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cc250b3f096e?w=786&h=222&f=png&s=10054) 97 | 我们把这些能够成功匹配的子串,称之为**好后缀**,所以呢,e,le,elp,mple 都是好后缀 98 | > 因为 e, le, elp在之前的步骤中,也是能够成功匹配。不过 mple 是最长的好后缀。 99 | 100 | 接下来我们要**在模式串的前面寻找与好后缀匹配的子串**,这句话的意思就是说,我们要在模式串中寻找这样一个子串s:s 与好后缀匹配,并且s中的字符不能与好后缀有重叠。 101 | 102 | 我举个例子吧,例如有模式串 abcddab,然后好后缀分别是 b, ab, dab。那么与好后缀匹配的字串有 b,ab。(因为abcddab前面中的b可以与好后缀 b 匹配,前面的 ab 与好后缀 ab 匹配)。不过,没有与好后缀 dab 匹配的子串。 103 | 104 | 那么问题来了,如果我们找到了多个这样的子串的话,我们要选择哪一个呢?例如上面我们找到了两个,分别是 a,ab。 105 | 106 | 这个时候,我们选择与比较长的那个好后缀匹配的子串,例如,上面的例子中,我们会选择 ab,我们把这个被选中的子串(ab)称之为**好前缀**吧(我是为了后面方便描述,才给它这个一个称呼)。 107 | 108 | 找出了**好后缀**和**好前缀**之后 ,我们就可以知道要移动几位了,公式如下: 109 | 110 | 移动的位数 = 好后缀的下标 - 好前缀的下标。 111 | 112 | 当然,好后缀有多个,我们是选择和好前缀匹配的那一个。那么**好后缀的下标怎么算呢?**,计算方法是按照好后缀的**最后一个字符的下标为准**,例如模式串 abcddab 中好后缀 ab 的下标为 6(下标从 0 开始算起)。好前缀下标的方法也是一样,以最后一个字符的下标位准,例如模式串 abcddab 中,好前缀 ab 的下标为 1。 113 | 114 | > 这里可能有人会问,那如果不存在这样的好前缀呢?如果不存在的话,就用 -1 充当好前缀的下标。 115 | 116 | 知道了移动位数之后,我们继续来匹配我们上面的例子 117 | 118 | 10、 119 | ![](https://user-gold-cdn.xitu.io/2019/6/9/16b3cbd17b892f22?w=742&h=242&f=png&s=9808) 120 | 好后缀是 e, le, ple, mple,但是模式串中只有一个子串能够与好后缀 e 匹配,所以好前缀为 e。 121 | 122 | 显然,这个时候好前缀 e 的下标为 0,好后缀 e 的下标为 6,所以移动的位数为 6。如果按照我们最开始坏字符的移动规则的话,只能移动 3 位,而用好后缀可以移动 6 位。 123 | 124 | #### 选择坏字符的规则还是好后缀? 125 | 126 | 11、 127 | 128 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b3cfd9384e77f3?w=756&h=246&f=png&s=9905) 129 | 130 | 可能有人会问,两个规则我们应该要选择哪一个呢? 131 | 132 | 答案很简单,把两个规则的移动位数都算出来,选择移动位数多的就是了。 133 | 134 | 这里 p 是坏字符,并且不存在好后缀,所以采用坏字符的规则,移动 2 位。 135 | 136 | 12、 137 | 138 | ![](https://user-gold-cdn.xitu.io/2019/6/10/16b3d0481b35d998?w=817&h=220&f=png&s=9705) 139 | 这个时候,我们可以发现,模式串的字符全部都匹配了,这也意味着匹配结束了。 140 | 141 | #### 总结 142 | 143 | 这篇文章我是采用直接举例子的方式来讲,我觉得这样反而容易懂,并且在讲的过程中,可能没有讲的那么全,这是因为我不想说的太全,因为把所有情况都罗列处理的话,相信你容易晕。所以我才用这种方式,让你先懂了这个 BM 的算法思想,之后的细节,你可以再去琢磨。 144 | 145 | 为了讲清楚这个算法,也算是绞尽脑汁,特别是为了能够以最简单的方式来讲解**好后缀**的规则,停笔思索了好久,最后也百度搜索了几篇文章,看看别人都怎么讲,还翻开了我之前购买的数据结构与算法的专栏,,,最后结合自己的想法写了出来。希望这篇文章,能够让你读懂给 BM 算法,这个算法的核心就是坏字符和好后缀了,相信你一定能够搞懂!后面会连续讲解几篇与**树**有关的文章。 146 | 147 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 148 | 149 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /学算法/学递归/训练1:在两个长度相等的排序数组中找到上中位数.md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 给定两个有序数组arr1和arr2,已知两个数组的长度都为N,求两个数组中所有数的上中位数。要求时间复杂度O(logN),空间复杂度O(1) 4 | 5 | ####【举例】 6 | 7 | 例如 arr1 = [1, 2,3,4],arr2 = [3,4,5,6]。 8 | 9 | 总共8个数,则中位数就是第 4 小的数,为 3. 10 | 11 | 例如 arr1 = [0,1,2],arr2 = [3,4,5]。 12 | 13 | 总共6个数,则中位数就是第 3 小的数,为 2. 14 | 15 | ####【难度】 16 | 17 | 中 18 | 19 | #### 解答 20 | 21 | 这道题可以采用**递归来解决**,注意,这道题数组是有序的,所以它有如下特点: 22 | 23 | **(1)、当 两个数组的长度为偶数时:** 24 | 25 | 我来举个例子说明他拥有的特点吧。我们假定 26 | arr1 = [1, 2,3,4],arr2 = [3,4,5,6]。则数组的长度为 n = 4。 27 | 28 | 29 | ![](https://user-gold-cdn.xitu.io/2019/3/16/169863e4347a5998?w=455&h=215&f=png&s=6957) 30 | 31 | 分别选出这两个数组的上中位数的下标,即 32 | 33 | mid1 = (n-1)/2 = 1。 34 | 35 | mid2 = (n - 1)/2 = 1。 36 | 37 | 38 | ![](https://user-gold-cdn.xitu.io/2019/3/16/1698642256c9a858?w=516&h=339&f=png&s=10752) 39 | 40 | 假如 arr2[mid2] > arr2[mid1],那么我们要找的目标数是一定存在于 arr1[mid1+1...n] 和 arr2[0...mid2]中。而不可能存在于 arr1[0...mid1] 和 arr2[mid2+1...n] 之中。 41 | 42 | 43 | ![](https://user-gold-cdn.xitu.io/2019/3/16/169864ff0800b80d?w=503&h=525&f=png&s=15706) 44 | 45 | 也就是说,我们接下来只需要在arr1[mid1+1...n] 和 arr2[0...mid2] 中查找就行了。 46 | 47 | **(2)、当两个数组的长度为奇数时:** 48 | 49 | 假定 arr1 = [1, 2,3,4,5],arr2 = [3,4,5,6,7]。则数组的长度为 n = 5。 50 | 51 | mid1 = (n-1)/2 = 2。 52 | 53 | mid2 = (n - 1)/2 = 2。 54 | 55 | 这个时候如果 arr2[mid2] > arr1[mid1] 时,则和上面那个情况有点小差别,这时候目标数只存在于 arr1[mid1...n] 和 arr2[0...mid2]中。注意他们的差别,从arr1[mid1+1...n] => arr1[mid1...n]。 56 | 57 | 理解了这个原理,配合上代码会更好理解,代码如下: 58 | 59 | ```java 60 | public static int getUpMedian(int[] arr1, int[] arr2) { 61 | if(arr1 == null || arr2 == null ) 62 | return -1; 63 | // 开始寻找 64 | return find(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1); 65 | } 66 | 67 | public static int find(int[] arr1, int l1, int r1, int[] arr2, int l2, int r2) { 68 | int mid1 = l1 + (r1 - l1) / 2; 69 | int mid2 = l2 + (r2 - l2) / 2; 70 | // 表示数组只剩下一个数,把两个数组中较小的数返回去 71 | if (l1 >= r1) { 72 | return Math.min(arr1[l1], arr2[l2]); 73 | } 74 | // 元素个数为奇数,则offset为0,为偶数则 offset 为 1 75 | int offset = ((r1 - l1 + 1) & 1) ^ 1;// 用位运算比较快 76 | if (arr1[mid1] < arr2[mid2]) { 77 | return find(arr1, mid1+offset, r1, arr2, l2, mid2); 78 | } else if (arr1[mid1] > arr2[mid2]) { 79 | return find(arr1, l1, mid1, arr2, mid2 + offset, r2); 80 | } else { 81 | return arr1[mid1];// 返回 arr2[mid2]也可以。 82 | } 83 | } 84 | ``` 85 | 86 | 也可以用迭代来做,反而更加简单,迭代版本如下: 87 | 88 | ```java 89 | // 迭代版本 90 | public int getUpMedian2(int[] arr1, int[] arr2) { 91 | if (arr1 == null || arr2 == null) { 92 | return -1; 93 | } 94 | int l1 = 0; 95 | int r1 = arr1.length - 1; 96 | int l2 = 0; 97 | int r2 = arr2.length - 1; 98 | int mid1 = 0; 99 | int mid2 = 0; 100 | int offset = 0; 101 | while (l1 < r1) { 102 | mid1 = l1 + (r1 - l1) / 2; 103 | mid2 = l2 + (r2 - l2) / 2; 104 | offset = ((r1 - l1 + 1) & 1)^1; 105 | if (arr1[mid1] < arr2[mid2]) { 106 | l1 = mid1 + offset; 107 | r2 = mid2; 108 | } else if (arr1[mid1] > arr2[mid2]) { 109 | r1 = mid1; 110 | l2 = mid2 + offset; 111 | } else { 112 | return arr2[mid1]; 113 | } 114 | } 115 | return Math.min(arr1[l1], arr2[l2]); 116 | } 117 | 118 | ``` 119 | 120 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 121 | 122 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /学算法/学递归/训练2:求两个有序数组的第K小数.md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 给定两个有序数组arr1和arr2,已知两个数组的长度分别为 m1 和 m2,求两个数组中的第 K 小数。要求时间复杂度O(log(m1 + m2))。 4 | 5 | ####【举例】 6 | 7 | 例如 arr1 = [1, 2,3],arr2 = [3,4,5,6],K = 4。 8 | 9 | 则第 K 小数为 3. 10 | 11 | 例如 arr1 = [0,1,2],arr2 = [3,4,5,7,8], K = 3; 12 | 13 | 则第 K 小数为 2. 14 | 15 | #### 【难度】 16 | 17 | 难 18 | 19 | #### 解答 20 | 21 | 这道题和我上次讲的那一道题是非常非常类似的:[递归打卡1:在两个长度相等的排序数组中找到上中位数](https://mp.weixin.qq.com/s?__biz=MzUxNzg0MDc1Mg==&mid=2247485278&idx=3&sn=b9dfc094dd0ccd41f442973593b36c03&chksm=f9934d41cee4c4575a63b0fe24fc612b1a3f12b417757e087465de83f21074ea7c627ad4c09d&token=2135598748&lang=zh_CN#rd),如果没看过的建议先看下,只是今天的这道题比上次的那道题少难一点,原理一样。 22 | 23 | 下面我随便讲一下原理吧:采用递归的方法不断缩小 K 的,把求第 K 小元素转化为第 (K-K/2) 小元素....我举个例子吧,比较容易理解。 24 | 25 | 我们假定 arr1 = [1, 2,3],arr2 = [3,4,5,6],K = 4。 26 | 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/3/19/1699663ae346441a?w=417&h=253&f=png&s=6773) 29 | 30 | 31 | 和上一道题类似(注意:这里我们假设K从0算起,也就是有第0小元素,相当于令 K = K - 1),令 32 | 33 | mid1 = K/2 = 1。 34 | 35 | mid2 = K/2 = 1。 36 | 37 | 38 | 39 | ![](https://user-gold-cdn.xitu.io/2019/3/19/169966b4419048a9?w=489&h=309&f=png&s=9367) 40 | 41 | 此时 arr2[mid2] > arr2[mid1],那么问题转化为在数组 arr1[mid1+1...m1]和数组 arr2[0...m2] 寻找第(K-md1-1)小的元素。 42 | 43 | 44 | ![](https://user-gold-cdn.xitu.io/2019/3/19/1699678931c6330f?w=438&h=463&f=png&s=13245) 45 | 46 | 不过这里需要注意的是,有可能 k/2 的值是大于 m1 或者 m2的,所以如果 k/2 > m1 或者 m2 的话,我们直接令 md1 = m1-1 或者 md2 = m2-1 就行了。 47 | 48 | 代码如下: 49 | 50 | ```java 51 | // 由于中位数会受长度是奇偶数的影响,所以我们可以把问题转化为求 52 | // ((n+m+1)/2+(n+m+2)/2)/2。 53 | public double findMedianSortedArrays(int[] nums1, int[] nums2) { 54 | int n = nums1.length; 55 | int m = nums2.length; 56 | // return (findKthNumber(nums1, 0, n-1, nums2,0,m-1,(n+m+1)/2) + 57 | // findKthNumber(nums1, 0, m-1,nums2,0,m-1,(n+m+2)/2)) /2; 58 | return 1; 59 | } 60 | 61 | public static int findKth(int[] arr1, int[] arr2, int k) { 62 | if(arr1 == null || arr1.length < 1) 63 | return arr2[k-1]; 64 | if(arr2 == null || arr2.length < 1) 65 | return arr1[k-1]; 66 | // 注意这个函数的参数有7个,上面那个函数的参数只有3个,同名不同函数哈 67 | return findKth(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1, k - 1); 68 | } 69 | 70 | public static int findKth(int[] arr1, int l1, int r1, int[] arr2, int l2, int r2, int k) { 71 | // 递归结束条件 72 | if(l1 > r1) 73 | return arr2[l2 + k]; 74 | if(l2 > r2) 75 | return arr1[l1 + k]; 76 | if (k == 0)// 注意,k == 0的结束条件与上面两个结束条件不能颠倒。 77 | return Math.min(arr1[l1],arr2[l2]); 78 | int md1 = l1 + k/2 < r1 ? l1 + k/2 : r1; 79 | int md2 = l2 + k/2 < (r2 - l1) ? l2 + k/2 : r2; 80 | if(arr1[md1] < arr2[md2]) 81 | return findKth(arr1, md1 + 1, r1, arr2, l2, r2, k - k / 2 - 1); 82 | else if (arr1[md1] > arr2[md2]) 83 | return findKth(arr1, l1, r1, arr2, md2 + 1, r2, k - k / 2 - 1); 84 | else 85 | return arr1[md1];//返回arr2[md2]也可以,一样的。 86 | } 87 | 88 | // 测试 89 | public static void main(String[] args) { 90 | int[] arr1 = {1, 2, 3}; 91 | int[] arr2 = {0,4, 5, 6, 7, 8}; 92 | System.out.println(findKth(arr1, arr2, 2)); 93 | } 94 | ```` 95 | 96 | 可以用迭代吗?当然可以,不过留给你自己。 97 | 98 | 下次我还会再出一道与这两道类似的题,不过,难度递增。总共有三道这种题,一定要自己手动写代码,一定要自己手动写代码,一定要自己手动写代码。 99 | 100 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 101 | 102 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /学算法/学递归/训练3:求两个有序数组的中位数(论思维转换的重要性).md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 给定两个有序数组arr1和arr2,已知两个数组的长度分别为 m1 和 m2,求两个数组中的**中位数**。要求时间复杂度O(log(m1 + m2))。 4 | 5 | ####【举例】 6 | 7 | 例如 arr1 = [1, 3],arr2 = [2]. 8 | 9 | 则中位数为 2. 10 | 11 | 例如 arr1 = [1,2],arr2 = [3,4]. 12 | 13 | 则中位数是 (2 + 3)/2 = 2.5 14 | 15 | ####【难度】 16 | 17 | 难 18 | 19 | #### 解答 20 | 21 | 这道题和我上两次讲的那两道题是非常相似的,不过这道题比上面两道题要难很多,大家可以先想想怎么做这道题哦。 22 | 23 | 为什么说它难呢?其实是有原因的,如果两个数组的长度和为**奇数**的话,那么这道题不难,它比“求两个有序数组的第 K 小数”还简单;难就难在两个数组的长度和为**偶数**时,这道题的难度顿时上升了。 24 | 25 | 为什么呢?因为你要求出两个数,然后再来求平均,而且主要,这两个数可能一个数是位于 arr1 数组,而一个数是位于 arr2 数组,这会导致我们的判断逻辑变的很复杂。不信的话,你可以去试试。 26 | 27 | 那怎么办呢?实际上,这道题我们是可以进行一下转换,就是**无论两个数组的长度和是奇数还是偶数**,我们都求出第 (m1+m2+1)/2 小数以及第 (m1+m2+2)/2小数,然后求这两个数的平均数,就可以了。这样,我们就屏蔽了奇偶数的影响,会容易了挺多,并且可以利用我们上次写的”求两个有序数组的第 K 小数“来解决,这就是问题转换的重要性,要善于把复杂度的题转化为我们比较熟悉的题,才能举一反三。 28 | 29 | 30 | 31 | 32 | 33 | 代码如下: 34 | 35 | ```java 36 | // 由于中位数会受长度是奇偶数的影响,所以我们可以把问题转化为求 37 | // ((n+m+1)/2+(n+m+2)/2)/2。 38 | public double findMedianSortedArrays(int[] arr1, int[] arr2) { 39 | int n = arr1.length; 40 | int m = arr2.length; 41 | return (findKth(arr1, arr2,(n+m+1)/2) + findKth(arr1,arr2,(n+m+2)/2)) /2; 42 | } 43 | 44 | public static int findKth(int[] arr1, int[] arr2, int k) { 45 | if(arr1 == null || arr1.length < 1) 46 | return arr2[k-1]; 47 | if(arr2 == null || arr2.length < 1) 48 | return arr1[k-1]; 49 | // 注意这个函数的参数有7个,上面那个函数的参数只有3个,同名不同函数哈 50 | return findKth(arr1, 0, arr1.length - 1, arr2, 0, arr2.length - 1, k - 1); 51 | } 52 | 53 | public static int findKth(int[] arr1, int l1, int r1, int[] arr2, int l2, int r2, int k) { 54 | // 递归结束条件 55 | if(l1 > r1) 56 | return arr2[l2 + k]; 57 | if(l2 > r2) 58 | return arr1[l1 + k]; 59 | if (k == 0)// 注意,k == 0的结束条件与上面两个结束条件不能颠倒。 60 | return Math.min(arr1[l1],arr2[l2]); 61 | int md1 = l1 + k/2 < r1 ? l1 + k/2 : r1; 62 | int md2 = l2 + k/2 < (r2 - l1) ? l2 + k/2 : r2; 63 | if(arr1[md1] < arr2[md2]) 64 | return findKth(arr1, md1 + 1, r1, arr2, l2, r2, k - k / 2 - 1); 65 | else if (arr1[md1] > arr2[md2]) 66 | return findKth(arr1, l1, r1, arr2, md2 + 1, r2, k - k / 2 - 1); 67 | else 68 | return arr1[md1];//返回arr2[md2]也可以,一样的。 69 | } 70 | ```` 71 | 这三道递归的题可以说是后一道是前一道的进阶,虽然思路上不怎么难,但要实现出来还是有一定的难度,如果你都搞懂了,并且自己把代码写出来了,对你编写代码的能力一定会大大提升。 72 | 73 | 74 | 75 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 76 | 77 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /学算法/必学排序算法/漫画:为什么说O(n)复杂度的基数排序没有快速排序快?.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ![](https://user-gold-cdn.xitu.io/2019/1/6/16823501959e2f93?w=650&h=266&f=png&s=61505) 5 | 6 | 7 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168235105ecb6b0a?w=628&h=298&f=png&s=68978) 8 | 9 | 10 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682351e330f315a?w=592&h=281&f=png&s=61372) 11 | 12 | 13 | ![](https://user-gold-cdn.xitu.io/2019/1/6/16823526e51ce0db?w=614&h=307&f=png&s=63502) 14 | 15 | 16 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682352c7bd85357?w=593&h=266&f=png&s=64745) 17 | 18 | 19 | ![](https://user-gold-cdn.xitu.io/2019/1/6/16823533c7c75f63?w=585&h=260&f=png&s=62848) 20 | 21 | 22 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682353cf14e62ad?w=585&h=249&f=png&s=60293) 23 | 24 | 老大:我简单给你讲下吧,你学过那么多排序,估计一看就懂了。基数排序,是一种基数“桶”的排序,他的排序思路是这样的:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小...... 25 | 26 | 排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是采用“桶”来排序的,基本原理就是把具有相同个(十、百等)位数的数放进同一个桶里。我直接给你个例子吧,保证你一看就懂。 27 | 28 | 例如我们现在要对这组元素来排序: 29 | 30 | 31 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168235a8c8acf914?w=643&h=110&f=png&s=5821) 32 | 33 | 由于我们是以每个数的某位数来排序的,这位数的范围是0-9,所以我们需要10个桶。 34 | 35 | 36 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168235b0c7451bb4?w=756&h=233&f=png&s=8910) 37 | 38 | 第一遍,先以个位数排序,把具有相同个位数的数放进桶里,结果如下: 39 | 40 | 41 | 42 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168235d5749360b8?w=750&h=233&f=png&s=13224) 43 | 44 | 之后再按照从0号桶到9号桶的顺序取出来,结果如下 45 | 46 | 47 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168235ea7ac03ef4?w=766&h=375&f=png&s=23622) 48 | 49 | 个位数排序完成。 50 | 51 | 第二遍,以十位数来排,结果如下: 52 | 53 | 54 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682361f3b9b3c69?w=752&h=251&f=png&s=14272) 55 | 56 | 再取出来放回去: 57 | 58 | 59 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168236361064e891?w=742&h=428&f=png&s=24738) 60 | 61 | 十位数排序完成,最终的结果就是一组有序的元素。如果元素中有百位数的话,大不了就按照百位数再给他重复排一遍。 62 | 63 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682364d6e841615?w=630&h=286&f=png&s=71151) 64 | 65 | 66 | 67 | 68 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168236540e839fa6?w=591&h=256&f=png&s=67697) 69 | 70 | 71 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682365b2272d74d?w=609&h=257&f=png&s=61908) 72 | 73 | 老二:那我想问下,为啥要从个位数开始排序呢?可以直接从最高位开始排序吗?如果从最高位开始排序的话,如果一个数最高位比另一个数大,那么这个数就一定比另外一个数大了,不用在比较次高位了。这样的话,不是可以排的更快吗? 74 | 75 | 76 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682366b798abc64?w=609&h=220&f=png&s=62332) 77 | 78 | 老大:脑子反应的挺快啊。是的,是可以以最高位来排序的,而且也像你说的,以最高位来排序的话,是可以减少数据之间比较的次数。但我们仍然不建议以最高位来排序,因为他有个**致命的缺点**。 79 | 80 | 81 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682367fe600f1cd?w=578&h=231&f=png&s=59105) 82 | 83 | 84 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682368884c5e873?w=603&h=239&f=png&s=60665) 85 | 86 | 老大:还是以刚才那个例子吧,我们一边用最高位来排序,一边来寻找这个致命的缺点。数组如下(元素的顺序改变了一些): 87 | 88 | 89 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168236d601ebe0c2?w=671&h=84&f=png&s=5242) 90 | 91 | 第一遍:最高位十位数排序,结果如下(有些没用到的桶给省略了): 92 | 93 | 94 | 95 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682374b9d4491da?w=672&h=372&f=png&s=17119) 96 | 97 | 98 | 显然,不在桶一个桶里的数,他们的大小顺序已经是已知的了,也就是说,**右边桶的数一定比左边桶的数大**,所有在接下来的个位数排序里,我们只需要进行“各部分”单独排序就可以了,每一小部分都类似于原问题的一个子问题,做的时候可以采用递归的形式来处理。 99 | 100 | 101 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168237762418ae56?w=783&h=394&f=png&s=27969) 102 | 103 | 最后汇总,即可完成排序: 104 | 105 | 106 | ![](https://user-gold-cdn.xitu.io/2019/1/6/1682378b68c12cd7?w=797&h=498&f=png&s=34502) 107 | 108 | 这种方法确实可以减少比较的次数,不过请大家注意,在每个小部分的排序中,我们也是需要10个桶来将他们进行排序,最后导致的结果就是,**每个不同值的元素都会占据一个“桶”**,如果你有1000个元素,并且1000个元素都是不同值的话,那么从最高位排序到最低位,需要1000个桶。 109 | 110 | 这样子的话,空间花费不仅大,而且看起来有点背离基数排序最初的思想了(“背离”这个词,个人感觉而已)。所以,我们一般采用从最低位到最高位的顺序哦。 111 | 112 | 113 | 114 | 115 | ![](https://user-gold-cdn.xitu.io/2019/1/6/168237abf463f181?w=639&h=260&f=png&s=63721) 116 | 117 | **关于基数排序,还有以下几个问题,你不妨也想一想?** 118 | 119 | 1、基数排序是一种用空间换时间的排序算法,数据量越大,额外的空间就越大? 120 | 121 | 我的想法:我觉得基数排序并非是一种时间换空间的排序,也就是说,数据量越大,额外的空间并非就越大。因为在把元素放进桶的时候,是完全可以用指针指向这个元素的,也就是说,只有初始的那些桶才算是额外的空间。 122 | 123 | 2、居然额外空间不是限制基数排序速度的原因,那为啥基数排序没有快速排序快呢? 124 | 125 | 基数的时间复杂度为O(n),不过他是忽略了常数项,即实际排序时间为kn(其中k是常数项),然而在实际排序的过程中,这个常数项k其实是很大的,这会很大程度影响实际的排序时间,而像快速排序虽然是nlogn,但它前面的常数项是相对比较小的,影响也相对比较小。 126 | 127 | 需要说明的是,**基数排序也并非比快速排序慢**,这得看具体情况,(不要被标题所影响哈)。而且,数据量越大的话,基数排序会越有优势。 128 | 129 | 3、有人可能会问,说了这么多,那到底是基数排序快还是快速排序快呢? 130 | 131 | 对于这样的问题,我只能建议你,自己根据不同的场景,撸几行代码,自己测试一下。 132 | 133 | 如果你问我,哪个排序在实际中用的更多,那么,我选快速排序。 134 | 135 | 文章讲这里,也结束了,如果你有什么其它想法,欢迎后台来骚扰。 136 | 137 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 138 | 139 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /学算法/必学算法思维与技巧/位运算装逼指南.md: -------------------------------------------------------------------------------- 1 | 2 | 位算法的效率有多快我就不说,不信你可以去用 10 亿个数据模拟一下,今天给大家讲一讲位运算的一些经典例子。不过,最重要的不是看懂了这些例子就好,而是要在以后多去运用位运算这些技巧,当然,采用位运算,也是可以装逼的,不信,你往下看。我会从最简单的讲起,一道比一道难度递增,不过居然是讲技巧,那么也不会太难,相信你分分钟看懂。 3 | 4 | #### 判断奇偶数 5 | 6 | 判断一个数是基于还是偶数,相信很多人都做过,一般的做法的代码如下 7 | 8 | ```java 9 | if( n % 2) == 01 10 | // n 是个奇数 11 | } 12 | ``` 13 | 如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,代码如下: 14 | 15 | ```java 16 | if(n & 1 == 1){ 17 | // n 是个奇数。 18 | } 19 | ``` 20 | 有人可能会说,我们写成 n % 2 的形式,编译器也会自动帮我们优化成位运算啊,这个确实,有些编译器确实会自动帮我们优化。但是,我们自己能够采用位运算的形式写出来,当然更好了。别人看到你的代码,我靠,牛逼啊。无形中还能装下逼,是不是。当然,时间效率也快很多,不信你去测试测试。 21 | 22 | #### 2、交换两个数 23 | 24 | 交换两个数相信很多人天天写过,我也相信你每次都会使用一个额外来变量来辅助交换,例如,我们要交换 x 与 y 值,传统代码如下: 25 | 26 | ```java 27 | int tmp = x; 28 | x = y; 29 | y = tmp; 30 | ``` 31 | 这样写有问题吗?没问题,通俗易懂,万一哪天有人要为难你,**不允许你使用额外的辅助变量来完成交换呢?**你还别说,有人面试确实被问过,这个时候,位运算大法就来了。代码如下: 32 | 33 | ```java 34 | x = x ^ y // (1) 35 | y = x ^ y // (2) 36 | x = x ^ y // (3) 37 | ``` 38 | 我靠,牛逼!三个都是 x ^ y,就莫名交换成功了。在此我解释下吧,我们知道,两个相同的数**异或**之后结果会等于 0,即 n ^ n = 0。并且任何数与 0 异或等于它本身,即 n ^ 0 = n。所以,解释如下: 39 | 40 | 把(1)中的 x 带入 (2)中的 x,有 41 | 42 | y = x^y = (x^y)^y = x^(y^y) = x^0 = x。 x 的值成功赋给了 y。 43 | 44 | 对于(3),推导如下: 45 | 46 | x = x^y = (x^y)^x = (x^x)^y = 0^y = y。 47 | 48 | > 这里解释一下,异或运算支持运算的**交换律和结合律**哦。 49 | 50 | 以后你要是别人看不懂你的代码,逼格装高点,就可以在代码里面采用这样的公式来交换两个变量的值了,被打了不要找我。 51 | 52 | 讲这个呢,是想告诉你位运算的强大,让你以后能够更多着去利用位运算去解决一些问题,一时之间学不会也没事,看多了就学会了,不信?继续往下看,下面的这几道题,也是非常常见的,可能你之前也都做过。 53 | 54 | #### 3、找出没有重复的数 55 | 56 | > 给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。 57 | 58 | 这道题可能很多人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。 59 | 60 | 然而我想告诉你的是,采用位运算来做,绝对高逼格! 61 | 62 | 我们刚才说过,两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下: 63 | 64 | 65 | 由于异或支持交换律和结合律,所以: 66 | 67 | 1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。 68 | 69 | 也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。就问这个解法牛不牛逼?所以代码如下 70 | 71 | ```java 72 | int find(int[] arr){ 73 | int tmp = arr[0]; 74 | for(int i = 1;i < arr.length; i++){ 75 | tmp = tmp ^ arr[i]; 76 | } 77 | return tmp; 78 | } 79 | ``` 80 | 时间复杂度为 O(n),空间复杂度为 O(1),而且看起来很牛逼。 81 | 82 | #### 4、m的n次方 83 | 84 | 如果让你求解 2 的 n 次方,并且不能使用系统自带的 pow 函数,你会怎么做呢?这还不简单,连续让 n 个 m 相乘就行了,代码如下: 85 | 86 | ```java 87 | int pow(int n){ 88 | int tmp = 1; 89 | for(int i = 1; i <= n; i++) { 90 | tmp = tmp * m; 91 | } 92 | return tmp; 93 | } 94 | ``` 95 | 不过你要是这样做的话,我只能呵呵,时间复杂度为 O(n) 了,怕是小学生都会!如果让你用位运算来做,你会怎么做呢? 96 | 97 | 我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为: 98 | 99 | m^1101 = m^0001 * m^0100 * m^1000。 100 | 101 | 我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解: 102 | 103 | ```java 104 | int pow(int n){ 105 | int sum = 1; 106 | int tmp = m; 107 | while(n != 0){ 108 | if(n & 1 == 1){ 109 | sum *= tmp; 110 | } 111 | tmp *= tmp; 112 | n = n >> 1; 113 | } 114 | 115 | return sum; 116 | } 117 | ``` 118 | 时间复杂度近为 O(logn),而且看起来很牛逼。 119 | 120 | > 这里说一下,位运算很多情况下都是很二进制扯上关系的,所以我们要判断是否是否位运算,很多情况下都会把他们拆分成二进制,然后观察特性,或者就是利用**与,或,异或**的特性来观察,总之,我觉得多看一些例子,加上自己多动手,就比较容易上手了。所以呢,继续往下看,注意,先别看答案,先看看自己会不会做。 121 | 122 | #### 5、找出不大于N的最大的2的幂指数 123 | 124 | 传统的做法就是让 1 不断着乘以 2,代码如下: 125 | 126 | ```java 127 | int findN(int N){ 128 | int sum = 1; 129 | while(true){ 130 | if(sum * 2 > N){ 131 | return sum; 132 | } 133 | sum = sum * 2; 134 | } 135 | } 136 | ``` 137 | 这样做的话,时间复杂度是 O(logn),那如果改成位运算,该怎么做呢?我刚才说了,如果要弄成位运算的方式,很多时候我们把某个数拆成二进制,然后看看有哪些发现。这里我举个例子吧。 138 | 139 | 例如 N = 19,那么转换成二进制就是 00010011(这里为了方便,我采用8位的二进制来表示)。那么我们要找的数就是,把二进制中**最左边的 1 保留,后面的 1 全部变为 0**。即我们的目标数是 00010000。那么如何获得这个数呢?相应解法如下: 140 | 141 | 1、找到最左边的 1,然后把它右边的所有 0 变成 1 142 | 143 | 144 | ![](https://user-gold-cdn.xitu.io/2019/5/15/16abadc81fb35135?w=436&h=279&f=png&s=6770) 145 | 146 | 2、把得到的数值加 1,可以得到 00100000即 00011111 + 1 = 00100000。 147 | 148 | 3、把 得到的 00100000 向右移动一位,即可得到 00010000,即 00100000 >> 1 = 00010000。 149 | 150 | 那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?我先给出代码再解释吧。下面这段代码就可以把最左边 1 中后面的 0 全部转化为 1, 151 | 152 | ```java 153 | n |= n >> 1; 154 | n |= n >> 2; 155 | n |= n >> 4; 156 | ``` 157 | 就是通过把 n 右移并且做**或**运算即可得到。我解释下吧,我们假设最左边的 1 处于二进制位中的第 k 位(从左往右数),那么把 n 右移一位之后,那么得到的结果中第 k+1 位也必定为 1,然后把 n 与右移后的结果做或运算,那么得到的结果中第 k 和 第 k + 1 位必定是 1;同样的道理,再次把 n 右移两位,那么得到的结果中第 k+2和第 k+3 位必定是 1,然后再次做或运算,那么就能得到第 k, k+1, k+2, k+3 都是 1,如此往复下去.... 158 | 159 | 最终的代码如下 160 | ```java 161 | int findN(int n){ 162 | n |= n >> 1; 163 | n |= n >> 2; 164 | n |= n >> 4; 165 | n |= n >> 8 // 整型一般是 32 位,上面我是假设 8 位。 166 | return (n + 1) >> 1; 167 | } 168 | ``` 169 | 这种做法的时间复杂度近似 O(1),重点是,高逼格。 170 | 171 | #### 总结 172 | 173 | 上面讲了 5 道题,本来想写十道的,发现五道就已经写了好久了,,,,十道的话,怕你们也没耐心写完,而且一道比一道难的那种,,,,。 174 | 175 | 不过呢,我给出的这些例子中,并不是让你们学会了这些题就 Ok,而且让你们有一个意识:**很多时候,位运算是个不错的选择**,至少时间效率会快很多,而且**高逼格**,装逼必备。所以呢,以后可以多尝试去使用位运算哦,以后我会再给大家找些题来讲讲,遇到高逼格的,感觉很不错的,就会拿来供大家学习了。 176 | 177 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 178 | 179 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /学算法/必学算法思维与技巧/分享一道解法巧妙的算法题.md: -------------------------------------------------------------------------------- 1 | 最近碰到很多通过巧妙着运用**位运算**来巧妙解决复杂问题的算法,今天分享的这道题,或许能够开拓你的一些算法思维。 2 | 3 | #### 题目描述 4 | 5 | 有一组存放 ID 的数据。并且 ID 取值为 0 - (N-1) 之间,其中只有一个 ID 出现的次数为 1,其他的 ID 出现的次数都等于 2,问如何找到这个次数为 1 的 ID ? 6 | 7 | #### 解法一:巧用数组下标 8 | 9 | 不知道有多少人还记得我之前分享的**巧用数组下标**的技巧:[一些常用的算法技巧总结](https://mp.weixin.qq.com/s/oncH5ya-J6vH-Yn66kFFhw)。 10 | 11 | 我的第一想法便是采用**下标法**来解决,把 ID 作为数组 arr 的下标,在遍历 ID 的过程中,用数组记下每个 ID 出现的次数,即每次遍历到 ID = n,则 arr[n]++。 12 | 13 | 之后我们在遍历数组 arr,找到 arr[n] = 1 的ID,该下标 n 便是我们要寻找的目的 ID。 14 | 15 | 这种方法的时间复杂度为 O(N),空间复杂度为 O(N)。 16 | 17 | #### 解法二:巧用哈希表 18 | 19 | 显然时间复杂度是无法再降低的了,因为我们必须要遍历所有的 ID,所以时间复杂度最少都得为 O(N)了,所以我们要想办法降低空间复杂度。 20 | 21 | 大家想一个问题,假如我们检测到某个 ID 已经出现了 2 次了,那么这个 ID 的数据我们还需要存储记录吗?大部分的 ID 都出现了 2 次,这一大部分的数据真的需要存储吗? 22 | 23 | 答是不用的,因为出现 2 次的 ID 不是我们所要找的。所以我们可以优化**解法一**,我们可以采用**哈希表**来记录 ID 出现的次数:利用哈希表记下每个 ID 出现的次数,每次遇见一个 ID,就把这个 ID 放进 哈希表,如果这个 ID 出现了次数已经为 2 了,我们就把这个 ID 从哈希表中移除,最后哈希表只会剩下一个我们要寻找的 ID。 24 | 25 | 这个方法最好的情况下空间复杂度可以降低到 O(1),最坏的情况仍然了 O(N)。 26 | 27 | #### 解法三:巧用位运算 28 | 29 | 那究竟有没办法让空间复杂度在最坏的情况下也是 O(1) 呢? 30 | 31 | 答是有的,按就是采用**异或**运算。异或运算有个特点: 32 | 33 | **异或运算特点**:相同的两个数**异或**之后,结果为 0,任何数与0异或运算,其结果不变并且,异或运算支持**结合律**。 34 | 35 | 所以,我们可以把所有的 ID 进行异或运算,由于那些出现两次的 ID 通过异或运算之后,结果都为 0,而出现一次的 ID 与 0 异或之后不变,又因为异或支持结合律,所以,把所有 ID 进行异或之后,最后的结果便是我们要找的 ID。 36 | 37 | 这个方法的空间复杂度为 O(1),巧妙利用了位运算,而且运算的效率是非常高效的。 38 | 39 | #### 问题拓展 40 | 41 | 假如有 2 个 ID 出现的次数为 1,其他 ID 出现的次数都为 2 呢?有该如何解决呢?是否还是可以用位运算呢? 42 | 43 | > 为了方便这里我们先假设 异或 的符号为 @, 44 | 45 | 答是必须的,假如这两个出现一次的 ID 分别为 A, B,则所有 ID 异或后的结果为 A@B,这时我们遇到的问题是无法确定 A,B的值。 46 | 47 | 由于 A 和 B 是不一样的值,所以 A@B 的结果不为 0,也就是说,这个**异或值的二进制中某一位为1**。显然,A 和 B 中有且仅有一个数的相同位上也为 1。 48 | 49 | 这个时候,我们可以把所有 ID 分为两类,一类在这个位上为 1,另一类为 0,那么对于这两类,一类会含有 A,另一类会含有 B。于是,我们可以分别计算这两类 ID 的异或值,几可得到 A 和 B 的值。 50 | 51 | #### 总结 52 | 53 | 大家做刷题的时候,不妨多加上一个想法:是否可以用的上位运算这种思路。 54 | 55 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 56 | 57 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /学算法/必学算法思维与技巧/最求极致:我是如何把easy级别的算法题做成hard级别的.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 我们平时在刷题的时候,我觉得大致可分为以下几类题 4 | 5 | 1、这道题的**暴力**解法很简单,几乎人人都会做,但**最优解**却很难。 6 | 7 | 2、如果你懂某些算法思想,这道题很简单,如果不懂,那么这道题顿时很难,例如有些需要dp来处理的。 8 | 9 | 3、这种题型没做过,没啥思路,但接触过好几道之后,便会觉得异常简单,例如不能使用加减乘除运算符来完成**加法运算**。 10 | 11 | 4、最后一种是属于真正的难题,思路难想 ,就算知道了思想,编码也很难,因为临界点之类的特别多,逻辑特别复杂。 12 | 13 | 而我今天要强调的就是**第一类**题,我这里给的建议是,**我们要追求极致**,并且我今天会给出2道例题,大家也可以想想这2道题的解法。如果这2道题你不懂怎么做,那么看完一篇文章会有所收获。 14 | 15 | 我在牛客网刷题的时候,发现有些题的解法,如果你是采用**暴力**的话,是异常简单的,但是每次遇到这种题,我都不会轻易马上写代码,而是苦思一会,看看有没有**优雅的解法**。不过我去看很多通过的代码,发现大部分人的解法都是**很普通**,几乎算是**暴力法**,可能那些人看到这道题的时候心想:**我去,又秒杀一道题了,三分钟撸好了代码,一次就ac了这道题,心里满满的快感,马上接着下一题走起。** 16 | 17 | 但是,这种做法我是不支持的,因为这道题如果你草草了事,那么你没有任何优势,因为你这种解法别人也会;而且,做完这道题,你可能没有任何收获,估计只收获了快感。也就是说,做完这道题,**你并没有收获到这道题最核心的解法。** 18 | 19 | 所以,我觉得,对于这种题,我们一定要**追求极致**,不能“得过且过”。把这道题的精华吸收进来,**有些题的精华、技巧,是可以开拓你的思路的,进而影响 20 | 你对其他题的解法的**。 21 | 22 | > 当然,收获快感提升下解题的动力也是挺好的,而且最普通的解法对部分人来说也是收获满满的。我这里只是一个建议,如果可以,请追求极致。 23 | 24 | 下面我举几个例子,也算是顺便一起来刷几道题。 25 | 26 | #### 案例1:构建乘积数组 27 | 28 | > 题目描述:给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。 29 | 30 | 这道题简单吗?就算不能使用除法,也是挺简单的,每次算 B[i],都全部遍历一下数组,两个 for 循环就搞定了,时间复杂度是 O(n^2)。 31 | 32 | 但是,我敢保证,这种做法95%的人都会做,没有任何优势。所以我们想想是否有更加优雅的解法,有人说,想不出来怎么办? 33 | 34 | 很简单,想不出来就看看别人怎么做,百度搜索 or 讨论区看答案。**看别人的解法,一点也不丢人**。 35 | 36 | 所以对于这道题,更好的解法应该是这样的: 37 | 38 | 1、先算出 B[i] = A[0] * ...A[i-1]。 39 | 40 | 2、接着算 B[i] = A[i+1]...A[n-1] * B[i](B[i]是步骤1 算出来的值) 41 | 42 | 代码如下 43 | ```java 44 | public int[] multiply(int[] A) { 45 | int len = A.length; 46 | int[] B = new int[len]; 47 | B[0] = 1; 48 | //分两步求解,先求 B[i] = A[0]*..A[i-1]; 49 | for(int i = 1; i < len; i++){ 50 | B[i] = B[i-1] * A[i-1]; 51 | } 52 | //再求B[i]=A[i+1]*...A[n-1]; 53 | int tmp = A[len-1]; 54 | for(int i = len - 2; i >= 0; i--){ 55 | B[i] *= tmp; 56 | tmp *= A[i]; 57 | } 58 | 59 | return B; 60 | } 61 | ``` 62 | 时间复杂度是 O(n)。有人可能会问,那我怎么知道这个解法是否就为**最优解**了呢?我觉得这个可以自己判断,加上多看一些点赞高的人的解法,如果时间复杂度、空间复杂度都和你差不多,那么几乎就可以认为是最优解了。就算实际上不是,而大佬们也都这样解,那么也可以姑且认为是最优解了。最重要的是,比你最开始想的解法好多了就行了。 63 | 64 | #### 案例2:数组中重复的数字 65 | 66 | **题目描述:**在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么2和3就是重复的数字了,那么可以随意返回2或者返回3都可以了,任意选择一个即可。 67 | 68 | **解法** 69 | 70 | 这道题简单吗?如果只是想 ac 过去的话,那么挺简单,例如以下两种解法 71 | 72 | 1、给数组排序下,然后从左到右遍历,看看有相邻的数有没有相等即可。时间复杂度是 O(nlogn),空间复杂度O(1). 73 | 74 | 2、用一个哈希表来存放这些数组,把数组元素值作为 key,相同元素的个数作为 value。遍历的过程中,只要发现某个 key 的 value 超过 1,那么这个数就是重复的了,直接返回。时间复杂度是 O(n),空间复杂度是 O(n)。 75 | 76 | 这两种解法相信都不难,也很容易想到,不过请记住,很容易想到的解法大多数时候都不是最优解。那么有更好的解法吗?答是有的,更好的解法是时间复杂度是 O(n),空间复杂度是 O(1)。方法如下: 77 | 78 | 由于数字的范围是 0-n-1,那么我们可以这样做:从左到右遍历数组arr,对于 arr[i],我们可以把arr[i]放到数组下标为arr[i]的位置上,即arr[arr[i]] = arr[i]。例如 arr[0] = 4,那么我们就把arr[0]与arr[4]进行交换。假如数组中没有重复的数,那么遍历完成后的结果是 arr[i] = i。如果数组有重复的元素,那么当我们要把 arr[i] 与 arr[arr[i]] 进行交换的时候,会发现 arr[arr[i]] 的值已经为arr[i]了,并且arr[i] != i。 79 | 80 | 没看懂?那我做个演示吧,例如数组为 arr = {2, 3, 3, 0}。步骤如下 81 | 82 | 1、从左到右遍历,此时数组下标i = 0,即arr[i] = 2。把 arr[0] 与 arr[2] 进行交换,交换的结果为 arr = [3, 3, 2, 0}。 83 | 84 | 2、i = 0,由于 arr[0] = 3,不满足 arr[i] = i,所以我们的下标还是不能右移,还是得继续交换。即把 arr[0] 与 arr[3] 进行交换,结果为 arr[0, 3, 2, 3}。 85 | 86 | 3、i = 0,此时 arr[i] = i。故下标右移,即 令 i = 1。此时 arr[i] = arr[1] = 3。把arr[1] 和 arr[3] 进行交换,但是这时候 arr[3] = 3.即此时出现了 arr[arr[1]] = arr[3]并且 arr[i] != i 的情况,故出现了重复的元素了,直接把 3 返回,遍历结束。 87 | 88 | 如果看不懂,多看两遍就能看懂了,代码如下: 89 | 90 | ```java 91 | public int duplicate(int arr[],int length) { 92 | int i = 0; 93 | while(i < length){ 94 | if(arr[arr[i]] == arr[i]){ 95 | if(arr[i] == i){ 96 | i++; 97 | }else{ 98 | return arr[i]; 99 | } 100 | }else{ 101 | int tmp = arr[arr[i]]; 102 | arr[arr[i]] = arr[i]; 103 | arr[i] = tmp; 104 | } 105 | } 106 | return false; 107 | } 108 | ``` 109 | 这种方法建议大家掌握,我有好几道是遇到这种方法来处理的,例如我之前分享的几道题:[【算法精讲】分享一道很不错的算法题](https://mp.weixin.qq.com/s?__biz=Mzg2NzA4MTkxNQ==&mid=2247485408&idx=1&sn=60d6a278f8aac3901725cb1d4d6107c6&scene=19&token=884853672&lang=zh_CN#wechat_redirect) 110 | 111 | #### 总结 112 | 113 | 今天 姑且分享一些对**追求极致**的一些看法以及举了两道题,这两道题也是自己精挑细选的,希望大家能够有所收获,并且以后做题的时候能够严格要求自己。后面我会分享后面的几类题,每次都会分享一些不过的例题,力求至少让你看完这些例题,能够有所收获。 114 | 115 | 116 | 117 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 118 | 119 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /学算法/必学算法思维与技巧/算法数据结构中有哪些奇技淫巧?.md: -------------------------------------------------------------------------------- 1 | 今天的这篇文章,算是一种补充,同时会列举一些常见的算法题,如何用这些技巧来解决,通过使用这些方法,可以让一些算法题变的更加简单。 2 | 3 | #### 1、用 n & (n - 1)消去 n 最后的一位 1 4 | 5 | 在 n 的二进制表示中,如果我们对 n 执行 6 | 7 | n = n & (n - 1) 8 | 9 | 那么可以把 n 左右边的 1 消除掉,例如 10 | ```java 11 | n = 1001 12 | n - 1 = 1000 13 | n = n & (n - 1) = (1001) & (1000) = 1000 14 | ``` 15 | 这个公式有哪些用处呢? 16 | 17 | 其实还是有挺多用处的,在做题的时候也是会经常碰到,下面我列举几道经典、常考的例题。 18 | 19 | **(1)、判断一个正整数 n 是否为 2 的幂次方** 20 | 21 | 如果一个数是 2 的幂次方,意味着 n 的二进制表示中,只有一个位 是1,其他都是0。我举个例子,例如 22 | 23 | 2^0 = 0.....0001 24 | 25 | 2^1 = 0.....0010 26 | 27 | 2^2 = 0....0100 28 | 29 | 2^3 = 0..01000 30 | 31 | ..... 32 | 33 | 所以呢,我们只需要判断N中的二进制表示法中是否只存在一个 1 就可以了。按照平时的做法的话,我们可能会对 n 进行移位,然后判断 n 的二进制表示中有多少个 1。所以做法如下 34 | 35 | ```java 36 | boolean judege(int n) { 37 | int count = 0; 38 | int k = 1; 39 | while (k != 0) { 40 | if ((n & k) != 0) { 41 | count++; 42 | } 43 | k = k << 1; 44 | } 45 | return count == 1; 46 | } 47 | ``` 48 | 但是如果采用 n & (n - 1) 的话,直接消去 n 中的一个 1,然后判断 n 是否为 0 即可,代码如下: 49 | 50 | ```java 51 | boolean judege(int n){ 52 | return n & (n - 1) == 0;// 53 | } 54 | ``` 55 | 而且这种方法的时间复杂度我 O(1)。 56 | 57 | **(2)、整数 n 二进制中 1 的个数** 58 | 59 | 对于这种题,我们可以用不断着执行 n & (n - 1),每执行一次就可以消去一个 1,当 n 为 0 时,计算总共执行了多少次即可,代码如下: 60 | 61 | ```java 62 | public int NumberOf12(int n) { 63 | int count = 0; 64 | int k = 1; 65 | while (n != 0) { 66 | count++; 67 | n = (n - 1) & n; 68 | } 69 | return count; 70 | ``` 71 | 72 | **(3)、将整数 n 转换为 m,需要改变多少二进制位?** 73 | 74 | 其实这道题和(2)那道题差不多一样的,我们只需要计算 n 和 m 这两个数有多少个二进制位不一样就可以了,那么我们可以先让 n 和 m 进行异或,然后在计算异或得到的结果有多少个 1 就可以了。例如 75 | 76 | 令 t = n & m 77 | 78 | 然后计算 t 的二进制位中有多少 1 就可以了,问题就可以转换为(2)中的那个问题了。 79 | 80 | #### 2、双指针的应用 81 | 82 | 在之前的文章中 ,我也有讲过双指针,这里我在讲一下,顺便补充一些例子。 83 | 84 | **(1)、在链表中的应用** 85 | 86 | 对于双指针,我觉得用的最对的就是在**链表**这里了,比如“判断单链表是否有环”、“如何一次遍历就找到链表中间位置节点”、“单链表中倒数第 k 个节点”等问题。对于这种问题,我们就可以使用双指针了,会方便很多。我顺便说下这三个问题怎么用双指针解决吧。 87 | 88 | 例如对于第一个问题 89 | 90 | 我们就可以设置一个慢指针和一个快指针来遍历这个链表。慢指针一次移动一个节点,而快指针一次移动两个节点,如果该链表没有环,则快指针会先遍历完这个表,如果有环,则快指针会在第二次遍历时和慢指针相遇。 91 | 92 | 对于第二个问题 93 | 94 | 一样是设置一个快指针和慢指针。慢的一次移动一个节点,而快的两个。在遍历链表的时候,当快指针遍历完成时,慢指针刚好达到中点。 95 | 96 | 对于第三个问题 97 | 98 | 99 | 设置两个指针,其中一个指针先移动k个节点。之后两个指针以相同速度移动。当那个先移动的指针遍历完成的时候,第二个指针正好处于倒数第k个节点。 100 | 101 | 有人可能会说,采用双指针时间复杂度还是一样的啊。是的,空间复杂度和时间复杂度都不会变,但是,我觉得采用双指针,更加容易理解,并且不容易出错。 102 | 103 | **(2)、遍历数组的应用** 104 | 105 | 采用头尾指针,来遍历数组,也是非常有用的,特别是在做题的时候,例如我举个例子: 106 | 107 | > 题目描述:给定一个**有序**整数数组和一个目标值,找出数组中和为目标值的两个数。你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。 108 | ```java 109 | 示例: 110 | 给定 nums = [2, 7, 11, 15], target = 9 111 | 因为 nums[0] + nums[1] = 2 + 7 = 9 112 | 所以返回 [0, 1] 113 | ``` 114 | 其实这道题也是 leetcode 中的两数之和,只是我这里进行了一下改版。对于这道题,一种做法是这样: 115 | 116 | 从左到右遍历数组,在遍历的过程中,取一个元素 a,然后让 sum 减去 a,这样可以得到 b,即 b = sum - a。然后由于数组是有序的,我们再利用二分查找,在数组中查询 b 的下标。 117 | 118 | 在这个过程中,二分查找的时间复杂度是 O(logn),从左到右扫描遍历是 O(n),所以这种方法的时间复杂度是 O(nlogn)。 119 | 120 | 不过我们采用**双指针**的方法,从数组的头尾两边向中间夹击的方法来做的话,时间复杂度仅需为 O(n),而且代码也会更加简洁,这里我给出代码吧,代码如下: 121 | ```java 122 | public int[] twoSum1(int[] nums, int target) { 123 | int[] res = new int[2]; 124 | int start = 0; 125 | int end = nums.length - 1; 126 | while(end > start){ 127 | if(nums[start] + nums[end] > target){ 128 | end--; 129 | }else if(nums[start] + nums[end] < target){ 130 | start ++; 131 | }else{ 132 | res[0] = start; 133 | res[1] = end; 134 | return res; 135 | } 136 | } 137 | return res; 138 | 139 | } 140 | ``` 141 | 这个例子相对比较简单,不过这个头尾双指针的方法,真的用的挺多的。 142 | 143 | #### 3、a ^ b ^ b = a 的应用 144 | 145 | 两个相同的数异或之后的结果是 0,而任意数和 0 进行异或的结果是它本身,利用这个特性,也是可以解决挺多题,我在 leetcode 碰到过好几道,这里我举一些例子。 146 | 147 | **(1)数组中,只有一个数出现一次,剩下都出现两次,找出出现一次的数** 148 | 149 | 这道题可能很多人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。 150 | 151 | 我们刚才说过,两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下: 152 | 153 | 由于异或支持交换律和结合律,所以: 154 | 155 | 1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。 156 | 157 | 通过这种方法,可以把空间复杂度降低到 O(1),而时间复杂度不变,相应的黛米如下 158 | 159 | ```java 160 | int find(int[] arr){ 161 | int tmp = arr[0]; 162 | for(int i = 1;i < arr.length; i++){ 163 | tmp = tmp ^ arr[i]; 164 | } 165 | return tmp; 166 | } 167 | ``` 168 | 169 | #### 总结 170 | 171 | 这阵子由于自己也忙着复习,所以并没有找太多的例子,上面的那些题,有些在之前的文章也是有写过,这里可以给那些看过的忘了的复习一些,并且也考虑到可能还有一大部分人没看过。 172 | 173 | 所以呢,希望看完这篇文章,以后遇到某些题,可以多一点思路,如果你能用上这些技巧,那肯定可以大大降低问题的难度。 174 | 175 | 176 | 177 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 178 | 179 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /学算法/必学算法思维与技巧/面试官,求求你不要问我这么简单但又刁难的算法题了.md: -------------------------------------------------------------------------------- 1 | 有时候面试官往往会问我们一些简单,但又刁难的问题,主要是看看你对问题的处理思路。如果你没接触过这些问题,可能一时之间还真不知道怎么处理才比较好,这种题更重要的是一种思维的散发吧,今天就来分享几道题面试中遇到的算法题(当然,不是我自己遇到过,是别人遇到过,我挑选出来的) 2 | 3 | #### 案例1 4 | 5 | > 题目描述:求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 6 | 7 | 我去,求和居然不让用乘除法,也不准我们用循环,如果单独这两个限制的话还好,我们还可以用地递归,例如: 8 | 9 | ```java 10 | int f(int n){ 11 | if(n == 0){ 12 | return n; 13 | }else{ 14 | return f(n-1) + n; 15 | } 16 | } 17 | ``` 18 | 19 | 然后 if, else, case 等各种关键字也不给用,想着那我用三元运算符(A?B:C),然后这种具有判断语句的三元运算符也不给用,我去,这也太刁难了吧(当然,大佬直接秒杀的可以飘过)。 20 | 21 | 这道题肯定是必须用递归来解决的,而这递归的核心就是需要判断一下递归条件是否结束了,然而题目不准我们使用条件判断语句。那我们该怎么办呢?大家可以散发思维想一下哦。 22 | 23 | 其实我们可以下面这样的语句来代替 **A?B:C** 这样具有判断能力的三元运算符 24 | 25 | ``` 26 | n != 0 && (f(n-1) + n) != 0; 27 | ``` 28 | 这个 **&&** 逻辑判断符的作用就是:如果 n != 0 成立的话,那么逻辑判断符后面的判断语句 (f(n-1) + n ) != 0 也会执行,如果 n != 0 不成立的话,那么后面的判断语句 ((f(n-1) + n)) != 0 就不会执行,通过这种方法,就可以达到我们递归结束条件判断的目的了。 29 | 30 | > 这里说明一下,(f(n-1) + n) != 0 这条判断语句是没有任意其他含义的,我们的目的是为了执行 f(n-1)+n,之所以加上个 != 0 的判断,是因为逻辑判断符号 **&&** 只支持 boolean 类型,不支持 int 类型。 31 | 32 | 最后的代码如下 33 | ```java 34 | public int f(int n) { 35 | int sum = n; 36 | boolean t = (n != 0) && (sum += f(n - 1))!= 0; 37 | return sum; 38 | } 39 | ``` 40 | 如果你做过这种类型的题,可能就会觉得很简单了,如果没做过,可能就需要思考一下,不过,往后你就可以直接秒杀了。 41 | 42 | #### 案例 2 43 | 44 | > 题目描述:写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 45 | 46 | 我去,求和不准加减乘除!面试官,能不能别这么任性,好好的加减乘除居然不给用。 47 | 48 | 不过我相信大家第一时间都能想到用**位运算**来解决,可能在大学期间学过电路相关知识的一下就能把代码写出来了,不过有些人也能可能是一个位一个位来处理的。例如我先处理第一个位(这里指的是二进制位哈),看看有没进位,然后处理第二个位,如果第一个位有进位就加到第二个位来,然后处理第三个位..... 49 | 50 | 如果你是这种方法处理的,那么恭喜你,看完这道题你能有所收获。实际上上面那种解法也可以,只是太复杂了,可能各种判断。其实这道题可以这样解:这里为了方便讲解,我先给出代码,再给出具体的讲解,你看完代码再来看讲解可能更好理解 51 | 52 | ```java 53 | public int Add(int num1,int num2) { 54 | int tmp = 0; 55 | while(num1 != 0){ 56 | tmp = num1 ^ num2; 57 | num1 = (num1 & num2) << 1; 58 | num2 = tmp; 59 | } 60 | return num2; 61 | } 62 | ``` 63 | 64 | 大家想一个问题,如果我们把两个数进行**异或**,例如num1 = 101, num2 = 001,做异或运算:tmp = num1 ^ num2,结果是 tmp = 100。那么此时得到的结果 tmp 其实就是两个数(num1,num2)各个二进制位上相加,**不算进位**的结果。而 num1 = (num1 & num2) << 1 的结果就是两个数相加时那些**需要进位**的二进制位。例如 (101 & 001)<< 1 = 010,那么两个数第一位相加需要进位,我们需要把进的那一位最后加到第二位上去。 65 | 66 | 好像有点绕,,大家可以动手试一下哈,说白就是 a + b = a ^ b + (a & b) << 1。 67 | 68 | 代码中,如果 num1 == 0 的话,代表没有进位了,此时就可以退出循环了。 69 | 70 | 对于很少用位运算的人来说可能有点懵,那么我建议可以多看几遍,然后一遍动手模拟哈。以后遇到这种题就可以直接秒杀了。 71 | 72 | #### 案例3 73 | 74 | 在这里我先声明一下,案例3 也不算一道刁难题,只是我来考考你们而已,大家看到题目之后可以自己想一下哈,看了答案不能打我哈。 75 | 76 | > 题目描述:实现两个整数的相乘,不能使用乘法运算符和循环 77 | 78 | 各位老哥可以想一下哈。 79 | 80 | 这道题可能很多人都想到用递归了,好像我说的大部分算法题,都会用到递归,所以说你不懂递归的话,看我的公众号就行了,不懂也得变懂了是不是。代码如下: 81 | 82 | ```java 83 | int mul(int a, int b){ 84 | if (a == 0 || b == 0) 85 | return 0; 86 | if (b == 1) 87 | return a; 88 | if (a == 1) 89 | return b; 90 | return a + mul(a, b - 1); 91 | } 92 | int mult(int a,int b){ 93 | // 这里我们取 b 的绝对值 94 | int sum = mul(a, abs(b)); 95 | return (b<0)?(-sum):sum; 96 | } 97 | ``` 98 | 99 | 你是不是这样做的呢?其实,我们还有更好的方法哦,如下 100 | 101 | 我去,不能使用乘法,又没说不能使用除法,那我用除法来代替乘法就得了,例如 a 乘以 b 就相当于 a 除以 b 分之一。代码如下: 102 | 103 | ```java 104 | int mult2 (int a,int b){ 105 | return b != 0 ? (int)(a / (1.0 / b) + 0.99 ): 0; 106 | } 107 | ``` 108 | 这里需要 int 进行转化类型,并且除法可能会导致后面尾数的丢失,所以我补了个 0.99。注意,进行 int 类型转化时,不是四舍五入的哈,二手小于 1 就行当做 0 处理。当然,我这里用的是 Java 语言,其他语言自己看情况处理。 109 | 110 | #### 总结 111 | 112 | 今天的几道题,更多的是一种投机取巧吧,不过看你看到一到陌生的题目时,你会如何处理,点子多不多,这个还是挺重要滴,而多看一些点子,慢慢着你的点子也会变多了。 113 | 114 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 115 | 116 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉搜索树的后序遍历序列.md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 4 | 5 | ####【难度】 6 | 7 | 中 8 | 9 | #### 解答 10 | 11 | 一般对于二叉树的题目大多数都可以使用递归来做的,这道题也是用递归来多,思路如下: 12 | 13 | 14 | 二叉搜索树的后序序列有个这样的特点:序列的最后一个值为二叉树的根 root ;二叉搜索树左子树值都比 root 小,右子树值都比 root 大。 15 | 16 | 所以我们可以这样: 17 | 18 | 1、确定找出 root; 19 | 20 | 2、遍历序列(除去root结点),找到第一个大于root的位置,则该位置左边为左子树,右边为右子树; 21 | 22 | 3、遍历右子树,若发现有小于root的值,则是不符合二叉树搜索树的规则的,则直接返回false; 23 | 24 | 4、分别判断左子树和右子树是否仍是二叉搜索树(即递归步骤1、2、3)。 25 | 26 | 27 | 28 | 29 | 代码如下: 30 | 31 | ```java 32 | public static boolean VerifySquenceOfBST(int [] sequence) { 33 | if(sequence == null || sequence.length < 1) 34 | return false; 35 | return judge(sequence, 0, sequence.length - 1); 36 | } 37 | 38 | private static boolean judge(int[] sequence, int left, int right) { 39 | // 只有一个节点,递归结束 40 | if(left >= right) 41 | return true; 42 | // 最右边的节点相当于根节点 43 | int t = sequence[right]; 44 | // 用来记录序列中第一个比根节点大节点的下标 45 | int index = right; 46 | for (int i = left; i <= right - 1; i++) { 47 | // 找到根节点的右孩子 48 | if (sequence[i] > t) { 49 | index = i; 50 | i++; 51 | // 如果右子树中有比根节点还小的树的话,显然是不成立的。 52 | while (i <= right - 1) { 53 | if(sequence[i] < t) 54 | return false; 55 | i++; 56 | } 57 | } 58 | } 59 | // 递归检查左右子树 60 | return judge(sequence, left, index - 1) && judge(sequence,index, right - 1); 61 | } 62 | ``` 63 | 64 | 65 | 66 | 67 | 68 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 69 | 70 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的中序遍历(非递归版).md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 按照二叉树的中序遍历打印二叉树,并且不能使用递归。 4 | 5 | ####【难度】 6 | 7 | 易 8 | 9 | ####解答 10 | 11 | 二叉树的中序遍历顺序是**左-根-右**。我们可以采用一个栈来辅助,我们把中序遍历的结果放到一个 ArrayList 容器中作为返回值,具体步骤如下: 12 | 13 | 14 | 1、进入 while 循环,接着把根节点及其所有左子节点放入栈中。 15 | 16 | 2、从栈中取出一个节点,把该节点放入容器的尾部;如果该节点的右子节点不为空,则把右子节点及其右子节点的所有左子节点放入队列。 17 | 18 | 3、一直重复步骤 2 ,直到栈为空并且当前节点也为空则退出循环。 19 | 20 | 可能看解释反而有点乱,直接看代码吧,配合代码就容易懂了。 21 | 22 | 23 | 24 | 25 | 代码如下: 26 | 27 | ```java 28 | // 中序遍历 29 | public List inOderTraversal(TreeNode root) { 30 | List res = new ArrayList<>(); 31 | Stack stack = new Stack<>(); 32 | 33 | while (root != null || !stack.isEmpty()) { 34 | if (root != null) { 35 | stack.push(root); 36 | root = root.left; 37 | } else { 38 | root = stack.pop(); 39 | res.add(root.val); 40 | root = root.right; 41 | } 42 | } 43 | return res; 44 | } 45 | ``` 46 | 47 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 48 | 49 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的先序遍历(非递归版).md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 按照二叉树的先序遍历打印二叉树,并且不能使用递归。 4 | 5 | ####【难度】 6 | 7 | 易 8 | 9 | ####解答 10 | 11 | 二叉树的先序遍历顺序是**根-左-右**。我们可以采用一个栈来辅助,我们把先序遍历的结果放到一个 ArrayList 容器中作为返回值,具体步骤如下: 12 | 13 | 14 | 1、把二叉树的根节点 root 放进栈。 15 | 16 | 2、如果栈不为空,从栈中取出一个节点,把该节点放入容器的尾部;如果该节点的右子树不为空,则把有节点放入栈;如果该节点的左子树不为空,则把左子树放入栈中。 17 | 18 | 3、一直重复步骤 2 ,直到栈为空,此时遍历结束,代码如下: 19 | 20 | 21 | 22 | 23 | 代码如下: 24 | 25 | ```java 26 | // 迭代版 27 | static List preOderTraversal(TreeNode root) { 28 | List result = new ArrayList<>(); 29 | Stack stack = new Stack<>(); 30 | if(root == null) 31 | return result; 32 | stack.push(root); 33 | while (!stack.isEmpty()) { 34 | TreeNode tmp = stack.pop(); 35 | result.add(tmp.val); 36 | if(tmp.right != null) 37 | stack.push(tmp.right); 38 | if(tmp.left != null) 39 | stack.push(tmp.left); 40 | } 41 | return result; 42 | } 43 | ``` 44 | 45 | 46 | 47 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 48 | 49 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的后序遍历(非递归版).md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 按照二叉树的后序遍历打印二叉树,并且不能使用递归。 4 | 5 | ####【难度】 6 | 7 | 易 8 | 9 | ####解答 10 | 11 | 二叉树的后序遍历顺序是**左-右-根**。我们可以采用一个栈来辅助,不过它和前序遍历以及中序遍历还是有点区别的,我们把后序遍历的结果放到一个 LinkedList 容器中作为返回值,具体步骤如下: 12 | 13 | 14 | 1、把二叉树的根节点 root 放进栈。 15 | 16 | 2、如果栈不为空,从栈中取出一个节点,把该节点插入到容器的**头部**。;如果该节点的左子树不为空,则把该左子树放入栈中;如果该节点的右子树不为空,则把右子树放入栈中。, 17 | 18 | 注意,之前的前序遍历和中序遍历,我们都是用 ArrayList 容器,并且是把节点插入到容器的尾部,这就是后序遍历的不同点。 19 | 20 | 3、一直重复步骤 2 ,直到栈为空,此时遍历结束,代码如下: 21 | 22 | 23 | 24 | 25 | 代码如下: 26 | 27 | ```java 28 | // 后序遍历 29 | public List postOderTraversal(TreeNode root) { 30 | LinkedList res = new LinkedList<>();// 注意,采用链表 31 | Stack stack = new Stack<>(); 32 | if(root == null) 33 | return res; 34 | stack.push(root); 35 | while (!stack.isEmpty()) { 36 | TreeNode tmp = stack.pop(); 37 | // 注意,是放在第一个位置 38 | res.addFirst(tmp.val); 39 | if(tmp.left != null) 40 | stack.push(tmp.left); 41 | if(tmp.right != null) 42 | stack.push(tmp.right); 43 | 44 | } 45 | return res; 46 | } 47 | ``` 48 | 49 | 50 | 51 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 52 | 53 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的子结构.md: -------------------------------------------------------------------------------- 1 | #### 题目描述 2 | 3 | 输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构) 4 | 5 | 二叉树结构: 6 | 7 | ```java 8 | class TreeNode { 9 | int val; 10 | TreeNode left; 11 | TreeNode right; 12 | TreeNode(int x) { val = x; } 13 | } 14 | ``` 15 | 16 | 注:点击左下角的**阅读原文**即可跳转到原文,可以提交代码 17 | 18 | #### 解答思路 19 | 20 | 对于与**二叉树**有关的题目,90% 是采取**递归**的方式来解决比较简单的,这道题也是。 21 | 22 | 首先我们先以 A 的根节点 root1 作为起点来判断 B 是否为 A的子结构。 如果是则直接返回 true,如果不是,则递归以 root1.left 和 root1.right 作为起点来判断。代码如下: 23 | 24 | ```java 25 | public class 树的子结构 { 26 | public boolean HasSubtree(TreeNode root1,TreeNode root2) { 27 | if (root2 == null || root1 == null) { 28 | return false; 29 | } 30 | // 判断 B 是否为 A 的子结构 31 | return isSubTree(root1, root2); 32 | } 33 | 34 | // 判断 B 是否为 A 的子结构 35 | private boolean isSubTree(TreeNode root1, TreeNode root2) { 36 | if (root1 == null) { 37 | return false; 38 | }// 以root1为root2的根节点,判断子结构是否成立 39 | if (judge(root1, root2)) { 40 | return true; 41 | } else { 42 | // 如果root1作为起点不行,则递归判断左右节点 43 | return isSubTree(root1.left, root2) || isSubTree(root1.right, root2); 44 | } 45 | } 46 | // 以root1为root2的根节点,判断子结构是否成立 47 | private boolean judge(TreeNode root1, TreeNode root2) { 48 | if(root2 == null) 49 | return true; 50 | if(root1 == null) 51 | return false; 52 | if(root1.val == root2.val) 53 | return judge(root1.left, root2.left) && judge(root1.right, root2.right); 54 | 55 | return false; 56 | } 57 | } 58 | ``` 59 | 60 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 61 | 62 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的构建.md: -------------------------------------------------------------------------------- 1 | 首先我们要知道,三种不同遍历方式的过程。看下图很容易理解,并且不容易忘。 2 | 前序遍历: 根 左 右 3 | 中序遍历: 左 根 右 4 | 后序遍历: 左 右 根 5 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191218095937280.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70) 6 | 7 | #### 前序 + 中序 8 | 9 | 题意: 给你一个前序遍历和中序遍历,你要构造出一个二叉树。 10 | 示例: 11 | 12 | ``` 13 | 前序遍历 preorder = [3,9,20,15,7] 14 | 中序遍历 inorder = [9,3,15,20,7] 15 | ``` 16 | 17 | 要想解决这类题目,我们就要掌握遍历的**特点**。 18 | 19 | 1. 前序遍历第一位数字一定是这个二叉树的**根结点**。 20 | 2. 中序遍历中,根结点讲序列分为了左右两个区间。左边的区间是左子树的结点集合,右边的区间是右子树的结点集合。 21 | 22 | **我们能找到根节点,就能找到左子树和右子树的集合**,那么这个二叉树是不是就已经有了一个大致的样子。 23 | 接下来要做的,就是使用**递归**去构建出左子树和右子树。 24 | 25 | 示例过程: 26 | 27 | ![1](https://img-blog.csdnimg.cn/20191218102821765.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70)![2](https://img-blog.csdnimg.cn/20191218102825998.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70)![3](https://img-blog.csdnimg.cn/2019121810282895.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70) 28 | 代码: 29 | 30 | ```java 31 | class Solution { 32 | public TreeNode buildTree(int[] preorder, int[] inorder) { 33 | //用 HashMap 存储中序遍历,目的是查找方便。因为我们从前序遍历找到根节点后,还要寻找根节点在中序遍历的哪个位置 34 | HashMap map = new HashMap<>(); 35 | for(int i = 0; i < inorder.length; i++) 36 | map.put(inorder[i],i); 37 | return build(preorder, map, 0, preorder.length - 1, 0); 38 | } 39 | 40 | // 传入了五个参数,分别是:先序序列,中序序列 41 | // 先序序列的开始,先序序列的结束,中序序列的开始 42 | public TreeNode build(int[] preorder, HashMap map, int preStart, int preEnd, int inStart){ 43 | // 递归边界 44 | if(preEnd < preStart) 45 | return null; 46 | // 先序序列的第一位是根节点 47 | TreeNode root = new TreeNode(preorder[preStart]); 48 | //找到中序序列中,根节点的索引 index 49 | int rootIndex = map.get(root.val); 50 | // len 代表左子树的结点个数 51 | int len = rootIndex - inStart; 52 | // 左右子树的递归调用 53 | root.left = build(preorder, map, preStart + 1, preStart + len, inStart); 54 | root.right = build(preorder, map, preStart + len + 1, preEnd, rootIndex + 1); 55 | return root; 56 | } 57 | ``` 58 | 59 | #### 后序+中序 60 | 61 | 我们会理解了前序和中序遍历构造二叉树,那么后序和中序构造二叉树就不是难事。 62 | 后序序列的特点是,左,右,根。 63 | 64 | 1. 找到根结点(后序遍历的最后一位) 65 | 2. 在中序遍历中,找到根结点的位置,划分左右子树,递归构建二叉树。 66 | 67 | 这里希望各位自行在草稿纸上画一下,二叉树构建过程。 68 | 69 | 代码: 70 | 71 | ```java 72 | public TreeNode buildTree(int[] inorder, int[] postorder) { 73 | HashMap map = new HashMap<>(); 74 | for(int i = 0; i < inorder.length; i++) 75 | map.put(inorder[i],i); 76 | return build(postorder, map, 0, postorder.length - 1, 0); 77 | } 78 | 79 | public TreeNode build(int[] postorder, HashMap map, int postStart, int postEnd, int inStart){ 80 | if(postEnd < postStart) 81 | return null; 82 | TreeNode root = new TreeNode(postorder[postEnd]); 83 | int rootIndex = map.get(root.val); 84 | int len = rootIndex - inStart; 85 | // 前面与先序遍历是一样的,仅仅是划分左右子树的地方不同。 86 | root.left = build(postorder, map, postStart, postStart + len - 1, inStart); 87 | root.right = build(postorder, map, postStart + len, postEnd - 1, rootIndex + 1); 88 | return root; 89 | } 90 | ``` 91 | 92 | 两者的比较: 93 | ![前序遍历](https://img-blog.csdnimg.cn/20191218111227619.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70)![在这里插入图片描述](https://img-blog.csdnimg.cn/20191218111346354.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x4eDUzMjc=,size_16,color_FFFFFF,t_70) 94 | 95 | 由图片可以很清晰的看到,前序遍历是根左右,后序遍历是左右根。代码中递归的参数传递,即划分区域就是按照这个图片得到的。没理解代码可以结合图片去看。 96 | 97 | 二叉树的问题绝大部分都是和三种遍历有关,思考问题的时候,可以从三种遍历的特点入手。 98 | 99 | 100 | 101 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 102 | 103 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/二叉树的镜像.md: -------------------------------------------------------------------------------- 1 | #### 题目描述 2 | 3 | 4 | ![](https://user-gold-cdn.xitu.io/2019/2/27/1692fa420df9e2aa?w=471&h=545&f=png&s=26726) 5 | 6 | 二叉树结构: 7 | 8 | ```java 9 | class TreeNode { 10 | int val; 11 | TreeNode left; 12 | TreeNode right; 13 | TreeNode(int x) { val = x; } 14 | } 15 | ``` 16 | 17 | 注:点击左下角的**阅读原文**即可跳转到原文,可以提交代码 18 | 19 | #### 解答思路 20 | 21 | 在上题中我说了对于与**二叉树**有关的题目,90% 是采取**递归**的方式来解决比较简单的。而且解法还都非常相似,没看过上道题的或许可以看一下: 22 | 23 | 如果你递归学了还不错的话,这道题用递归会很简单,我们假设函数 Mirror() 就是求源二叉树的镜像。 24 | 25 | 刚开始的源二叉树: 26 | 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/2/27/1692fa729e01fe32?w=761&h=427&f=png&s=35478) 29 | 30 | 那么我们可以先对根节点的左右子树进行镜像化: 31 | 32 | left = Mirror(root.left); 33 | 34 | right = Mirror(root.right); 35 | 36 | 镜像化之后的结果: 37 | 38 | 39 | ![](https://user-gold-cdn.xitu.io/2019/2/27/1692fa7dd84b1f0b?w=792&h=415&f=png&s=35774) 40 | 41 | 之后我们只需要将 root 的左右节点进行交换即可 42 | 43 | 44 | ![](https://user-gold-cdn.xitu.io/2019/2/27/1692fa94c58b966f?w=748&h=328&f=png&s=33430) 45 | 46 | 代码如下: 47 | 48 | ```java 49 | public class 二叉树的镜像 { 50 | public void Mirror(TreeNode root) { 51 | if(root == null) 52 | return; 53 | root = solve(root); 54 | } 55 | private TreeNode solve(TreeNode root) { 56 | if(root == null) 57 | return root; 58 | // 递归先把左右节点镜像化 59 | TreeNode left = solve(root.left); 60 | TreeNode right = solve(root.right); 61 | // 对左右子树进行交换。 62 | root.left = right; 63 | root.right = left; 64 | return root; 65 | } 66 | } 67 | ``` 68 | 69 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 70 | 71 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/从上往下打印二叉树.md: -------------------------------------------------------------------------------- 1 | ####【题目】 2 | 3 | 从上往下打印出二叉树的每个节点,同层节点从左至右打印。 4 | 5 | ####【难度】 6 | 7 | 易 8 | 9 | ####解答 10 | 11 | 这个像相当于二叉树四种遍历中的**层序遍历**了,其思想是采用**广度优先遍历**,借助一个辅助队列,步骤如下: 12 | 13 | 14 | 1、把二叉树的根节点 root 放进队列。 15 | 16 | 2、如果队列不为空,取出队列的一个节点,把这个节点的左右孩子放进队列中(为空的话就不用放),然后打印这个节点。 17 | 18 | 3、一直重复步骤 2 ,直到队列为空,此时遍历结束,代码如下: 19 | 20 | 21 | 22 | 23 | 代码如下: 24 | 25 | ```java 26 | public ArrayList PrintFromTopToBottom(TreeNode root) { 27 | ArrayList list = new ArrayList<>(); 28 | Queue queue = new LinkedList<>(); 29 | if(root == null) 30 | return list; 31 | // 根放入队列 32 | queue.offer(root); 33 | while (!queue.isEmpty()) { 34 | TreeNode node = queue.poll(); 35 | list.add(node.val); 36 | if(node.left != null) 37 | queue.offer(node.left); 38 | if(node.right != null) 39 | queue.offer(node.right); 40 | } 41 | return list; 42 | } 43 | ``` 44 | 45 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 46 | 47 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /学算法/搞定二叉树/重建二叉树.md: -------------------------------------------------------------------------------- 1 | #### 题目描述 2 | 3 | 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 4 | 5 | 二叉树结构: 6 | 7 | ```java 8 | class TreeNode { 9 | int val; 10 | TreeNode left; 11 | TreeNode right; 12 | TreeNode(int x) { val = x; } 13 | } 14 | ``` 15 | 16 | 注:点击左下角的**阅读原文**即可跳转到原文,可以提交代码 17 | 18 | #### 解答思路 19 | 20 | 前序遍历序列的第一个元素 1 就是二叉树的根节点,中序遍历序列的根节点 1 把这个序列分成两半 21 | 部分,分别是[4,7,2]和[5,3,8,6],左半分部是根节点的左子树,右半分布是根节点的右子树。 22 | 23 | 基于这个特点,我们可以采用递归的方法来做,如果对递归的使用不是很熟的,建议看我之前的链表打卡[链表打卡汇总](),很多题都用到了递归,其大致逻辑如下。 24 | 25 | 1、通过前序序列第一个元素确定根节点(例如 1)。 26 | 27 | 2、通过根节点把中序序列分成两个序列,一个是左子树序列([4,7,2)],一个是右子树序列([5,3,8,6)]。 28 | 29 | 3、通过左右子树的中序序列可以求出前序遍历的左右子树序列(左:[2,4,7],右:[3,5,8,6])。 30 | 31 | 4、左右子树的前序序列第一个元素分别是根节点的左右儿子。 32 | 33 | 5、通过递归重复以上步骤(看代码吧,看了代码可能就好理解点了)。 34 | 35 | ```java 36 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 37 | 38 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 39 | 40 | 41 | 42 | public TreeNode reConstructBinaryTree(int [] pre,int [] in) { 43 | TreeNode root; 44 | root = rebuildTree(pre, 0, pre.length - 1, in, 0, in.length - 1); 45 | 46 | return root; 47 | } 48 | // preStart-preEnd表示前序序列的起始位置,inStart-inEnd也一样 49 | private TreeNode rebuildTree(int[] pre, int preStart, int preEnd, int[] in, int inStart, int inEnd) { 50 | if(preStart > preEnd | inStart > inEnd) 51 | return null; 52 | // 根节点 53 | TreeNode root = new TreeNode(pre[preStart]); 54 | // 寻找根节点在中序序列的位置 55 | for (int i = inStart; i <= inEnd; i++) { 56 | if (in[i] == pre[preStart]) { 57 | // 可以计算出中序序列的左右子树序列为:左:inStart~i -1,右:i+1~inEnd。 58 | // 前序序列的左右子树:左:preStart+1~preStart+i-inStart,右:preStart+i-inStart+1~preEnd 59 | root.left = rebuildTree(pre,preStart+1, preStart+i-inStart,in, inStart, i - 1); 60 | root.right = rebuildTree(pre,preStart+i-inStart+1, preEnd, in, i+1, inEnd); 61 | } 62 | } 63 | return root; 64 | } 65 | ``` 66 | 67 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练1:删除单链表的第K个节点.md: -------------------------------------------------------------------------------- 1 | #### 题目描述 2 | 3 | 在单链表中删除倒数第 K 个节点 4 | 5 | #### 要求 6 | 7 | 如果链表的长度为 N, 时间复杂度达到 O(N), 额外空间复杂度达到 O(1) 8 | 9 | #### 难度 10 | 11 | 士 12 | 13 | #### 解答 14 | 15 | 删除的时候会出现三种情况: 16 | 17 | 1、不存在倒数第 K 个节点,此时不用删除 18 | 19 | 2、倒数第 K 个节点就是第一个节点 20 | 21 | 3、倒数第 K 个节点在第一个节点之后 22 | 23 | 所以我们可以用一个变量 sum 记录链表一共有多少个节点。 24 | 25 | 如果 num < K,则属于第一种情况。 26 | 27 | 如果 num == K,则属于第二中情况。 28 | 29 | 如果 num > K, 则属于第三种情况,此时删除倒数第 K 个节点等价于删除第 (num - k + 1) 个节点。 30 | 31 | 代码如下: 32 | 33 | ```java 34 | //节点 35 | class Node{ 36 | public int value; 37 | public Node next; 38 | public Node(int data) { 39 | this.value = data; 40 | } 41 | } 42 | 43 | public class 删除倒数第K个节点 { 44 | public Node removeLastKthNode(Node head, int K) { 45 | if(head == null || K < 1) 46 | return head; 47 | Node temp = head; 48 | int num = 0; 49 | while (temp != null) { 50 | num++; 51 | temp = temp.next; 52 | } 53 | if (num == K) { 54 | return head.next; 55 | } 56 | if (num > K) { 57 | temp = head; 58 | //删除第(num-k+1)个节点 59 | //定位到这个点的前驱 60 | while (num - K != 0) { 61 | temp = temp.next; 62 | num--; 63 | } 64 | temp.next = temp.next.next; 65 | } 66 | return head; 67 | } 68 | } 69 | ``` 70 | 71 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 72 | 73 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练2:删除单链表的中间节点.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 给定链表的头节点head,实现删除链表的中间节点的函数。 4 | 5 |   例如: 6 | 7 |   步删除任何节点; 8 | 9 |   1->2,删除节点1; 10 | 11 |   1->2->3,删除节点2; 12 | 13 |   1->2->3->4,删除节点2; 14 | 15 |   1->2->3->4-5,删除节点3; 16 | 17 | ####【要求】 18 | 19 | 如果链表的长度为 N, 时间复杂度达到 O(N), 额外空间复杂度达到 O(1) 20 | 21 | ####【难度】 22 | 23 | 士:★☆☆☆ 24 | 25 | ####【解答】 26 | 27 | 这道题要求删除中间节点,我们可以采用双指针的方法来做,就是用一个快指针和一个慢指针,快指针每次前进两个节点,而慢指针每次前进一个节点。当快指针遍历完节点时,慢指针刚好就在中间节点了。之前写过一篇**一些算法的常用技巧**也有所过指针使用的一些技巧。 28 | 29 | 不过在做的时候,最好是先把一些特殊情况先处理好,例如删除的可能是第一个节点,也有可能不用删除节点(只有一个节点时就不用删除了。 30 | 31 | 32 | **代码如下** 33 | 34 | ```java 35 | public static Node removeMidNode(Node head) { 36 | if(head == null || head.next == null) 37 | return head; 38 | if (head.next.next == null) { 39 | return head.next; 40 | } 41 | Node fast = head.next.next;//快指针 42 | Node slow = head;//慢指针 43 | 44 | //slow最终指向中间节点的前驱 45 | while (fast.next != null && fast.next.next != null) { 46 | slow = slow.next; 47 | fast = fast.next.next; 48 | } 49 | //进行删除 50 | slow.next = slow.next.next; 51 | return head; 52 | } 53 | ``` 54 | > 上次拿到删除倒数第 K 个节点的题其实也是可以使用双指针的,但个人认为,那道题使用双指针的方法并没有我上次那个做法优雅,而这次删除中间节点,则用双指针比较优雅。至于原因,可以自己打下代码看看 55 | 56 | 57 | ### 问题拓展 58 | 59 | 题目:删除链表中 a / b 处的节点 60 | 61 | ### 【题目描述】 62 | 63 |   给定链表的头节点 head、整数 a 和 b,实现删除位于 a/b 处节点的函数。 64 | 65 |   例如: 66 | 67 |   链表:1->2->3->4->5,假设 a/b 的值为 r。 68 | 69 |   如果 r = 0,不删除任何节点; 70 | 71 |   如果 r 在区间 (0,1/5] 上,删除节点 1; 72 | 73 |   如果 r 在区间 (1/5,2/5] 上,删除节点 2; 74 | 75 |   如果 r 在区间 (2/5,3/5] 上,删除节点 3; 76 | 77 |   如果 r 在区间 (3/5,4/5] 上,删除节点 4; 78 | 79 |   如果 r 在区间 (4/5,1] 上,删除节点 5; 80 | 81 |   如果 r 大于 1,不删除任何节点。 82 | 83 | ####【要求】 84 | 85 | 如果链表的长度为 N, 时间复杂度达到 O(N), 额外空间复杂度达到 O(1) 86 | 87 | ####【难度】 88 | 89 | 士:★☆☆☆ 90 | 91 | ####【解答】 92 | 93 | 可以自己动手做一下或者想一下,如果想要获取答案,可以在公众号回复 **解答1** 获取代码。 94 | 95 | ```java 96 | //这道题可以转换为删除第 K = (a * n / b)个节点。其中n表示链表节点 97 | //的个数,但由于(a * n / b)有可能出现小数,所以我们取 K的上限。 98 | //所谓上限就是大于等于K的最小整数。 99 | public static Node removeByRatio(Node head, int a, int b) { 100 | if(a < 1 || a > b) 101 | return head; 102 | int n = 0; 103 | Node cur = head; 104 | //统计一共有多少个节点 105 | while (cur != null) 106 | n++; 107 | //问题转换为删除第K个节点,取(a * n / b)的整数上限 108 | int K = (int)Math.ceil((double)(a * n) / (double)b); 109 | if(K == 1) 110 | return head.next; 111 | if (K > 1) { 112 | cur = head; 113 | //定位到第K个节点的前驱 114 | while (--K != 1) { 115 | cur = cur.next; 116 | } 117 | cur.next = cur.next.next; 118 | } 119 | return head; 120 | } 121 | 122 | ``` 123 | 124 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 125 | 126 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练3:如何优雅着反转单链表.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 反转单链表。例如链表为: 4 | 5 | 1->2->3->4 6 | 7 | 反转后为 8 | 9 | 4->3->2->1 10 | 11 | ####【要求】 12 | 13 | 如果链表的长度为 N, 时间复杂度达到 O(N), 额外空间复杂度达到 O(1) 14 | 15 | ####【难度】 16 | 17 | 士:★☆☆☆ 18 | 19 | ####【解答】 20 | 21 | **方法1** 22 | 23 | 这道题还是挺简单的,当我们在反转一个节点的时候,把一个节点的后驱改为指向它前驱就可以了。这里需要注意的点就是,当你把当前节点的后驱指向前驱的时候,这个时候链表会被截断,也就是说后面的节点和当前节点分开了,所以我们需要一个变量来保存当前节点的后驱,以访丢失。 24 | 25 | 具体代码如下: 26 | 27 | 28 | 29 | **代码如下** 30 | 31 | ```java 32 | //节点 33 | class Node{ 34 | public int value; 35 | public Node next; 36 | public Node(int data) { 37 | this.value = data; 38 | } 39 | } 40 | ``` 41 | 42 | ``` java 43 | //反转单链表 44 | public static Node reverseList(Node head) { 45 | Node next = null;//指向当前节点的后驱 46 | Node pre = null;//指向当前节点的前驱 47 | while (head != null) { 48 | next = head.next; 49 | //当前节点的后驱指向前驱 50 | head.next = pre; 51 | pre = head; 52 | //处理下一个节点 53 | head = next; 54 | } 55 | return pre; 56 | ``` 57 | 58 | **方法二** 59 | 60 | 这道题也可以用递归来做,假设 方法 reverse() 的功能是将单链表进行逆转。采用递归的方法时,我们可以不断着对子链表进行递归。例如对于如下的链表: 61 | 62 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed67b2fe20cd?w=598&h=152&f=png&s=17604) 63 | 64 | 我们对子链表 2->3->4 进行递归,即 65 | Node newList = reverse(head.next)。递归之后的结果如下: 66 | 67 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed68e260b78f?w=512&h=264&f=png&s=23672) 68 | 69 | 逆转之后子链表 2->3->变为了 4->3->2。 70 | 注意,我刚才假设 reverse() 的功能就是对链表进行逆转。不过此时节点 1 仍然是指向节点 2 的。这个时候,我们再把节点1 和 2逆转一下,然后 1 的下一个节点指向 null 就可以了。如图: 71 | 72 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed6a6753073c?w=514&h=210&f=png&s=21170) 73 | 74 | 递归的结束条件就是:当子链表只有一个节点,或者为 null 时,递归结束。代码如下: 75 | 76 | ``` 77 | //用递归的方法反转链表 78 | public static Node reverseList2(Node head){ 79 | if (head == null || head.next == null) { 80 | return head; 81 | } 82 | //递归反转子lian链表 83 | Node newList = reverseList2(head.next); 84 | //第三张图 85 | head.next.next = head; 86 | head.next = null; 87 | return newList; 88 | } 89 | ``` 90 | 91 | ####问题拓展 92 | 93 | 题目:反转部分链表节点 94 | 95 | ####【题目描述】 96 | 97 | 98 | 题目:给定一个单向链表的头结点head,以及两个整数from和to ,在单项链表上把第from个节点和第to个节点这一部分进行反转 99 | 100 | 列如: 101 | 1->2->3->4->5->null,from=2,to=4 102 | 103 | 结果:1->4->3->2->5->null 104 | 105 | 列如: 106 | 107 | 1->2->3->null from=1,to=3 108 | 109 | 结果为3->2->1->null 110 | 111 | ####【要求】 112 | 113 | 1、如果链表长度为N,时间复杂度要求为O(N),额外空间复杂度要求为O(1) 114 | 115 | 2、如果不满足1<=from<=to<=N,则不调整 116 | 117 | ####【难度】 118 | 119 | 士:★☆☆☆ 120 | 121 | ####【解答】 122 | 123 | 可以自己动手做一下或者想一下,如果想要获取答案,可以在公众号 ** 帅地玩编程**回复 **解答2** 获取代码。 124 | 125 | ```java 126 | public static Node reversePart(Node head, int from, int to) { 127 | int len = 0;//记录链表的长度 128 | Node node1 = head; 129 | Node fPre = null;//指向第 from-1个节点 130 | Node tPos = null;//指向第 to + 1个节点 131 | while (node1 != null) { 132 | len++; 133 | if(len == from - 1) 134 | fPre = node1; 135 | if(len == to + 1) 136 | tPos = node1; 137 | node1 = node1.next; 138 | } 139 | //判断给定的值是否合理 140 | if(from > to || from < 1 || to > len) 141 | return head; 142 | //把from-to这部分链表进行反转 143 | //node1指向部分链表的第一个节点 144 | node1 = fPre == null ? head : fPre.next; 145 | Node cur = node1.next;//cur指向当前要处理的节点 146 | node1.next = tPos;//先把第一个节点给反转处理了 147 | Node next = null; 148 | while (cur != tPos) { 149 | next = cur.next;//保存当前节点的下一个节点 150 | cur.next = node1; 151 | node1 = cur; 152 | cur = next; 153 | } 154 | if (fPre != null) { 155 | fPre.next = node1; 156 | return head; 157 | } 158 | return node1; 159 | } 160 | 161 | ``` 162 | 163 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 164 | 165 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练4:环形单链表约瑟夫问题.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed5440456dc8?w=1080&h=270&f=png&s=204044) 4 | 5 | ####【要求】 6 | 7 | 输入:一个环形单向链表的头节点 head 和报数 m. 8 | 9 | 返回:最后生存下来的节点,且这个节点自己组成环形单向链表,其他节点都删除掉。 10 | 11 | ####【难度】 12 | 13 | 士:★☆☆☆ 14 | 15 | ####【解答】 16 | 17 | **方法1**:时间复杂度为 O( n * m) 18 | 19 | 这道题如果不考虑时间复杂度的话还是挺简单的,就遍历环形链表,每遍历 m 个节点就删除一个节点,知道链表只剩下一个节点就可以了。 20 | 21 | 22 | **代码如下** 23 | 24 | ```java 25 | //时间复杂度为O(n*m)的解决方法 26 | public static Node josephusKill(Node head, int m) { 27 | if(head == null || m < 1) 28 | return head; 29 | Node last = head; 30 | //定位到最后一个节点 31 | while (head.next != last) { 32 | head = head.next; 33 | } 34 | System.out.println(head.value); 35 | int count = 0; 36 | while (head.next != head) { 37 | if (++count == m) { 38 | head.next = head.next.next; 39 | count = 0; 40 | } else { 41 | head = head.next; 42 | } 43 | } 44 | return head; 45 | } 46 | ``` 47 | 这个方法的时间复杂度为 O(n * m)。下面用时间复杂度为方法解决。 48 | 49 | **方法二**:时间复杂度为 O(n) 50 | 51 | 这个方法的难度为: 52 | 53 | 校:★★★☆ 54 | 55 | 我们可以给环形链表的节点编号,如果链表的节点数为 n, 则从头节点开始,依次给节点编号,即头节点为 1, 下一个节点为2, 最后一个节点为 n. 56 | 57 | 58 | 我们用 f(n) 表示当环形链表的长度为n时,生存下来的人的编号为 f(n),显然当 n = 1 时,f(n) = 1。假如我们能够找出 f(n) 和 f(n-1) 之间的关系的话,我们我们就可以用递归的方式来解决了。我们假设 人员数为 n, 报数到 m 的人就自杀。则刚开始的编号为 59 | 60 | ... 61 | 62 | m - 2 63 | 64 | m - 1 65 | 66 | m 67 | 68 | m + 1 69 | 70 | m + 2 71 | 72 | ... 73 | 74 | 进行了一次删除之后,删除了编号为m的节点。删除之后,就只剩下 n - 1 个节点了,删除前和删除之后的编号转换关系为: 75 | 76 | 删除前 -------- 删除后 77 | 78 | ... ---------- ... 79 | 80 | m - 2 ------- n - 2 81 | 82 | m - 1 ------ n - 1 83 | 84 | m ---------- 无(因为编号被删除了) 85 | 86 | m + 1 ------ 1(因为下次就从这里报数了) 87 | 88 | m + 2 ------ 2 89 | 90 | ... -------- ... 91 | 92 | 93 | 新的环中只有 n - 1 个节点。且编号为 m + 1, m + 2, m + 3 的节点成了新环中编号为 1, 2, 3 的节点。 94 | 95 | 假设 old 为删除之前的节点编号, new 为删除了一个节点之后的编号,则 old 与 new 之间的关系为 old = (new + m - 1) % n + 1。 96 | 97 | > 注:有些人可能会疑惑为什么不是 old = (new + m ) % n 呢?主要是因为编号是从 1 开始的,而不是从 0 开始的。如果 new + m == n的话,会导致最后的计算结果为 old = 0。所以 old = (new + m - 1) % n + 1. 98 | 99 | 这样,我们就得出 f(n) 与 f(n - 1)之间的关系了,而 f(1) = 1.所以我们可以采用递归的方式来做。 100 | 101 | 102 | 103 | 104 | 105 | **代码如下:** 106 | 107 | ```Java 108 | //时间复杂度为O(n) 109 | public static Node josephusKill2(Node head, int m) { 110 | if(head == null || m < 1) 111 | return head; 112 | int n = 1;//统计一共有多少个节点 113 | Node last = head; 114 | while (last.next != head) { 115 | n++; 116 | last = last.next; 117 | } 118 | //直接用递归算出目的编号 119 | int des = f(n, m); 120 | //把目的节点取出来 121 | while (--des != 0) { 122 | head = head.next; 123 | } 124 | head.next = head; 125 | return head; 126 | } 127 | 128 | private static int f(int n, int m) { 129 | if (n == 1) { 130 | return 1; 131 | } 132 | return (getDes(n - 1, m) + m - 1) % n + 1; 133 | } 134 | ``` 135 | 136 | #### 问题拓展 137 | 138 | 对于上道题,假设是从第 K 个节点开始报数删除呢? 又该如何解决呢? 139 | 140 | #### 解答 141 | 142 | 在我的公众号 **帅地玩编程** 后台回复 **解答3**获取答案 143 | 144 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 145 | 146 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练5:三种方法带你优雅判断回文链表.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 给定一个链表的头节点 head, 请判断该链表是否为回文结构。 4 | 5 | 例如: 6 | 7 | 1->2->1,返回 true. 8 | 9 | 1->2->2->1, 返回 true。 10 | 11 | 1->2->3,返回 false。 12 | 13 | ####【要求】 14 | 15 | 如果链表的长度为 N, 时间复杂度达到 O(N)。 16 | 17 | ####【难度】 18 | 19 | 普通解法:士:★☆☆☆ 20 | 21 | 进阶解法:尉:★★☆☆ 22 | 23 | ####【解答】 24 | 25 | **方法1** 26 | 27 | 我们可以利用栈来做辅助,把链表的节点全部入栈,在一个一个出栈与链表进行对比,例如对于链表 1->2->3->2->2,入栈后如图: 28 | 29 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed38f4fe6936?w=328&h=325&f=png&s=19562) 30 | 然后再逐一出栈与链表元素对比。 31 | 32 | 这种解法比较简单,时间复杂度为 O(n), 空间复杂度为 O(n)。 33 | 34 | 35 | 36 | **代码如下** 37 | 38 | 39 | ``` java 40 | //方法1 41 | public static boolean f1(Node head) { 42 | if (head == null || head.next == null) { 43 | return true; 44 | } 45 | Node temp = head; 46 | Stack stack = new Stack<>(); 47 | while (temp != null) { 48 | stack.push(temp); 49 | temp = temp.next; 50 | } 51 | while (!stack.isEmpty()) { 52 | Node t = stack.pop(); 53 | if (t.value != head.value) { 54 | return false; 55 | } 56 | head = head.next; 57 | } 58 | return true; 59 | } 60 | ``` 61 | 62 | **方法二** 63 | 64 | 真的需要全部入栈吗?其实我们也可以让链表的后半部分入栈就可以了,然后把栈中的元素与链表的前半部分对比,例如 1->2->3->2->2 后半部分入栈后如图: 65 | 66 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed3a6a9b6433?w=341&h=314&f=png&s=17911) 67 | 68 | 然后逐个出栈,与链表的前半部分(1->2)对比。这样做的话空间复杂度会减少一半。 69 | 70 | 代码如下: 71 | 72 | ```java 73 | //方法2 74 | public static boolean f(Node head) { 75 | if(head == null || head.next == null) 76 | return true; 77 | Node slow = head;//慢指针 78 | Node fast = head;//快指针 79 | Stack stack = new Stack<>(); 80 | //slow最终指向中间节点 81 | while (fast.next != null && fast.next.next != null) { 82 | slow = slow.next; 83 | fast = fast.next.next; 84 | } 85 | System.out.println(slow.value); 86 | slow = slow.next; 87 | while (slow != null) { 88 | stack.push(slow); 89 | slow = slow.next; 90 | } 91 | //进行判断 92 | while (!stack.isEmpty()) { 93 | Node temp = stack.pop(); 94 | if (head.value != temp.value) { 95 | return false; 96 | } 97 | head = head.next; 98 | } 99 | return true; 100 | } 101 | ``` 102 | **方法三:**空间复杂度为 O(1)。 103 | 104 | 上道题我们有作过链表的反转的,没看过的可以看一下勒:[【链表问题】如何优雅着反转单链表](https://mp.weixin.qq.com/s?__biz=MzUxNzg0MDc1Mg==&mid=2247484857&idx=2&sn=e02aef30d1ec07df8ff6436c6f0e8518&chksm=f9934fa6cee4c6b007c7888358ea84d7bb929c0574ff6f233c49e669c4c13556c19f4f12cb77&token=1249924209&lang=zh_CN#rd/)],我们可以把链表的后半部分进行反转,然后再用后半部分与前半部分进行比较就可以了。这种做法额外空间复杂度只需要 O(1), 时间复杂度为 O(n)。 105 | 106 | 代码如下: 107 | 108 | ```java 109 | //方法3 110 | public static boolean f2(Node head) { 111 | if(head == null || head.next == null) 112 | return true; 113 | Node slow = head;//慢指针 114 | Node fast = head;//快指针 115 | //slow最终指向中间节点 116 | while (fast.next != null && fast.next.next != null) { 117 | slow = slow.next; 118 | fast = fast.next.next; 119 | } 120 | Node revHead = reverse(slow.next);//反转后半部分 121 | //进行比较 122 | while (revHead != null) { 123 | System.out.println(revHead.value); 124 | if (revHead.value != head.value) { 125 | return false; 126 | } 127 | head = head.next; 128 | revHead = revHead.next; 129 | } 130 | return true; 131 | } 132 | //反转链表 133 | private static Node reverse(Node head) { 134 | if (head == null || head.next == null) { 135 | return head; 136 | } 137 | Node newHead = reverse(head.next); 138 | head.next.next = head; 139 | head.next = null; 140 | return newHead; 141 | } 142 | ``` 143 | 144 | #### 问题拓展 145 | 146 | **思考**:如果给你的是一个环形链表,并且指定了头节点,那么该如何判断是否为回文链表呢? 147 | 148 | ####【题目描述】 149 | 150 | 无 151 | 152 | ####【要求】 153 | 154 | 无 155 | 156 | ####【难度】 157 | 158 | 未知。 159 | 160 | ####【解答】 161 | 162 | 无。此题为开放题,你可以根据这个设定各种其他要求条件。 163 | 164 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 165 | 166 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练6:将单向链表按某值划分成左边小,中间相等,右边大的形式.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 给定一个单向链表的头结点head,节点的值类型是整型,再给定一个整数privot。实现一个调整链表的函数,将链表调整为左部分都是值小于privot的节点,中间部分都是值等于privot的节点,右部分都是大于privot的节点。且对某部分内部节点的顺序不做要求 4 | 5 | 例如:链表9-0-4-5-1,pivot=3。 6 | 7 | 调整后是1-0-4-9-5, 8 | 9 | 也可以是0-1-9-5-4 10 | 11 | ####【要求】 12 | 13 | 如果链表的长度为 N, 时间复杂度达到 O(N)。 14 | 15 | ####【难度】 16 | 17 | 尉:★★☆☆ 18 | 19 | ####【解答】 20 | 21 | 这道题在思路上还是比较简单的,但是在实现上还是有一些细节需要主要的。 22 | 23 | 本题对某部分的内部节点不做要求,一种很简单的方法就是用一个数组来存链表的节点,然后像类似于**快速排序**的分割函数那样,按照某个值把他们进行划分。 24 | 25 | 不过这样做的话,空间复杂度为 O(N)。我们也可以采取使用3个指针,把原链表依次划分成三个部分的链表,然后再把他们合并起来,这种做法不但空间复杂度为 O(1), 而且内部节点的顺序也是和原链表一样的。虽然思路简单,但在代码实现上也是有很多细节需要注意的,有时间的话希望大家动手打下码。 26 | 27 | 28 | 29 | **代码如下** 30 | 31 | 32 | ``` java 33 | //用三个指针处理,这道题主要是要注意串联链表时的一些细节处理 34 | public static Node listPartition(Node head, int pivot) { 35 | Node sB = null;//小的指针头,即small begin 36 | Node sE = null;//小的指针尾,即 small end 37 | Node eB = null;//中的指针头,即 equal begin 38 | Node eE = null;//中的指针尾,即emall end 39 | Node bB = null;//大的指针头,即 big begin 40 | Node bE = null;//大的指针尾,即 big end 41 | Node next = null;//保存下一个节点 42 | //进行划分 43 | while (head != null) { 44 | next = head.next; 45 | head.next = null; 46 | if (head.value < pivot) { 47 | if (sB == null) { 48 | sB = head; 49 | sE = head; 50 | } else { 51 | sE.next = head; 52 | sE = sE.next; 53 | } 54 | } else if (head.value == pivot) { 55 | if (eB == null) { 56 | eB = head; 57 | eE = head; 58 | } else { 59 | eE.next = head; 60 | eE = eE.next; 61 | } 62 | } else { 63 | if (bB == null) { 64 | bB = head; 65 | bE = head; 66 | } else { 67 | bE.next = head; 68 | bE = bE.next; 69 | } 70 | } 71 | head = next; 72 | } 73 | //把三部分串连起来,串联的时候细节还是挺多的, 74 | //串联的过程下面代码的精简程度是最学习的部分了 75 | 76 | //1.小的与中的串联 77 | if (sB != null) { 78 | sE.next = eB; 79 | eE = eE == null ? sE : eE; 80 | } 81 | //2.中的和大的连接 82 | if (eB != null) { 83 | eE.next = bB; 84 | } 85 | return sB != null ? sB : eB != null ? eB : bB; 86 | } 87 | ``` 88 | 89 | #### 问题拓展 90 | 91 | **思考**:如果给你的是一个环形链表,让你来划分,又该如何实现呢? 92 | 93 | 94 | 95 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 96 | 97 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练7:复制含有随机指针节点的链表.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ed07f4042e26?w=1080&h=650&f=png&s=287459) 4 | 5 | ####【要求】 6 | 7 | 如果链表的长度为 N, 时间复杂度达到 O(N)。 8 | 9 | ####【难度】 10 | 11 | 尉:★★☆☆ 12 | 13 | ####【解答】 14 | 15 | **方法一**:使用额外的存储空间 16 | 17 | 这道题的难点在于我们需要定位好随机指针,一个比较简单的解法就是把原节点与复制的节点关联起来,可以使用哈希表把他们关联起来。 18 | 19 | 首先把副节点全部创建出来,然后把原节点与对应的副节点用哈希表关联起来。关联的时候原节点作为key,副节点作为value。例如对于链表 1->2->3->null。创建副节点 1', 2', 3'。然后用哈希表关联起来: 20 | 21 | 22 | key | value 23 | ---|--- 24 | 1 | 1' 25 | 2 | 2' 26 | 3 | 3' 27 | 28 | 29 | 30 | 之后在把所有副节点连接成一个链表。在连接的时候,我们 可以通过哈希表很容易这找到对应的随机节点。 31 | 32 | **代码如下** 33 | 34 | 35 | ``` java 36 | //方法1:采用哈希表 37 | public static Node1 copyListWithRand(Node1 head) { 38 | Map map = new HashMap<>(); 39 | Node1 cur = head; 40 | while (cur != null) { 41 | map.put(cur, new Node1(cur.value)); 42 | cur = cur.next; 43 | } 44 | //把副节点连接起来 45 | cur = head; 46 | while (cur != null) { 47 | map.get(cur).next = map.get(cur.next); 48 | map.get(cur).rand = map.get(cur.rand); 49 | cur = cur.next; 50 | } 51 | return map.get(head); 52 | } 53 | ``` 54 | 55 | 这种方法的时间复杂度为 O(n), 空间复杂度也为 O(n)。 56 | 57 | **方法2** 58 | 59 | 其实我们也可以不需要哈希表来辅助,也就是说 ,我们是可以做到空间复杂度为 O(1)的,我们可以把复制的副节点插入到原链表中去,这样也能把原节点与副节点进行关联,进而 60 | 定位到随机节点。例如,对于链表 1->2->3->null。首先生成副节点 1', 2', 3。然后把副节点插入到原节点的相邻位置,即把原链表变成 1->1'->2->2'->3->3'->null。 61 | 62 | 这样我们也可以在连接副节点的时候,找到相应的随机节点。例如 1 的随机节点是 3,则 1' 的随机节点是 3'。显然,1节点的随机节点的下一个节点就是 1'的随机节点。具体代码如下: 63 | 64 | ```java 65 | //方法二 66 | public static Node1 copyListWithRand2(Node1 head){ 67 | Node1 cur = head; 68 | Node1 next = null; 69 | 70 | //把复制的节点插进去 71 | while (cur != null) { 72 | next = cur.next; 73 | Node1 temp = new Node1(cur.value);//复制节点 74 | temp.next = cur.next; 75 | cur.next = temp; 76 | cur = next; 77 | } 78 | //在一边把复制的节点取出来一边连接。 79 | cur = head; 80 | next = null; 81 | while (cur != null) { 82 | next = cur.next.next;//保存原链表的下一个节点 83 | cur.next.next = next != null ? next.next : null; 84 | cur.next.rand = cur.rand != null ? cur.rand.next : null; 85 | cur = next; 86 | } 87 | return head.next; 88 | } 89 | ``` 90 | 采用这种方法的时候,由于随机节点有可能是空指针,随意写代码的时候要注意。 91 | 92 | #### 问题拓展 93 | 94 | **思考**:如果是有两个随机指针呢?又该如何处理呢?三个呢? 95 | 96 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 97 | 98 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练8:将单链表的每K个节点之间逆序.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 给定一个单链表的头节点head, 实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点。 4 | 5 | 例如: 6 | 7 | 链表:1->2->3->4->5->6->7->8->null, K = 3。 8 | 9 | 调整后:3->2->1->6->5->4->7->8->null。其中 7,8不调整,因为不够一组。 10 | 11 | ####【要求】 12 | 13 | 如果链表的长度为 N, 时间复杂度达到 O(N)。 14 | 15 | ####【难度】 16 | 17 | 尉:★★☆☆ 18 | 19 | ####【解答】 20 | 21 | 22 | 对于这道题,如果你不知道怎么逆序一个单链表,那么可以看一下我之前写的[【链表问题】如何优雅着反转单链表](https://mp.weixin.qq.com/s?__biz=MzUxNzg0MDc1Mg==&mid=2247484857&idx=2&sn=e02aef30d1ec07df8ff6436c6f0e8518&chksm=f9934fa6cee4c6b007c7888358ea84d7bb929c0574ff6f233c49e669c4c13556c19f4f12cb77&token=1837255454&lang=zh_CN#rd) 23 | 24 | 这道题我们可以用递归来实现,假设方法reverseKNode()的功能是**将单链表的每K个节点之间逆序**。reverse()方法的功能是将一个单链表逆序。 25 | 26 | 那么对于下面的这个单链表,其中 K = 3。 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ecb9444d13b8?w=961&h=139&f=png&s=8444) 29 | 30 | 我们把前K个节点与后面的节点分割出来: 31 | 32 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ecba5689fd9a?w=708&h=301&f=png&s=11117) 33 | 34 | temp指向的剩余的链表,可以说是原问题的一个子问题。我们可以调用reverseKNode()方法将temp指向的链表每K个节点之间进行逆序。再调用reverse()方法把head指向的那3个节点进行逆序,结果如下: 35 | 36 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ecbd6ffd36e7?w=653&h=261&f=png&s=11081) 37 | 38 | 接着,我们只需要把这两部分给连接起来就可以了。最后的结果如下: 39 | 40 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ecbe9fc958ea?w=1004&h=135&f=png&s=8852) 41 | 42 | 如果不大理解,看下代码可能就比较好理解了。 43 | 44 | 45 | 46 | 47 | **代码如下** 48 | 49 | 50 | ``` java 51 | //每k个节点为一组的逆转 52 | public static Node reverseKNodes(Node head, int k) { 53 | if (head == null || head.next == null) { 54 | return head; 55 | } 56 | Node cur = head; 57 | for (int i = 1; cur != null && i < k; i++) { 58 | cur = cur.next; 59 | } 60 | //判断是否能组成一组。 61 | if (cur == null) { 62 | return head; 63 | } 64 | //temp指向剩余的链表 65 | Node temp = cur.next; 66 | cur.next = null; 67 | //把k个节点进行反转 68 | Node newHead = reverse(head); 69 | //把之后的部分链表进行每K个节点逆转转 70 | Node newTemp = reverseKNodes(temp, k); 71 | //把两部分节点连接起来 72 | return newHead; 73 | } 74 | 75 | //单链表逆序 76 | public static Node reverse(Node head) { 77 | if (head == null || head.next == null) { 78 | return head; 79 | } 80 | Node newHead = reverse(head.next); 81 | head.next.next = head; 82 | head.next = null; 83 | return newHead; 84 | } 85 | ``` 86 | 当然,这道题一个很简单的做法就是利用栈来辅助,每K个节点入栈就把这K个节点出栈连接成一个链表,之后剩余再在进栈..... 87 | 88 | 不过这种做法的额外空间复杂度是O(K)。 89 | 90 | #### 问题拓展 91 | 92 | **思考**:如果这是一个环形单链表呢?该如何实现呢? 93 | 94 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 95 | 96 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /学算法/搞定链表/链表训练9:将搜索二叉树转换成双向链表.md: -------------------------------------------------------------------------------- 1 | ####【题目描述】 2 | 3 | 对于二叉树的节点来说,有本身的值域,有指向左孩子和右孩子的两个指针;对双向链表的节点来说,有本身的值域,有指向上一个节点和下一个节点的指针。在结构上,两种结构有相似性,现有一棵搜索二叉树,请将其转为成一个有序的双向链表。 4 |   5 | 节点定义: 6 | ```java 7 | class Node2{ 8 | public int value; 9 | public Node2 left; 10 | public Node2 right; 11 | 12 | public Node2(int value) { 13 | this.value = value; 14 | } 15 | } 16 | ``` 17 | 18 | 例如: 19 | 20 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec79011edc04?w=291&h=263&f=png&s=12049) 21 | 22 | 这棵二查搜索树转换后的双向链表从头到尾依次是 1~9。对于每一个节点来说,原来的 right 指针等价于转换后的 next 指针,原来的 left 指针等价于转换后的 last 指针,最后返回转换后的双向链表的头节点。 23 | 24 | ####【要求】 25 | 26 | 如果链表的长度为 N, 时间复杂度达到 O(N)。 27 | 28 | ####【难度】 29 | 30 | 尉:★★☆☆ 31 | 32 | ####【解答】 33 | 34 | **方法一:采用队列辅助** 35 | 36 | 如果用一个队列来辅助的话,还是挺容易。采用**中序遍历**的方法,把二叉树的节点全部放进队列,之后在逐一弹出来连接成双向链表。 37 | 38 | **代码如下** 39 | 40 | 41 | ``` java 42 | public static Node2 convert1(Node2 head) { 43 | Queue queue = new LinkedList<>(); 44 | //将节点按中序遍历放进队列里 45 | inOrderToQueue(head, queue); 46 | head = queue.poll(); 47 | Node2 pre = head; 48 | pre.left = null; 49 | Node2 cur = null; 50 | while (!queue.isEmpty()) { 51 | cur = queue.poll(); 52 | pre.right = cur; 53 | cur.left = pre; 54 | pre = cur; 55 | } 56 | pre.right = null; 57 | return head; 58 | } 59 | 60 | private static void inOrderToQueue(Node2 head, Queue queue) { 61 | if (head == null) { 62 | return; 63 | } 64 | inOrderToQueue(head.left, queue); 65 | queue.offer(head); 66 | inOrderToQueue(head.right, queue); 67 | } 68 | ``` 69 | 70 | 这种方法的时间复杂度为 O(n), 空间复杂度也为 O(n)。 71 | 72 | **方法2:通过递归的方式** 73 | 74 | 在之前打卡的9道题中,几乎超过一般都用到了递归,如果这些题目使用的递归大家都理解了,并且能够自己独立写出代码了,那么我相信大家对递归的思想、使用已经有一定的熟练性。 75 | 76 | 我们假设函数conver的功能就是把二叉树变成双向链表,例如对于这种一棵二叉树: 77 | 78 | 79 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec8732b7f51e?w=464&h=250&f=png&s=35540) 80 | 81 | 经过conver转换后变成这样: 82 | 83 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec88f58a042e?w=548&h=149&f=png&s=34178) 84 | 85 | 注意,转换之后,把最右边节点的right指针指向了最左边的节点的。 86 | 87 | 对于下面这样一颗二叉树: 88 | 89 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec8b754db44d?w=841&h=331&f=png&s=87389) 90 | 91 | 采用conver函数分别对左右子树做处理,结果如下: 92 | 93 | 94 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec8d9fc7f2ca?w=1032&h=160&f=png&s=73652) 95 | 96 | 之后,再把他们连接起来 97 | 98 | 99 | ![](https://user-gold-cdn.xitu.io/2019/2/24/1691ec8f369c64c4?w=1080&h=160&f=png&s=74649) 100 | 101 | 了解了基本原理之后,直接看代码吧。 102 | 103 | ```java 104 | public static Node2 conver(Node2 head) { 105 | if (head == null) { 106 | return head; 107 | } 108 | Node2 leftE = conver(head.left); 109 | Node2 rightE = conver(head.right); 110 | Node2 leftB = leftE != null ? leftE.right : null; 111 | Node2 rightB = rightE != null ? rightE.right : null; 112 | if (leftE != null && rightE != null) { 113 | leftE.right = head; 114 | head.left = leftE; 115 | head.right = rightB; 116 | rightB.left = head; 117 | rightE.right = leftB; 118 | return rightE; 119 | } else if (leftE != null) { 120 | leftE.right = head; 121 | head.left = leftE; 122 | head.right = leftB; 123 | return head; 124 | } else if (rightE != null) { 125 | head.right = rightB; 126 | rightB.left = head; 127 | rightE.right = head; 128 | return rightE; 129 | } else { 130 | head.right = head; 131 | return head; 132 | } 133 | } 134 | 135 | ``` 136 | 时间复杂度为O(n),空间复杂度为O(h),其中h是二叉树的高度。 137 | 138 | 原理虽然不难,但写起代码,还是有挺多细节需要注意的,所以一直强调,有时间的话,一定要自己手打一遍代码,有时你以为自己懂了,可能在写代码的时候,发现自己并没有懂,一写就出现很多bug。 139 | 140 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 141 | 142 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /学计算机网络/什么是TCP流量控制.md: -------------------------------------------------------------------------------- 1 | #### 一、为什么需要流量控制? 2 | 3 | 4 | 5 | 双方在通信的时候,发送方的速率与接收方的速率是不一定相等,如果发送方的发送速率太快,会导致接收方处理不过来,这时候接收方只能把处理不过来的数据存在**缓存区**里(失序的数据包也会被存放在缓存区里)。 6 | 7 | 如果缓存区满了发送方还在疯狂着发送数据,接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源,因此,我们需要控制发送方的发送速率,让接收方与发送方处于一种**动态平衡**才好。 8 | 9 | 对发送方发送速率的控制,我们称之为**流量控制**。 10 | ![](https://img-blog.csdnimg.cn/20191215181446264.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 11 | 12 | #### 二、如何控制? 13 | 14 | 接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为**接收窗口**大小,用变量 **win** 来表示接收窗口的大小。 15 | 16 | 发送方收到之后,便会调整自己的发送速率,也就是调整自己**发送窗口**的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。 17 | 18 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215181603583.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 19 | 20 | #### 三、发送方何时再继续发送数据? 21 | 22 | 23 | 24 | 当发送方停止发送数据后,**该怎样才能知道自己可以继续发送数据?** 25 | 26 | 我们可以采用这样的策略:当接收方处理好数据,接受窗口 win > 0 时,接收方发个**通知报文**去通知发送方,告诉他可以继续发送数据了。当发送方收到窗口大于0的报文时,就继续发送数据。 27 | 28 | 不过这时候可能会遇到一个问题,假如接收方发送的通知报文,由于某种网络原因,这个报文丢失了,这时候就会引发一个问题:接收方发了通知报文后,继续等待发送方发送数据,而发送方则在等待接收方的通知报文,此时双方会陷入一种僵局。 29 | 30 | 为了解决这种问题,我们采用了另外一种策略:当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个**定时器**,每隔一段时间就发个**测试报文**去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。 31 | 32 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215181929551.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 33 | #### 四、一些术语及其注意点说明 34 | 35 | 36 | 37 | 1、这里说明下,由于TCP/IP支持全双工传输,因此通信的双方都拥有两个滑动窗口,一个用于接受数据,称之为**接收窗口**;一个用于发送数据,称之为**拥塞窗口**(即发送窗口)。指出接受窗口大小的通知我们称之为**窗口通告**。 38 | 39 | **2、接收窗口的大小固定吗?** 40 | 41 | 在早期的TCP协议中,接受接受窗口的大小确实是固定的,不过随着网络的快速发展,固定大小的窗口太不灵活了,成为TCP性能瓶颈之一,也就是说,在现在的TCP协议中,接受窗口的大小是根据某种算法动态调整的。 42 | 43 | **3、接受窗口越大越好吗?** 44 | 45 | 接受窗口如果太小的话,显然这是不行的,这会严重浪费链路利用率,增加丢包率。那是否越大越好呢?答否,当接收窗口达到某个值的时候,再增大的话也不怎么会减少丢包率的了,而且还会更加消耗内存。所以接收窗口的大小必须根据网络环境以及发送发的的拥塞窗口来动态调整。 46 | 47 | **4、发送窗口和接受窗口相等吗?** 48 | 49 | 接收方在发送确认报文的时候,会告诉发送发自己的接收窗口大小,而发送方的发送窗口会据此来设置自己的发送窗口,但这并不意味着他们就会相等。首先接收方把确认报文发出去的那一刻,就已经在一边处理堆在自己缓存区的数据了,所以一般情况下接收窗口 >= 发送窗口。 50 | 51 | 52 | 我这篇文章算是可以让你知道流量控制的大致原理,如果你想知道更多细节,可以参考TCP/IP详解这本书,挺不错。 53 | 54 | 55 | 56 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 57 | 58 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /学计算机网络/什么是广播路由算法?如何解决广播风暴?.md: -------------------------------------------------------------------------------- 1 | 对于广播,我相信在现实生活中我们时常都能接触到,例如学校一言不合就响起了校歌,搞的全校的人都能够听到,想假装没听到都不行。 2 | 3 | 假如我们把学校比作一个局域网的话,某台主机发起了一个广播,意味着局域网内的其他所有主机都会收到这个广播,那发起广播的主机是如何选择路径来给其他主机发送**广播分组**的呢?考虑下面由几个节点组成的网络: 4 | 5 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f760f2f064a553?w=392&h=366&f=png&s=14145) 6 | 7 | 假如节点 R1 要做一个广播给 R2, R3, R4发广播分组,显然,一种很简单的方法就是R1给 R2, R3, R4三个节点分别发一次广播分组,这意味着R1一共要发送三次同样的广播分组。 8 | 9 | 10 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f760f7aac02410?w=455&h=364&f=png&s=16137) 11 | 12 | **大家想一个问题:这种发送方式你觉得合理吗?** 13 | 14 | 是的,这种发送方式在实现上很简单,源节点(R1)每次带上目的节点的地址,然后发送给它就行了。 15 | 16 | 不过这种方式在效率上是极低的,例如,R1发送的这三个广播分组都会经过同一段链路(R1-R2这段链路),而且R2要是再连接上n个节点的话,代表着这R1需要再发送n次广播分组,这n个报文也会经过同一段链路。 17 | 18 | #### 解决方法 19 | 20 | 为了解决这个问题,我们或许可以这样做:就是R1把广播分组发给他的邻居节点R2,然后R1就不管了,R2再把报文发送给他的所有邻居节点R3, R4(除了从其接收该分组的那个邻居R1)。 21 | 22 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76100fef672bd?w=479&h=368&f=png&s=16171) 23 | 24 | 显然这种方式也是挺不错的,R1只发送了一次广播分组,而且R1-R2这段链路也不会出现同一个广播分组重复经过的情况。嗯,这很nice。 25 | 26 | #### 广播风暴 27 | 28 | 不过,这种给所有邻居节点发送广播分组的方式够优雅吗? 29 | 30 | 看下面的一个网络组成: 31 | 32 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76107120ddb88?w=338&h=284&f=png&s=10286) 33 | 34 | 35 | 36 | 按照刚才的方法,R1会给R2发送广播分组,接着R2会给R3, R4发送广播分组。刚才我们说过,收到广播分组的节点会给他的所有邻居发送报文(除了从其接受到该报文的那个邻居)。 37 | 38 | 所以这个时候 R3会给R4发送广播分组文,而R4接收到R3的广播分组之后,R4会给R2发送广播分组,R2收到R4的广播分组之后 ,也会给R3再次发送广播分组….. 39 | 40 | 如果节点中形成了一个圈,那么就会像上面那样,节点之间不停着发送广播分组,这时网络上充斥着大量重复的广播分组,这将会严重影响资源的利用。 41 | 42 | 我们也把这种情况称之为**广播风暴**。 43 | 44 | #### 控制广播风暴 45 | 46 | 因此,我们必须想出某种策略,来控制这种广播风暴。 47 | 48 | 一种很简单的方法,就是给这一份广播分组做一个标记。例如,源节点(发起广播的节点)可以将其**地址**以及**广播序号**放入这个广播分组中,然后发送给他的所有邻居节点,每个节点会维护它已经收到的、转发的源地址和广播分组的序号列表。 49 | 50 | 当节点收到一个广播分组时,会检查这个广播分组是否之前接收过(可以通过源地址、报文序号来检查),如果接收过,那么就把该广播分组丢弃,否则,把该广播分组接收,且向所有邻居节点转发。 51 | 52 | 例如对于下面由7个节点组成是网络 53 | 54 | 55 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f7611e2368649f?w=511&h=383&f=png&s=21169) 56 | 57 | 如果 节点 A 要做一个广播,那么 A就会给他的邻居节点B,C发一份广播分组,B,C也会给他的邻居节点发送一个广播分组。意味着B会给 C,D发送广播分组,而 C也会给 B,E,F发送一份广播分组: 58 | 59 | 60 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76122cd7cfc8d?w=495&h=386&f=png&s=23666) 61 | 62 | 当B收到C发给他的报文时,B检测到已经有了该报文,所以B会丢弃C发送给他的广播分组,C也一样会丢弃B发送给他的广播分组。图中青色的箭头代表该广播分组会被丢弃。 63 | 64 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76128ab69e179?w=504&h=369&f=png&s=25319) 65 | 66 | 从图中不难看出,就算节点之间形成了圈,但也不会出现节点之间循环转发的情况。 67 | 68 | 虽然该方法简单 ,但确实有效着控制了广播风暴,当然,这只是控制广播风暴的方法之一,实际上还有其他方法,在此我就不说了。 69 | 70 | #### 生成树广播 71 | 72 | 虽然上面的那种方法有效着控制了广播风暴,但也是存在着很多的冗余广播分组(那些被丢弃的广播分组就是冗余的广播分组)。 73 | 74 | 75 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f7612faa67d643?w=504&h=369&f=png&s=25319) 76 | 77 | 如果可以,我想让每个节点仅接收一次广播分组,也不用 考虑丢弃广播分组,所以理想的情况应该是这样: 78 | 79 | 80 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f761332e37d1aa?w=492&h=377&f=png&s=23092) 81 | 82 | 有没有一种方法,可以让广播分组像上面这种情况来传送呢?请大家看下面一个图: 83 | 84 | 85 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76136dfa2f789?w=991&h=414&f=png&s=193512) 86 | 87 | 如果把节点当作一个图的顶点,大家观察下左边的图与右边的图有什么联系。 88 | 89 | 右边的图不就是左边图的**生成树**吗?(学了这么多年的生成树,终于给用到了),如果我们给每一段链路加上相应的费用的话,那么我们最理想的情况就是找到一颗**最小生成树**。 90 | 91 | 所以,我们最理想的情况就是让广播报文在最小生成树的路径中传送,于是 ,我们现在的问题就是**找出这些节点组成的网络中的最小生成树**。 92 | 93 | 那么,如何构造一颗生成树呢?下面提供一种基于某个中心的方法来建立一颗生成树。注意,是生成树,不是最小生成树。 94 | 95 | 该方法是这样的:我们先选出一个中心节点,然后其他节点向这个中心节点发送**加入树报文**,加入树报文经过的路径,都会被嫁接到生成树上。我举个例子吧,好理解点。例如对于这个网络结构: 96 | 97 | 98 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f7615739bf5858?w=511&h=383&f=png&s=21169) 99 | 100 | 我们选择 E为中心点,然后其他节点给E发送加入树报文: 101 | 102 | 1、F节点给E发送加入树报文,此时E-F链路成为初始的生成树,如下图(红色路径表示生成树) 103 | 104 | 105 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f7615da6c3a0af?w=537&h=366&f=png&s=21963) 106 | 107 | 2、接着B给E发送加入树报文,假设B经过的路径是B->D->E。此时路径B-D-E也加入了生成树。 108 | 109 | 110 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f761643dc523a7?w=456&h=398&f=png&s=22310) 111 | 112 | 注:D不用在不用在发送加入树报文了,因此他此时已经在生成树里了。 113 | 114 | 3、接着C给E发送加入树报文,C-E加入生成树。 115 | 116 | 117 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76167f0b55e92?w=489&h=363&f=png&s=23146) 118 | 119 | 4、接着,A给E发送报文,假设A选择的路径是A->C->E。不过当A的报文到达C之后,由于原本C-E就在生成树里面了,所以A的报文不用经过C-E,A-C就加入到生成树了。 120 | 121 | 122 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f7616e79d47c3c?w=517&h=476&f=png&s=26127) 123 | 124 | 5、最后G通过D加入生成树。 125 | 126 | 127 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76171bc545f79?w=466&h=369&f=png&s=24753) 128 | 129 | 到此,生成树构建完毕,此时生成树如下: 130 | 131 | ![](https://user-gold-cdn.xitu.io/2020/1/5/16f76174c29182db?w=437&h=392&f=png&s=19429) 132 | 133 | 然后在广播的时候,就可以沿着这条路径来转发复制广播报文了。 134 | 135 | 文章讲到这里,也大致结束。如果文中有哪里讲错了,欢迎你指出一下。 136 | 137 | 138 | 139 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 140 | 141 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /学计算机网络/什么是拥塞控制?.md: -------------------------------------------------------------------------------- 1 | 大家可能都听说过**拥塞控制**和**流量控制**,想必也有一些人可能还分不清拥塞控制和流量控制,进而把他们当作一回事。拥塞控制和流量控制虽然采取的动作很相似,但拥塞控制与网络的拥堵情况相关联,而流量控制与接收方的缓存状态相关联。 2 | 3 | 4 | 也就是说,拥塞控制和流量控制是针对完全不同的问题而采取的措施。今天这篇文章,我们先来讲讲拥塞控制。 5 | 6 | #### 一、为何要进行拥塞控制? 7 | 8 | 为了方便,我们假设主机A给主机B传输数据。 9 | 10 | 11 | 我们知道,两台主机在传输数据包的时候,如果发送方迟迟没有收到接收方反馈的ACK,那么发送方就会认为它发送的数据包丢失了,进而会重新传输这个丢失的数据包。 12 | 13 | 14 | 15 | 然而实际情况有可能此时有太多主机正在使用信道资源,导致**网络拥塞**了,而A发送的数据包被堵在了半路,迟迟没有到达B。这个时候A误认为是发生了丢包情况,会重新传输这个数据包。 16 | 17 | 18 | 19 | 结果就是不仅浪费了信道资源,还会使网络更加拥塞。因此,我们需要进行**拥塞控制**。 20 | 21 | #### 二、如何知道网络的拥塞情况? 22 | 23 | 24 | 25 | A 与 B 建立连接之后,就可以向B发送数据了,然而这个时候 A 并不知道此时的网络拥塞情况如何,也就是说,A 不知道一次性连续发送多少个数据包好,我们也把 A 一次性连续发送多少个数据包称之为**拥塞窗口**,用 N 代表此时拥塞窗口的大小吧。 26 | 27 | 为了探测网络的拥塞情况,我们可以采取以下**两种策略**: 28 | 29 | 1、先发送一个数据包试探下,如果该数据包没有发生超时事件(也就是没有丢包)。那么下次发送时就发送2个,如果还是没有发生超时事件,下次就发送3个,以此类推,即N = 1, 2, 3, 4, 5..... 30 | 31 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182544831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 32 | 2、一个一个增加实在是太慢了,所以可以刚开始发送1个,如果没有发生超时时间,就发送2个,如果还是没有发送超时事件就发送4个,接着8个...,用翻倍的速度类推,即 N = 1, 2, 4, 8, 16... 33 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182609357.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 34 | 无论是第一种方法还是第二种方法,最后都会出现**瓶颈值**。不过这里值得注意的是,第一种情况的增长速率确实有点慢,但是第二种情况以指数增长,增长速度有点太快了,可能一下子就到瓶颈值了。 35 | 36 | 37 | 38 | 为了解决这个过慢或过快的问题,我们可以把第一种方法和第二种方法结合起来。也就是说,我们刚开始可以以指数的速度增长,增长到某一个值,我们把这个值称之为**阈值**吧,用变量 ssthresh 代替。当增长到阈值时,我们就不在以指数增长了,而是一个一个线性增长。 39 | 40 | 41 | 42 | 所以最终的策略是:**前期指数增长,到达阈值之后,就以一个一个线性的速度来增长**。 43 | 44 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182657659.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 45 | (注:8之后其实是直线的,那里只是弯曲了一下) 46 | 47 | 我们也把指数增长阶段称之为**慢启动**,线性增长阶段称之为**拥塞避免** 48 | 49 | #### 三、到了瓶颈值之后怎么办? 50 | 51 | 52 | 53 | 无论是指数增长还是一个一个增长,最终肯定会出现**超时事件**,总不可能无限增长吧。当出现超时事件时,我们就认为此时网络出现了拥塞了,不能再继续增长了。我们就把这个时候的N的值称之为**瓶颈值**吧,用MAX 这个字母来代替吧,即最大值。 54 | 55 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182826506.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 56 | *注:这里再次提醒阈值过后是一个一个线性增长,图中之所以弯曲是因为我画图原因导致的* 57 | 58 | **当达到最大值MAX之后,我们该怎么办呢?** 59 | 60 | 61 | 62 | 当到达最大值之后我们采取的策略是这样的: 63 | 64 | 65 | 66 | 我们就回到最初的最初的状态,也就是说从1,2,4,8.....开始,不过这个时候我们还会把ssthresh调小,调为MAX值的一半,即ssthresh = MAX / 2。 67 | 68 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182905190.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 69 | 图中阈值为8,瓶颈值是14;超时事件发生后,阈值为14 / 2 = 7。 70 | 71 | #### 四、超时事件就一定是网络拥塞? 72 | 73 | 74 | 75 | 超时事件发送就一定是网络出现了拥堵吗?其实也有可能不是出现了网络拥堵,有可能是因为某个数据包出现了丢失或者损害了,导致了这个数据包超时事件发生了 76 | 77 | 78 | 79 | 为了防止这种情况,我们是通过**冗余 ACK**来处理的。我们都知道,数据包是有序号的,如果A给B发送M1, M2, M3, M4, M5...N个数据包,如果B收到了M1, M2, M4....却始终没有收到M3,这个时候就会重复确认M2,意在告诉A,M3还没收到,可能是丢失 80 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215182951178.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 81 | 当A连续收到了三个确认M2的ACK,且M3超时事件还没发生。A就知道M3可能丢失了,这个时候A就不必等待M3设置的计时器到期了,而是快速重传M3。并且把ssthresh设置为MAX的一半,即ssthresh = MAX/2,但是这个时候并非把控制窗口N设置为1,而是让N = ssthresh,N在一个一个增长。 82 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20191215183020186.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 83 | 84 | 我们也把这种情况称之为**快速恢复**。而这种具有快速恢复的TCP版本称之为**TCP Reno**。 85 | 86 | 还有另外一种TCP版本,无论是收到三个相同的ACK还是发生超时事件,都把拥塞窗口的大小设为1,从最初状态开始,这种版本的TCP我们称之为**TCP Tahoe**。 87 | 88 | #### 最后 89 | 90 | 偷偷透露一下,由于第一次画这种图,这几个图画了差不多两个小时,也是醉了。 91 | 92 | 93 | 94 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 95 | 96 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /学计算机网络/关于三次握手与四次挥手面试官想考我们什么?.md: -------------------------------------------------------------------------------- 1 | 在面试中,三次握手和四次挥手可以说是问的最频繁的一个知识点了,我相信大家也都看过很多关于三次握手与四次挥手的文章,今天的这篇文章,重点是围绕着面试,我们应该掌握哪些比较重要的点,哪些是比较被面试官给问到的,我觉得如果你能把我下面列举的一些点都记住、理解,我想就差不多了。 2 | 3 | #### 三次握手 4 | 5 | 当面试官问你为什么需要有三次握手、三次握手的作用、讲讲三次三次握手的时候,我想很多人会这样回答: 6 | 7 | 首先很多人会先讲下握手的过程: 8 | 9 | 1、第一次握手:客户端给服务器发送一个 SYN 报文。 10 | 11 | 2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。 12 | 13 | 3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。 14 | 15 | 4、服务器收到 ACK 报文之后,三次握手建立完成。 16 | 17 | 作用是为了确认双方的接收与发送能力是否正常。 18 | 19 | > **这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以**: 20 | 第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 21 | 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。 22 | 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。 23 | 24 | 因此,需要三次握手才能确认双方的接收与发送能力是否正常。 25 | 26 | 27 | 这样回答其实也是可以的,但我觉得,这个过程的我们应该要描述的更详细一点,因为三次握手的过程中,双方是由很多状态的改变的,而这些状态,也是面试官可能会问的点。所以我觉得在回答三次握手的时候,我们应该要描述的详细一点,而且描述的详细一点意味着可以扯久一点。加分的描述我觉得应该是这样: 28 | 29 | 30 | 31 | **刚开始客户端处于 closed 的状态,服务端处于 listen 状态**。然后 32 | 33 | 1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 **ISN(c)**。此时客户端处于 **SYN_Send** 状态。 34 | 35 | 2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 **SYN_REVD** 的状态。 36 | 37 | 3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 **establised** 状态。 38 | 39 | 4、服务器收到 ACK 报文之后,也处于 **establised 状态**,此时,双方以建立起了链接。 40 | 41 | 42 | ![](https://user-gold-cdn.xitu.io/2019/4/10/16a074b331fb0d85?w=414&h=196&f=png&s=23307) 43 | 44 | **三次握手的作用** 45 | 46 | 三次握手的作用也是有好多的,多记住几个,保证不亏。例如: 47 | 48 | 1、确认双方的接受能力、发送能力是否正常。 49 | 50 | 2、指定自己的初始化序列号,为后面的可靠传送做准备。 51 | 52 | 单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如: 53 | 54 | **1、(ISN)是固定的吗** 55 | 56 | 三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。 57 | 58 | 如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。 59 | 60 | **2、什么是半连接队列** 61 | 62 | 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为**半连接队列**。当然还有一个**全连接队列**,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。 63 | 64 | > 这里在补充一点关于**SYN-ACK 重传次数**的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, .... 65 | 66 | **3、三次握手过程中可以携带数据吗** 67 | 68 | 很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。 69 | 70 | 为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。 71 | 72 | 而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。 73 | 74 | 关于三次握手的,https 的认证过程能知道一下最好,不过我就不说了,留着写 http 面试相关时的文章再说。 75 | 76 | #### 四次挥手 77 | 78 | 四次挥手也一样,千万不要对方一个 FIN 报文,我方一个 ACK 报文,再我方一个 FIN 报文,我方一个 ACK 报文。然后结束,最好是说的详细一点,例如想下面这样就差不多了,要把每个阶段的**状态**记好,我上次面试就被问了几个了,呵呵。我答错了,还以为自己答对了,当时还解释的头头是道,呵呵。 79 | 80 | 81 | 刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则: 82 | 83 | 1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于**FIN_WAIT1**状态。 84 | 85 | 2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 ***CLOSE_WAIT2**状态。 86 | 87 | 3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 **LAST_ACK** 的状态。 88 | 89 | 4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 **TIME_WAIT** 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态 90 | 91 | 5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 92 | 93 | 94 | ![](https://user-gold-cdn.xitu.io/2019/4/10/16a074b855ad3850?w=419&h=187&f=png&s=24976) 95 | 96 | 这里特别需要主要的就是**TIME_WAIT**这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。 97 | 98 | 至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。 99 | 100 | 101 | 这里我给出每个状态所包含的含义,有兴趣的可以看看。 102 | 103 | > LISTEN - 侦听来自远方TCP端口的连接请求; 104 | 105 | > SYN-SENT -在发送连接请求后等待匹配的连接请求; 106 | 107 | > SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认; 108 | 109 | > ESTABLISHED- 代表一个打开的连接,数据可以传送给用户; 110 | 111 | > FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认; 112 | 113 | > FIN-WAIT-2 - 从远程TCP等待连接中断请求; 114 | 115 | > CLOSE-WAIT - 等待从本地用户发来的连接中断请求; 116 | 117 | > CLOSING -等待远程TCP对连接中断的确认; 118 | 119 | > LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认; 120 | 121 | > TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认; 122 | 123 | > CLOSED - 没有任何连接状态; 124 | 125 | 最后,在放在三次握手与四次挥手的图 126 | ![](https://user-gold-cdn.xitu.io/2019/3/28/169c39cc18e7592a?w=1268&h=810&f=png&s=108105) 127 | 128 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 129 | 130 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /学计算机网络/数字签名是什么.md: -------------------------------------------------------------------------------- 1 | 2 | 今天,我读到一篇好文章。 3 | 4 | 它用图片通俗易懂地解释了,"数字签名"(digital signature)和"数字证书"(digital certificate)到底是什么。 5 | 我对这些问题的理解,一直是模模糊糊的,很多细节搞不清楚。读完这篇文章后,发现思路一下子就理清了。为了加深记忆,我把文字和图片都翻译出来了 6 | 7 | 1、 8 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a342f7edf507?w=550&h=311&f=png&s=30024) 9 | 鲍勃有两把钥匙,一把是公钥,另一把是私钥。 10 | 11 | 2、 12 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a363cfacd60a?w=600&h=277&f=png&s=44677) 13 | 鲍勃把公钥送给他的朋友们----帕蒂、道格、苏珊----每人一把。 14 | 15 | 3、 16 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3680a2c4263?w=600&h=247&f=png&s=33440) 17 | 苏珊要给鲍勃写一封保密的信。她写完后用鲍勃的公钥加密,就可以达到保密的效果。 18 | 19 | 4、 20 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a36df91f206e?w=600&h=276&f=png&s=35177) 21 | 鲍勃收信后,用私钥解密,就看到了信件内容。这里要强调的是,只要鲍勃的私钥不泄露,这封信就是安全的,即使落在别人手里,也无法解密。 22 | 23 | 5、 24 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a37188008812?w=550&h=291&f=png&s=43707) 25 | 鲍勃给苏珊回信,决定采用"数字签名"。他写完后先用Hash函数,生成信件的**摘要(digest)**。 26 | 27 | 6、 28 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3788c6ac279?w=550&h=245&f=png&s=13132) 29 | 然后,鲍勃使用私钥,对这个摘要加密,生成"数字签名"(signature)。 30 | 31 | 7、 32 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a37c07636063?w=550&h=304&f=png&s=44131) 33 | 鲍勃将这个签名,附在信件下面,一起发给苏珊。 34 | 35 | 8、 36 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a37f017f60d7?w=550&h=170&f=png&s=11350) 37 | 苏珊收信后,取下数字签名,用鲍勃的公钥解密,得到信件的摘要。由此证明,这封信确实是鲍勃发出的。 38 | 39 | 9、 40 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3828e96690b?w=550&h=296&f=png&s=46830) 41 | 苏珊再对信件本身使用Hash函数,将得到的结果,与上一步得到的摘要进行对比。如果两者一致,就证明这封信未被修改过。 42 | 43 | 10、 44 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a386ae32f387?w=550&h=272&f=png&s=31544) 45 | 复杂的情况出现了。道格想欺骗苏珊,他偷偷使用了苏珊的电脑,用自己的公钥换走了鲍勃的公钥。此时,苏珊实际拥有的是道格的公钥,但是还以为这是鲍勃的公钥。因此,道格就可以冒充鲍勃,用自己的私钥做成"数字签名",写信给苏珊,让苏珊用假的鲍勃公钥进行解密。 46 | 47 | 11、 48 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a389b205f033?w=650&h=427&f=png&s=38567) 49 | 后来,苏珊感觉不对劲,发现自己无法确定公钥是否真的属于鲍勃。她想到了一个办法,要求鲍勃去找"证书中心"(certificate authority,简称CA),为公钥做认证。证书中心用自己的私钥,对鲍勃的公钥和一些相关信息一起加密,生成"数字证书"(Digital Certificate)。 50 | 51 | 12、 52 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a38e8c260a4b?w=549&h=430&f=png&s=69792) 53 | 鲍勃拿到数字证书以后,就可以放心了。以后再给苏珊写信,只要在签名的同时,再附上数字证书就行了。 54 | 55 | 13、 56 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a392ff20aefd?w=550&h=356&f=png&s=30001) 57 | 苏珊收信后,用CA的公钥解开数字证书,就可以拿到鲍勃真实的公钥了,然后就能证明"数字签名"是否真的是鲍勃签的。 58 | 59 | 14、 60 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a39898815b9e?w=550&h=399&f=png&s=110184) 61 | 下面,我们看一个应用"数字证书"的实例:https协议。这个协议主要用于网页加密。 62 | 63 | #### 案例 64 | 65 | 1、 66 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a39c6bb9cd1c?w=550&h=300&f=png&s=32912) 67 | 首先,客户端向服务器发出加密请求。 68 | 69 | 2、 70 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3a01ee6b51f?w=550&h=286&f=png&s=34749) 71 | 服务器用自己的私钥加密网页以后,连同本身的数字证书,一起发送给客户端。 72 | 73 | 3、 74 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3a3de3ce605?w=676&h=536&f=png&s=42600) 75 | 客户端(浏览器)的"证书管理器",有"受信任的根证书颁发机构"列表。客户端会根据这张列表,查看解开数字证书的公钥是否在列表之内。 76 | 77 | 4、 78 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3aa86188396?w=550&h=312&f=png&s=32763) 79 | 如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。 80 | 81 | 5、 82 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3aeae6acc02?w=510&h=349&f=png&s=116824) 83 | 如果这张数字证书不是由受信任的机构颁发的,浏览器会发出另一种警告。 84 | 85 | 6、 86 | ![](https://user-gold-cdn.xitu.io/2019/8/4/16c5a3b40d8668a4?w=550&h=312&f=png&s=32798) 87 | 如果数字证书是可靠的,客户端就可以使用证书中的服务器公钥,对信息进行加密,然后与服务器交换加密信息。 88 | 89 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 90 | 91 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /学计算机网络/电脑的ip是怎么来的?我又没有配置过.md: -------------------------------------------------------------------------------- 1 | 对于我们平时上网的电脑的 ip 是如何来的呢?一种简单的方法就是我们自己来配置了 2 | 3 | ![](https://user-gold-cdn.xitu.io/2019/5/24/16ae9b9a7cf26a76?w=566&h=709&f=png&s=100672) 4 | 5 | 显然,这里有两种配置方式,一种是自动获取 ip 地址,一种是我们手动来设置,我相信大部分人都是通过**自动获取**的方式来得到 ip 的,那么问题来了,它是如何自动获得到的呢? 6 | 7 | #### 客户端请求 ip 8 | 9 | 可能很多人都知道,是通过 DHCP 服务器来获取 ip 的,那么问题来了,你要给 DHCP 服务器发报文来获取 ip,那么你知道 DHCP 服务器的 ip 是多少吗?自己客户端的源 ip 又是多少呢?现在啥也不知道,该如何发送报文呢? 10 | 11 | 为了解决这个问题,客户端会发送一个广播,我们知道,广播报文是会发送局域网内的所有其他主机的,广播的目的 **ip 是 255.255.255.255,目的端口是 68**,为了让别人知道它是来请求一个 ip 的,我们的客户端会把 **0.0.0.0 作为自己的源 ip,源端口是 67**。意在告诉别人:我现在啥也没有,急需一个 ip,哪位老铁能给我提供一个 ip。 12 | 13 | 14 | 15 | ![](https://user-gold-cdn.xitu.io/2019/5/24/16aea06673527065?w=837&h=114&f=png&s=92062) 16 | 17 | 我们把这个请求 ip 的报文称之为 **discover 报文**。 18 | 19 | > 这里提醒一些,这里发送的报文都是采用 UDP 报文,而不是 TCP 报文哈,下同。 20 | 21 | #### DHCP响应 22 | 23 | 24 | 当 DHCP 服务器收到这个报文之后,一看源地址是 0.0.0.0,就知道生意来了,知道这是一个请求 ip 的报文,DHCP 服务器就会给它提供一个 ip,包括 **ip 地址,子码掩码,网关,ip 的有效期等信息**。 25 | 26 | 有人可能会问,只有源 ip 为 0.0.0.0 的信息,我们怎么把报文发送到它的手里呢?这不,我们每台电脑不都有 Mac 地址吗?在 discover 报文中,就会包含它的 MAC 地址了,DHCP 服务器,只需要发一个广播报文就可以了,广播报文的源ip是 DHCP 服务器自己的 ip,源端口是 67,目的地址是 255.255.255.255,目的端口是 68 27 | 28 | ![](https://user-gold-cdn.xitu.io/2019/5/24/16aea107a7d8af18?w=837&h=120&f=png&s=102590) 29 | 30 | 31 | 我们把 DHCP 提供 ip 地址的报文称之为**offer报文**。 32 | 33 | #### 客户端挑选 ip 地址 34 | 35 | 我们知道,有可能不知一台 DHCP 服务器收到了 discover 请求报文,也就是说,我们的主机可能会收到多个 offer 报文,所以呢,我们的主机会选择其中一个心仪的 offer 报文来作为自己的 ip,一般是选择最先收到的 offer 报文,选择好之后,会给对应的 DHCP 服务器次发送一个 **request 报文**,意在告诉它,我看中了你的报文。 36 | 37 | 38 | DHCP 收到 request 报文之后,会给它回复一个 ACK 报文,并且把这个分配出去的 ip 进行登记(例如把这个 ip 标记为已使用状态)。 39 | 40 | 当我们的主机收到 ACK 报文之后,就可以开始冲浪在网上冲浪了。 41 | 42 | #### 几点说明 43 | 44 | 这里可能有人会说,如果 DHCP 服务器没有在我们所在的局域网里怎么办?这个时候,这个 discover 报文 就会通过我们的网关来进行传递,并且会把源 ip 替换成网络的 ip,源端口是 68,这里涉及到 NAT 地址到转换,不懂的可以看我之前的一篇文章。 45 | 46 | [谈谈NAT:什么?全球IP和私有IP是什么鬼?](https://mp.weixin.qq.com/s/H7Qx9W7W_CHZanGfsrWh9Q) 47 | 48 | DHCP 服务器收到报文之后,就可以根据源端口 68 来判断这是一个 discover 请求报文了。就会把 offer 发给网关,网关再发给我们的主机。 49 | 50 | 51 | ![](https://user-gold-cdn.xitu.io/2019/5/24/16aea27b698434e8?w=521&h=474&f=png&s=45580) 52 | 53 | #### 租期 54 | 55 | 56 | 在DHCP客户端的租约时间到达 1/2 时,客户端会向为它分配 IP 地址的DHCP服务器发送 request 单播报文,以进行 IP 租约的更新。如果服务器判断客户端可以继续使用这个 IP 地址,就回复 ACK 报文,通知客户端更新租约成功。如果此IP地址不能再分配给客户端,则回复 NAK 报文,通知客户端续约失败。 57 | 58 | 如果客户端在租约到达 1/2 时续约失败,客户端会在租约到 7/8 时间时,广播发送 request 报文进行续约。DHCP服务器处理同首次分配 IP 地址的流程。 59 | 60 | #### 最后 61 | 62 | 这个过程中,涉及到听多种报文,为了篇幅不要太长,我有些报文没有详细说,这里为了方便大家查看,我把所有报文都总结了一下 63 | 64 | | 报文类型 | 描述 | 65 | | --- | --- | 66 | | DHCP Discove | DHCP客户端请求地址时,并不知道DHCP服务器的位置,因此DHCP客户端会在本地网络内以广播方式发送请求报文,这个报文成为Discover报文,目的是发现网络中的DHCP服务器,所有收到Discover报文的DHCP服务器都会发送回应报文,DHCP客户端据此可以知道网络中存在的DHCP服务器的位置。 | 67 | | DHCP Offer | DHCP服务器收到Discover报文后,就会在所配置的地址池中查找一个合适的IP地址,加上相应的租约期限和其他配置信息(如网关、DNS服务器等),构造一个Offer报文,发送给用户 | 68 | | DHCP Request | DHCP客户端可能会收到很多Offer,所以必须在这些回应中选择一个。Client通常选择第一个回应Offer报文的服务器作为自己的目标服务器,并回应一个广播Request报文,通告选择的服务器 | 69 | | DHCP ACK | DHCP服务器收到Request报文后,根据Request报文中携带的用户MAC来查找有没有相应的租约记录,如果有则发送ACK报文作为回应,通知用户可以使用分配的IP地址 | 70 | | DHCP NAK | 如果DHCP服务器收到Request报文后,没有发现有相应的租约记录或者由于某些原因无法正常分配IP地址,则发送NAK报文作为回应,通知用户无法分配合适的IP地址。 | 71 | | DHCP Release | 当用户不再需要使用分配IP地址时,就会主动向DHCP服务器发送Release报文,告知服务器用户不再需要分配IP地址,DHCP服务器会释放被绑定的租约。 | 72 | | DHCP Decline | DHCP客户端收到DHCP服务器回应的ACK报文后,通过地址冲突检测发现服务器分配的地址冲突或者由于其他原因导致不能使用,则发送Decline报文,通知服务器所分配的IP地址不可用。 | 73 | | DHCP Inform | DHCP客户端如果需要从DHCP服务器端获取更为详细的配置信息,则发送Inform报文向服务器进行请求,服务器收到该报文后,将根据租约进行查找,找到相应的配置信息后,发送ACK报文回应DHCP客户端 | 74 | 75 | 如果大家对计算机网络这块感兴趣的话,后续会算法和计算机网络穿插讲勒。如果有哪里说错了,欢迎指点出来! 76 | 77 | 78 | 79 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 80 | 81 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /学计算机网络/电路交换与分组交换的区别.md: -------------------------------------------------------------------------------- 1 | #### 分组 2 | 3 | 首先我们来了解下分组的概念。所谓分组,就是将一个数据包分成一个个更小的数据包。例如对于一个10GB的数据包,总不可以一次性发送过去吧,而是把它分成若干个小的数据包发送过去。每个分组数据块的结构图: 4 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200208203544592.png) 5 | 文件头一般是一些说明性数据,例如源地址和目标地址,数据类型等。数据部分就是真正要传达给对象的内容 6 | 7 | #### 电路交换 8 | 9 | 所谓**交换**,指的就是服务器与服务器之间的数据交换。数据传输交换的方式有几种,而电路交换便是其中的一种。 10 | 11 | 假如A和B之间要进行通信,我们就假设A要和E打个电话吧。当A输入E的电话号码,开始拨号之后,那么服务器要做的第一件事就是根据E的电话号码找到E在哪里,由于A通往E的路径有多条,会根据某种算法找到E之后,建立一条**虚拟通路**,然后进行数据的传输。 12 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020020820372751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 13 | 我们假设选的路径是A→D-→E 14 | 15 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200208203757384.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 16 | 找到一条通往E的路径并建立会话的过程中,我们称之为**电路交换**的第一阶段—-**建立连接**。之后A和E在通话的过程中会始终霸占着这条路径,数据传输的过程称为电路交换的第二阶段—-**数据传输**。 17 | 18 | 电路交换的第三阶段,也就是最后一个阶段—-**释放连接**。A和B只要有一方挂了电话,那便了开始释放连接。 19 | 20 | 传输例题图: 21 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200208203856560.png) 22 | 在这个过程中,新建连接需要花销一定的额外时间(想象你打电话的时候是不是出现正在拨号的字眼),释放连接也会花销一些额外的时间。 23 | 24 | 那么,电话交换的过程中,数据需要分组来传送吗? 25 | 26 | 答是不用的,因为电话交换的过程中,A和B两个人始终霸占着一条通信电路,他们每说一句话,都会实时被对方获取,因此数据是不用分组的。 27 | 28 | 从这也可以看出,电路交换的方式,在数据的传输上是比较高效、实时的,只要A一发出数据,E立马就能收到了,这也是为什么我们的电话通信使用的是电路交换的方式。 29 | 30 | 但由于一直霸占着这条路径,假如霸占的过程中A与E都在沉默不说话,那么将是对这条路径的极大浪费。因此,电路连接的方式资源的利用率是比较低的。 31 | 32 | 而且,如果你通话的时间超级短,可能花在新建连接的时间比通话的时间还要长,这就更加难受了。 33 | 34 | **稍微总结一下** 35 | 36 | 电路连接的三个阶段: 37 | 38 | 1、建立连接。 39 | 40 | 2、数据传输。 41 | 42 | 3、释放连接。 43 | 44 | 优点: 45 | 46 | 1、传输速度快、高效。 47 | 48 | 2、实时。 49 | 50 | 缺点: 51 | 52 | 1、资源利用率低。 53 | 54 | 2、新建连接需要占据一定的时间,甚至比通话的时间还长。 55 | 56 | #### 分组交换 57 | 58 | 从名字分组字眼,我们就可以知道,这种方式数据包是分组成更小的数据包进行传输的。分组交换的数据传输过程和电路交换不一样,分组交换采取**存储转发**传输的机制。我们下面还是以A给E传输数据作为例子来讲解。 59 | 60 | 假如A要给E发送一个数据包P,但这个数据包有点大,需要分成三组,例如分成p1,p2,p3三个更小的数据包。 61 | 62 | 这时A给E传输数据不需要新建连接这个过程,即不需要寻找一个通往E的路径。而且A直接把小的数据包丢给附近的路由器,然后A就不管了,例如A把p1丢给了B,这个时候A就不在去管p1的,当B收到p1这个完整的小数据包之后,B再丢给E。 63 | 64 | 但是A不一定都会把剩下的数据包都丢给B,有可能会把其他的数据包p2丢给C,之后再把p3丢给D,然后C和D在转发丢给E。这些都是不确定的,会根据某种算法的选择路由器。 65 | 66 | 这里有一个关键词**存储**,就是说,B必须收到**完整**的p1数据包后才能进行转发,这也不难理解,因为p1数据包包含E的地址,如果不是完整的数据包,B也不知道该发给谁啊。 67 | 68 | 示例图: 69 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200208204209822.png) 70 | 71 | 从电路交换的机制我们可以看出如下的一些问题: 72 | 73 | 由于A把数据包丢给B之后就不管了,B什么时候会把p1转发出去,谁也不知道,而且可能B会绕几个圈子再发给E也是有可能的。因此,电路交换的机制在数据传输方面不具有**实时性**。 74 | 75 | 而且,很有可能会有很多路由器把数据包丢给B,这个时候就会造成**通信阻塞**,这时可能p1只能排队等待B来发送。 76 | 77 | 由于B路由器的容量是有限的,如果有太多的数据包丢给它,它可能会容纳不下,这时候就可能会出现**丢包**的情况。 78 | 79 | 再者,由于p1,p2,p3数据包都有文件头,里面都包含了A和E的一些信息,当然还有其他的信息。可以说这些文件头有很多重复的数据,因此分组交换发送的数据具有很多的**重复无用数据**。 80 | 81 | 当然,分组交换还包括**时延**的缺点,因为B必须收到一个完整的p1才能把p1转发出去,因为这个接受存储的过程中存在时延,这种时延也成为传输时延,当然还存在传播时延和处理时延等。所谓处理时延就是每次都得检查这个数据包的文件头和决定将该数据包传输给谁。 82 | 83 | 说了电路交换的这么多缺点,那总得有优点吧? 84 | 85 | 实际上,上面的那些缺点,其实都不是什么大问题的。电路交换最主要的优点就是**设计简单,资源利用率高了**。 86 | 87 | **总结下分组交换** 88 | 89 | 分组交换采用把一个个小的数据包存储转发传输的机制。 90 | 91 | 主要的一些缺点: 92 | 93 | 1、不具有实时性。 94 | 95 | 2、存在延时。 96 | 97 | 3、会造成通信阻塞。 98 | 99 | 4、存在无用的重复数据。 100 | 101 | 5、会出现丢包的情况。 102 | 103 | 致命的优点: 104 | 105 | 1、设计简单。 106 | 107 | 2、资源利用率很高。 108 | 109 | #### 生活中的通信选择 110 | 111 | 两种交换传输的特点决定了我们平时的电话通信使用的是电路交换,像互联网中的微信等这种不要求实时的通信用分组交换。 112 | 113 | 这也就是为什么急事的时候会打电话,因为比较实时嘛。像微信这些,有时你发个信息,可能网络不好的话,或者太多人在同时使用的话,可能你的信息要过一阵子对方才能收到。 114 | 115 | 这里可能有些人会说,分组交换为何要把数据包分成一小个来存储转发呢?一个大的数据包发过去不好吗? 116 | 117 | 假如你的一个数据包100GB,那B这个路由器就得能存100GB的容量,可是发100GB的概率是极少数的,那把路由器设计成100GB不是很浪费?这也是为什么要分组成小数据包的原因之一。 118 | 119 | 当然,还有一种**报文交换**的方式,就是一整个数据包存储转发的,不过这种方式使用的比较少,再此就不详细展开了。 120 | 121 | 来一张三种交换传输的图: 122 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200208204548767.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 123 | 124 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 125 | 126 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /看过的优质书籍推荐/算法与计算机基础,有哪些值得阅读的书籍?.md: -------------------------------------------------------------------------------- 1 | 一直关注我的读者估计都清楚,我写的文章,主要以**算法 + 数据结构 + 计算机底层基础**为主,其他为辅,因为我觉得,理解掌握这些基础知识是非常重要的,可能你在平时的工作中并没有具体用到,不过它却是**处处不在,处处在**,特别是对于还在读大学的你,那就更加要把这些学好了。 2 | 3 | 阅读我文章的读者中,可能有挺多是非科班的,对于非科班的,最大的短板就是有很多计算机基础的书籍没看过,特别是对于那些直接培训之后进入工作的;当然,不得不说,有很多科班的,也是有非常多没学的。 4 | 5 | 所以呢,今天我给大家介绍一些我认为还不错的书籍,主要是讲解算法 + 计算机基础的,并且这些是书籍,供大家年后充电,如果你的基础很不扎实,那么读完这些书籍,相信一定会有不错的收获。 6 | 7 | > 之前也介绍过一些书籍,不过那些书籍和今天介绍的还是有一点点不同滴:今天介绍的挺多书籍适合在闲时、业余时间看一看哦,例如蹲马桶时间,哈哈 8 | 9 | #### 计算机基础 10 | 11 | ##### 1、程序是怎样跑起来的 12 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123101256697.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 13 | **级别**:入门 14 | 15 | 如果你认真阅读这本书,我估计一两天可能你就读完的,这算是一本入门书籍,就算你是小白,认真看,也能读懂。如书名所说,这本书主要讲解了我们平时所使用的程序,是如何在电脑中运行起来的,例如信息是如何存储的?为什么要用补码来表示二进制呢?数据如何压缩呢?等等 16 | 17 | 具体有哪些内容,大家可以去搜索这本书,然后看看目录,我这里由于篇幅原因,就不贴出来了 18 | 19 | ##### 2、网络是怎样连接的 20 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123101946209.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 21 | 级别:入门 22 | 23 | 我们每时每刻都在使用网络,那么那些数据是如何在网络传递的呢?两台陌生的主机怎么就能够通过 ip 地址寻找到对方呢?为什么要有 ip 地址呢?等等 24 | 25 | 我觉得不管你是学前端,后端,还是什么岗位,都有必要了解下网络相关的知识,这本书将带你从零学习这些知识,你看这本书里面有一句话叫**蹲马桶就能看懂的网络基础知识**,所以呢,对于想入门的你,还是挺友好的。当然,还是那句话,自己去找目录看看。 26 | 27 | 28 | 29 | ##### 3、计算机是怎样跑起来的 30 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112156928.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 31 | 级别:入门 32 | 33 | 这本书也挺不错,可以说也是和上面两本相辅相成,例如在《程序是怎样跑起来的》这本书中,我们知道计算机中所有的数据都是用 0 和 1 来表示的,那么计算机是如何识别 0 和 1 的呢?又是怎么做**加减乘除**的呢? 34 | 35 | 这本书比起上面两本,更加底层,类似于本科教学中的《计算机组成原理》,当然,《计算机组成原理》这本是被当成教材来用的,比较难读懂,而这本,则容易懂点,属于小白入门级别。 36 | 37 | 这三本书可以说是一个系列的,不过说实话,这本书我没看过,,,,不过我看了下,评价还是非常不错滴,加上三本又是一个系列,所以推荐给大家。 38 | 39 | ##### 4、计算机网络:自顶向下 40 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112346251.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 41 | 42 | 这本书我介绍过挺多次了,估计有些人都听烂了,不过我觉得这本书真心不错,这里再次介绍下。那么这本书于《网络是怎样连接的》有什么区别?我认为这本,算是进阶吧,当然,是相比之下算是进阶,其实它也可以当做入门的来看。 43 | 44 | 对于想学习计算机网络的,推荐这本书。 45 | 46 | ##### 5、汇编语言(王爽著) 47 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112309117.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 48 | 49 | 级别:入门 50 | 51 | 我觉得,学点汇编语言还是非常非常有必要的,虽然你写程序的时候并没有用到汇编。 52 | 53 | 为什么呢? 54 | 55 | 我们平时使用的语言,例如 Java,C 算是高级语言,而计算机只能看到机器码,而汇编,是最接近机器码的语言了,通过学习汇编,可以让我们更加了解计算机是如何执行我们的代码的;当然,有时候对于不大理解的代码,例如 i++ 和 ++i 有什么区别,我们可以直接看看它翻译成的汇编代码,这样一目了然。 56 | 57 | 总之,学习汇编,能够让你变的更强。而王爽写的这本《汇编语言》,我觉得对于新手非常非常 nice,我最开始看学校的教材,真的是一脸懵逼,一开始就介绍一大堆概念,把我都搞晕了,直到入手了王爽写的这一本,才让我重拾信心。 58 | 59 | 不过,王爽的这本,只能算入门,学校的教材,感觉像是进阶,对于还没有学过汇编的你,推荐这本书勒,小白也能看懂,书也不厚,很快就能学完。 60 | 61 | #### 算法 62 | 63 | ##### 1、程序员的算法趣题 64 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112426577.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 65 | 级别:入门 66 | 67 | 我觉得这本书还不错吧,没有全部看过,看过前面几章,比较基础,不过后面的越来越难。书名居然包含**趣题**两个字,可见这本书主要是以算法题来驱动讲解的,不过,题是否很有趣,这个我倒看不怎么出来,哈哈,可能是因为比较无趣,,,,,不过那些题还是挺不错滴,挺多题可能我们都看过,只是它用了另外一种方式来描述,可能就显的比较有趣了点 68 | 69 | 70 | ##### 2、编程之美 71 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112502230.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 72 | 73 | 级别:进阶 74 | 75 | 这本书,我也介绍过好几次了。这本书真心不错,题有一定的难度,这本书更加重要的是,拓展你的思路,而不是像其他算法一样,一个专题一个专题来。很多人问我刷了多少道 leetcode,其实我刷的题很少,不过我认为以刷多少道来衡量是不对的,因为有一些人可能刷的很少,不过看了很多算法书,我就属于刷的比较少,书看的多一点的那种了。 76 | 77 | 总之,学习算法,这本书挺推荐,挺有意思滴,不过不适合很多算法还没学过的新手,如果你在这方面是新手,那么可以看《图解算法》这种,当然,你如果连数据结构都没学过,那么建议你先学习数据结构,推荐《数据结构与算法分析:C语言描述版》这本书。 78 | 79 | 80 | ##### 3、算法(第四版) 81 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200123112628410.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 82 | 83 | 级别:进阶 84 | 85 | 感觉这本书也可以当做入门,也可以当做进阶,这个我也不好说,这本书主要讲解了各类算法,例如十大排序算法,各种图算法,各种树算法,各种高级的数据结构,并且使用了大量的图来帮助你理解这些算法。 86 | 87 | 不过,这本书是默认你已经懂链表,队列的,书籍主要使用 Java 代码来演示,对于那些想要学习各种经典算法的,还是挺推荐的,看完这本书,你估计再说算法方面,要强不少。 88 | 89 | 90 | ### 总结 91 | 目前就介绍了这几本吧,我就不介绍的太多了,介绍的多了,反而让你不知道看哪一步好,不过今天介绍的这些,感觉每一本都可以看吧,也不存在重叠之类的,当然,小许的重叠肯定是有的。 92 | 93 | 明天就是除夕了,帅地现在这里祝大家新春快乐,同时大家也可以好好做个计划,然后好好休息,之后过完年,好好让自己充电一波。 94 | 95 | 这些书籍的电子版,大家可以在我的公众号**帅地玩编程**回复**春节**获取哦。(因为这篇文章春节写滴) 96 | 97 | 学习更多**算法** + **计算机基础知识**,欢迎关注我的微信公众号,每天准时推送技术干货 98 | 99 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200306223728524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzM3OTA3Nzk3,size_16,color_FFFFFF,t_70) 100 | 101 | 102 | 103 | --------------------------------------------------------------------------------