高级数据结构总结
前期回顾:常用数据结构总结
仅仅掌握好它们是不足以应对大厂的算法面试的,很多大厂的算法面试题都涉及到了大数据、图论以及动态规划的数据等,这些问题如果只凭借常用数据结构来解决的话,是很难达到理想的时间和空间复杂度要求的。
1.1 高级数据结构
- 1.2 优先队列 / Priority Queue
- 1.3 图 / Graph
- 1.4 前缀树 / Trie
- 1.5 线段树 / Segment Tree
- 1.6 树状数组 / Fenwick Tree / Binary Indexed Tree
- 总结
1.2 优先队列 / Priority Queue
与普通队列的区别
优先队列的作用是能保证每次取出的元素都是队列中优先级别最高的,这个优先级别可以是自定义的,例如:数据的数值越大,数据的优先级别越高;或者是数据的数值越小,优先级别越高。优先级别可以是通过一些复杂计算得到的。
最常用的场景
从杂乱无章的数据中按照一定的顺序(或者优先级)筛选数据。例如:任意给定一个数组,要求找出前K大的数,试想一下,最直接的办法就是先对这个数组进行排序,然后一次输出前k大的数,这样的复杂度将会是O(nlogN)。而如果我们借用优先队列,就能将复杂度降低到O(K+nlogK),当n很大而k较小时,显然,可以有效降低时间复杂度,其本质在于,要找出前k大的数,我们并不需要对所有的数进行排序。
本质
优先队列的本质是二叉堆结构,是利用一个数组结构来实现的完全二叉树。
特性
数组里的第一个元素array[0]拥有最高的优先级,给定一个下标 i ,那么对于元素array[i]而言
1.父节点对应的元素下标是(i-1) / 2;
2.左侧子节点对应的元素2 * i +1;
3.右侧子节点对应的元素下标是2 * i + 2;
数组中的每个元素的优先级都必须要高于它两侧子节点。
基本操作
向上筛选(sift up / bubble up):当有新的数据加入到优先队列中,新的数据首先被放置在二叉堆的底部,然后不断的对它进行向上筛选的操作,即如果发现它的优先级别比它的父节点的优先级别还要高,那么就和父节点的元素相互交换,再接着往上进行比较,直到无法交换为止。由于二叉堆是一棵完全二叉树,并假设堆的大小为k,因此,整个过程其实就是沿着树的高度往上爬,所以只需要O(logK)的时间。
向下筛选(sift down / bubble down):当堆顶的元素被取出时,我们要更新堆顶的元素来作为下一次按照优先顺序被取出的对象,我们所需要的是要将堆底部的元素放置到堆顶,然后不断的对它执行向下筛选的操作,在这个过程中,该元素和它的两个孩子节点对比,看看哪个的优先级别最高,如果优先级别最高的是其中一个孩子,就将该元素和那个孩子进行交换,然后反复进行下去,直到无法交换。整个过程其实就是沿着树的高度往下爬,所以只需要O(logK)的时间。
题目:
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
解题思路
当我们看到前k个的时候,就应该想到使用优先队列,那么优先级别怎么计算?
优先级别可以用数值出现的次数来决定,出现的次数越多,优先级越高,反之越低,统计词频的最佳数据结构就是哈希表,利用一个哈希表,我们就能快速的知道哪个数字出现的次数,然后将数字和其次数作为一个新的对象来构建一个优先队列,这个问题就可以解决了。
可以知道,解这类求前k个的问题时,关键是看如何定义优先级别以及优先队列中元素的数据结构。
1.3 图 / Graph
图可以说是所有数据结构里面知识点最丰富的一个。
基本知识点
阶、度
树、森林、环
有向图、无向图、完全有向图、完全无向图
连通图、连通分量
图的存储和表达方式:邻接矩阵、邻接链表
围绕图的算法
图的遍历:深度优先、广度优先
环的检测:有向图、无向图
拓扑排序
最短路径算法:Dijkstra、Bellman-Ford、Floyd Warshall
连通性相关算法:Kosaraju、Tarjan、求解孤岛的数量、判断是否为树
图的着色、旅行商问题提等
力扣里面有很多经典的算法题,建议有方法的去学习。
建议熟练掌握的知识点
图的存储和表达方式:邻接矩阵、邻接链表
图的遍历:深度优先、广度优先
二部图的检测(Bipartite)、树的检测、环的检测:有向图、无向图(基于图的遍历)
拓扑排序(基于图的遍历)
联合-查找算法(Union-Find)
最短路径:Dijkstra、Bellman-Ford
其中,掌握好图的遍历是重中之重,至于最短路径算法,能区分它们的不同特点,知道在什么情况下用什么算法就行。
题目:
存在一个 无向图 ,图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph ,其中 graph[u] 是一个节点数组,由节点 u 的邻接节点组成。形式上,对于 graph[u] 中的每个 v ,都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性:
1.不存在自环(graph[u] 不包含 u)。
2.不存在平行边(graph[u] 不包含重复值)。
3.如果 v 在 graph[u] 内,那么 u 也应该在 graph[v] 内(该图是无向图)
4.这个图可能不是连通图,也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。
二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。
如果图是二分图,返回 true ;否则,返回 false 。
示例1:
输入:graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
输出:false
解释:不能将节点分割成两个独立的子集,以使每条边都连通一个子集中的一个节点与另一个子集中的一个节点。
示例2:
输入:graph = [[1,3],[0,2],[1,3],[0,2]]
输出:true
解释:可以将节点分成两组: {0, 2} 和 {1, 3} 。
很显然,我们必须要对这个图进行一个遍历,遍历的方法有深度优先和广度优先。(后面会详细介绍)
1.4 前缀树 / Trie
前缀树也被称为字典树,因为这种数据结构被广泛地运用在字典查找当中。
什么是字典查找?
例如:给定一系列构成字典的字符串,要求在字典当中找出所有以"ABC"开头的字符串。
方法一:暴力搜索法,假设字典有N个单词,要对比的开头平均长度为M,时间复杂度为O(M*N)
方法二:前缀树,时间复杂度为O(M)。
经典运用
在网站的搜索框中输入搜索文字时,会罗列出以搜索词开头的相关搜索信息。这里就运用到了前缀树,在后端进行快速的检索。如下:
还有我们经常使用的汉语拼音输入法,它的联想输入功能也用到了前缀树。
重要性质
每个节点至少包含两个基本属性
1.children:数组或者集合,罗列出每个分支当中包含的所有字符。
2.isEnd:布尔值,表示该节点是否为某字符串的结尾
根节点是空的
除了根节点,其他所有节点都可能是单词的结尾,叶子节点一定都是单词的结尾
最基本的操作
创建:
方法:
1.遍历一遍输入的字符串,对每个字符串的字符进行遍历
2.从前缀树的根节点开始,将每个字符加入到节点的children字符集当中
3.如果字符集已经包含了这个字符,跳过
4.如果当前字符是字符串的最后一个,把当前节点的isEnd标记为真
前缀树真正强大的地方在于每个节点还能用来保存额外的信息,比如可以用来记录拥有相同前缀的所有字符串,这样一来,当用户输入某个前缀时,我们就能在O(1)的时间内查找出对应的推荐字符串。
搜索:
方法:
1.从前缀树的根节点出发,逐个匹配输入的前缀字符
2.如果遇到了,继续往下一层搜索
3.如果没遇到,立即返回
题目:
给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例1:
输入:board = [[“o”,“a”,“a”,“n”],[“e”,“t”,“a”,“e”],[“i”,“h”,“k”,“r”],[“i”,“f”,“l”,“v”]], words = [“oath”,“pea”,“eat”,“rain”]
输出:[“eat”,“oath”]
示例2:
输入:board = [[“a”,“b”],[“c”,“d”]], words = [“abcb”]
输出:[]
解题思路:
根据题意,我们需要逐个遍历二维网格中的每一个单元格;然后搜索从该单元格出发的所有路径,找到其中对应words 中的单词的路径。因为这是一个回溯的过程,所以我们有如下算法:
1.遍历二维网格中的所有单元格。
2.深度优先搜索所有从当前正在遍历的单元格出发的、由相邻且不重复的单元格组成的路径。因为题目要求同一个单元格内的字母在一个单词中不能被重复使用;所以我们在深度优先搜索的过程中,每经过一个单元格,都将该单元格的字母临时修改为特殊字符(例如 #),以避免再次经过该单元格。
3.如果当前路径是 words 中的单词,则将其添加到结果集中。如果当前路径是 words 中任意一个单词的前缀,则继续搜索;反之,如果当前路径不是 words 中任意一个单词的前缀,则剪枝。我们可以将 words 中的所有字符串先添加到前缀树中,而后用 O(∣S∣) 的时间复杂度查询当前路径是否为words 中任意一个单词的前缀。
1.5 线段树 / Segment Tree
先从一个例题出发
假设我们有一个数组array[0…n-1],里面有n个元素,现在我们要经常对这个数组做两件事:
1.更新数组元素的数值
2.求数值任意一段区间内元素的总和(或者平均值)
方法一: 遍历一遍数组
时间复杂度:O(n)
方法二: 线段树
时间复杂度:O(logn)
什么是线段树
一种按照二叉树的形式存储数据的结构,每个节点保存的都是数组里某一段的总和。
例如:
数组是[1,3,5,7,9,11],它的线段树为:
例题
LeetCode 315. 计算右侧小于当前元素的个数
题目:给你`一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于nums[i] 的元素的数量。
示例1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
示例2:
输入:nums = [-1]
输出:[0]
示例3:
输入:nums = [-1,-1]
输出:[0,0]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
解题思路:
对于这道题来说,步骤如下:
1.遍历数组,找到数组的min和max,构造线段树[min, max]
2.从右往左遍历,记当前元素为x
2.1统计线段树中[min, x-1]范围内的元素,即右侧小于x的元素个数
2.2往线段树中插入元素x,并更新线段树各个结点的count值
1.6 树状数组 / Fenwick Tree / Binary Indexed Tree
先从一个例题出发
假设我们有一个数组array[0…n-1],里面有n个元素,现在我们要经常对这个数组做两件事:
1.更新数组元素的数值
2.求数值前k个元素的总和(或者平均值)
方法一: 线段树
时间复杂度:O(logn)
方法二: 树状数组
时间复杂度:O(logn)
重要基本特征
利用数组来表示多叉树的结构,和优先队列有些类似
优先队列是用数组来表示完全二叉树,而树状数组是多叉树
树状数组的第一个元素是空节点
如果节点tree[y]是tree[x]的父节点,那么需要满足y = x *(x &(-x))
因为和线段树相似,所以不在仔细讲解。
拓展练习
LeetCode 308等
总结
优先队列: 常见面试考点,实现过程比较繁琐。在解决面试中的问题时,实行“拿来主义”即可。
图: 被广泛运用的数据结构,如大数据问题都得运用图论
1.在社交网络里,每个人都可以用图的顶点表示,人与人直接的关系可以用图的边表示
2.在地图上,要求解从起始点到目的地,如何行驶会更快捷,需要运用图论里的最短路径算法
前缀树: 出现在面试的难题中,要求能熟练地书写它的实现以及运用
线段树和树状数组: 应用场合比较明确,本文章里面都是一维的例子。如果要求在一副图片当中修改像素的颜色,求解任意矩形区间的灰度平均值,则需要采用二维的线段树。
在LeetCode中上面的数据结构都有丰富的例子,大家可以自己选择练习。
本文章是小朱近日刷算法题,看视频之后做的一些笔记和总结,希望大家有所收获,不足之处,请指正,如果对你有帮助可以点赞加收藏。