├── .gitattributes ├── docs ├── _config.yml ├── index.md ├── 22.md ├── 24.md ├── 16.md ├── 14.md ├── 20.md ├── 19.md └── 17.md ├── .gitignore ├── 22-《进阶》资源限制类问题.md ├── README.md ├── 24-《进阶》AC自动机和卡特兰数.md ├── 16-《进阶》Manacher(马拉车)算法.md ├── 27-附:字符串专题汇总.md ├── 20-《进阶》数组累加和问题.md ├── 25-附:链表专题汇总.md ├── 28-附:动态规划专题汇总.md ├── 01-复杂度、排序、二分、异或.md ├── 17-《进阶》Morris遍历.md ├── 14-《进阶》斐波那契数列相关的递归.md ├── 04-堆、结构体排序.md ├── 05-前缀树、桶排序、排序总结.md ├── 09-贪心算法解题思路.md ├── 19-《进阶》打表和矩阵处理相关问题.md ├── 15-《进阶》KMP算法与bfprt算法.md ├── 26-附:二叉树专题汇总.md ├── 03-归并排序、随机快排.md ├── 02-链表、栈、队列、递归、哈希表、顺序表.md ├── 18-《进阶》线段树(interval-tree).md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-language=Go 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | markdown: kramdown 3 | encoding: utf-8 with BOM 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 8 | .idea/ 9 | *.iml 10 | 11 | # Vscode files 12 | .vscode 13 | 14 | # BlueJ files 15 | *.ctxt 16 | 17 | # Mobile Tools for Java (J2ME) 18 | .mtj.tmp/ 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | .DS_store 32 | -------------------------------------------------------------------------------- /22-《进阶》资源限制类问题.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 如何解决资源限制类题目 4 | 5 | ## 1.1布隆过滤器用于集合的建立与查询,并可以节省大量空间(已讲) 6 | 7 | ## 1.2 一致性哈希解决数据服务器的负载管理问题(已讲) 8 | 9 | ## 1.3 利用并查集结构做岛问题的并行计算(已讲) 10 | 11 | ## 1.4 哈希函数可以把数据按照种类均匀分流 12 | 13 | ## 1.5 位图解决某一范围上数字的出现情况,并可以节省大量空间 14 | 15 | ## 1.6 利用分段统计思想、并进一步节省大量空间 16 | 17 | ## 1.7 利用堆、外排序来做多个处理单元的结果合并 18 | 19 | ### 题目1(位图和分段统计): 20 | 21 | 32位无符号整数的范围是0~4,294,967,295, 22 | 现在有一个正好包含40亿个无符号整数的文件, 23 | 所以在整个范围中必然存在没出现过的数。 24 | 25 | 1、可以使用最多1GB的内存,怎么找到所有未出现过的数? 26 | 27 | 【进阶】 28 | 29 | 2、内存限制为 10MB,但是只用找到一个没出现过的数即可 30 | 31 | 3、内存限制为 3K,但是只用找到一个没出现过的数即可 32 | 33 | 4、内存限制为几个变量,但是只用找到一个没出现过的数即可 34 | 35 | > 如果用HashMap来存所有的key,每个无符号整数是4个字节,40亿个数,大约160亿字节,10亿字节大约1G, 大约需要16个G内存才能存下 36 | 37 | - 第一问题解 利用位图 38 | 39 | 如果限制1GB,那么可以使用位图,0到2的32次方减1范围的无符号数,只需要2的32次方个bit来存记录。Hash表需要4个字节才能表示一个数出现过还是没出现过,Bit来代表一个数出现过还是没出现过,空间上缩小了32倍。原本使用Hash需要的16G空间,现在缩小32倍,大约500M可以拿下,对应上述第五点 40 | 41 | 42 | - 第二问和第三问题解 利用分段统计 43 | 44 | 当限制10M,或者3K,那么位图也失效了。位图解决该问题大约500M。 45 | 46 | 拿3K举例,3KB(字节)如果都做成整形数组的的话,3KB除以4等于750个容量; 47 | 48 | 接着我们找到离750最近的2的某次方,找到512。那么我们把我们的数组申请512长度,一定不会超过3KB; 49 | 50 | 根据给定的范围,我们的数据大概有2的32次方个数字;且2的32次方,一定能够被512均分;均分为8388608份;512个位置的0位置统计0到8388608范围上出现了几次,1位置统计8388609到8388608*2范围上的数出现了几次,依次类推;每统计一个数,让该数除以8388608得到属于512个位置的哪个位置,该位置统计的数加1; 51 | 52 | 经过统计,512个位置,至少有一个位置统计的个数,不够8388608个,找到该位置代表的数据范围。该范围大小再除以512,得到新的512个数统计的新范围,循环往复,那么一定能够找到一个数没出现过 53 | 54 | 55 | - 第四问题解 56 | 57 | 三个变量找没出现过的一个数,运用二分来找。第一次二分,如果2的32次方的数都有,那么左右两边都是2的31次方。由于只有40亿个数,那么左右两侧必定有不满2的31次方个。接着二分不满的那个 58 | 59 | 60 | 61 | 62 | ### 题目2(位图) 63 | 64 | 32位无符号整数的范围是0~4294967295,现在有40亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。 65 | 66 | 67 | 参考题目1,也采用位图的思想,使用位图中的两位来表示一个数有没有出现过,且出现了几次。当两位的状态变为11,那么维持住。最后统计10的状态 68 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## 目录概览 2 | 3 | - [x] [第一节 复杂度、排序、二分、异或](https://dairongpeng.github.io/algorithm-note/01) 4 | - [x] [第二节 链表、栈、队列、递归、哈希表、顺序表](https://dairongpeng.github.io/algorithm-note/02) 5 | - [x] [第三节 归并排序、随机快排介绍](https://dairongpeng.github.io/algorithm-note/03) 6 | - [x] [第四节 比较器与堆](https://dairongpeng.github.io/algorithm-note/04) 7 | - [x] [第五节 前缀树、桶排序以及排序总结](https://dairongpeng.github.io/algorithm-note/05) 8 | - [x] [第六节 链表相关面试题总结](https://dairongpeng.github.io/algorithm-note/06) 9 | - [x] [第七节 二叉树基本算法](https://dairongpeng.github.io/algorithm-note/07) 10 | - [x] [第八节 二叉树的递归思维建立](https://dairongpeng.github.io/algorithm-note/08) 11 | - [x] [第九节 认识贪心算法](https://dairongpeng.github.io/algorithm-note/09) 12 | - [x] [第十节 并查集、图相关算法介绍](https://dairongpeng.github.io/algorithm-note/10) 13 | - [x] [第十一节 暴力递归思维、动态规划思维建立](https://dairongpeng.github.io/algorithm-note/11) 14 | - [x] [第十二节 用简单暴力递归思维推导动态规划思维](https://dairongpeng.github.io/algorithm-note/12) 15 | - [x] [第十三节 单调栈和窗口及其更新结构](https://dairongpeng.github.io/algorithm-note/13) 16 | - [x] [第十四节 类似斐波那契数列的递归](https://dairongpeng.github.io/algorithm-note/14) 17 | - [x] [第十五节 认识KMP算法与bfprt算法](https://dairongpeng.github.io/algorithm-note/15) 18 | - [x] [第十六节 认识Manacher(马拉车)算法](https://dairongpeng.github.io/algorithm-note/16) 19 | - [x] [第十七节 认识Morris遍历](https://dairongpeng.github.io/algorithm-note/17) 20 | - [x] [第十八节 线段树](https://dairongpeng.github.io/algorithm-note/18) 21 | - [x] [第十九节 打表技巧和矩阵处理技巧](https://dairongpeng.github.io/algorithm-note/19) 22 | - [x] [第二十节 组累加和问题整理](https://dairongpeng.github.io/algorithm-note/20) 23 | - [x] [第二十一节 哈希函数有关的结构和岛问题](https://dairongpeng.github.io/algorithm-note/21) 24 | - [x] [第二十二节 解决资源限制类题目](https://dairongpeng.github.io/algorithm-note/22) 25 | - [x] [第二十三节 有序表原理及扩展](https://dairongpeng.github.io/algorithm-note/23) 26 | - [x] [第二十四节 AC自动机和卡特兰数](https://dairongpeng.github.io/algorithm-note/23) 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《algorithm-note》中文版 2 | 3 | ## 笔记简介 4 | 5 | * 对于常用数据结构及算法的系统性总结,java版本参考algorithm-note-java分支,master分支基于Golang实现。 6 | * 出发点是,算法内容杂且难,系统性整理当成工具书查阅,可以有效帮助复习 7 | * 如果本系列对您有用,求个star~ 8 | 9 | ## 笔记阅读传送门 10 | 11 | - 完整阅读:[进入](https://www.yuque.com/dairongpeng/no7xzv/zw88wn) 12 | 13 | ## 目录概览 14 | 15 | - [x] [第一节 复杂度、排序、二分、异或](https://www.yuque.com/dairongpeng/no7xzv/tkqyqh) 16 | - [x] [第二节 链表、栈、队列、递归、哈希表、顺序表](https://www.yuque.com/dairongpeng/no7xzv/wxk6gu) 17 | - [x] [第三节 归并排序、随机快排介绍](https://www.yuque.com/dairongpeng/no7xzv/wck819) 18 | - [x] [第四节 堆、结构体排序](https://www.yuque.com/dairongpeng/no7xzv/wck819) 19 | - [x] [第五节 前缀树、桶排序以及排序总结](https://www.yuque.com/dairongpeng/no7xzv/mkuhxb) 20 | - [x] [第六节 链表相关高频题总结](https://www.yuque.com/dairongpeng/no7xzv/zk422u) 21 | - [x] [第七节 二叉树基本算法](https://www.yuque.com/dairongpeng/no7xzv/os4mpm) 22 | - [x] [第八节 二叉树的递归解题思路](https://www.yuque.com/dairongpeng/no7xzv/bvkf4t) 23 | - [x] [第九节 贪心算法解题思路](https://www.yuque.com/dairongpeng/no7xzv/runxe4) 24 | - [x] [第十节 并查集、图相关算法介绍](https://www.yuque.com/dairongpeng/no7xzv/fssemq) 25 | - [x] [第十一节 暴力递归、动态规划](https://www.yuque.com/dairongpeng/no7xzv/sa6xlq) 26 | - [x] [第十二节 简单暴力递归推导动态规划思路](https://www.yuque.com/dairongpeng/no7xzv/pbvuat) 27 | - [x] [第十三节 单调栈和窗口结构](https://www.yuque.com/dairongpeng/no7xzv/xwqq1z) 28 | - [x] [第十四节 类似斐波那契数列的递归](https://www.yuque.com/dairongpeng/no7xzv/nw8vti) 29 | - [x] [第十五节 KMP算法与BfPrt算法总结](https://www.yuque.com/dairongpeng/no7xzv/pkwrz3) 30 | - [x] [第十六节 Manacher(马拉车)算法介绍](https://www.yuque.com/dairongpeng/no7xzv/icb5d0) 31 | - [x] [第十七节 认识Morris遍历](https://www.yuque.com/dairongpeng/no7xzv/amf408) 32 | - [x] [第十八节 线段树(interval-tree)](https://www.yuque.com/dairongpeng/no7xzv/oa8zft) 33 | - [x] [第十九节 打表技巧和矩阵处理法](https://www.yuque.com/dairongpeng/no7xzv/fspk7r) 34 | - [x] [第二十节 组累加和问题整理](https://www.yuque.com/dairongpeng/no7xzv/mz72mg) 35 | - [x] [第二十一节 哈希、位图、布隆过滤器及岛问题](https://www.yuque.com/dairongpeng/no7xzv/uhrorf) 36 | - [x] [第二十二节 资源限制类问题总结](https://www.yuque.com/dairongpeng/no7xzv/ks9lg4) 37 | - [x] [第二十三节 有序表介绍及其原理](https://www.yuque.com/dairongpeng/no7xzv/ks0v3y) 38 | - [x] [第二十四节 AC自动机](https://www.yuque.com/dairongpeng/no7xzv/ah28p1) 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/22.md: -------------------------------------------------------------------------------- 1 | - [1 如何解决资源限制类题目](#1) 2 | * [1.1布隆过滤器用于集合的建立与查询,并可以节省大量空间(已讲)](#11) 3 | * [1.2 一致性哈希解决数据服务器的负载管理问题(已讲)](#12) 4 | * [1.3 利用并查集结构做岛问题的并行计算(已讲)](#13) 5 | * [1.4 哈希函数可以把数据按照种类均匀分流](#14) 6 | * [1.5 位图解决某一范围上数字的出现情况,并可以节省大量空间](#15) 7 | * [1.6 利用分段统计思想、并进一步节省大量空间](#16) 8 | * [1.7 利用堆、外排序来做多个处理单元的结果合并](#17) 9 | + [1.7.1 题目1(位图和分段统计):](#171) 10 | + [1.7.2 题目2(位图)](#172) 11 | 12 |

1 如何解决资源限制类题目

13 | 14 |

1.1布隆过滤器用于集合的建立与查询,并可以节省大量空间(已讲)

15 | 16 |

1.2 一致性哈希解决数据服务器的负载管理问题(已讲)

17 | 18 |

1.3 利用并查集结构做岛问题的并行计算(已讲)

19 | 20 |

1.4 哈希函数可以把数据按照种类均匀分流

21 | 22 |

1.5 位图解决某一范围上数字的出现情况,并可以节省大量空间

23 | 24 |

1.6 利用分段统计思想、并进一步节省大量空间

25 | 26 |

1.7 利用堆、外排序来做多个处理单元的结果合并

27 | 28 |

1.7.1 题目1(位图和分段统计):

29 | 30 | 31 | 32位无符号整数的范围是0~4,294,967,295, 32 | 现在有一个正好包含40亿个无符号整数的文件, 33 | 所以在整个范围中必然存在没出现过的数。 34 | 35 | 1、可以使用最多1GB的内存,怎么找到所有未出现过的数? 36 | 37 | 【进阶】 38 | 39 | 2、内存限制为 10MB,但是只用找到一个没出现过的数即可 40 | 41 | 3、内存限制为 3K,但是只用找到一个没出现过的数即可 42 | 43 | 4、内存限制为几个变量,但是只用找到一个没出现过的数即可 44 | 45 | > 如果用HashMap来存所有的key,每个无符号整数是4个字节,40亿个数,大约160亿字节,10亿字节大约1G, 大约需要16个G内存才能存下 46 | 47 | - 第一问题解 利用位图 48 | 49 | 如果限制1GB,那么可以使用位图,0到2的32次方减1范围的无符号数,只需要2的32次方个bit来存记录。Hash表需要4个字节才能表示一个数出现过还是没出现过,Bit来代表一个数出现过还是没出现过,空间上缩小了32倍。原本使用Hash需要的16G空间,现在缩小32倍,大约500M可以拿下==对应第五点== 50 | 51 | 52 | - 第二问和第三问题解 利用分段统计 53 | 54 | 当限制10M,或者3K,那么位图也失效了。位图解决该问题大约500M。 55 | 56 | 拿3K举例,3KB(字节)如果都做成整形数组的的话,3KB除以4等于750个容量; 57 | 58 | 接着我们找到离750最近的2的某次方,找到512。那么我们把我们的数组申请512长度,一定不会超过3KB; 59 | 60 | 根据给定的范围,我们的数据大概有2的32次方个数字;且2的32次方,一定能够被512均分;均分为8388608份;512个位置的0位置统计0到8388608范围上出现了几次,1位置统计8388609到8388608*2范围上的数出现了几次,依次类推;每统计一个数,让该数除以8388608得到属于512个位置的哪个位置,该位置统计的数加1; 61 | 62 | 经过统计,512个位置,至少有一个位置统计的个数,不够8388608个,找到该位置代表的数据范围。该范围大小再除以512,得到新的512个数统计的新范围,循环往复,那么一定能够找到一个数没出现过 63 | 64 | 65 | - 第四问题解 66 | 67 | 三个变量找没出现过的一个数,运用二分来找。第一次二分,如果2的32次方的数都有,那么左右两边都是2的31次方。由于只有40亿个数,那么左右两侧必定有不满2的31次方个。接着二分不满的那个 68 | 69 | 70 | 71 |

1.7.2 题目2(位图)

72 | 73 | 32位无符号整数的范围是0~4294967295,现在有40亿个无符号整数,可以使用最多1GB的内存,找出所有出现了两次的数。 74 | 75 | 76 | 参考题目1,也采用位图的思想,使用位图中的两位来表示一个数有没有出现过,且出现了几次。当两位的状态变为11,那么维持住。最后统计10的状态 77 | -------------------------------------------------------------------------------- /24-《进阶》AC自动机和卡特兰数.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 AC自动机 4 | 5 | KMP算法解决的问题是,在一个大字符串中,求目标match串存在还是不存在,最早存在的地方在哪 6 | 7 | AC自动机要解决的问题是,在一个文章中,有一些候选字符串,求这个文章中命中了哪些候选串。 8 | 9 | ## 1.1 AC自动机的实现 10 | 11 | 为每一个候选串建立一个前缀树,每个树节点都有一个fail指针。头节点fail指针人为规定指向null,第一层节点的fail指针人为规定,指向头节点。建立好前缀树后,宽度优先遍历设置全部的fail指针 12 | 13 | > 比较绕,可以考虑看代码详细步骤来理解 14 | 15 | 宽度优先遍历设置fail的指针的过程,如果某个节点的指针指向null,孩子的fail指针指向当前的父亲;如果某个节点的fail指针指向不为空的节点A,A孩子的路径为B,那么A的fail指针有没有指向B的路径,如果有,A孩子的fail指针,指向父亲节点的fail指针指向的B;如果父亲没有指向B的路,再找fail直到为null后,孩子fail指针指向头结点 16 | 17 | 18 | 19 | ```Go 20 | package main 21 | 22 | import "fmt" 23 | 24 | // Node 前缀树的节点 25 | type Node struct { 26 | // 如果一个node,end为空,不是结尾 27 | // 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串 28 | End string 29 | // 只有在上面的end变量不为空的时候,endUse才有意义 30 | // 表示,这个字符串之前有没有加入过答案 31 | EndUse bool 32 | Fail *Node 33 | // 假设前缀树的节点上的值只是小写字母,有26个指向。经典前缀树 34 | Nexts []*Node 35 | } 36 | 37 | func InitACAutomationNode() *Node { 38 | root := &Node{ 39 | End: "", 40 | EndUse: false, 41 | Fail: new(Node), 42 | Nexts: make([]*Node, 26), 43 | } 44 | return root 45 | } 46 | 47 | // insert 先建前缀树,建好之后再build所有节点的fail指针 48 | func (root *Node) insert(s string) { 49 | str := []byte(s) 50 | cur := root 51 | index := 0 52 | for i := 0; i < len(str); i++ { 53 | index = int(str[i] - 'a') 54 | if cur.Nexts[index] == nil { 55 | next := InitACAutomationNode() 56 | cur.Nexts[index] = next 57 | } 58 | cur = cur.Nexts[index] 59 | } 60 | cur.End = s 61 | } 62 | 63 | // 建立所有节点的fail指针 64 | func (root *Node) build() { 65 | queue := make([]*Node, 0) 66 | queue = append(queue, root) 67 | var cur *Node 68 | var cfail *Node 69 | 70 | for len(queue) != 0 { 71 | // 当前节点弹出, 72 | // 当前节点的所有后代加入到队列里去, 73 | // 当前节点给它的子去设置fail指针 74 | // cur -> 父亲 75 | cur = queue[0] 76 | queue = queue[1:] 77 | 78 | for i := 0; i < 26; i++ { // 所有的路 79 | if cur != nil && cur.Nexts != nil && cur.Nexts[i] != nil { // 找到所有有效的路 80 | cur.Nexts[i].Fail = root 81 | cfail = cur.Fail 82 | 83 | for cfail != nil { 84 | if cfail.Nexts != nil && cfail.Nexts[i] != nil { 85 | cur.Nexts[i].Fail = cfail.Nexts[i] 86 | break 87 | } 88 | cfail = cfail.Fail 89 | } 90 | queue = append(queue, cur.Nexts[i]) 91 | } 92 | } 93 | } 94 | } 95 | 96 | 97 | // build好之后,可以查文章有哪些候选串 98 | func (root *Node) containWords(content string) []string { 99 | str := []byte(content) 100 | 101 | cur := root 102 | var follow *Node 103 | ans := make([]string, 0) 104 | 105 | for i := 0; i < len(str); i++ { 106 | index := int(str[i] - 'a') // 路 107 | // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径 108 | for cur.Nexts[index] == nil && cur != root { 109 | cur = cur.Fail 110 | } 111 | 112 | // 1) 现在来到的路径,是可以继续匹配的 113 | // 2) 现在来到的节点,就是前缀树的根节点 114 | if cur.Nexts[index] != nil { 115 | cur = cur.Nexts[index] 116 | } else { 117 | cur = root 118 | } 119 | follow = cur 120 | 121 | for follow != root { 122 | if follow.EndUse { 123 | break 124 | } 125 | 126 | // 不同的需求,在这一段之间修改 127 | if len(follow.End) != 0 { 128 | ans = append(ans, follow.End) 129 | follow.EndUse = true 130 | } 131 | // 不同的需求,在这一段之间修改 132 | follow = follow.Fail 133 | } 134 | } 135 | return ans 136 | } 137 | 138 | //he 139 | //abcdheks 140 | func main() { 141 | ac := InitACAutomationNode() 142 | ac.insert("ahe") 143 | ac.insert("he") 144 | ac.insert("abcdheks") 145 | // 设置fail指针 146 | ac.build() 147 | 148 | contains := ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv") 149 | for _, word := range contains { 150 | fmt.Println(word) 151 | } 152 | } 153 | ``` 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /docs/24.md: -------------------------------------------------------------------------------- 1 | - [1 AC自动机](#1) 2 | * [1.1 AC自动机的实现](#11) 3 | 4 |

1 AC自动机

5 | 6 | 7 | KMP算法解决的问题是,在一个大字符串中,求目标match串存在还是不存在,最早存在的地方在哪 8 | 9 | AC自动机要解决的问题是,在一个文章中,有一些候选字符串,求这个文章中命中了哪些候选串。 10 | 11 |

1.1 AC自动机的实现

12 | 13 | 为每一个候选串建立一个前缀树,每个树节点都有一个fail指针。头节点fail指针人为规定指向null,第一层节点的fail指针人为规定,指向头节点。建立好前缀树后,宽度优先遍历设置全部的fail指针 14 | 15 | > 比较绕,看不懂看代码 16 | 17 | 宽度优先遍历设置fali的指针的过程,如果某个节点的指针指向null,孩子的fail指针指向当前的父亲;如果某个节点的fail指针指向不为空的节点A,A孩子的路径为B,那么A的fali指针有没有指向B的路径,如果有,A孩子的fail指针,指向父亲节点的fail指针指向的B;如果父亲没有指向B的路,再找fail直到为null后,孩子fail指针指向头结点 18 | 19 | 20 | 21 | ```Java 22 | package class08; 23 | 24 | import java.util.ArrayList; 25 | import java.util.LinkedList; 26 | import java.util.List; 27 | import java.util.Queue; 28 | 29 | public class Code01_AC { 30 | 31 | // 前缀树的节点 32 | public static class Node { 33 | // 如果一个node,end为空,不是结尾 34 | // 如果end不为空,表示这个点是某个字符串的结尾,end的值就是这个字符串 35 | public String end; 36 | // 只有在上面的end变量不为空的时候,endUse才有意义 37 | // 表示,这个字符串之前有没有加入过答案 38 | public boolean endUse; 39 | public Node fail; 40 | public Node[] nexts; 41 | public Node() { 42 | endUse = false; 43 | end = null; 44 | fail = null; 45 | // 假设前缀树的节点上的值只是小写字母,有26个指向。经典前缀树 46 | nexts = new Node[26]; 47 | } 48 | } 49 | 50 | // AC自动机 51 | public static class ACAutomation { 52 | private Node root; 53 | 54 | // 建头结点 55 | public ACAutomation() { 56 | root = new Node(); 57 | } 58 | 59 | // 先建前缀树,建好之后再build所有节点的fail指针 60 | public void insert(String s) { 61 | char[] str = s.toCharArray(); 62 | Node cur = root; 63 | int index = 0; 64 | for (int i = 0; i < str.length; i++) { 65 | index = str[i] - 'a'; 66 | if (cur.nexts[index] == null) { 67 | Node next = new Node(); 68 | cur.nexts[index] = next; 69 | } 70 | cur = cur.nexts[index]; 71 | } 72 | cur.end = s; 73 | } 74 | 75 | // 建立所有节点的fail指针 76 | public void build() { 77 | Queue queue = new LinkedList<>(); 78 | queue.add(root); 79 | Node cur = null; 80 | Node cfail = null; 81 | while (!queue.isEmpty()) { 82 | // 当前节点弹出, 83 | // 当前节点的所有后代加入到队列里去, 84 | // 当前节点给它的子去设置fail指针 85 | // cur -> 父亲 86 | cur = queue.poll(); 87 | for (int i = 0; i < 26; i++) { // 所有的路 88 | if (cur.nexts[i] != null) { // 找到所有有效的路 89 | cur.nexts[i].fail = root; // 90 | cfail = cur.fail; 91 | while (cfail != null) { 92 | if (cfail.nexts[i] != null) { 93 | cur.nexts[i].fail = cfail.nexts[i]; 94 | break; 95 | } 96 | cfail = cfail.fail; 97 | } 98 | queue.add(cur.nexts[i]); 99 | } 100 | } 101 | } 102 | } 103 | 104 | // build好之后,可以查文章有哪些候选串 105 | public List containWords(String content) { 106 | char[] str = content.toCharArray(); 107 | Node cur = root; 108 | Node follow = null; 109 | int index = 0; 110 | List ans = new ArrayList<>(); 111 | for (int i = 0; i < str.length; i++) { 112 | index = str[i] - 'a'; // 路 113 | // 如果当前字符在这条路上没配出来,就随着fail方向走向下条路径 114 | while (cur.nexts[index] == null && cur != root) { 115 | cur = cur.fail; 116 | } 117 | // 1) 现在来到的路径,是可以继续匹配的 118 | // 2) 现在来到的节点,就是前缀树的根节点 119 | cur = cur.nexts[index] != null ? cur.nexts[index] : root; 120 | follow = cur; 121 | while (follow != root) { 122 | if(follow.endUse) { 123 | break; 124 | } 125 | // 不同的需求,在这一段之间修改 126 | if (follow.end != null) { 127 | ans.add(follow.end); 128 | follow.endUse = true; 129 | } 130 | // 不同的需求,在这一段之间修改 131 | follow = follow.fail; 132 | } 133 | } 134 | return ans; 135 | } 136 | 137 | } 138 | 139 | public static void main(String[] args) { 140 | ACAutomation ac = new ACAutomation(); 141 | ac.insert("dhe"); 142 | ac.insert("he"); 143 | ac.insert("abcdheks"); 144 | // 设置fail指针 145 | ac.build(); 146 | 147 | List contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv"); 148 | for (String word : contains) { 149 | System.out.println(word); 150 | } 151 | } 152 | 153 | } 154 | 155 | ``` 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /16-《进阶》Manacher(马拉车)算法.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 Manacher算法 3 | 4 | ## 1.1 简介 5 | 6 | 回文串概念:一个字符串是轴对称的,轴的左侧和右侧是逆序的关系,例如"abcba","abccba" 7 | 8 | Manacher算法解决在一个字符串中最长回文子串的大小,例如"abc12321ef"最长回文子串为"12321"大小为5 9 | 10 | 11 | 回文串的用途,例如我们可以把DNA当成一个字符串,有一些基因片段是回文属性的,在生理学上有实际意义 12 | 13 | 14 | ## 1.2 字符串最长回文子串暴力解 15 | 16 | > 扩散法 17 | 18 | 遍历str,以i为回文对称的回文串有多长,在i位置左右扩展。如果字符串"abc123df" 19 | 20 | i为0的时候,回文串为"a",长度为1; 21 | 22 | i为1的时候,回文串为"b",左边和右边不相等,长度为1; 23 | 24 | i为1的时候,回文串为"b",长度为1; 25 | 26 | 。。。。。。 27 | 28 | 29 | 此种方式,无法找到以一个虚轴的最长回文串 30 | 31 | 所以我们可以通过填充字符,来解决,例如"31211214"我们前后和字符间填充'#'得到"#3#1#2#1#1#2#1#4#",利用该串进行上述步骤来寻找。 32 | 33 | i为0的时候,回文串为"#",长度为1; 34 | 35 | i为1的时候,回文串为"#3#",长度为3; 36 | 37 | ... 38 | 39 | i为8的时候,回文串为"#1#2#1#1#2#1#",长度为13; 40 | 41 | 可以找到所有基数或者偶数的所有回文串,但是回文串的长度和原始回文串长度的关系为:当前回文串长度除以2,得到原始串中各个位置的回文串长度 42 | 43 | 44 | 复杂度最差情况是,所有字符长度为n,且所有字符都相等。经过我们的填充,字符串规模扩展到2n+1。每次寻找,都会寻找到左边界或者右边界,该方法的事件复杂度为O(N*N) 45 | 46 | 47 | Manacher算法解该类问题,O(N)复杂度可以解决 48 | 49 | ## 1.3 Manacher解决最长回文串O(N) 50 | 51 | 概念1:回文半径和回文直径,回文直径指的是,在我们填充串中,每个位置找到的回文串的整体长度,回文半径指的是在填充串中回文串从i位置到左右边界的一侧的长度 52 | 53 | 54 | 概念2:回文半径数组,对于填充串每个i位置,求出来的回文串的长度,都记录下来。pArr[]长度和填充串长度相同 55 | 56 | 概念3:回文最右右边界R和中心C,R和C初始为-1。例如扩展串为"#1#2#2#1#3#"。O(N) 57 | 58 | - i为0的时候,R为0,C为0 59 | - i为1的时候,R为2,C为1 60 | - i为2的时候,R为不变,只要R不变,C就不变 61 | 62 | 63 | Manacher算法的核心概念就是,回文半径数组pArr[],回文最右边界R,中心C 64 | 65 | 在遍历填充数组时,会出现i在R外,和i在R内两种情况。 66 | 67 | 68 | 当i在R外时,没有任何优化,继续遍历去寻找。当i在R内涉及到Manacher算法的优化。i在R内的时候,i肯定大于C,我们可以根据R和C求出左边界L,也可以根据i和C求出以C对称的i'。 69 | 70 | - 情况1:i'的回文区域在L和R的内部。i不需要再求回文串,i的回文串的大小等于pArr[]中i'位置的长度。原因是i和i'关于C对称,整体在R和L范围内,R和L也是关于C对称,传递得到。O(1) 71 | 72 | 73 | - 情况2:i'的回文区域的左边界在L的左侧。i的回文半径就是i位置到R位置的长度。原因是,L以i'为对称的L',R以i为堆成的R'一定在L到R的范围内。且L'到L和R'到R互为回文。所以i区域的回文区域的回文半径至少为i到R的距离。由于以C为对称,得到区域为L到R,L不等于R,此处画图根据传递得到i的回文半径就是i位置到R位置的长度。O(1) 74 | 75 | 76 | - 情况3:i'的回文区域的左边界和L相等。i'的右区域一定不会再R的右侧。根据情况2,R以i对称的R'。R和R'确定是回文,需要验证R下一个位置和R'前一个位置是否回文,这里也可以省掉R'到R之间的验证。O(N) 77 | 78 | 79 | 经过以上的情况,整体O(N)复杂度 80 | 81 | ```Go 82 | package main 83 | 84 | import ( 85 | "fmt" 86 | "math" 87 | ) 88 | 89 | // manacher 给定一个字符串,求该字符串的最长回文子串的大小 90 | func manacher(s string) int { 91 | if len(s) == 0 { 92 | return 0 93 | } 94 | 95 | // "12132" -> "#1#2#1#3#2#" 96 | str := manacherString(s) 97 | // 回文半径的大小 98 | pArr := make([]int, len(str)) 99 | C := -1 100 | // 算法流程中,R代表最右的扩成功的位置。coding:最右的扩成功位置的,再下一个位置,即失败的位置 101 | R := -1 102 | max := math.MinInt 103 | for i := 0; i < len(str); i++ { 104 | // R是第一个违规的位置,i>= R就表示i在R外 105 | // i位置扩出来的答案,i位置扩的区域,至少是多大。 106 | // 2 * C - i 就是i的对称点。 107 | // 得到各种情况下无需验的区域 108 | if R > i { 109 | pArr[i] = int(math.Min(float64(pArr[2 * C - i]), float64(R - i))) 110 | } else { 111 | pArr[i] = 1 112 | } 113 | 114 | // 右侧不越界,且左侧不越界,检查需不需要扩 115 | for i + pArr[i] < len(str) && i - pArr[i] > -1 { 116 | if str[i + pArr[i]] == str[i - pArr[i]] { 117 | pArr[i]++ 118 | } else { 119 | break 120 | } 121 | } 122 | 123 | //i的右边界有没有刷新之前的最右边界。R刷新C要跟着刷新 124 | if i + pArr[i] > R { 125 | R = i + pArr[i] 126 | C = i 127 | } 128 | max = int(math.Max(float64(max), float64(pArr[i]))) 129 | } 130 | 131 | return max - 1 132 | } 133 | 134 | func manacherString(str string) []byte { 135 | charArr := []byte(str) 136 | res := make([]byte, len(str) * 2 + 1) 137 | index := 0 138 | for i := 0; i != len(res); i++ { 139 | if (i & 1) == 0 { // 奇数位填充'#' 140 | res[i] = '#' 141 | } else { 142 | res[i] = charArr[index] 143 | index++ 144 | } 145 | } 146 | return res 147 | } 148 | 149 | func main() { 150 | s := "abc12321ef" 151 | fmt.Println(manacher(s)) // 5 152 | } 153 | ``` 154 | 155 | ## 1.4 例题 156 | 157 | 给定一个字符串str,只可在str后添加字符,把该串变成回文字符串最少需要多少个字符? 158 | 159 | > 解题思路:转化为必须包含最后一个字符的最长回文串多长?例如,"abc12321",以最后一个1的最长回文串为"12321",那么最少需要添加"cba"3个字符 160 | 161 | 162 | ```Go 163 | package main 164 | 165 | import ( 166 | "fmt" 167 | "math" 168 | ) 169 | 170 | func shortestEnd(s string) string { 171 | if len(s) == 0 { 172 | return "" 173 | } 174 | 175 | str := manacherString(s) 176 | pArr := make([]int, len(str)) 177 | 178 | C := -1 179 | R := -1 180 | maxContainsEnd := -1 181 | 182 | for i := 0; i != len(str); i++ { 183 | if R > i { 184 | pArr[i] = int(math.Min(float64(pArr[2 * C - i]), float64(R - i))) 185 | } else { 186 | pArr[i] = 1 187 | } 188 | 189 | for i + pArr[i] < len(str) && i - pArr[i] > -1 { 190 | if str[i + pArr[i]] == str[i - pArr[i]] { 191 | pArr[i]++ 192 | } else { 193 | break 194 | } 195 | } 196 | 197 | if i + pArr[i] > R { 198 | R = i + pArr[i] 199 | C = i 200 | } 201 | 202 | if R == len(str) { 203 | maxContainsEnd = pArr[i] 204 | break 205 | } 206 | } 207 | 208 | res := make([]byte, len(s) - maxContainsEnd + 1) 209 | for i := 0; i < len(res); i++ { 210 | res[len(res) - 1 -i] = str[i * 2 + 1] 211 | } 212 | 213 | return string(res) 214 | } 215 | 216 | func manacherString(str string) []byte { 217 | charArr := []byte(str) 218 | res := make([]byte, len(str) * 2 + 1) 219 | index := 0 220 | for i := 0; i != len(res); i++ { 221 | if (i & 1) == 0 { // 奇数位填充'#' 222 | res[i] = '#' 223 | } else { 224 | res[i] = charArr[index] 225 | index++ 226 | } 227 | } 228 | return res 229 | } 230 | 231 | func main() { 232 | s := "abcd123321" 233 | fmt.Println(shortestEnd(s)) // dcba => abcd123321dcba 234 | } 235 | ``` 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /27-附:字符串专题汇总.md: -------------------------------------------------------------------------------- 1 | ```Go 2 | package main 3 | 4 | import ( 5 | "math" 6 | ) 7 | 8 | // 1、 判断两个字符串是否互为变形词 9 | func isDeformation(str1, str2 string) bool { 10 | if len(str1) == 0 || len(str2) == 0 || len(str1) != len(str2) { 11 | return false 12 | } 13 | 14 | chars1 := []byte(str1) 15 | chars2 := []byte(str2) 16 | 17 | // 字符词频统计表 18 | m := make([]int, 256) 19 | 20 | // 对第一个字符串中的字符进行词频统计 21 | for _, c := range chars1 { 22 | m[c]++ 23 | } 24 | 25 | // 用第二个字符串的字符去消除词频 26 | for _, c := range chars2 { 27 | if m[c] == 0 { 28 | return false 29 | } 30 | m[c]-- 31 | } 32 | 33 | return true 34 | } 35 | 36 | // 2、 移除字符串中连续出现k个0的子串 37 | func removeKZeros(str string, k int) string { 38 | if len(str) == 0 || k < 1 { 39 | return str 40 | } 41 | 42 | chars := []byte(str) 43 | count := 0 44 | start := -1 45 | 46 | for i := 0; i < len(chars); i++ { 47 | if chars[i] == '0' { 48 | count++ 49 | if start == -1 { 50 | start = i 51 | } 52 | } else { 53 | // 如果不等于'0'需要从start位置开始,去掉count个'0'字符 54 | if count == k { 55 | for ; count != 0; count++ { 56 | // ascii码空白字符的表示为十进制的0。chars[1] = 0 表示把1位置的字符,替换为空白符 57 | chars[start] = 0 58 | start++ 59 | } 60 | } 61 | // 一轮剔除结束,count和start归位 62 | count = 0 63 | start = -1 64 | } 65 | } 66 | 67 | // 最后一轮,即如果字符串是以'0'字符结尾的。最后要单独结算一次 68 | if count == k { 69 | for ; count != 0; count-- { 70 | chars[start] = 0 71 | start++ 72 | } 73 | } 74 | 75 | return string(chars) 76 | } 77 | 78 | // 3、判断字符数组中,是否所有的字符均出现一次 79 | func isUnique(chars []byte) bool { 80 | if len(chars) == 0 { 81 | return true 82 | } 83 | 84 | m := make([]bool, 256) 85 | for i := 0; i < len(chars); i++ { 86 | if m[chars[i]] { 87 | return false 88 | } 89 | m[chars[i]] = true 90 | } 91 | 92 | return true 93 | } 94 | 95 | // 4、括号字符匹配问题:输入一个字符串,包含'(','[','{',')',']','}'几种括号,求是否是括号匹配的结果。 96 | func isValid(s string) bool { 97 | if len(s) == 0 { 98 | return true 99 | } 100 | 101 | chars := []byte(s) 102 | stack := make([]byte, 0) 103 | 104 | for i := 0; i < len(chars); i++ { 105 | c := chars[i] 106 | // 遇到左括号,添加相应的右括号 107 | if c == '(' || c == '[' || c == '{' { 108 | if c == '(' { 109 | stack = append(stack, ')') 110 | } 111 | if c == '[' { 112 | stack = append(stack, ']') 113 | } 114 | if c == '{' { 115 | stack = append(stack, '}') 116 | } 117 | } else { // 遇到右括号,弹出栈,比对相等 118 | if len(stack) == 0 { 119 | return false 120 | } 121 | 122 | last := stack[len(stack)-1] 123 | stack = stack[:len(stack)-1] 124 | 125 | if c != last { 126 | return false 127 | } 128 | } 129 | } 130 | 131 | // 遍历结束,栈刚好为空。满足匹配要求 132 | return len(stack) == 0 133 | } 134 | 135 | // 5、求一个字符串无重复最长子串 136 | // 子串和子序列的区别,子串必须要连续,子序列不一定要连续。 137 | // 遇到子串和子序列的问题,可以按照一种经典思路: 138 | // 按照i位置结尾的情况下答案是什么?求所有可能的结尾即可,所有位置结尾的答案都求出,最大的就是我们的目标答案 139 | // 时间复杂度O(N),空间复杂度O(1),由于申请的空间是固定长度256 140 | func lengthOfLongestSubstring(s string) int { 141 | // base case 过滤无效参数 142 | if len(s) == 0 { 143 | return 0 144 | } 145 | 146 | chars := []byte(s) 147 | m := make([]int, 256) 148 | // 辅助数组。保存字符出现的位置,字符的范围为可显示字符0~127,扩展ascii字符128~255。0~255共256 149 | for i := 0; i < 256; i++ { 150 | // 默认所有的字符都没出现过 151 | m[i] = -1 152 | } 153 | 154 | // i位置往左推,推不动的位置第一个因素是再次遇到了i位置上的元素,第二个因素是i-1位置当初推了多远。 155 | // 这两个因素的限制,哪个限制位置离当前i位置近,就是当前字符i最远推到的位置,map[i] 156 | // 收集答案。len是收集全局的最大长度 157 | length := 0 158 | pre := -1 // i-1位置结尾的情况下,往左推,推不动的位置是谁。用来每次保存i之前一个位置的答案 159 | cur := 0 160 | 161 | for i := 0; i != len(chars); i++ { 162 | // i位置结尾的情况下,往左推,推不动的位置是谁 163 | // pre (i-1信息) 更新成 pre(i 结尾信息) 164 | // 上次推不动的,和当前字符上次出现的位置map[str[i]]的位置,取大的 165 | pre = int(math.Max(float64(pre), float64(m[chars[i]]))) 166 | // 找到了当前推不动的位置,当前不重复子串的长度就是i-pre 167 | cur = i - pre 168 | // 全局最大的子串长度,是否被更新,决定是否要收集 169 | length = int(math.Max(float64(length), float64(cur))) 170 | // 更新当前字符出现的位置是当前位置 171 | m[chars[i]] = i 172 | } 173 | 174 | return length 175 | } 176 | 177 | // 6、最长回文子串问题。 178 | // 该解法是扩散法。时间复杂度为O(N * N)。(最优解是马拉车算法,可以优化该题到O(N),不掌握) 179 | func longestPalindrome2(s string) string { 180 | if len(s) == 0 { 181 | return s 182 | } 183 | 184 | // 全局最大回文长度 185 | res := 1 186 | // 全局最大回文长度对应的左位置 187 | ll := 0 188 | // 全局最大回文长度对应的右位置 189 | rr := 0 190 | 191 | for i := 0; i < len(s); i++ { 192 | // 以i为下标的奇数情况,是否有更大的len来更新res 193 | l := i - 1 194 | r := i + 1 195 | // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 196 | for l >= 0 && r < len(s) && s[l] == s[r] { 197 | length := r - l + 1 198 | // 更新最长回文串的长度 199 | if length > res { 200 | res = length 201 | ll = l 202 | rr = r 203 | } 204 | // 扩散 205 | l-- 206 | r++ 207 | } 208 | 209 | // 以i为下标偶数的情况。是否有更大的len来更新全局res 210 | l = i 211 | r = i + 1 212 | // l和r都在合法范围。且l和r位置字符相等,可以继续扩散 213 | for l >= 0 && r < len(s) && s[l] == s[r] { 214 | length := r - l + 1 215 | // 更新最长回文串的长度 216 | if length > res { 217 | res = length 218 | ll = l 219 | rr = r 220 | } 221 | // 扩散 222 | l-- 223 | r++ 224 | } 225 | } 226 | return s[ll : rr+1] // 等价于s.subString(2, 7)都是左闭右开 227 | } 228 | 229 | // 7、字符串最长公共前缀问题 230 | func longestCommonPrefix(strs []string) string { 231 | if len(strs) == 0 { 232 | return "" 233 | } 234 | 235 | // 拿出第一个字符串。当成初始值 236 | chars := []byte(strs[0]) 237 | // 所有字符串都匹配的最大长度,等同于每个字符串和初始字符串匹配的全局最小长度 238 | min := math.MaxInt 239 | 240 | for _, str := range strs { 241 | tmp := []byte(str) 242 | index := 0 243 | 244 | for index < len(tmp) && index < len(chars) { 245 | if chars[index] != tmp[index] { 246 | break 247 | } 248 | index++ 249 | } 250 | 251 | // 更新min 252 | min = int(math.Min(float64(index), float64(min))) 253 | // 如果有任意一个字符串和初始串不匹配,直接返回"" 254 | if min == 0 { 255 | return "" 256 | } 257 | } 258 | return strs[0][0:min] // strs[0].substring(0, min); 259 | } 260 | ``` -------------------------------------------------------------------------------- /20-《进阶》数组累加和问题.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 数组累加和问题三连 4 | 5 | 知识点补充:系统设计类题目 6 | 7 | 设计一个系统,该系统的功能是可以一直不停的提供不同的UUID,该UUID使用的极为频繁,比如全球卖西瓜的,每个西瓜子是一个UUID 8 | 9 | > 思路,如果使用hashcode,有可能会产生碰撞。不使用hashcode,使用机器的ip或者mac地址加上纳秒时间,也不行,所有机器时间是否强同步 10 | 11 | > 解决思路:定义一台服务器,对世界的国家进行分类,比如中国下是省份,美国下是州,英国下是邦。每一个国家向中央服务器要随机范围,中央服务器分配出去的是start和range。比如给中国分配的是start从1开始,range到100w,中国uuid不够用了,可以再向中央服务器要,分配后中央服务器的start要增大到已分配出去后的位置。其他国家类似 12 | 13 | > 该设计是垂直扩展的技术,当前很多是水平扩展,比如直接hashcode,random等。但有些场景适合用这种垂直扩展的解决方案 14 | 15 | 16 | 17 | ## 1.1 数组累加和问题 18 | 19 | ### 1.1.1 第一连例题 20 | 21 | 有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组最长多长。例如[3,2,1,1,1,6,1,1,1,1,1,1],sum等于6。最长的子数组为[1,1,1,1,1,1]返回长度6 22 | 23 | 24 | > 由于是正数数组,累加和和范围具有单调性。对于具有单调性的题目,要么定义左右指针,要么定义窗口滑动 25 | 26 | 27 | 定义窗口window,windowSum初始为0。滑动的过程中: 28 | 29 | 1、如果windowSum小于sum,窗口右边界R向右移动一个位置; 30 | 31 | 2、如果windowSum大于sum,窗口左边界L向右移动一个位置; 32 | 33 | 3、如果windowSum等于sum,此时的窗口大小就是一个满足条件的子数组大小,决定是否要更新答案; 34 | 35 | ```Go 36 | package main 37 | 38 | import ( 39 | "fmt" 40 | "math" 41 | ) 42 | 43 | // getMaxLength 有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组最长多长。 滑动窗口的表示 44 | func getMaxLength(arr []int, K int) int { 45 | if len(arr) == 0 || K <= 0 { 46 | return 0 47 | } 48 | 49 | // 初始窗口位置[0,0],窗口当前只有第一个数 50 | left := 0 51 | right := 0 52 | sum := arr[0] 53 | length := 0 54 | 55 | for right < len(arr) { 56 | // 窗口的累加和sum等于我们的目标k。求窗口大小len 57 | if sum == K { 58 | length = int(math.Max(float64(length), float64(right - left + 1))) 59 | // 窗口累加和减去左窗口位置的值,左位置再出窗口 60 | sum -= arr[left] 61 | left++ 62 | } else if sum < K { 63 | // 窗口右边界扩,如果不越界把扩之后的那个位置的值加到窗口累加值上 64 | right++ 65 | if right == len(arr) { 66 | break 67 | } 68 | sum += arr[right] 69 | } else { 70 | sum -= arr[left] 71 | left++ 72 | } 73 | } 74 | return length 75 | } 76 | 77 | // 最长的子数组为[1,1,1,1,1,1]返回长度6 78 | func main() { 79 | arr := []int{3,2,1,1,1,6,1,1,1,1,1,1} 80 | sum := 6 81 | fmt.Println(getMaxLength(arr, sum)) 82 | } 83 | ``` 84 | 85 | ### 1.1.2 第二连例题 86 | 87 | 有一个数组,值可以为正可以为负可以为0。给定一个值sum,求子数组中累加和等于sum的最大长度? 88 | 89 | > 该题和第一连问题的区别是,数组的值可正可负可零,单调性消失了。对于数组问题,我们常见的解决子数组的思考思路,如果以每一个位置开头能求出一个答案,那么目标答案一定在其中。反过来如果以每一个位置为结尾能求出一个答案,那么目标答案一定也在其中 90 | 91 | > 该题思路用第二种比较方便,我们以某个位置i结尾,之前的数累加和等于目标sum,求该位置满足此条件的最长数组。该种思路等同于,从0位置开始到i位置的累加和(allSum),减去从0位置到最早和0位置的累加和等于allSum-sum的位置j。那么原问题的答案是j+1到j位置的长度。预置,0位置累加和位置等于-1位置 92 | 93 | 94 | ```Go 95 | package main 96 | 97 | import "math" 98 | 99 | // 有一个数组,值可以为正可以为负可以为0。给定一个值sum,求子数组中累加和等于sum的最大长度 100 | // arr数组,累加和为k的最长子数组返回 101 | func maxLength(arr []int, k int) int { 102 | if len(arr) == 0 { 103 | return 0 104 | } 105 | 106 | // key表示累加和,value表示最早出现的位置 107 | m := make(map[int]int, 0) 108 | // 0位置的累加和,最早出现在-1位置。预置 109 | m[0] = -1 // important 110 | // 最大长度是多少 111 | length := 0 112 | // 累加和多大 113 | sum := 0 114 | for i := 0; i < len(arr); i++ { 115 | sum += arr[i] 116 | if v1, ok := m[sum - k]; ok { 117 | // j+1到i有多少个数,i-j个 118 | length = int(math.Max(float64(i - v1), float64(length))) 119 | } 120 | if _, ok := m[sum]; !ok { 121 | m[sum] = i 122 | } 123 | } 124 | return length 125 | } 126 | ``` 127 | 128 | 129 | 对于数组arr,可正可负可零。求子数组中1和2数值个数相等的子数组最长的长度,返回? 130 | 131 | > 把数组中其他数值变为0,为1的数值仍为1,为2的数值变为-1。处理好之后,求子数组累加和为0的最长子数组。 132 | 133 | > 很多数组问题,可以转化为求子数组累加和问题,需要训练敏感度 134 | 135 | 136 | 137 | ### 1.1.2 第三连例题 138 | 139 | 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度? 140 | 141 | 142 | 概念定义: 143 | 144 | 以i开头的子数组中,所有可能性中,哪一个能让累加和最小的范围是什么? 145 | 146 | 例如[3,7,4,-6,6,3,-2,0,7-3,2],准备两个辅助数组minSum[],minEnd[]。minSum记录从i开始后续子数组中累加和最小的值。minEnd记录i开始后续子数组中累加和最小的累加和的终止位置j 147 | 148 | 对于i位置,如果有变为正数,那么我累加和最小的就是我自己;如果自身是正数,累加和最小就是我右侧位置计算出来的最小累加和 149 | 150 | 如此操作,任意i位置,最小子数组累加和我们能拿到,最小累加和子数组的右边界我们能拿到 151 | 152 | 153 | > 该题巧妙之处是排除可能性,比较难 154 | 155 | 156 | ```Go 157 | package main 158 | 159 | import ( 160 | "fmt" 161 | "math" 162 | ) 163 | 164 | // 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度 165 | func maxLengthAwesome(arr []int, k int) int { 166 | if len(arr) == 0 { 167 | return 0 168 | } 169 | 170 | minSums := make([]int, len(arr)) 171 | minSumEnds := make([]int, len(arr)) 172 | minSums[len(arr) - 1] = arr[len(arr) - 1] 173 | minSumEnds[len(arr) - 1] = len(arr) - 1 174 | 175 | for i := len(arr) - 2; i >= 0; i-- { 176 | if minSums[i + 1] < 0 { 177 | minSums[i] = arr[i] + minSums[i + 1] 178 | minSumEnds[i] = minSumEnds[i + 1] 179 | } else { 180 | minSums[i] = arr[i] 181 | minSumEnds[i] = i 182 | } 183 | } 184 | 185 | end := 0 186 | sum := 0 187 | res := 0 188 | // i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置 189 | // end也是下一块儿的开始位置 190 | // 窗口:[i~end) 191 | for i := 0; i < len(arr); i++ { 192 | // for循环结束之后: 193 | // 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res; 194 | // 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果; 195 | for end < len(arr) && sum + minSums[end] <= k { 196 | sum += minSums[end] 197 | end = minSumEnds[end] + 1 198 | } 199 | res = int(math.Max(float64(res), float64(end - i))) 200 | if end > i { // 窗口内还有数 [i~end) [4,4) 201 | sum -= arr[i] 202 | } else { // 窗口内已经没有数了,说明从i开头的所有子数组累加和都不可能<=k 203 | end = i + 1 204 | } 205 | } 206 | return res 207 | } 208 | 209 | // 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度; 暴力解法 210 | func maxLength2(arr []int, k int) int { 211 | h := make([]int, len(arr) + 1) 212 | sum := 0 213 | h[0] = sum 214 | for i := 0; i != len(arr); i++ { 215 | sum += arr[i] 216 | h[i + 1] = int(math.Max(float64(sum), float64(h[i]))) 217 | } 218 | sum = 0 219 | res := 0 220 | pre := 0 221 | length := 0 222 | for i := 0; i != len(arr); i++ { 223 | sum += arr[i] 224 | pre = getLessIndex(h, sum - k) 225 | if pre == -1 { 226 | length = 0 227 | } else { 228 | length = i - pre + 1 229 | } 230 | res = int(math.Max(float64(res), float64(length))) 231 | } 232 | return res 233 | } 234 | 235 | func getLessIndex(arr []int, num int) int { 236 | low := 0 237 | high := len(arr) - 1 238 | mid := 0 239 | res := -1 240 | for low <= high { 241 | mid = (low + high) / 2 242 | if arr[mid] >= num { 243 | res = mid 244 | high = mid - 1 245 | } else { 246 | low = mid + 1 247 | } 248 | } 249 | return res 250 | } 251 | 252 | //7 253 | //7 254 | func main() { 255 | arr := []int{3,7,4,-6,6,3,-2,0,7-3,2} 256 | k := 10 257 | fmt.Println(maxLength2(arr, k)) 258 | fmt.Println(maxLengthAwesome(arr, k)) 259 | } 260 | ``` 261 | 262 | 263 | -------------------------------------------------------------------------------- /docs/16.md: -------------------------------------------------------------------------------- 1 | - [1 Manacher算法](#1) 2 | * [1.1 简介](#11) 3 | * [1.2 字符串最长回文子串暴力解](#12) 4 | * [1.3 Manacher解决最长回文串O(N)](#13) 5 | * [1.4 例题](#14) 6 | 7 |

1 Manacher算法

8 | 9 | 10 |

1.1 简介

11 | 12 | 回文串概念:一个字符串是轴对称的,轴的左侧和右侧是逆序的关系,例如"abcba","abccba" 13 | 14 | Manacher算法解决在一个字符串中最长回文子串的大小,例如"abc12321ef"最长回文子串为"12321"大小为5 15 | 16 | 17 | 回文串的用途,例如我们可以把DNA当成一个字符串,有一些基因片段是回文属性的,在生理学上有实际意义 18 | 19 |

1.2 字符串最长回文子串暴力解

20 | 21 | 遍历str,以i为回文对称的回文串有多长,在i位置左右扩展。所以字符串"abc123df" 22 | 23 | i为0的时候,回文串为"a",长度为1; 24 | 25 | i为1的时候,回文串为"b",左边和右边不相等,长度为1; 26 | 27 | i为1的时候,回文串为"b",长度为1; 28 | 29 | 。。。。。。 30 | 31 | 32 | 此种方式,无法找到以一个虚轴的最长回文串 33 | 34 | 所以我们可以通过填充字符,来解决,例如"31211214"我们前后和字符间填充'#'得到"#3#1#2#1#1#2#1#4#",利用该串进行上述步骤来寻找。 35 | 36 | i为0的时候,回文串为"#",长度为1; 37 | 38 | i为1的时候,回文串为"#3#",长度为3; 39 | 40 | ... 41 | 42 | i为8的时候,回文串为"#1#2#1#1#2#1#",长度为13; 43 | 44 | 可以找到所有基数或者偶数的所有回文串,但是回文串的长度和原始回文串长度的关系为:当前回文串长度除以2,得到原始串中各个位置的回文串长度 45 | 46 | 47 | 复杂度最差情况是,所有字符长度为n,且所有字符都相等。经过我们的填充,字符串规模扩展到2n+1。每次寻找,都会寻找到左边界或者右边界,该方法的事件复杂度为O(N*N) 48 | 49 | 50 | Manacher算法解决该类问题,O(N)复杂度! 51 | 52 |

1.3 Manacher解决最长回文串O(N)

53 | 54 | 概念1:回文半径和回文直径,回文直径指的是,在我们填充串中,每个位置找到的回文串的整体长度,回文半径指的是在填充串中回文串从i位置到左右边界的一侧的长度 55 | 56 | 57 | 概念2:回文半径数组,对于填充串每个i位置,求出来的回文串的长度,都记录下来。pArr[]长度和填充串长度相同 58 | 59 | 概念3:回文最右右边界R和中心C,R和C初始为-1。例如扩展串为"#1#2#2#1#3#"。O(N) 60 | 61 | - i为0的时候,R为0,C为0 62 | - i为1的时候,R为2,C为1 63 | - i为2的时候,R为不变,只要R不变,C就不变 64 | 65 | 66 | Manacher算法的核心概念就是,回文半径数组pArr[],回文最右边界R,中心C 67 | 68 | 在遍历填充数组时,会出现i在R外,和i在R内两种情况。 69 | 70 | 71 | 当i在R外时,没有任何优化,继续遍历去寻找。当i在R内涉及到Manacher算法的优化。i在R内的时候,i肯定大于C,我们可以根据R和C求出左边界L,也可以根据i和C求出以C堆成的i'。 72 | 73 | - 情况1:i'的回文区域在彻底在L和R的内部。i不需要再求回文串,i的回文串的大小等于pArr[]中i'位置的长度。原因是i和i'关于C对称,整体在R和L范围内,R和L也是关于C对称,传递得到。O(1) 74 | 75 | 76 | - 情况2:i'的回文区域的左边界在L的左侧。i的回文半径就是i位置到R位置的长度。原因是,L以i'为堆成的L',R以i为堆成的R'一定在L到R的范围内。且L'到L和R'到R互为回文。所以i区域的回文区域的回文半径至少为i到R的距离。由于以C为对称,得到区域为L到R,L不等于R,此处画图根据传递得到i的回文半径就是i位置到R位置的长度。O(1) 77 | 78 | 79 | - 情况3:i'的回文区域的左边界和L相等。i'的右区域一定不会再R的右侧。根据情况2,R以i堆成的R'。R和R'确定是回文,需要验证R下一个位置和R'前一个位置是否回文,这里也可以省掉R'到R之间的验证。O(N) 80 | 81 | 82 | 经过以上的情况,整体O(N)复杂度 83 | 84 | 85 | Code: 86 | 87 | ```Java 88 | public class Code01_Manacher { 89 | 90 | public static int manacher(String s) { 91 | if (s == null || s.length() == 0) { 92 | return 0; 93 | } 94 | // "12132" -> "#1#2#1#3#2#" 95 | char[] str = manacherString(s); 96 | // 回文半径的大小 97 | int[] pArr = new int[str.length]; 98 | int C = -1; 99 | // 算法流程中,R代表最右的扩成功的位置。coding:最右的扩成功位置的,再下一个位置,即失败的位置 100 | int R = -1; 101 | int max = Integer.MIN_VALUE; 102 | for (int i = 0; i < str.length; i++) { 103 | // R是第一个违规的位置,i>= R就表示i在R外 104 | // i位置扩出来的答案,i位置扩的区域,至少是多大。 105 | // 2 * C - i 就是i的对称点。 106 | // 得到各种情况下无需验的区域 107 | pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1; 108 | 109 | // 右侧不越界,且左侧不越界,检查需不需要扩 110 | while (i + pArr[i] < str.length && i - pArr[i] > -1) { 111 | if (str[i + pArr[i]] == str[i - pArr[i]]) 112 | pArr[i]++; 113 | else { 114 | break; 115 | } 116 | } 117 | //i的右边界有没有刷新之前的最右边界。R刷新C要跟着刷新 118 | if (i + pArr[i] > R) { 119 | R = i + pArr[i]; 120 | C = i; 121 | } 122 | max = Math.max(max, pArr[i]); 123 | } 124 | return max - 1; 125 | } 126 | 127 | public static char[] manacherString(String str) { 128 | char[] charArr = str.toCharArray(); 129 | char[] res = new char[str.length() * 2 + 1]; 130 | int index = 0; 131 | for (int i = 0; i != res.length; i++) { 132 | res[i] = (i & 1) == 0 ? '#' : charArr[index++]; 133 | } 134 | return res; 135 | } 136 | 137 | // for test 138 | public static int right(String s) { 139 | if (s == null || s.length() == 0) { 140 | return 0; 141 | } 142 | char[] str = manacherString(s); 143 | int max = 0; 144 | for (int i = 0; i < str.length; i++) { 145 | int L = i - 1; 146 | int R = i + 1; 147 | while (L >= 0 && R < str.length && str[L] == str[R]) { 148 | L--; 149 | R++; 150 | } 151 | max = Math.max(max, R - L - 1); 152 | } 153 | return max / 2; 154 | } 155 | 156 | // for test 157 | public static String getRandomString(int possibilities, int size) { 158 | char[] ans = new char[(int) (Math.random() * size) + 1]; 159 | for (int i = 0; i < ans.length; i++) { 160 | ans[i] = (char) ((int) (Math.random() * possibilities) + 'a'); 161 | } 162 | return String.valueOf(ans); 163 | } 164 | 165 | public static void main(String[] args) { 166 | int possibilities = 5; 167 | int strSize = 20; 168 | int testTimes = 5000000; 169 | System.out.println("test begin"); 170 | for (int i = 0; i < testTimes; i++) { 171 | String str = getRandomString(possibilities, strSize); 172 | if (manacher(str) != right(str)) { 173 | System.out.println("Oops!"); 174 | } 175 | } 176 | System.out.println("test finish"); 177 | } 178 | 179 | } 180 | ``` 181 | 182 |

1.4 例题

183 | 184 | 185 | 给定一个字符串str,只可在str后添加字符,把该串变成回文字符串最少需要多少个字符? 186 | 187 | > 解题思路:转化为必须包含最后一个字符的最长回文串多长?例如,"abc12321",以最后一个1的最长回文串为"12321",那么最少需要添加"cba"3个字符 188 | 189 | 190 | ```Java 191 | public class Code02_AddShortestEnd { 192 | 193 | public static String shortestEnd(String s) { 194 | if (s == null || s.length() == 0) { 195 | return null; 196 | } 197 | char[] str = manacherString(s); 198 | int[] pArr = new int[str.length]; 199 | int C = -1; 200 | int R = -1; 201 | int maxContainsEnd = -1; 202 | for (int i = 0; i != str.length; i++) { 203 | pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1; 204 | while (i + pArr[i] < str.length && i - pArr[i] > -1) { 205 | if (str[i + pArr[i]] == str[i - pArr[i]]) 206 | pArr[i]++; 207 | else { 208 | break; 209 | } 210 | } 211 | if (i + pArr[i] > R) { 212 | R = i + pArr[i]; 213 | C = i; 214 | } 215 | if (R == str.length) { 216 | maxContainsEnd = pArr[i]; 217 | break; 218 | } 219 | } 220 | char[] res = new char[s.length() - maxContainsEnd + 1]; 221 | for (int i = 0; i < res.length; i++) { 222 | res[res.length - 1 - i] = str[i * 2 + 1]; 223 | } 224 | return String.valueOf(res); 225 | } 226 | 227 | public static char[] manacherString(String str) { 228 | char[] charArr = str.toCharArray(); 229 | char[] res = new char[str.length() * 2 + 1]; 230 | int index = 0; 231 | for (int i = 0; i != res.length; i++) { 232 | res[i] = (i & 1) == 0 ? '#' : charArr[index++]; 233 | } 234 | return res; 235 | } 236 | 237 | public static void main(String[] args) { 238 | String str1 = "abcd123321"; 239 | System.out.println(shortestEnd(str1)); 240 | } 241 | 242 | } 243 | ``` 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /25-附:链表专题汇总.md: -------------------------------------------------------------------------------- 1 | ```Go 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | // Node 链表的节点结构 10 | type Node struct { 11 | Value int 12 | Next *Node 13 | } 14 | 15 | // DuLNode 双向链表的节点结构 16 | type DuLNode struct { 17 | Value int 18 | Pre *DuLNode 19 | Next *DuLNode 20 | } 21 | 22 | // 1、检测链表是否成环。返回成环是否,第一次相遇并不保证是成环的节点 23 | func hasCycle(head *Node) bool { 24 | if head == nil || head.Next == nil { 25 | return false 26 | } 27 | 28 | slow := head 29 | fast := head.Next 30 | 31 | for slow != fast { 32 | if fast == nil || fast.Next == nil { 33 | return false 34 | } 35 | 36 | slow = slow.Next 37 | fast = fast.Next.Next 38 | } 39 | 40 | // 有环的话一定追的上,但不一定是第一次成环的节点 41 | return true 42 | } 43 | 44 | // 2、传入头节点,翻转单项链表。返回翻转后的新的头节点 45 | func reverseLinkedList(head *Node) *Node { 46 | var pre *Node 47 | var next *Node 48 | 49 | for head != nil { 50 | next = head.Next 51 | head.Next = pre 52 | pre = head 53 | head = next 54 | } 55 | 56 | return pre 57 | } 58 | 59 | // 3、移除链表中等于值的节点。返回处理后的头结点 60 | // 例如:1->2->3->3->4->5->3, 和 val = 3, 你需要返回删除3之后的链表:1->2->4->5。 61 | func removeValue(head *Node, num int) *Node { 62 | 63 | // 从链表的头开始,舍弃掉开头的且连续的等于num的节点 64 | for head != nil { 65 | if head.Value != num { 66 | break 67 | } 68 | head = head.Next 69 | } 70 | 71 | if head == nil { // 头结点处理完毕,发现全部都等于num的情况。 72 | return head 73 | } 74 | 75 | // head来到 第一个不需要删的位置 76 | pre := head 77 | cur := head 78 | 79 | // 快慢指针 80 | for cur != nil { 81 | if cur.Value == num { // 快指针cur向下滑动,如果值等于num,则暂时把下一个节点给慢指针的下一个指向。从而跳过等于num的节点 82 | pre.Next = cur.Next 83 | } else { // cur此时到了不等于num的节点,则慢指针追赶上去。达到的效果就是等于num的节点都被删掉了 84 | pre = cur 85 | } 86 | 87 | // 快指针向下滑动 88 | cur = cur.Next 89 | } 90 | return head 91 | } 92 | 93 | // 4、打印两个有序链表的公共部分 94 | // 例如:head1: 1->2->3->3->4->5 head2: 0->0->1->2->3->3->7->9 95 | // 公共部分为:1 2 3 3 96 | func printCommonPart(head1, head2 *Node) { 97 | fmt.Println("Common Part: ") 98 | 99 | for head1 != nil && head2 != nil { 100 | if head1.Value < head2.Value { 101 | head1 = head1.Next 102 | } else if head1.Value > head2.Value { 103 | head2 = head2.Next 104 | } else { 105 | fmt.Println(head1.Value) 106 | head1 = head1.Next 107 | head2 = head2.Next 108 | } 109 | } 110 | fmt.Println() 111 | } 112 | 113 | // 5、删除单链表的倒数第k个节点 114 | func removeLastKthNode(head *Node, lastKth int) *Node { 115 | if head == nil || lastKth < 1 { 116 | return head 117 | } 118 | 119 | // cur指针也指向链表头节点 120 | cur := head 121 | // 检查倒数第lastKth个节点的合法性 122 | for cur != nil { 123 | lastKth-- 124 | cur = cur.Next 125 | } 126 | 127 | // 需要删除的是头结点 128 | if lastKth == 0 { 129 | head = head.Next 130 | } 131 | 132 | if lastKth < 0 { 133 | // cur回到头结点 134 | cur = head 135 | for lastKth != 0 { 136 | lastKth++ 137 | cur = cur.Next 138 | } 139 | 140 | // 此次cur就是要删除的前一个节点。把原cur.next删除 141 | cur.Next = cur.Next.Next 142 | } 143 | 144 | // lastKth > 0的情况,表示倒数第lastKth节点比原链表程度要大,即不存在 145 | return head 146 | } 147 | 148 | // 6、删除链表中间节点 149 | // 思路:如果链表为空或者只有一个节点,不做处理。链表两个节点删除第一个节点,链表三个节点,删除中间第二个节点,链表四个节点,删除上中点 150 | func removeMidNode(head *Node) *Node { 151 | // 无节点,或者只有一个节点的情况,直接返回 152 | if head == nil || head.Next == nil { 153 | return head 154 | } 155 | 156 | // 链表两个节点,删除第一个节点 157 | if head.Next.Next == nil { 158 | // free first node mem 159 | return head.Next 160 | } 161 | 162 | pre := head 163 | cur := head.Next.Next 164 | 165 | // 快慢指针 166 | if cur.Next != nil && cur.Next.Next != nil { 167 | pre = pre.Next 168 | cur = cur.Next.Next 169 | } 170 | 171 | // 快指针走到尽头,慢指针奇数长度停留在中点,偶数长度停留在上中点。删除该节点 172 | pre.Next = pre.Next.Next 173 | return head 174 | } 175 | 176 | // 7、给定一个链表,如果成环,返回成环的那个节点 177 | // 思路: 178 | // 1. 快慢指针fast和slow,开始时,fast和slow都指向头节点,fast每次走两步,slow每次走一步 179 | // 2. 快指针向下移动的过程中,如果提前到达null,则链表无环,提前结束 180 | // 3. 如果该链表成环,那么fast和slow一定在环中的某个位置相遇 181 | // 4. 相遇后,立刻让fast回到head头结点,slow不动,fast走两步改为每次走一步。fast和slow共同向下滑动,再次相遇,就是成环节点 182 | 183 | func getLoopNode(head *Node) *Node { 184 | // 节点数目不足以成环,返回不存在成环节点 185 | if head == nil || head.Next == nil || head.Next.Next == nil { 186 | return nil 187 | } 188 | 189 | n1 := head.Next // slow指针 190 | n2 := head.Next.Next // fast指针 191 | 192 | for n1 != n2 { 193 | // 快指针提前到达终点,该链表无环 194 | if n2.Next == nil || n2.Next.Next == nil { 195 | return nil 196 | } 197 | 198 | n2 = n2.Next.Next 199 | n1 = n1.Next 200 | } 201 | 202 | // 确定成环,n2回到头节点 203 | n2 = head 204 | 205 | for n1 != n2 { 206 | n2 = n2.Next 207 | n1 = n1.Next 208 | } 209 | 210 | // 再次相遇节点,就是成环节点 211 | return n1 212 | } 213 | 214 | 215 | // 8、判断两个无环链表是否相交,相交则返回相交的第一个节点 216 | // 由于单链表,两个链表相交要不然两个无环链表相交,最后是公共部分;要不然两个链表相交,最后是成环部分. 217 | // 思路: 218 | // 1. 链表1从头结点遍历,统计长度,和最后节点end1 219 | // 2. 链表2从头结点遍历,统计长度,和最后节点end2 220 | // 3. 如果end1不等一end2则一定不相交,如果相等则相交,算长度差,长的链表遍历到长度差的长度位置,两个链表就汇合在该位置 221 | func noLoop(head1, head2 *Node) *Node { 222 | if head1 == nil || head2 == nil { 223 | return nil 224 | } 225 | 226 | cur1 := head1 227 | cur2 := head2 228 | n := 0 229 | 230 | for cur1.Next != nil { 231 | n++ 232 | cur1 = cur1.Next 233 | } 234 | 235 | for cur2.Next != nil { 236 | n-- 237 | cur2 = cur2.Next 238 | } 239 | 240 | // 最终没汇聚,说明两个链表不相交 241 | if cur1 != cur2 { 242 | return nil 243 | } 244 | 245 | if n <= 0 { 246 | cur1 = cur2 247 | } 248 | 249 | if cur1 == head1 { 250 | cur2 = head2 251 | } else { 252 | cur2 = head1 253 | } 254 | 255 | n = int(math.Abs(float64(n))) 256 | 257 | for n != 0 { 258 | n-- 259 | cur1 = cur1.Next 260 | } 261 | 262 | for cur1 != cur2 { 263 | cur1 = cur1.Next 264 | cur2 = cur2.Next 265 | } 266 | 267 | return cur1 268 | } 269 | 270 | // 9、合并两个有序链表 271 | func mergeTwoList(head1, head2 *Node) *Node { 272 | // base case 273 | if head1 == nil { 274 | return head2 275 | } 276 | if head2 == nil { 277 | return head1 278 | } 279 | 280 | var head *Node 281 | 282 | // 选出两个链表较小的头作为整个合并后的头结点 283 | if head1.Value <= head2.Value { 284 | head = head1 285 | } else { 286 | head = head2 287 | } 288 | 289 | // 链表1的准备合并的节点,就是头结点的下一个节点 290 | cur1 := head.Next 291 | // 链表2的准备合并的节点,就是另一个链表的头结点 292 | var cur2 *Node 293 | if head == head1 { 294 | cur2 = head2 295 | } else { 296 | cur2 = head1 297 | } 298 | 299 | // 最终要返回的头结点,预存为head,使用引用拷贝的pre向下移动 300 | pre := head 301 | for cur1 != nil && cur2 != nil { 302 | if cur1.Value <= cur2.Value { 303 | pre.Next = cur1 304 | // 向下滑动 305 | cur1 = cur1.Next 306 | } else { 307 | pre.Next = cur2 308 | // 向下滑动 309 | cur2 = cur2.Next 310 | } 311 | 312 | // pre向下滑动 313 | pre = pre.Next 314 | } 315 | 316 | // 有一个链表耗尽了,没耗尽的链表直接拼上 317 | if cur1 != nil { 318 | pre.Next = cur1 319 | } else { 320 | pre.Next = cur2 321 | } 322 | 323 | return head 324 | } 325 | ``` -------------------------------------------------------------------------------- /28-附:动态规划专题汇总.md: -------------------------------------------------------------------------------- 1 | ```Go 2 | package main 3 | 4 | import "math" 5 | 6 | // 1-1、🎒背包问题:给定两个长度都为N的数组weights和values,weight[i]和values[i]分别代表i号物品的重量和价值。 7 | // 给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少? 8 | // w 重量数组 9 | // v 价值数组 10 | // bag 背包的最大容量 11 | // 返回该背包所能装下的最大价值 12 | func getMaxValue(w []int, v []int, bag int) int { 13 | // 初始传入w,v。index位置开始,alreadyW表示在index位置的时候,重量已经到达了多少 14 | return processBag(w, v, 0, 0, bag) 15 | } 16 | 17 | // 递归的第一种尝试 18 | // 0..index-1上做了货物的选择,使得你已经达到的重量是多少 alreadyW 19 | // 如果返回-1,认为没有方案 20 | // 如果不返回-1,认为返回的值是真实价值 21 | func processBag(w []int, v []int, index int, alreadyW int, bag int) int { 22 | // base case 23 | if alreadyW > bag { 24 | return -1 25 | } 26 | 27 | // 重量没超 28 | if index == len(w) { 29 | return 0 30 | } 31 | 32 | // 当前不选择index的货物情况下,后续的价值 33 | // 无需传递当前index的重量,且p1就是总价值 34 | p1 := processBag(w, v, index+1, alreadyW, bag) 35 | // 当前选择了index的货物,把重量加上,继续向下递归 36 | p2next := processBag(w, v, index+1, alreadyW+w[index], bag) 37 | // p2表示要了当前货物之后总价值应该是后续价值加上当前价值 38 | p2 := -1 39 | if p2next != -1 { 40 | p2 = v[index] + p2next 41 | } 42 | 43 | return int(math.Max(float64(p1), float64(p2))) 44 | } 45 | 46 | // 1-2、背包问题的第二种递归解法。 47 | func maxValue(w []int, v []int, bag int) int { 48 | // 相比上一个暴力递归尝试,去掉了alreadyW。用背包剩余空间代替;rest表示背包剩余空间,初始剩余空间就是背包容量 49 | return processBag2(w, v, 0, bag) 50 | } 51 | 52 | func processBag2(w []int, v []int, index int, rest int) int { 53 | // base case 1 无效方案。背包剩余容量装不下当前重量的情况 54 | if rest < 0 { 55 | return -1 56 | } 57 | 58 | // rest >=0。index来到终止位置,没货物了,当前返回0价值 59 | // base case 2 60 | if index == len(w) { 61 | return 0 62 | } 63 | 64 | // 有货也有空间。当前index不选择,得到p1总价值 65 | p1 := processBag2(w, v, index+1, rest) 66 | p2 := -1 67 | // 选择了index位置,剩余空间减去当前重量 68 | p2Next := processBag2(w, v, index+1, rest-w[index]) 69 | // 选择index的总价值,是index...的价值加上个当前index的价值 70 | if p2Next != -1 { 71 | p2 = v[index] + p2Next 72 | } 73 | 74 | return int(math.Max(float64(p1), float64(p2))) 75 | } 76 | 77 | // 1-3、0-1背包问题:动态规划解决方案。在递归的思路上改进 78 | // 以背包问题举例,我们每一个重量有要和不要两个选择,且都要递归展开。那么我们的递归时间复杂度尾O(2^N)。 79 | // 而记忆化搜索,根据可变参数得到的长为N价值为W的二维表,那么我们的时间复杂度为O(N*bag)。 80 | // 如果递归过程中状态转移有化简继续优化的可能,例如枚举。那么经典动态规划可以继续优化, 81 | // 否则记忆化搜索和动态规划的时间复杂度是一样的 82 | func dpWay(w []int, v []int, bag int) int { 83 | N := len(w) 84 | // 准备一张dp表,行号为我们的重量范围bag+1。列为我们的价值数目个数的范围N+1。dp数组装下所有的可能性。 85 | dp := make([][]int, N+1) 86 | for i := 0; i < N+1; i++ { 87 | dp[i] = make([]int, bag+1) 88 | } 89 | 90 | // 由于暴力递归中index==w.length的时候,总是返回0。所以: 91 | // dp[N][...] = 0。整形数组初始化为0,无需处理 92 | // 由于N行已经初始化为0,我们从N-1开始。填我们的dp表 93 | for index := N - 1; index >= 0; index-- { 94 | // 剩余空间从0开始,一直填写到bag 95 | for rest := 0; rest <= bag; rest++ { 96 | // 通过正常位置的递归处理。我们转而填写我们的dp表 97 | // 所以我们p1等于dp表的下一层向上一层返回 98 | p1 := dp[index+1][rest] 99 | p2 := -1 100 | // rest - w[index] 不越界 101 | if rest-w[index] >= 0 { 102 | p2 = v[index] + dp[index+1][rest-w[index]] 103 | } 104 | // p1和p2取最大值 105 | dp[index][rest] = int(math.Max(float64(p1), float64(p2))) 106 | } 107 | } 108 | // 最终返回dp表的0,bag位置,就是我们暴力递归的主函数调用 109 | return dp[0][bag] 110 | } 111 | 112 | // 2、最长递增子序列问题 113 | // 问题描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 114 | // 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 115 | // 例如:nums = [10,9,2,5,3,7,101,18], 返回结果是4。最长递增子序列是 [2,3,7,101],因此长度为 4 。 116 | func lengthOfLIS(nums []int) int { 117 | if len(nums) == 0 { 118 | return 0 119 | } 120 | 121 | dp := make([]int, len(nums)) 122 | 123 | dp[0] = 1 124 | // 全局最大 125 | max := 1 126 | 127 | for i := 1; i < len(nums); i++ { 128 | // 默认每个元素的dp[i]都为1,表示自己形成的递增子序列 129 | dp[i] = 1 130 | 131 | for j := 0; j < i; j++ { 132 | // 如果在当前位置的前面,存在一个比自己小的元素,该元素的dp[j]加上当前元素形成的新的dp[j] + 1比dp[i]大。更新这个dp[i]。否则不更新 133 | if nums[i] > nums[j] { 134 | dp[i] = int(math.Max(float64(dp[i]), float64(dp[j]+1))) 135 | } 136 | } 137 | 138 | // 最上层循环,每一轮检查是否需要更新全局max 139 | max = int(math.Max(float64(max), float64(dp[i]))) 140 | } 141 | return max 142 | } 143 | 144 | // 3、最大连续子数组的和(最大子序和) 145 | // 问题描述:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 146 | // 例如:nums = [-2,1,-3,4,-1,2,1,-5,4],返回6。连续子数组 [4,-1,2,1] 的和最大,为 6 。 147 | func maxSubArray(nums []int) int { 148 | if len(nums) == 0 { 149 | return 0 150 | } 151 | 152 | N := len(nums) 153 | // dp[i] 含义:子数组必须以i结尾的时候,所有可以得到的子数组中,最大累加和是多少? 154 | dp := make([]int, N) 155 | dp[0] = nums[0] 156 | // 记录全局最大的子数组的和 157 | max := dp[0] 158 | for i := 1; i < N; i++ { 159 | // 当前的值 160 | p1 := nums[i] 161 | // 当前的值和上一个位置的最大和累加 162 | p2 := nums[i] + dp[i - 1] 163 | // dp[i]等于,当前的值,和当前值与上一个位置最大和的累加,取大的 164 | dp[i] = int(math.Max(float64(p1), float64(p2))) 165 | // 判断是否要更新全局最大值 166 | max = int(math.Max(float64(max), float64(dp[i]))) 167 | } 168 | // 返回全局最大值 169 | return max 170 | } 171 | 172 | // 4、打家劫舍问题 173 | // 问题描述:你是一个专业的小偷,计划偷窃沿街的房屋。 174 | // 每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 175 | // 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 176 | // 示例输入:[1,2,3,1], 输出4;偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。 177 | func rob(nums []int) int { 178 | if len(nums) == 0 { 179 | return 0 180 | } 181 | 182 | dp := make([]int, len(nums)) 183 | 184 | for i := 0; i < len(nums); i++ { 185 | if i == 0 { 186 | dp[0] = nums[i] 187 | } 188 | if i == 1 { 189 | dp[1] = int(math.Max(float64(dp[0]), float64(nums[i]))) 190 | } 191 | if i > 1 { 192 | dp[i] = int(math.Max(float64(dp[i - 1]), float64(dp[i - 2] + nums[i]))) 193 | } 194 | } 195 | return dp[len(nums) - 1] 196 | } 197 | 198 | // 5、爬楼梯问题。 199 | // 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 200 | // 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 201 | func climbStairs(n int) int { 202 | if n == 0 { 203 | return 0 204 | } 205 | if n == 1 { 206 | return 1 207 | } 208 | if n == 2 { 209 | return 2 210 | } 211 | 212 | dp := make([]int, n + 1) 213 | dp[1] = 1 214 | dp[2] = 2 215 | for i := 3; i <= n; i++ { 216 | dp[i] = dp[i - 1] + dp[i - 2] 217 | } 218 | return dp[n] 219 | } 220 | 221 | // 6、两个字符串的最长公共子序列问题 222 | // 例如“ab1cd2ef345gh”和“opq123rs4tx5yz”的最长公共子序列为“12345”。即在两个字符串所有相等的子序列里最长的。所以返回子序列的长度5 223 | func lcse(str1 []byte, str2 []byte) int { 224 | if len(str1) == 0 { 225 | return 0 226 | } 227 | if len(str2) == 0 { 228 | return 0 229 | } 230 | 231 | dp := make([][]int, len(str1)) 232 | for i := 0; i < len(str1); i++ { 233 | dp[i] = make([]int, len(str2)) 234 | } 235 | if str1[0] == str2[0] { 236 | dp[0][0] = 1 237 | } else { 238 | dp[0][0] = 0 239 | } 240 | 241 | // 填第0列的所有值 242 | // 一旦st1r的i位置某个字符等于str2的0位置,那么之后都是1 243 | for i := 1; i < len(str1); i++ { 244 | flag := -1 245 | if str1[i] == str2[0] { 246 | flag = 1 247 | } else { 248 | flag = 0 249 | } 250 | dp[i][0] = int(math.Max(float64(dp[i - 1][0]), float64(flag))) 251 | } 252 | 253 | // 填第0行的所有值 254 | // 一旦str2的j位置某个字符等于str1的0位置,那么之后都是1 255 | for j := 1; j < len(str2); j++ { 256 | flag := -1 257 | if str1[0] == str2[j] { 258 | flag = 1 259 | } else { 260 | flag = 0 261 | } 262 | dp[0][j] = int(math.Max(float64(dp[0][j - 1]), float64(flag))) 263 | } 264 | 265 | for i := 1; i < len(str1); i++ { 266 | for j := 1; j < len(str2); j++ { 267 | // dp[i - 1][j]表示可能性2 268 | // dp[i][j - 1] 表示可能性3 269 | dp[i][j] = int(math.Max(float64(dp[i - 1][j]), float64(dp[i][j - 1]))) 270 | if str1[i] == str2[j] { 271 | dp[i][j] = int(math.Max(float64(dp[i][j]), float64(dp[i - 1][j - 1] + 1))) 272 | } 273 | } 274 | } 275 | return dp[len(str1) - 1][len(str2) - 1] 276 | } 277 | ``` 278 | 279 | -------------------------------------------------------------------------------- /01-复杂度、排序、二分、异或.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 时间复杂度、空间复杂度、排序、异或运算 3 | ## 1.1 时间复杂度 4 | - 常数时间操作: 5 | 1. 算数运算:`+` `-` `*` `/` 6 | 2. 位运算:`>>`(带符号右移动)、 `>>>`(不带符号右移动) 、 `<<`、 `|` 、`&` 、`^` 7 | 8 | > 带符号就是最高位补符号位,不带符号就是最高位补0 9 | 10 | 3. 赋值操作:比较,自增,自减操作 11 | 4. 数组寻址等 12 | 13 | > 总之,执行时间固定的操作都是常数时间的操作。反之执行时间不固定的操作,都不是常数时间的操作 14 | 15 | - 通过基本动作的常数时间,推导时间复杂度 16 | 17 | > 对于双层循环来说,n*(常数)+ (n-1)*(常数)+ ... + 2*(常数) + 1*(常数) => 推导出 18 | 19 | ```math 20 | y = an^2 + bn + c 21 | ``` 22 | 23 | > 忽略掉低阶项,忽略掉常数项,忽略掉高阶项的系数,得到时间复杂度为n^2 24 | 25 | ### 1.1.1 排序操作 26 | --- 27 | #### 1.1.1.1 选择排序 28 | ```Go 29 | package main 30 | 31 | import "fmt" 32 | 33 | func main() { 34 | arr := []int{1, 4, 5, 8, 3, 1, 0, 22, 53, 21} 35 | selectionSort(arr) 36 | fmt.Println(arr) 37 | } 38 | 39 | // 选择排序 40 | func selectionSort(arr []int) { 41 | if len(arr) == 0 || len(arr) < 2 { 42 | return 43 | } 44 | 45 | // 在i到n-1的位置依次处理, i从0开始 46 | for i := 0; i < len(arr)-1; i++ { 47 | minIndex := i 48 | // 寻找当前剩余元素的最小值放在当前位置 49 | for j := i + 1; j < len(arr); j++ { 50 | if arr[j] < arr[minIndex] { 51 | minIndex = j 52 | } 53 | } 54 | swap(arr, i, minIndex) 55 | } 56 | } 57 | 58 | func swap(arr []int, a, b int) { 59 | tmp := arr[a] 60 | arr[a] = arr[b] 61 | arr[b] = tmp 62 | } 63 | ``` 64 | --- 65 | #### 1.1.1.2 冒泡排序 66 | ```Go 67 | // 冒泡排序 68 | func bubbleSort(arr []int) { 69 | if len(arr) == 0 || len(arr) < 2 { 70 | return 71 | } 72 | 73 | // 外循环从最大位置开始处理,形成冒泡的效果 74 | for i := len(arr) - 1; i > 0; i-- { 75 | // 内循环的一轮次,搞定外循环的一个位置。 76 | for j := 0; j < i; j ++ { 77 | if arr[j] > arr[i] { 78 | swap(arr, i, j) 79 | } 80 | } 81 | } 82 | } 83 | ``` 84 | --- 85 | #### 1.1.1.3 插入排序 86 | ```Go 87 | // 选择排序 88 | func insertionSort(arr []int) { 89 | if len(arr) == 0 || len(arr) < 2 { 90 | return 91 | } 92 | 93 | // 类比打扑克牌 94 | for i := 1; i < len(arr); i++ { 95 | // 每一轮内循环,与前面的元素来一轮比较,达到的效果是最小元素经过一轮内循环总能放到0位置 96 | for j := i - 1; j >=0 ; j-- { 97 | if arr[j] > arr[j + 1] { 98 | swap(arr, j, j + 1) 99 | } 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | > 插入排序和前面两种排序的不同是在于,插入排序跟数组初始顺序有关,在初始有序的情况下,有可能时间复杂度为O(N),有可能为O(N ^2),但是我们估计时间复杂度要按照最差的情况来估计,所以插入排序的时间复杂度仍然O(N ^2) 106 | 107 | ## 1.2 空间复杂度 108 | 109 | 申请有限几个变量,和样本量n没关系,就是空间复杂度O(1),如果要开辟一个空间数组和样本量n是一样大,用来支持我们的算法流程那么O(N)。反之用户就是要实现数组拷贝,我们开辟一个新的n大小数组用来支撑用户的需求,那么仍然是O(1) 110 | 111 | 112 | ## 1.3 常数项时间复杂度 113 | 114 | 如果两个相同时间复杂度的算法要比较性能,这个时候需要比较单个常数项时间,对能力要求较高,没有意义,不如样本量试验实际测试来比较 115 | 116 | ## 1.4 算法最优解 117 | 118 | 我们认为最优解的考虑顺序是,先满足时间复杂度指标,再去使用较少的空间。一般来说,算法题,ACM等不会卡常数项时间 119 | 120 | ## 1.5 常见时间复杂度 121 | > 依次从好到坏 O(1) -> O(logN) -> O(N) -> O(N*logN) -> O(N^2) -> O(N^3) ... -> O(N!) 122 | 123 | ## 1.6 算法和数据结构脉络 124 | 125 | 1. 知道怎么算的算法 126 | 2. 知道怎么试的算法(递归) 127 | 128 | ## 1.7 认识对数器 129 | 130 | 1. 准备你想要测试的方法a 131 | 2. 实现一个复杂度不好,但是容易实现的方法b 132 | 3. 实现一个随机样本产生器 133 | 4. 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样 134 | 5. 如果有一个随机样本使得对比结果不一致,打印样本进行人工干预,改对方法a和方法b 135 | 6. 当样本数量很多,测试对比依然正确,可以确定方法a已经正确 136 | 137 | ## 1.8 认识二分法 138 | 139 | 1. 在一个有序数组中,找某个数是否存在 140 | 141 | > 二分查找值,基于有序数组,算法复杂度为二分了多少次,O(log2N)可以写成O(logN) 142 | 143 | ```Go 144 | // 有序数组二分查找 145 | func exist(arr []int, target int) bool { 146 | if len(arr) == 0 { 147 | return false 148 | } 149 | 150 | var L = 0 151 | var R = len(arr) - 1 152 | var mid = 0 153 | for L < R { 154 | // 防止整数越界 155 | mid = L + (R - L) / 2 156 | if arr[mid] == target { 157 | return true 158 | } else if arr[mid] < target { // mid位置已经比较过,L置为mid+1 159 | L = mid + 1 160 | } else if arr[mid] > target { // mid位置已经比较过,R置为mid-1 161 | R = mid - 1 162 | } 163 | } 164 | return arr[L] == target 165 | } 166 | ``` 167 | 168 | 2. 在一个有序数组中,找>=某个数最左侧的位置 169 | 170 | > 122222333578888999999 找大于等于2最左侧的位置 171 | 172 | ```Go 173 | // 在一个有序数组上,找到大于等于value的最左位置 174 | func nearestLeftIndex(arr []int, value int) int { 175 | L := 0 176 | R := len(arr) - 1 177 | index := -1 178 | for L <= R { 179 | mid := L + (R - L) / 2 180 | // 当前中间位置大于value,寻找大于value最左位置,缩小右边界,继续寻找 181 | if arr[mid] >= value { 182 | index = mid 183 | R = mid - 1 184 | } else { 185 | L = mid + 1 186 | } 187 | } 188 | return index 189 | } 190 | ``` 191 | 192 | 3. 在一个有序数组中,找小于等于某个数最右侧的位置 193 | 194 | ```Go 195 | // 在一个有序数组上,找到小于等于value的最右位置 196 | func nearestRightIndex(arr []int, value int) int { 197 | L := 0 198 | R := len(arr) - 1 199 | index := -1 200 | for L <= R { 201 | mid := L + (R - L) / 2 202 | if arr[mid] <= value { 203 | index = mid 204 | L = mid + 1 205 | } else { 206 | L = mid - 1 207 | } 208 | } 209 | return index 210 | } 211 | ``` 212 | 213 | ## 1.9 认识异或运算 214 | 215 | 异或运算:相同为0,不同为1 216 | 217 | 同或运算:相同为1, 不同为0,不掌握 218 | 219 | >上述规则不容易记住,异或运算就记成无进位相加:比如十进制6异或7,就理解为110和111按位不进位相加,得到001 220 | 221 | 1. 所以 0^N = N , N^N = 0 222 | 2. 异或运算满足交换律和结合律,所以A异或B异或C = A异或(B异或C) = (A异或C)异或B 223 | 224 | **题目一:如何不用额外变量就交换两个数** 225 | 226 | ```shell 227 | # 三步操作,实现交换ab的值 228 | # a = x b = y两个数交换位置 229 | 230 | a = a ^ b # 第一步操作,此时 a = x^y , b=y 231 | b = a ^ b # 第二步操作,此时 a = x^y , b = x^y^y => b = x^0 => b = x 232 | a = a ^ b # 第三步操作,此时 a = x^y^x, b = x, a=> x^x^y => a=y 233 | ``` 234 | 235 | ```Go 236 | // IsoOr 异或交换数据 237 | func IsoOr() { 238 | a := 3 239 | b := 4 240 | a = a ^ b 241 | b = a ^ b 242 | a = a ^ b 243 | fmt.Println(a) 244 | fmt.Println(b) 245 | 246 | arr := []int{3, 1, 100} 247 | IsoOrSwap(arr, 0, 3) 248 | fmt.Println(arr) 249 | 250 | // i和j指向同一块内存,这种位运算交换变量的方法就不可行了。 251 | IsoOrSwap(arr, 0, 0) 252 | } 253 | 254 | func IsoOrSwap(arr []int, i, j int) { 255 | arr[i] = arr[i] ^ arr[j] 256 | arr[j] = arr[i] ^ arr[j] 257 | arr[i] = arr[i] ^ arr[j] 258 | } 259 | ``` 260 | 261 | > 注意,如果a和b指向同一块内存,该交换方法不可行 262 | 263 | --- 264 | 265 | **题目二:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数** 266 | 267 | > [2,2,1,3,2,3,2,1,1] 数组中存在四个2,两个3,三个1,定义一个常量等于0,分别对该数组中的数遍历一遍进行异或,最后,该变量等于多少,那么奇数的值就是多少。因为异或运算满足交换和结合律 268 | 269 | ```Go 270 | package main 271 | 272 | import "fmt" 273 | 274 | func main() { 275 | arr := []int{2, 2, 1, 3, 2, 3, 2, 1, 1} 276 | flag := 0 277 | for i := 0; i < len(arr); i++ { 278 | flag = flag ^ arr[i] 279 | } 280 | 281 | fmt.Println(flag) 282 | } 283 | // 打印 1 284 | ``` 285 | 286 | --- 287 | 288 | **题目三:怎么把一个int类型的数,提取出最右侧的1来** 289 | 290 | > n与上(n取反加1)即可 => N & ( (~N)+1 ) 291 | 292 | 例如一个int数n:00000000 00000000 00001010 01000000 293 | 294 | 先对n取反得到: 11111111 11111111 11110101 10111111 295 | 296 | 再加1得到: 11111111 11111111 11110101 11000000 297 | 298 | 再与原n做与运算:11111111 11111111 11110101 11000000 & 00000000 00000000 00001010 01000000 299 | 300 | 得到:00000000 00000000 00000000 01000000 301 | 302 | --- 303 | 304 | **题目四:一个数组中有两种不相等的数出现了奇数次,其他数出现了偶数次,怎么找到并打印这两种数** 305 | 306 | 思路:定义一个常量flag = 0,分别对该数组每个数异或,最终结果为a异或b,其中a和b就是这两个奇数,由于a!=b所以a异或b不等于0,即flag的值某一位上一定为1(有可能不止一个1随便选一个例如第八位),用该位做标记对原有数组的数进行分类,那么a和b由于第八位不相同一定被分开,再定义常量flag' = 0分别对第八位为0的数异或,那么得到的值,就是a和b其中一个,由于之前flag = a异或b,那么在用flag和flag'异或,就是另外一个值。一般来说,随便找一个1我们就找最右侧的那个1,如题目三 307 | 308 | 309 | ```Go 310 | package main 311 | 312 | import "fmt" 313 | 314 | func main() { 315 | arr := []int{4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2} 316 | printNum(arr) 317 | } 318 | 319 | func printNum(arr []int) { 320 | flag := 0 321 | // 经过循环处理,flag等于这两个不相等的且出现奇数次的异或结果;a ^ b 322 | for i := 0; i < len(arr); i++ { 323 | flag = flag ^ arr[i] 324 | } 325 | 326 | // 由于a != b 所以flag不为0。则flag的二进制位上一定存在1,选最后一位的1 327 | // 选取办法是,用flag 与 自身取反加1的结果做与运算 328 | rightOne := flag & ((^flag) + 1) 329 | onlyOne := 0 330 | // 经过这层循环的筛选,onlyOne等于a或者b其中的一个 331 | for j := 0; j < len(arr); j++ { 332 | if arr[j]&rightOne != 0 { 333 | onlyOne = onlyOne ^ arr[j] 334 | } 335 | } 336 | result1 := onlyOne 337 | result2 := flag ^ onlyOne 338 | // result1和result2就是数组中不相等的且为奇数的两个未知数a、b 339 | fmt.Println(result1) 340 | fmt.Println(result2) 341 | } 342 | ``` 343 | -------------------------------------------------------------------------------- /17-《进阶》Morris遍历.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 Morris遍历 4 | 5 | ## 1.1 Morris遍历目的 6 | 7 | 在二叉树的遍历中,有递归方式遍历和非递归遍历两种。不管哪种方式,时间复杂度为O(N),空间复杂度为O(h),h为树的高度。Morris遍历可以在时间复杂度O(N),空间复杂度O(1)实现二叉树的遍历 8 | 9 | 10 | ### 算法流程 11 | 12 | 从一个树的头结点cur开始: 13 | 14 | 1、cur的左树为null,cur = cur.right 15 | 16 | 2、cur有左树,找到左树的最右的节点mostRight 17 | 18 | - mostRight的右孩子指向null,让mostRigth.right = cur;cur = cur.left 19 | - mostRight的右孩子指向当前节点cur,让mostRigth.right = null;cur = cur.rigth 20 | - cur为null的时候,整个流程结束 21 | 22 | cur经过的各个节点,称为Morris序,例如如下的树结构: 23 | 24 | ``` 25 | graph TD 26 | 1-->2 27 | 1-->3 28 | 2-->4 29 | 2-->5 30 | 3-->6 31 | 3-->7 32 | ``` 33 | 34 | cur经过的路径也就是Morris序为:1,2,4,2,5,1,3,6,3,7 35 | 36 | 可以发现,只要当前节点有左孩子,当前节点会来到两次。当左树最右节点的右孩子指向null,可以判定第一次来到cur节点,下一次来到cur就是发现左树的最右节点指向的是自己,说明第二次来到cur节点 37 | 38 | 在Morris遍历的过程中,第一次到达cur就打印(只会来cur一次的节点也打印)就是树的先序遍历,第二次到达(只会来到cur一次的节点也打印)打印为中序。第二次来到cur节点的时候逆序打印cur左树的右边界,最后逆序打印整颗树的右边界,逆序打印右边界不可以使用额外的结构,因为我们要求空间复杂度为O(1),可以使用翻转链表 39 | 40 | 翻转链表:例如a->b->c->d->null。可以让a-null,b->a,c->b,d->c即可。打印完之后把链表再翻转过来 41 | 42 | ### 时间复杂度估计 43 | 44 | cur来到节点的时间复杂度为O(N),每个cur遍历左树最右边界的代价最多为O(2N),所以整个遍历过程为O(N),整个遍历过程只用到到有限的几个变量,其他使用的都是树本身的指针关系。 45 | 46 | 47 | 48 | ```Go 49 | package main 50 | 51 | import ( 52 | "fmt" 53 | ) 54 | 55 | type Node struct { 56 | Left *Node 57 | Right *Node 58 | Val int 59 | } 60 | 61 | // morris遍历二叉树,实现时间复杂度O(N), 空间复杂度O(1)。正常的递归遍历,时间复杂度O(N),空间复杂度O(N) 62 | func morris(head *Node) { 63 | if head == nil { 64 | return 65 | } 66 | 67 | cur := head 68 | var mostRight *Node 69 | for cur != nil { 70 | // cur有没有左树 71 | mostRight = cur.Left 72 | if mostRight != nil { // 有左树的情况下 73 | // 找到cur左树上,真实的最右 74 | for mostRight.Right != nil && mostRight.Right != cur { 75 | mostRight = mostRight.Right 76 | } 77 | 78 | // 从while中出来,mostRight一定是cur左树上的最右节点 79 | // mostRight如果等于null,说明第一次来到自己 80 | if mostRight.Right == nil { 81 | mostRight.Right = cur 82 | cur = cur.Left 83 | continue 84 | } else { 85 | // 否则第二次来到自己,意味着mostRight.right = cur 86 | // mostRight.right != null -> mostRight.right == cur 87 | mostRight.Right = nil 88 | } 89 | } 90 | cur = cur.Right 91 | } 92 | } 93 | 94 | // morris 先序遍历二叉树 95 | func morrisPre(head *Node) { 96 | if head == nil { 97 | return 98 | } 99 | 100 | // cur 101 | cur1 := head 102 | // mostRight 103 | var cur2 *Node 104 | 105 | for cur1 != nil { 106 | cur2 = cur1.Left 107 | if cur2 != nil { 108 | for cur2.Right != nil && cur2.Right != cur1 { 109 | cur2 = cur2.Right 110 | } 111 | if cur2.Right == nil { 112 | cur2.Right = cur1 113 | fmt.Print(fmt.Sprintf("%d%s", cur1.Val, " ")) 114 | cur1 = cur1.Left 115 | continue 116 | } else { 117 | cur2.Right = nil 118 | } 119 | } else { 120 | fmt.Print(fmt.Sprintf("%d%s", cur1.Val, " ")) 121 | } 122 | cur1 = cur1.Right 123 | } 124 | fmt.Println() 125 | } 126 | 127 | // morris 中序遍历 128 | func morrisIn(head *Node) { 129 | if head == nil { 130 | return 131 | } 132 | 133 | cur := head 134 | var mostRight *Node 135 | for cur != nil { 136 | mostRight = cur.Left 137 | if mostRight != nil { 138 | for mostRight.Right != nil && mostRight.Right != cur { 139 | mostRight = mostRight.Right 140 | } 141 | if mostRight.Right == nil { 142 | mostRight.Right = cur 143 | cur = cur.Left 144 | continue 145 | } else { 146 | mostRight.Right = nil 147 | } 148 | } 149 | fmt.Print(fmt.Sprintf("%d%s", cur.Val, " ")) 150 | cur = cur.Right 151 | } 152 | fmt.Println() 153 | } 154 | 155 | // morris 后序遍历 156 | func morrisPos(head *Node) { 157 | if head == nil { 158 | return 159 | } 160 | 161 | cur := head 162 | var mostRight *Node 163 | for cur != nil { 164 | mostRight = cur.Left 165 | if mostRight != nil { 166 | for mostRight.Right != nil && mostRight.Right != cur { 167 | mostRight = mostRight.Right 168 | } 169 | if mostRight.Right == nil { 170 | mostRight.Right = cur 171 | cur = cur.Left 172 | continue 173 | } else { // 回到自己两次,且第二次回到自己的时候是打印时机 174 | mostRight.Right = nil 175 | // 翻转右边界链表,打印 176 | printEdge(cur.Left) 177 | } 178 | } 179 | cur = cur.Right 180 | } 181 | // while结束的时候,整颗树的右边界同样的翻转打印一次 182 | printEdge(head) 183 | fmt.Println() 184 | } 185 | 186 | func printEdge(head *Node) { 187 | tali := reverseEdge(head) 188 | cur := tali 189 | for cur != nil { 190 | fmt.Print(fmt.Sprintf("%d%s", cur.Val, " ")) 191 | cur = cur.Right 192 | } 193 | reverseEdge(tali) 194 | } 195 | 196 | func reverseEdge(from *Node) *Node { 197 | var pre *Node 198 | var next *Node 199 | for from != nil { 200 | next = from.Right 201 | from.Right = pre 202 | pre = from 203 | from = next 204 | } 205 | return pre 206 | } 207 | 208 | // 在Morris遍历的基础上,判断一颗树是不是一颗搜索二叉树 209 | // 搜索二叉树是左比自己小,右比自己大 210 | // 一颗树中序遍历,值一直在递增,就是搜索二叉树 211 | func isBST(head *Node) bool { 212 | if head == nil { 213 | return true 214 | } 215 | 216 | cur := head 217 | var mostRight *Node 218 | var pre int 219 | var ans bool 220 | for cur != nil { 221 | mostRight = cur.Left 222 | if mostRight != nil { 223 | for mostRight.Right != nil && mostRight.Right != cur { 224 | mostRight = mostRight.Right 225 | } 226 | if mostRight.Right == nil { 227 | mostRight.Right = cur 228 | cur = cur.Left 229 | continue 230 | } else { 231 | mostRight.Right = nil 232 | } 233 | } 234 | if pre >= cur.Val { 235 | ans = false 236 | } 237 | pre = cur.Val 238 | cur = cur.Right 239 | } 240 | return ans 241 | } 242 | ``` 243 | 244 | 245 | ## 1.2 Morris遍历的应用 246 | 247 | 在一颗二叉树中,求该二叉树的最小高度。最小高度指的是,所有叶子节点距离头节点的最小值 248 | 249 | > 二叉树递归求解,求左树的最小高度加1和右树的最小高度加1,比较 250 | 251 | > Morris遍历求解,每到达一个cur的时候,记录高度。每到达一个cur的时候判断cur是否为叶子节点,更新全局最小值。最后看一下最后一个节点的高度和全局最小高度对比,取最小高度 252 | 253 | ```Go 254 | package main 255 | 256 | import "math" 257 | 258 | type Node struct { 259 | Left *Node 260 | Right *Node 261 | Val int 262 | } 263 | 264 | // 求二叉树最小高度;解法1 运用二叉树的递归 265 | func minHeight1(head *Node) int { 266 | if head == nil { 267 | return 0 268 | } 269 | return p(head) 270 | } 271 | 272 | func p(x *Node) int { 273 | if x.Left == nil && x.Right == nil { 274 | return 1 275 | } 276 | 277 | // 左右子树起码有一个不为空 278 | leftH := math.MaxInt 279 | if x.Left != nil { 280 | leftH = p(x.Left) 281 | } 282 | 283 | rightH := math.MaxInt 284 | if x.Right != nil { 285 | rightH = p(x.Right) 286 | } 287 | 288 | return 1 + int(math.Min(float64(leftH), float64(rightH))) 289 | } 290 | 291 | // 解法2 根据morris遍历改写 292 | func minHeight2(head *Node) int { 293 | if head == nil { 294 | return 0 295 | } 296 | 297 | cur := head 298 | var mostRight *Node 299 | curLevel := 0 300 | minnHeight := math.MaxInt 301 | for cur != nil { 302 | mostRight = cur.Left 303 | if mostRight != nil { 304 | rightBoardSize := 1 305 | for mostRight.Right != nil && mostRight.Right != cur { 306 | rightBoardSize++ 307 | mostRight = mostRight.Right 308 | } 309 | 310 | if mostRight.Right == nil { // 第一次到达 311 | curLevel++ 312 | mostRight.Right = cur 313 | cur = cur.Left 314 | continue 315 | } else { // 第二次到达 316 | if mostRight.Left == nil { 317 | minnHeight = int(math.Min(float64(minnHeight), float64(minnHeight))) 318 | } 319 | curLevel -= rightBoardSize 320 | mostRight.Right = nil 321 | } 322 | } else { // 只有一次到达 323 | curLevel++ 324 | } 325 | cur = cur.Right 326 | } 327 | 328 | finalRight := 1 329 | cur = head // 回到头结点 330 | for cur.Right != nil { 331 | finalRight++ 332 | cur = cur.Right 333 | } 334 | if cur.Left == nil && cur.Right == nil { 335 | minnHeight = int(math.Min(float64(minnHeight), float64(finalRight))) 336 | } 337 | return minnHeight 338 | } 339 | ``` 340 | 341 | ## 1.3 Morris遍历为最优解的情景 342 | 343 | > 如果我们算法流程设计成,每来到一个节点,需要左右子树的信息进行整合,那么无法使用Morris遍历。该种情况的空间复杂度也一定不是O(1)的 344 | 345 | > 如果算法流程是,当前节点使用完左树或者右树的信息后,无需整合,那么可以使用Morris遍历 346 | 347 | > Morris遍历属于比较复杂的问题 348 | -------------------------------------------------------------------------------- /14-《进阶》斐波那契数列相关的递归.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 类似斐波那契数列的递归 3 | 4 | ## 1.1 求斐波那契数列矩阵乘法的方法 5 | 6 | 1、菲波那切数列的线性求解(O(N))的方法非常好理解(一次遍历,N项依赖于N-1项和N-2项) 7 | 8 | 2、同时利用线性代数,也可以改写出另外一种表示`|F(N),F(N-1)| = |F(2),F(1)| * 某个二阶矩阵的N-2次方` 9 | 10 | 3、求出这二阶矩阵,进而最快求出这个二阶矩阵的N-2次方 11 | 12 | 13 | ## 1.2 菲波那切数列可优化为O(logN)时间复杂度 14 | 15 | ### 1.2.1 矩阵加快速幂方法 16 | 17 | #### 1.2.1.1 矩阵推导 18 | 19 | > 由于菲波那切数列是一个严格的递推式,不会发生条件转移。 20 | 21 | > 在线性代数中,对于(严格递推式)菲波那切数列,第二项和第三项所组成的行列式,等于第二项和第一项组成的行列式,乘以某一个2乘2的矩阵 22 | 23 | > 同理第四项和第三项行列式等于第三项和第二项组成的行列式乘以相同的矩阵,其他同理 24 | 25 | ```math 26 | |F(3),F(2)| = |F(2),F(1)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 27 | 28 | |F(4),F(3)| = |F(3),F(2)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 29 | ``` 30 | 31 | 举例:例如1 1 2 3 5 8 ... 的菲波那切数列,我们带入公式 32 | 33 | ```math 34 | |2,1| = |1,1| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 35 | 36 | => 2 = 1 * a + 1 * c 37 | 38 | => 1 = 1 * b + 1 * d 39 | 40 | => a + c = 2; b + d = 1; 41 | 42 | 43 | |3,2| = |2,1| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 44 | 45 | => 2a + c = 3; 2b + d =2; 46 | 47 | 48 | => a=1; b=1; c=1; d=0; 49 | 50 | ``` 51 | 52 | 可以得到我们的相同二阶矩阵中,a=1; b=1; c=1; d=0; 53 | 54 | 55 | > 继续推导,由于我们的严格递推式满足,我们可以把f(3)f(2)的公式带入到f(4)f(3)中,以此替换到f(n)f(n-1) = f(n-1)f(n-2) * 固定矩阵中 56 | 57 | 继续推导得出: 58 | 59 | ```math 60 | |F(N) * F(N-1)| = |F(2)F(1)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\}^{n-2} 61 | ``` 62 | 63 | > 由于菲波那切数列第二项和第一项是1,1,那么化简后,我们可以得到:f(n)和f(n-1)构成的行列式,等于1,1的行列式,乘以1110构成的矩阵的n-1次方。算该矩阵的n-2次方直接影响我们的算法复杂度。 64 | 65 | > 转化为,怎么求多个相同矩阵乘起来,怎么算比较快的问题?我们先思考怎么求一个数乘方怎么算比较快的问题? 66 | 67 | #### 1.2.1.2 快速幂转化思路 68 | 69 | 问:怎么快速求出10的75次方的值是多少。 70 | 71 | > 思路:利用一种精致的二分来求,75次方我们拆分成二进制形式,1001011 72 | 73 | ```math 74 | 10^{75} = 10^{64} * 10^8 * 10^2 * 10^1 75 | ``` 76 | 77 | 我们先从10的1次方开始,用tmp中间变量接收,依次和我们指数的二进制做比较。如果指数的二进制某一位为1,我们就要乘我们的t当成我们的result(初始为1),如果为0,我们就不乘。tmp每次判断结束和自身相乘,生成新的tmp。最终result就是我们的结果 78 | 79 | > 10的75次方推演为:tmp为10开始,对比二进制,需要乘进result。tmp和自己相乘变为10^2,仍然需要,result为1乘10乘10的平方乘10的八次方...。tmp虽然在变化,但我们不是都选择相乘 80 | 81 | 82 | 所以上述矩阵次方的乘法,我们可以类似处理。把指数变为二进制。result初始值为单位矩阵,就是对角线全为1的矩阵。其他处理和数字次方的处理类似 83 | 84 | 85 | **在JDK中,Math.power函数的内部,也是这样实现指数函数的(整数次方)** 86 | 87 | 88 | ## 1.3 递推推广式 89 | 90 | 如果某一个递推式,`F(N) = c1F(n-1) + C2F(n-2) + ... + czF(N-k)` k表示最底层减到多少,我们称之为k阶递归式。c1,c2,cz为常数系数,k为常数,那么都有O(logN)的解。系数只会影响到我们的k阶矩阵的不同 91 | 92 | 奶牛问题: 93 | 94 | 一个农场第一年有一只奶牛A,每一只奶牛每年会产一只小奶牛。假设所有牛都不会死,且小牛需要三年,可以产小奶牛。求N年后牛的数量。 95 | 96 | > 思路:牛的数量的轨迹为:1,2,3,4,6,9...。`F(N) = F(N-1) + F(N-3)` 既今年的牛F(N)等于去年的牛F(N-1) + 三年前牛的数量F(N-3)。三阶问题k=3。 97 | 98 | > 套上述公式为: `F(N) = 1 * F(N-1) + 1 * 0 * F(N-2) + 1 * F(N-3)` 99 | 100 | > 对于三阶问题,可以写成:**`|F(n)F(n-1)F(n-2)| = |F3F2F1| * |3*3|^{n-3}`** 101 | 102 | 通用的表达式如下,同理根据前几项,求出原始矩阵。 103 | 104 | ``` 105 | |F(n)F(n-1)...F(n-k+1)| = |Fk...F2F1| * |k*k|^{n-k} 106 | ``` 107 | 108 | 109 | ```Go 110 | package main 111 | 112 | import "fmt" 113 | 114 | // 斐波那契数列暴力解法 115 | func f1(n int) int { 116 | if n < 1 { 117 | return 0 118 | } 119 | 120 | if n == 1 || n == 2 { 121 | return 1 122 | } 123 | 124 | return f1(n-1) + f1(n-2) 125 | } 126 | 127 | // 斐波那契数列线性求解方法 128 | func f2(n int) int { 129 | if n < 1 { 130 | return 0 131 | } 132 | 133 | if n == 1 || n == 2 { 134 | return 1 135 | } 136 | 137 | res := 1 138 | pre := 1 139 | tmp := 0 140 | 141 | for i := 1; i <= n; i++ { 142 | tmp = res 143 | res = res + pre 144 | pre = tmp 145 | } 146 | 147 | return res 148 | } 149 | 150 | // 斐波那契矩阵加快速幂O(logN)方法 151 | func f3(n int) int { 152 | if n < 1 { 153 | return 0 154 | } 155 | 156 | if n == 1 || n == 2 { 157 | return 1 158 | } 159 | 160 | // [ 1 ,1 ] 161 | // [ 1, 0 ] 162 | base := [][]int{ 163 | {1, 1}, 164 | {1, 0}, 165 | } 166 | // 求出base矩阵的n-2次方,得到的矩阵返回 167 | res := matrixPower(base, n-2) 168 | // 最终通过单位矩阵乘以该res,矩阵运算后返回fn的值。 169 | // 得到xyzk组成的矩阵 170 | // f(n)F(n-1) = {1, 0} * {x, y} 171 | // {0, 1} {z, k} 172 | // 推导出fn = x + z 173 | return res[0][0] + res[1][0] 174 | } 175 | 176 | // 快速求一个矩阵m的p次方 177 | func matrixPower(m [][]int, p int) [][]int { 178 | res := make([][]int, len(m)) 179 | for i := 0; i < len(res); i++ { 180 | res[i] = make([]int, len(m[0])) 181 | } 182 | 183 | for i := 0; i < len(res); i++ { 184 | // 单位矩阵,对角线都是1。相当于矩阵概念中的'1' 185 | res[i][i] = 1 186 | } 187 | 188 | // res = 矩阵中的1 189 | tmp := m // 矩阵1次方 190 | // 基于次方的p,做位运算。右移 191 | for ; p != 0; p >>= 1 { 192 | // 右移之后的末位不是0,才乘当前的tmp 193 | if (p & 1) != 0 { 194 | res = muliMatrix(res, tmp) 195 | } 196 | // 自己和自己相乘,得到下一个tmp 197 | tmp = muliMatrix(tmp, tmp) 198 | } 199 | return res 200 | } 201 | 202 | // 两个矩阵乘完之后的结果返回 203 | func muliMatrix(m1 [][]int, m2 [][]int) [][]int { 204 | res := make([][]int, len(m1)) 205 | for i := 0; i < len(res); i++ { 206 | res[i] = make([]int, len(m2[0])) 207 | } 208 | 209 | for i := 0; i < len(m1); i++ { 210 | for j := 0; j < len(m2[0]); j++ { 211 | for k := 0; k < len(m2); k++ { 212 | res[i][j] += m1[i][k] * m2[k][j] 213 | } 214 | } 215 | } 216 | return res 217 | } 218 | 219 | // 奶牛问题O(logN) 220 | func s3(n int) int { 221 | if n < 1 { 222 | return 0 223 | } 224 | 225 | if n == 1 || n == 2 { 226 | return n 227 | } 228 | 229 | base := [][]int{ 230 | {1, 1}, 231 | {1, 0}, 232 | } 233 | 234 | res := matrixPower(base, n-2) 235 | return 2*res[0][0] + res[1][0] 236 | } 237 | 238 | // 奶牛问题暴力递归 239 | func c1(n int) int { 240 | if n < 1 { 241 | return 0 242 | } 243 | if n == 1 || n == 2 || n == 3 { 244 | return n 245 | } 246 | 247 | return c1(n-1) + c1(n-3) 248 | } 249 | 250 | // 奶牛问题线性解 251 | func c2(n int) int { 252 | if n < 1 { 253 | return 0 254 | } 255 | if n == 1 || n == 2 || n == 3 { 256 | return n 257 | } 258 | res := 3 259 | pre := 2 260 | prepre := 1 261 | tmp1 := 0 262 | tmp2 := 0 263 | for i := 4; i <= n; i++ { 264 | tmp1 = res 265 | tmp2 = pre 266 | res = res + prepre 267 | pre = tmp1 268 | prepre = tmp2 269 | } 270 | return res 271 | } 272 | 273 | // 奶牛问题矩阵解 274 | func c3(n int) int { 275 | if n < 1 { 276 | return 0 277 | } 278 | if n == 1 || n == 2 || n == 3 { 279 | return n 280 | } 281 | 282 | // 原始矩阵 283 | base := [][]int{ 284 | {1, 1, 0}, 285 | {0, 0, 1}, 286 | {1, 0, 0}, 287 | } 288 | res := matrixPower(base, n - 3) 289 | return 3 * res[0][0] + 2 * res[1][0] + res[2][0] 290 | } 291 | 292 | func main() { 293 | n := 19 294 | fmt.Println(f1(n)) 295 | fmt.Println(f2(n)) 296 | fmt.Println(c1(n)) 297 | fmt.Println(c3(n)) 298 | } 299 | ``` 300 | 301 | ## 1.4 迈楼梯问题 302 | 303 | 问题描述:小明想要迈到n层台阶上去,可以一次迈一层台阶,可以一次迈两层台阶。问:迈到n层一共可以有多少种方法 304 | 305 | > 思路:1层台阶的时候,只有一种方法。2层台阶的时候迈两次一步1中,一次迈两步1种共两种方法。n层台阶等于迈到n-1层的方法数,加上迈到n-2层台阶的方法数 306 | 307 | ```math 308 | F(N) = F(n-1) + F(n-2) 309 | ``` 310 | 311 | **二阶递推式,类似菲波那切数列问题,区别在与斐波那契数列的初始值为1,1,而本题的初始值为1,2而已** 312 | 313 | 314 | 问题升级:同样条件,可以迈1步,可以迈2步,可以迈5步,问迈到n层台阶,有多少种方法 315 | 316 | 317 | ```math 318 | F(N) = F(n-1) + F(n-2) + F(n-5) 319 | ``` 320 | 321 | **五阶递推,求5乘5的原始矩阵即可,拿到矩阵表达式** 322 | 323 | 324 | 问题升级:奶牛问题,假设原有条件不变的情况下,奶牛寿命为10年 325 | 326 | ```math 327 | F(N) = F(n-1) + F(n-3) - F(n-10) 328 | ``` 329 | 330 | **十阶递推,求10乘10的原始矩阵,拿到矩阵关系表达式** 331 | 332 | 每年,这种问题在面试笔试中大量出现,兔子生乌龟问题,乌龟生兔子问题,等等,层出不穷,都可以用这种方法模型求解 333 | 334 | ## 1.5 递推经典例题一 335 | 336 | 题目描述:给定一个数N,想象只有0和1两种字符,组成的所有长度为N的字符串。如果某个字符串,任何0字符的左边都有1紧挨着,认为这个字符串达标,返回有多少达标的字符串 337 | 338 | 339 | > 思路:N=1时,有0和1组成的01字符只有0或者1。单个字符'0'不达标,只有'1'一种达标的字符串 340 | 341 | > N=1时,有0和1组成的01字符有四种'00','01','10','11'。'10'和'11'达标,两个 342 | 343 | > N=3时,有0和1组成的01字符有八种。达标的为'101','110','111'三个 344 | 345 | > 暴力解为O(2^n * n)。优化:我们定义一个i长度的字符串,该字符串最左边一定是'1'。在i长度上自由变换,最终有多少个达标的。如果n=8,由于最左侧已经规定了'1',我们调用f(7),在这7个位置的第一个位置判断,如果1位置是0,2位置必定不能为0,为1。那么f(i-2)长度可以任意变。如果第一个字符填的是1,那么f(i-1)可以随意变。衍生出菲波那切数列的递推公式;**二阶递推** 346 | 347 | 348 | ```math 349 | f(i) = f(i-1) + f(i-2) 350 | ``` 351 | 352 | ## 1.6 递推经典例题二 353 | 354 | 题目描述:有一款2行,N列的区域。假设我们只有1*2大小的瓷砖,我们把该区域填满有多少种方案? 355 | 356 | > 思路:定义F(N)函数,返回2*N区域都没贴瓷砖的会有多少种方法数。 357 | 358 | > 第一块瓷砖竖着拜,剩下的区域有F(N-1)种,第一块瓷砖横着摆,第一块瓷砖下方区域只能横着摆,剩下的方法数自由摆放为F(N-2) 359 | 360 | ```math 361 | F(N) = F(N-1) + F(N-2) 362 | ``` 363 | 364 | **仍然是一个二阶递推式,菲波那切数列问题** 365 | 366 | 菲波那切数列问题及其推广,适用递推的限制为:**严格的,没有条件转移的递推** 367 | -------------------------------------------------------------------------------- /04-堆、结构体排序.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 比较器与堆 3 | 4 | ## 1.1 堆结构 5 | 6 | ### 1.1.1 完全二叉树结构 7 | 8 | > 完全二叉树结构:要么本层是满的,要么先满左边的,以下都是完全二叉树 9 | 10 | 1. 11 | 12 | ``` 13 | graph TD 14 | A-->B 15 | A-->C 16 | ``` 17 | 18 | 2. 19 | 20 | ``` 21 | graph TD 22 | A-->B 23 | A-->C 24 | B-->D 25 | B-->E 26 | C-->F 27 | ``` 28 | 29 | 30 | ### 1.1.2 数组实现堆 31 | 32 | - 堆结构就是用数组实现的完全二叉树结构 33 | 34 | 35 | > 用数组实现完全二叉树结构,从数组下标0开始,当成依次补齐二叉树结构的数据 36 | 37 | ``` 38 | graph TD 39 | 0--> 1 40 | 0--> 2 41 | 1--> 3 42 | 1-->4 43 | 2-->5 44 | 2-->6 45 | ``` 46 | 47 | 某位置i的左孩子下标为: 48 | ```math 49 | lchild = 2*i + 1 50 | ``` 51 | 某位置i的右孩子的下标为: 52 | ```math 53 | rchild = 2*i + 2 54 | ``` 55 | 某位置i的父节点位置为: 56 | ```math 57 | parent = (i-1) / 2 58 | ``` 59 | 60 | > 当我们不使用数组的0下标,从1位置开始构建完全二叉树时,方便使用位操作: 61 | 62 | 某位置i的左孩子下标为: 63 | ```math 64 | lchild = 2*i <==> i << 1 65 | ``` 66 | 某位置i的右孩子的下标为: 67 | ```math 68 | rchild = 2*i + 1 <==> (i << 1) | 1 69 | ``` 70 | 某位置i的父节点位置为: 71 | ```math 72 | parent = i / 2 <==> i >> 1 73 | ``` 74 | ### 1.1.3 大根堆与小根堆 75 | 76 | - 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 77 | 78 | - 完全二叉树中如果每颗子树的最小值都在顶部就是小根堆 79 | 80 | **我们认为堆就是大根堆或者小根堆,既不是大根堆也不是小根堆的完全二叉树只是完全二叉树,不能称之为堆** 81 | 82 | 83 | 84 | ### 1.1.4 构建堆 85 | 86 | - 堆结构的heapInsert与heapify操作 87 | 88 | 1、heapInsert 89 | 90 | 思路:例如我们要构建一个大根堆,我们把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式parent = (i-1) / 2找到父节点区比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较...,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆,对应代码的push方法 91 | 92 | > 我们的调整代价实际上就是这颗树的高度层数,logN 93 | 94 | 2、heapify 95 | 96 | > 原堆结构,删除最大值,继续调整维持成大根堆 97 | 98 | 思路:我们删除了最大值,也就是arr[0]位置,之后我们把堆最末尾的位置调整到arr[0]位置,堆大小减一。让现在arr[0]位置的数找左右孩子比较...,进行hearify操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。对应代码的pop方法 99 | 100 | > heapify的下沉操作,仍然是树的高度,logN。堆结构很重要 101 | 102 | ```Go 103 | package main 104 | 105 | import ( 106 | "errors" 107 | ) 108 | 109 | type Heap interface { 110 | IsEmpty() bool 111 | IsFull() bool 112 | Push(value int) error 113 | Pop() int 114 | } 115 | 116 | func assertListImplementation() { 117 | var _ Heap = (*MaxHeap)(nil) 118 | } 119 | 120 | type MaxHeap struct { 121 | // 大根堆的底层数组结构 122 | heap []int 123 | // 分配给堆的空间限制 124 | limit int 125 | // 表示目前这个堆收集了多少个数,即堆大小。也表示添加的下一个数应该放在哪个位置 126 | heapSize int 127 | } 128 | 129 | // NewMaxHeap 初始化一个大根堆结构 130 | func NewMaxHeap(limit int) *MaxHeap { 131 | maxHeap := &MaxHeap{ 132 | heap: make([]int, 0), 133 | limit: limit, 134 | heapSize: 0, 135 | } 136 | return maxHeap 137 | } 138 | 139 | func (h *MaxHeap) IsEmpty() bool { 140 | return len(h.heap) == 0 141 | } 142 | 143 | func (h *MaxHeap) IsFull() bool { 144 | return h.heapSize == h.limit 145 | } 146 | 147 | func (h *MaxHeap) Push(value int) error { 148 | if h.heapSize == h.limit { 149 | return errors.New("heap is full") 150 | } 151 | 152 | h.heap[h.heapSize] = value 153 | // heapSize的位置保存当前value 154 | heapInsert(h.heap, h.heapSize) 155 | h.heapSize++ 156 | return nil 157 | } 158 | 159 | // Pop 返回堆中的最大值,并且在大根堆中,把最大值删掉。弹出后依然保持大根堆的结构 160 | func (h *MaxHeap) Pop() int { 161 | tmp := h.heap[0] 162 | h.heapSize-- 163 | swap(h.heap, 0, h.heapSize) 164 | heapify(h.heap, 0, h.heapSize) 165 | return tmp 166 | } 167 | 168 | // 往堆上添加数,需要从当前位置找父节点比较 169 | func heapInsert(arr []int, index int) { 170 | for arr[index] > arr[(index-1)/2] { 171 | swap(arr, index, (index-1)/2) 172 | index = (index - 1) / 2 173 | } 174 | } 175 | 176 | // 从index位置,不断的与左右孩子比较,下沉。下沉终止条件为:1. 左右孩子都不大于当前值 2. 没有左右孩子了 177 | func heapify(arr []int, index int, heapSize int) { 178 | // 左孩子的位置 179 | left := index*2 + 1 180 | // 左孩子越界,右孩子一定越界。退出循环的条件是:2. 没有左右孩子了 181 | for left < heapSize { 182 | var largestIdx int 183 | rigth := left + 1 184 | // 存在右孩子,且右孩子的值比左孩子大,选择右孩子的位置 185 | if rigth < heapSize && arr[rigth] > arr[left] { 186 | largestIdx = rigth 187 | } else { 188 | largestIdx = left 189 | } 190 | 191 | // 1. 左右孩子的最大值都不大于当前值,终止寻找。无需继续下沉 192 | if arr[largestIdx] <= arr[index] { 193 | break 194 | } 195 | // 左右孩子的最大值大于当前值 196 | swap(arr, largestIdx, index) 197 | // 当前位置移动到交换后的位置,继续寻找 198 | index = largestIdx 199 | // 移动后左孩子理论上的位置,下一次循环判断越界情况 200 | left = index*2 + 1 201 | } 202 | } 203 | 204 | // swap 交换数组中的两个位置的数 205 | func swap(arr []int, i, j int) { 206 | tmp := arr[i] 207 | arr[i] = arr[j] 208 | arr[j] = tmp 209 | } 210 | ``` 211 | 212 | ### 1.1.5 堆排序 213 | 214 | 1. 对于用户给的所有数据,我们先让其构建成为大根堆 215 | 2. 对于0到N-1位置的数,我们依次让N-1位置的数和0位置的数(全局最大值)交换,此时全局最大值来到了数组最大位置,堆大小减一,再heapify调整成大根堆。再用N-2位置的数和调整后的0位置的数交换,相同操作。直至0位置和0位置交换。每次heapify为logN,交换调整了N次 216 | 3. 所以堆排序的时间复杂度为O(NlogN) 217 | 4. 堆排序额为空间复杂度为O(1),且不存在递归行为 218 | 219 | ```Go 220 | package main 221 | 222 | // HeapSort 堆排序额外空间复杂度O(1) 223 | func HeapSort(arr []int) { 224 | if len(arr) < 2 { 225 | return 226 | } 227 | 228 | // 原始版本, 调整arr满足大根堆结构。O(N*logN) 229 | //for i := 0; i < len(arr); i++ { // O(N) 230 | // heapInsert(arr, i) // O(logN) 231 | //} 232 | 233 | // 优化版本:heapInsert改为heapify。从末尾开始看是否需要heapify=》O(N)复杂度。 234 | // 但是这只是优化了原有都是构建堆(O(NlogN)),最终的堆排序仍然是O(NlogN)。比原始版本降低了常数项 235 | for i := len(arr) - 1; i >= 0; i-- { 236 | heapify(arr, i, len(arr)) 237 | } 238 | 239 | // 实例化一个大根堆,此时arr已经是调整后满足大根堆结构的arr 240 | mh := MaxHeap{ 241 | heap: arr, 242 | limit: len(arr), 243 | heapSize: len(arr), 244 | } 245 | 246 | mh.heapSize -- 247 | swap(arr, 0, mh.heapSize) 248 | // O(N*logN) 249 | for mh.heapSize > 0 { // O(N) 250 | heapify(arr, 0, mh.heapSize) // O(logN) 251 | mh.heapSize-- 252 | swap(arr, 0, mh.heapSize) // O(1) 253 | } 254 | 255 | } 256 | ``` 257 | 258 | > 关于上述heapInsert改为heapIfy的优化: 259 | 260 | 在我们从0到N-1进行heapInsert的时候,是O(NlogN),很容易理解。当我们从N-1到0上依次heapify的时候,整体来看,整棵树的根节点的heapify层数N/2,第二层为N/4且有两个节点。那么实质是N个不同的层数相加: 261 | 262 | ```math 263 | T(N) = (\frac{N}{2} * 1) + (\frac{N}{4} * 2) + (\frac{N}{8} * 3) + (\frac{N}{16} * 4) + ... 264 | 265 | => 266 | 267 | 2T(N) = (\frac{N}{2} * 2) + (\frac{N}{2} * 2) + (\frac{N}{4} * 3) + (\frac{N}{8} * 4) + ... 268 | 269 | => 270 | 271 | T(N) = N + \frac{N}{2} + \frac{N}{4} + \frac{N}{8} + ... 272 | 273 | => O(N) 274 | 275 | ``` 276 | 277 | **同理,可以按同样方式实现一个小根堆** 278 | 279 | **在有些语言中,已经实现了堆,例如Java的优先级队列java.util.PriorityQueue,Golang中的container/heap** 280 | 281 | ### 1.1.6 语言、系统提供的堆和手写堆的选择 282 | 283 | #### 1.1.6.1 系统堆和手写堆选择 284 | 285 | > 使用系统提供的堆:如果我们只是要依次拿最大值,那么做成大根堆,如果我们要最小值我们把堆结构做成小根堆。就是简单的我们添加值,拿值,我们就选择系统提供的堆 286 | 287 | > 选择手写堆:如果已经放到系统堆中的元素,加入我们根据需求会在放入堆之后要改动这些元素的值,系统堆并不保证弹出来的东西是正确的,这个时候需要我们手动写一个我们自定义的堆。虽然存在那种排好堆改某些元素让其重新有序的堆结构,但是实质上它是重新扫每个元素去heapinsert,代价太高。手动改写堆的例子例如Dijkstra算法就存在改写堆的优化 288 | 289 | ## 1.2 比较器 290 | 291 | 1、比较器的实质就是重载比较运算符 292 | 293 | 2、比较器可以很好的应用在特殊标准的排序上 294 | 295 | 3、比较器可以很好的应用在根据特殊标准排序的结构上 296 | 297 | > 任何有序结构,我们可以传入我们的比较器,自定义我们自己的排序规则,不传它会按自己默认的规则排序 298 | 299 | ### 1.2.1 Golang中自定义比较行为 300 | 301 | > 在Golang中如果需要自定义比较规则,只需要实现sort/srot.go中的Interface接口的Len、Less、Swap三个方法即可 302 | 303 | ```Go 304 | package main 305 | 306 | import ( 307 | "fmt" 308 | "sort" 309 | ) 310 | 311 | // Comparator 312 | // negative , if a < b 313 | // zero , if a == b 314 | // positive , if a > b 315 | type Comparator func(a, b interface{}) int 316 | 317 | // 定义可排序的结构 318 | type sortable struct { 319 | values []interface{} 320 | // 该结构携带一个自定义的排序策略 321 | comparator Comparator 322 | } 323 | 324 | // Sort 使用Go原生的排序进行包装,该排序在数据规模大的时候使用快排,数据规模小的时候使用插入排序 325 | func Sort(values []interface{}, comparator Comparator) { 326 | sort.Sort(sortable{values, comparator}) 327 | } 328 | 329 | func (s sortable) Len() int { 330 | return len(s.values) 331 | } 332 | 333 | func (s sortable) Swap(i, j int) { 334 | s.values[i], s.values[j] = s.values[j], s.values[i] 335 | } 336 | 337 | func (s sortable) Less(i, j int) bool { 338 | return s.comparator(s.values[i], s.values[j]) < 0 339 | } 340 | 341 | // IntComparator 是自定义的整形排序策略,可以实现其他自定义排序策略 342 | func IntComparator(a, b interface{}) int { 343 | aAsserted := a.(int) 344 | bAsserted := b.(int) 345 | switch { 346 | case aAsserted > bAsserted: 347 | return 1 348 | case aAsserted < bAsserted: 349 | return -1 350 | default: 351 | return 0 352 | } 353 | } 354 | 355 | func main() { 356 | tests := [][]interface{}{ 357 | {1, 1, 0}, 358 | {1, 2, -1}, 359 | {2, 1, 1}, 360 | {11, 22, -1}, 361 | {0, 0, 0}, 362 | {1, 0, 1}, 363 | {0, 1, -1}, 364 | } 365 | for _, test := range tests { 366 | Sort(test, IntComparator) 367 | fmt.Println(test) 368 | } 369 | } 370 | ``` 371 | -------------------------------------------------------------------------------- /05-前缀树、桶排序、排序总结.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 前缀树结构(trie)、桶排序、排序总结 3 | 4 | ## 1.1 前缀树结构 5 | 6 | - 单个字符串中,字符从前到后的加到一颗多叉树上 7 | - 字符放在路上,节点上有专属的数据项(常见的是pass和end值) 8 | - 所有样本都这样添加。如果没有路就新建,如果有路就复用 9 | - 沿途节点的pass值增加1.每个字符串结束时来到的节点end值增加1 10 | 一个字符串数组中,所有字符串的字符数为N,整个数组加入前缀树种的代价是O(N) 11 | 12 | 功能一:构建好前缀树之后,我们查询某个字符串在不在前缀树中,某字符串在这颗前缀树中出现了几次都是特别方便的。例如找"ab"在前缀树中存在几次,可以先看有无走向a字符的路径(如果没有,直接不存在),再看走向b字符的路径,此时检查该节点的end标记的值,如果为0,则前缀树中不存在"ab"字符串,如果e>0则,e等于几则"ab"在前缀树种出现了几次 13 | 14 | 功能二:如果单单是功能一,那么哈希表也可以实现。现查询所有加入到前缀树的字符串,有多少个以"a"字符作为前缀,来到"a"的路径,查看p值大小,就是以"a"作为前缀的字符串数量 15 | 16 | ```Go 17 | package main 18 | 19 | // Node 前缀树的节点 20 | type Node struct { 21 | Pass int 22 | End int 23 | Childes []*Node 24 | } 25 | 26 | func NewTrie() (root *Node) { 27 | trie := &Node{ 28 | Pass: 0, 29 | End: 0, 30 | // 默认保存26个英文字符a~z 31 | // 0 a 32 | // 1 b 33 | // .. .. 34 | // 25 z 35 | // Childes[i] == nil 表示i方向的路径不存在 36 | Childes: make([]*Node, 26), 37 | } 38 | return trie 39 | } 40 | 41 | // Insert 往该前缀树中添加字符串 42 | func (root *Node) Insert(word string) { 43 | if len(word) == 0 { 44 | return 45 | } 46 | // 字符串转字符数组,每个元素是字符的ascii码 47 | chs := []byte(word) 48 | node := root 49 | // 头结点的pass首先++ 50 | node.Pass++ 51 | // 路径的下标 52 | var path int 53 | // 从左往右遍历字符 54 | for i := 0; i < len(chs); i++ { 55 | // 当前字符减去'a'的ascii码得到需要添加的下个节点下标。即当前字符去往的路径 56 | path = int(chs[i] - 'a') 57 | // 当前方向上没有建立节点,即一开始不存在这条路,新开辟 58 | if node.Childes[path] == nil { 59 | node.Childes[path] = &Node{ 60 | Pass: 0, 61 | End: 0, 62 | Childes: make([]*Node, 26), 63 | } 64 | } 65 | // 引用指向当前来到的节点 66 | node = node.Childes[path] 67 | // 当前节点的pass++ 68 | node.Pass++ 69 | } 70 | // 当新加的字符串所有字符处理结束,最后引用指向的当前节点就是该字符串的结尾节点,end++ 71 | node.End++ 72 | } 73 | 74 | // Search 在该前缀树中查找word这个单词之前加入过几次 75 | func (root *Node) Search(word string) int { 76 | if len(word) == 0 { 77 | return 0 78 | } 79 | chs := []byte(word) 80 | node := root 81 | index := 0 82 | for i := 0; i < len(chs); i++ { 83 | index = int(chs[i] - 'a') 84 | // 寻找该字符串的路径中如果提前找不到path,就是未加入过,0次 85 | if node.Childes[index] == nil { 86 | return 0 87 | } 88 | node = node.Childes[index] 89 | } 90 | // 如果顺利把word字符串在前缀树中走完路径,那么此时的node对应的end值就是当前word在该前缀树中添加了几次 91 | return node.End 92 | } 93 | 94 | // Delete 删除该前缀树的某个字符串 95 | func (root *Node) Delete(word string) { 96 | // 首先要查一下该字符串是否加入过 97 | if root.Search(word) != 0 { 98 | // 沿途pass-- 99 | chs := []byte(word) 100 | node := root 101 | node.Pass-- 102 | path := 0 103 | for i := 0; i < len(chs); i++ { 104 | path = int(chs[i] - 'a') 105 | // 在寻找的过程中,pass为0,提前可以得知在本次删除之后,该节点以下的路径不再需要,可以直接删除。 106 | // 那么该节点之下下个方向的节点引用置为空(JVM垃圾回收,相当于该节点下的路径被删了) 107 | node.Childes[path].Pass-- 108 | if node.Childes[path].Pass == 0 { 109 | node.Childes[path] = nil 110 | return 111 | } 112 | node = node.Childes[path] 113 | } 114 | // 最后end-- 115 | node.End-- 116 | } 117 | } 118 | 119 | // PrefixNumber 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 120 | func (root *Node) PrefixNumber(pre string) int { 121 | if len(pre) == 0 { 122 | return 0 123 | } 124 | 125 | chs := []byte(pre) 126 | node := root 127 | index := 0 128 | for i := 0; i < len(chs); i++ { 129 | index = int(chs[i] - 'a') 130 | // pre走不到最后,就没有以pre作为前缀的字符串存在 131 | if node.Childes[index] == nil { 132 | return 0 133 | } 134 | node = node.Childes[index] 135 | } 136 | // 顺利走到最后,返回的pass就是有多少个字符串以当前pre为前缀的 137 | return node.Pass 138 | } 139 | ``` 140 | 141 | > Trie的孩子Childes,可以用一个map实现。可以容纳针对各种字符串的情况,实现自由扩展,make(map[int]*Node),表示字符的ascii码对应的节点映射。此处略 142 | 143 | 144 | ## 1.2 不基于比较的排序-桶排序 145 | 146 | > 例如:一个代表员工年龄的数组,排序。数据范围有限,对每个年龄做词频统计。arr[0~200] = 0,M=200 147 | 148 | > 空间换时间 149 | 150 | ### 1.2.1 计数排序 151 | 152 | 桶排序思想下的排序:计数排序 & 基数排序: 153 | 154 | 1、 桶排序思想下的排序都是不基于比较的排序 155 | 156 | 2、 时间复杂度为O(N),二维空间复杂复杂度为O(M) 157 | 158 | 3、 应用范围有限,需要样本的数据状况满足桶的划分 159 | 160 | > 缺点:与样本数据状况强相关。 161 | 162 | ### 1.2.2 基数排序 163 | 164 | > 应用条件:十进制数据,非负 165 | 166 | 如果对:[100,17,29,13,5,27] 进行排序: 167 | 168 | 1、找最高位的那个数的长度,这里100的长度为3,其他数前补0,得出 [100,017,029,013,005,027] 169 | 170 | 2、 准备10个桶,对应的数字0~9号桶,每个桶是一个队列。根据样本按个位数字对应进桶,相同个位数字进入队列,再从0号桶以此倒出,队列先进先出。个位进桶再依次倒出,得出 [100,013,005,017,027,029] 171 | 172 | 3、 再把按照个位进桶倒出的样本,再按十位进桶,再按相同规则倒出得 [100,005,013,017,027,029] 173 | 174 | 4、再把得到的样本按百位进桶,倒出得 [005,013,017,027,029,100] 175 | 176 | 此时达到有序! 177 | 178 | > 思想:先按各位数字排序,各位数字排好序,再用十位数字的顺序去调整,再按百位次序调整。优先级依次递增,百位优先级最高,百位优先级一样默认按照上一层十位的顺序... 179 | 180 | **结论:基于比较的排序,时间复杂度的极限就是O(NlogN),而不基于比较的排序,时间复杂度可以达到O(N)。在面试或刷题,估算排序的时间复杂度的时候,必须用基于比较的排序来估算** 181 | 182 | ```Go 183 | package main 184 | 185 | import "math" 186 | 187 | // BucketSort 计数排序 188 | func BucketSort(arr []int) { 189 | if len(arr) < 2 { 190 | return 191 | } 192 | 193 | max := math.MinInt 194 | for i := 0; i < len(arr); i++ { 195 | max = int(math.Max(float64(max), float64(arr[i]))) 196 | } 197 | 198 | bucket := make([]int, max+1) 199 | for i := 0; i < len(arr); i++ { 200 | bucket[arr[i]]++ 201 | } 202 | k := 0 203 | for i := 0; i < len(bucket); i++ { 204 | bucket[i]-- 205 | for bucket[i] > 0 { 206 | arr[k] = i 207 | k++ 208 | } 209 | } 210 | } 211 | ``` 212 | 213 | > 下面代码的思想: 214 | 215 | > 例如原数组[101,003,202,41,302]。得到按个位的词频conut数组为[0,2,2,1,0,0,0,0,0,0]。通过conut词频累加得到conut'为[0,2,4,5,5,5,5,5,5,5],此时conut'的含义表示个位数字小于等于0的数字有0个,个位数字小于等于1的有两个,个位数字小于等于2的有4个...... 216 | 217 | > 得到conut'之后,对原数组[101,003,202,41,302]从右往左遍历。根据基数排序的思想,302应该是2号桶最后被倒出的,我们已经知道个位数字小于等于2的有4个,那么302就是4个中的最后一个,放在help数组的3号位置,相应的conut'小于等于2位置的词频减减变为3。同理,41是1号桶的最后一个,个位数字小于等于1的数字有两个,那么41需要放在1号位置,小于等于1位置的词频减减变为1,同理...... 218 | 219 | > 实质增加conut和count'结构,避免申请十个队列结构,不想炫技直接申请10个队列结构,按基数排序思想直接做没问题 220 | 221 | > 实质上,基数排序的时间复杂度是O(Nlog10max(N)),log10N表示十进制的数的位数,但是我们认为基数排序的应用样本范围不大。如果要排任意位数的值,严格上就是O(Nlog10max(N)) 222 | 223 | ```Go 224 | package main 225 | 226 | import "fmt" 227 | 228 | func RadixSort(nums []int) []int { 229 | numberBit := howManyBit(maximum(nums)) 230 | // 循环的次数 231 | // 定义一个rec 二维切片 rec[i][x] 用来接受尾数是 i的数字 232 | for i := 0; i < numberBit; i++ { 233 | rec := make([][]int, 10) 234 | 235 | for _, num := range nums { 236 | rec[(num/pow10(i))%10] = append(rec[(num/pow10(i))%10], num) 237 | } 238 | // flatten the rec slice to the one dimension slice 239 | numsCopy := make([]int, 0) 240 | for j := 0; j < 10; j++ { 241 | numsCopy = append(numsCopy, rec[j]...) 242 | } 243 | // refresh nums,使得他变为 经过一次基数排序之后的数组 244 | nums = numsCopy 245 | } 246 | return nums 247 | } 248 | 249 | func pow10(num int) int { 250 | res := 1 251 | base := 10 252 | for num != 0 { 253 | if num&1 ==1 { 254 | num -= 1 255 | res *= base 256 | } 257 | num >>= 1 258 | base *= base 259 | } 260 | return res 261 | } 262 | 263 | func maximum(list []int) int { 264 | max := 0 265 | for _, i2 := range list { 266 | if i2 > max { 267 | max = i2 268 | } 269 | } 270 | return max 271 | } 272 | 273 | func howManyBit(number int) int { 274 | count := 0 275 | for number != 0 { 276 | number = number/10 277 | count += 1 278 | } 279 | return count 280 | } 281 | 282 | func main() { 283 | var theArray = []int{10, 1, 18, 30, 23, 12, 7, 5, 18, 233, 144} 284 | fmt.Print("排序前") 285 | fmt.Println(theArray) 286 | fmt.Print("排序后") 287 | fmt.Println(RadixSort(theArray)) 288 | } 289 | ``` 290 | 291 | ## 1.3 排序算法的稳定性 292 | 293 | > 稳定性是指同样大小的样本在排序之后不会改变相对次序。基础类型稳定性没意义,用处是按引用传递后是否稳定。比如学生有班级和年龄两个属性,先按班级排序,再按年龄排序,那么如果是稳定性的排序,不会破坏之前已经按班级拍好的顺序 294 | 295 | > 稳定性排序的应用场景:购物时候,先按价格排序商品,再按好评度排序,那么好评度实在价格排好序的基础上。反之不稳定排序会破坏一开始按照价格排好的次序 296 | 297 | 298 | ### 1.3.1 稳定的排序 299 | 300 | 1、 冒泡排序(处理相等时不交换) 301 | 302 | 2、 插入排序(相等不交换) 303 | 304 | 3、 归并排序(merge时候,相等先copy左边的) 305 | 306 | ### 1.3.2 不稳定的排序 307 | 308 | 1、 选择排序 309 | 310 | 2、 快速排序 (partion过程无法保证稳定) 311 | 312 | 3、 堆排序 (维持堆结构) 313 | 314 | ### 1.3.3 排序稳定性对比 315 | 316 | 排序 | 时间复杂度 | 空间复杂度 | 稳定性 317 | ---|---|---|--- 318 | 选择排序 | O(N^2) | O(1) | 无 319 | 冒泡排序 | O(N^2) | O(1) | 有 320 | 插入排序 | O(N^2) | O(1) | 有 321 | 归并排序 | O(NlogN) | O(N) | 有 322 | 随机快拍 | O(NlogN) | O(logN) | 无 323 | 堆排序 | O(NlogN) | O(1) | 无 324 | 计数排序 | O(N) | O(M) | 有 325 | 堆排序 | O(N) | O(N) | 有 326 | 327 | 328 | ## 1.4 排序算法总结 329 | 330 | 1. 不基于比较的排序,对样本数据有严格要求,不易改写 331 | 2. 基于比较的排序,只要规定好两个样本怎么比较大小就可以直接复用 332 | 3. 基于比较的排序,时间复杂度的极限是O(NlogN) 333 | 4. 时间复杂度O(NlogN)、额外空间复杂度低于O(N),且稳定的基于比较的排序是不存在的 334 | 5. 为了绝对的速度选择快排(快排的常数时间低),为了节省空间选择堆排序,为了稳定性选归并 335 | 336 | ## 1.5 排序常见的坑点 337 | 338 | > 归并排序的额为空间复杂度可以变为O(1)。“归并排序内部缓存法”,但是将会变的不稳定。不考虑稳定不如直接选择堆排序 339 | 340 | > “原地归并排序”是垃圾帖子,会让时间复杂度变成O(N ^2)。时间复杂度退到O(N ^2)不如直接选择插入排序 341 | 342 | > 快速排序稳定性改进,“01 stable sort”,但是会对样本数据要求更多。对数据进行限制,不如选择桶排序 343 | 344 | > 在整形数组中,请把奇数放在数组左边,偶数放在数组右边,要求所有奇数之间原始次序不变,所有偶数之间原始次序不变。要求时间复杂度O(N),额为空间复杂度O(1)。这是个01标准的partion,奇偶规则,但是快速排序的partion过程做不到稳定性。所以正常实现不了,学术论文(01 stable sort,不建议碰,比较难)中需要把数据阉割限制之后才能做到 345 | 346 | ## 1.6 工程上对排序的改进 347 | 348 | > 稳定性考虑:值传递,直接快排,引用传递,归并排序 349 | 350 | > 充分利用O(NlogN)和O(N^2)排序各自的优势:根据样本量底层基于多种排序实现,比如样本量比较小直接选择插入排序。 351 | 352 | > 比如Java中系统实现的快速排序 -------------------------------------------------------------------------------- /docs/14.md: -------------------------------------------------------------------------------- 1 | 2 | - [1 类似斐波那契数列的递归](#1) 3 | * [1.1 求斐波那契数列矩阵乘法的方法](#11) 4 | * [1.2 菲波那切数列可优化为O(logN)时间复杂度](#12) 5 | + [1.2.1 矩阵加快速幂方法](#121) 6 | - [1.2.1.1 矩阵推导](#1211) 7 | - [1.2.1.2 快速幂转化思路](#1212) 8 | * [1.3 递推推广式](#13) 9 | * [1.4 迈楼梯问题](#14) 10 | * [1.5 递推经典例题一](#15) 11 | * [1.6 递推经典例题二](#16) 12 | 13 |

1 类似斐波那契数列的递归

14 | 15 | 16 |

1.1 求斐波那契数列矩阵乘法的方法

17 | 18 | 1、菲波那切数列的线性求解(O(N))的方法非常好理解(一次遍历,N项依赖于N-1项和N-2项) 19 | 20 | 2、同时利用线性代数,也可以改写出另外一种表示`|F(N),F(N-1)| = |F(2),F(1)| * 某个二阶矩阵的N-2次方` 21 | 22 | 3、求出这二阶矩阵,进而最快求出这个二阶矩阵的N-2次方 23 | 24 | 25 |

1.2 菲波那切数列可优化为O(logN)时间复杂度

26 | 27 |

1.2.1 矩阵加快速幂方法

28 | 29 |

1.2.1.1 矩阵推导

30 | 31 | > 由于菲波那切数列是一个严格的递推式,不会发生条件转移。 32 | 33 | > 在线性代数中,对于(严格递推式)菲波那切数列,第二项和第三项所组成的行列式,等于第二项和第一项组成的行列式,乘以某一个2乘2的矩阵 34 | 35 | > 同理第四项和第三项行列式等于第三项和第二项组成的行列式乘以相同的矩阵,其他同理 36 | 37 | ```math 38 | |F(3),F(2)| = |F(2),F(1)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 39 | 40 | |F(4),F(3)| = |F(3),F(2)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 41 | ``` 42 | 43 | 举例:例如1 1 2 3 5 8 ... 的菲波那切数列,我们带入公式 44 | 45 | ```math 46 | |2,1| = |1,1| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 47 | 48 | => 2 = 1 * a + 1 * c 49 | 50 | => 1 = 1 * b + 1 * d 51 | 52 | => a + c = 2; b + d = 1; 53 | 54 | 55 | |3,2| = |2,1| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\} 56 | 57 | => 2a + c = 3; 2b + d =2; 58 | 59 | 60 | => a=1; b=1; c=1; d=0; 61 | 62 | ``` 63 | 64 | 可以得到我们的相同二阶矩阵中,a=1; b=1; c=1; d=0; 65 | 66 | 67 | > 继续推导,由于我们的严格递推式满足,我们可以把f(3)f(2)的公式带入到f(4)f(3)中,以此替换到f(n)f(n-1) = f(n-1)f(n-2) * 固定矩阵中 68 | 69 | 继续推导得出: 70 | 71 | ```math 72 | |F(N) * F(N-1)| = |F(2)F(1)| * \left\{ \begin{matrix} a & b \\ c & d \end{matrix} \right\}^{n-2} 73 | ``` 74 | 75 | > 由于菲波那切数列第二项和第一项是1,1,那么化简后,我们可以得到:f(n)和f(n-1)构成的行列式,等于1,1的行列式,乘以1110构成的矩阵的n-1次方。算该矩阵的n-2次方直接影响我们的算法复杂度。 76 | 77 | > 转化为,怎么求多个相同矩阵乘起来,怎么算比较快的问题?我们先思考怎么求一个数乘方怎么算比较快的问题? 78 | 79 |

1.2.1.2 快速幂转化思路

80 | 81 | 82 | 问:怎么快速求出10的75次方的值是多少。 83 | 84 | > 思路:利用一种精致的二分来求,75次方我们拆分成二进制形式,1001011 85 | 86 | ```math 87 | 10^{75} = 10^{64} * 10^8 * 10^2 * 10^1 88 | ``` 89 | 90 | 我们先从10的1次方开始,用tmp中间变量接收,依次和我们指数的二进制做比较。如果指数的二进制某一位为1,我们就要乘我们的t当成我们的result(初始为1),如果为0,我们就不乘。tmp每次判断结束和自身相乘,生成新的tmp。最终result就是我们的结果 91 | 92 | > 10的75次方推演为:tmp为10开始,对比二进制,需要乘进result。tmp和自己相乘变为10^2,仍然需要,result为1乘10乘10的平方乘10的八次方...。tmp虽然在变化,但我们不是都选择相乘 93 | 94 | 95 | ==所以上述矩阵次方的乘法,我们可以类似处理。把指数变为二进制。result初始值为单位矩阵,就是对角线全为1的矩阵。其他处理和数字次方的处理类似== 96 | 97 | 98 | **在JDK中,Math.power函数的内部,也是这样实现指数函数的(整数次方)** 99 | 100 |

1.3 递推推广式

101 | 102 | 如果某一个递推式,`F(N) = c1F(n-1) + C2F(n-2) + ... + czF(N-k)` k表示最底层减到多少,我们称之为k阶递归式。c1,c2,cz为常数系数,k为常数,那么都有O(logN)的解。系数只会影响到我们的k阶矩阵的不同 103 | 104 | **奶牛问题**:一个农场第一年有一只奶牛A,每一只奶牛每年会产一只小奶牛。假设所有牛都不会死,且小牛需要三年,可以产小奶牛。求N年后牛的数量。 105 | 106 | > 思路:牛的数量的轨迹为:1,2,3,4,6,9...。`F(N) = F(N-1) + F(N-3)` 既今年的牛F(N)等于去年的牛F(N-1) + 三年前牛的数量F(N-3)。三阶问题k=3。 107 | 108 | > 套上述公式为: `F(N) = 1 * F(N-1) + 1 * 0 * F(N-2) + 1 * F(N-3)` 109 | 110 | > 对于三阶问题,可以写成:**`|F(n)F(n-1)F(n-2)| = |F3F2F1| * |3*3|^{n-3}`** 111 | 112 | 通用的表达式如下,同理根据前几项,求出原始矩阵。 113 | 114 | ``` 115 | |F(n)F(n-1)...F(n-k+1)| = |Fk...F2F1| * |k*k|^{n-k} 116 | ``` 117 | 118 | 119 | ```Java 120 | 121 | package class02; 122 | 123 | public class Code01_FibonacciProblem { 124 | // 斐波那契数列暴力解法 125 | public static int f1(int n) { 126 | if (n < 1) { 127 | return 0; 128 | } 129 | if (n == 1 || n == 2) { 130 | return 1; 131 | } 132 | return f1(n - 1) + f1(n - 2); 133 | } 134 | 135 | // 线性求解方法 136 | public static int f2(int n) { 137 | if (n < 1) { 138 | return 0; 139 | } 140 | if (n == 1 || n == 2) { 141 | return 1; 142 | } 143 | int res = 1; 144 | int pre = 1; 145 | int tmp = 0; 146 | for (int i = 3; i <= n; i++) { 147 | tmp = res; 148 | res = res + pre; 149 | pre = tmp; 150 | } 151 | return res; 152 | } 153 | 154 | // 矩阵加快速幂O(logN)方法 155 | public static int f3(int n) { 156 | if (n < 1) { 157 | return 0; 158 | } 159 | if (n == 1 || n == 2) { 160 | return 1; 161 | } 162 | // [ 1 ,1 ] 163 | // [ 1, 0 ] 164 | int[][] base = { 165 | { 1, 1 }, 166 | { 1, 0 } 167 | }; 168 | // 求出base矩阵的n-2次方,得到的矩阵返回 169 | int[][] res = matrixPower(base, n - 2); 170 | // 最终通过单位矩阵乘以该res,矩阵运算后返回fn的值。 171 | // 得到xyzk组成的矩阵 172 | // f(n)F(n-1) = {1, 0} * {x, y} 173 | // {0, 1} {z, k} 174 | // 推导出fn = x + z 175 | return res[0][0] + res[1][0]; 176 | } 177 | 178 | // 快速求一个矩阵m的p次方 179 | public static int[][] matrixPower(int[][] m, int p) { 180 | int[][] res = new int[m.length][m[0].length]; 181 | for (int i = 0; i < res.length; i++) { 182 | // 单位矩阵,对角线都是1。相当于矩阵概念中的'1' 183 | res[i][i] = 1; 184 | } 185 | 186 | // res = 矩阵中的1 187 | int[][] tmp = m;// 矩阵1次方 188 | // 基于次方的p,做位运算。右移 189 | for (; p != 0; p >>= 1) { 190 | // 右移之后的末位不是0,才乘当前的tmp 191 | if ((p & 1) != 0) { 192 | res = muliMatrix(res, tmp); 193 | } 194 | // 自己和自己相乘,得到下一个tmp 195 | tmp = muliMatrix(tmp, tmp); 196 | } 197 | return res; 198 | } 199 | 200 | // 两个矩阵乘完之后的结果返回 201 | public static int[][] muliMatrix(int[][] m1, int[][] m2) { 202 | int[][] res = new int[m1.length][m2[0].length]; 203 | for (int i = 0; i < m1.length; i++) { 204 | for (int j = 0; j < m2[0].length; j++) { 205 | for (int k = 0; k < m2.length; k++) { 206 | res[i][j] += m1[i][k] * m2[k][j]; 207 | } 208 | } 209 | } 210 | return res; 211 | } 212 | 213 | 214 | public static int s1(int n) { 215 | if (n < 1) { 216 | return 0; 217 | } 218 | if (n == 1 || n == 2) { 219 | return n; 220 | } 221 | return s1(n - 1) + s1(n - 2); 222 | } 223 | 224 | public static int s2(int n) { 225 | if (n < 1) { 226 | return 0; 227 | } 228 | if (n == 1 || n == 2) { 229 | return n; 230 | } 231 | int res = 2; 232 | int pre = 1; 233 | int tmp = 0; 234 | for (int i = 3; i <= n; i++) { 235 | tmp = res; 236 | res = res + pre; 237 | pre = tmp; 238 | } 239 | return res; 240 | } 241 | 242 | // 奶牛问题O(logN) 243 | public static int s3(int n) { 244 | if (n < 1) { 245 | return 0; 246 | } 247 | if (n == 1 || n == 2) { 248 | return n; 249 | } 250 | int[][] base = { { 1, 1 }, { 1, 0 } }; 251 | int[][] res = matrixPower(base, n - 2); 252 | return 2 * res[0][0] + res[1][0]; 253 | } 254 | 255 | public static int c1(int n) { 256 | if (n < 1) { 257 | return 0; 258 | } 259 | if (n == 1 || n == 2 || n == 3) { 260 | return n; 261 | } 262 | return c1(n - 1) + c1(n - 3); 263 | } 264 | 265 | public static int c2(int n) { 266 | if (n < 1) { 267 | return 0; 268 | } 269 | if (n == 1 || n == 2 || n == 3) { 270 | return n; 271 | } 272 | int res = 3; 273 | int pre = 2; 274 | int prepre = 1; 275 | int tmp1 = 0; 276 | int tmp2 = 0; 277 | for (int i = 4; i <= n; i++) { 278 | tmp1 = res; 279 | tmp2 = pre; 280 | res = res + prepre; 281 | pre = tmp1; 282 | prepre = tmp2; 283 | } 284 | return res; 285 | } 286 | 287 | public static int c3(int n) { 288 | if (n < 1) { 289 | return 0; 290 | } 291 | if (n == 1 || n == 2 || n == 3) { 292 | return n; 293 | } 294 | // 原始矩阵 295 | int[][] base = { 296 | { 1, 1, 0 }, 297 | { 0, 0, 1 }, 298 | { 1, 0, 0 } }; 299 | int[][] res = matrixPower(base, n - 3); 300 | return 3 * res[0][0] + 2 * res[1][0] + res[2][0]; 301 | } 302 | 303 | public static void main(String[] args) { 304 | int n = 19; 305 | System.out.println(f1(n)); 306 | System.out.println(f2(n)); 307 | System.out.println(f3(n)); 308 | System.out.println("==="); 309 | 310 | System.out.println(s1(n)); 311 | System.out.println(s2(n)); 312 | System.out.println(s3(n)); 313 | System.out.println("==="); 314 | 315 | System.out.println(c1(n)); 316 | System.out.println(c2(n)); 317 | System.out.println(c3(n)); 318 | System.out.println("==="); 319 | 320 | } 321 | 322 | } 323 | 324 | ``` 325 | 326 |

1.4 迈楼梯问题

327 | 328 | 问题描述:小明想要迈到n层台阶上去,可以一次迈一层台阶,可以一次迈两层台阶。问:迈到n层一共可以有多少种方法 329 | 330 | > 思路:1层台阶的时候,只有一种方法。2层台阶的时候迈两次一步1中,一次迈两步1种共两种方法。n层台阶等于迈到n-1层的方法数,加上迈到n-2层台阶的方法数 331 | 332 | ```math 333 | F(N) = F(n-1) + F(n-2) 334 | ``` 335 | 336 | **二阶递推式,类似菲波那切数列问题,区别在与斐波那契数列的初始值为1,1,而本题的初始值为1,2而已** 337 | 338 | 339 | 问题升级:同样条件,可以迈1步,可以迈2步,可以迈5步,问迈到n层台阶,有多少种方法 340 | 341 | 342 | ```math 343 | F(N) = F(n-1) + F(n-2) + F(n-5) 344 | ``` 345 | 346 | **五阶递推,求5乘5的原始矩阵即可,拿到矩阵表达式** 347 | 348 | 349 | 问题升级:奶牛问题,假设原有条件不变的情况下,奶牛寿命为10年 350 | 351 | ```math 352 | F(N) = F(n-1) + F(n-3) - F(n-10) 353 | ``` 354 | 355 | **十阶递推,求10乘10的原始矩阵,拿到矩阵关系表达式** 356 | 357 | 358 | ==每年,这种问题在面试笔试中大量出现,兔子生乌龟问题,乌龟生兔子问题,等等,层出不穷,都可以用这种方法模型求解== 359 | 360 |

1.5 递推经典例题一

361 | 362 | 题目描述:给定一个数N,想象只有0和1两种字符,组成的所有长度为N的字符串。如果某个字符串,任何0字符的左边都有1紧挨着,认为这个字符串达标,返回有多少达标的字符串 363 | 364 | 365 | > 思路:N=1时,有0和1组成的01字符只有0或者1。单个字符'0'不达标,只有'1'一种达标的字符串 366 | 367 | > N=1时,有0和1组成的01字符有四种'00','01','10','11'。'10'和'11'达标,两个 368 | 369 | > N=3时,有0和1组成的01字符有八种。达标的为'101','110','111'三个 370 | 371 | > 暴力解为O(2^n * n)。优化:我们定义一个i长度的字符串,该字符串最左边一定是'1'。在i长度上自由变换,最终有多少个达标的。如果n=8,由于最左侧已经规定了'1',我们调用f(7),在这7个位置的第一个位置判断,如果1位置是0,2位置必定不能为0,为1。那么f(i-2)长度可以任意变。如果第一个字符填的是1,那么f(i-1)可以随意变。衍生出菲波那切数列的递推公式;**二阶递推** 372 | 373 | 374 | ```math 375 | f(i) = f(i-1) + f(i-2) 376 | ``` 377 | 378 |

1.6 递推经典例题二

379 | 380 | 381 | 题目描述:有一款2行,N列的区域。假设我们只有1*2大小的瓷砖,我们把该区域填满有多少种方案? 382 | 383 | > 思路:定义F(N)函数,返回2*N区域都没贴瓷砖的会有多少种方法数。 384 | 385 | > 第一块瓷砖竖着拜,剩下的区域有F(N-1)种,第一块瓷砖横着摆,第一块瓷砖下方区域只能横着摆,剩下的方法数自由摆放为F(N-2) 386 | 387 | ```math 388 | F(N) = F(N-1) + F(N-2) 389 | ``` 390 | 391 | **仍然是一个二阶递推式,菲波那切数列问题** 392 | 393 | ==菲波那切数列问题及其推广,适用递推的限制为:**严格的,没有条件转移的递推**== 394 | -------------------------------------------------------------------------------- /09-贪心算法解题思路.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 贪心算法 4 | 5 | ## 1.1 基本概念 6 | 7 | 1、最自然智慧的算法 8 | 9 | 2、用一种局部最功利的标准,总是能做出在当前看来是最好的选择 10 | 11 | 3、难点在于证明局部最优解可以最终得到全局最优解 12 | 13 | 4、对于贪心算法的学习主要是以经验为主,尝试为主 14 | 15 | ### 1.2.1 贪心算法解释 16 | 17 | 正例:通过一个例子来解释,假设一个数组中N个正数,第一个挑选出来的数乘以1,第二个挑选出来的数乘以2,同理,第N次挑选出来的数乘以N,总的加起来是我们的分数。怎么挑选数字使我们达到最大分数? 18 | 19 | > 数组按从小到大的顺序排序,我们按顺序依次挑选,最终结果就是最大的。本质思想是因子随着挑选次数的增加会增大,我们尽量让大数去结合大的因子。 20 | 21 | > 贪心算法有时是无效的,下文会举贪心算法无效的例子 22 | 23 | ### 1.2.2 贪心算法的证明问题 24 | 25 | 如何证明贪心算法的有效性? 26 | 27 | > 一般来说,贪心算法不推荐证明,很多时候证明是非常复杂的。通过下面例子来说明贪心算法证明的复杂性; 28 | 29 | 例子:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。 30 | 31 | > 字典序概念:直观理解,两个单词放到字典中,从头开始查找这个单词,哪个先被查找到,哪个字典序小。 32 | 33 | > 字典序严格定义,我们把字符串当成k进制的数,a-z当成26进制的正数,字符长度一样,abk>abc,那么我们说abk的字典序更大。字符长度不一样ac和b,那么我们要把短的用0补齐,0小于a的accil,那么aca即可比较出来大小。 34 | 35 | 常见的语言标准库中比较字符串的函数,大都是比较字典序。 36 | 37 | 思路1:按照单个元素字典序贪心,例如在[ac,bk,sc,ket]字符串数组中,我们拼接出来最终的字符串字典序最小,那么我们依次挑选字典序最小的进行拼接的贪心策略得到acbkketsc。 38 | 39 | > 但是这样的贪心不一定是正确的,例如[ba,b]按照上述思路的贪心结果是bba,但是bab明显是最小的结果 40 | 41 | 思路2:两个元素x和y,x拼接y小于等于y拼接x,那么x放前,否则y放前面。例如x=b,y=ba。bba大于bab的字典序,那么ba放前面 42 | 43 | 44 | 证明: 45 | 46 | 我们把拼接当成k进制数的数学运算,把a-z的数当成26进制的数,'ks'拼接'ts'实质是ks * 26^2 + te。 47 | 48 | 目标先证明我们比较的传递性:证明a拼接b小于b拼接a,b拼接c小于等于c拼接b,推导出a拼接c小于等于c拼接a。 49 | 50 | a拼接b等于a乘以k的b长度次方 + b。我们把k的x长度次方这个操作当成m(x)函数。所以: 51 | 52 | ```math 53 | a * m(b) + b <= b * m(a) + a 54 | 55 | b * m(c) + c <= c * m(b) + b 56 | 57 | => 58 | 59 | a * m(b) * c <= b * m(a) * c + ac - bc 60 | 61 | b * m(c) * a + ca - ba <= c * m(b) * a 62 | 63 | => 64 | 65 | b * m(c) * a + ca - ba <= b * m(a) * c + ac - bc 66 | 67 | => 68 | 69 | m(c) * a + c <= m(a) * c + a 70 | 71 | ``` 72 | 73 | 至此,我们证明出我们的排序具有传递性质。 74 | 75 | 根据我们排序策略得到的一组序列,证明我们任意交换两个字符的位置,都会得到更大的字典序。 76 | 77 | 78 | 例如按照思路二得到的amnb序列,我们交换a和b。我们先把a和m交换,由于按照思路二得到的序列,满足a.m <= m.a 那么所以manb > amnb,同理得到amnb < bmna。 79 | 80 | 再证明任意三个交换都会变为更大的字典序,那么最终数学归纳法,得到思路二的正确性 81 | 82 | > 所以贪心算法的证明实质是比较复杂的,我们大可不必每次去证明贪心的正确性 83 | 84 | ```Go 85 | package main 86 | 87 | import ( 88 | "sort" 89 | "strings" 90 | ) 91 | 92 | // 方法1 暴力法穷举,排列组合。略 93 | 94 | // LowestStringByGreedy 方法2 贪心法 95 | func LowestStringByGreedy(strs []string) string { 96 | if len(strs) == 0 { 97 | return "" 98 | } 99 | 100 | Sort(strs, func(a, b string) int { 101 | return strings.Compare(a, b) 102 | }) 103 | 104 | res := "" 105 | for i := 0; i < len(strs); i++ { 106 | res += strs[i] 107 | } 108 | return res 109 | } 110 | 111 | type Comparator func(a, b string) int 112 | 113 | func Sort(values []string, comparator Comparator) { 114 | sort.Sort(sortable{values, comparator}) 115 | } 116 | 117 | type sortable struct { 118 | values []string 119 | comparator Comparator 120 | } 121 | 122 | func (s sortable) Len() int { 123 | return len(s.values) 124 | } 125 | 126 | func (s sortable) Swap(i, j int) { 127 | s.values[i], s.values[j] = s.values[j], s.values[i] 128 | } 129 | 130 | func (s sortable) Less(i, j int) bool { 131 | return s.comparator(s.values[i], s.values[j]) < 0 132 | } 133 | ``` 134 | 135 | > 全排列的时间复杂度为:O(N!) 136 | 137 | > 每一种贪心算法有可能都有属于他自身的特有证明,例如哈夫曼树算法,证明千变万化 138 | 139 | > 贪心策略算法,尽量不要陷入复杂的证明 140 | 141 | ## 1.2 贪心算法求解思路 142 | 143 | ### 1.2.1 标准求解过程 144 | 145 | 1、分析业务 146 | 147 | 2、根据业务逻辑找到不同的贪心策略 148 | 149 | 3、对于能举出反例的策略,直接跳过,不能举出反例的策略要证明有效性,这往往是比较困难的,要求数学能力很高且不具有统一的技巧性 150 | 151 | ### 1.2.2 贪心算法解题套路 152 | 153 | 1、实现一个不依靠贪心策略的解法X,可以用暴力尝试 154 | 155 | 2、脑补出贪心策略A,贪心策略B,贪心策略C...... 156 | 157 | 3、用解法X和对数器,用实验的方式得知哪个贪心策略正确 158 | 159 | 4、不要去纠结贪心策略的证明 160 | 161 | > 贪心类的题目在笔试中,出现的概率高达6到7成,而面试中出现贪心的概率不到2成。因为笔试要的是淘汰率,面试要的是区分度。由于贪心的解决完全取决于贪心策略有没有想对,有很高的淘汰率但是没有很好的区分度 162 | 163 | ## 1.3 贪心算法套路解题实战 164 | 165 | ### 1.3.1 例一:会议日程安排问题 166 | 167 | 一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目宣讲。给你每个项目的开始时间和结束时间,你来安排宣讲的日程,要求会议室进行宣讲的场数最多。 168 | 169 | 返回最多的宣讲场次。 170 | 171 | > 思路:本题常见的几种贪心策略,一种是按照谁先开始安排谁,第二种按照持续时间短的先安排,第三种按照谁先结束安排谁。 172 | 173 | > 通过验证,无需证明得出第三种贪心策略是正确的 174 | 175 | ```Go 176 | package main 177 | 178 | import "sort" 179 | 180 | type Program struct { 181 | start int 182 | end int 183 | } 184 | 185 | type Programs []*Program 186 | 187 | func bestArrange(programs Programs) int { 188 | sort.Sort(programs) 189 | // timeline表示来到的时间点 190 | timeLine := 0 191 | // result表示安排了多少个会议 192 | result := 0 193 | // 由于刚才按照结束时间排序,当前是按照谁结束时间早的顺序遍历 194 | for i := 0; i < len(programs); i++ { 195 | if timeLine <= programs[i].start { 196 | result++ 197 | timeLine = programs[i].end 198 | } 199 | } 200 | return result 201 | } 202 | 203 | func (p Programs) Len() int { 204 | return len(p) 205 | } 206 | 207 | // Less 根据谁的结束时间早排序 208 | func (p Programs) Less(i, j int) bool { 209 | return p[i].end-p[j].end > 0 210 | } 211 | 212 | func (p Programs) Swap(i, j int) { 213 | p[i], p[j] = p[j], p[i] 214 | } 215 | ``` 216 | 217 | ### 1.3.2 例二:居民楼路灯问题 218 | 219 | 给定一个字符串str,只由‘X’和‘.’两个字符构成。 220 | 221 | ‘X’表示墙,不能放灯,也不需要点亮,‘.’表示居民点,可以放灯,需要点亮。 222 | 223 | 如果灯放在i位置,可以让i-1,i和i+1三个位置被点亮,返回如果点亮str中所需要点亮的位置,至少需要几盏灯 224 | 225 | 例如: X..X......X..X. 需要至少5盏灯 226 | 227 | ```Go 228 | package main 229 | 230 | // minLight 给定一个由'X'和'.'组成的居民楼路径。要照亮所有居民楼,返回最少需要几盏灯 231 | func minLight(road string) int { 232 | str := []byte(road) 233 | // index从0出发 234 | index := 0 235 | // 当前灯的个数 236 | light := 0 237 | for index < len(str) { 238 | // 当前i位置是X,直接跳到下一个位置做决定 239 | if str[index] == 'X' { 240 | index++ 241 | } else { // i 位置是 . 不管i+1是X还是.当前区域需要放灯 242 | light++ 243 | // 接下来没字符了,遍历结束 244 | if index + 1 == len(str) { 245 | break 246 | } else { 247 | // 如果i+1位置是X,在i位置放灯,去i+2位置做决定 248 | if str[index + 1] == 'X' { 249 | index = index + 2 250 | } else { // i位置是. i+1也是. 那么不管i+2是什么,都在i+1位置放灯,到i+3去做决定 251 | index = index + 3 252 | } 253 | } 254 | } 255 | } 256 | return light 257 | } 258 | ``` 259 | 260 | ### 1.3.3 例三:哈夫曼树问题 261 | 262 | 一根金条切成两半,是需要花费和长度值一样的铜板的。 263 | 264 | 比如长度为20的金条,不管怎么切,都需要花费20个铜板。一群人想整分整块金条,怎么分最省铜板? 265 | 266 | 例如:给定数组[10,20,30],代表一共三个人,整块金条长度为60,金条要分成10,20,30三个部分。 267 | 268 | 如果先把长度为60的金条分成10和50,花费60;再把长度为50的金条分成20和30,花费50;一共需要花费110个铜板。但是如果先把长度为60的金条分成30和30,花费60;再把30的金条分成10和20,花费30;一共花费90个铜板。 269 | 270 | 输入一个数组,返回分割的最小代价。 271 | 272 | > 小根堆,大根堆,排序。是贪心策略最常用的手段,coding代码量很少。因为堆天然就具备根据我们自定义的排序规则重新组织数据 273 | 274 | ```Go 275 | package main 276 | 277 | import ( 278 | "container/heap" 279 | "fmt" 280 | ) 281 | 282 | // CutCost Array 例如[10, 20, 30]表示价值为60的金条,需要切割成10 20 30的条段给三个人分 283 | type CutCost struct { 284 | Array []int 285 | } 286 | 287 | func (c CutCost)Len() int { 288 | return len(c.Array) 289 | } 290 | 291 | func (c CutCost)Less(i, j int) bool { 292 | return c.Array[i] > c.Array[j] 293 | } 294 | 295 | func (c CutCost)Swap(i, j int) { 296 | c.Array[i], c.Array[j] = c.Array[j], c.Array[i] 297 | } 298 | 299 | func (c *CutCost) Push(h interface{}) { 300 | c.Array = append(c.Array, h.(int)) 301 | } 302 | 303 | func (c *CutCost) Pop() (x interface{}) { 304 | n := len(c.Array) 305 | x = c.Array[n - 1] 306 | c.Array = c.Array[:n-1] 307 | return x 308 | } 309 | 310 | // 切金条,贪心解法,建立一个小根堆,把所有数扔进去 311 | func lessMoney (c *CutCost) int { 312 | fmt.Println("原始slice: ", c.Array) 313 | 314 | heap.Init(c) 315 | // 通过堆初始化后的arr 316 | fmt.Println("堆初始化后的slice:", c.Array) 317 | 318 | sum := 0 319 | cur := 0 320 | for len(c.Array) > 1 { 321 | // 每一次弹出两个数,合并成一个数 322 | // 合成的数一定输非叶子节点 323 | cur = c.Pop().(int) + c.Pop().(int) 324 | // 把合成的数累加到sum中去 325 | sum += cur 326 | // 把合成的数加入小根堆中 327 | c.Push(cur) 328 | } 329 | return sum 330 | } 331 | ``` 332 | 333 | ### 1.3.4 例四:项目花费和利润问题 334 | 335 | 输入:正数数组costs,正数数组profits,正数K,正数M 336 | 337 | costs[i]表示i号项目的花费,profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润) 338 | 339 | K表示你只能串行的最多K个项目,M表示你的初始资金。 340 | 341 | 说明:每做完一个项目,马上获得收益,可以支持你去做下一个项目。不能并行的做项目。 342 | 343 | 输出:你最后获得的最大钱数。 344 | 345 | ```Go 346 | package main 347 | 348 | import ( 349 | "container/heap" 350 | ) 351 | 352 | // Item 项目 353 | type Item struct { 354 | C int 355 | P int 356 | } 357 | 358 | // MinCostQ 项目最小花费。由花费组织的小根堆 359 | type MinCostQ struct { 360 | Items []*Item 361 | } 362 | 363 | func (c MinCostQ) Len() int { 364 | return len(c.Items) 365 | } 366 | 367 | // Less i对应的花费C的值小于j对应的值为true,则从小到大排序,即小根堆 368 | func (c MinCostQ) Less(i, j int) bool { 369 | return c.Items[i].C < c.Items[j].C 370 | } 371 | 372 | func (c MinCostQ) Swap(i, j int) { 373 | c.Items[i], c.Items[j] = c.Items[j], c.Items[i] 374 | } 375 | 376 | func (c *MinCostQ) Push(h interface{}) { 377 | c.Items = append(c.Items, h.(*Item)) 378 | } 379 | 380 | func (c *MinCostQ) Pop() (x interface{}) { 381 | n := len(c.Items) 382 | x = c.Items[n-1] 383 | c.Items = c.Items[:n-1] 384 | return x 385 | } 386 | 387 | // MaxProfitQ 项目最大利润,由利润组织的大根堆 388 | type MaxProfitQ struct { 389 | Items []*Item 390 | } 391 | 392 | func (c MaxProfitQ) Len() int { 393 | return len(c.Items) 394 | } 395 | 396 | // Less i对应的利润P的值大于j对应的值为true,则从大到小排序,即大根堆 397 | func (c MaxProfitQ) Less(i, j int) bool { 398 | return c.Items[i].P > c.Items[j].P 399 | } 400 | 401 | func (c MaxProfitQ) Swap(i, j int) { 402 | c.Items[i], c.Items[j] = c.Items[j], c.Items[i] 403 | } 404 | 405 | func (c *MaxProfitQ) Push(h interface{}) { 406 | c.Items = append(c.Items, h.(*Item)) 407 | } 408 | 409 | func (c *MaxProfitQ) Pop() (x interface{}) { 410 | n := len(c.Items) 411 | x = c.Items[n-1] 412 | c.Items = c.Items[:n-1] 413 | return x 414 | } 415 | 416 | // findMaximizedCapital 找到项目最大利润。由于Profits和Capital一一对应 417 | // K表示你只能串行的最多K个项目,M表示你的初始资金。 418 | func findMaximizedCapital(K, W int, Profits, Capital []int) int { 419 | Items := make([]*Item, 0) 420 | for i := 0; i < len(Profits); i++ { 421 | im := &Item{ 422 | C: Capital[i], 423 | P: Profits[i], 424 | } 425 | Items = append(Items, im) 426 | } 427 | minC := &MinCostQ{ 428 | Items: Items, 429 | } 430 | 431 | maxQ := &MaxProfitQ{ 432 | Items: Items, 433 | } 434 | 435 | // 由花费组织的小根堆。初始化 436 | heap.Init(minC) 437 | // 由利润组织的大根堆。初始化 438 | heap.Init(maxQ) 439 | 440 | // 做k轮项目 441 | for i := 0; i < K; i++ { 442 | // 小根堆不为空,且堆顶的花费被我当前启动资金cover住 443 | for len(minC.Items) != 0 && minC.Items[len(minC.Items) - 1].C <= W { 444 | // 小根堆的堆顶扔到大根堆中去 445 | maxQ.Push(minC.Pop()) 446 | } 447 | // 大根堆没有可以做的项目,直接返回总钱数 448 | if len(maxQ.Items) == 0 { 449 | return W 450 | } 451 | // 大根堆不为空,堆顶元素的利润直接加到我们的总钱数上 452 | // 大根堆弹出堆顶元素 453 | W += maxQ.Pop().(Item).P 454 | } 455 | return W 456 | } 457 | ``` 458 | -------------------------------------------------------------------------------- /docs/20.md: -------------------------------------------------------------------------------- 1 | - [1 数组累加和问题三连](#1) 2 | * [1.1 数组累加和问题](#11) 3 | + [1.1.1 第一连例题](#111) 4 | + [1.1.2 第二连例题](#112) 5 | + [1.1.2 第三连例题](#112) 6 | 7 |

1 数组累加和问题三连

8 | 9 | 知识点补充:系统设计类题目 10 | 11 | 设计一个系统,该系统的功能是可以一直不停的提供不同的UUID,该UUID使用的极为频繁,比如全球卖西瓜的,每个西瓜子是一个UUID 12 | 13 | > 思路,如果使用hashcode,有可能会产生碰撞。不使用hashcode,使用机器的ip或者mac地址加上纳秒时间,也不行,所有机器时间是否强同步 14 | 15 | > 解决思路:定义一台服务器,对世界的国家进行分类,比如中国下是省份,美国下是州,英国下是邦。每一个国家向中央服务器要随机范围,中央服务器分配出去的是start和range。比如给中国分配的是start从1开始,range到100w,中国uuid不够用了,可以再向中央服务器要,分配后中央服务器的start要增大到已分配出去后的位置。其他国家类似 16 | 17 | > 该设计师垂直扩展的技术,当前很多有事水平扩展,比如直接hashcode,random等。但有些场景适合用这种垂直扩展的解决方案 18 | 19 | 20 |

1.1 数组累加和问题

21 | 22 |

1.1.1 第一连例题

23 | 24 | 有一个全是正数的数组,和一个正数sum。求该数组的累加和等于sum的子数组多长。例如[3,2,1,1,1,6,1,1,1,1,1,1],sum等于6。最长的子数组为[1,1,1,1,1,1]返回长度6 25 | 26 | 27 | > 由于是正数数组,累加和和范围具有单调性。对于具有单调性的题目,要么定义左右指针,要么定义窗口滑动 28 | 29 | 30 | 定义窗口window,windowSum初始为0。滑动的过程中: 31 | 32 | 1、如果windowSum小于sum,窗口右边界R向右移动一个位置; 33 | 34 | 2、如果windowSum大于sum,窗口左边界L向右移动一个位置; 35 | 36 | 3、如果windowSum等于sum,此时的窗口大小就是一个满足条件的子数组大小,决定是否要更新答案; 37 | 38 | ```Java 39 | public class Code01_LongestSumSubArrayLengthInPositiveArray { 40 | 41 | // 滑动窗口的表示 42 | public static int getMaxLength(int[] arr, int K) { 43 | if (arr == null || arr.length == 0 || K <= 0) { 44 | return 0; 45 | } 46 | // 初始窗口位置[0,0],窗口当前只有第一个数 47 | int left = 0; 48 | int right = 0; 49 | int sum = arr[0]; 50 | int len = 0; 51 | while (right < arr.length) { 52 | // 窗口的累加和sum等于我们的目标k。求窗口大小len 53 | if (sum == K) { 54 | len = Math.max(len, right - left + 1); 55 | // 窗口累加和减去左窗口位置的值,左位置再出窗口 56 | sum -= arr[left++]; 57 | } else if (sum < K) { 58 | // 窗口右边界扩,如果不越界把扩之后的那个位置的值加到窗口累加值上 59 | right++; 60 | if (right == arr.length) { 61 | break; 62 | } 63 | sum += arr[right]; 64 | } else { 65 | sum -= arr[left++]; 66 | } 67 | } 68 | return len; 69 | } 70 | 71 | // for test 72 | public static int right(int[] arr, int K) { 73 | int max = 0; 74 | for (int i = 0; i < arr.length; i++) { 75 | for (int j = i; j < arr.length; j++) { 76 | if (valid(arr, i, j, K)) { 77 | max = Math.max(max, j - i + 1); 78 | } 79 | } 80 | } 81 | return max; 82 | } 83 | 84 | // for test 85 | public static boolean valid(int[] arr, int L, int R, int K) { 86 | int sum = 0; 87 | for (int i = L; i <= R; i++) { 88 | sum += arr[i]; 89 | } 90 | return sum == K; 91 | } 92 | 93 | // for test 94 | public static int[] generatePositiveArray(int size, int value) { 95 | int[] ans = new int[size]; 96 | for (int i = 0; i != size; i++) { 97 | ans[i] = (int) (Math.random() * value) + 1; 98 | } 99 | return ans; 100 | } 101 | 102 | // for test 103 | public static void printArray(int[] arr) { 104 | for (int i = 0; i != arr.length; i++) { 105 | System.out.print(arr[i] + " "); 106 | } 107 | System.out.println(); 108 | } 109 | 110 | public static void main(String[] args) { 111 | int len = 50; 112 | int value = 100; 113 | int testTime = 500000; 114 | System.out.println("test begin"); 115 | for (int i = 0; i < testTime; i++) { 116 | int[] arr = generatePositiveArray(len, value); 117 | int K = (int) (Math.random() * value) + 1; 118 | int ans1 = getMaxLength(arr, K); 119 | int ans2 = right(arr, K); 120 | if (ans1 != ans2) { 121 | System.out.println("Oops!"); 122 | printArray(arr); 123 | System.out.println("K : " + K); 124 | System.out.println(ans1); 125 | System.out.println(ans2); 126 | break; 127 | } 128 | } 129 | System.out.println("test end"); 130 | } 131 | 132 | } 133 | 134 | ``` 135 | 136 |

1.1.2 第二连例题

137 | 138 | 139 | 有一个数组,值可以为正可以为负可以为0。给定一个值sum,求子数组中累加和等于sum的最大长度? 140 | 141 | 142 | > 该题和第一连问题的区别是,数组的值可正可负可零,单调性消失了。对于数组问题,我们常见的解决子数组的思考思路,如果以每一个位置开头能求出一个答案,那么目标答案一定在其中。反过来如果以每一个位置为结尾能求出一个答案,那么目标答案一定也在其中 143 | 144 | 145 | > 该题思路用第二种比较方便,我们以某个位置i结尾,之前的数累加和等于目标sum,求该位置满足此条件的最长数组。该种思路等同于,从0位置开始到i位置的累加和(allSum),减去从0位置到最早和0位置的累加和等于allSum-sum的位置j。那么原问题的答案是j+1到j位置的长度。预置,0位置累加和位置等于-1位置 146 | 147 | 148 | ```Java 149 | public class Code02_LongestSumSubArrayLength { 150 | 151 | // arr数组,累加和为k的最长子数组返回 152 | public static int maxLength(int[] arr, int k) { 153 | if (arr == null || arr.length == 0) { 154 | return 0; 155 | } 156 | // key表示累加和,value表示最早出现的位置 157 | HashMap map = new HashMap(); 158 | // 0位置的累加和,最早出现在-1位置。预置 159 | map.put(0, -1); // important 160 | // 最大长度是多少 161 | int len = 0; 162 | // 累加和多大 163 | int sum = 0; 164 | for (int i = 0; i < arr.length; i++) { 165 | sum += arr[i]; 166 | if (map.containsKey(sum - k)) { 167 | // j+1到i有多少个数,i-j个 168 | len = Math.max(i - map.get(sum - k), len); 169 | } 170 | if (!map.containsKey(sum)) { 171 | map.put(sum, i); 172 | } 173 | } 174 | return len; 175 | } 176 | 177 | // for test 178 | public static int right(int[] arr, int K) { 179 | int max = 0; 180 | for (int i = 0; i < arr.length; i++) { 181 | for (int j = i; j < arr.length; j++) { 182 | if (valid(arr, i, j, K)) { 183 | max = Math.max(max, j - i + 1); 184 | } 185 | } 186 | } 187 | return max; 188 | } 189 | 190 | // for test 191 | public static boolean valid(int[] arr, int L, int R, int K) { 192 | int sum = 0; 193 | for (int i = L; i <= R; i++) { 194 | sum += arr[i]; 195 | } 196 | return sum == K; 197 | } 198 | 199 | // for test 200 | public static int[] generateRandomArray(int size, int value) { 201 | int[] ans = new int[(int) (Math.random() * size) + 1]; 202 | for (int i = 0; i < ans.length; i++) { 203 | ans[i] = (int) (Math.random() * value) - (int) (Math.random() * value); 204 | } 205 | return ans; 206 | } 207 | 208 | // for test 209 | public static void printArray(int[] arr) { 210 | for (int i = 0; i != arr.length; i++) { 211 | System.out.print(arr[i] + " "); 212 | } 213 | System.out.println(); 214 | } 215 | 216 | public static void main(String[] args) { 217 | int len = 50; 218 | int value = 100; 219 | int testTime = 500000; 220 | 221 | System.out.println("test begin"); 222 | for (int i = 0; i < testTime; i++) { 223 | int[] arr = generateRandomArray(len, value); 224 | int K = (int) (Math.random() * value) - (int) (Math.random() * value); 225 | int ans1 = maxLength(arr, K); 226 | int ans2 = right(arr, K); 227 | if (ans1 != ans2) { 228 | System.out.println("Oops!"); 229 | printArray(arr); 230 | System.out.println("K : " + K); 231 | System.out.println(ans1); 232 | System.out.println(ans2); 233 | break; 234 | } 235 | } 236 | System.out.println("test end"); 237 | 238 | } 239 | 240 | } 241 | ``` 242 | 243 | 244 | 对于数组arr,可正可负可零。求子数组中1和2数值个数相等的子数组最长的长度,返回? 245 | 246 | > 把数组中其他数值变为0,为1的数值仍为1,为2的数值变为-1。处理好之后,求子数组累加和为0的最长子数组。 247 | 248 | > 很多数组问题,可以转化为求子数组累加和问题,需要训练敏感度 249 | 250 | 251 |

1.1.2 第三连例题

252 | 253 | 一个数组arr中,有正数,有负数,有0。给定累加和目标k,求所有子数组累加和中小于等于k的数组的最大长度? 254 | 255 | 256 | 概念定义: 257 | 258 | 以i开头的子数组中,所有可能性中,哪一个能让累加和最小的范围是什么? 259 | 260 | 例如[3,7,4,-6,6,3,-2,0,7-3,2],准备两个辅助数组minSum[],minEnd[]。minSum记录从i开始后续子数组中累加和最小的值。minEnd记录i开始后续子数组中累加和最小的累加和的终止位置j 261 | 262 | 对于i位置,如果有变为正数,那么我累加和最小的就是我自己;如果自身是正数,累加和最小就是我右侧位置计算出来的最小累加和 263 | 264 | 如此操作,任意i位置,最小子数组累加和我们能拿到,最小累加和子数组的右边界我们能拿到 265 | 266 | 267 | > 该题巧妙之处是排除可能性,比较难 268 | 269 | 270 | ```Java 271 | public class Code03_LongestLessSumSubArrayLength { 272 | 273 | public static int maxLengthAwesome(int[] arr, int k) { 274 | if (arr == null || arr.length == 0) { 275 | return 0; 276 | } 277 | int[] minSums = new int[arr.length]; 278 | int[] minSumEnds = new int[arr.length]; 279 | minSums[arr.length - 1] = arr[arr.length - 1]; 280 | minSumEnds[arr.length - 1] = arr.length - 1; 281 | for (int i = arr.length - 2; i >= 0; i--) { 282 | if (minSums[i + 1] < 0) { 283 | minSums[i] = arr[i] + minSums[i + 1]; 284 | minSumEnds[i] = minSumEnds[i + 1]; 285 | } else { 286 | minSums[i] = arr[i]; 287 | minSumEnds[i] = i; 288 | } 289 | } 290 | int end = 0; 291 | int sum = 0; 292 | int res = 0; 293 | // i是窗口的最左的位置,end扩出来的最右有效块儿的最后一个位置的,再下一个位置 294 | // end也是下一块儿的开始位置 295 | // 窗口:[i~end) 296 | for (int i = 0; i < arr.length; i++) { 297 | // while循环结束之后: 298 | // 1) 如果以i开头的情况下,累加和<=k的最长子数组是arr[i..end-1],看看这个子数组长度能不能更新res; 299 | // 2) 如果以i开头的情况下,累加和<=k的最长子数组比arr[i..end-1]短,更新还是不更新res都不会影响最终结果; 300 | while (end < arr.length && sum + minSums[end] <= k) { 301 | sum += minSums[end]; 302 | end = minSumEnds[end] + 1; 303 | } 304 | res = Math.max(res, end - i); 305 | if (end > i) { // 窗口内还有数 [i~end) [4,4) 306 | sum -= arr[i]; 307 | } else { // 窗口内已经没有数了,说明从i开头的所有子数组累加和都不可能<=k 308 | end = i + 1; 309 | } 310 | } 311 | return res; 312 | } 313 | 314 | public static int maxLength(int[] arr, int k) { 315 | int[] h = new int[arr.length + 1]; 316 | int sum = 0; 317 | h[0] = sum; 318 | for (int i = 0; i != arr.length; i++) { 319 | sum += arr[i]; 320 | h[i + 1] = Math.max(sum, h[i]); 321 | } 322 | sum = 0; 323 | int res = 0; 324 | int pre = 0; 325 | int len = 0; 326 | for (int i = 0; i != arr.length; i++) { 327 | sum += arr[i]; 328 | pre = getLessIndex(h, sum - k); 329 | len = pre == -1 ? 0 : i - pre + 1; 330 | res = Math.max(res, len); 331 | } 332 | return res; 333 | } 334 | 335 | public static int getLessIndex(int[] arr, int num) { 336 | int low = 0; 337 | int high = arr.length - 1; 338 | int mid = 0; 339 | int res = -1; 340 | while (low <= high) { 341 | mid = (low + high) / 2; 342 | if (arr[mid] >= num) { 343 | res = mid; 344 | high = mid - 1; 345 | } else { 346 | low = mid + 1; 347 | } 348 | } 349 | return res; 350 | } 351 | 352 | // for test 353 | public static int[] generateRandomArray(int len, int maxValue) { 354 | int[] res = new int[len]; 355 | for (int i = 0; i != res.length; i++) { 356 | res[i] = (int) (Math.random() * maxValue) - (maxValue / 3); 357 | } 358 | return res; 359 | } 360 | 361 | public static void main(String[] args) { 362 | System.out.println("test begin"); 363 | for (int i = 0; i < 10000000; i++) { 364 | int[] arr = generateRandomArray(10, 20); 365 | int k = (int) (Math.random() * 20) - 5; 366 | if (maxLengthAwesome(arr, k) != maxLength(arr, k)) { 367 | System.out.println("Oops!"); 368 | } 369 | } 370 | System.out.println("test finish"); 371 | } 372 | 373 | } 374 | ``` 375 | 376 | 377 | -------------------------------------------------------------------------------- /19-《进阶》打表和矩阵处理相关问题.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 打表技巧和矩阵处理技巧 4 | 5 | 在一个数组arr中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? 6 | 7 | `10=2*5` 8 | 9 | `9=3*3` 10 | 11 | `6=3*3` 12 | 13 | `12=3*2*2` 14 | 15 | 我们可以把1000以内的数的质数因子个数求出来,存到我们的表中,查表即可 16 | 17 | 18 | ## 1.1 打表法 19 | 20 | 1)问题如果返回值不太多,可以用hardcode的方式列出,作为程序的一部分 21 | 22 | 2)一个大问题解决时底层频繁使用规模不大的小问题的解,如果小问题的返回值满足条件1),可以把小问题的解列成一张表,作为程序的一部分 23 | 24 | 3)打表找规律(本节课重点) 25 | 26 | 27 | ### 1.1.1 打表找规律 28 | 29 | 1)某个面试题,输入参数类型简单,并且只有一个实际参数 30 | 31 | 2)要求的返回值类型也简单,并且只有一个 32 | 33 | 3)用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code 34 | 35 | 36 | ### 1.1.2 例题1 小虎买苹果 37 | 38 | 小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。 39 | 40 | 1)能装下6个苹果的袋子 41 | 42 | 2)能装下8个苹果的袋子 43 | 44 | 小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。 45 | 给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1 46 | 47 | 48 | > 暴力思路,例如N=100个苹果,我们全部用8号袋装,最多使用12个8号袋子,剩4个苹果,6号袋没装满。8号袋减1,需要2个6号袋,满足。如果依次递减8号袋,为0个仍未有答案,则无解 49 | 50 | 51 | ```Go 52 | package main 53 | 54 | import "fmt" 55 | 56 | func minBags(apple int) int { 57 | if apple < 0 { 58 | return -1 59 | } 60 | 61 | bag6 := -1 62 | bag8 := apple / 8 63 | rest := apple - 8 * bag8 64 | for bag8 >= 0 && rest < 24 { 65 | restUse6 := minBagBase6(rest) 66 | if restUse6 != -1 { 67 | bag6 = restUse6 68 | break 69 | } 70 | 71 | rest = apple - 8 * (bag8) 72 | bag8-- 73 | } 74 | if bag6 == -1 { 75 | return -1 76 | } else { 77 | return bag6 + bag8 78 | } 79 | } 80 | 81 | // 如果剩余苹果rest可以被装6个苹果的袋子搞定,返回袋子数量 82 | // 不能搞定返回-1 83 | func minBagBase6(rest int) int { 84 | if rest % 6 == 0 { 85 | return rest / 6 86 | } else { 87 | return -1 88 | } 89 | } 90 | 91 | // 根据打表规律写code 92 | func minBagAwesome(apple int) int { 93 | if apple & 1 != 0 {// 如果是奇数,返回-1 94 | return -1 95 | } 96 | 97 | if apple < 18 { 98 | if apple == 0 { 99 | return 0 100 | } else { 101 | if apple == 6 || apple == 8 { 102 | return 1 103 | } else { 104 | if apple == 12 || apple == 24 || apple == 16 { 105 | return 2 106 | } else { 107 | return -1 108 | } 109 | } 110 | } 111 | } 112 | 113 | return (apple - 18) / 8 + 3 114 | } 115 | 116 | // 打表看规律,摒弃数学规律 117 | func main() { 118 | for apple:=1; apple < 100; apple++ { 119 | fmt.Println(minBags(apple)) 120 | } 121 | } 122 | ``` 123 | 124 | ### 1.1.2 例题2 牛羊吃草 125 | 126 | 给定一个正整数N,表示有N份青草统一堆放在仓库里 127 | 有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草 128 | 不管是牛还是羊,每一轮能吃的草量必须是: 129 | 130 | 1,4,16,64…(4的某次方) 131 | 132 | 谁最先把草吃完,谁获胜 133 | 134 | 假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定 135 | 136 | 根据唯一的参数N,返回谁会赢 137 | 138 | 139 | > 暴力思路打表找规律 140 | 141 | ``` 142 | package main 143 | 144 | import "fmt" 145 | 146 | // n份青草放在一堆 147 | // 先手后手都绝顶聪明 148 | // string "先手" "后手" 149 | func winner1(n int) string { 150 | // 0 1 2 3 4 151 | // 后 先 后 先 先 152 | // base case 153 | if n < 5 { 154 | if n == 0 || n == 2 { 155 | return "后手" 156 | } else { 157 | return "先手" 158 | } 159 | } 160 | 161 | // n >= 5 时 162 | base := 1 // 当前先手决定吃的草数 163 | // 当前是先手在选 164 | for base <= n { 165 | // 当前一共n份草,先手吃掉的是base份,n - base 是留给后手的草 166 | // 母过程 先手 在子过程里是 后手 167 | if winner1(n -base) == "后手" { 168 | return "先手" 169 | } 170 | if base > n / 4 { // 防止base*4之后溢出 171 | break 172 | } 173 | base *= 4 174 | } 175 | return "后手" 176 | } 177 | 178 | // 根据打表的规律,写代码 179 | func winner2(n int) string { 180 | if n % 5 == 0 || n % 5 == 2 { 181 | return "后手" 182 | } else { 183 | return "先手" 184 | } 185 | } 186 | 187 | // 暴力打表找规律 188 | func main() { 189 | for i:=0; i<=50; i++ { 190 | fmt.Println(fmt.Sprintf("%d : %s", i, winner1(i))) 191 | } 192 | } 193 | ``` 194 | 195 | ### 1.1.3 例题3 196 | 197 | 定义一种数:可以表示成若干(数量>1)连续正数和的数 198 | 比如: 199 | 200 | 5 = 2+3,5就是这样的数 201 | 202 | 12 = 3+4+5,12就是这样的数 203 | 204 | 1不是这样的数,因为要求数量大于1个、连续正数和 205 | 206 | 2 = 1 + 1,2也不是,因为等号右边不是连续正数 207 | 208 | 给定一个参数N,返回是不是可以表示成若干连续正数和的数 209 | 210 | 211 | ```Go 212 | package main 213 | 214 | import "fmt" 215 | 216 | // isMSum1 暴力法。给定一个参数N,返回是不是可以表示成若干连续正数和的数 217 | func isMSum1(num int) bool { 218 | for i := 1; i<=num; i++ { 219 | sum := i 220 | for j := i + 1; j <= num; j++ { 221 | if sum + j > num { 222 | break 223 | } 224 | if sum + j == num { 225 | return true 226 | } 227 | sum += j 228 | } 229 | } 230 | return false 231 | } 232 | 233 | // 根据打表的规律写代码 234 | func isMSum2(num int) bool { 235 | if num < 3 { 236 | return false 237 | } 238 | return (num & (num - 1)) != 0 239 | } 240 | 241 | // 打表 242 | func main() { 243 | for num := 1; num <200; num++ { 244 | fmt.Println(fmt.Sprintf("%d : %v", num, isMSum1(num))) 245 | } 246 | fmt.Println("test begin") 247 | for num := 1; num < 5000; num++ { 248 | if isMSum1(num) != isMSum2(num) { 249 | fmt.Println("Oops!") 250 | } 251 | } 252 | fmt.Println("test end") 253 | } 254 | ``` 255 | 256 | ### 1.2 矩阵处理技巧 257 | 258 | 1)zigzag打印矩阵 259 | 260 | 2)转圈打印矩阵 261 | 262 | 3)原地旋转正方形矩阵 263 | 264 | 核心技巧:找到coding上的宏观调度 265 | 266 | #### zigzag打印矩阵 267 | 268 | > 矩阵的特殊轨迹问题,不要把思维限制在具体某个坐标怎么变化 269 | 270 | 对于一个矩阵,如何绕圈打印,例如: 271 | 272 | ```math 273 | \begin{matrix} 274 | 1&2&3 \\ 275 | 4&5&6 \\ 276 | 7&8&9 \\ 277 | \end{matrix} 278 | ``` 279 | 280 | 打印的顺序为:1,2,4,7,5,3,6,8,9 281 | 282 | > 思路:准备A和B两个点,坐标都指向0,0位置。A和B同时走,A往右走,走到尽头后往下走,B往下走,走到不能再走了往右走。通过这么处理,A和B每个位置的连线都是一条斜线,且无重复。A和B每同时走一步,打印每次逆序打印,即开始时从B往A打印,下一步从A往B打印,循环往复 283 | 284 | 285 | ```Go 286 | package main 287 | 288 | import "fmt" 289 | 290 | func printMatrixZigZag(matrix [][]int) { 291 | // A的行row 292 | tR := 0 293 | // A的列coulum 294 | tC := 0 295 | // B的行row 296 | dR := 0 297 | // B的列coulum 298 | dC := 0 299 | // 终止位置的行和列 300 | endR := len(matrix) - 1 301 | endC := len(matrix[0]) - 1 302 | // 是不是从右上往左下打印 303 | fromUp := false 304 | // A的轨迹不会超过最后一行 305 | for tR != endR + 1 { 306 | // 告诉当前A和B,打印方向,完成打印 307 | printLevel(matrix, tR, tC, dR, dC, fromUp) 308 | // 打印完之后,A和B再移动。A到最右再向下,B到最下再向右 309 | if tC == endC { 310 | tR = tR + 1 311 | } else { 312 | tC = tC + 1 313 | } 314 | if dR == endR { 315 | dC = dC + 1 316 | } else { 317 | dR = dR + 1 318 | } 319 | // A和B来到下一个位置之后,改变打印方向 320 | fromUp = !fromUp 321 | } 322 | fmt.Println() 323 | } 324 | 325 | func printLevel(m [][]int, tR int, tC int, dR int, dC int, f bool) { 326 | if f { 327 | for tR != dR + 1 { 328 | fmt.Print(fmt.Sprintf("%d ", m[tR][tC])) 329 | tR++ 330 | tC-- 331 | } 332 | } else { 333 | for dR != tR -1 { 334 | fmt.Print(fmt.Sprintf("%d ", m[dR][dC])) 335 | dR-- 336 | dC++ 337 | } 338 | } 339 | } 340 | 341 | // 1 2 5 9 6 3 4 7 10 11 8 12 342 | func main() { 343 | matrix := [][]int{ 344 | { 1, 2, 3, 4 }, 345 | { 5, 6, 7, 8 }, 346 | { 9, 10, 11, 12 }, 347 | } 348 | printMatrixZigZag(matrix) 349 | } 350 | ``` 351 | 352 | #### 转圈打印矩阵 353 | 354 | 355 | ```math 356 | \begin{matrix} 357 | 1&2&3&4 \\ 358 | 5&6&7&8 \\ 359 | 9&10&11&12 \\ 360 | 13&14&15&16 \\ 361 | \end{matrix} 362 | ``` 363 | 364 | 打印轨迹是:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10 365 | 366 | > 思路:每个圈,我们知道左上角的位置,和右下角的位置,我们就可以得到需要转圈的圈的大小, 367 | 368 | ```Go 369 | package main 370 | 371 | import "fmt" 372 | 373 | // 转圈打印矩阵 374 | func spiralOrderPrint(matrix [][]int) { 375 | // A行 376 | tR := 0 377 | // A列 378 | tC := 0 379 | // B行 380 | dR := len(matrix) - 1 381 | // B列 382 | dC := len(matrix[0]) - 1 383 | for tR <= dR && tC <= dC { 384 | printEdge(matrix, tR, tC, dR, dC) 385 | tR++ 386 | tC++ 387 | dR-- 388 | dC-- 389 | } 390 | } 391 | 392 | // 当前打印,左上角和右下角的位置 393 | func printEdge(m [][]int, tR int, tC int, dR int, dC int) { 394 | // 表示区域只剩下一条横线的时候 395 | if tR == dR { 396 | for i:=tC; i<=dC; i++ { 397 | fmt.Print(fmt.Sprintf("%d ", m[tR][i])) 398 | } 399 | } else if tC == dC { // 表示区域只剩下一条竖线了 400 | for i:=tR; i<=dR; i++ { 401 | fmt.Print(fmt.Sprintf("%d ", m[i][tC])) 402 | } 403 | } else { // 通用情况 404 | curC := tC 405 | curR := tR 406 | for curC != dC { 407 | fmt.Print(fmt.Sprintf("%d ", m[tR][curC])) 408 | curC++ 409 | } 410 | for curR != dR { 411 | fmt.Print(fmt.Sprintf("%d ", m[curR][dC])) 412 | curR++ 413 | } 414 | for curC != tC { 415 | fmt.Print(fmt.Sprintf("%d ", m[dR][curC])) 416 | curC-- 417 | } 418 | for curR != tR { 419 | fmt.Print(fmt.Sprintf("%d ", m[curR][tC])) 420 | curR-- 421 | } 422 | } 423 | } 424 | 425 | // 1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10 426 | func main() { 427 | matrix := [][]int{ 428 | { 1, 2, 3, 4 }, 429 | { 5, 6, 7, 8 }, 430 | { 9, 10, 11, 12 }, 431 | { 13, 14, 15, 16 }, 432 | } 433 | spiralOrderPrint(matrix) 434 | } 435 | ``` 436 | 437 | #### 矩阵调整-原地旋转正方形矩阵 438 | 439 | 必须要是正方形矩阵,非正方形的旋转会越界;题意的意思是每一个数都顺时针旋转90度 440 | 441 | 442 | ```math 443 | \begin{matrix} 444 | 1&2&3&4 \\ 445 | 5&6&7&8 \\ 446 | 9&10&11&12 \\ 447 | 13&14&15&16 \\ 448 | \end{matrix} 449 | ``` 450 | 451 | 调整后的结构为: 452 | 453 | ```math 454 | \begin{matrix} 455 | 13&9&5&1 \\ 456 | 14&10&6&2 \\ 457 | 5&11&7&3 \\ 458 | 16&12&8&4 \\ 459 | \end{matrix} 460 | ``` 461 | 462 | 463 | > 思路:一圈一圈的转,和旋转打印思路比较像。按圈,再按小组旋转,第一圈的第一个小组为四个角。分别为:1,4,16,13;第二小组为:2,8,15,9;依次旋转小组,最终达到旋转该圈的目的。接着旋转下一个圈的各个小组。每一层的小组数目等于该圈的边长减1 464 | 465 | ```Go 466 | package main 467 | 468 | import "fmt" 469 | 470 | // 原地旋转正方形矩阵 471 | func rotate(matrix [][]int) { 472 | // a行 473 | a := 0 474 | // b列 475 | b := 0 476 | // c行 477 | c := len(matrix) - 1 478 | // d列 479 | d := len(matrix[0]) - 1 480 | // 由于是正方形矩阵,只需要判断行不越界,等同于判断列不越界 481 | for a < c { 482 | rotateEdge(matrix, a, b, c, d) 483 | a++ 484 | b++ 485 | c-- 486 | d-- 487 | } 488 | } 489 | 490 | // 当前需要转的圈的左上角和右下角 491 | func rotateEdge(m [][]int, a, b, c, d int) { 492 | tmp := 0 493 | // 得到左上角右下角坐标,我们可以知道右上角和左下角的位置,这四个位置先旋转。这四个位置称为一个小组。 494 | // 旋转完之后,找下四个位置的小组再旋转 495 | for i := 0; i < d - b; i++ { 496 | tmp = m[a][b + i] 497 | m[a][b + i] = m[c - i][b] 498 | m[c - i][b] = m[c][d - i] 499 | m[c][d - i] = m[a + i][d] 500 | m[a + i][d] = tmp 501 | } 502 | } 503 | 504 | func printMatrix(matrix [][]int) { 505 | for i := 0; i != len(matrix); i++ { 506 | for j := 0; j != len(matrix[0]); j++ { 507 | fmt.Print(fmt.Sprintf("%d ", matrix[i][j])) 508 | } 509 | fmt.Println() 510 | } 511 | } 512 | 513 | //1 2 3 4 514 | //5 6 7 8 515 | //9 10 11 12 516 | //13 14 15 16 517 | //============== 518 | //13 9 5 1 519 | //14 10 6 2 520 | //15 11 7 3 521 | //16 12 8 4 522 | func main() { 523 | matrix := [][]int{ 524 | { 1, 2, 3, 4 }, 525 | { 5, 6, 7, 8 }, 526 | { 9, 10, 11, 12 }, 527 | { 13, 14, 15, 16 }, 528 | } 529 | printMatrix(matrix) 530 | rotate(matrix) 531 | fmt.Println("==============") 532 | printMatrix(matrix) 533 | } 534 | ``` 535 | 536 | > 大量的矩阵变换都会涉及到一个宏观调度,不到万不得已,不要把自己陷入每个位置怎么变,扣每个位置的变化,会非常难 537 | 538 | 539 | 540 | 541 | -------------------------------------------------------------------------------- /15-《进阶》KMP算法与bfprt算法.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 KMP算法(面试高频,劝退) 4 | 5 | ## 1.1 KMP算法分析 6 | 7 | 查找字符串问题:例如我们有一个字符串str="abc1234efd"和match="1234"。我们如何查找str字符串中是否包含match字符串的子串? 8 | 9 | 10 | > 暴力解思路:循环str和match,挨个对比,最差情况为O(N*M)。时间复杂度为O(N*M) 11 | 12 | > KMP算法,在N大于M时,可以在时间复杂度为O(N)解决此类问题 13 | 14 | 15 | 我们对str记录字符坐标前的前缀后缀最大匹配长度,例如str="abcabck" 16 | 17 | 1、对于k位置前的字符,前后缀长度取1时,前缀为"a"后缀为"c"不相等 18 | 19 | 2、对于k位置前的字符,前后缀长度取2时,前缀为"ab"后缀为"bc"不相等 20 | 21 | 3、对于k位置前的字符,前后缀长度取3时,前缀为"abc"后缀为"abc"相等 22 | 23 | 4、对于k位置前的字符,前后缀长度取4时,前缀为"abca"后缀为"cabc"不相等 24 | 25 | 5、对于k位置前的字符,前后缀长度取5时,前缀为"abcab"后缀为"bcabc"不相等 26 | 27 | > 注意前后缀长度不可取k位置前的整体长度6。那么此时k位置前的最大匹配长度为3 28 | 29 | 所以,例如"aaaaaab","b"的坐标为6,那么"b"坐标前的前后缀最大匹配长度为5 30 | 31 | 32 | 我们对match建立坐标前后缀最大匹配长度数组,概念不存在的设置为-1,例如0位置前没有字符串,就为-1,1位置前只有一个字符,前后缀无法取和坐标前字符串相等,规定为0。例如"aabaabc",nextArr[]为[-1,0,1,0,1,2,3] 33 | 34 | 35 | > 暴力方法之所以慢,是因为每次比对,如果match的i位置前都和str匹配上了,但是match的i+1位置没匹配成功。那么str会回退到第一次匹配的下一个位置,match直接回退到0位置再次比对。str和match回退的位置太多,之前的信息全部作废,没有记录 36 | 37 | 38 | > 而KMP算法而言,如果match的i位置前都和str匹配上了,但是match的i+1位置没匹配成功,那么str位置不回跳,match回跳到当前i+1位置的最大前后缀长度的位置上,去和当前str位置比对。 39 | 40 | 原理是如果我们当前match位置i+1比对失败了,我们跳到最大前后缀长度的下一个位置去和当前位置比对,如果能匹配上,由于i+1位置之前都匹配的上,那么match的最大后缀长度也比对成功,可以被我们利用起来。替换成match的前缀长度上去继续对比,起到加速的效果 41 | 42 | 43 | 那么为什么str和match最后一个不相等的位置,之前的位置无法配出match,可以反证,如果可以配置出来,那么该串的头信息和match的头信息相等,得出存在比match当前不等位置最大前后缀还要大的前后缀,矛盾 44 | 45 | Code: 46 | 47 | ```Go 48 | package main 49 | 50 | import "fmt" 51 | 52 | // getIndexOf O(N) 53 | func getIndexOf(s string, m string) int { 54 | if len(s) == 0 || len(m) == 0 || len(s) < len(m) { 55 | return -1 56 | } 57 | 58 | str := []byte(s) 59 | match := []byte(m) 60 | x := 0 // str中当前比对到的位置 61 | y := 0 // match中当前比对到的位置 62 | // match的长度M,M <= N O(M) 63 | next := getNextArray(match) // next[i] match中i之前的字符串match[0..i-1],最长前后缀相等的长度 64 | // O(N) 65 | // x在str中不越界,y在match中不越界 66 | for x < len(str) && y < len(match) { 67 | // 如果比对成功,x和y共同往各自的下一个位置移动 68 | if str[x] == match[y] { 69 | x++ 70 | y++ 71 | } else if next[y] == -1 { // 表示y已经来到了0位置 y == 0 72 | // str换下一个位置进行比对 73 | x++ 74 | } else { // y还可以通过最大前后缀长度往前移动 75 | y = next[y] 76 | } 77 | } 78 | // 1、 x越界,y没有越界,找不到,返回-1 79 | // 2、 x没越界,y越界,配出 80 | // 3、 x越界,y越界 ,配出,str的末尾,等于match 81 | // 只要y越界,就配出了,配出的位置等于str此时所在的位置x,减去y的长度。就是str存在匹配的字符串的开始位置 82 | if y == len(match) { 83 | return x - y 84 | } else { 85 | return -1 86 | } 87 | } 88 | 89 | // M O(M) 90 | func getNextArray(match []byte) []int { 91 | // 如果match只有一个字符,人为规定-1 92 | if len(match) == 1 { 93 | return []int{-1} 94 | } 95 | 96 | // match不止一个字符,人为规定0位置是-1,1位置是0 97 | next := make([]int, len(match)) 98 | 99 | next[0] = -1 100 | next[1] = 0 101 | 102 | i := 2 103 | // cn代表,cn位置的字符,是当前和i-1位置比较的字符 104 | cn := 0 105 | for i < len(next) { 106 | if match[i - 1] == match[cn] { // 跳出来的时候 107 | // next[i] = cn+1 108 | // i++ 109 | // cn++ 110 | // 等同于 111 | cn++ 112 | next[i] = cn 113 | i++ 114 | } else if cn > 0 { // 跳失败,如果cn>0说明可以继续跳 115 | cn = next[cn] 116 | } else { // 跳失败,跳到开头仍然不等 117 | next[i] = 0 118 | i++ 119 | } 120 | } 121 | return next 122 | } 123 | 124 | func main() { 125 | s := "abc1234efd" 126 | m := "1234" 127 | fmt.Println(getIndexOf(s, m)) 128 | } 129 | ``` 130 | 131 | ## 1.2 KMP算法应用 132 | 133 | ### 题目1:旋转词 134 | 135 | 例如Str1="123456",对于Str1的旋转词,字符串本身也是其旋转词,Str1="123456"的旋转词为,"123456","234561","345612","456123","561234","612345"。给定Str1和Str2,那么判断这个两个字符串是否互为旋转词?是返回true,不是返回false 136 | 137 | 138 | 暴力解法思路:把str1的所有旋转词都列出来,看str2是否在这些旋转词中。挨个便利str1,循环数组的方式,和str2挨个比对。O(N*N) 139 | 140 | KMP解法:str1拼接str1得到str',"123456123456",我们看str2是否是str'的子串 141 | 142 | 143 | ### 题目2:子树问题 144 | 145 | 给定两颗二叉树头结点,node1和node2,判断node2为头结点的树,是不是node1的某个子树? 146 | 147 | 148 | 149 | # 2 bfprt算法 (面试常见) 150 | 151 | 情形:在一个无序数组中,怎么求第k小的数。如果通过排序,那么排序的复杂度为O(n*logn)。问,如何O(N)复杂度解决这个问题? 152 | 153 | 思路1:我们利用快排的思想,对数组进行荷兰国旗partion过程,每一次partion可以得到随机数m小的区域,等于m的区域,大于m的区域。我们看我们m区域是否包含我们要找的第k小的树,如果没有根据比较,在m左区间或者m右区间继续partion,直到第k小的数在我们的的中间区域。 154 | 155 | 快排是左右区间都会再进行partion,而该问题只会命中大于区域或小于区域,时间复杂度得到优化。T(n)=T(n/2)+O(n),时间复杂度为O(N),由于m随机选,概率收敛为O(N) 156 | 157 | 158 | 思路2:bfprt算法,不使用概率求期望,复杂度仍然严格收敛到O(N) 159 | 160 | ## 2.1 bfprt算法分析 161 | 162 | 通过上文,利用荷兰国旗问题的思路为: 163 | 164 | 1、随机选一个数m 165 | 166 | 2、进行荷兰国旗,得到小于m区域,等于m区域,大于m区域 167 | 168 | 3、index命中到等于m区域,返回等于区域的左边界,否则比较,进入小于区域,或者大于区域,只会进入一个区域 169 | 170 | 171 | bfprt算法,再此基础上唯一的区别是,第一步,如何选择m。快排的思想是随机选择一个 172 | 173 | bfprt如何选择m? 174 | 175 | - 1、对arr分组,5个一组,所以0到4为一组,5到9为一组,最后不够一组的当成最后一组 176 | - 2、对各个小组进行排序。第一步和第二步进行下来,时间复杂度为O(N) 177 | - 3、把每一小组排序后的中间位置的数拿出来。放入一个数组中m[]。前三步统称为bfprt方法 178 | - 4、对m数组,取中位数,这个数就是我们需要的m 179 | 180 | ```math 181 | T(N) = T(N/5) + T(?) + O(N) 182 | ``` 183 | 184 | 建议画图分析: 185 | 186 | T(?)在我们随机选取m的时候,是不确定的,但是在bfprt中,m的左侧范围最多有多少个数,等同于m右侧最少有几个数。 187 | 188 | 假设我们经过分组拿到的m数组有5个数,中位数是我们的m,在m[]数组中,大于m的有2个,小于m的有2个。对于整的数据规模而言,m[]的规模是n/5。大于m[]中位数的规模为m[]的一半,也就是整体数据规模的n/10。 189 | 190 | 191 | 由于m[]中的每个数都是从小组中选出来的,那么对于整体数据规模而言,大于m的数整体为3n/10(每个n/10规模的数回到自己的小组,大于等于的每小组有3个) 192 | 193 | 194 | 那么最少有3n/10的规模是大于等于m的,那么对于整体数据规模而言最多有7n/10的小于m的。同理最多有7n/10的数据是大于m的 195 | 196 | 可得: 197 | 198 | ```math 199 | T(N) = T(N/5) + T(7n/10) + O(N) 200 | ``` 201 | 202 | 数学证明,以上公式无法通过master来算复杂度,但是数学证明复杂度严格O(N),证明略(算法导论第九章第三节) 203 | 204 | 205 | > bfprt算法在算法上的地位非常高,它发现只要涉及到我们随便定义的一个常数分组,得到一个表达式,最后收敛到O(N),那么就可以通过O(N)的复杂度测试 206 | 207 | 208 | ```Go 209 | package main 210 | 211 | import ( 212 | "container/heap" 213 | "fmt" 214 | "math" 215 | "math/rand" 216 | "time" 217 | ) 218 | 219 | type Heap []int 220 | 221 | func (h Heap) Less(i, j int) bool { 222 | return h[i] > h[j] // 大根堆。小根堆实现为: h[i] <= h[j] 223 | } 224 | 225 | func (h Heap) Len() int { 226 | return len(h) 227 | } 228 | 229 | func (h Heap) Swap(i, j int) { 230 | h[i], h[j] = h[j], h[i] 231 | } 232 | 233 | func (h *Heap) Push(v interface{}) { 234 | *h = append(*h, v.(int)) 235 | } 236 | 237 | func (h *Heap) Pop() interface{} { 238 | n := len(*h) 239 | x := (*h)[n-1] 240 | *h = (*h)[:n-1] 241 | return x 242 | } 243 | 244 | // minKth1 找一个数组中第k小的数。方法1:利用大根堆,时间复杂度O(N*logK) 245 | func minKth1(arr []int, k int) int { 246 | maxHeap := &Heap{} 247 | for i := 0; i < k; i++ { // 加入大根堆 248 | heap.Push(maxHeap, arr[i]) 249 | } 250 | heap.Init(maxHeap) 251 | 252 | for i := k; i < len(arr); i++ { 253 | if arr[i] < (*maxHeap)[0] { // arr[i] 小于堆顶元素。堆顶元素就是0位置元素 254 | // !!! 这里一定要使用系统包中的pop和push,然后把实现当前栈接口的结构传入 255 | heap.Pop(maxHeap) // 弹出 256 | heap.Push(maxHeap, arr[i]) // 入堆 257 | } 258 | } 259 | 260 | // return maxHeap.peek() 261 | return (*maxHeap)[0] 262 | } 263 | 264 | // minKth2 找一个数组中第k小的数。方法2:利用快排,时间复杂度O(N) 265 | func minKth2(array []int, k int) int { 266 | arr := copyArr(array) 267 | return process2(arr, 0, len(arr)-1, k-1) 268 | } 269 | 270 | // copyArr 克隆数组,防止快排影响原数组的元素顺序 271 | func copyArr(arr []int) []int { 272 | ans := make([]int, len(arr)) 273 | for i := 0; i < len(ans); i++ { // 这里copy数组,不可以使用append。 274 | ans[i] = arr[i] 275 | } 276 | return ans 277 | } 278 | 279 | // arr 第k小的数: process2(arr, 0, N-1, k-1) 280 | // arr[L..R] 范围上,如果排序的话(不是真的去排序),找位于index的数 281 | // index [L..R] 282 | // 通过荷兰国旗的优化,概率期望收敛于O(N) 283 | func process2(arr []int, L, R, index int) int { 284 | if L == R { // L == R ==INDEX 285 | return arr[L] 286 | } 287 | 288 | // 不止一个数 L + [0, R -L],随机选一个数. 289 | pivot := arr[L+rand.Intn(R-L)] 290 | 291 | // 返回以pivot为划分值的中间区域的左右边界 292 | // range[0] range[1] 293 | // L ..... R pivot 294 | // 0 1000 70...800 295 | rg := partition(arr, L, R, pivot) 296 | // 如果我们第k小的树正好在这个范围内,返回区域的左边界 297 | if index >= rg[0] && index <= rg[1] { 298 | return arr[index] 299 | } else if index < rg[0] { // index比该区域的左边界小,递归左区间 300 | return process2(arr, L, rg[0]-1, index) 301 | } else { // index比该区域的右边界大,递归右区间 302 | return process2(arr, rg[1]+1, R, index) 303 | } 304 | } 305 | 306 | // partition 荷兰国旗partition问题 307 | func partition(arr []int, L, R, pivot int) []int { 308 | less := L - 1 309 | more := R + 1 310 | cur := L 311 | for cur < more { 312 | if arr[cur] < pivot { 313 | less++ 314 | arr[less], arr[cur] = arr[cur], arr[less] 315 | cur++ 316 | } else if arr[cur] > pivot { 317 | more-- 318 | arr[cur], arr[more] = arr[more], arr[cur] 319 | } else { 320 | cur++ 321 | } 322 | } 323 | return []int{less + 1, more - 1} 324 | } 325 | 326 | // minKth3 找一个数组中第k小的数。方法3:利用bfprt算法,时间复杂度O(N) 327 | func minKth3(array []int, k int) int { 328 | arr := copyArr(array) 329 | return bfprt(arr, 0, len(arr)-1, k-1) 330 | } 331 | 332 | // bfprt arr[L..R] 如果排序的话,位于index位置的数,是什么,返回 333 | func bfprt(arr []int, L, R, index int) int { 334 | if L == R { 335 | return arr[L] 336 | } 337 | 338 | // 通过bfprt分组,最终选出m。不同于随机选择m作为划分值 339 | pivot := medianOfMedians(arr, L, R) 340 | rg := partition(arr, L, R, pivot) 341 | if index >= rg[0] && index <= rg[1] { 342 | return arr[index] 343 | } else if index < rg[0] { 344 | return bfprt(arr, L, rg[0]-1, index) 345 | } else { 346 | return bfprt(arr, rg[1]+1, R, index) 347 | } 348 | } 349 | 350 | // arr[L...R] 五个数一组 351 | // 每个小组内部排序 352 | // 每个小组中位数拿出来,组成marr 353 | // marr中的中位数,返回 354 | func medianOfMedians(arr []int, L, R int) int { 355 | size := R - L - L 356 | // 是否需要补最后一组,例如13,那么需要补最后一组,最后一组为3个数 357 | offset := -1 358 | if size%5 == 0 { 359 | offset = 0 360 | } else { 361 | offset = 1 362 | } 363 | 364 | // 初始化数组 365 | mArr := make([]int, size/5+offset) 366 | for team := 0; team < len(mArr); team++ { 367 | teamFirst := L + team*5 368 | // L ... L + 4 369 | // L +5 ... L +9 370 | // L +10....L+14 371 | mArr[team] = getMedian(arr, teamFirst, int(math.Min(float64(R), float64(teamFirst+4)))) 372 | } 373 | 374 | // marr中,找到中位数,原问题是arr拿第k小的数,这里是中位数数组拿到中间位置的数(第mArr.length / 2小的数),相同的问题 375 | // 返回值就是我们需要的划分值m 376 | // marr(0, marr.len - 1, mArr.length / 2 ) 377 | return bfprt(mArr, 0, len(mArr)-1, len(mArr)/2) 378 | } 379 | 380 | func getMedian(arr []int, L, R int) int { 381 | insertionSort(arr, L, R) 382 | return arr[(L+R)/2] 383 | } 384 | 385 | // insertionSort 插入排序 386 | func insertionSort(arr []int, L, R int) { 387 | for i := L + 1; i <= R; i++ { 388 | for j := i - 1; j >= L && arr[j] > arr[j+1]; j-- { 389 | arr[j], arr[j+1] = arr[j+1], arr[j] 390 | } 391 | } 392 | } 393 | 394 | func main() { 395 | /* 396 | rand.Seed: 397 | 还函数是用来创建随机数的种子,如果不执行该步骤创建的随机数是一样的,因为默认Go会使用一个固定常量值来作为随机种子。 398 | 399 | time.Now().UnixNano(): 400 | 当前操作系统时间的毫秒值 401 | */ 402 | rand.Seed(time.Now().UnixNano()) 403 | arr := []int{3, 4, 6, 1, 77, 35, 26, 83, 56, 37} 404 | fmt.Println(minKth1(arr, 3)) 405 | fmt.Println(minKth2(arr, 3)) 406 | fmt.Println(minKth3(arr, 3)) 407 | } 408 | ``` 409 | 410 | ## 2.2 bfprt算法应用 411 | 412 | 题目:求一个数组中,拿出所有比第k小的数还小的数 413 | 414 | 可以通过bfprt拿到第k小的数,再对原数组遍历一遍,小于该数的拿出来,不足k位的,补上第k小的数 415 | 416 | > 对于这类问题,笔试的时候最好选择随机m,进行partion。而不是选择bfprt。bfprt的常数项高。面试的时候可以选择bfprt算法 417 | 418 | 419 | -------------------------------------------------------------------------------- /26-附:二叉树专题汇总.md: -------------------------------------------------------------------------------- 1 | - 二叉树基础 2 | 3 | ```Go 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "strconv" 9 | ) 10 | 11 | // Node 二叉树的节点定义 12 | type Node struct { 13 | Value int 14 | Left *Node 15 | Right *Node 16 | } 17 | 18 | // 1、递归先序遍历 19 | func pre(head *Node) { 20 | if head == nil { 21 | return 22 | } 23 | 24 | fmt.Println(head.Value) 25 | pre(head.Left) 26 | pre(head.Right) 27 | } 28 | 29 | // 2、递归中序遍历 30 | func mid(head *Node) { 31 | if head == nil { 32 | return 33 | } 34 | 35 | mid(head.Left) 36 | fmt.Println(head.Value) 37 | mid(head.Right) 38 | } 39 | 40 | // 3、递归后序遍历 41 | func pos(head *Node) { 42 | if head == nil { 43 | return 44 | } 45 | 46 | pos(head.Left) 47 | pos(head.Right) 48 | fmt.Println(head.Value) 49 | } 50 | 51 | // 4、非递归先序 52 | func notRPre(head *Node) { 53 | fmt.Println("pre-order: ") 54 | 55 | if head != nil { 56 | // 借助栈结构,手动压栈 57 | stack := make([]*Node, 0) 58 | stack = append(stack, head) 59 | 60 | for len(stack) != 0 { 61 | // 弹出就打印 62 | head = stack[len(stack) - 1] 63 | stack = stack[:len(stack) - 1] 64 | 65 | fmt.Println(head.Value) 66 | 67 | // 右孩子不为空,先压入右孩子。右孩子就会后弹出 68 | if head.Right != nil { 69 | stack = append(stack, head.Right) 70 | } 71 | 72 | // 左孩子不为空,压入左孩子,左孩子在右孩子之后压栈,先弹出 73 | if head.Left != nil { 74 | stack = append(stack, head.Left) 75 | } 76 | } 77 | } 78 | } 79 | 80 | // 5、非递归中序 81 | func notRMid(head *Node) { 82 | fmt.Println("mid-order: ") 83 | 84 | if head != nil { 85 | stack := make([]*Node, 0) 86 | 87 | for len(stack) != 0 || head != nil { 88 | // 整条左边界依次入栈 89 | if head != nil { 90 | stack = append(stack, head) 91 | // head滑到自己的左孩子,左孩子有可能为空,但空的节点不会加入栈,下一个分支会判空处理 92 | head = head.Left 93 | // 左边界到头弹出一个打印,来到该节点右节点,再把该节点的左树以此进栈 94 | } else { // head为空的情况,栈顶是上次头结点的现场,head等于栈顶,回到上一个现场。打印后,head往右树上滑动 95 | head = stack[len(stack) - 1] 96 | stack = stack[:len(stack) - 1] 97 | 98 | fmt.Println(head.Value) 99 | head = head.Right 100 | } 101 | } 102 | } 103 | } 104 | 105 | // 6、非递归后序,借助两个栈,比借助一个栈容易理解 106 | func notRPos(head *Node) { 107 | fmt.Println("pos-order: ") 108 | 109 | if head != nil { 110 | stack1 := make([]*Node, 0) 111 | stack2 := make([]*Node, 0) 112 | 113 | stack1 = append(stack1, head) 114 | 115 | for len(stack1) != 0 { 116 | head = stack1[len(stack1) - 1] 117 | stack1 = stack1[:len(stack1) - 1] 118 | 119 | stack2 = append(stack2, head) 120 | if head.Left != nil { 121 | stack1 = append(stack1, head.Left) 122 | } 123 | if head.Right != nil { 124 | stack1 = append(stack1, head.Right) 125 | } 126 | } 127 | 128 | for len(stack2) != 0 { 129 | cur := stack2[len(stack2) - 1] 130 | stack2 = stack2[:len(stack2) - 1] 131 | fmt.Println(cur.Value) 132 | } 133 | } 134 | fmt.Println() 135 | } 136 | 137 | // 7、非递归后序,仅借助一个栈,比较有技巧 138 | func notRPos2(head *Node) { 139 | fmt.Println("pos-order: ") 140 | 141 | if head != nil { 142 | stack := make([]*Node, 0) 143 | stack = append(stack, head) 144 | var c *Node 145 | 146 | for len(stack) != 0 { 147 | c = stack[len(stack) - 1] // stack peek 148 | if c.Left != nil && head != c.Left && head != c.Right { 149 | stack = append(stack, c.Left) 150 | } else if c.Right != nil && head != c.Right { 151 | stack = append(stack, c.Right) 152 | } else { 153 | stack = stack[:len(stack) - 1] // pop 154 | fmt.Println(c.Value) 155 | head = c 156 | } 157 | } 158 | } 159 | fmt.Println() 160 | } 161 | 162 | // 8、按层遍历,即宽度优先遍历 163 | func level(head *Node) { 164 | if head == nil { 165 | return 166 | } 167 | 168 | queue := make([]*Node, 0) 169 | queue = append(queue, head) 170 | 171 | for len(queue) != 0 { 172 | cur := queue[0] // queue poll 173 | queue = queue[1:] 174 | 175 | // 打印当前 176 | fmt.Println(cur.Value) 177 | 178 | // 当前节点有左孩子,加入左孩子进队列 179 | if cur.Left != nil { 180 | queue = append(queue, cur.Left) 181 | } 182 | // 当前节点有右孩子,加入右孩子进队列 183 | if cur.Right != nil { 184 | queue = append(queue, cur.Right) 185 | } 186 | } 187 | } 188 | 189 | // 9、二叉树的先序序列化 190 | func preSerial(head *Node) []string { 191 | ansQueue := make([]string, 0) 192 | 193 | pres(head, ansQueue) 194 | return ansQueue 195 | } 196 | 197 | func pres(head *Node, ans []string) { 198 | if head == nil { 199 | ans = append(ans, "") 200 | } else { 201 | ans = append(ans, fmt.Sprintf("%d", head.Value)) 202 | pres(head.Left, ans) 203 | pres(head.Right, ans) 204 | } 205 | } 206 | 207 | // 10、根据先序序列化的结果,反序列化成一颗树 208 | func buildByPreQueue(prelist []string) *Node { 209 | if len(prelist) == 0 { 210 | return nil 211 | } 212 | 213 | return preb(prelist) 214 | } 215 | 216 | func preb(prelist []string) *Node { 217 | value := prelist[0] 218 | prelist = prelist[1:] 219 | 220 | // 如果头节点是空的话,返回空 221 | if value == "" { 222 | return nil 223 | } 224 | 225 | // 否则根据第一个值构建先序的头结点 226 | v, _ := strconv.Atoi(value) 227 | head := &Node{ 228 | Value: v, 229 | Left: nil, 230 | Right: nil, 231 | } 232 | // 递归建立左树 233 | head.Left = preb(prelist) 234 | // 递归建立右树 235 | head.Right = preb(prelist) 236 | return head 237 | } 238 | ``` 239 | 240 | - 二叉树应用 241 | 242 | ```Go 243 | package main 244 | 245 | import "math" 246 | 247 | // Node 二叉树的节点定义 248 | type Node struct { 249 | Value int 250 | Left *Node 251 | Right *Node 252 | } 253 | 254 | 255 | // IsBalanced 1、判断二叉树是否是平衡的 256 | func IsBalanced(head *Node) bool { 257 | return isBalancedProcess(head).isBalanced 258 | } 259 | 260 | // 递归过程信息 261 | type isBalancedInfo struct { 262 | isBalanced bool 263 | height int 264 | } 265 | 266 | // 递归调用,head传入整体需要返回一个信息 267 | // 解决当前节点的Info信息怎么得来 268 | func isBalancedProcess(head *Node) *isBalancedInfo { 269 | if head == nil { 270 | return &isBalancedInfo{ 271 | isBalanced: true, 272 | height: 0, 273 | } 274 | } 275 | 276 | leftInfo := isBalancedProcess(head.Left) 277 | rightInfo := isBalancedProcess(head.Right) 278 | 279 | // 当前节点的高度,等于左右树最大的高度,加上当前节点高度1 280 | cHeight := int(math.Max(float64(leftInfo.height), float64(rightInfo.height))) + 1 281 | isBalanced := true 282 | 283 | if !leftInfo.isBalanced || !rightInfo.isBalanced || int(math.Abs(float64(leftInfo.height - rightInfo.height))) > 1 { 284 | isBalanced = false 285 | } 286 | 287 | return &isBalancedInfo{ 288 | isBalanced: isBalanced, 289 | height: cHeight, 290 | } 291 | } 292 | 293 | // MaxDistance 2、二叉树中,获取任意两个节点的最大距离 294 | func MaxDistance(head *Node) int { 295 | return maxDistanceProcess(head).maxDistance 296 | } 297 | 298 | type maxDistanceInfo struct { 299 | maxDistance int 300 | height int 301 | } 302 | 303 | func maxDistanceProcess(head *Node) *maxDistanceInfo { 304 | if head == nil { 305 | return &maxDistanceInfo{ 306 | maxDistance: 0, 307 | height: 0, 308 | } 309 | } 310 | 311 | leftInfo := maxDistanceProcess(head.Left) 312 | rightInfo := maxDistanceProcess(head.Right) 313 | 314 | // 当前节点为头的情况下,高度等于左右树较大的高度,加上1 315 | height := int(math.Max(float64(leftInfo.height), float64(rightInfo.height))) + 1 316 | 317 | // 当前节点为头的情况下,最大距离等于,左右树距离较大的那个距离(与当前节点无关的情况) 318 | // 和左右树高度相加再加上当前节点距离1的距离(与当前节点有关的情况)取这两种情况较大的那个 319 | maxDistance := int(math.Max(math.Max(float64(leftInfo.maxDistance), float64(rightInfo.maxDistance)), 320 | float64(leftInfo.height + rightInfo.height + 1))) 321 | 322 | return &maxDistanceInfo{ 323 | maxDistance: maxDistance, 324 | height: height, 325 | } 326 | } 327 | 328 | // IsFull 3、判断一颗树是否是满二叉树 329 | func IsFull(head *Node) bool { 330 | if head == nil { 331 | return true 332 | } 333 | 334 | all := isFullProcess(head) 335 | 336 | return (1 << all.height) - 1 == all.nodes 337 | } 338 | 339 | // 判断一棵树是否是满二叉树,每个节点需要返回的信息 340 | type isFullInfo struct { 341 | height int 342 | nodes int 343 | } 344 | 345 | func isFullProcess(head *Node) *isFullInfo { 346 | if head == nil { // base 空节点的高度为0,节点数量也0 347 | return &isFullInfo{ 348 | height: 0, 349 | nodes: 0, 350 | } 351 | } 352 | 353 | leftInfo := isFullProcess(head.Left) 354 | rightInfo := isFullProcess(head.Right) 355 | 356 | // 当前节点为头的树,高度 357 | height := int(math.Max(float64(leftInfo.height), float64(rightInfo.height)) + 1) 358 | // 当前节点为头的树,节点数量 359 | nodes := leftInfo.nodes + rightInfo.nodes + 1 360 | 361 | return &isFullInfo{ 362 | height: height, 363 | nodes: nodes, 364 | } 365 | } 366 | 367 | // GetMaxLength 4、找到二叉树中节点和等于sum的最长路径 368 | func GetMaxLength(head *Node, sum int) int { 369 | sumMap := make(map[int]int, 0) 370 | sumMap[0] = 0 371 | 372 | return preOrder(head, sum, 0, 1, 0, sumMap) 373 | } 374 | 375 | func preOrder(head *Node, sum int, preSum int, level int, maxLen int, sumMap map[int]int) int { 376 | if head == nil { 377 | return maxLen 378 | } 379 | 380 | curSum := preSum + head.Value 381 | if _, ok := sumMap[curSum]; !ok { 382 | sumMap[curSum] = level 383 | } 384 | 385 | if v, ok := sumMap[curSum - sum]; ok { 386 | maxLen = int(math.Max(float64(level - v), float64(maxLen))) 387 | } 388 | 389 | maxLen = preOrder(head.Left, sum, curSum, level + 1, maxLen, sumMap) 390 | maxLen = preOrder(head.Right, sum, curSum, level + 1, maxLen, sumMap) 391 | 392 | if level == sumMap[curSum] { 393 | delete(sumMap, curSum) 394 | } 395 | 396 | return maxLen 397 | } 398 | 399 | // LowestCommonAncestor 5、二叉树,给定头结点节点,及树上的两个人节点,求这两个节点的最近公共祖先 400 | func LowestCommonAncestor(root *Node, p *Node, q *Node) *Node { 401 | // 如果树为空,直接返回null; 402 | // 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先) 403 | if root == nil || p == root || q == root { 404 | return root 405 | } 406 | 407 | // 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁 408 | left := LowestCommonAncestor(root.Left, p, q) 409 | // 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁 410 | right := LowestCommonAncestor(root.Right, p, q) 411 | 412 | // left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root 413 | if left != nil && right != nil { 414 | return root 415 | } 416 | 417 | // 如果在左子树中p和q都找不到,则p和q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) 418 | // 否则,如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况, 419 | // 如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先) 420 | if left == nil { 421 | return right 422 | } else { 423 | return left 424 | } 425 | } 426 | 427 | // IsSymmetric 6、给定一个二叉树头节点,判断这颗树是否是镜面堆成的。即是否是是镜像二叉树 428 | func IsSymmetric(root *Node) bool { 429 | // 自身,和自身的镜像树去递归比较 430 | return isMirror(root, root) 431 | } 432 | 433 | // 一棵树是原始树 head1 434 | // 另一棵是翻面树 head2 435 | func isMirror(head1, head2 *Node) bool { 436 | // base case 当前镜像的节点都为空,也算合法的镜像 437 | if head1 == nil && head2 == nil { 438 | return true 439 | } 440 | 441 | // 互为镜像的两个点不为空 442 | if head1 != nil && head2 != nil { 443 | // 当前两个镜像点要是相等的, 444 | // A树的左树和B树的右树互为镜像且满足,且A树的右树和B树的左树互为镜像,且满足。 445 | // 那么当前的镜像点下面的都是满足的 446 | return head1.Value == head2.Value && isMirror(head1.Left, head2.Right) && isMirror(head1.Right, head2.Left) 447 | } 448 | // 一个为空,一个不为空 肯定不构成镜像 false 449 | return false 450 | } 451 | ``` -------------------------------------------------------------------------------- /03-归并排序、随机快排.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 归并排序、随机快排 3 | 4 | ## 1.1 归并排序 5 | 6 | 1、 整体是递归的,左边排好序右边排好序,最后merge让整体有序,merge过程需要申请和被排序数组等长度的辅助空间 7 | 8 | 2、 让其整体有序的过程里用了排外序的方法 9 | 10 | 3、 利用master公式来求解归并的时间复杂度 11 | 12 | 4、 归并排序可改为非递归实现 13 | 14 | ### 1.1.1 递归思路: 15 | 16 | > 主函数希望一个数组的0~3位置排序f(arr, 0, 3) 17 | 18 | > 第一层递归希望f(arr, 0, 1)和f(arr, 2, 3)分别有序。 19 | 20 | > 第二层递归:f(arr, 0, 1)希望f(arr, 0, 0)和f(arr, 1, 1)有序, f(arr, 2, 3)希望f(arr, 2, 2)和f(arr, 3, 3)分别有序。 21 | 22 | > f(arr, 0, 0)和f(arr, 1, 1)已经有序,回到第一层递归f(arr, 0, 1)中去merge0位置的数和1位置的数后刷回元素组的0到1位置,0到1位置变为有序; f(arr, 2, 2)和f(arr, 3, 3)已经有序,回到f(arr, 2, 3)中去merge2位置的数和3位置的数后刷回原数组的2到3位置,2到3位置变为有序。 23 | 24 | > f(arr, 0, 3)需要merge f(arr, 0, 1)和f(arr, 2, 3)此时f(arr, 0, 1)和f(arr, 2, 3)已经有序merge后copy到原数组的0到3位置。于是f(arr, 0, 3)整体有序 25 | 26 | ### 1.1.2 非递归思路 27 | 28 | > 对于一个给定长度为n的数组arr,我们希望arr有序 29 | 30 | > 初始分组为a=2,我们让每两个有序,不够一组的当成一组 31 | 32 | > 分组变为a=2*2=4,由于上一步已经保证了两两有序,那么我们可以当前分组的四个数的前两个和后两个数merge使得每四个数有序 33 | 34 | > 分组变为a=2*4=8,...直至a>=n,整体有序 35 | 36 | ```Go 37 | // mergeSort归并排序递归实现 38 | func mergeSort(arr []int) { 39 | // 空数组或者只存在1个元素 40 | if len(arr) < 2 { 41 | return 42 | } 43 | 44 | // 传入被排序数组,以及左右边界到递归函数 45 | process(arr, 0, len(arr)-1) 46 | } 47 | 48 | // process 使得数组arr的L到R位置变为有序 49 | func process(arr []int, L, R int) { 50 | if L == R { // base case 51 | return 52 | } 53 | 54 | mid := L + (R-L)/2 55 | process(arr, L, mid) 56 | process(arr, mid+1, R) 57 | // 当前栈顶左右已经排好序,准备左右merge,注意这里的merge动作递归的每一层都会调用 58 | merge(arr, L, mid, R) 59 | } 60 | 61 | // merge arr L到M有序 M+1到R有序 变为arr L到R整体有序 62 | func merge(arr []int, L, M, R int) { 63 | // merge过程申请辅助数组,准备copy 64 | help := make([]int, 0) 65 | p1 := L 66 | p2 := M + 1 67 | // p1未越界且p2未越界 68 | for p1 <= M && p2 <= R { 69 | if arr[p1] <= arr[p2] { 70 | help = append(help, arr[p1]) 71 | p1++ 72 | } else { 73 | help = append(help, arr[p2]) 74 | p2++ 75 | } 76 | } 77 | 78 | // p2越界的情况 79 | for p1 <= M { 80 | help = append(help, arr[p1]) 81 | p1++ 82 | } 83 | 84 | // p1越界的情况 85 | for p2 <= R { 86 | help = append(help, arr[p2]) 87 | p2++ 88 | } 89 | 90 | // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 91 | for j := 0; j < len(help); j++ { 92 | arr[L+j] = help[j] 93 | } 94 | } 95 | ``` 96 | 97 | ```Go 98 | // 归并排序非递归实现 99 | func mergeSort2(arr []int) { 100 | if len(arr) < 2 { 101 | return 102 | } 103 | 104 | N := len(arr) 105 | // 当前有序的,左组长度, 那么实质分组大小是从2开始的 106 | mergeSize := 1 107 | for mergeSize < N { 108 | // L表示当前分组的左组的位置,初始为第一个分组的左组位置为0 109 | L := 0 110 | for L < N { 111 | // L...M 当前左组(mergeSize) 112 | M := L + mergeSize - 1 113 | // 当前左组包含当前分组的所有元素,即没有右组了,无需merge已经有序 114 | if M >= N { 115 | break 116 | } 117 | // L...M为左组 M+1...R(mergeSize)为右组。 118 | // 右组够mergeSize个的时候,右坐标为M + mergeSize,右组不够的情况下右组边界坐标为整个数组右边界N - 1 119 | R := math.Min(float64(M+mergeSize), float64(N-1)) 120 | // 把当前组进行merge 121 | merge(arr, L, M, int(R)) 122 | L = int(R) + 1 123 | } 124 | // 如果mergeSize乘2必定大于N,直接break。 125 | // 防止mergeSize溢出,有可能N很大,下面乘2有可能范围溢出(整形数大于21亿) 126 | if mergeSize > N/2 { 127 | break 128 | } 129 | mergeSize *= 2 130 | } 131 | } 132 | 133 | // merge arr L到M有序 M+1到R有序 变为arr L到R整体有序 134 | func merge(arr []int, L, M, R int) { 135 | // merge过程申请辅助数组,准备copy 136 | help := make([]int, 0) 137 | p1 := L 138 | p2 := M + 1 139 | // p1未越界且p2未越界 140 | for p1 <= M && p2 <= R { 141 | if arr[p1] <= arr[p2] { 142 | help = append(help, arr[p1]) 143 | p1++ 144 | } else { 145 | help = append(help, arr[p2]) 146 | p2++ 147 | } 148 | } 149 | 150 | // p2越界的情况 151 | for p1 <= M { 152 | help = append(help, arr[p1]) 153 | p1++ 154 | } 155 | 156 | // p1越界的情况 157 | for p2 <= R { 158 | help = append(help, arr[p2]) 159 | p2++ 160 | } 161 | 162 | // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 163 | for j := 0; j < len(help); j++ { 164 | arr[L+j] = help[j] 165 | } 166 | } 167 | ``` 168 | 169 | ### 1.1.3 归并排序时间复杂度 170 | 171 | > 递归复杂度计算,用master公式带入,子问题规模N/2,调用2次,除了递归之外的时间复杂度为merge的时间复杂度,为O(N)。a=2,b=2,d=1满足master第一条logb^a == d规则 172 | 173 | ```math 174 | T(N) = 2T(N/2) + O(N) => O(N*logN) 175 | ``` 176 | 177 | > 非递归复杂度计算,mergeSize*2等于分组从2->4->8->...,每个分组下执行merge操作O(N)。所以非递归和递归的时间复杂度相同,也为O(N)*O(logN) = O(NlogN) 178 | 179 | > 所以递归和非递归的归并排序时间复杂度都为O(NlogN) 180 | 181 | Tips: 为什么选择,冒泡,插入排序的时间复杂度为O(N^2)而归并排序时间复杂度为O(NlogN),因为选择,冒泡,插入排序的每个元素浪费了大量的比较行为,N次。而归并没有浪费比较行为,每次比较的结果有序后都会保存下来,最终merge 182 | 183 | ### 1.1.4 归并面试题 184 | 185 | 1、在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[1, 3, 4, 2, 5] 186 | 187 | ``` 188 | 1左边比1小的数:没有 189 | 190 | 3左边比3小的数:1 191 | 192 | 4左边比4小的数:1、3 193 | 194 | 2左边比2小的数为:1 195 | 196 | 5左边比5小的数为:1、3、4、2 197 | 198 | 所以该数组的小和为:1+1+3+1+1+3+4+2 = 16 199 | ``` 200 | 201 | > 暴力解法,每个数找之前比自己小的数,累加起来,时间复杂度为O(N^2),面试没分。但是暴力方法可以用来做对数器 202 | 203 | > 归并排序解法思路:O(NlogN)。在递归merge的过程中,产生小和。规则是左组比右组数小的时候产生小和,除此之外不产生;当左组和右组数相等的时候,拷贝右组的数,不产生小和;当左组的数大于右组的时候,拷贝右组的数,不产生小和。实质是把找左边比本身小的数的问题,转化为找这个数右侧有多少个数比自己大,在每次merge的过程中,一个数如果处在左组中,那么只会去找右组中有多少个数比自己大 204 | 205 | ```Go 206 | // smallSum 数组小和问题 207 | func smallSum(arr []int) int { 208 | if len(arr) < 2 { 209 | return 0 210 | } 211 | 212 | return sSum(arr, 0, len(arr) - 1) 213 | } 214 | 215 | // arr[L..R]既要排好序,也要求小和返回 216 | // 所有merge时,产生的小和,累加 217 | // 左 排序 merge 218 | // 右 排序 merge 219 | // arr 整体 merge 220 | func sSum(arr []int, l, r int) int { 221 | // 只有一个数,不存在右组,小和为0 222 | if l == r { 223 | return 0 224 | } 225 | 226 | mid := l + (r - l) / 2 227 | // 左侧merge的小和+右侧merge的小和+整体左右两侧的小和 228 | return sSum(arr, l, mid) + sSum(arr, mid + 1, r) + sumMerge(arr, l, mid, r); 229 | } 230 | 231 | func sumMerge(arr []int, L, M, R int) int { 232 | // merge过程申请辅助数组,准备copy 233 | help := make([]int, 0) 234 | p1 := L 235 | p2 := M + 1 236 | res := 0 237 | // p1未越界且p2未越界 238 | for p1 <= M && p2 <= R { 239 | // 当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到res。否则res加0 240 | if arr[p1] < arr[p2] { 241 | help = append(help, arr[p1]) 242 | res += (R - p2 + 1) * arr[p1] 243 | p1++ 244 | } else { 245 | help = append(help, arr[p2]) 246 | res += 0 247 | p2++ 248 | } 249 | } 250 | 251 | // p2越界的情况 252 | for p1 <= M { 253 | help = append(help, arr[p1]) 254 | p1++ 255 | } 256 | 257 | // p1越界的情况 258 | for p2 <= R { 259 | help = append(help, arr[p2]) 260 | p2++ 261 | } 262 | 263 | // 把辅助数组help中整体merge后的有序数组,copy回原数组arr中去 264 | for j := 0; j < len(help); j++ { 265 | arr[L+j] = help[j] 266 | } 267 | return res 268 | } 269 | ``` 270 | 271 | > 什么样的题目以后可以借助归并排序:纠结每个数右边(左边)有多少个数比自身大,比自身小等。求这种数的数量等等 272 | 273 | ## 1.2 快排 274 | 275 | ### 1.2.1 Partion过程 276 | 277 | > 给定一个数组arr,和一个整数num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求有序)。要求额外空间复杂度为O(1),时间复杂度为O(N)。例如[5,3,7,2,3,4,1],num=3,把小于等于3的放在左边,大于3的放在右边 278 | 279 | 思路:设计一个小于等于区域,下标为-1。 280 | 281 | 1、 开始遍历该数组,如果arr[i]<=num,当前数和区域下一个数交换,区域向右扩1,i++ 282 | 283 | 2、 arr[i] > num, 不做操作,i++ 284 | 285 | > 给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。该问题实质就是经典的荷兰国旗问题 286 | 287 | 思路:设计一个小于区域,下标为-1。设计一个大于区域,下表为arr.length, 数组的左右越界位置。 288 | 289 | 1、 如果arr[i]等于当前位置的数num, i++直接跳下一个。间接的扩大了等于区域 290 | 291 | 2、 如果arr[i]当前位置的数小于num,当前位置的数arr[i]和小于区域的右一个交换,小于区域右扩一个位置,当前位置i++ 292 | 293 | 3、 如果arr[i]当前位置的数大于num,当前位置的数arr[i]与大于区域的左边一个交换,大于区域左移一个位置,i停在原地不做处理,这里不做处理是因为当前位置的数是刚从大于区域交换过来的数,还没做比较 294 | 295 | 4、当i和大于区域的边界相遇,停止操作 296 | 297 | ### 1.2.2 快排1.0:每次partion搞定一个位置 298 | 299 | 思路:在给定数组上做partion, 选定数组最右侧的位置上的数作为num,小于num的放在该数组的左边,大于num的放在该数组的右边。完成之后,把该数组最右侧的数组num,交换到大于num区域的第一个位置,确保了交换后的num是小于等于区域的最后一个数(该数直至最后可以保持当前位置不变,属于已经排好序的数),把该num左侧和右侧的数分别进行同样的partion操作(递归)。相当于每次partion搞定一个数的位置,代码实现quickSort1 300 | 301 | 302 | ### 1.2.3 快排2.0:每次partion搞定一批位置 303 | 304 | 思路:借助荷兰国旗问题的思路,把arr进行partion,把小于num的数放左边,等于放中间,大于放右边。递归时把小于num的区域和大于num的区域做递归,等于num的区域不做处理。相当于每次partion搞定一批数,该批数都与标记数相等。代码实现quickSort2 305 | 306 | > 第一版和第二版的快排时间复杂度相同O(N^2):用最差情况来评估,本身有序,每次partion只搞定了一个数是自身,进行了N次partion 307 | 308 | ### 1.2.4 快排3.0:随机位置作为num标记位 309 | 310 | > 随机选一个位置i,让arr[i]和arr[R]交换,再选取arr[R]的值作为标记位。剩下的所有过程跟快排2.0一样。即为最经典的快排,时间复杂度为O(NlogN) 311 | 312 | > 为什么随机选择标记位的时间复杂度由原本不随机的O(N^2)变为O(NlogN)了呢? 如果我们随机选择位置那么就趋向于标记位的左右两侧的递归规模趋向于N/2。那么根据master公式,可以计算出算法复杂度为O(NlogN)。实质上,在我们选择随机的num时,最差情况,最好情况,其他各种情况的出现概率为1/N。对于这N种情况,数学上算出的时间复杂度最终期望是O(NlogN),这个数学上可以进行证明,证明相对较复杂 313 | 314 | > 例如我们的num随机到数组左侧三分之一的位置,那么master公式为 315 | 316 | ```math 317 | T(N) = T((1/3)N) + T((2/3)N) + O(N) 318 | ``` 319 | 320 | > 对于这个递归表达式,master公式是解不了的,master公式只能解决子问题规模一样的递归。对于这个递归,算法导论上给出了计算方法,大致思路为假设一个复杂度,看这个公式是否收敛于这个复杂度的方式 321 | 322 | ### 1.2.5 快排的时间复杂度与空间复杂度 323 | 324 | > 时间复杂度参考上文每种的复杂度 325 | 326 | > 空间复杂度:O(logN)。空间复杂度产生于每次递归partion之后,我们需要申请额外的空间变量保存相等区域的左右两侧的位置。那么每次partion需要申请两个变量,多少次partion?实质是该递归树被分了多少层,树的高度,有好有坏,最好logN,最差N。随机选择num之后,期望仍然是概率累加,收敛于O(logN)。 327 | 328 | ```Go 329 | package main 330 | 331 | // swap 交换数组中的两个位置的数 332 | func swap(arr []int, i, j int) { 333 | tmp := arr[i] 334 | arr[i] = arr[j] 335 | arr[j] = tmp 336 | } 337 | 338 | // partition 对数组进行partition处理 339 | func partition(arr []int, L, R int) int { 340 | if L > R { 341 | return -1 342 | } 343 | if L == R { 344 | return L 345 | } 346 | // 选定左边界的左边一个位置作为小于区域的起点 347 | lessEqual := L - 1 348 | index := L 349 | // 每次搞定一个位置 350 | for index < R { 351 | if arr[index] <= arr[R] { 352 | lessEqual++ 353 | swap(arr, index, lessEqual) 354 | } 355 | index++ 356 | } 357 | lessEqual++ 358 | swap(arr, lessEqual, R) 359 | return lessEqual 360 | } 361 | 362 | // arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值 363 | // 小于arr[R]放左侧 等于arr[R]放中间 大于arr[R]放右边 364 | // 返回中间区域的左右边界 365 | func netherlandsFlag(arr []int, L, R int) []int { 366 | // 不存在荷兰国旗问题 367 | if L > R { 368 | return []int{-1, -1} 369 | } 370 | 371 | // 已经都是等于区域,由于用R做划分返回R位置 372 | if L == R { 373 | return []int{L, R} 374 | } 375 | 376 | // < 区 右边界 377 | less := L - 1 378 | // > 区 左边界 379 | more := R 380 | index := L 381 | for index < more { 382 | // 当前值等于右边界,不做处理,index++ 383 | if arr[index] == arr[R] { 384 | index++ 385 | } else if arr[index] < arr[R] { // 小于交换当前值和左边界的值 386 | less++ 387 | swap(arr, index, less) 388 | index++ 389 | } else { // 大于右边界的值 390 | more-- 391 | swap(arr, index, more) 392 | } 393 | } 394 | // 比较完之后,把R位置的数,调整到等于区域的右边,至此大于区域才是真正意义上的大于区域 395 | swap(arr, more, R) 396 | return []int{less + 1, more} 397 | } 398 | 399 | func QuickSort1(arr []int) { 400 | if len(arr) < 2 { 401 | return 402 | } 403 | 404 | sortByPartition(arr, 0, len(arr)-1) 405 | } 406 | 407 | func sortByPartition(arr []int, L int, R int) { 408 | if L >= R { 409 | return 410 | } 411 | 412 | // L到R上进行partition 标记位为arr[R] 数组被分成 [ <=arr[R] arr[R] >arr[R] ],M为partition之后标记位处在的位置 413 | M := partition(arr, L, R) 414 | sortByPartition(arr, L, M-1) 415 | sortByPartition(arr, M+1, R) 416 | } 417 | 418 | func QuickSort2(arr []int) { 419 | if len(arr) < 2 { 420 | return 421 | } 422 | sortByNetherlandsFlag(arr, 0, len(arr)-1) 423 | } 424 | 425 | func sortByNetherlandsFlag(arr []int, L int, R int) { 426 | if L >= R { 427 | return 428 | } 429 | 430 | // 每次partition返回等于区域的范围,荷兰国旗问题 431 | equalArea := netherlandsFlag(arr, L, R) 432 | // 对等于区域左边的小于区域递归,partition 433 | sortByNetherlandsFlag(arr, L, equalArea[0]-1) 434 | // 对等于区域右边的大于区域递归,partition 435 | sortByNetherlandsFlag(arr, equalArea[1]+1, R) 436 | } 437 | ``` -------------------------------------------------------------------------------- /docs/19.md: -------------------------------------------------------------------------------- 1 | - [1 打表技巧和矩阵处理技巧](#1) 2 | * [1.1 打表法](#11) 3 | + [1.1.1 打表找规律](#111) 4 | + [1.1.2 例题1 小虎买苹果](#112) 5 | + [1.1.2 例题2 牛羊吃草](#112) 6 | + [1.1.3 例题3](#113) 7 | + [1.2 矩阵处理技巧](#12) 8 | - [1.2.1 zigzag打印矩阵](#121) 9 | - [1.2.2 转圈打印矩阵](#122) 10 | - [1.2.3 矩阵调整-原地旋转正方形矩阵](#123) 11 | 12 |

1 打表技巧和矩阵处理技巧

13 | 14 | 15 | 在一个数组arr[]中,每个数的大小不超过1000,例如[10,9,6,12],所有的数,求所有数质数因子的个数总和? 16 | 17 | `10=2*5` 18 | 19 | `9=3*3` 20 | 21 | `6=3*3` 22 | 23 | `12=3*2*2` 24 | 25 | 我们可以把1000以内的数的质数因子个数求出来,存到我们的表中,查表即可 26 | 27 | 28 |

1.1 打表法

29 | 30 | 1)问题如果返回值不太多,可以用hardcode的方式列出,作为程序的一部分 31 | 32 | 2)一个大问题解决时底层频繁使用规模不大的小问题的解,如果小问题的返回值满足条件1),可以把小问题的解列成一张表,作为程序的一部分 33 | 34 | 3)打表找规律(本节课重点),有关1)和2)内容欢迎关注后序课程 35 | 36 | 37 |

1.1.1 打表找规律

38 | 39 | 40 | 1)某个面试题,输入参数类型简单,并且只有一个实际参数 41 | 42 | 2)要求的返回值类型也简单,并且只有一个 43 | 44 | 3)用暴力方法,把输入参数对应的返回值,打印出来看看,进而优化code 45 | 46 | 47 |

1.1.2 例题1 小虎买苹果

48 | 49 | 小虎去买苹果,商店只提供两种类型的塑料袋,每种类型都有任意数量。 50 | 51 | 1)能装下6个苹果的袋子 52 | 53 | 2)能装下8个苹果的袋子 54 | 55 | 小虎可以自由使用两种袋子来装苹果,但是小虎有强迫症,他要求自己使用的袋子数量必须最少,且使用的每个袋子必须装满。 56 | 给定一个正整数N,返回至少使用多少袋子。如果N无法让使用的每个袋子必须装满,返回-1 57 | 58 | 59 | > 暴力思路,例如N=100个苹果,我们全部用8号袋装,最多使用12个8号袋子,剩4个苹果,6号袋没装满。8号袋减1,需要2个6号袋,满足。如果依次递减8号袋,为0个仍未有答案,则无解 60 | 61 | 62 | ```Java 63 | public class Code01_AppleMinBags { 64 | 65 | public static int minBags(int apple) { 66 | if (apple < 0) { 67 | return -1; 68 | } 69 | int bag6 = -1; 70 | int bag8 = apple / 8; 71 | int rest = apple - 8 * bag8; 72 | while (bag8 >= 0 && rest < 24) { 73 | int restUse6 = minBagBase6(rest); 74 | if (restUse6 != -1) { 75 | bag6 = restUse6; 76 | break; 77 | } 78 | rest = apple - 8 * (--bag8); 79 | } 80 | return bag6 == -1 ? -1 : bag6 + bag8; 81 | } 82 | 83 | // 如果剩余苹果rest可以被装6个苹果的袋子搞定,返回袋子数量 84 | // 不能搞定返回-1 85 | public static int minBagBase6(int rest) { 86 | return rest % 6 == 0 ? (rest / 6) : -1; 87 | } 88 | 89 | // 根据打表规律写code 90 | public static int minBagAwesome(int apple) { 91 | if ((apple & 1) != 0) { // 如果是奇数,返回-1 92 | return -1; 93 | } 94 | if (apple < 18) { 95 | return apple == 0 ? 0 : (apple == 6 || apple == 8) ? 1 96 | : (apple == 12 || apple == 14 || apple == 16) ? 2 : -1; 97 | } 98 | return (apple - 18) / 8 + 3; 99 | } 100 | 101 | // 打表看规律,摒弃数学规律 102 | public static void main(String[] args) { 103 | for(int apple = 1; apple < 100;apple++) { 104 | System.out.println(apple + " : "+ minBags(apple)); 105 | } 106 | 107 | } 108 | 109 | } 110 | ``` 111 | 112 |

1.1.2 例题2 牛羊吃草

113 | 114 | 给定一个正整数N,表示有N份青草统一堆放在仓库里 115 | 有一只牛和一只羊,牛先吃,羊后吃,它俩轮流吃草 116 | 不管是牛还是羊,每一轮能吃的草量必须是: 117 | 118 | 1,4,16,64…(4的某次方) 119 | 120 | 谁最先把草吃完,谁获胜 121 | 122 | 假设牛和羊都绝顶聪明,都想赢,都会做出理性的决定 123 | 124 | 根据唯一的参数N,返回谁会赢 125 | 126 | 127 | > 暴力思路打表找规律 128 | 129 | ```Java 130 | public class Code02_EatGrass { 131 | 132 | // n份青草放在一堆 133 | // 先手后手都绝顶聪明 134 | // string "先手" "后手" 135 | public static String winner1(int n) { 136 | // 0 1 2 3 4 137 | // 后 先 后 先 先 138 | // base case 139 | if (n < 5) { // base case 140 | return (n == 0 || n == 2) ? "后手" : "先手"; 141 | } 142 | // n >= 5 时 143 | int base = 1; // 当前先手决定吃的草数 144 | // 当前是先手在选 145 | while (base <= n) { 146 | // 当前一共n份草,先手吃掉的是base份,n - base 是留给后手的草 147 | // 母过程 先手 在子过程里是 后手 148 | if (winner1(n - base).equals("后手")) { 149 | return "先手"; 150 | } 151 | if (base > n / 4) { // 防止base*4之后溢出 152 | break; 153 | } 154 | base *= 4; 155 | } 156 | return "后手"; 157 | } 158 | 159 | // 根据打表的规律,写代码 160 | public static String winner2(int n) { 161 | if (n % 5 == 0 || n % 5 == 2) { 162 | return "后手"; 163 | } else { 164 | return "先手"; 165 | } 166 | } 167 | 168 | // 暴力打表找规律 169 | public static void main(String[] args) { 170 | for (int i = 0; i <= 50; i++) { 171 | System.out.println(i + " : " + winner1(i)); 172 | } 173 | } 174 | 175 | } 176 | ``` 177 | 178 |

1.1.3 例题3

179 | 180 | 定义一种数:可以表示成若干(数量>1)连续正数和的数 181 | 比如: 182 | 183 | 5 = 2+3,5就是这样的数 184 | 185 | 12 = 3+4+5,12就是这样的数 186 | 187 | 1不是这样的数,因为要求数量大于1个、连续正数和 188 | 189 | 2 = 1 + 1,2也不是,因为等号右边不是连续正数 190 | 191 | 给定一个参数N,返回是不是可以表示成若干连续正数和的数 192 | 193 | 194 | ```Java 195 | public class Code03_MSumToN { 196 | 197 | // 暴力法 198 | public static boolean isMSum1(int num) { 199 | for (int i = 1; i <= num; i++) { 200 | int sum = i; 201 | for (int j = i + 1; j <= num; j++) { 202 | if (sum + j > num) { 203 | break; 204 | } 205 | if (sum + j == num) { 206 | return true; 207 | } 208 | sum += j; 209 | } 210 | } 211 | return false; 212 | } 213 | 214 | // 根据打表的规律写代码 215 | public static boolean isMSum2(int num) { 216 | if (num < 3) { 217 | return false; 218 | } 219 | return (num & (num - 1)) != 0; 220 | } 221 | 222 | // 打表 223 | public static void main(String[] args) { 224 | for (int num = 1; num < 200; num++) { 225 | System.out.println(num + " : " + isMSum1(num)); 226 | } 227 | System.out.println("test begin"); 228 | for (int num = 1; num < 5000; num++) { 229 | if (isMSum1(num) != isMSum2(num)) { 230 | System.out.println("Oops!"); 231 | } 232 | } 233 | System.out.println("test end"); 234 | 235 | } 236 | } 237 | ``` 238 | 239 |

1.2 矩阵处理技巧

240 | 241 | 1)zigzag打印矩阵 242 | 243 | 2)转圈打印矩阵 244 | 245 | 3)原地旋转正方形矩阵 246 | 247 | 核心技巧:找到coding上的宏观调度 248 | 249 |

1.2.1 zigzag打印矩阵

250 | 251 | > 矩阵的特殊轨迹问题,不要把思维限制在具体某个坐标怎么变化 252 | 253 | 对于一个矩阵,如何绕圈打印,例如: 254 | 255 | ```math 256 | \begin{matrix} 257 | 1&2&3 \\ 258 | 4&5&6 \\ 259 | 7&8&9 \\ 260 | \end{matrix} 261 | ``` 262 | 263 | 打印的顺序为:1,2,4,7,5,3,6,8,9 264 | 265 | > 思路:准备A和B两个点,坐标都指向0,0位置。A和B同时走,A往右走,走到尽头后往下走,B往下走,走到不能再走了往右走。通过这么处理,A和B每个位置的连线都是一条斜线,且无重复。A和B每同时走一步,打印每次逆序打印,即开始时从B往A打印,下一步从A往B打印,循环往复 266 | 267 | 268 | ```Java 269 | public class Code06_ZigZagPrintMatrix { 270 | 271 | public static void printMatrixZigZag(int[][] matrix) { 272 | // A的行row 273 | int tR = 0; 274 | // A的列coulum 275 | int tC = 0; 276 | // B的行row 277 | int dR = 0; 278 | // B的列coulum 279 | int dC = 0; 280 | // 终止位置的行和列 281 | int endR = matrix.length - 1; 282 | int endC = matrix[0].length - 1; 283 | // 是不是从右上往左下打印 284 | boolean fromUp = false; 285 | // A的轨迹不会超过最后一行 286 | while (tR != endR + 1) { 287 | // 告诉当前A和B,打印方向,完成打印 288 | printLevel(matrix, tR, tC, dR, dC, fromUp); 289 | // 打印完之后,A和B再移动。A到最右再向下,B到最下再向右 290 | tR = tC == endC ? tR + 1 : tR; 291 | tC = tC == endC ? tC : tC + 1; 292 | dC = dR == endR ? dC + 1 : dC; 293 | dR = dR == endR ? dR : dR + 1; 294 | // A和B来到下一个位置之后,改变打印方向 295 | fromUp = !fromUp; 296 | } 297 | System.out.println(); 298 | } 299 | 300 | public static void printLevel(int[][] m, int tR, int tC, int dR, int dC, 301 | boolean f) { 302 | if (f) { 303 | while (tR != dR + 1) { 304 | System.out.print(m[tR++][tC--] + " "); 305 | } 306 | } else { 307 | while (dR != tR - 1) { 308 | System.out.print(m[dR--][dC++] + " "); 309 | } 310 | } 311 | } 312 | 313 | public static void main(String[] args) { 314 | int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }; 315 | printMatrixZigZag(matrix); 316 | 317 | } 318 | 319 | } 320 | 321 | ``` 322 | 323 |

1.2.2 转圈打印矩阵

324 | 325 | 326 | ```math 327 | \begin{matrix} 328 | 1&2&3&4 \\ 329 | 5&6&7&8 \\ 330 | 9&10&11&12 \\ 331 | 13&14&15&16 \\ 332 | \end{matrix} 333 | ``` 334 | 335 | 打印轨迹是:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10 336 | 337 | > 思路:每个圈,我们知道左上角的位置,和右下角的位置,我们就可以得到需要转圈的圈的大小, 338 | 339 | ```Java 340 | public class Code05_PrintMatrixSpiralOrder { 341 | 342 | public static void spiralOrderPrint(int[][] matrix) { 343 | // A行 344 | int tR = 0; 345 | // A列 346 | int tC = 0; 347 | // B行 348 | int dR = matrix.length - 1; 349 | // B列 350 | int dC = matrix[0].length - 1; 351 | 352 | while (tR <= dR && tC <= dC) { 353 | printEdge(matrix, tR++, tC++, dR--, dC--); 354 | } 355 | } 356 | 357 | // 当前打印,左上角和右下角的位置 358 | public static void printEdge(int[][] m, int tR, int tC, int dR, int dC) { 359 | // 表示区域只剩下一条横线的时候 360 | if (tR == dR) { 361 | for (int i = tC; i <= dC; i++) { 362 | System.out.print(m[tR][i] + " "); 363 | } 364 | } else if (tC == dC) { // 表示区域只剩下一条竖线了 365 | for (int i = tR; i <= dR; i++) { 366 | System.out.print(m[i][tC] + " "); 367 | } 368 | } else { 369 | int curC = tC; 370 | int curR = tR; 371 | while (curC != dC) { 372 | System.out.print(m[tR][curC] + " "); 373 | curC++; 374 | } 375 | while (curR != dR) { 376 | System.out.print(m[curR][dC] + " "); 377 | curR++; 378 | } 379 | while (curC != tC) { 380 | System.out.print(m[dR][curC] + " "); 381 | curC--; 382 | } 383 | while (curR != tR) { 384 | System.out.print(m[curR][tC] + " "); 385 | curR--; 386 | } 387 | } 388 | } 389 | 390 | public static void main(String[] args) { 391 | int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, 392 | { 13, 14, 15, 16 } }; 393 | spiralOrderPrint(matrix); 394 | 395 | } 396 | 397 | } 398 | ``` 399 | 400 |

1.2.3 矩阵调整-原地旋转正方形矩阵

401 | 402 | 必须要是正方形矩阵,非正方形的旋转会越界;题意的意思是每一个数都顺时针旋转90度 403 | 404 | 405 | ```math 406 | \begin{matrix} 407 | 1&2&3&4 \\ 408 | 5&6&7&8 \\ 409 | 9&10&11&12 \\ 410 | 13&14&15&16 \\ 411 | \end{matrix} 412 | ``` 413 | 414 | 调整后的结构为: 415 | 416 | ```math 417 | \begin{matrix} 418 | 13&9&5&1 \\ 419 | 14&10&6&2 \\ 420 | 5&11&7&3 \\ 421 | 16&12&8&4 \\ 422 | \end{matrix} 423 | ``` 424 | 425 | 426 | > 思路:一圈一圈的转,和旋转打印思路比较像。按圈,再按小组旋转,第一圈的第一个小组为四个角。分别为:1,4,16,13;第二小组为:2,8,15,9;依次旋转小组,最终达到旋转该圈的目的。接着旋转下一个圈的各个小组。每一层的小组数目等于该圈的边长减1 427 | 428 | ```Java 429 | public class Code04_RotateMatrix { 430 | 431 | public static void rotate(int[][] matrix) { 432 | // a行 433 | int a = 0; 434 | // b列 435 | int b = 0; 436 | // c行 437 | int c = matrix.length - 1; 438 | // d列 439 | int d = matrix[0].length - 1; 440 | // 由于是正方形矩阵,只需要判断行不越界,等同于判断列不越界 441 | while (a < c) { 442 | rotateEdge(matrix, a++, b++, c--, d--); 443 | } 444 | } 445 | 446 | // 当前需要转的圈的左上角和右下角 447 | public static void rotateEdge(int[][] m, int a, int b, int c, int d) { 448 | int tmp = 0; 449 | // 得到左上角右下角坐标,我们可以知道右上角和左下角的位置,这四个位置先旋转。这四个位置称为一个小组。 450 | // 旋转完之后,找下四个位置的小组再旋转 451 | for (int i = 0; i < d - b; i++) { 452 | tmp = m[a][b + i]; 453 | m[a][b + i] = m[c - i][b]; 454 | m[c - i][b] = m[c][d - i]; 455 | m[c][d - i] = m[a + i][d]; 456 | m[a + i][d] = tmp; 457 | } 458 | } 459 | 460 | public static void printMatrix(int[][] matrix) { 461 | for (int i = 0; i != matrix.length; i++) { 462 | for (int j = 0; j != matrix[0].length; j++) { 463 | System.out.print(matrix[i][j] + " "); 464 | } 465 | System.out.println(); 466 | } 467 | } 468 | 469 | public static void main(String[] args) { 470 | int[][] matrix = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }; 471 | printMatrix(matrix); 472 | rotate(matrix); 473 | System.out.println("========="); 474 | printMatrix(matrix); 475 | 476 | } 477 | 478 | } 479 | ``` 480 | 481 | > 大量的矩阵变换都会涉及到一个宏观调度,不到万不得已,不要把自己陷入每个位置怎么变,扣每个位置的变化,会非常难 482 | 483 | 484 | 485 | 486 | -------------------------------------------------------------------------------- /docs/17.md: -------------------------------------------------------------------------------- 1 | - [1 Morris遍历](#1) 2 | * [1.1 Morris遍历目的](#11) 3 | + [1.1.1 算法流程](#111) 4 | + [1.1.2 时间复杂度估计](#112) 5 | * [1.2 Morris遍历的应用](#12) 6 | * [1.3 Morris遍历为最优解的情景](#13) 7 | 8 |

1 Morris遍历

9 | 10 | 11 |

1.1 Morris遍历目的

12 | 13 | 在二叉树的遍历中,有递归方式遍历和非递归遍历两种。不管哪种方式,时间复杂度为O(N),空间复杂度为O(h),h为树的高度。Morris遍历可以在时间复杂度O(N),空间复杂度O(1)实现二叉树的遍历 14 | 15 | 16 |

1.1.1 算法流程

17 | 18 | 从一个树的头结点cur开始: 19 | 20 | 1、cur的左树为null,cur = cur.right 21 | 22 | 2、cur有左树,找到左树的最右的节点mostRight 23 | 24 | - mostRight的右孩子指向null,让mostRigth.right = cur;cur = cur.left 25 | - mostRight的右孩子指向当前节点cur,让mostRigth.right = null;cur = cur.rigth 26 | - cur为null的时候,整个流程结束 27 | 28 | cur经过的各个节点,称为Morris序,例如如下的树结构: 29 | 30 | ``` 31 | graph TD 32 | 1-->2 33 | 1-->3 34 | 2-->4 35 | 2-->5 36 | 3-->6 37 | 3-->7 38 | ``` 39 | 40 | cur经过的路径也就是Morris序为:1,2,4,2,5,1,3,6,3,7 41 | 42 | 可以发现,只要当前节点有左孩子,当前节点会来到两次。当左树最右节点的右孩子指向null,可以判定第一次来到cur节点,下一次来到cur就是发现左树的最右节点指向的是自己,说明第二次来到cur节点 43 | 44 | 在Morris遍历的过程中,第一次到达cur就打印(只会来cur一次的节点也打印)就是树的先序遍历,第二次到达(只会来到cur一次的节点也打印)打印为中序。第二次来到cur节点的时候逆序打印cur左树的右边界,最后逆序打印整颗树的右边界,逆序打印右边界不可以使用额外的结构,因为我们要求空间复杂度为O(1),可以使用翻转链表 45 | 46 | 翻转链表:例如a->b->c->d->null。可以让a-null,b->a,c->b,d->c即可。打印完之后把链表再翻转过来 47 | 48 |

1.1.2 时间复杂度估计

49 | 50 | cur来到节点的时间复杂度为O(N),每个cur遍历左树最右边界的代价最多为O(2N),所以整个遍历过程为O(N),整个遍历过程只用到到有限的几个变量,其他使用的都是树本身的指针关系。 51 | 52 | 53 | 54 | ```Java 55 | public class Code01_MorrisTraversal { 56 | 57 | public static class Node { 58 | public int value; 59 | Node left; 60 | Node right; 61 | 62 | public Node(int data) { 63 | this.value = data; 64 | } 65 | } 66 | 67 | // morris遍历 68 | public static void morris(Node head) { 69 | if (head == null) { 70 | return; 71 | } 72 | Node cur = head; 73 | Node mostRight = null; 74 | while (cur != null) { 75 | // cur有没有左树 76 | mostRight = cur.left; 77 | if (mostRight != null) { // 有左树的情况下 78 | // 找到cur左树上,真实的最右 79 | while (mostRight.right != null && mostRight.right != cur) { 80 | mostRight = mostRight.right; 81 | } 82 | // 从while中出来,mostRight一定是cur左树上的最右节点 83 | // mostRight如果等于null,说明第一次来到自己 84 | if (mostRight.right == null) { 85 | mostRight.right = cur; 86 | cur = cur.left; 87 | continue; 88 | // 否则第二次来到自己,意味着mostRight.right = cur 89 | } else { // mostRight.right != null -> mostRight.right == cur 90 | mostRight.right = null; 91 | } 92 | } 93 | cur = cur.right; 94 | } 95 | } 96 | 97 | // Morris中序遍历 98 | public static void morrisIn(Node head) { 99 | if (head == null) { 100 | return; 101 | } 102 | Node cur = head; 103 | Node mostRight = null; 104 | while (cur != null) { 105 | mostRight = cur.left; 106 | if (mostRight != null) { 107 | while (mostRight.right != null && mostRight.right != cur) { 108 | mostRight = mostRight.right; 109 | } 110 | if (mostRight.right == null) { 111 | mostRight.right = cur; 112 | cur = cur.left; 113 | continue; 114 | } else { 115 | mostRight.right = null; 116 | } 117 | } 118 | System.out.print(cur.value + " "); 119 | cur = cur.right; 120 | } 121 | System.out.println(); 122 | } 123 | 124 | // Morris先序遍历 125 | public static void morrisPre(Node head) { 126 | if (head == null) { 127 | return; 128 | } 129 | // cur 130 | Node cur1 = head; 131 | // mostRight 132 | Node cur2 = null; 133 | while (cur1 != null) { 134 | cur2 = cur1.left; 135 | if (cur2 != null) { 136 | while (cur2.right != null && cur2.right != cur1) { 137 | cur2 = cur2.right; 138 | } 139 | if (cur2.right == null) { 140 | cur2.right = cur1; 141 | System.out.print(cur1.value + " "); 142 | cur1 = cur1.left; 143 | continue; 144 | } else { 145 | cur2.right = null; 146 | } 147 | } else { 148 | System.out.print(cur1.value + " "); 149 | } 150 | cur1 = cur1.right; 151 | } 152 | System.out.println(); 153 | } 154 | 155 | 156 | // Morris后序遍历 157 | public static void morrisPos(Node head) { 158 | if (head == null) { 159 | return; 160 | } 161 | Node cur = head; 162 | Node mostRight = null; 163 | while (cur != null) { 164 | mostRight = cur.left; 165 | if (mostRight != null) { 166 | while (mostRight.right != null && mostRight.right != cur) { 167 | mostRight = mostRight.right; 168 | } 169 | if (mostRight.right == null) { 170 | mostRight.right = cur; 171 | cur = cur.left; 172 | continue; 173 | // 回到自己两次,且第二次回到自己的时候是打印时机 174 | } else { 175 | mostRight.right = null; 176 | // 翻转右边界链表,打印 177 | printEdge(cur.left); 178 | } 179 | } 180 | cur = cur.right; 181 | } 182 | // while结束的时候,整颗树的右边界同样的翻转打印一次 183 | printEdge(head); 184 | System.out.println(); 185 | } 186 | 187 | public static void printEdge(Node head) { 188 | Node tail = reverseEdge(head); 189 | Node cur = tail; 190 | while (cur != null) { 191 | System.out.print(cur.value + " "); 192 | cur = cur.right; 193 | } 194 | reverseEdge(tail); 195 | } 196 | 197 | public static Node reverseEdge(Node from) { 198 | Node pre = null; 199 | Node next = null; 200 | while (from != null) { 201 | next = from.right; 202 | from.right = pre; 203 | pre = from; 204 | from = next; 205 | } 206 | return pre; 207 | } 208 | 209 | // for test -- print tree 210 | public static void printTree(Node head) { 211 | System.out.println("Binary Tree:"); 212 | printInOrder(head, 0, "H", 17); 213 | System.out.println(); 214 | } 215 | 216 | public static void printInOrder(Node head, int height, String to, int len) { 217 | if (head == null) { 218 | return; 219 | } 220 | printInOrder(head.right, height + 1, "v", len); 221 | String val = to + head.value + to; 222 | int lenM = val.length(); 223 | int lenL = (len - lenM) / 2; 224 | int lenR = len - lenM - lenL; 225 | val = getSpace(lenL) + val + getSpace(lenR); 226 | System.out.println(getSpace(height * len) + val); 227 | printInOrder(head.left, height + 1, "^", len); 228 | } 229 | 230 | public static String getSpace(int num) { 231 | String space = " "; 232 | StringBuffer buf = new StringBuffer(""); 233 | for (int i = 0; i < num; i++) { 234 | buf.append(space); 235 | } 236 | return buf.toString(); 237 | } 238 | 239 | 240 | 241 | // 在Morris遍历的基础上,判断一颗树是不是一颗搜索二叉树 242 | // 搜索二叉树是左比自己小,右比自己大 243 | // 一颗树中序遍历,值一直在递增,就是搜索二叉树 244 | public static boolean isBST(Node head) { 245 | if (head == null) { 246 | return true; 247 | } 248 | Node cur = head; 249 | Node mostRight = null; 250 | Integer pre = null; 251 | boolean ans = true; 252 | while (cur != null) { 253 | mostRight = cur.left; 254 | if (mostRight != null) { 255 | while (mostRight.right != null && mostRight.right != cur) { 256 | mostRight = mostRight.right; 257 | } 258 | if (mostRight.right == null) { 259 | mostRight.right = cur; 260 | cur = cur.left; 261 | continue; 262 | } else { 263 | mostRight.right = null; 264 | } 265 | } 266 | if (pre != null && pre >= cur.value) { 267 | ans = false; 268 | } 269 | pre = cur.value; 270 | cur = cur.right; 271 | } 272 | return ans; 273 | } 274 | 275 | public static void main(String[] args) { 276 | Node head = new Node(4); 277 | head.left = new Node(2); 278 | head.right = new Node(6); 279 | head.left.left = new Node(1); 280 | head.left.right = new Node(3); 281 | head.right.left = new Node(5); 282 | head.right.right = new Node(7); 283 | printTree(head); 284 | morrisIn(head); 285 | morrisPre(head); 286 | morrisPos(head); 287 | printTree(head); 288 | 289 | } 290 | 291 | } 292 | ``` 293 | 294 |

1.2 Morris遍历的应用

295 | 296 | 在一颗二叉树中,求该二叉树的最小高度。最小高度指的是,所有叶子节点距离头节点的最小值 297 | 298 | > 二叉树递归求解,求左树的最小高度加1和右树的最小高度加1,比较 299 | 300 | > Morris遍历求解,每到达一个cur的时候,记录高度。每到达一个cur的时候判断cur是否为叶子节点,更新全局最小值。最后看一下最后一个节点的高度和全局最小高度对比,取最小高度 301 | 302 | ```Java 303 | public class Code05_MinHeight { 304 | 305 | public static class Node { 306 | public int val; 307 | public Node left; 308 | public Node right; 309 | 310 | public Node(int x) { 311 | val = x; 312 | } 313 | } 314 | 315 | // 解法1 运用二叉树的递归 316 | public static int minHeight1(Node head) { 317 | if (head == null) { 318 | return 0; 319 | } 320 | return p(head); 321 | } 322 | 323 | public static int p(Node x) { 324 | if (x.left == null && x.right == null) { 325 | return 1; 326 | } 327 | // 左右子树起码有一个不为空 328 | int leftH = Integer.MAX_VALUE; 329 | if (x.left != null) { 330 | leftH = p(x.left); 331 | } 332 | int rightH = Integer.MAX_VALUE; 333 | if (x.right != null) { 334 | rightH = p(x.right); 335 | } 336 | return 1 + Math.min(leftH, rightH); 337 | } 338 | 339 | // 解法2 根据morris遍历改写 340 | public static int minHeight2(Node head) { 341 | if (head == null) { 342 | return 0; 343 | } 344 | Node cur = head; 345 | Node mostRight = null; 346 | int curLevel = 0; 347 | int minHeight = Integer.MAX_VALUE; 348 | while (cur != null) { 349 | mostRight = cur.left; 350 | if (mostRight != null) { 351 | int rightBoardSize = 1; 352 | while (mostRight.right != null && mostRight.right != cur) { 353 | rightBoardSize++; 354 | mostRight = mostRight.right; 355 | } 356 | if (mostRight.right == null) { // 第一次到达 357 | curLevel++; 358 | mostRight.right = cur; 359 | cur = cur.left; 360 | continue; 361 | } else { // 第二次到达 362 | if (mostRight.left == null) { 363 | minHeight = Math.min(minHeight, curLevel); 364 | } 365 | curLevel -= rightBoardSize; 366 | mostRight.right = null; 367 | } 368 | } else { // 只有一次到达 369 | curLevel++; 370 | } 371 | cur = cur.right; 372 | } 373 | int finalRight = 1; 374 | cur = head; 375 | while (cur.right != null) { 376 | finalRight++; 377 | cur = cur.right; 378 | } 379 | if (cur.left == null && cur.right == null) { 380 | minHeight = Math.min(minHeight, finalRight); 381 | } 382 | return minHeight; 383 | } 384 | 385 | // for test 386 | public static Node generateRandomBST(int maxLevel, int maxValue) { 387 | return generate(1, maxLevel, maxValue); 388 | } 389 | 390 | // for test 391 | public static Node generate(int level, int maxLevel, int maxValue) { 392 | if (level > maxLevel || Math.random() < 0.5) { 393 | return null; 394 | } 395 | Node head = new Node((int) (Math.random() * maxValue)); 396 | head.left = generate(level + 1, maxLevel, maxValue); 397 | head.right = generate(level + 1, maxLevel, maxValue); 398 | return head; 399 | } 400 | 401 | public static void main(String[] args) { 402 | int treeLevel = 7; 403 | int nodeMaxValue = 5; 404 | int testTimes = 100000; 405 | System.out.println("test begin"); 406 | for (int i = 0; i < testTimes; i++) { 407 | Node head = generateRandomBST(treeLevel, nodeMaxValue); 408 | int ans1 = minHeight1(head); 409 | int ans2 = minHeight2(head); 410 | if (ans1 != ans2) { 411 | System.out.println("Oops!"); 412 | } 413 | } 414 | System.out.println("test finish!"); 415 | 416 | } 417 | 418 | } 419 | ``` 420 | 421 | 422 |

1.3 Morris遍历为最优解的情景

423 | 424 | > 如果我们算法流程设计成,每来到一个节点,需要左右子树的信息进行整合,那么无法使用Morris遍历。该种情况的空间复杂度也一定不是O(1)的 425 | 426 | > 如果算法流程是,当前节点使用完左树或者右树的信息后,无需整合,那么可以使用Morris遍历 427 | 428 | > Morris遍历属于比较复杂的问题 429 | -------------------------------------------------------------------------------- /02-链表、栈、队列、递归、哈希表、顺序表.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | # 1 链表、栈、队列、递归、哈希 3 | 4 | ## 1.1 链表 5 | 6 | ### 1.1.1 单向链表 7 | > 单向链表的节点结构: 8 | 9 | ```Go 10 | // Node 单项链表节点结构 11 | type Node struct { 12 | V int 13 | Next *Node 14 | } 15 | ``` 16 | ### 1.1.2 双向链表 17 | 18 | > 双向链表的节点结构: 19 | 20 | ```Go 21 | // DoubleNode 双向链表节点结构 22 | type DoubleNode struct { 23 | V int 24 | Pre *DoubleNode 25 | Next *DoubleNode 26 | } 27 | ``` 28 | 29 | ### 1.1.3 单双链表简单练习 30 | 31 | 1. 单链表和双链表如何反转 32 | 33 | > 例如单链表:1 -> 2 -> 3 转换为 3 -> 2 -> 1; 34 | 35 | 36 | ```Go 37 | // ReverseLinkedList 翻转单链表 38 | func ReverseLinkedList(head *Node) *Node { 39 | var pre *Node 40 | var next *Node 41 | for head != nil { 42 | // 把当前节点的下一个节点保存到next 43 | next = head.Next 44 | // 当前节点的指向,改为指向前一个节点 45 | head.Next = pre 46 | // pre 节点按原链表方向向下移动 47 | pre = head 48 | // head 节点按原链表方向向下移动 49 | head = next 50 | } 51 | // 按照原链表方向移动,当前节点为nil退出循环的时候,那么pre节点就是原链表的最后一个节点,链表被成功翻转。 52 | // 当前头结点pre返回 53 | return pre 54 | } 55 | ``` 56 | 57 | ```Go 58 | // ReverseDoubleLinkedList 翻转双向链表 59 | func ReverseDoubleLinkedList(head *DoubleNode) *DoubleNode { 60 | var pre *DoubleNode 61 | var next *DoubleNode 62 | for head != nil { 63 | // 保留当前节点的next节点的地址 64 | next = head.Next 65 | // 当前节点的下一个节点指pre 66 | head.Next = pre 67 | // 当前节点的上一个节点指向原链表当前节点的next节点。 68 | head.Pre = next 69 | // pre 节点按原链表方向向下移动 70 | pre = head 71 | // head 节点按原链表方向向下移动 72 | head = next 73 | } 74 | return pre 75 | } 76 | ``` 77 | 78 | 2. 把给定的值都删除 79 | 80 | > 比如给定一个链表头结点,删除该节点上值为3的节点,那么可能头结点就是3,存在删头部的情况,这里最终返回应该是删除所有值为3的节点之后的新的头部 81 | 82 | ```Go 83 | // RemoveValue 删除链表中值等于target的节点 84 | func RemoveValue(head *Node, target int) *Node { 85 | // 处理链表头结点的值即等于target的节点 86 | for head != nil { 87 | if head.V != target { 88 | break 89 | } 90 | head = head.Next 91 | } 92 | 93 | // 1、链表中的节点值全部都等于target 94 | // 2、原始链表为nil 95 | if head == nil { 96 | return head 97 | } 98 | 99 | // head来到第一个不需要删除的位置 100 | pre := head 101 | cur := head 102 | for cur != nil { 103 | // 当前节点cur往下,有多少v等于target的节点,就删除多少节点 104 | if cur.V == target { 105 | pre.Next = cur.Next 106 | } else { 107 | pre = cur 108 | } 109 | // 当前节点向下滑动 110 | cur = cur.Next 111 | } 112 | return head 113 | } 114 | ``` 115 | 116 | ## 1.2 栈、队列 117 | 1. 逻辑概念 118 | 119 | > 栈:数据先进后出,犹如弹夹 120 | 121 | > 队列: 数据先进先出,排队 122 | 123 | ```Go 124 | // 利用双向链表实现双端队列 125 | package main 126 | 127 | // DoubleEndsNode 双端队列节点 128 | type DoubleEndsNode struct { 129 | val int 130 | pre *DoubleEndsNode 131 | next *DoubleEndsNode 132 | } 133 | 134 | // DoubleEndsList 双端队列接口 135 | type DoubleEndsList interface { 136 | // AddFromHead 从头部添加节点 137 | AddFromHead(v int) 138 | // AddFromBottom 从尾部添加节点 139 | AddFromBottom(v int) 140 | // PopFromHead 从头部弹出节点 141 | PopFromHead() (int, bool) 142 | // PopFromBottom 从尾部弹出节点 143 | PopFromBottom() (int, bool) 144 | // IsEmpty 双端队列是否为空 145 | IsEmpty() bool 146 | } 147 | 148 | type DoubleEndsQueue struct { 149 | head *DoubleEndsNode 150 | tail *DoubleEndsNode 151 | } 152 | 153 | func (q *DoubleEndsQueue) AddFromHead(v int) { 154 | cur := &DoubleEndsNode{ 155 | val: v, 156 | } 157 | if q.head == nil { 158 | q.head = cur 159 | q.tail = cur 160 | } else { 161 | cur.next = q.head 162 | q.head.pre = cur 163 | q.head = cur 164 | } 165 | } 166 | 167 | func (q *DoubleEndsQueue) AddFromBottom(v int) { 168 | cur := &DoubleEndsNode{ 169 | val: v, 170 | } 171 | if q.head == nil { 172 | q.head = cur 173 | q.tail = cur 174 | } else { 175 | q.tail.next = cur 176 | cur.pre = q.tail 177 | q.tail = cur 178 | } 179 | } 180 | 181 | func (q *DoubleEndsQueue) PopFromHead() (int, bool) { 182 | if q.head == nil { 183 | return 0, false 184 | } 185 | v := q.head.val 186 | if q.head == q.tail { 187 | q.head = nil 188 | q.tail = nil 189 | return v, true 190 | } else { 191 | h := q.head 192 | q.head = q.head.next 193 | q.head.pre = nil 194 | h.next = nil 195 | return v, true 196 | } 197 | } 198 | 199 | func (q *DoubleEndsQueue) PopFromBottom() (int, bool) { 200 | if q.head == nil { 201 | return 0, false 202 | } 203 | v := q.tail.val 204 | if q.head == q.tail { 205 | q.head = nil 206 | q.tail = nil 207 | return v, true 208 | } else { 209 | t := q.tail 210 | q.tail = q.tail.pre 211 | q.tail.next = nil 212 | t.pre = nil 213 | return v, true 214 | } 215 | } 216 | 217 | func (q *DoubleEndsQueue) IsEmpty() bool { 218 | return q.head == nil 219 | } 220 | ``` 221 | 222 | 2. 栈、队列的底层实现方式 223 | 224 | > 利用双向链表(双端队列)封装栈和队列 225 | 226 | ```Go 227 | // Stack 利用双端队列实现栈 228 | type Stack struct { 229 | qu *DoubleEndsQueue 230 | } 231 | 232 | func (s *Stack) push(v int) { 233 | s.qu.AddFromHead(v) 234 | } 235 | 236 | func (s *Stack) pop() (int, bool) { 237 | return s.qu.PopFromHead() 238 | } 239 | 240 | func (s *Stack) peek() (int, bool){ 241 | if s.qu.IsEmpty() { 242 | return 0, false 243 | } 244 | return s.qu.head.val, true 245 | } 246 | ``` 247 | 248 | ```Go 249 | // Queue 利用双端队列实现队列 250 | type Queue struct { 251 | qu *DoubleEndsQueue 252 | } 253 | 254 | func (q *Queue) push(v int) { 255 | q.qu.AddFromHead(v) 256 | } 257 | 258 | func (q *Queue) poll() (int, bool) { 259 | return q.qu.PopFromBottom() 260 | } 261 | 262 | func (q *Queue)IsEmpty() bool { 263 | return q.qu.IsEmpty() 264 | } 265 | ``` 266 | 267 | > 数组实现栈和队列, 对于栈特别简单,略过,对于队列,如下 268 | 269 | 270 | ```Go 271 | package main 272 | 273 | import "fmt" 274 | 275 | type Que struct { 276 | // 队列的底层结构 277 | arr []int 278 | } 279 | 280 | func (q *Que) push (v int) { 281 | q.arr = append(q.arr, v) 282 | } 283 | 284 | func (q *Que) poll () (int, bool){ 285 | if len(q.arr) == 0 { 286 | return 0, false 287 | } 288 | v := q.arr[0] 289 | q.arr = q.arr[1:] 290 | return v, true 291 | } 292 | 293 | func main () { 294 | q := Que{} 295 | q.push(1) 296 | q.push(9) 297 | q.push(3) 298 | if poll, ok := q.poll(); ok { 299 | fmt.Println(poll) 300 | } 301 | if poll, ok := q.poll(); ok { 302 | fmt.Println(poll) 303 | } 304 | if poll, ok := q.poll(); ok { 305 | fmt.Println(poll) 306 | } 307 | if poll, ok := q.poll(); ok { 308 | fmt.Println(poll) 309 | } 310 | } 311 | ``` 312 | 313 | ## 1.3 栈、队列常见面试题 314 | 315 | 一、实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能 316 | 317 | 1、pop、push、getMin操作的时间复杂度都是O(1) 318 | 319 | 2、设计的栈类型可以使用现成的栈结构 320 | 321 | > 思路:准备两个栈,一个data栈,一个min栈。数据压data栈,min栈对比min栈顶元素,谁小加谁。这样的话data栈和min栈是同步上升的,元素个数一样多,且min栈的栈顶,是data栈所有元素中最小的那个。数据弹出data栈,我们同步弹出min栈,保证个数相等,且min栈弹出的就是最小值 322 | 323 | ```Go 324 | type MinStack struct { 325 | data *Stack 326 | min *Stack 327 | } 328 | 329 | func (s *MinStack) push(v int) { 330 | // min栈只保存最小的v,当然这里也可以设计成min栈和data栈同步上升的策略 331 | if s.min.IsEmpty() { 332 | s.min.push(v) 333 | } else if c, ok := s.min.peek(); ok { 334 | if v <= c { // 小于等于都入栈,弹出的时候等于也同步弹出min栈 335 | s.min.push(v) 336 | } else { 337 | s.min.push(c) 338 | } 339 | } 340 | // 数据栈稳步上升 341 | s.data.push(v) 342 | } 343 | 344 | func (s *MinStack) pop() (int, bool) { 345 | if s.data.IsEmpty() { 346 | return 0, false 347 | } 348 | v, _ := s.data.pop() 349 | if m, ok := s.min.peek(); ok { 350 | if m == v { 351 | s.min.pop() 352 | } 353 | } 354 | return v, true 355 | } 356 | 357 | func (s *MinStack) getMin() (int, bool){ 358 | if s.min.IsEmpty() { 359 | return 0, false 360 | } 361 | return s.min.peek() 362 | } 363 | ``` 364 | 365 | ## 1.4 递归 366 | 367 | 1、从思想上理解递归 368 | 369 | 2、从实现角度出发理解递归 370 | 371 | 例子: 372 | 373 | 求数组arr[L...R]中的最大值,怎么用递归方法实现 374 | 375 | 1、 将[L...R]范围分成左右两半。左[L...Mid],右[Mid+1...R] 376 | 2、 左部分求最大值,右部分求最大值 377 | 3、[L...R]范围上的最大值,就是max{左部分最大值,右部分最大值} 378 | 379 | > 2步骤是个递归过程,当范围上只有一个数,就可以不用再递归了 380 | 381 | ```Go 382 | package main 383 | 384 | import "fmt" 385 | 386 | func getMax(arr []int) (int, error) { 387 | if len(arr) == 0 { 388 | return 0, fmt.Errorf("arr len is zero") 389 | } 390 | return process(arr, 0, len(arr)-1), nil 391 | } 392 | 393 | func process(arr []int, l, r int) int { 394 | if l == r { 395 | return arr[l] 396 | } 397 | mid := l + (r-l)/2 398 | // 左范围最大值 399 | lm := process(arr, l, mid) 400 | // 右范围最大值 401 | rm := process(arr, mid+1, r) 402 | if lm > rm { 403 | return lm 404 | } else { 405 | return rm 406 | } 407 | } 408 | 409 | func main() { 410 | arr := []int{1, 4, 2, 6, 992, 4, 2234, 83} 411 | m, err := getMax(arr) 412 | if err != nil { 413 | panic(err) 414 | } 415 | fmt.Println(m) 416 | } 417 | ``` 418 | 419 | > 递归在系统中是怎么实现的?递归实际上利用的是系统栈来实现的。保存当前调用现场,去执行子问题,子问题的返回作为现场的需要的参数填充,最终构建还原栈顶的现场,返回。任何递归都可以改为非递归实现,需要我们自己压栈用迭代等实现 420 | 421 | ### 1.4.1 递归行为的时间复杂度 422 | 423 | > 对于满足 424 | 425 | 426 | ```math 427 | T(N) = aT(N/b) + O(N^d) 428 | ``` 429 | 其中: a,b,d为常数 430 | 431 | > 公式表示,子问题的规模是一致的,该子问题调用了a次,N/b代表子问题的规模,O(N^d)为除去递归调用剩余的时间复杂度。 432 | 433 | > 比如上述问题的递归,[L...R]上有N个数,第一个子问题的规模是N/2,第二个子问题的规模也是N/2。子问题调用了2次。额为复杂度为O(1),那么公式为: 434 | 435 | 436 | ```math 437 | T(N) = 2T(N/2) + O(N^0) 438 | ``` 439 | 440 | 结论:如果我们的递归满足这种公式,那么该递归的时间复杂度(Master公式)为 441 | 442 | ```math 443 | logb^a > d => O(N ^ (logb^a)) 444 | 445 | logb^a < d => O(N^d) 446 | 447 | logb^a == d => O(N^d * logN) 448 | 449 | ``` 450 | 451 | 那么上述问题的a=2, b=2,d=0,满足第一条,递归时间复杂度为:O(N) 452 | 453 | ## 1.5 哈希表HashMap、HashSet 454 | 455 | > Hash表的增删改查,在使用的时候,一律认为时间复杂度是O(1)的 456 | 457 | > Golang中hashMap的结构为map,hashSet可以由map结构进行简单改造即可实现 458 | 459 | ## 1.6 顺序表 TreeMap、TreeSet 460 | 461 | > 顺序表比哈希表功能多,但是顺序表的很多操作时间复杂度是O(logN) 462 | 463 | > 有序表的底层可以有很多结构实现,比如AVL树,SB树,红黑树,跳表。其中AVL,SB,红黑都是具备各自平衡性的搜索二叉树 464 | 465 | > 由于平衡二叉树每时每刻都会维持自身的平衡,所以操作为O(logN)。后面篇幅会单独介绍平衡二叉树。 466 | 467 | > 由于满足去重排序功能来维持底层树的平衡,所以如果是基础类型key直接按值来做比较,但是如果我们的key是自己定义的结构体类型,那么我们要自己制定比较规则,Go中为实现sort的接口,用来让底层的树保持比较后的平衡 468 | 469 | ```Go 470 | // Go中HashSet的简单实现 471 | package set 472 | 473 | type Set interface { 474 | Add(elements ...interface{}) 475 | Remove(elements ...interface{}) 476 | Contains(elements ...interface{}) bool 477 | } 478 | 479 | var itemExists = struct{}{} 480 | 481 | type HashSet struct { 482 | items map[interface{}]struct{} 483 | } 484 | 485 | func New(values ...interface{}) *HashSet { 486 | set := &HashSet{items: make(map[interface{}]struct{})} 487 | if len(values) > 0 { 488 | set.Add(values...) 489 | } 490 | return set 491 | } 492 | 493 | func (set *HashSet) Add(items ...interface{}) { 494 | for _, item := range items { 495 | set.items[item] = itemExists 496 | } 497 | } 498 | 499 | func (set *HashSet) Remove(items ...interface{}) { 500 | for _, item := range items { 501 | delete(set.items, item) 502 | } 503 | } 504 | 505 | func (set *HashSet) Contains(items ...interface{}) bool { 506 | for _, item := range items { 507 | if _, contains := set.items[item]; !contains { 508 | return false 509 | } 510 | } 511 | return true 512 | } 513 | ``` 514 | 515 | ```Go 516 | // Go中基于红黑树TreeSet的简单使用 517 | package main 518 | 519 | import ( 520 | "fmt" 521 | "github.com/emirpasic/gods/sets/treeset" 522 | ) 523 | 524 | // treeSet => 去重排序 525 | func main() { 526 | set := treeset.NewWithIntComparator() 527 | set.Add() 528 | set.Add(1) 529 | set.Add(2) 530 | set.Add(2, 3) 531 | set.Add() 532 | set.Add(6) 533 | set.Add(4) 534 | if actualValue := set.Empty(); actualValue != false { 535 | fmt.Printf("Got %v expected %v", actualValue, false) 536 | } 537 | if actualValue := set.Size(); actualValue != 3 { 538 | fmt.Printf("Got %v expected %v", actualValue, 3) 539 | } 540 | if actualValue, expectedValue := fmt.Sprintf("%d%d%d", set.Values()...), "12346"; actualValue != expectedValue { 541 | fmt.Printf("Got %v expected %v", actualValue, expectedValue) 542 | } 543 | 544 | fmt.Println(set.Values()...) 545 | } 546 | ``` -------------------------------------------------------------------------------- /18-《进阶》线段树(interval-tree).md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | # 1 线段树(又名为线段修改树) 4 | 5 | 线段树所要解决的问题是,区间的修改,查询和更新,如何更新查询的更快? 6 | 7 | 线段树结构提供三个主要的方法, 假设大小为N的数组,以下三个方法,均要达到O(logN) : 8 | 9 | ```Go 10 | type SegmentTreeInterface interface { 11 | // Add L到R范围的数,每个数加上V 12 | // Add(L, R, V int, arr []int) 13 | Add(L, R, C, l, r, rt int) 14 | 15 | // Update L到R范围的数,每个数都更新成V 16 | // Update(L, R, V int, arr []int) 17 | Update(L, R, C, l, r, rt int) 18 | 19 | // GetSum L到R范围的数,累加和返回 20 | // GetSum(L, R int, arr []int) 21 | GetSum(L, R, l, r, rt int) int 22 | } 23 | ``` 24 | 25 | 26 | ## 1.1 线段树概念建立 27 | 28 | ### 1.1.1 累加和数组建立 29 | 30 | 1、对于大小为n的数组,我们二分它,每次二分我们都记录一个信息 31 | 32 | 2、对于每次二分,成立树结构,我们想拿任何区间的信息,可以由我们的二分结构组合得到。例如我们1到8的数组,可以二分得到的信息为: 33 | 34 | ``` 35 | graph TD 36 | '1-8'-->'1-4' 37 | '1-8'-->'5-8' 38 | '1-4'-->'1-2' 39 | '1-4'-->'3-4' 40 | '5-8'-->'5-6' 41 | '5-8'-->'7-8' 42 | '1-2'-->'1' 43 | '1-2'-->'2' 44 | '3-4'-->'3' 45 | '3-4'-->'4' 46 | '5-6'-->'5' 47 | '5-6'-->'6' 48 | '7-8'-->'7' 49 | '7-8'-->'8' 50 | ``` 51 | 52 | 每一个节点的信息,可以由该节点左右孩子信息得到,最下层信息就是自己的信息。由以上的规则,对于N个数,我们需要申请2N-1个空间用来保存节点信息。如果N并非等于2的某次方,我们把N补成2的某次方的长度,用来保证我们构建出来的信息数是满二叉树。例如我们的长度是6,我们补到8个,后两个位置值为0。 53 | 54 | 55 | 对于任意的N,我们需要准备多少空间,可以把N补成2的某次方,得到的二分信息都装下?答案是4N。4N虽然有可能多分空间,但是多余的空间都是0,并无影响,而且兼容N为任意值的情况 56 | 57 | 例如四个数长度的数组arr[4]{3,2,5,7},我们得到累加和的二分信息为如下的树: 58 | 59 | ``` 60 | graph TD 61 | '1到4=17'-->'1到2=5' 62 | '1到4=17'-->'3到4=12' 63 | '1到2=5'-->'3' 64 | '1到2=5'-->'2' 65 | '3到4=12'-->'5' 66 | '3到4=12'-->'7' 67 | ``` 68 | 69 | 我们申请4N的空间,即16,arr[16]。0位置不用。arr[1]=17,arr[2]=5,arr[3]=12,arr[4]=3,arr[5]=2,arr[6]=5,arr[7]=7。剩下位置都为0。任何一个节点左孩子下标为2i,右孩子下标为2i+1 70 | 71 | 72 | 得到累加和信息的分布树的大小,和值的情况,那么update更新树,和add累加树,同样的大小和同样的坐标关系构建。 73 | 74 | 75 | ### 1.1.2更新结构数组建立 76 | 77 | 懒更新概念,例如有8个数,我们要把1到6的数都减小2。那么先看1到6是否完全囊括8个数,如果囊括直接更新。很显然这里没有囊括,记录要更新1到6,下发该任务给1到4和5到8。1到6完全囊括1到4,记录到lazy中,不再下发;5到8没有囊括1到6,继续下发给5到6和7到8,5到6被囊括,记录到lazy不再继续下发,7到8不接受该任务 78 | 79 | 这种懒更新机制的时间复杂度为O(logN),由于一个区间经过左右子树下发,只会经过一个绝对路径到叶子节点,其他节点都会被懒住。如果某个节点有新的任务进来,会把之前懒住的信息下发给左右孩子 80 | 81 | 82 | 对于update操作,如果update操作经过的信息节点上存在懒任务,那么该次update操作会取消该节点的lazy,无需下发,因为下发了也会给update覆盖掉; 83 | 84 | 85 | ```Go 86 | package main 87 | 88 | import "fmt" 89 | 90 | type SegmentTreeInterface interface { 91 | // Add L到R范围的数,每个数加上V 92 | // Add(L, R, V int, arr []int) 93 | Add(L, R, C, l, r, rt int) 94 | 95 | // Update L到R范围的数,每个数都更新成V 96 | // Update(L, R, V int, arr []int) 97 | Update(L, R, C, l, r, rt int) 98 | 99 | // GetSum L到R范围的数,累加和返回 100 | // GetSum(L, R int, arr []int) 101 | GetSum(L, R, l, r, rt int) int 102 | } 103 | 104 | // SegmentTree 线段树 105 | type SegmentTree struct { 106 | // arr[]为原序列的信息从0开始,但在arr里是从1开始的 107 | // sum[]模拟线段树维护区间和 108 | // lazy[]为累加懒惰标记 109 | // change[]为更新的值 110 | // update[]为更新慵懒标记 111 | maxN int 112 | arr []int 113 | // 4*len(arr) 114 | sum []int 115 | // 4*len(arr) 116 | lazy []int 117 | // 4*len(arr) 118 | change []int 119 | // 4*len(arr) 120 | update []bool 121 | } 122 | 123 | // InitSegmentTree 初始化一个线段树。根据int[] origin来初始化线段树结构 124 | func InitSegmentTree(origin []int) *SegmentTree { 125 | sgTree := SegmentTree{} 126 | MaxN := len(origin) + 1 127 | arr := make([]int, MaxN) // arr[0] 不用 从1开始使用 128 | for i := 1; i < MaxN; i++ { 129 | arr[i] = origin[i - 1] 130 | } 131 | 132 | // sum数组开辟的大小是原始数组的4倍 133 | sum := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围的累加和信息 134 | lazy := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围沒有往下傳遞的纍加任務 135 | change := make([]int, MaxN << 2) // 用来支持脑补概念中,某一个范围有没有更新操作的任务 136 | update := make([]bool, MaxN << 2) // 用来支持脑补概念中,某一个范围更新任务,更新成了什么 137 | 138 | sgTree.maxN = MaxN 139 | sgTree.arr = arr 140 | sgTree.sum = sum 141 | sgTree.lazy = lazy 142 | sgTree.change = change 143 | sgTree.update = update 144 | return &sgTree 145 | } 146 | 147 | // PushUp 汇总线段树当前位置rt的信息,为左孩子信息加上右孩子信息 148 | func (sgTree *SegmentTree) PushUp(rt int) { 149 | sgTree.sum[rt] = sgTree.sum[rt << 1] + sgTree.sum[rt << 1 | 1] 150 | } 151 | 152 | // PushDown 线段树之前的,所有懒增加,和懒更新,从父范围,发给左右两个子范围 153 | // 分发策略是什么 154 | // ln表示左子树元素结点个数,rn表示右子树结点个数 155 | func (sgTree *SegmentTree) PushDown(rt, ln, rn int) { 156 | // 首先检查父亲范围上有没有懒更新操作 157 | if sgTree.update[rt] { 158 | // 父范围有懒更新操作,左右子范围就有懒更新操作 159 | sgTree.update[rt << 1] = true 160 | sgTree.update[rt << 1 | 1] = true 161 | // 左右子范围的change以父亲分发的为准 162 | sgTree.change[rt << 1] = sgTree.change[rt] 163 | sgTree.change[rt << 1 | 1] = sgTree.change[rt] 164 | // 左右子范围的懒任务全部清空 165 | sgTree.lazy[rt << 1] = 0 166 | sgTree.lazy[rt << 1 | 1] = 0 167 | // 左右子范围的累加和全部变为当前父节点下发的change乘以左右孩子的范围个数 168 | sgTree.sum[rt << 1] = sgTree.change[rt] * ln 169 | sgTree.sum[rt << 1 | 1] = sgTree.change[rt] * rn 170 | // 父范围的更新任务被分发到左右子范围,当前父范围的更新任务改为false 171 | sgTree.update[rt] = false 172 | } 173 | 174 | // 如果上面的if也进入,该if也进入,表示之前的最晚懒住的更新到现在还没有发生过新的更新使之下发,却来了个add任务 175 | // 所以该节点即懒住了更新任务,又懒住一个add任务,接着又来了一个update任务,所以更新要先下发到子范围,接着要把当前的add任务下发下去 176 | // 如果当前节点的懒信息不为空。 177 | if sgTree.lazy[rt] != 0 { 178 | // 下发给左孩子 179 | sgTree.lazy[rt << 1] += sgTree.lazy[rt] 180 | sgTree.sum[rt << 1] += sgTree.lazy[rt] * ln 181 | // 下发给右孩子 182 | sgTree.lazy[rt << 1 | 1] += sgTree.lazy[rt] 183 | sgTree.sum[rt << 1 | 1] += sgTree.lazy[rt] * rn 184 | // 清空当前节点的懒任务信息 185 | sgTree.lazy[rt] = 0 186 | } 187 | } 188 | 189 | // Build 在初始化阶段,先把sum数组,填好 190 | // 在arr[l~r]范围上,去build,1~N, 191 | // rt : 这个范围在sum中的下标 192 | func (sgTree *SegmentTree) Build(l, r, rt int) { 193 | if l == r { 194 | sgTree.sum[rt] = sgTree.arr[l] 195 | return 196 | } 197 | // 得到l到r的中间位置 198 | mid := (l + r) >> 1 199 | // l到r左侧,填充到sum数组rt下标的2倍的位置,因为在数组中当前节点和左孩子的关系得到 200 | // 递归rt左区间 201 | sgTree.Build(l, mid, rt << 1) 202 | // 右侧,填充到2*rt+1的位置 203 | // 递归rt右区间 204 | sgTree.Build(mid + 1, r, rt << 1 | 1) 205 | sgTree.PushUp(rt) 206 | } 207 | 208 | // Update 线段树更新操作 209 | func (sgTree *SegmentTree) Update(L, R, C, l, r, rt int) { 210 | // 如果更新任务彻底覆盖当前边界 211 | if L <= l && r <= R { 212 | // 当前位置的update标记为true 213 | sgTree.update[rt] = true 214 | // 当前位置需要改变为C, update和change搭配使用 215 | sgTree.change[rt] = C 216 | // 当前节点的累加和信息,被C * (r - l + 1)覆盖掉 217 | sgTree.sum[rt] = C * (r -l + 1) 218 | // 清空之前存在该节点的懒任务 219 | sgTree.lazy[rt] = 0 220 | return 221 | } 222 | // 当前任务躲不掉,无法懒更新,要往下发 223 | mid := (l + r) >> 1 224 | // 之前的,所有懒更新,从父范围,发给左右两个子范围 225 | sgTree.PushDown(rt, mid - l + 1, r - mid) 226 | // 更新任务发给左孩子 227 | if L <= mid { 228 | sgTree.Update(L, R, C, l, mid, rt << 1) 229 | } 230 | // 更新任务发给右孩子 231 | if R > mid { 232 | sgTree.Update(L, R, C, mid + 1, r, rt << 1 | 1) 233 | } 234 | 235 | sgTree.PushUp(rt) 236 | } 237 | 238 | // Add 线段树加值操作 239 | // L..R -> 任务范围 ,所有的值累加上C 240 | // l,r -> 表达的范围 241 | // rt 去哪找l,r范围上的信息 242 | func (sgTree *SegmentTree) Add(L, R, C, l, r, rt int) { 243 | // 任务的范围彻底覆盖了,当前表达的范围,懒住 244 | if L <= l && r <= R { 245 | // 当前位置的累加和加上C * (r - l + 1),等同于下边节点都加上C,由于被懒住,下面节点并没有真正意思上add一个C 246 | sgTree.sum[rt] += C * (r - l + 1) 247 | // 之前懒住的信息,例如之前该节点加上3,又来一个加上7的任务,那么此时lazt[rt]==10 248 | sgTree.lazy[rt] += C 249 | return 250 | } 251 | 252 | // 任务并没有把l...r全包住 253 | // 要把当前任务往下发 254 | // 任务 L, R 没有把本身表达范围 l,r 彻底包住 255 | mid := (l + r) >> 1 // l..mid (rt << 1) mid+1...r(rt << 1 | 1) 256 | // 下发之前该节点所有攒的懒任务到孩子节点 257 | sgTree.PushDown(rt, mid - l + 1, r - mid) 258 | // 左孩子是否需要接到任务 259 | if L <= mid { 260 | sgTree.Add(L, R, C, l, mid, rt << 1) 261 | } 262 | 263 | // 右孩子是否需要接到任务 264 | if R > mid { 265 | sgTree.Add(L, R, C, mid + 1, r, rt << 1 | 1) 266 | } 267 | // 左右孩子做完任务后,我更新我的sum信息 268 | sgTree.PushUp(rt) 269 | } 270 | 271 | // GetSum 1~6 累加和是多少? 1~8 rt 272 | func (sgTree *SegmentTree) GetSum(L, R, l, r, rt int) int { 273 | // 累加任务覆盖当前节点范围,返回当前节点范围的累加和 274 | if L <= l && r <= R { 275 | return sgTree.sum[rt] 276 | } 277 | 278 | // 没覆盖当前节点的范围,汇总左右子范围的累加和,汇总给到当前节点 279 | mid := (l + r) >> 1 280 | sgTree.PushDown(rt, mid - l + 1, r - mid) 281 | ans := 0 282 | if L <= mid { 283 | ans += sgTree.GetSum(L, R, l, mid, rt << 1) 284 | } 285 | if R > mid { 286 | ans += sgTree.GetSum(L, R, mid + 1, r, rt << 1 | 1) 287 | } 288 | return ans 289 | } 290 | 291 | 292 | // ---------- // 293 | // 线段树暴力解,用来做对数器 294 | // sgTree 模拟线段树结构 295 | type sgTree []int 296 | 297 | // BuildTestTree 构建测试线段树 298 | func BuildTestTree(origin []int) *sgTree { 299 | arr := make([]int, len(origin) + 1) 300 | // 做一层拷贝,arr[0]位置废弃不用,下标从1开始 301 | for i := 0; i < len(origin); i++ { 302 | arr[i + 1] = origin[i] 303 | } 304 | sg := sgTree{} 305 | sg = arr 306 | return &sg 307 | } 308 | 309 | func (sgt *sgTree) Update(L, R, C int) { 310 | for i := L; i <= R; i++ { 311 | (*sgt)[i] = C 312 | } 313 | } 314 | 315 | func (sgt *sgTree) Add(L, R, C int) { 316 | for i := L; i <= R; i++ { 317 | (*sgt)[i] += C 318 | } 319 | } 320 | 321 | func (sgt *sgTree) GetSum(L, R int) int { 322 | ans := 0 323 | for i := L; i <= R; i++ { 324 | ans += (*sgt)[i] 325 | } 326 | return ans 327 | } 328 | 329 | func main() { 330 | origin := []int{2, 1, 1, 2, 3, 4, 5} 331 | // 构建一个线段树 332 | sg := InitSegmentTree(origin) 333 | sgTest := BuildTestTree(origin) 334 | 335 | 336 | S := 1 // 整个区间的开始位置,规定从1开始,不从0开始 -> 固定 337 | N := len(origin) // 整个区间的结束位置,规定能到N,不是N-1 -> 固定 338 | root := 1 // 整棵树的头节点位置,规定是1,不是0 -> 固定 339 | L := 2 // 操作区间的开始位置 -> 可变 340 | R := 5 // 操作区间的结束位置 -> 可变 341 | C := 4 // 要加的数字或者要更新的数字 -> 可变 342 | // 区间生成,必须在[S,N]整个范围上build 343 | sg.Build(S, N , root) 344 | // 区间修改,可以改变L、R和C的值,其他值不可改变 345 | sg.Add(L, R, C, S, N, root) 346 | // 区间更新,可以改变L、R和C的值,其他值不可改变 347 | sg.Update(L, R, C, S, N ,root) 348 | // 区间查询,可以改变L和R的值,其他值不可改变 349 | sum := sg.GetSum(L, R, S, N, root) 350 | fmt.Println(fmt.Sprintf("segmentTree: %d", sum)) 351 | 352 | sgTest.Add(L, R, C) 353 | sgTest.Update(L, R, C) 354 | testSum := sgTest.GetSum(L, R) 355 | fmt.Println(fmt.Sprintf("segmentTreeTest: %d", testSum)) 356 | } 357 | ``` 358 | 359 | ## 1.2 线段树案例实战 360 | 361 | 想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线 362 | 下面是这个游戏的简化版: 363 | 364 | 1)只会下落正方形积木 365 | 366 | 2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落 367 | 368 | 3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的 369 | 370 | 4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。 371 | 372 | 给定一个N*2的二维数组matrix,可以代表N个积木依次掉落, 373 | 返回每一次掉落之后的最大高度 374 | 375 | > 线段树原结构,是收集范围累加和,本题是范围上收集最大高度当成收集的信息 376 | 377 | ```Go 378 | package main 379 | 380 | import "math" 381 | 382 | // fallingSquares 383 | func fallingSquares(positions [][]int) []int { 384 | m := index(positions) 385 | // 100 -> 1 306 -> 2 403 -> 3 386 | // [100,403] 1~3 387 | N := len(m) // 1 ~ N 388 | var res []int 389 | origin := make([]int, N) 390 | max := 0 391 | sgTree := InitSegmentTree(origin) 392 | // 每落一个正方形,收集一下,所有东西组成的图像,最高高度是什么 393 | for _, arr := range positions { 394 | L := m[arr[0]] 395 | R := m[arr[0] + arr[1] - 1] 396 | height := sgTree.GetSum(L, R, 1, N, 1) + arr[1] 397 | max = int(math.Max(float64(max), float64(height))) 398 | res = append(res, max) 399 | sgTree.Update(L, R, height, 1, N, 1) 400 | } 401 | return res 402 | } 403 | 404 | // positions 405 | // [2,7] -> 表示位置从2开始,边长为7的方块,落下的x轴范围为2到8,不包括9是因为下一个位置为9可以落得下来; 2 , 8 406 | // [3, 10] -> 3, 12 407 | // 408 | // 用treeSet做离散化,避免多申请空间 409 | func index(positions [][]int) map[int]int { 410 | pos := make(map[int]string, 0) 411 | for _, arr := range positions { 412 | pos[arr[0]] = "" 413 | pos[arr[0] + arr[1] - 1] = "" 414 | } 415 | 416 | m := make(map[int]int, 0) 417 | count := 0 418 | for key := range pos { 419 | count++ 420 | m[key] = count 421 | } 422 | return m 423 | } 424 | ``` 425 | 426 | 本题为leetCode原题:https://leetcode.com/problems/falling-squares/ 427 | 428 | 429 | ## 1.3 什么样的题目可以用线段树来解决? 430 | 431 | 区间范围上,统一增加,或者统一更新一个值。大范围信息可以只由左、右两侧信息加工出, 432 | 而不必遍历左右两个子范围的具体状况 433 | 434 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------