顺便记录一下自己的理解和收获。
一. 数据结构
- 数据结构
- 定义:指一组数据的存储结构。服务于算法。
- 常用分类:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;
- 数组(Array)
- 预先开辟一组足够大的内存空间,存储相同类型的数据。
- 数组的下标就是偏移offset
a[k]_address = base_address + k * type_size - 数组,链表、队列、栈等都是线性表结构
- 使用数据:存储基本数据类型,知道大小,多维数组
- 为什么数组越界就会无限循环
- 为什么有些数组的下标是从0开始?05 | 数组:为什么很多编程语言中数组都从0开始编号?-极客时间
-
链表(List)
-
将一组零散的内存块串联起来使用
-
常见的链表结构:单链表、双向链表和循环链表
-
适合插入和删除,不适合查询
-
与数组对比
-
- 栈(Stack)
- 栈是一种“操作受限”的线性表
- 后进者先出,先进者后出 入栈 push()和出栈 pop()
-
实现: 数组-->顺序栈 链表-->链式栈
-
应用场景: 函数调用, 表达式求值,括号匹配等
- 队列(Queue)
- 队列也是一种“操作受限”的线性表
- 先进者先出
- 入队 enqueue() 出队 dequeue()
- 实现: 数组-->顺序队列 链表-->链式队列
- 循环队列,阻塞队列,并发队列
- 编写代码时, 明确队空,队满的条件
- 实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。
- 散列表(Hash Table)
- 散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
- 键(key,关键字)--> 散列函数(哈希函数)--> 散列值(Hash 值,哈希值)
- 几乎无法找到一个完美的无冲突的散列函数,即便能找到,付出的时间成本、计算成本也是很大的,所以针对散列冲突问题,我们需要通过其他途径来解决。
- 开放寻址法(open addressing)
- 链表法(chaining)
- 如果研究过HashMap的源码,就会发现,在JDK1.7版本及以下,HashMap采取的就是链表法处理Hash冲突;但是如果一个链表过长,数组会退化成一个链表,查询的时间复杂度会变成O(n);所以在JDK1.8后引入了红黑树,降低链表过长时,数组退化成链表
- 二叉树(Binary Tree 非线性结构)
- 高度、深度和层的关系
- 两种存储:基于链表的二叉链式存储法,一种是基于数组的顺序存储法
- 链表:节点是由存储数据的data,left和right两个指针组成
- 数组:如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。
- 如何选择存储:当一棵树是完全二叉树或者是满二叉树时可以用数组存储时非常节省空间而且计算节点位置也非常方便,如果是非完全二叉树或者是单臂树时最好使用链表来存储
- 堆其实就是一种完全二叉树,最常用的存储方式就是数组
- 三种遍历:二叉树的前、中、后序遍历就是一个递归的过程
- 掌握二叉查找树(Binary Search Tree)红黑树、递归树的特点和支持的操作
- 堆(Heap)
- 最常用的应用是堆排序。时间复杂度为 O(nlogn)
- 特点(定义):
- 堆是一个完全二叉树;
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
- 对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”
- 堆实现排序两步:建堆和排序
- 每种数据结构都会有其特定的算法和使用场景,堆适用于有顺序要求,但又不需要完全排序的场景,可以达到㏒时间复杂度。凡是涉及n个元素集合中取最值元素场景或取第n大元素的都适合堆,比如n个有序小文件排序,定时器,中位数
- 跳表(Skip List)
- 可以支持快速地插入、删除、查找操作,甚至可以替代红黑树
- Redis 中的有序集合(Sorted Set)就是用跳表来实现的
- 跳表结构
- 图(Graph)
- 两种存储:邻接矩阵(二维数组) 和 邻接表(链表)
- 相关算法 a. 图的搜索 b. 最短路径 c. 最小生成树 d. 二分图
- Trie树(字典树)
- 它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题
- Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起
- trie的两个操作: a.一个是将字符串集合构造成 Trie 树 b.在 Trie 树中查询一个字符串
- Trie 树是非常耗内存的,用的是一种空间换时间的思路
- Trie 树只是不适合精确匹配查找,Trie 树比较适合的是查找前缀匹配的字符串
- 数组(Array)
二. 算法
- 算法
- 定义:操作数据的一组方法。算法要作用在特定的数据结构之上。
- 常用分类:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
- 递归
- 三个条件(递归公式 + 终止条件):
- 一个问题的解可以分解为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
- 优缺点
- 优点:表达能力强,简洁明了
- 缺点:空间复杂度高、有堆栈溢出的风险、存在重复计算
- 应用于动规的前两种解法:递归的暴力解法—>带备忘录的递归解法(自顶向下 f20→f1)—>非递归的动态解法(自底向上 f1→f20)
- 笼统的讲,所有的递归代码都可以改为迭代循环的非递归写法
- 三个条件(递归公式 + 终止条件):
- 排序(Sort)
- 明白以下排序的实现、复杂度、稳定性。详见notion
- 二分查找(Binary Search)
- 定义是在一个有序且没有重复的数组中,查找某个指定的元素,并返回指定元素的位置,如果没有找到,则返回-1。
- 原理是分治思想,即定位在指定区间n-m的中间元素k,判断中间元素k跟要查找的值是否相等,如果相等就返回k,如果大于就m=k-1,如果小于就n=k+1,继续递归处理,直到n>m。
- 易出错点
- 1.循环退出条件是n<=m,不是n<m。
- 2.mid取值算法最好是n+((m-n)>>1),不要用最普通的(n+m)/2,因为n+m有可能很大值导致int类型越界,除2的操作没有位运算快。 二分查找的时间复杂度是logn。
- 使用场景
- 数据结构是采用数组
- 数据是有序的且非重复元素,如果无序还要使用排序算法对数组进行排序操作,然后再进行二分查找
- 数据量过小的时候不适用,用for循环遍历也可以
- 数据量过大的时候不适用,因为数组的内存空间需要连续的,如果数据量过大,对连续内存空间要求过高,数组可能无法装下。
- 搜索
- BFS 和 DFS 都是用来遍历图的算法
- BFS(Breadth-First-Search)
- 它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
- 通常使用队列实现
- DFS(Depth-First-Search)
- 深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。
- 通常用递归实现
- BFS(Breadth-First-Search)
- BFS 和 DFS 都是用来遍历图的算法
- 哈希算法
- 定义是将任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法,而通过原始数据映射之后得到的二进制值串就是哈希值
- 特点
-
单项推导
-
数据敏感
-
冲突概率低
-
高效执行
-
-
使用场景
-
安全加密
-
唯一标识
-
数据校验
-
散列函数
-
负载均衡
-
数据分片
-
分布式存储
-
- 分治算法
- 定义是核心思想其实就是四个字,分而治之 ,也就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。
- 使用场景
- 原问题与分解成的小问题具有相同的模式;
- 原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别,等我们讲到动态规划的时候,会详细对比这两种算法;
- 具有分解终止条件,也就是说,当问题足够小时,可以直接求解;
- 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了。
- 区别是分治算法中“分”与“治”与递归思想中“递”与“归”相结合。“递”往往是为了“分”,而“归”的同时做到“治”。分治算法是一种处理问题的思想,递归是一种编程技巧。
- 应用
- MapReduce
- 归并算法等
- ……
- 回溯算法
- 定义是本质上就是枚举。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。(通过剪枝少走冤枉路,它可能适合应用于缺乏规律,或我们还不了解其规律的搜索场景中。)
- 应用
- 深度优先
- 正则表达式匹配
- 编译原理中的语法分析
- 数学问题(比如数独、八皇后、0-1 背包、图的着色、旅行商问题、全排列等等)
- ……
- 动规
- 递归的暴力解法—>带备忘录的递归解法(自顶向下 f20→f1)—>非递归的动态解法(自底向上 f1→f20)(一般是循环,不是递归)
- 特点
- 存在「重叠子问题」
- 具备「最优子结构」
- 正确的「状态转移方程」
- 贪心算法
- 使用场景
- 首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
- 尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据。
- 我们举几个例子看下贪心算法产生的结果是否是最优的
- 应用
- 分糖果
- 钱币找零
- 区间覆盖
- 霍夫曼编码
- 使用场景
- 字符串匹配算法
- BF 算法(常用)
- BF是 Brute Force 的缩写,中文叫作暴力匹配算法,也叫朴素匹配算法。
- 最坏的时间复杂度 O(n*m)
- RK 算法
- RK 是 Rabin-Karp 算法,是 BF 算法的升级版。是借助哈希算法对 BF 算法进行改造,即对每个子串分别求哈希值,然后拿子串的哈希值与模式串的哈希值比较,减少了比较的时间
- 理想时间复杂度 O(n)
- BM 算法
- KMP 算法
- 33 | 字符串匹配基础(中):如何实现文本编辑器中的查找功能?-极客时间
- 34 | 字符串匹配基础(下):如何借助BM算法轻松理解KMP算法?-极客时间
- BF 算法(常用)
- 递归