第一课 常见排序
一、时间复杂度 & 额外空间复杂度
二、选择排序&冒泡排序
三、插入排序
类似斗地主抓牌
四、二分法
- 在一个有序数组中,找某个数是否存在
- 在一个有序数组中,找>=某个数最左侧的位置
- 局部最小值问题
五、异或
- 0^N == N, N^N == 0(相同是0,不同是1)
- 异或运算满足交换律和结合率
- 不用额外变量交换两个数(用加法也行但是不好)
- 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这一个数
- 一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两个数
六、对数器的概念和使用
- 有一个你想要测的方法a
- 实现复杂度不好但是容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
- 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者方法b
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
七、递归
用递归方法找一个数组中的最大值,系统上到底是怎么做的?
时间复杂度的估算,master公式
- log(b,a) > d 则复杂度为O(N^log(b,a))
- log(b,a) = d 则复杂度为O(N^d * logN)
- log(b,a) < d 则复杂度为O(N^d)
第二课 优化排序
一、归并排序
- 整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
- 让其整体有序的过程里用了外排序方法
- master公式得到时间复杂度O(N * logN),额外空间复杂度O(N)
二、归并拓展应用
小和问题(每一个数左侧比自己小的数之和),逆序对问题 思路:首先照搬归并排序,与此同时在合并操作时,记录打印所求内容
三、堆
- 堆(heap)结构就是用数组实现的完全二叉树结构;堆中某个结点的值总是不大于或不小于其父结点的值。
- 如果每棵子树的最大值都在顶部就是大根堆
- 如果每棵子树的最小值都在顶部就是小根堆
- 堆的heapInsert操作:把新元素M放在末尾;然后开始循环,看M是否比父亲大,大则和父亲交换,直到不大于父亲或者抵达堆顶。
- 堆的heapify操作:弹出堆顶元素,把堆末尾元素M放在堆顶;然后开始循环操作,看M是否比两个子节点都大,假如不是就把较大的节点和M交换,周而复始即可。
- 优先级队列结构,就是堆结构,java中是 PriorityQueue.class
四、堆排序
- 先让整个数组都变成大根堆结构。建立堆的普通方法,即是逐个元素向堆中heap insert,不难想象时间复杂度为O(N * logN)。
- 优化方法:在树中,从下至上,依次为每个节点,进行heapify的下沉操作(叶子根本不动),此方法遍历后,大根堆形成,其时间复杂度为O(N),优先考虑此方法!
- 把堆的最大值和堆末尾的值交换,然后减少堆的大小,再去调整堆,周而复始,时间复杂度为O(N * logN)
- 堆的大小减小成0之后,排序完成
五、堆排序拓展
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
思路:用高度为k的堆,来过一遍数组即可!
六、荷兰国旗问题
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
额外准备两个指针位于收尾,遍历一遍数据和指定值进行比较,进而和收尾两指针的值交换,合理控制指针的行为,即可解决。
七、快速排序
- 在数组范围中,等概率随机选一个数作为划分值,然后把数组通过荷兰国旗问题分 成三个部分:左侧<划分值、中间==划分值、右侧>划分值
- 对左侧范围和右侧范围,递归执行
- 时间复杂度为O(N * logN)
也称随机快排,是排序最优选择!随机选择动作,可以使空间复杂度,在数学期望上收敛到 N * logN
第三课 非比较排序&排序总结
一、比较器
二、桶排序
1)计数排序
2)基数排序:准备10(因为是十进制)个桶(队列),记为0到9,依照先低位后高位的顺序,进出桶
分析:
- 桶排序思想下的排序都是不基于比较的排序
- 时间复杂度为O(N),额外空间负载度O(M)
- 应用范围有限,需要样本的数据状况满足桶的划分
三、排序算法汇总
| 时间复杂度 | 空间复杂度 | 稳定性 | |
|---|---|---|---|
| 选择排序 | O(N^2) | O(1) | X |
| 冒泡排序 | O(N^2) | O(1) | √ |
| 插入排序 | O(N^2) | O(1) | √ |
| 归并排序 | O(N * logN) | O(N) | √ |
| 随机快排 | O(N * logN) | O(logN) | X |
| 堆排序 | O(N * logN) | O(1) | X |
稳定性:同样值的个体之间,如果不因为排序改变相对次序,即称这个排序是有稳定性的;否则没有。
排序时间复杂度小于 O(N * logN) 的算法,没有!!!
排序时间复杂度O(N * logN),额外空间复杂度O(1),又稳定的排序,没有!!!
第四课 链表
一、哈希表
- Java中,是HashMap 和 HashSet ,这两个认为是同种结构(仅仅是有无伴随数据的区别)
- 其他语言中叫 UnOrderedSet、UnSortedSet
- 它的增删改查操作,都是O(1),但是常数很大
- 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小;如果不是基础类型,内部按引用传递,内存占用就是其内存地址的大小
二、有序表
- Java中,是TreeMap 和 TreeSet ,这两个认为是同种结构(仅仅是有无伴随数据的区别)
- 其他语言中叫 OrderedSet、SortedSet
- 有序表和哈希表的区别是,有序表把key按照某种顺序组织起来,而哈希表完全不组织
- 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
- 内存情况同哈希
- 它除了增删改查操作外,还有其他操作,比如找到最小(firstKey),找到小于等于某数的最大的那个数(floorKey),这些操作的时间复杂度是 O(logn)
三、链表
- 有单链表和双链表,只需要给定一个头部节点head,就可以找到剩下的所有的节点。
- 单链表的题目看似简单,只要懂了流程即可,但是coding十分恶心,建议自己手写!
- 注意自己实操时,可以使用额外空间(哈希表),很很简单;但是面试时,力求额外空间为O(1)
- 题目:反转单向和双向链表,【要求】时O(N),空O(1)
- 题目:打印两个有序链表的公共部分,【要求】时O(N),空O(1)
四、题:判断回文结构
判断一个链表是否为回文结构
【要求】时O(N),空O(1)
思路:快慢指针。或者先遍历一遍得到长度,然后逆序到中点,然后进行比较即可
五、题:荷兰国旗
将单向链表按某值划分成左边小、中间相等、右边大的形式。
【要求】时O(N),空O(1),并且保证稳定性!
思路:需要6个指针,遍历一遍原链表,自己生成三根大中小链表,再拼接即可。
六、题:随机指针
【题目】复制含有随机指针节点的链表,一种特殊的单链表节点类描述如下
class Node {
int value;
Node next;
Node rand;
Node(int val) {
value = val;
}
}
rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】时间复杂度O(N),额外空间复杂度O(1)
思路:把原链表改造成 1,1‘,2,2‘,3,3’,...... 这样的结构,然后就好办了
七、题:综合判断两单链表是否相交
【题目】给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返回null
【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。
思路:
- 先写出返回一个单链表入环节点的函数,没环就返回空。作法是快慢指针:必然会在环内相遇,然后从起点和相遇点重新开始以步长1前进,再次相遇点,即为所求。
- 讨论两链表是否有环:假如情况不同,则注定不会相遇;
- 假如同为无环,先看终点是否同,同则继续,求两链表长度,长的链表先甩掉多余部分,而后两链表等速前进,容易求出答案。
- 假如同为有环,则首先拿到两个入环点,如同,则等效于3;不同,则loop1前进看能否在再次回倒loop1之前遇到loop2,遇到则返回loop1或者loop2;没遇到,则说明不相遇返回空。
第五课 二叉树
一、二叉树
- 递归方式遍历一棵二叉树:先序、中序、后序。
- 非递归方式先序遍历:先把头节点入栈,然后开始循环【出栈一个节点,打印,把此节点的右节点入栈,把此节点的左节点入栈】,在栈不为空时,往复此循环。
- 非递归方式中序遍历:和先序思路一样,区别是先左后右,重点是不打印而是存入一个暂存栈,最后再把暂存栈依次出栈打印即为所求。
- 非递归方式后序遍历:cur节点指向头节点,开始循环【如果cur不为空,则入栈,同时cur指向cur的左节点;否则出栈一个元素,cur指向此元素并打印,然后cur指向cur的右节点】,栈不为空或者cur节点不为空,就一直往复此循环。
- 二叉树的深度遍历(就是先序遍历)。
- 宽度遍历(使用队列)。求最大宽度问题(也需要一个队列,和一个挡板元素)。
二、常见概念及算法
- 判断一棵树是搜索二叉树的算法(中序遍历的角度考虑)
- 判断一棵树是完全二叉树的算法(宽度遍历的角度)
- 判断一棵树是满二叉树的算法(节点个数和最大深度满足固定的关系)
- 平衡二叉树
- 树形DP(基本通用的递归套路):向左树和右树要全量信息,然后写递归即可
三、求最低公共祖先节点
给定一个二叉树的头节点 head ,和任意两个后辈节点 node1、node2 ,求他们俩的最低公共祖先节点
- 简单的方法:维护一个字典记录每个节点的父节点,然后把node1至head的所有节点放入一个HashSet,再把node2向上追溯,直至与HashSet中某个相同,即为所求
- 有一个优秀的递归算法,记录下
public static Node lowestAncestor(Node head, Node o1, Node o2) {
if (head == null || head == o1 || head == o2) {
return head;
}
Node left = lowestAncestor(head.left, o1, o2);
Node right = lowestAncestor(head.right, o1, o2);
if (left != null && right != null) {
return head;
}
return left != null ? left : right;
}
四、求后继节点
- 二叉树中序遍历, node的下一个节点叫作node的后继节点。
- 假如每个节点额外新增一个指针,指向自己的parent,如何优化算法?
- 答:先看本节点是否有右节点,如果有则打印右节点的最左侧节点;否则一路向上找,找到扮演左儿子的那个节点,打印它;假如都找不到,那么说明本节点就是中序的最后节点了。
五、二叉树的序列化和反序列化
- 随便使用前中后排序。
- 自定义分隔符(用来区分元素,不然12是十二,还是一和二),和null的表示符。
六、有趣的折纸问题
- 递归方法:其实这就是一棵起始为凹,每节点左子为凹,右子为凸,的完全二叉树,中序遍历即为所求。
- 法二:纸面面对玩家的会进化成正凹反,背对玩家的会进化成正凸反
第六课 图
一、图的存储表达方式
- 邻接表
- 邻接矩阵
- 很多种方式都可以表示图。思路是选择固定好一种自己喜欢的方式,然后用此方式实现所有图的算法。遇到真实问题时,可以先将真实问题转换为自己熟悉喜欢的算法,再套已经实现的算法即可。
二、图的宽度优先遍历和深(广)度优先遍历
三、拓补排序算法
- 真实案例:Maven 编译时,各种依赖图的结构,那么先编译哪个呢?
- 适用范围:要求有向图,且有入度为0的节点,且没有环
- 方法:先处理入度为0的节点,然后去掉此节点的影响,周而复始即可。
四、最小生成树(kruskal算法)
- 最小生成树概念:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有结点,并且有保持图连通的最少权重的边。
- 算法流程:首先把所有节点罗列好,然后把边,按照权重由小到大的顺序,依次尝试加入,倘若发现会导致出现环则不加入。遍历所有边后即为所求。
- 判断是否出现环的Coding思路:起初时,每个点独占一个集合(维护一个HashSet记录每个点到自己所属集合的映射,K是点,V是点集的HashSet);随着边的加入,被链起来的点所在的集合进行合并;新边加入时,只要没产生合并集合的影响,即说明产生了环。
- 通过并查集来判断是否产生环
五、最小生成树(prim算法)
- 任取一个点,并以此点成立一个已处理点集,再把此点关联的所有边纳入一个解锁边集,
- 从解锁边集中弹出一个最小权重的边,看此边是否能触及已处理点集中不存在的点,不能就继续弹出,能的话记录此边进入结果集,再把此边关联的新点纳入已处理点集,同时又能解锁新点的关联边(纳入解锁边集)
- 重复2号步骤
- (假如整片图不是连通的,就需要)对所有点都要再从1开始走一遍流程。
- 结果集即为所求。
六、Dijkstra算法
- 目的为了求图中某点A到其他所有点的最短路径
- 需要注意,不能有累计路径合为负数的环存在,存在则最短路径是无穷小!
- 算法流程:首先初始化一张表,记录A到A自己距离为0,A到其他点为无穷大;然后从表中找到距离最短的点,设想以此点为中继节点,判断A经过中继节点,到达其他所有点的距离,能否缩小(最开始都是正无穷,必然能缩小),若确实能缩小则记录;随后从表中排除A点,再次从其他的点位找距离最短的点,周而复始,即为所求。
第七课 前缀树&贪心
一、前缀树
- 有一个含有若干元素(字符串)的集合,我们可以将这些元素,逐一放入一个二叉树,方法是例如把字符串拆成字符,然后每个字符是二叉树的一个节点。
- 二叉树每个节点加入两个要素:pass 和 end,意味经过次数和终点次数。
- 如此以来,就可以用一棵二叉树来表示我们的字符串集合。
- 这样可以优化很多对于此集合的操作,比如:efefe前缀的出现次数?等.......
二、贪心算法
- 在某一个标准下,优先考虑最满足标准的行为流程,最后考虑最不满足标准的行为流程,最终得到一个答案的算法,叫作贪心算法。
- 也就是说,没有从整体最优上加以考虑,我们所做出的,仅仅是在某种意义上的局部最优解。
三、默认潜规则
- 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
- 脑补出贪心策略A、贪心策略B、贪心策略C...
- 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
- 不要去纠结贪心策略的证明
四、拼接最小字典序问题
- 组织给定的字符串集合,形成最小字典序问题,字典序:A就是1,B就是2,C就是3......
- 解:以Comparator:AB < BA 排序,再顺序拼接即可
五、技巧
- 根据某标准建立一个比较器来排序
- 根据某标准建立一个比较器来组成堆
六、花费铜板切割黄金问题
- 也叫哈夫曼编码
- 所有数据进入小根堆,每次弹出两个,然后把相加的结果再heap insert,周而复始,即为所求。
七、安排会议问题
- 给定有限个会议起止时间,求能安排的最大会议数
- 解:循环优先挑选结束时间最早,且和已订会议不冲突的即可
八、做项目问题
有限的资金和做项目次数,面对若干有启动资金门槛和确定净利润的项目,该如何决定项目串行顺序,以求在有限做项目次数内,获得最大受益?
假如常规排序算法(依次把够门槛的项目找到其最大值),时间复杂度是n平方,可以使用一个大根堆(收益)和一个小根堆(门槛),这样时间复杂度是 n * logn;先全部进门槛小根堆,弹出进入收益大根堆,直到限额,随后去做大根堆堆顶的项目,回来后小根堆继续弹出进入大根堆,周而复始!
九、动态中位数
一个数据流中,随时可以取得中位数(跟贪心没有关系)
依旧使用一个大根堆和一个小根堆,维持两堆数量差不大于2;这样可以把时间复杂度从 n 降低为 logn
补、N皇后问题
N皇后问题是指在N * N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。给定一个整数n,返回n皇后的摆法有多少种。
n=1,返回1。
n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。
n=8,返回92。
- 假定第一行注定就是放最靠左的位置,从第二行开始,进行满足条件的排列,即可求得,时间复杂度是 n-1 的阶乘。
- 一个有趣的优化方式,是把计算转化成位运算,可极大幅度提高算力。
第八课 暴力递归
一、暴力递归
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
二、汉诺塔问题
左中右三个空间,大不能压小,打印n层汉诺塔从最左边移动到最右边的全部过程
三、打印一个字符串的全部子序列
四、打印一个字符串的全部排列
打印一个字符串的全部排列,要求不要出现重复的排列
五、逆序栈
要求:不能申请额外的数据结构,只能使用递归函数。
首先写一个获取并移除栈底的递归函数
public static int getAndRemoveBottomElement(Stack<Integer> stack) {
int result = stack.pop();
if (stack.isEmpty()) {
return result;
} else {
int last = getAndRemoveBottomElement(stack);
stack.push(result);
return last;
}
}
然后再写反转的递归函数即可
public static void reverse(Stack<Integer> stack) {
if (stack.isEmpty()) {
return;
}
int i = getAndRemoveBottomElement(stack);
reverse(stack);
stack.push(i);
}
六、数字转字母
规定1和A对应、2和B对应、3和C对应... 那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。 给定一个只有数字字符组成的字符串str,返回有多少种转化结果。(需考虑0的问题)
七、装袋子问题
给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?
八、拿牌问题
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。