算法学习资源
93k 算法小抄.leetcode https://github.com/labuladong/fucking-algorithm
算法可视化https://visualgo.net/zh
coursera上受世界好评的网课算法 https://www.coursera.org/learn/algorithms-part1/home/welcome
https://www.cs.usfca.edu/~galles/visualization/
数据类型是什么】数据类型,指的是一组值和一组对这些值的操作的集合。
数据结构的应用:数据库、操作系统、压缩文件
线性表,概述】线性表是一种数据结构。元素有前后关系。具有相同类型的数据元素的有限序列。
线性表的存储结构】:1.顺序存储结构(即数组)2.链式存储结构(即链表)。线性表的相关的操作要基于这两种存储方式。
数组和链表的特点和优缺点】
顺序表,概述】用一组连续的内存单元依次存放线性表的数据元素,所以可用数组,但是每一个单元的数据类型可以是基本数据类型,也可以是自己创建的。由于要对数组进行添加或删除操作,所以还需要变量,存储当前顺序表的基本状态信息。
什么时候更适合顺序而不是链式
基本操作算法:创建 销毁 置空 判空 求表长度 返回第i个某个元素 查某个元素的序号 插入 (头,尾,中间) 删除(头,尾,中间)
链表,概述】线性表的链式存储结构(即链表)。链表基本结构实现:数据域+指针域。
线性表的三种链式存储结构】:1.线性链表(即单链表) 2.循环链表 3.双向链表
线性链表(即单链表)的基本概念、结构、
线性链表(即单链表)的基本操作方法】:1.顺序建立线性链表 2.逆序建立线性链表 3.遍历操作 4.已知位序返回节点值 5.已知值返回指针 6.插入节点 7.删除节点
循环链表的基本概念:循环链表是将线性链表的最后一个结点的指针指向链表头结点的一种链式存储结构 。
循环链表的基本操作方法:判空、定位去第i个节点、插入节点、删除节点
何时用循环链表/ 循环链表的应用】 资料)循环链表可以用来解决约瑟夫环问题
双向链表的基本概念】:双向链表中,每个结点有两个指针域,一个指向直接后继元素结点,另一个指向直接前趋元素结点。
双向链表的基本操作方法:插入节点、删除节点
资料)双链表是最常用的一种,因为它提供了在常数时间内的 addAtHead 和 addAtTail 操作,并且优化的插入和删除。
链表,杂】
链表操作:在链表固定位置插入节点(借助哑节点)。
什么时候更适合用链式而不是顺序。
串
存储
不同的存储方式,有不同的具体的实现“基本操作”的方式
定长顺序存储
即用定长数组
堆分配顺序存储
即程序执行过程动态分配而得(malloc和free)
快链存储
模式匹配(子串定位)算法
BF简单匹配算法
基本思想:
存在问题:
KMP算法
栈,概述】一种线性结构,是一种限定仅在表尾进行插入或删除操作的线性表,表尾即栈顶,表头即栈底,后进先出。
栈,基本操作】
创建栈、摧毁栈、清空栈、判断栈空、访问栈顶元素、入栈、出栈、遍历栈元素
栈,存储结构】
顺序栈(即用数组实现栈)和链栈(即用单链表实现栈)
顺序栈(即用数组实现栈):mine:顺序栈,其实是靠一个自定义数组类来实现的,顺序栈的特性操作的底层是调用一个自实现的数组类,eg:栈的pop(),内部实现是调用一个自实现的数组类的removeLast()方法。
顺序栈的基本认识
顺序栈的基本操作
栈顶指针top:指示栈顶元素的下一个位置
栈底指针base指针始终指向栈底的位置
链栈(即用单链表实现栈)
链栈的基本认识:
链栈的基本操作
什么时候考虑使用栈】
当前节点的解依赖后驱节点。
栈的应用】
数的进制转换、函数调用、解析?计算表达式。括号匹配、反转字符串、
应用:数制转换(实现),括号匹配检验,行编辑程序,迷宫求解,表达式求值,八皇后问题,函数调用,递归调用
单调栈
https://blog.csdn.net/lucky52529/article/details/89155694
leecode 739、496、503
队列,概述】一种线性结构,允许删除(出队)的一端称为队头,允许插入(入队)的一端称为队尾。队列中元素的个数称为队列的长度。先进先出
mine:数据结构-----队列
队列的基本概念:一种线性结构,先进后出。。。。
队列的基本操作:创建队列、销毁队列、清空队列、队列判空、队列元素个数、访问队头元素、入队、出队、遍历队列
顺序队列,用数组实现: mine:顺序队列,其实是靠一个自定义数组类来实现的,顺序队列特性操作的底层是调用一个自实现的数组类,
顺序队列的基本概念:
顺序队列基本操作的实现:
存储结构定义
顺序队列进阶版:循环队列。
循环队列,是什么/原理
循环队列,判断空和判断满
循环队列的基本操作的实现。
如何实现入队、出队、队空、满的判断
(用单链表实现):
链队列的基本概念:
链队列基本操作的实现:
队头指针指向链表的头结点,队尾指针指向尾结点
优先队列,概述】优先队列中元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除,优先队列具有最高级先出的行为特征。优先队列通常采用堆数据结构来实现。
优先队列操作:放入元素、删除最元素
优先队列的应用dai:
LC.239.滑动窗口最大值
单调队列
有某种单调性的队列
如果维护区间最小值,那么维护的队列就是单调递增的。这就是为什么叫单调队列。
单调递增队列:保证队列头元素一定是当前队列的最小值,用于维护区间的最小值。
单调递减队列:保证队列头元素一定是当前队列的最大值,用于维护区间的最大值。
不断地向缓存数组里读入元素,也不时地去掉最老的元素,不定期的询问当前缓存数组里的最小的元素。
用单调队列来解决问题,一般都是需要得到当前的某个范围内的最小值或最大值。
单调队列是一种主要用于解决滑动窗口类问题的数据结构,即,在长度为 [公式] 的序列中,求每个长度为 [公式] 的区间的区间最值。
时间复杂度】
单调队列的操作】
单调队列应用场景/利用单调队列求解的常见题型】
举个例子:有 7 6 8 12 9 10 3 七个数字,现在让你找出范围( i-4,i )即窗口为5, 的最小值。
https://blog.csdn.net/LJD201724114126/article/details/80663855
https://www.jianshu.com/p/e59d51e1eef5
数据结构视频14-4之后几节dai看。
哈希函数的作用:键作为哈希函数的输入,哈希函数输出一个值作为数组的索引,将值存在索引对应的数组中。哈希时间复杂度就是O(1)。哈希函数的目的,就是为了构造一个将输入映射为一个可作为数组索引的数字,所以希望这个哈希函数能使得输出结果均匀、尽量少冲突。如何设计出哈希函数是很重要的。
处理散列冲突的方法:开放地址法;再散列函数法;链地址法;建立公共溢出区。
树,概述】(自己理解就行)非线性,一对多,元素间层次、分支
树的基本术语】度、层数、树的高度(深度)、…
层数:根结点的层数为1,其它结点的层数为从根结点到该结点所经过的分支数目再加1。
树的高度(深度):树中结点所处的最大层数称为树的高度,如空树的高度为0,只有一个根结点的树高度为1。
二叉树,概述】每个节点至多2颗子树,子树有左右之分且次序不能颠倒,有一颗子树也要分左右
二叉树,性质】
第i层最多有…个结点
叶子结点数=度为2的节点数+1
深度为k的二叉树最多有…个结点
具有n个结点的完全二叉树的深度k为└ log2n ┘+1
完全二叉树:节点数和叶子结点树的关系
特殊形态的二叉树】满二叉树、完全二叉树、正则二叉树。
满二叉树】
满二叉树,定义】如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。
满二叉树,若是h层,则一共有(2h -1)个节点,即大约是2h个节点;最后一层有2(h-1)个节点,可以看出最后一层的节点数大致等于前面所有层节点之和,此性质是线段树的基础。
注意区分满二叉树和完全二叉树
完全二叉树】
正则二叉树】
二叉树的存储结构】:顺序存储结构、链式存储结构(二叉链表、三叉链表)
二叉树的遍历】遍历方法(先序、中序、后序、层次)。3种遍历的递归和非递归算法。
先序递归遍历二叉树算法】
//先序递归遍历二叉树
void preOrder(TreeNode root){
if(root==null)
return null;
print(root.val)
preOrder(root.left)
preOrder(root.right)
}
二叉树的遍历方式】
深度优先遍历:先往深走,遇到叶子节点再往回走。
广度优先遍历:一层一层的去遍历。
基于二叉树遍历的算法】二叉树建立、复制、求深度、计算结点总数、叶子总数。
二叉树的序列化与反序列化,算法说明】这个算法源于LC.297.二叉树的序列化与反序列化,题解。可以根据字符串生成一个二叉树。
/*
"1,2,3,None,None,4,None,None,5,None,None,"
1
2 5
3 4
*/
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Codec {
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode deserialize(String data) {
String[] dataArray = data.split(",");
List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
return rdeserialize(dataList);
}
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public TreeNode rdeserialize(List<String> dataList) {
if (dataList.get(0).equals("None")) {
dataList.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
dataList.remove(0);
root.left = rdeserialize(dataList);
root.right = rdeserialize(dataList);
return root;
}
}
线索二叉树,概述】用二叉链表储存形式,其中的空指针改为前驱或后继(此时指针称为线索),前驱或后继的设立也是首先要声明先序/中序/后序
线索二叉树,具体实现(线索化过程)】先有二叉树(且此二叉树结点:再设置两个标志位,指明指针是孩子or前驱后继。)再对二叉树线索化(自己想。在遍历时改空指针域为前驱或后继)
遍历算法
二叉搜索树Binary Search Tree
二叉查找树 (Binary Search Tree),(又:二叉搜索树,二叉排序树)
public class BST<E extends Comparable<E>> {
private class Node {
public E e;
public Node left, right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
private Node root;
private int size;
public BST(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
//方法1-1:
// 向二分搜索树中添加新的元素e
public void add(E e){
if(root == null){
root = new Node(e);
size ++;
}
else
add(root, e);
}
// 方法1-2
//向以node为根的二分搜索树中插入元素e,递归算法
private void add(Node node, E e){
if(e.equals(node.e))
return;
else if(e.compareTo(node.e) < 0 && node.left == null){
node.left = new Node(e);
size ++;
return;
}
else if(e.compareTo(node.e) > 0 && node.right == null){
node.right = new Node(e);
size ++;
return;
}
if(e.compareTo(node.e) < 0)
add(node.left, e);
else //e.compareTo(node.e) > 0
add(node.right, e);
}
}
//方法2-1:
// 向二分搜索树中添加新的元素e
public void add(E e){
root = add(root, e);
}
//方法2-2:
// 向以node为根的二分搜索树中插入元素e,递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, E e){
if(node == null){
size ++;
return new Node(e);
}
if(e.compareTo(node.e) < 0)
node.left = add(node.left, e);
else if(e.compareTo(node.e) > 0)
node.right = add(node.right, e);
return node;
}
// 看二分搜索树中是否包含元素e
public boolean contains(E e){
return contains(root, e);
}
// 看以node为根的二分搜索树中是否包含元素e, 递归算法
private boolean contains(Node node, E e){
if(node == null)
return false;
if(e.compareTo(node.e) == 0)
return true;
else if(e.compareTo(node.e) < 0)
return contains(node.left, e);
else // e.compareTo(node.e) > 0
return contains(node.right, e);
}
向BST中插入元素的算法】
用BST实现Set】
用BST实现Map】
BST的中序遍历】
删除BST中的最小值元素、最大值元素、任意一个元素,算法】
基本概念
如何构造
哈夫曼编码方法、算法实现
见《算法导论》
B树,概述】B树又称多路平衡查找树,B树是从二叉搜索树的基础上演变而来的,其还要保证是平衡的。
每个节点最多可以有K个孩子,K被称为B树的阶,K的值取决于磁盘页的大小,B树中所有结点的孩子结点数的最大值称为B树的阶。
B树常用来组织和维护外村文件的非常有效的数据结构,B树中每一个节点中存储的值往往是
B树的查找规则】
在B树中查找给定关键字的方法是,首先把根结点取来,在根结点所包含的关键字K1,…,Kn查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;否则,一定可以确定要查找的关键字在Ki与Ki+1之间,Pi为指向子树根节点的指针,此时取指针Pi所指的结点继续查找,直至找到,或指针Pi为空时查找失败。
B树的建立规则/B树的性质】
一棵K阶B树(balanced tree of order )是一棵平衡的K路搜索树。它或者是空树,或者是满足下列性质的树:
1、根节点若不是叶子节点,根结点至少有两个子节点;
2.树中每个节点最多有(K-1)个关键字,每个节点最多有K个子节点
3.除根节点外,每个非叶子节点至少有 (K/2)向上取整个子节点
4.每个节点中的元素,从小到大排列
5.所有的叶子结点都位于同一层。
6.所有外部节点都在同一层上,外部节点就是失败的节点,它不含任何信息,是虚设
7.一棵B树中共有N个关键字,则外部节点个数为N+1个
向B树插入关键字;在B树中删除关键字’注意过程中的分裂页和合并页的过程】
B+树是B树的一种变形形式。
B+树中的关键字叶子层节点中存放了所有的关键字。
很多数据库索引就是B+树。
一棵K阶的B+树定义如下:
1.根节点没有子树,或者根节点至少有两颗子树。
2.每个结点至多有K个子树;
(3)除根结点外,每个结点至少有(K/2)个向上取整个子树,
(4)有N个关键字的节点就有N个子树。
(5)所有叶子结点包含全部关键字及指向相应记录的指针,叶子节点中的关键字按照关键字大小顺序排列,
(6)所有分支结点(可看成是索引的索引)中仅包含它的各个子结点(即下级索引的索引块)中最大关键字及指向子结点的指针。
7)B+树可以通过树来查找,B+树也可以直接在串起来的叶子节点层中顺序查找。
B+树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。
B+Tree与B树对比】
在B树中,非叶子节点和叶子节点都会存放关键字;B+树非叶子节点仅仅起到索引数据作用,具体的关键字都是在叶子节点存放的;
B树的外层节点为空;B+树中将外层节点用来存放关键字指向的记录,叶子节点形成一个单向链表。
堆,概述】两种堆:最大堆,最小堆。最大堆(最小堆),只知道最大值(最小值),其他节点是第几大不知道。以最大堆为例,每一个节点都≥它的左右子节点。最大的节点是根节点。每个子树,也是一个最大堆。堆的高度定义为树的高度, 为lg n n为节点个数
最大堆,最小堆,应用】
堆,物理存储上是:数组(下标从1开始),逻辑上:完全二叉树。我们在一个完全二叉树上讨论和分析,但实际操作是在数组中的。数组下标也可以从0开始,则节点和父、子节点之间的公式会变化。
对应关系:将数组中的元素,从第一个开始依次,从上到下,从左到右,放入构成完全二叉树二叉树
给定某个节点的下标i,可以求出父节点、左右孩子节点的下标。有公式。
Trie 树,概述】Trie树,又叫字典树、前缀树(Prefix Tree)、单词查找树 或 键树,是一种多叉树结构。
线段树,概述】
使用数组实现线段树,在储存上,线段树在实际储存实现上就是满二叉树,但不一定会利用所有空间。
如果区间有n个元素,若用数组实现线段树,需要需要4n的空间
由于线段树不考虑添加元素,即区间固定,使用4n的静态空间即可
线段树是平衡二叉树
为什么用线段树/什么时候使用线段树】
什么时候考虑线段树:对于给定区间:更新:更新区间中一个元素或者一个区间的值,查询一个区间[i, j]的最大值,最小值,或者区间数字和
。区间范围/规模是固定的,不涉及添加或者删除元素问题,但是区间中的元素可能变化。
区间查询问题(比如,想查询出一组数字中某个区间的最大值、最小值、区间数字和)。区间染色问题。
2017年注册用户中消费最高的用户?消费最少的用户?学习时间最长的用户?-----题目分析:2017年注册的用户,它的这些属性一直在变化,不是静态的,而是动态的,所以考虑使用线段树。
跳表概述】--------dai看极客时间17讲
可以支持快速的插入、删除、查找操作,写起来也不复杂,甚至可以替代红黑树(Red-black tree)。
Redis 中的有序集合(Sorted Set)就是用跳表来实现的。
Redis 为什么会选择用跳表来实现有序集合呢?
如何理解“跳表”?
cache】----并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。孩子指向父亲。并查集的使用场景:网络间节点间的连接状态。并查集对存储的数据,主要支持两个动作:合并集合。判断两个数据是否属于同一个集合。
见数据结构与算法-----图
算法入门
算法,特征】五个特征
杂】算法描述方式:自然语言,流程图,伪代码,具体代码
设计算法思路】
研究一个新问题时,最好的方法是先实现一个你能想到的最简单的程序,当它成为瓶颈的时候再继续改进它。
mine:算法的设计前提要清楚物理结构,因为算法中会用到
如何判断一个算法是否正确】
不用花括号,缩进
“and"代表&& “or"代表”||”
变量不用声明定义,直接用
for i=1 to 边界
数组:访问数组“数组名[下标]” 。数组范围:A[1…j]
可以写注释
看极客时间专栏
YW:for中嵌套while语句,时间复杂度?
算法分析,概述】时间复杂度和空间复杂度。
时间复杂度,概述】表示某个算法的运行时间的趋势。假定一条语句执行时间相同,时间复杂度取决于执行语句的次数,常以一个函数来表示。计算时间复杂度,即估计算法的操作单元数量,每个单元运行的时间都是相同的。计算出来以后,算法运行时间是从其增速的角度度量的。增速大,代表其所需时间长。算法决定了程序运行时间的增长量级,不同计算机上只是系数不同。
程序运行时间T(),影响因素】执行每条语句的耗时(硬件)。执行每条语句的频次/频率 (mine:是不是就是 增长数量级)
计算大O的准则】抛弃前导常数&抛弃低阶项
渐进确界:大Cita】A=大Cita(B) 即意为可以找到常数c和d,使得第当N规模足够大,总有cB<A<dB
渐进上界 :大O】A=O(B) 即意为可以找到一个常数c,使得当N规模足够大,总有A<cB
渐进下界:大Ω】A=O(B) 即意为可以找到一个常数c,使得当N规模足够大,总有cB<A
渐进记号的性质】传递性、自反性、对称性:A函数是B的渐进确界,B也是A的渐进确界。
递归式型的时间复杂度
排序算法代码实现
排序算法,概述】我们关注的主要对象是重新排列数组元素的算法,其中每个元素都有一个主键。排序算法的目标就是将所有元素的主键按照某种方式排列(通常是按照大小或是字母顺序)。排序后索引较大的主键大于等于索引较小的主键。排序算法可以分为两类:除了函数调用所需的栈和固定数目的实例变量之外无需额外内存的原地排序算法,以及需要额外内存空间来存储另一份数组副本的其他排序算法。排序算法时间复杂度最小是O(NlgN)
对于部分有序和小规模的数组应该选择插入排序。
排序算法的时间复杂度】
排序算法是否基于比较】
如何分析、评价一个“排序算法”?】
要从哪几个方面入手呢?排序算法的执行效率。排序算法的内存消耗(即讨论空间复杂度,是不是原地排序)
排序算法的稳定性。
排序算法的执行效率—1. 最好情况、最坏情况、平均情况时间复杂度
2. 时间复杂度的系数、常数 、低阶
3. 比较次数和交换(或移动)次数
原地的排序算法】
原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
稳定的排序算法】
稳定排序算法可以保持数字相同的两个对象,在排序之后的前后顺序不变。
线性排序算法的时间复杂度比较低,适用场景比较特殊。如果对小规模数据进行排序,可以选择时间复杂度是 O(n2) 的算法;如果对大规模数据进行排序,时间复杂度是 O(nlogn) 的算法更加高效。所以,为了兼顾任意规模数据的排序,一般都会首选时间复杂度是 O(nlogn) 的排序算法来实现排序函数。
排序算法的稳定性介绍、什么时候应该注意排序算法的稳定性 选择排序,概述】选择排序(Selection sort)是一种简单直观的排序算法,它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。这种方法叫做选择排序,因为它在不断地选择剩余元素之中的最小(或最大)者。 选择排序算法,特点】资料)运行时间和输入无关&&移动数据是其他排序算法中最少的。 选择排序种类】简单选择排序。树形选择排序。堆排序。 插入排序】插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序。 插入排序的分类】基础插入排序;折半插入;2路插入;表插入;希尔排序 归并排序,知识点概述】归并排序的思想。归并排序是否稳定。写出归并排序的算法。归并排序的空间复杂度和时间复杂度。 归并排序的思想】要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序,递归调用发生在处理整个数组之前。 归并排序是稳定的】归并排序是稳定的。 归并排序算法的实现方式】方法1.将两个不同的有序数组归并到第三个新创建的数组中。存在的缺陷:当数组较大时,创建新数组浪费存储空间。方法2.抽象化的原地的归并排序算法。 方法3. 自顶向下的归并排序。这是一种运用了分治思想的递归式排序。方法四:自底向上的归并排序daiBD。 归并排序伪代码】 自底向上的归并排序】 归并排序,时间复杂度】 归并排序,空间复杂度】 递归形式实现的归并排序的时间复杂度分析】 极客时间第12讲 快速排序,概述】 快速排序的实现算法】主要思想:切分方法+递归思想。快速排序递归地将子数组a[lo…hi]排序,先用切分方法partition()方法将a[j]放到一个合适位置,然后再用递归调用将其他位置的元素排序。 切分方法partition()方法dai】 快速排序算法QUICKSORT,PARTITION算法。 快速排序,时间复杂度】将长度为N的数组排序,平均时间复杂度是O(NlogN),在极端情况下平均复杂度是0(N2)。 快速排序,空间复杂度】 快速排序需要注意的点】 快速排序随机版本 算法 快速排序,缺点】它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能,已经有无数例子显示许多种错误都能致使它在实际中的性能只有平方级别。快速排序是不稳定的。 一般形式的快速排序(??)的缺点】 改进的快速排序dai】 将快速排序和归并排序对比来学习】 堆排序,相关知识点陈列】cache-----堆、最大最小堆基本概念。保持堆特性(重点)。建堆 (重点)。堆排序算法。优先级队列。 堆排序,概述】 堆排序,复杂度分析】 根据堆排序的思想,自顶向下推出我们需要考虑的两个问题 对某些节点操作,破坏了原堆的性质,为了恢复原堆的性质,该如何调整】方法原来是一个正常的特性堆,其中一个节点变化了,和子节点调整,一次调整后可能还需要继续。递归思路。 堆排序,应用:优先级队列】 最大优先级队列上的操作,每种操作如何设计】 优先级队列,用途:】操作系统中的作业调度 计数排序(Counting sort) 不涉及元素之间的比较操作。 技术排序的使用场景】 计数排序,时间复杂度】 都不涉及元素之间的比较操作。 假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢?------专栏13讲 基数排序,时间复杂度】 桶排序,概述】 桶排序的时间复杂度】 桶排序不涉及元素之间的比较操作。 桶排序对要排序数据的要求是非常苛刻的。 比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。这个时候该怎么办呢?如何借助桶排序的处理思想来解决这个问题。----------看专栏13讲 二分查找,概述】首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。1.必须采用顺序存储结构。2.必须按关键字大小有序排列。 二分查找的算法实现】递归实现。迭代实现。 二分查找的 时间复杂度】 二分查找的 时间复杂度为 O (log n) 二分查找应用场景的局限性】 二分查找的变体问题】–16讲 数据结构与算法—图 递归,知识点概述】什么时候考虑用递归、哪些类型的题考虑使用递归。如何写递归算法、递归算法的要素----base case和recurrence relation。 什么时候考虑用递归】 递归,概述】 递归的要求】有基准情形&向基准情形不断推进(mine 这个基准情形即问题规模小到足够时可直接求) 递归算法的时间复杂度】 有哪些可以降低递归算法的时间复杂度的方法】 用归纳法来证明递归算法的正确性】 递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。—卡尔 如何设计递归算法】 递归树有啥用】 调试递归代码的方法dai)】:mine:在原递归代码中,添加一些辅助代码,将递归函数的执行过程打印出来,方便观察。 资料“帅地推荐数据结构算法视频中5-6 递归典型题】:斐波那契数列、阶乘(会)、倒序输出正整数、汉诺塔、排列组合字符串、快速排序、求n个自然数的最大公约数与最小公倍数、反转链表、倒序输出字符串、leetcode 24、leetcode 203、用递归函数和栈操作逆序一个栈(from左)、leetcode—98—判断搜索二叉树。 递归算法的优点和缺点】 mine递归题中的感悟】:递归函数返回值void是不是一般都是在函数内打印的。除了边界条件一定要return,在自己调用自己的时候,用不用return,用或不用。不用return,不打印,可能得用全局变量,最后取出值 边界条件的return,return什么。 《计算之魂》—第6章:分治。 分治法,概述】分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧 见动态规划 贪心算法,概述】是在当前步骤上选择最好的方案,即是一种局部最优的选择,并希望这能导致一个全局最优, 比动态规划(需要将所有选择遍历)更加简单,但有时候不可能导致全局最优。有些贪心算法产生全局最优,如MST,单源最短路径 什么时候想到用贪心算法?】 贪心法和动态规划的比较】 《代码随想录》---------------------------------------------------------- 贪心算法一般分为如下四步: 其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。 一些同学可能感觉,我在讲贪心系列的时候,题目和题目之间貌似没有什么联系?是真的就是没什么联系,因为贪心无套路!没有个整体的贪心框架解决一些列问题,只能是接触各种类型的题目锻炼自己的贪心思维! ------------------------------------------《代码随想录》 回溯,知识点概述】回溯算法,基本介绍。回溯法解题,算法框架。什么时候想到用回溯法。回溯算法的应用/适合求解哪些类型的题。 参考学习资料:https://zhuanlan.zhihu.com/p/302415065 回溯算法,基本介绍】回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。回溯法,一个解的各个部分是逐步生成的,当发现当前生成的某部分不满足约束条件时,就放弃该步所做的工作,退到上一步进行新的尝试,而满足回溯条件的某个状态的点称为“回溯点”。 极客时间--------”回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。“ 回溯法解题,算法框架】 (2) 确定易于搜索的解空间结构;在定义了问题的解空间后,还需要将解空间有效地组织起来,使得回溯法能方便地搜索整个解空间,通常将解空间组织成树或图的形式。 (3) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。确定了问题的解空间结构后,回溯法将从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。开始结点成为活结点,同时也成为扩展结点。在当前的扩展结点处,向纵深方向搜索并移至一个新结点,这个新结点就成为一个新的活结点,并成为当前的扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使其成为当前的扩展结点。回溯法以上述工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。 递归函数实现回溯法,递归函数模板如下: https://baike.baidu.com/item/%E5%9B%9E%E6%BA%AF%E6%B3%95/86074 迭代的方式也可实现回溯算法,迭代回溯算法的模板如下: https://baike.baidu.com/item/%E5%9B%9E%E6%BA%AF%E6%B3%95/86074 回溯法的模板】 回溯算法,能解决的题目&&应用】 网友-------“回溯法,一般可以解决如下几种问题: 八皇后问题】 回溯法解决0-1背包】 -----0-1 背包问题,问题描述】一个背包,背包总的承载重量是 Wkg。现在我们有 n 个物品,每个物品的重量不等,并且不可分割。我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大? 正则表达式中的回溯】 极客时间《数据结构与算法之美》 回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。 回溯算法非常适合用递归代码实现 在实现回溯算法的过程中,剪枝操作是提高回溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率。 用回溯法解决0-1 背包】 用回溯法解决正则表达式匹配】 递归式的解法】代换法、 递归树方法、主方法 哈希算法又叫“Hash 算法”或者“散列算法 将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值。 哈希Hash算法的应用】安全加密、数据校验、唯一标识、散列函数、负载均衡、数据分片、分布式存储、负载均衡。----极客时间21、22讲 什么才是好的散列函数呢】 如何选择冲突解决方法?】 “其中20 个最常用的、最基础数据结构与算法:这里面有 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。”-----------极客时间专栏
“排序算法”"什么意思】待排序的记录序列中可能存在两个或两个以上关键字相等的记录。排序前的序列中Ri领先于Rj(即i冒泡排序
选择排序
插入排序
归并排序
归并排序是算法设计中分治思想的典型应用。
自底向上的归并排序比较适合用链表组织的数据。
归并排序的时间复杂度任何情况下都是 O(nlogn),平均时间复杂度为O(NlogN)。最坏时间复杂度为O(NlogN)。
"对于含有以任意概率分布的重复元素的输入,归并排序无法保证最佳性能。
额外空间复杂度O(N).
需要额外空间复杂度O(N),这是归并排序的缺点。快速排序
快速排序是一种分治的排序算法。
它将一个数组分成两个子数组,将两部分独立地排序,当两个子数组都有序时整个数组也就自然有序了。
快速排序流行的原因是它实现简单、适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。
一般策略是先随意地取a[lo]作为切分元素,即那个将会被排定的元素,然后我们从数组的左端开始向右扫描直到找到一个大于等于它的元素,再从数组的右端开始向左扫描直到找到一个小于等于它的元素。这两个元素显然是没有排定的,因此我们交换它们的位置。如此继续,我们就可以保证左指针i的左侧元素都不大于切分元素,右指针j的右侧元素都不小于切分元素。当两个指针相遇时,我们只需要将切分元素a[lo]和左子数组最右侧的元素(a[j])交换然后返回j即可。
最坏,最佳,平衡。
快速排序是原地排序(只需要一个很小的辅助栈)
额外空间复杂度是O(logN)。
在切分不平衡时这个程序可能会极为低效。例如,如果第一次从最小的元素切分,第二次从第二小的元素切分,如此这般,每次调用只会移除一个元素。这会导致一个大子数组需要切分很多次。在快速排序前将数组随机排序,使产生糟糕的切分的可能性降到极低,堆排序
注“特性堆”代指最大堆/最小堆。由于最大堆or最小堆,其根节点为最大(最小)值,将根节点拿走,再对剩下的节点重新进行建堆,反复这样,就会依次得到第二大、第三大,实现排序。最后期望要达到的目的就是,一个数组里按从大到小/从小到大依次放置。 刚开始时数组中是随意顺序的,我们要建堆(从第└n/2 ┘个元素至顶),(比如建成一个最大堆来辅助)。建好后,根节点的值是所有节点中最大的,刚好它此时下标为1,即数组的第一个位置,将它和树中最后一个节点(数组最后一个元素)换位置,并输出它(剔除出树),进行重新建立最大堆(),重复以上行为。
时间复杂度】恒为O(nlogn),最初元素怎样排列对时间复杂度没影响
空间复杂度】O(1)
堆排序是不稳定的排序
如何由无序序列变成一个特性堆(建堆)】从无序序列的第 n/2 个元素B(这个节点之前是非叶子节点)开始,(以大顶堆为例)若B的孩子和B不满足目的堆关系,则与它的子节点中更大的交换(有可能不止换一次,要检查其于此时子节点的关系),重复以上步骤,直至第一个节点。树的叶子节点,本身就已经是堆了。从非叶子节点开始调整
输出堆顶元素后,如何调整剩下元素成为新堆】将最后一个元素A放到根节点,(以大根堆为例)比较此时其左右孩子的大小,将更大者和根节点交换,重复以上过程,知道A没有子节点为止。
优先级队列,概述 定义】是一种用来维护由一组选元素构成的集合S的数据结构,这一组元素中的每一个都有一个关键字key
将元素x插入集合S:先插入一个-∞的数到树的最后(即在数组的末尾),这时还是保持最大堆。我们再用“将元素关键字增加到k”的操作,辅助实现
返回S中具有最大关键字元素
去掉并返回S中的具有最大关键字的元素
将元素x的关键字的值增加到k:x和它的子树一定还是一个最大堆,但是可能x和父节点之间不符合最大堆的条件,所以可能需要涉及x节点与父节点的调换位置。可能需要重复这个过程计数排序
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
时间复杂度是 O(n)基数排序
时间复杂度是 O(n) 的排序算法桶排序
桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
时间复杂度是 O(n) 的排序算法,
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
桶排序比较适合用在外部排序中。查找,相关算法
二分查找
图,相关算法
递归
(1)数据的定义是按递归定义的,如斐波那契数列。
(2)当问题和子问题具有递推关系,比如杨辉三角、计算阶乘
(3)数据的结构形式是按递归定义的。链表、树、图。
(4)反向性问题,比如取反。
最根本的还是要抓住问题本身是否可以通过层层拆解到最小粒度来得解。
递推关系、边界情况
调试递归函数时编写辅助函数打印关键值配合缩进,直观地观察递归函数执行情况。
递归函数参数,可多个参数。
递归函数返回时,注意此时参数值是啥,别被转换式影响。返回时,一定是它当时进去的状态,认清它当时进去的状态。
递归函数内可以打印也可以不打印
递归返回时,清楚每层当前值,刚出来就带着这个值去执行当前层的剩余部分,别又进去了
return语句并未调用此递归函数(leetcode 50)
mine:瞬感对于递归的理解:一直到底,然后再从底开始往回执行。
分治法常会用到递归
“典例--------如何仅用递归函数和栈操作逆序一个栈(from左)” 方法1:并不是我以为的,递归走到底,然后对底的数据倒着往回进行处理,这道题是走到底,但在底部处理的是顶部的数据。 方法2:就是走到底,从底部往回处理数据
递归疑问:“理解递归函数最重要的就是画出递归树”?分治
动态规划
贪心算法
都用到了最优子结构思想
贪心法如果能做到全局最优,则比动态规划法效率更高
动态规划能解决的,贪心法不一定能解决
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法并没有固定的套路。唯一的难点就是如何通过局部最优,推出整体最优。没有固定策略或者套路能看出局部最优是否能推出整体最优,要靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。如何验证是否可以选用贪心算法:如果不能举出反例,应该就可以。
手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心的思想。贪心这种思路,很多时候就是通过常识推导出来的。
将问题分解为若干个子问题
找出适合的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解回溯
回溯算法,基本介绍
什么时候想到用回溯法
回溯法解题,算法框架
运用回溯法解题的关键要素有以下三点:
(1) 针对给定的问题,定义问题的解空间;1.应用回溯法求解问题时,首先应明确定义问题的解空间,该解空间应至少包含问题的一个最优解。回溯算法的应用/适合求解哪些类型的题
正则表达式匹配、编译原理中的语法分析。数独、八皇后、0-1 背包、图的着色、旅行商问题、全排列等等。
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等”
//八皇后问题。代码源自极客时间专栏。
int[] result = new int[8];// 全局或成员变量, 下标表示行, 值表示 queen 存储在哪一列
public void cal8queens(int row) { // 调用方式:cal8queens(0);
if (row == 8) { // 8 个棋子都放置好了,打印结果
printQueens(result);
return; // 8 行棋子都放好了,已经没法再往下递归了,所以就 return
}
for (int column = 0; column < 8; ++column) { // 每一行都有 8 中放法
if (isOk(row, column)) { // 有些放法不满足要求
result[row] = column; // 第 row 行的棋子放到了 column 列
cal8queens(row+1); // 考察下一行
}
}
}
private boolean isOk(int row, int column) {// 判断 row 行 column 列放置是否合适
int leftup = column - 1, rightup = column + 1;
for (int i = row-1; i >= 0; --i) { // 逐行往上考察每一行
if (result[i] == column) return false; // 第 i 行的 column 列有棋子吗?
if (leftup >= 0) { // 考察左上对角线:第 i 行 leftup 列有棋子吗?
if (result[i] == leftup) return false;
}
if (rightup < 8) { // 考察右上对角线:第 i 行 rightup 列有棋子吗?
if (result[i] == rightup) return false;
}
--leftup; ++rightup;
}
return true;
}
private void printQueens(int[] result) { // 打印出一个二维矩阵
for (int row = 0; row < 8; ++row) {
for (int column = 0; column < 8; ++column) {
if (result[row] == column) System.out.print("Q ");
else System.out.print("* ");
}
System.out.println();
}
System.out.println();
}
/*
//回溯法解决0-1背包
”对于每个物品来说,都有两种选择,装进背包或者不装进背包。对于 n 个物品来说,总的装法就有 2^n^ 种,去掉总重量超过 Wkg 的,从剩下的装法中选择总重量最接近 Wkg 的。不过,我们如何才能不重复地穷举出这 2^n^ 种装法呢?这里就可以用回溯的方法。我们可以把物品依次排列,整个问题就分解为了 n 个阶段,每个阶段对应一个物品怎么选择。先对第一个物品进行处理,选择装进去或者不装进去,然后再递归地处理剩下的物品。描述起来很费劲,我们直接看代码,反而会更加清晰一些。
这里还稍微用到了一点搜索剪枝的技巧,就是当发现已经选择的物品的重量超过 Wkg 之后,我们就停止继续探测剩下的物品。"---极客时间
*/
public int maxW = Integer.MIN_VALUE; // 存储背包中物品总重量的最大值
// cw 表示当前已经装进去的物品的重量和;i 表示考察到哪个物品了;
// w 背包重量;items 表示每个物品的重量;n 表示物品个数
// 假设背包可承受重量 100,物品个数 10,物品重量存储在数组 a 中,那可以这样调用函数:
// f(0, 0, a, 10, 100)
public void f(int i, int cw, int[] items, int n, int w) {
if (cw == w || i == n) { // cw==w 表示装满了 ;i==n 表示已经考察完所有的物品
if (cw > maxW) maxW = cw;
return;
}
f(i+1, cw, items, n, w);
if (cw + items[i] <= w) {// 已经超过可以背包承受的重量的时候,就不要再装了
f(i+1,cw + items[i], items, n, w);
}
}
/*
正则表达式中的回溯。
"我们依次考察正则表达式中的每个字符,当是非通配符时,我们就直接跟文本的字符进行匹配,如果相同,则继续往下处理;如果不同,则回溯。
如果遇到特殊字符的时候,我们就有多种处理方式了,也就是所谓的岔路口,比如“*”有多种匹配方案,可以匹配任意个文本串中的字符,我们就先随意的选择一种匹配方案,然后继续考察剩下的字符。如果中途发现无法继续匹配下去了,我们就回到这个岔路口,重新选择一种匹配方案,然后再继续匹配剩下的字符。"---《极客时间》
*/
public class Pattern {
private boolean matched = false;
private char[] pattern; // 正则表达式
private int plen; // 正则表达式长度
public Pattern(char[] pattern, int plen) {
this.pattern = pattern;
this.plen = plen;
}
public boolean match(char[] text, int tlen) { // 文本串及长度
matched = false;
rmatch(0, 0, text, tlen);
return matched;
}
private void rmatch(int ti, int pj, char[] text, int tlen) {
if (matched) return; // 如果已经匹配了,就不要继续递归了
if (pj == plen) { // 正则表达式到结尾了
if (ti == tlen) matched = true; // 文本串也到结尾了
return;
}
if (pattern[pj] == '*') { // * 匹配任意个字符
for (int k = 0; k <= tlen-ti; ++k) {
rmatch(ti+k, pj+1, text, tlen);
}
} else if (pattern[pj] == '?') { // ? 匹配 0 个或者 1 个字符
rmatch(ti, pj+1, text, tlen);
rmatch(ti+1, pj+1, text, tlen);
} else if (ti < tlen && pattern[pj] == text[ti]) { // 纯字符匹配才行
rmatch(ti+1, pj+1, text, tlen);
}
}
}
笼统地讲,回溯算法很多时候都应用在“搜索”这类问题上。不过这里说的搜索,并不是狭义的指我们前面讲过的图的搜索算法,而是在一组可能的解中,搜索满足期望的解。算法学习,杂
哈希Hash算法
哈希Hash算法的应用
首先,散列函数的设计不能太复杂。过于复杂的散列函数,势必会消耗很多计算时间,也就间接的影响到散列表的性能。其次,散列函数生成的值要尽可能随机并且均匀分布,这样才能避免或者最小化散列冲突,而且即便出现冲突,散列到每个槽里的数据也会比较平均,不会出现某个槽内数据特别多的情况。
数据结构与算法,知识图谱