算法和数据结构基础
2021-08-25 10:58:17 2 举报
使用 (¥10)
AI智能生成
左程云老师算法和数据结构基础学习笔记
作者其他创作
大纲/内容
核心指标
时间复杂度
衡量流程中进行常数操作的次数
原则
需要考虑最差情况
将整个流程拆解为一个一个基本动作,<b>每个动作是常数操作</b>
查看数据量为N时,基本操作和N的关系
最后,只考虑最高阶项
<b>额外</b>空间复杂度
常数项时间
同一复杂度的算法比较的时候,才需要比较
通过大样本数据测试,为简单
与功能无关,实现算法是额外需要的空间
基本数据结构
Stack
Vector
Queue
Linked List
Priority Queue
Heap
Set
Hash Set
Tree Set
Map
HashMap
TreeMap
基本算法
Greedy
Recursion、Backtrace
Traversal
Breadth-first / Depth-first search
Divide and Conquer
Dynamice Programming
Binary Search
Graph
排序
选择排序
O(N^2)
每次循环找出最小的,放到最前面<br>循环次数:N, N-1, N-2....2, 1
不稳定
冒泡排序
O(N^2)
稳定
插入排序
O(N^2)
稳定
实现
适用于小的数组
当长度小于15,<br>执行时间会比归并实现<br>少10-15%
因为常数项低,当数量少(<=60)时,速度很快
归并排序
左有序,右有序,合并为一个大的有序数组
实质:把比较行为变成了有序信息并传递。<br>大量的比较结果信息被保留传递下去。<br>
时间复杂度O(NlogN)
额外空间复杂度O(N)
稳定
实现
递推实现
由顶向下归并<br>Top-Down Mergesort
应用分治思想<br>(divide-and-conquer)
分解为更小规模的问题,<br>然后递推解决问题
最多访问6NlogN次数组<br>
2N拷贝
2N次拷贝回原数组
最多2N次比较
由底向上归并<br>Down-Top Mergesort
分治
把小问题构建为大一些规模的问题求解
最多访问6NlogN次数组
对底层是链表的数据结构,更好<br>可以对链表in-place排序(不需要额外节点)
非递归实现
Tips
长度小于15时,用插入排序提高速度
测试左右半区是否有序,<br>而skip调用merge()
当a[mid] <= a[mid +1],则skip merge
避免多次拷贝
两个数组,交换职责
应用
计算小和:<br>计算一个数组中,一个数左边比他小的数<br>的总和,叫数的小和。所有数的小和累加<br>起来,叫数组的小和。求数组的小和。<br>
比较一个数和另一半区的比较
逆序对
排序功能不会被破坏,但稳定性破坏
快速排序
给定一个数组,和 一个整数num,把小于等于num的数防止数组的左边,大于num的数防止数组右边<br>
分治思想
partition一个数组为两个子数组,对子数组独立排序
不稳定
partition的时候不能保证稳定性
时间复杂度O(NlogN)
最差情况O(N^2)
例如对有序数组排序
对策1:随机shuffle数组
对策2:随机选择数组中的<br>一个数为基点<br>
额外空间复杂度O(logN)
桶排序<br>Bucket Sort
<b>不基于比较的排序</b>,必须<b>和数据的样本的数据状况强相关</b><br>当最小值和最大值很窄的才有意义
桶就是一个容器,可以是数组,队列,栈,或状态
计数排序
实现
时间复杂度O(N)
额外空间复杂度O(M)
稳定
基数排序
只能<b>为非负整数</b>
理论方法
建十个桶
从个位开始,按此位按队列顺序入桶,然后按队列顺序出桶
然后依次向高位比较入桶,出桶
时间复杂度,实质为O(N*Log(10)N)<br>但经常认为是O(N)
额外空间负载度O(M)
稳定
经典实现
核心实现(不用队列实现)
堆排序
堆结构:逻辑概念,就是<b><font color="#c41230">用数组实现的完全二叉树结构</font></b><br>实现可以是数组或链表
完全二叉树数组实现的时候:<br>i =><font color="#c41230">LC: 2*i + 1<br> RC: 2*i + 2</font><br> Parant: <font color="#c41230">(i - 1)/ 2</font>
改进:0位置不用,从1位置开始排列。<br>i => <font color="#c41230">LC: 2 * i ==> i << 1</font><br> <font color="#c41230">LR: 2 * i + 1 ==> (i << 1 | 1)</font><br> Parent : <font color="#c41230">i / 2 ==> i >> 1</font>
完全二叉树中,如果每棵子树的<b><font color="#c41230">最大值都在顶部</font></b>,就是<font color="#c41230"><b>大根堆</b></font>
完全二叉树中,如果每棵子树的最<font color="#c41230"><b>小</b></font>值都在顶部,就是<b><font color="#c41230">小根堆</font></b>
结构中heapSize的作用
堆有效数值的个数
堆中最后一个有效位置
参与到运算中判断越界的条件
重要操作:
heapInsert
1. Insert到数组的下一个位置(index);<br>2. arr[index]和父节点进行比较,直到不比父节点大的时候结束。<br>(实际有两个条件:1)不比父节点大;2)来到根节点<br>因为:当为根节点的时候,根节点的本身不会大于自己,所以该条件等同条件1)。)
插入操作的时间复杂度O(logN)<br>也就是树的高度
heapify
从index位置,往下看,不断下沉;<br>当1)我的孩子都不再比我大;或2)已经没有孩子了,停止
比较操作的时间复杂度O(LogN)
优先级队列结构,就是堆结构
参考实现:algorithmbasic2020/src/class04/Code02_Heap01.java
不稳定
时间复杂度O(NlogN)
额外空间复杂度O(1)
结构类型
大根堆
小根堆
PriorityQueue默认是小根堆
堆排序算法
将数组变成大根堆
从上到下的方法:时间复杂度为O(N*LogN)
从下到上的方法,时间复杂度为O(N)
swap(0,N-1);目的把Max放在了N-1的位置
对0~(N-2)做heapify操作,获得另一个大根堆
时间复杂度O(N*LogN)
重复2nd,直到0
两种堆调整的代价<br>heapify
从上往下
复杂度高<br>因为页节点,节点数多,<br>且复杂度最高(移动logN步)<br>
靠近上层节点少,移动代价低<br>靠近下层节点多,移动代价高
从下往上
复杂度低<br>因为层级越多,复杂度越少,<br>页节点,则都不会产生移动
靠近上层的节点少,移动代价高<br>靠近下层的节点多,移动代价低<br>
语言提供的堆结构 vs 手写堆结构
取决于,有没有动态该信息的需要!
语言提供的堆结构,如果<font color="#c41230">动态改数据</font>,不保证依然有序
代码示例:algorithmbasic2020/src/class04/Code03_Heap02.java
例如:迪杰斯特拉算法(Dijkstra) (见下,“图”那一节)
手写堆结构,因为增加了对象的位置表,所以能满足动态改信息的需求
例题:<br>已知一个几乎有序的数组。几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离一定不超过k,<b>并且k相对于数组长度来说是比较小的</b>。<br>请选择一个合适的排序策略,对这个数组进行排序。<br><br>两个参数:arr + k<br>参考代码:algorithmbasic2020/src/class04/Code05_SortArrayDistanceLessK.java
1.建一个k+1的小根堆
小根堆放的是当前位置的最小的可能性
2. 从0 - k+1加入小根堆,弹出小根堆头,然后放到 0 位置
3. 从k+2加入小根堆,然后放到1位置
4. 重复3,直到遍历结束
排序算法的稳定性
稳定性指同样大小的样本再排序后会不会改变相对顺序
对基础类型而言,稳定性没有意义
对非基础类型而言,稳定性有重要意义
总结
不基于比较的排序,对样本数据有严格要求,不易改写
基于比较的排序,只要规定好连个样本怎么比较大小就可以直接复用
基于比较的排序,时间复杂度的极限是O(N*logN)
时间复杂度O(N*logN),额外空间复杂度低于O(N),且稳定的基于比较的排序是不存在的
为了绝对的速度选快排
为了省空间选堆排
为了稳定选归并
常见的坑
归并排序的额外空间复杂度可以变成O(1),但是不再稳定<br>搜索“归并排序 内部缓存法”
应该用堆排
"原地归并排序“是垃圾帖,会让时间复杂度变成O(N^2)
快速排序稳定性改进,“01 stable sort”,但是对样本数据要求更多
在整型数组中个,请把奇数放在数组左边,偶数放在数组右边,要求所有奇数之间原始的校对次序不变,所有偶数之间的原始相对次序不变。<br><br>时间复杂度做到O(1), 额外空间复杂度做到O(1)
同理“01 stable sort"<br>但对样本数据要求更多
工程上对排序的改进
通常对稳定性进行考虑
实现时,查排序的数据类型,如果为基础数据,则采用快排
如果是非基础类型,则采用归并排序==>为了保持稳定性
需充分考虑利用O(N*logN) 和 O(N^2) 排序的各自优势
例如在归并排序中,经常存在:<br><br>if (L + 60 < R) {<br> 选用插入排序算法<br>}
快排、堆排、归并,虽然排序调度算法很好,为O(N*logN), 但常数项很高,在少量样本排序的时候,比插入排序慢。
栈和对列
均为逻辑概念
实现
双向链表
数组
参考代码:algorithmbasic2020/src/class02/Code03_DoubleEndsQueueToStackAndQueue.java
Ring Buffer实现
判断栈满或空的方法
方法1: 浪费Buffe中的一个slot<br>Full => head + 1 == tail<br>Empty => head == tail
一个slot可能的代价比较大,<br>如果成员是一个大Obj<br>
方法二:用size变量记录成员个数
方法三: 用一个布尔变量full和额外逻辑判断。<br>Full => full == true<br>Empty => (head == tail) && !full<br><br>full在每次添加成员的时候,判断赋值。<br>eg static void add(Node N) {<br>.....<br> if (head == tail) this.full = true;<br>}<br>
逻辑相对复杂,而且<br>每次添加均需要判断<br>
固定长度
实现一个栈,push/pos/getMin的复杂度都是O(1)
设计两个栈,一个Data,一个Min
比较:
方法一:<br>push同时加入Data栈和Min栈,但加入元素和Min的栈顶比较,加入小的值<br>pop同时弹出Data和Min, Data栈顶返回<br>getMin直接从Min栈顶返回
方法二:<br>push压入Data栈,同时比较Min栈顶,如果New<=Min栈顶,则压入Min<br>pop当Data栈顶和Min栈顶相等的时候,Data和Min均需要弹出<br>getMin直接从Min栈顶返回
用队列实现栈
两个队列:Data 和 Helper
用栈实现队列
两个栈:Push和Pop
pop栈需要为空
push必须一次性倒完
递归
只有递归规模是等规模的时候,<br>如果递归的复杂度符合公式:<br><b>T(N) = a * T(N/b) + O(N^d)</b><br>a: 调用次数<br>b: 分治大小<br>d: 除递归以外部分的指数<br>
log(b,a) > d ==> O(N^log(b,a))
log(b,a) <d ==> O(N^d)
log(b,a) == d ==> O(N^d * logN)
递归实现的三个条件
可分解性
一个问题可以分解为几个问题的解
分解后的等价性
这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
存在递归终止条件
递归都可以用非递归的方式表示
缺点
警惕堆栈溢出
代码中限制递归调用的深度
警惕重复计算
通过Hash表来保存已经求解过的f(k)
函数调用耗时多
考虑空间复杂度
哈希表<br>HashMap/HashSet
增删改查,使用时,时间复杂度O(1)
HashMap中,基础类型作为Key,即便是Wrapper<br>例如:Integer,Double等,<br>永远按值传递
(HashMap中,非基础类型作为Key,则按引用传递
有序表<br>TreeMap
时间复杂度O(logN)
Key有序
底层实现通常为
平衡二叉树
AVL
SB (Size-Balance) Tree
红黑树
跳表(Skip Map)
比较器
实质:重载比较运算符
<ol><li>返回负数的时候,第一个参数排在前面</li><li>返回正数的时候,第二个参数排在前面</li><li>返回0的时候,谁在前面无所谓</li></ol>
应用在特殊标准的排序上
应用在根据特殊标准排序的结构上
简化代码,适用于泛型编程
代码示例:algorithmbasic2020/src/class04/Code01_Comparator.java
前缀树
<ol><li>单个字符串中,字符从前到后的加到一棵多叉树上</li><li>字符防在路上,字节上有专属的数据项(常见的是pass和end值)</li><li>所有样本都这样添加,如果没有路就新建,如有路就复用</li><li>沿途节点的pass值加1,每个字符串结束时来到的节点end值增加</li></ol><br>可以完成前缀相关的查询
实现一
Node结构:<br>public class TerieTree {<br> <b>public static class Node1 {<br> private int pass = 0;<br> private int end = 0;<br> private Node1[] nexts;<br><br> public Node1() {<br> pass = 0;<br> end = 0;<br> nexts = new Node1[26];<br> }<br> }</b><br><br> public static class Ter1() {<br> <b>private Node1 root;</b><br><br> public Trie01() {<br> root = new Node1();<br> }<br> ......<br> }<br>}
插入Insert
查找Word单词出现的次数
查找有几个字符串是以pre这个字符串为前缀的
删除
实现二
Node结构:<br>public static class Node2 {<br> public int pass;<br> public int end;<br><b> public HashMap<Integer, Node2> nexts;</b><br><br> public Node2() {<br> pass = 0;<br> end = 0;<br> nexts = new HashMap<>();<br> }<br>}
当字符比较多的时候,用Hash表的方式来代表每一条路。
实现三(Brute Force)
应用和扩展
如果某个问题带有前缀查找特征,则可以在节点内部增加某种特定信息,从而用前缀树的方法来处理
链表
面试方法论<br>
笔试,重点考察时间复杂度
面试,时间复杂度依然第一位,但一定要找到空间最省的方法
面试常用数据结构和技巧
使用容器(哈希表,数组等)
快慢指针
快慢指针
输入链表头节点,奇数长度返回中点,偶数长度返回<b>上中点</b>
arr.get((arr.size() - 1) / 2)
输入链表头节点,奇数长度返回中点,偶数长度返回<b>下中点</b>
arr.get(arr.size() / 2)
输入链表头节点,奇数长度返回中点,偶数长度返回<b>上中点前一个Node</b>
arr.get((arr.size() - 3) / 2)
输入链表头节点,奇数长度返回中点,偶数长度返回<b>下中点前一个Node</b>
arr.get((arr.size() - 2) / 2)
链表的反转
参考代码:algorithmbasic2020/src/class02/Code01_ReverseList.java
单链表实现
双链表实现
链表删除特定值
参考代码:algorithmbasic2020/src/class02/Code02_DeleteGivenValue.java
实现
考虑删除值为头节点的连续的节点
删除从新节点开始的后续节点中,给定值的节点
常见面试题:<br>给定一个单链表的头节点head,请判断该链表是否为回文结构。
笔试:<br>遍历链表,每个节点加入一个栈。<br>然后再次遍历,每一个动作,从栈中弹出一个节点进行比较。<br>如果有不同,则返回false。否则,true。<br><br>因为stack加入,再弹出,表示一个逆序。
利用<b><font color="#c41230">快慢指针</font></b>:<br>找到中点,<br>然后从慢指针开始,后半部压入栈,<br>然后从链表头开始比对栈中的每一个出栈节点,直到栈为空。<br>如果有任何一个栈节点不等于链表的节点,则为false,否则为true。
利用快慢指针:<br>找到中点;<br>调整中点指向null;<br><b>中点后的next指针反转指向前一个节点;</b><br>然后从两端向中间遍历,如果有不等,则为false。<br>直到任何L指针或R指针指向null,则结束比较,返回true
单链表逆序实现
常见面试题:<br>将单链表按某值划分成左边小,中间相等,右边大的形式
笔试:<br>把链表放在数组里,在数组上做partition
常见面试题:<br>一种特殊的单链表类如下:<br>class Node {<br> int value;<br> Node next;<br> Node rand;<br> Node(int val) { value = val;}<br>}<br>rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。<br>给定一个有Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。<br>【要求】时间复杂度O(N), 空间复杂度O(1)
笔试:<br>利用Hash表,记录<br><key,value> => <Origin Node, Clone Node>
面试:<br>不用Hash表,强制将Clone Node插入原链表中。<br><b>如 1->2->3 ==> 1->1'->2->2'->3->3'<br>利用此结构代替Hash表,记录新老Node对信息</b>。
常见面试题:难度 *****<br>给定两个可能有环也可能无环的单链表,头节点head1和head2,请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null<br>【要求】如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)<br><br>相交,在此同节点上的值没有关系,指的是内存地址。<br>code:algorithmbasic2020/src/class06/Code05_FindFirstIntersectNode.java
判断是否有环,有环的情况下返回第一个入环的节点
方法一: 利用Set Map
方法二: <b>利用快慢指针</b><br>1. S指针一次走一步,F指针一次走两步,然后从开头一起出发,最后到相遇;<br>2. 相遇后,F指针从头开始走,一次走一步,S指针从相遇的节点开始走,直到下次相遇。此节点为产生环的第一个节点。
如果两个链表都无环,则此两链表一定最后的节点是相同的节点。<br>实现:
如果为两个有环链表,放回第一个相交节点,否则不相交,返回null。<br>如果两个有环链表相交,一定共用环。<br>
如果一个有环,一个无环,则这两个链表不可能相交。
主方法
常见面试题:<br>能不能不给单链表的头节点,只给想要删除的节点,就能做到在链表上把这个节点删除掉?
取巧的办法可以。<br>来到删除节点,copy下一个节点的值到该节点,然后删除下一个节点。
问题1:其实采用了copy方法,并没有删除自己<br>
问题2:如果节点不能调用copy或构造方法,则有问题
问题3: 绝对无法删除最后一个节点
约瑟夫环????
二叉树<br>Binary Tree
结构<br>Class Node {<br> V value;<br> Node left;<br> Node right;<br>}
深度优先遍历
递归实现
先序:任何子树的处理顺序都是,先头节点,再左子树,然后右子树
中序:任何子树的处理顺序都是,先左子树,再头节点,然后右子树
后序:任何子树的处理顺序都是,先左子树,再右子树,然后头节点
非递归实现
先序:<br>利用一个栈<br>压头节点,<br>以后按统一逻辑计算:<br>1. 弹出打印<br>2. 如有右,要入右<br>3. 如有左,压入左
中序:<br>1. 整条左边界依次入栈:<br>2. 如1无法继续,则弹出节点,就打印,且来到节点右树上,继续执行条件1.
其实每一棵树都可以用“左头”分解,而且每个节点是处理完了左子树,然后再处理右子树,继续用“左头“的方式分解
后序:<br>利用两个个栈<br>压头节点,<br>以后按统一逻辑计算:<br>1. 弹出,压入栈2<br>2. 如有左,压入左<br>3. 如有右,要入右<br>4. 从栈2中出栈,然后打印
后序实现2
宽度优先,按层遍历
用队列<br>头压队列<br>队列不为空,弹出,加左,再加右,
利用flag实现监控某层是否结束
求树最大宽度
用map机制
不用map
二叉树的序列号和反序列化
可以用先序或者中序或者后序或者按层遍历,来实现二叉树的序列化<br>用了什么方式序列化,就用什么方式反序列化<br>
利用null补齐树,不要忽略空,然后进行遍历
中序的序列化,<b>不能</b>用此方法实现反序列化。<br><br>二叉树无法通过中序遍历的方式实现序列化和反序列化<br>因为不同的两棵树,可能得到同样的中序序列,即便补了空位置也可能一样。<br>比如如下两棵树<br> __2<br> /<br> 1<br>和<br> 1__<br> \<br> 2<br>补足空位置的中序遍历结果都是{ null, 1, null, 2, null}<br>
先序实现
序列化<br>把序列化的时机放在<b>加入</b>队列的时候
反序列化
宽度优先序列化实现
序列化
反序列化
如何设计一个打印整棵树的打印函数
二叉树结构定义如下:<br>Class Node {<br> V value;<br> Node left;<br> Node right;<br> Node parent;<br>}<br>给你二叉树中的某一个节点,找出后继节点
Broto force方法
实现:<br>1. 当X有右树的时候,后继节点是右子树的最左的节点;<br>2. 当X没有右树的时候,X节点是后继父节点的左子树的最右的孩子。<br>方法:向上找父节点,直到自己是父节点的左孩子,且父节点不是空,则此父节点是后继;<br>如果父节点为空,则X为此树的最右节点,不存在后继节点
题目:<br>请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折一次,压出折痕后展开。此时折痕是凹下去的,即折痕凸起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。<br>给定一个输入参数N,代表纸条都从下边向上方连续对折N次。请从上到下打印所有折痕的方向。<br><br>例如:N=1时,打印:down; N=2时,打印down down up。
二叉树的递归套路
可以解决面试中绝大多数的二叉树问题,尤其是树形dp问题
本质是利用递归遍历二叉树的便利性
套路
假设以X节点为头,假设可以向X左树和X右树要任何信息
在上一步的假设下,讨论以X为头节点的树,得到答案的可能性(最重要)
常见分类:<br>需获取信息和X有关还是无关?
与X无关
不通过X
与X有关
通过X
列出所有可能性后,确定到底需要向左树和右树要什么样的信息
<b>把左树信息和右树信息求全集</b>,就是任何一棵子树都需要返回的信息S
递归函数都返回S,每一颗子树都这么要求
写代码,在代码中考虑如何把左树的信息和右树的信息整合出整棵树的信息
递归套路
给定一棵二叉树的头节点head,返回这颗二叉树是不是平衡二叉树
实现:
给定一棵二叉树的头节点head,任何两个节点之间都存在距离,返回整棵二叉树的最大距离
与X无关
左树最大距离
右树最大距离
与X有关
左树高度 + 右树高度 + 1
实现
给定一棵二叉树的头节点head,返回这颗二叉树中最大的二叉搜索子树的size<br><br>搜索二叉树:整个二叉树没有重复值,且左树比头节点小,右树比头节点大,每棵子树都如此
与X无关
左树是最大二叉搜索子树
右树是最大二叉搜索子树
与X有关
左树和右树都是最大二叉搜索子树,<br>且左树(最大值)小于X,右树(最小值)大于X
可获得信息:<br>左:最大子搜size<br> bool isAllBST<br> maxValue
可获得信息:<br>右:最大子搜size<br> bool isAllBST<br> minValue
实现
给定一棵二叉树的头节点,返回这颗二叉树中最大的二叉搜索子树的头节点
派对的最大快乐值:<br>员工信息定义如下:<br>class Employee {<br> public int happy; // 这名员工可以带来的快乐值<br> List<Employee> subordinates; // 这名员工有哪些直接下级<br>}<br><br>公司的每个员工都符合Employee结构。整个公司的人员机构可以看做是一棵标准的、没有环的多叉树。树的头节点是公司唯一的老板。除老板外的每一个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每一个员工都有一个或多个直接下级。
X来:<br>X.happy<br>a不来的子树的最大快乐值<br>b不来的子树的最大快乐值<br>c不来的子树的最大快乐值
X不来:<br>X = 0;<br>a来的子树的最大值,或a不来的子树的最大值;<br>b来的子树的最大值,或b不来的子树的最大值;<br>c来的子树的最大值,或c不来的子树的最大值<br>
ChildTopic
判断是否是满二叉树
实现
判断是否为完全二叉树
<b>宽度优先遍历</b>:<br>1)任何节点,有右无左,则false。否则继续。<br>2)<b>一旦遇到左右孩子不双全,后续遇到的所有节点必须为叶节点</b>。
实现
实现1
递归套路:
1. 满二叉树,无缺口;<br>2. 有缺口:<br>1)左树有缺口(没有越过左树)==>左 完全,右树满,左高 = 右高 + 1<br>2)左树刚满 ==> 左满,右满,且 左高= 右高 + 1<br>3)右树有缺口 ==> 左满,右是完全二叉树,且 左高 = 右高
信息:左树 是否满,是否完全二叉树 及 高度<br> 右树:是否满,是否完全二叉树 及 高度
实现:
return new Info(isFull, isCBT, height);<br>}
测试
难度:***<br>给定一棵二叉树的头节点head,和另外两个节点a和b。<br>返回a和b的最低公共祖先。
方法1:<br>通过遍历,生成一个map<节点,父节点>。<br>对a遍历,把所有的父节点加入一个set<br>对b遍历,查看是否父节点在set中。<br>第一个发现的节点,则是最低公共祖先。
方法2:递归套路
1.O1,O2 都没有一个在X上;<br>2. O1, O2 只有一个在X上;<br>3. O1, O2 都在X为头的树上;<br> 1) 左右各一个;<br> 2)左O1,O2;<br> 3) 右O1,O2;<br> 4) X是O2,左树或右树有O1
Info:1)任何子树的交汇节点<br> 2) 是否有O1;<br> 3) 是否有O2;
实现
4) 没有交汇点(ans = null)
贪心算法
1)最自然智慧的算法;<br>2)用一种局部最功利的标准,总是做出在当前看来最好的选择;<br>3)<b>难点在于证明局部最功利的标准可以得到全局最优解;</b><br>4)<b>对于贪心算法的学习主要以增加阅历和经验为主</b>
给定一个由字符串组成的数组strs,<br>必须把所有的字符串拼接起来,<br>返回所有可能的拼接结果中,字典序最小的结果
ChildTopic
标准过程:<br>1. 分析业务;<br>2. 根据业务逻辑找到不同的贪心策略;<br>3. 对于能举出反例的策略直接跳过,不能举出反例的策略要证明有效性
解决套路:<br>1. 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试;<br>2. 脑补出贪心策略A,贪心策略B,贪心策略C。。。<br>3. 用解法X和对数器,用实验的方法得知哪个贪心策略正确;<br>4. 不用纠结贪心策略的证明
例题:<br>一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。<br>给你每一个项目开始时间和结束时间,你来安排宣讲的日程,要求会议室<br>进行的宣讲场次最多。返回最多的宣讲场次。
暴力解法
贪心算法:<br>选择对结束时间进行排序,早结束的先安排
例题:<br>给定一个字符串str,只由'X'和'.'两种字符构成。<br>'X'表示墙,不能放灯,也不需要点亮,<br>'.'表示居民点,可以放灯,需要点亮,<br>如果灯放在i位置,可以让i-1,i和i+1三个位置点亮<br>返回如果点亮str中所有需要点亮的位置,至少需要几盏灯?
暴力解法:<br>1。每个位置去放灯,和不放灯;<br>2. 验证是否满足条件
贪心算法:<br>1. i位置为X,则略过,直接到i+1去判断;<br>2. i位置为.,则:(此分支情况下,均需要放灯)<br>1) i+1为X,则i应该放灯;<br> 转到i+2位置上去判断<br>2)i+1为., 则:<br> i) i+2位置为X,则i+1上放灯<br> ii)i+2位置为.,则i+1上放灯<br> 转到i+3位置上去判断<br><br>(i位置不会被之前位置影响到)
例题:(哈夫曼数)<br>一块金条切成两半,是需要花费和长度数值一样的铜板的。<br>比如长度为20的金条,不管怎么切,都需要花费20个铜板。一群人想分整个金条,怎么分最省铜板?<br><br>例如:给定数组{10,20,30},代表一共三个人,整块金条长度为60,金条要分成10,20,30三个部分。<br>如果先把长度为60的金条分成10和50,花费60;再把长度50分成20和30,花费50,一共花费110;<br>如果先把长度为60的金条分成30和30,花费60;再把长度30分成10和20,花费30,一共花费90.<br>输入一个数组,返回分割的最小代价
例题:<br>输入:正数数组costs,正数数组profits,正数K和M<br><br>cost[i]表示i号项目的花费<br>profits[i]在i号厦门在扣除花费后还能挣到的钱(利润)<br>K表示你只能串行的最多做K个项目<br>M表示你的初始资金<br>说明:每一个项目做完马上获得利润,可以支持去做下一个项目。不能并行地做项目。<br>输出:你最后获得的最大钱数。
并查集<br>UnionFind
1) 有若干个样本a、b、c、d。。。类型假设是V<br>2) 在并查集中一开始认为每个样本都在单独的集合里<br>3)用户可以在任何时候调用如下两个方法:<br> boolean isSameSet(V x, V y):查询x和y是否属于一个集合<br> void union(V x, V y): 把x和y各自所在集合的所有样本合并成一个集合<br>4) isSameSet和Union方法的代价越低越好
如果两个user,a字段一样,或者b字段一样,或者c字段一样,就认为是同一个人,<br>请合并users,返回合并后的用户数量
可以利用数组下标代表user标号=>优化
图
结构定义:
Node节点定义:<br>// 点结构的描述 A 0<br>public class Node {<br> public int value;<br> public int in; // 直接入度<br> public int out; // 直接出度<br> public ArrayList<Node> nexts; //直接邻居(由自己出发能到达的节点)<br> public ArrayList<Edge> edges; //到达直接邻居的边<br><br> public Node(int value) {<br> this.value = value;<br> in = 0;<br> out = 0;<br> nexts = new ArrayList<>();<br> edges = new ArrayList<>();<br> }<br>}<br>
”边“的定义:<br>public class Edge {<br> public int weight;<br> public Node from;<br> public Node to;<br><br> public Edge(int weight, Node from, Node to) {<br> this.weight = weight;<br> this.from = from;<br> this.to = to;<br> }<br>}<br>
”图“的定义:<br>public class Graph {<br> public HashMap<Integer, Node> nodes;<br> public HashSet<Edge> edges;<br> <br> public Graph() {<br> nodes = new HashMap<>();<br> edges = new HashSet<>();<br> }<br>}<br>
宽度优先遍历<br>bfs
Set:标记访问过的节点(避免环的访问)
队列:
深度优先遍历<br>dfs
Set:标记访问过的节点(避免环的访问)
栈:记录遍历时走的路径
业务处理在第一次入栈的时候做
拓扑排序算法
1)在图中找到所有入度为0的点输出<br>2)把所有入度为0的点在图中删除,继续找入度为0的点输出,周而复始<br>3)图的所有点都被删除后,依次输出的顺序就是拓扑排序<br><br>要求:有向图且其中没有环<br>应用:事件安排、编译顺序
实现
最小生成树算法<br>(Spanning Tree)<br>Kruskal Algo
中心思想时始终以边为主袋地位,无论如何都要选择权值最小的边,但是前提是使用此边后不能构成连通图。所以这个算法的第一步是将所有的边从小到大排序,之后按排列的顺序来一步一步选择我们需要的边。
1)总是从权值最小的边开始考虑,依次考察权值依次变大的边;<br>2)当前的边要么进入最小生成树的集合,要么丢弃<br>3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边<br>4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边<br>4)考察完所有边之后,最小生成树的集合也就得到了
实现
时间复杂度为O(e⋅loge),e为边的数目,与顶点数量无关,适合稀疏图。
最小生成树算法<br>Prim算法
从某个顶点v开始,列出顶点 v 所有邻接点的边,选择权值最小的边(vi-->vj)加入到最小生成树中,并标记该边已被访问过;<br>再从vj开始 列出顶点vj所有邻接点的边,从中选择所有未被访问过的边中权值最小的边 vj-->vk 加入到最小生成树中,并标记该边已被访问过。<br><br>重复上述操作,直到找到n-1条边为止。
时间复杂度为O(n2),n为顶点的数量,其时间复杂度与边得数目无关,适合稠密图。
Dijkstra路径算法<br>Dijkstra's Shortest Path First algorithm, <br>SPF algorithm
贪心算法的一种
ChildTopic
暴力递归
就是<b><font color="#c41230">尝试</font></b>:<br><ol><li><b>把问题转化为规模缩小了的同类问题的子问题</b>;</li><li>有明确的不需要继续进行的递归的条件(base case);</li><li>有当得到了子问题的结构之后的决策过程;</li><li>不记录每一个子问题的解;</li></ol>
汉诺塔问题:<br>有三根杆子A,B,C。A杆上有 N 个 (N>1) 穿孔圆盘,盘的尺寸由下到上依次变小。<br>要求按下列规则将所有圆盘移至 C 杆:<br>1. 每次只能移动一个圆盘;<br>2. 大盘不能叠在小盘上面。<br>提示:可将圆盘临时置于 B 杆,也可将从 A 杆移出的圆盘重新移回 A 杆,<br>但都必须遵循上述两条规则。<br>
分解为三步
N-1个圆盘移动到B杆
第N个圆盘移动到B杆
N-1个圆盘移动到C杆
考虑所有的情况
A到C
A到B
B到A
B到C
C到A
C到B
所有情况都归结为一种实现:<br>From,To, Other<br>从From移动到To,依靠Other做辅助
f(N-1, From, Other, To)
N: From to To
f(N-1, Other, To, From)
最优解时间复杂度O(2^N - 1)
例题:<br>给你一个栈,请你逆序这个栈,不能申请额外的数据结构,<br>只能使用递归函数。如何实现?
实现
两次递归调用
打印一个字符串的全部子序列
子串:<br>子序列:位置先后不能乱,但可以不连续
ChildTopic
实现
打印一个字符串的全部子序列, <br>要求不要出现重复字面值的子序列
实现
打印一个字符串的全部排列
打印一个字符串的全部排列,要求不要出现重复的排列
利用“分治限界”的思想
实现
<font color="#c41230"><b>从左往右的尝试模型1</b><br></font><br>规定1和A对应、2和B对应、3和C对应。。。<br>那么一个数字字符串比如“111”就可以转化为:<br>AAA,KA和AK<br>给定一个只有数字字符组成的字符串str,返回有多少种转化结果?
algorithmbasic2020/src/class11/Code06_ConvertToLetterString.java
实现方法一
<b><font color="#c41230">从左往右的尝试模型2</font></b><br><br>给定两个长度都为N的数组weights和values<br>weight[i]和values[i]分别代表i号物品的重量和价值。<br>给定一个正数bag,表示一个载重bag的袋子,<br>你装的物品不能超过这个重量。<br>返回你能装下最多的价值是多少?
背包问题
对应code:algorithmbasic2020/src/class11/Code07_Knapsack.java
实现一
实现二
动态规划实现
对应code: algorithmbasic2020/src/class12/Code03_Knapsack.java
<b><font color="#c41230">范围上尝试的模型<br></font></b><br>给定一个整型数组arr,代表数值不同的纸牌排成一条线,<br>玩家A和玩家B依次拿走每张纸牌,<br>规定玩家A先拿,玩家B后拿,<br>但是每一个玩家每次只能拿走最左或最右的纸牌,<br>玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。
对应code:algorithmbasic2020/src/class11/Code08_CardsInLine.java
实现一
N皇后问题<br><br>在一个N*N的棋盘上放置N个皇后,每行一个并使其不能互相攻击(同一行、同一列、同一斜线上的皇后都会自动攻击)<br><br>给定一个整数n,返回N皇后的摆法有多少种。<br>n=1,返回1<br>n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0<br>n=8,返回92
基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
时间复杂度O(N^N)
应用位运算方法,减少常数项,加速了常数时间
假设有排成一行的N个位置,记为1~N,N一定大于等于2<br>开始时机器人在其中的M位置(M一定是1~N中的一个)<br>如果机器人来到1位置,那么下一步只能往右来到2位置;<br>如果机器人来到N位置,那么下一步只能往右来到N-1位置;<br>如果机器人来到中间位置,那么下一步可以往左走或者往右走;<br>规定机器人必须走K步,最终来到P位置(P也是1~N的一个)的方法有多少种<br>给定四个参数N、M、K、P,返回方法数。
code link:algorithmbasic2020/src/class12/Code01_RobotWalk.java
可以利用缓存子过程的结果
也称为“记忆化搜索”
给定一个字符串str,给定一个字符串类型的数组arr<br>arr里的每一个字符串,代表一张贴纸,可以把单个字符剪开使用,目的是拼出str来<br>返回需要至少多少张贴纸可以完成这个任务。<br>例如:str="babac", arr={"ba", "c", "abcd"}<br>至少需要两张贴纸"ba"和“abcd",因为使用这两张贴纸,把每一个字符单独剪开,<br>含有两个a,2个b,1个c。是可以拼出str来的。所以返回2。
code:algorithmbasic2020/src/class12/Code02_StickersToSpellWord.java<br>
arr中的顺序完全无所谓
ChildTopic
两个字符串的最长公共子序列问题
algorithmbasic2020/src/class12/Code05_PalindromeSubsequence.java
做一个二维表
填充第一行和第一列
dp[i][j]
公共子序列:即不以st1r[i]结尾,也不以str2[j]结尾<br>此时dp[i][j] = dp[i-1][j-1]
公共子序列:以st1r[i]结尾,但不以str2[j]结尾<br>此时dp[i][j] = dp[i][j-1]
公共子序列:不以st1r[i]结尾,但以str2[j]结尾<br>此时dp[i][j] = dp[i-1][j]
公共子序列:即以st1r[i]结尾,也以str2[j]结尾<br>也即是str[i] == str[j]时候,<br>此时dp[i][j] = dp[i-1][j-1] + 1
返回dp[str1.length -1][str2.length - 1]
给定一个数组,代表每个人喝完咖啡准备刷杯子的时间<br>只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一个杯子<br>每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发<br>返回让所有咖啡杯变干净的最早完成时间<br>三个参数:int[] arr, int a, int b
动态规划<br>Dynamic programming<br>
常见的4种尝试模型
从左往右的尝试模型
范围上的尝试模型
多样本位置全对应的尝试模型
寻找业务限制的尝试模型
面试中设计递归过程的原则:<br>
每一个可变参数的类型,一定不要比int类型更加复杂
原则1)可以违反,让类型突破到一维线性结构,那必须是唯一可变参数
如果发现原则1)被违反,但不违反原则2),只需要做到记忆化搜索即可
可变参数的个数,尽可能少!
思路:<br>1)设计暴力递归:重要原则+4种常见尝试模型;<br>2)分析有没有重复解:套路解决;<br>3)用记忆化搜索-->用严格表结构实现动态规划:套路解决<br>4)看看能否继续优化:套路解决
图例
可变参数数量尽可能少
暴力递归到动态规划的套路
1)已经有了一个不违背原则的暴力递归,而且的确存在解的重复调用;<br>2)找到哪些参数的变化会影响返回值,对每一个列出变化范围;<br>3)参数间的所有的组合数量,意味着表的大小;<br>4)记忆化搜索的方法就是傻缓存,非常容易得到;<br>5)规定好严格表的大小,分析位置的依赖顺序,然后从基础填写到最终解:<br>6)对于有枚举行为的决策过程,进一步优化
动态规划的进一步优化
空间压缩
状态化简
四边形不等式
其他
什么暴力递归可以继续优化?
有重复调用同一个子问题的解,这种递归可以优化
如果每一个子问题都是不同的解,无法优化也不用优化
暴力递归和动态规划的关系
摸一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划
任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归
但不是所有的暴力递归,都一定对应着动态规划
面试题和动态规划的关系
解决一个问题,可能有很多尝试方法<br>可能在很多尝试方法中,又有若干个尝试方法有动态规划的方式<br>一个问题可能要若干个的动态规划的解法
滑动窗口
概念
<ol><li>R++新数从右侧进入窗口</li><li>L++数从左侧出窗口</li><li>任何时候L<=R</li></ol>
找到窗口中的最大值<br>时间复杂度O(1) ==> 指平均时间复杂度。<br>因为:N个数,每个数进一次队列,出一次队列<br>(可能是加数时,淘汰;也可能是减数时,返回且淘汰)<br>一共N个数,所以平均为O(1)
创建一个双端队列(LinkedList) (其实队列中存放arr的下标)<br>只能从尾进,只能从头出<br>维持双端队列中<b>从大到小</b>排列。
双端队列头部的值就是此时窗口中的最大值
如果此时形成的窗口状况,不想让R向右动了(即不再增加数了),而让L向右移动(即减少数),双端队列代表的是此时窗口中成为最大值的数的优先级。
加数逻辑:<br>当尾进时,查看是否队列中有比加入的数小的值,如果有,则弹出扔弃(相等时,也弹出旧值)<br>直到比队列中的值小的位置。
减数逻辑(当L向右移动时):<br>如果L指向的值小于队列中的头节点<b>下标</b>,则直接返回头节点的值。<br>如果L指向的值为队列中的头节点的<b>下标</b>,则则返回头节点的值,且将头节点移除队列
例题:<br>假设一个固定大小为W的窗口,依次划过arr,返回每一次滑出状况的最大值。<br>例如:arr=[4,3,5,4,3,3,6,7], W=3<br>返回:[5,5,5,4,6,7]
实现:trainingcamp001/src/class01/Code01_SlidingWindowMaxArray.java
例题:<br>给定一个整型数组arr,和一个整数num。某个arr中的子数组sub,如果想达标,必须满足:<br>sub中最大值 - sub中最小值 <= num,<br>返回arr中达标子数组的数量。<br>
实现:trainingcamp001/src/class01/Code02_AllLessNumSubArray.java
优化总结:<br>1)数据状况是否可以优化<br>2)问题本身是否可以优化
此题:数组的范围达标,则数组内部的子数组达标<br>由此:数组范围和滑动窗口建立的单调性映射
问题求解达标子数组的个数。<br>可以转化为求解以0位置开始的达标子数组个数,加上以1位置开始的达标子数组个数,以此类推。<br>由此:建立了问题本身的单调性和滑动窗口的轨迹的单调性联系。
例题:单调栈<br><br>
实现:trainingcamp001/src/class01/Code03_MonotonousStack.java<br>
ChildTopic
例题:<br>给定一个只包含<b>正整数</b>的数组arr,arr中任何一个子数组sub,一定都可以算出(sub累加和)*(sub中的最小值)是什么,那么所有子数组中,这个值最大是多少?
实现:trainingcamp001/src/class01/Code04_AllTimesMinToMax.java<br>
建立一个以0开始的累加和数组:<br>i位置表示0~i的累加和
题目中的“正整数”建立了累加和的单调性映射
利用单调栈
0 条评论
下一页