第一课 数据结构与算法总览
一维:
基础 | 数组array(string),链表linked list |
---|---|
高级 | 栈stack,队列queue,双端队列deque,集合set,映射map(hash or map),etc |
二维:
基础 | 树tree,图graph |
---|---|
高级 | 二叉搜索树binary search tree(red-black,AVL),堆heap,并查集disjoint set,字典树Trie,ect |
特殊:
位运算Bitwise,布隆过滤器BloomFilter
算法
else,switch_>branch |
---|
for,while loop_>lteration |
递归Recursion(Divide&Conquer,Backtrace) |
搜索Search:深度搜索Depth first search,广度优先搜索Breadth first search,A*,ect |
动态规划Dynamic Programming |
二分查找Binary Search |
贪心Greedy |
数学Math,几何Geometry |
第二课 学习准备与复杂度分析
Mac:iTerm2+zsh(oh my zsh)简书
Windows:Microsoft new terminal微软终端:(https:/github.com/microsoft/terminal)开源代码库
编译环境:VSCode;Java:InterlliJ;Python:Pycharm
代码风格:
- 单目运算符(Unary Operators)与它的操作数之间应紧密相接,不需要空格。
如:y = ++x; // ++ 在这里是前缀单目运算,它与x之间无空格 - 在双目、三目运算符(Binary/Ternary Operators)的左右两侧分别添加空格。
如:int a = 3 + 5; // 在双目运算符左右添加空格
int d = b++ * c–; // 虽然有单目运算符,但双目运算符两侧仍应添加空格
int e = a > 0 ? 1 : 0; // 在三目运算符左右添加空格 - 括号(包括小括号、中括号与大括号)的内侧应该紧靠操作数或其他运算符,不需要添加额外的空格。
如:int f = (a + b) * c; // 括号内侧紧靠操作数,因其他运算符添加的空格留在外侧 - // 函数形式的调用,括号前没有空格
cmd = Console.ReadLine();
// 语句结构,括号前有空格
if (cmd.Length > 0)
自顶向下的编程方式:
层次(主干)逻辑
时间复杂度Big O notation
O(1) | Constant Complexity 常数复杂度 |
---|---|
O(log n) | Logarithmic Complexity 对数复杂度 |
O (n) | Linear Complexity 线性时间复杂度 |
O(n^2) | N square Complexity 平方 |
O(n^3) | N cubic Complexity立方 |
O(2^n) | Expoential Growth指数 |
O(n!) | Factorial 阶乘 |
ster Theorem
Binary search二分查找 | O(log n) |
---|---|
Binary tree traversal二叉树的遍历 | O(n) |
Optimal sorted matrix search排好的有序矩阵二分查找 | O(n) |
Merge sort归并排序 | O(nlog n) |
遍历:前序、中序、后序的时间复杂度是O(n),n是二叉树的节点总数。遍历时每个结点都会访问,且只访问一次。
图的遍历:时间复杂度是O(n),n是图里的节点总数
搜索算法:DFS深度优先、BFS广度优先 时间复杂度是O(n),n是搜索空间里面的节点总数。
VSCode快捷键
Alt+↑ / ↓ | 移动当前行 上/下 |
---|---|
Shift+Alt+↑ / ↓ | 复制行 上/下 |
Ctrl+Shift+K | 删除行 |
Ctrl+Enter | 插入行(当前行下方) |
Crtl+Shift+Enter | 插入行(当前行上方) |
Home/End | 行首/行尾 |
Ctrl+End | 到文件末尾 |
Ctrl+Home | 到文件开始 |
Alt+Z | 自动换行 启用/禁用 |
F8 | 跳转至指下一个错误或警告 |
Shift+F8 | 跳转之前一个错误或警告 |
Ctrl+F | 查找 |
Ctrl+H | 替换 |
F2 | 重命名 |
Ctrl+N | 新建文件 |
Ctrl+O | 打开文件 |
Ctrl+S | 保存 |
Ctrl+Shift+S | 另存为 |
Ctrl+F4 | 关闭 |
Ctrl+Tab | 打开下一个 |
Ctrl+Shift+Tab | 打开前一个 |
Ctrl+Shift+D | 显示调试 |
F9 | 切换断点 |
F5 | 开始/继续 |
Shift+F5 | 停止 |
F11/Shift+F11 | 跟进/跟出 |
F10 | 跟出 |
Ctrl+↑ / ↓ | 上/下滚动 |
Shift+PgUp/PgDn | 上/下滚动页面 |
Ctrl+Home/End | 顶端/底端滚动页面 |
windows10快捷键
Win+I | 打开设置 |
---|---|
Win+E | 打开文件管理器 |
Win+A | 打开操作中心 |
Win+S | 打开搜索 |
Win+K | 打开连接设备 |
Win+V | 打开云剪贴板 |
Win+D | 显示桌面 |
Win+L | 锁定桌面 |
Win+Shift+S | 召唤windows截图工具 |
Win+; | 调出Emoji表情 |
快捷键也太神奇太方便了吧 哈哈 要勤加练习哟!😁😁
第三课
3.1 数组、链表、跳表的基本实现和特性
Array数组,在内存管理器申请地址。
数组的时间复杂度
prepend | O(1) |
---|---|
append | O(1) |
lookup | O(1) |
insert | O(n) |
delete | O(n) |
Linked List链表,便于删除与插入,不容易访问任意位置。
链表的时间复杂度
prepend | O(1) |
---|---|
append | O(1) |
lookup | O(n) |
insert | O(1) |
delete | O(1) |
优化思想:升维 空间换时间
链表的优化 —— 跳表
查询的时间复杂度是logn。
添加第一级索引(比如原始链表是跨一步,而一级索引是跨两步)
添加第二级索引(速度是第一级索引的二倍)
跳表查询的时间复杂度分析:
n/2、n/4、n/8、第K级索引结点的个数就是n/(2^k)
假设索引有h级,最高级的索引有2个结点。n/(2^h)=2,从而求得h=log2(n)-1
跳表的空间复杂度分析:
原始链表大小为n,每2个结点抽1个,每层索引的结点数:n/2,n/4,n/8,…,8,4,2
原始链表大小为n,每3个结点抽1个,每层索引的结点数:n/3,n/9,n/27,…,9,3,1
空间复杂度是O(n)
LRU缓存算法
跳跃表
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
计算方法:
①忽略常数,用O(1)表示
②递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
③对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。
链表的操作
链表的操作
链表的创建与插入(在最后节点添加新节点)
由于链接列表通常由其头指针表示,因此需要遍历列表直到最后一个节点,然后将最后一个节点的下一个更改为新节点。
链表的遍历
链表的删除(给定值)
情况1:钥匙位于头部
- 在这种情况下,将节点的头更改为当前头的下一个节点。
- 释放已替换头节点的内存。
情况2:钥匙位于中间或最后,头部除外
- 在这种情况下,查找要删除的节点的上一个节点。
- 将上一个节点的下一个更改为当前节点的下一个节点。
- 释放替换节点的内存。
情况3:在列表中找不到密钥
在这种情况下,无需执行任何操作。
链表的删除(给定位置)
通过计算节点索引遍历列表
对于每个索引,将索引与位置匹配
现在,三个条件中的任何一个都可以存在:
情况1:位置为0,即要删除磁头
- 在这种情况下,将节点的头更改为当前头的下一个节点。
- 释放已替换头节点的内存。
情况2:位置大于0但小于列表的大小,即在中间或最后,除了头部
- 在这种情况下,查找要删除的节点的上一个节点。
- 将上一个节点的下一个更改为当前节点的下一个节点。
- 释放替换节点的内存。
情况3:位置大于列表的大小,即在列表中找不到位置
在这种情况下,无需执行任何操作。
第四课 栈和队列的实现与特性
栈stack:先入后出;添加、删除为O(1) 查找为O(n)
具有最近相关性——栈
队列queue:先入先出;添加、删除为O(1) 查找为O(n)
先来后到——队列
双端队列deque:两端可以进出的Queue;插入、删除都是O(1) 查找为O(n)
优先队列priority queue:按照元素的优先级取出;查入O(1) 取出O(logN);底层具体实现的数据结构较为多样和复杂:heap堆、bst二叉搜索树、treap
复杂度分析
第五课 哈希表、映射、集合的实现与特性
哈希表(Hash table),也叫散列表,是根据关键码值而直接进行访问的数据结构。
它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫作散列函数,存放记录的数组叫作哈希表(或散列表)。
面试时的做题步骤:过一遍题 找最优算法 写代码 测试样例
😌还不会建立哈希表 加油
第六课 二叉树、二叉搜索树的实现与特性
树与图的最大的区别:有无环
Linked List 是特殊化的 Tree
Tree是特殊化的Graph
二叉树的遍历:
- 前序(Pre-order):根左右
- 中序(In-order):左根右
- 后序(Post-order):左右根
二叉搜索树Binary Search Tree
二叉搜索树,也称有序二叉树(Ordered Binary Tree)、排序二叉树(Sorted Binary Tree),是指一棵孔数或者具有下列性质的二叉树:
- 左子树上左右结点的值均小于它的根结点的值;
- 右子树上左右结点的值均大于它的根结点的值;
- 以此类推:左、右子树也分别为二叉查找树。(重复性)
中序遍历:升序排列
二叉搜索树常见操作:
- 查询
- 插入新结点(创建)
- 删除
树的解法一般都是递归
第七课 递归的实现、特性以及思维要点
递归本质就是循环,通过循环体调用自己来进行循环。
向下进入不同的递归层,向上又回到原来层。
用参数来进行函数不同层之间的传递变量。
递归模板:
递归终结条件
处理当前层逻辑
下探到下一层
清理当前层
void recur(int level, int param) {
//terminator
if(level > MAX_LEVEL) {
//process result
return;
}
//process current logic
process(level, param);
//drill down
recur(level+1, newParam);
//restore current status;
}
思维要点:
- 不要人肉进行递归(最大误区)
- 找到最近最简方法,将其拆解成可重复解决的问题(重复子问题)
- 数学归纳法思维
第八课 分治、回溯的实现和特性
特殊的递归 (找重复性)
分治代码模板:
recursion terminator
prepare data
conquer subproblems
process and generate the final result
revert the current level states
递归终止条件
拆分子问题
调子问题的递归函数
合并结果
恢复当前层状态
(当前层只考虑当前层 不要下探)
回溯法
回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的工程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其他的可能的分步解答再次尝试寻找问题的答案。
回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案。
- 在尝试了所有可能的分步方法后宣告该问题没有答案。
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
第九课
深度优先搜索和广度优先搜索
搜索:一般来说是,暴力搜索
遍历搜索:在树(图/状态集)中寻找特定结点
每个结点都要访问一次
每个结点仅仅要访问一次
对于结点的访问顺序不同:
深度优先:depth first search 广度优先:breadth first search
优先级优先(启发式搜索)
第十课 贪心算法Greedy
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都作出选择,不能回退。
动态规划会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心:当下做局部最优判断
回溯:能够回退
动态规划:最优判断+回退
贪心法可以解决一些最优化问题,如:求图中的最小生成树,求哈夫曼编码等。然而对于工程和生活中的问题,贪心算法一般不能得到我们所要求的答案。
一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。对于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
贪心算法的应用场景:
简单地说,问题能够分解成子问题来解决,子问题的最优解能递归到最终问题的最优解。这种子问题最优解称为最优子结构。
贪心法使用形式:
正常贪心、转换后贪心、从前往后贪心、从后往前贪心
第十一课 二分查找
化繁为简
二分查找的前提:
- 目标函数单调性、有序(单调递增或者递减)
- 存在上下界(bounded)
- 能够通过索引访问(index accessible)
代码模板
left = 0, right = numslen-1
while(left <= right) {
mid = (left + right)/2;
if(nums[mid] == target;
break or return result;
else if(nums[mid] < target) {
left = mid + 1;
}
else if(nums[mid] > target){
right = mid - 1;
}
}
第十二课 动态规划的实现及关键点
分治、回溯、递归、动态规划
- 人肉递归低效、很累
- 找到最近最简方法,将其拆解成可重复解决的问题
- 数学归纳法(抵制人肉递归的诱惑)
本质:寻找重复性->计算机指令集
若必须使用人肉递归时:画递归状态树发现问题
动态规划Dynamic Programming
- 将一个复杂的问题,把它分解成简单的子问题(用递归的方式)。
- 一般来说,动态规划的问题会让你求一个最优解或者求一个最大值或者求一个最少的方式。(分治+最优子结构)
关键点:
- 动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)
- 共性:找到重复子问题
- 差异性:最优子结构、中途可以淘汰次优解
动态规划关键点:
- 最优子结构 opt[n] = best_of(opt[n-1], opt[n-2],…)
- 存储中间状态: opt[i]
- 递推公式(美其名曰:状态转移方程或DP方程)
Fib: opt[i] = opt[n-1] + opt[n-2]
二维路径: opt[i, j] = opt[i+1][j] + opt[i][j+1](且判断a[i,j]是否空地)
动态规划小结
- 打破自己思维惯性,形成机器思维
- 理解复杂逻辑的关键
- 也是职业进阶 要点要领
动态规划五步走
- 进行分治,化繁为简,定义子问题
- 猜如果写递推方程
- 合并子问题的解
- 递归和记忆化 或者 建立DP的状态表 自底向上进行递推
- 解决最初的问题
第十六课 位运算基础及实战要点
位运算符
算数移位与逻辑移位
位运算的应用
- 左移 << 0011=>0110
- 右移 >> 0110=>0011
- 或运算 | (0011) | (1011) => 1011 一个为1,则为1
- 与运算 & (0011) & (1011) => 0011 一个为0,则为0
- 取反运算 ~ 0011 =>1100
- 异或运算 ^ (0011) ^ (1011) =>1000 相异为1,相同为0
异或运算:
x^0 = x
x^1s = ~x //1s = ~0
x^(~x) = 1s
x^x = 0
c = a^b => a^c = b, b^c = a //交换率
abc = a(bc) = (ab)c //结合律
指定位置的位运算:
- 将x最右边的n位清零:x&(~0<<n)
- 获取x的第n位值(0或者1): (x >> n) & 1
- 获取x的第n位的幂值: x & (1 << (n-1))
- 仅将第n位置为1: x | (1 << n)
- 仅将第n位置为0: x & (~(1 << n))
- 将x最高位至第n位(含)清零:x&((1<<n) - 1)
- 将第n位至第0位(含)清零:x&(~((1<<(n+1)) - 1))
判断奇偶:
x%2 == 1 ——>(x&1) == 1
x%2 == 0 ——>(x&1) == 0
x = x*2; ——> x = x<<1;
x = x/2; ——> x = x>>1;
mid = (left + right)/2; ——> mid = (left+right) >>1;
X = X&(X-1) 清零最低位的1
X&-X => 得到最低位的1
X&~X => 0
第十八课 初级排序和高级排序的实现和特性
- 比较类排序:
通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性比较类排序。 - 非比较类排序:
不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非线性比较类排序。
初级排序 O(n^2)
- 选择排序 (Selecrion Sort)
每次找最小值,然后放到待排序数组的起始位置。
//选择排序
//每次找最小值,然后放到待排序数组的起始位置。
void selectionSort(int *arr, int len) {
int minIndex, temp;
for (int i = 0; i <= len; i++) {
minIndex = i;
for (int j = i + 1; j <= len; j++) {
// 寻找最小的数
if (arr[j] < arr[minIndex]) {
minIndex = j; // 将最小的数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
- 插入排序 (Insertion Sort)
从前到后逐步构建有序序列;对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。
//插入排序
//从前到后逐步构建有序序列;对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。
void insertSort(int *arr, int len) {
int preIndex, current, i;
for (i = 1; i <= len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
}
- 冒泡排序 (Bubble Sort)
嵌套循环,每次查看相邻的元素如果逆序,则交换。每经过一次循环冒泡,最大元素就在最后面。
高级排序 O(N*logN)
-
快速排序 (Quick Sort)
数组取标杆pivot,将小元素放pivot左边,大元素放右侧,然后依次对右边和右边的子数组继续快排;以达到整个序列有序。 -
归并排序 (Merge Sort) 分治
(1)把长度为n的输入序列分成两个长度为n/2的子序列;
(2)对这两个子序列分别采用归并序列;
(3)将两个排序好的子序列合并成一个最终的排序序列。
总结:
归并和快排具有相似性,但步骤顺序相反
归并:先排序左右子数组,然后合并两个有序子数组
快排:先调配出左右子数组,然后对于左右子数组进行排序 -
堆排序 (Heap Sort)
堆插入O(logN),取最大/小值O(1)
(1) 数组元素一次建立小顶堆
(2)依次取堆顶元素,并删除
特殊排序 O(n)
- 计数排序
计数排序要求输入的数据必须是有确定范围的整数。将输入的数据值转化为键存储在额外开辟的数组空间中;然后依次把计数大于1的填充回原数组。 - 桶排序
桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。 - 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最到位。有时候有些属性是有优先级顺序的,先按低优先级顺序排序,再按高优先级排序。
第二十课 字符串基础知识和引申题目
字符串匹配算法
暴力法
O(mn)
Rabin-Karp 算法
为了避免挨个字符对目标字符串和子串进行比较,我们可以尝试一次性判断两者是否相等。因此,我们需要一个好的哈希函数。通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。
算法思想:
- 假设子串的长度为M,目标字符串的长度为N
- 计算子串的hash值
- 计算目标字符串中每个长度为M的子串的hash值(共需要计算N-M+1次)
- 比较hash值:如果hash值不同,字符串必然不匹配;如果hash值相同,还需要使用朴素算法再次判断
KMP算法
当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那一部分的字符(包括子串与目标字符串)。KMP算法的想法是,设法利用这个已知信息,不要把“搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。