数据结构和算法
数据结构和算法
背景
代码效率-时间复杂度与空间复杂度
代码运行效率用复杂度衡量。
代码运行消耗时间和空间。
现实中的例子:
- 汽车在有红绿灯的十字路口通行。红绿灯时汽车等待,消耗时间。
- 汽车在建有立交桥的十字路口通行,不用等待,立交桥消耗空间。
时间、空间消耗和输入量密切相关,无法直接定量衡量。为了更客观的评判时间消耗和空间消耗,我们关注输入量与时间、空间的关系。即时间复杂度、空间复杂度。
时间复杂度与代码结构密切相关,空间复杂度与数据结构密切相关。
不同时间复杂度的计算次数例子:100万条数据的处理
- O(n²),10的十二次方
- O(n),10的六次方
- O(log n),20次
时间无价,空间有价。
数据结构:连接时间和空间复杂度
代码优化:
- 暴力解法
- 剔除无效计算、无效存储。(递归、回溯、分治(包括二分法)、排序算法、查找算法、贪婪、动态规划)
- 使用数据结构,将时间复杂度向空间复杂度转移
代码的时间复杂度和常量系数无关,代码的执行时间与常量系数有关。
复杂度估算
复杂度是关于输入数据量n的函数。
我们假定输入量为n,代码执行n次,复杂度为O(n);
我们假定输入量为n,代码执行k常数次,与输入量无关,则复杂度为O(1);
不同数量级的多项复杂度相加时,去数量级高的值。O(n²)+O(n) = O(n²)
常见的复杂度:
- 顺序执行,O(1);
- 一层for循环,O(n);
- 两个顺序执行for循环,O(n);
- 两层嵌套for循环,O(n²);
- 二分查找,即二分分而治之,O(log n);
数据操作
遍历
增
- 中间增加,元素位置顺序改变
- 末尾增加
删
- 中间删除,元素位置顺序改变
- 末尾删除
查
能否在数据结构中找到此元素。
- 基于位置查询(索引、下标)
- 基于数值特征查询(数值相等)
算法分析
算法:递归、分治(二分是重点)、回溯、贪婪、动态规划
数据结构选择
分析数据处理顺序、数据操作,选择不同的数据结构。
不同数据结构特点[参考,按住Control点我](# 各类数据结构对比)
Java 数据结构API
数组
1 | // 复制数组 |
Map
1 | Map<String,String> map = new HashMap(); |
双端队列-链表
LinkedList
| 第一个(头) | 最后一个(尾) | |
|---|---|---|
| 插入 | offerFirst(e) | offerLast(e) |
| 删除 | pollFirst() | pollLast() |
| 获取元素 | peekFirst() | peekLast() |
| 入队 | 出队(拿最大元素 | |
|---|---|---|
| 数组线性结构 | O(1) | O(n) |
| 链式线性结构 | O(n) | O(1) |
| 二叉堆 | O(logn) | O(logn) |
优先队列(二叉堆)PriorityQueue
1 | // 初始化大小为k的元素为int[]的小根堆(顶为堆中数组元素int[1]最小) |
数据结构
数据的组织形式。对数据组织成一定的结构。
1.线性表-链表
介绍
n 个相同特性数据元素的有限序列。
在链表中存储的数据元素也叫作结点,一个结点存储的就是一条数据记录。每个结点的结构包括两个部分:
- 第一是具体的数据值;
- 第二是指向下一个结点的指针。
在链表的最前面,通常会有个头指针用来指向第一个结点。对于链表的最后一个结点,由于在它之后没有下一个结点,因此它的指针是个空指针。
存储结构
数据操作
新增
有一个链表,它存储了 10 个同学的考试成绩。现在将一个同学的成绩插入到第4个位置。
链表在执行数据新增的时候非常容易,只需要把待插入结点的指针指向原指针的目标,把原来的指针指向待插入的结点,就可以了。
p s(带插入) t
1 | s.next = p.next; |
复杂度:O(1)
删除
1 | p.next = p.next.next; |
复杂度:O(1)
查找
按照位置序号来查找
一个一个地遍历去查找。
复杂度:O(n)
按照具体的成绩来查找
一个一个地遍历去查找。
复杂度:O(n)
总结
如果处理数据时有顺序,数据的元素个数不确定,且需要经常进行数据的新增和删除时,那么链表会比较合适。
- 数据按照顺序存储,可以有序遍历数据。
- 按照数值的条件进行查找时,需要遍历,时间复杂度O(n)。
- 新增数据,复杂度是O(1)。伴随查找的动作,新增复杂度变为了O(n)
- 删除数据时,复杂度是O(1)。伴随查找的动作,新增复杂度变为了O(n)
链表算法题
链表的翻转。给定一个链表,输出翻转后的链表。例如,输入1 ->2 -> 3 -> 4 ->5,输出 5 -> 4 -> 3 -> 2 -> 1。
分析:单向链表,它的指针结构造成了它的数据通路有去无回,一旦修改了某个指针,后面的数据就会造成失联的状态。为了解决这个问题,我们需要构造三个指针 prev、curr 和 next,对当前结点、以及它之前和之后的结点进行缓存,再完成翻转动作。
求奇数结点链表的中间结点的数据
分析:未知长度的数据,获取总长度的一半。利用快慢指针,快指针是慢指针的前进速度的两倍,循环次数是数组长度的一半。
链表是否有环
快慢指针,有环时,慢指针循环一圈必定与快指针相遇一次。
2.线性表-栈
介绍
线性表的变种,元素先进后出。栈的数据新增操作只能在末端进行,不允许在栈的中间某个结点后新增数据。
数组或者链表的操作过于灵活,过多暴露了可操作的接口。这些没有意义的接口过多,当数据量很大的时候就会出现一些隐藏的风险。
存储结构
同链表,有表头和表尾
数据操作
新增
顺序栈
java中使用List实现栈。
栈的顺序存储可以借助数组来实现。一般来说,会把数组的首元素存在栈底,最后一个元素放在栈顶。然后定义一个 top 指针来指示栈顶元素在数组中的位置。假设栈中只有一个数据元素,则 top = 0。一般以 top 是否为 -1 来判定是否为空栈。当定义了栈的最大容量为 StackSize 时,则栈顶 top 必须小于 StackSize。
只在栈顶操作,时间复杂度O(1)
链栈
用链表的方式实现栈。
通常,可以把栈顶放在单链表的头部,如下图所示。由于链栈的后进先出,原来的头指针就毫无作用了。因此,对于链栈来说,是不需要头指针的。相反,它需要增加指向栈顶的 top 指针,这是压栈和出栈操作的重要支持。
只在栈顶操作,时间复杂度O(1)
删除
同新增
查找
同链表
总结
- 数据有序
- 新增时间复杂度O(1),但只能在栈顶操作
- 删除时间复杂度O(1),但只能在栈顶操作
- 查询时间复杂度O(n)
栈算法题
- 给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。有效字符串需满足:左括号必须与相同类型的右括号匹配,左括号必须以正确的顺序匹配。
- 链表每k个元素翻转,如:链表:123456,k=3,则结果 321654
应用场景
- 浏览器回退页面效果。
3.线性表-队列
介绍
线性表的变种,元素先进先出。
队列的数据新增操作只能在末端进行,不允许在队列的中间某个结点后新增数据;
队列的数据删除操作只能在始端进行,不允许在队列的中间某个结点后删除数据。
存储结构
数据操作
总结
- 数据有序
- 新增时间复杂度O(1),但只能在队尾操作
- 删除时间复杂度O(1),但只能在队头操作
- 查询时间复杂度O(n)
队列算法题
- 约瑟夫环问题。已知 n 个人(以编号 1,2,3…n 分别表示)围坐在一张圆桌周围。从编号为 k 的人开始报数,数到 m 的那个人出列;他的下一个人又从 1 开始报数,数到 m 的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
应用场景
面对数据处理顺序非常敏感的问题时,使用队列。
可以确定队列长度最大值时,建议使用循环队列。
无法确定队列长度时,应考虑使用链式队列。
双端队列
LinkedList
| 第一个(头) | 最后一个(尾) | |
|---|---|---|
| 插入 | offerFirst(e) | offerLast(e) |
| 删除 | pollFirst() | pollLast() |
| 获取元素 | peekFirst() | peekLast() |
优先队列
普通队列:先进先出;后进先出
优先队列:出队顺序和入队顺序无关;和优先级相关。优先级最高的在队首
底层可以使用不同的数据结构:
| 入队 | 出队(拿最大元素 | |
|---|---|---|
| 数组线性结构 | O(1) | O(n) |
| 链式线性结构 | O(n) | O(1) |
| 二叉堆 | O(logn) | O(logn) |
PriorityQueue
1 | // 初始化大小为k的元素为int[]的小根堆(顶为堆中数组元素int[1]最小) |
4.线性表衍生-数组
介绍
存储一个固定大小的相同类型元素的顺序集合。
存储结构
数组在内存中是连续存放的,数组内的数据,可以通过索引值直接取出得到。
数据操作
新增
数组的最后增加一个新的元素
对原数据产生没有任何影响。时间复杂度是 O(1)。
数组中间的某个位置新增数据
原数据的位置需要依次向后挪动 1 个位置。时间复杂度是 O(n)。
删除
同新增
查询
- 根据位置值查找,时间复杂度是 O(1)。
- 根据数值查找,时间复杂度是 O(n)。
数组算法题
总结
- 数据有序存储,遍历有序。
- 根据索引查找快,时间复杂度O(1)
- 根据数值查找,时间复杂度O(n)
- 中间位置删除数据,时间复杂度O(n);末尾删除,时间复杂度O(1)。
- 中间位置新增数据,时间复杂度O(n);末尾新增,时间复杂度O(1)。
应用场景
5.字符串
介绍
字符串(string) 是由 n 个字符组成的一个有序整体( n >= 0 )。
特殊字符串:
空串,指含有零个字符的串。例如,s = “”
空格串,只包含空格的串。例如,s = “ “
子串,串中任意连续字符组成的字符串叫作该串的子串。
原串通常也称为主串。
例如:a = “BEI”,b = “BEIJING”,c = “BJINGEI” 。
- 对于字符串 a 和 b 来说,由于 b 中含有字符串 a ,所以可以称 a 是 b 的子串,b 是 a 的主串;
- 对于 c 和 a 而言,虽然 c 中也含有 a 的全部字符,但不是连续的 “BEI” ,所以串 c 和 a 没有任何关系。
存储结构
有顺序存储和链式存储两种。
顺序存储
用一组地址连续的存储单元来存储串中的字符序列,一般是用定长数组来实现。
链式存储
由于串结构的特殊性(结构中的每个元素数据都是一个字符),如果也简单地将每个链结点存储为一个字符,就会造成很大的空间浪费。因此,一个结点可以考虑存放多个字符,如果最后一个结点未被占满时,可以使用 “#” 或其他非串值字符补全。
数据操作
字符串的数据操作与线性表有很大差别。线性表更关注的是单个元素的操作,比如增删查一个元素,而字符串中更多关注的是查找子串的位置、替换等操作。
新增
顺序存储
字符串的新增操作和数组相似,都牵涉对插入字符串之后字符的挪移操作,时间复杂度是 O(n)。
删除
和新增相似
查找
需要做的是子串查找或字符串匹配。
在主串n中找模式串m,时间复杂度是 n 和 m 的函数。O(nm)
假设要从主串 s = “goodgoogle” 中找到 t = “google” 子串。
算法题
- 两个字符串的最大公共字串。如:a = “13452439”, b = “123456”。输出字符串”345”
6.树(二叉树、红黑树、堆、前缀树、线段树)
博客 TODO
7.哈希表
介绍
实现 “地址 = f (关键字)” 的映射关系
存储结构
特点:
数据没有顺序。
数据存储与数据数值(关键字)有关。
数据操作
总结
- 数据没有顺序。
- 根据数值查找,时间复杂度O(1)
- 新增时间复杂度O(1)
- 删除时间复杂度O(1)
算法题
应用场景
满足以下:
- 不需要有序遍历数据
- n次查询
- 可以提前预测数据量的大小。
8.图
知识点 TODO
图的存储和表达方式:邻接矩阵(Adjacency Matrix)、邻接链表(Adjacency List)
图的遍历:深度优先、广度优先
二部图的检测(Bipartite)(785)、树的检测、环的检测:有向图、无向图
拓扑排序
联合-查找算法(Union-Find)
最短路径:Dijkstra、Bellman-Ford
各类数据结构对比
| 链表 | 数组 | 字符串 | 栈 | 普通队列 | 优先队列(二叉堆) | 二叉搜索树 | 前缀树 | 哈希表 | ||
|---|---|---|---|---|---|---|---|---|---|---|
| 数据有序性 | 有 | 无 | ||||||||
| 查询时间复杂度 | 按位置 | O(n) | O(1) | O(1) | O(n) | O(n) | - | - | - | |
| 按数值 | O(n) | O(n) | O(mn) | O(n) | O(n) | ? | O(logn) | O(1) | ||
| 新增时间复杂度 | 中间 | O(1) | O(n) | O(n) | - | - | O(logn) | O(logn) | O(1) | |
| 末尾 | O(1) | O(1) | O(1) | O(1) | O(1) | - | - | - | ||
| 删除时间复杂度 | 中间 | O(1) | O(n) | O(n) | - | - | O(logn) | O(logn) | O(1) | |
| 末尾 | O(1) | O(1) | O(1) | O(1) | O(1) | - | - | - |
算法
1.递归
介绍
某个函数自己调用自己。
递归的使用条件需满足以下两个条件:
可以拆解为除了数据规模以外,求解思路与原问题完全相同的子问题;
存在终止条件。
模板
1 | function fn(n) { |
递归算法题
- 二叉树中序遍历
- 汉诺塔
- 斐波那契数列
2.分治
介绍
定义:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
特征:
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
应用场景
分治算法题
- 二分搜索
- 大整数乘法
- 快速排序
- 归并排序
- 汉诺塔
3.回溯
4.排序算法
冒泡
归并
快排
5.查找算法
二分查找
满足以下两点:
- 时间复杂度O(logn)
- 数据有序
二叉搜索树
深度优先搜索DFS
介绍
从起点出发,从规定的方向中选择其中一个不断地向前走,直到无法继续为止,然后尝试另外一种方向,直到最后走到终点。
解决连通性问题,从起点能不能到终点。
依赖栈,后进先出。
例子
给定一个二维矩阵代表一个迷宫,迷宫里面有通道,也有墙壁,通道由数字 0 表示,而墙壁由 -1 表示,有墙壁的地方不能通过,那么,能不能从 A 点走到 B 点。