高级数据结构:优先队列、图、前缀树、分段树以及树状数组详解

优秀的算法每每取决于你采用哪一种数据结构,除了常规数据结构,平常更多也会遇到高级的数据结构,实现要比那些经常使用的数据结构要复杂得多,这些高级的数据结构可以让你在处理一些复杂问题的过程当中多拥有一把利器。同时,掌握好它们的性质以及所适用的场合,在分析问题的时候回归本质,不少题目都能迎刃而解了。面试

 

这篇文章将重点介绍几种高级的数据结构,它们是:优先队列、图、前缀树、分段树以及树状数组。 算法

 

1、优先队列后端

 

1.优先队列的做用数组

       

优先队列最大的做用是能保证每次取出的元素都是队列中优先级别最高的,这个优先级别能够是自定义的,例如,数据的数值越大,优先级越高,或者是数据的数值越小,优先级越高,优先级别甚至能够经过各类复杂的计算获得。数据结构

 

优先队列最经常使用的场景是从一堆杂乱无章的数据当中按照必定的顺序(或者优先级)逐步地筛选出部分的乃至所有的数据。oop

 

例如,任意给定一个数组,要求找出前k大的数。最直接的办法就是先对这个数组进行排序,而后依次输出前k大的数,这样的复杂度将会是O(nlogn),其中,n是数组的元素个数。性能

 

若是咱们借用优先队列,就能将复杂度优化成O(k + nlogk),当数据量很大(即n很大),而k相对较小的时候,显然,利用优先队列能有效地下降算法复杂度,其本质就在于,要找出前k大的数,并不须要对全部的数进行排序。优化

 

2.优先队列的实现方法网站

 

优先队列的本质是一个二叉堆结构,堆在英文里叫Binary Heap,它是利用一个数组结构来实现的彻底二叉树。换句话说,优先队列的本质是一个数组,数组里的每一个元素既有多是其余元素的父节点,也有多是其余元素的子节点,并且,每一个父节点只能有两个子节点,这就很像一棵二叉树的结构了。设计

 

这里有三个重要的性质须要牢记:

 

a.数组里的第一个元素array[0]拥有最高的优先级别。

 

b.给定一个下标 i,那么对于元素 array[i] 而言:

  • 它的父节点所对应的元素下标是 (i-1) / 2
  • 它的左孩子所对应的元素下标是 2*i + 1
  • 它的右孩子所对应的元素下标是 2*i + 2

 c.数组里每一个元素的优先级别都要高于它两个孩子的优先级别。

 

       

      

 

 

3.优先队列最基本的操做

 

a.向上筛选

当有新的数据加入到优先队列中,新的数据首先被放置在二叉堆的底部,而后不断地对它进行向上筛选的操做,即若是发现它的优先级别比父节点的优先级别还要高,那么就和父节点的元素相互交换,再接着网上进行比较,直到没法再继续交换为止。因为二叉堆是一棵彻底二叉树,并假设堆的大小为k,所以整个过程其实就是沿着树的高度网上爬,因此只须要O(logk)的时间。

                            

b.向下筛选

当堆顶的元素被取出时,咱们要更新堆顶的元素来做为下一次按照优先级顺序被取出的对象,咱们所须要的是将堆底部的元素放置到堆顶,而后不断地对它执行向下筛选的操做,在这个过程当中,该元素和它的两个孩子节点对比,看看哪一个优先级最高,若是优先级最高的是其中一个孩子,就将该元素和那个孩子进行交换,而后反复进行下去,直到没法继续交换为止,整个过程就是沿着树的高度往下爬,因此时间复杂度也是O(logk)。

 

 

所以,不管是添加新的数据仍是取出堆顶的元素,都须要O(logk)的时间。

 

另一个最重要的时间复杂度是优先队列的初始化,这是分析运用优先队列性能时必不可少的,也是常常容易弄错的地方。

 

假设咱们有n个数据,咱们须要建立一个大小为n的堆,乍一看,每当把一个数据加入到堆里,咱们都要对其执行向上筛选的操做,这样以来就是O(nlogn)。可是,在建立这个堆的过程当中,二叉树的大小是从1逐渐增加到n的,因此整个算法的复杂度是:

 

       

      

 

通过进一步的推导,最终的结果是O(n)。算法面试中是不要求推导的,你只须要记住,初始化一个大小为n的堆,所须要的时间是O(n)便可。

 

注:

向上筛选,能够用这个静态图

                               

01例题分析

力扣(LeetCode)第347题. Top K Frequent Words 从一系列单词中找出使用频率最高的前K个单词

 

解题思路:这道题的输入是一个字符串数组,数组里的元素可能会重复一次甚至屡次,要求按顺序输出前K个出现次数最多的字符串。

 

当咱们拿到这个题目的时候,看到”前K个“这样的字眼就应该很天然地联想到运用优先队列。优先级别能够由字符串出现的次数来决定,出现的次数越多,优先级别越高,反之越低。

 

统计词频的最佳数据结构就是哈希表(Hash Map),利用一个哈希表,咱们就能快速的知道每一个单词出现的次数。

 

而后将单词和其出现的次数做为一个新的对象来构建一个优先队列,那么这个问题就很垂手可得地解决了。

 

解这类求前K个的题目,关键是看如何定义优先级以及优先队列中元素的数据结构。

                                  Desk (3)

                                    /    \

                             car(2)   book(1)          

2、图

 

1.图的基本知识点        

 

图是全部数据结构里面知识点最丰富的一个,最基本的知识点就有以下这些:

阶(Order)、度:出度(Out-Degree)、入度(In-Degree)

树(Tree)、森林(Forest)、环(Loop)

有向图(Directed Graph)、无向图(Undirected Graph)、彻底有向图、彻底无向图

连通图(Connected Graph)、连通份量(Connected Component)

存储和表达方式:邻接矩阵(Adjacency Matrix)、邻接链表(Adjacency List)

 

2.图的算法

 

围绕图的算法也是五花八门:

 

  • 图的遍历:深度优先、广度优先
  • 环的检测:有向图、无向图
  • 拓扑排序
  • 最短路径算法:Dijkstra、Bellman-Ford、Floyd Warshall
  • 连通性相关算法:Kosaraju、Tarjan、求解孤岛的数量、判断是否为树
  • 图的着色、旅行商问题等

以上的知识点只是图论里的冰山一角,对于算法面试而言,彻底不须要对每一个知识点都一一掌握,而应该有的放矢地进行准备。

 

3.关于图的热门考题

 

力扣(LeeCode)里边有许多关于图论的算法题,并且都是很是经典的题目,如下的知识点是必须充分掌握并反复练习的:

 

  • 图的存储和表达方式:邻接矩阵(Adjacency Matrix)、邻接链表(Adjacency List)
  • 图的遍历:深度优先、广度优先
  • 二部图的检测(Bipartite)、树的检测、环的检测:有向图、无向图
  • 拓扑排序
  • 联合-查找算法(Union-Find)
  • 最短路径:Dijkstra、Bellman-Ford

其中,环的检测、二部图的检测、树的检测以及拓扑排序都是基于图的遍历,尤为是深度优先方式的遍历。而遍历能够在邻接矩阵或者邻接链表上进行,因此掌握好图的遍历是重中之重!由于它是全部其余图论算法的基础。

 

至于最短路径算法,能区分它们的不一样特色,知道在什么状况下用哪一种算法就很好了。对于有充足时间准备的面试者,能熟练掌握它们的写法固然是最好的。

 

可经过一道例题来复习图论的知识。

 

02例题分析

 

力扣(LeetCode) 第785题. Is Graph Bipartite? 检测一个图是否为二部图

  

       

      

 

二部图就是在图里面,图的全部顶点能够分红两个子集U和V,子集里的顶点互不直接相连,图里面全部的边,一头连着子集U里的顶点,一头连着子集V里的顶点。

 

必需要对这个图进行一次遍历才能判断它是否为二部图。遍历的方法有深度优先以及广度优先。

 

基本的思想是,给图里的顶点涂上颜色。子集U里的顶点都涂上红色,子集V里的顶点都涂上蓝色。接着开始遍历这个图的全部顶点了,想象手里握有两种颜色(红色和蓝色)的画笔,每次都是交替地给遍历当中遇到的顶点涂上颜色,若是这个顶点尚未颜色,那就给它涂上颜色,而后换成另一支画笔,遇到下一个顶点的时候,若是发现这个顶点已经涂上了颜色,并且颜色跟我手里画笔的颜色不一样,那么表示这个顶点它既能在子集U里,也能在子集V里,因此,它不是一个二部图。

 

3、前缀树

 

1.前缀树的用法                    

 

前缀树也被称为字典树,由于这种数据结构被普遍地运用在字典查找当中。什么是字典查找?举例:给定一系列字符串,这些字符串构成了一种字典,要求你在这个字典当中找出全部以“ABC”开头的字符串。

 

对于这样的问题,常规想法是直接遍历一遍字典,而后逐个判断每一个字符串是否由“ABC”开头。假设字典很大,有N个单词,须要对比的不是“ABC”,而是任意的,不妨假设所要对比的开头平均长度为M,那么这种暴力的搜索算法,时间复杂度就是O(M*N)。

 

若是咱们用前缀树头帮助对字典的存储进行优化,就能够把搜索的时间复杂度降低为O(M),其中M表示字典里最长的那个单词的字符个数,在不少状况下,字典里的单词个数N是远远大于M的。所以,前缀树在这种场合中是很是高效的。

 

2.前缀树的经典应用

 

在网站上的搜索框中输入要搜索的文字时,搜索框会罗列出以搜索文字做为开头的相关搜索信息,这里就运用到了前缀树,在后端进行快速地检索。

 

另外,汉字拼音输入法,它的联想输出功能也运用到了前缀树。

 

3.前缀树的结构和性质

 

这里可经过一个例子来深刻理解它,假若有一个字典,字典里面有以下单词:"A","to","tea","ted","ten","i","in","inn",每一个单词还能有本身的一些权重值,那么用前缀树来构建这个字典将会是以下的样子:

 

       

       

 

前缀树有3个重要的性质:

 

a.每一个节点至少包含两个基本属性:

  • children:数组或者集合,罗列出每一个分支当中包含的全部字符;
  • isEnd: 布尔值,表示该节点是否为某字符串的结尾。

b.前缀树的根节点是空的,所谓空,也就是说咱们只利用到了这个节点的children属性,即咱们只关心在这个字典里,有哪些打头的字符。

c.除了根节点,其余全部节点都有多是单词的结尾,叶子节点必定都是单词的结尾。

 

4.前缀树的基础操做

 

前缀树最基本的操做就是两个:建立和搜索。

 

建立前缀树的方法很直观,遍历一遍输入的字符串,对每一个字符串的字符进行遍历,在遍历的过程当中,从前缀树的根节点开始,将每一个字符加入到节点的children字符集当中,若是字符集已经包含了这个字符,就能够跳过,若是当前字符是字符串的最后一个,那么就把当前节点的isEnd标记为真。

 

前缀树真正强大的地方在于,每一个节点还能用来保存额外的信息,好比能够用来记录拥有相同前缀的全部字符串。这样一来,当用户输入某个前缀时,就能在O(1)的时间内给出对应的推荐字符串。

 

建立好前缀树以后,搜索就会容易,方法相似,能够从前缀树的根节点出发,逐个匹配输入的前缀字符,若是遇到了就继续往下一层搜索,若是没遇到,就当即返回。

 

03例题分析

 

 LeetCode(力扣) 第212题:Word Search II 单词查找 II

 

这是一道出现较为频繁的难题,题目给出了一个二维的字符矩阵,而后给出了一个字典,如今要求在这个字符矩阵中找到出如今字典里的单词。若有以下的字符矩阵:

 

 

解题思路:因为字符矩阵的每一个点都能做为一个字符串的开头,因此必须尝试从矩阵中的全部字符出发,上下左右一步步地走,而后去和字典进行匹配,若是发现那些通过的字符能组成字典里的单词,就把它记录下来。

 

分别从矩阵的每一个字符出发,上下左右一步步地走,能够借用深度优先的算法。关于深度优先算法,若是你对它不熟悉,能够把它想象成走迷宫。

 

基本的算法有了,如何和字典匹配?直观的作法是每次都循环遍历字典,看看是否存在字典里面,若是把输入的字典变为哈希集合的话,彷佛只须要O(1)的时间就能完成匹配。

 

可是,这样的对比并不能进行前缀的对比,也就是说,必须每次都要进行一次全面的深度优先搜索,或者搜索的长度为字典里最长的字符串长度,这样仍是不够高效。假如在矩阵里遇到了一个字符”V”,而字典里根本就没有以“V”开头的字符串,那么根本就不须要将深度优先搜索进行下去,这样一来,能够大大地提升搜索效率。

 

刚才提到了对比字符串的前缀,这里须要借助前缀树来从新构建字典。构建好了前缀树以后,每次从矩阵里的某个字符出发进行搜索的时候,同步地对前缀树进行对比,若是发现字符一直能被找到,就继续进行下去,一步一步地匹配,直到在前缀树里发现一个完整的字符串,把它输出便可。

4、分段树

    

所谓分段树,就是一种按照二叉树的形式存储数据的结构,每一个节点保存的都是数组里某一段的总和。例如,数组是[1, 3, 5, 7, 9, 11],那么它的分段树就是:

 

       

      

 

由图能够看出,根节点保存的是从下标0到下标5的全部元素的总和,即36,它的左右两个子节点分别保存左右两半元素的总和,按照这样的逻辑不断地切分下去,最终的叶子节点保存的就是每一个元素的数值。

 

当更新数组里某个元素的数值时,须要从分段树的根节点出发,更新节点的数值,由于它保存的是数组元素的总和。接下来,修改的元素有可能会落在分段树里一些区间里,至少叶子节点是确定须要更新的,因此,须要从根节点往下,判断元素的下标是否在左边仍是右边,而后更新分支里的节点大小。所以,复杂度就是遍历树的高度,即O(logn)。

 

完成了元素数值的更新以后,要对数组某个区间段里的元素进行求和了。方法和更新操做相似,首先从跟节点出发,判断所求的区间是否落在节点所表明的区间中,若是所要求的区间彻底包含了节点所表明的区间,那么就得加上该节点的数值,意味着该节点所记录的区间总和只是咱们所要求解总和的一部分。接下来,不断地往下寻找其余的子区间,最终得出所要求的总和。

 

分段树的实如今书写起来有些繁琐,须要不断地练习才能加深印象。

 

04例题分析

LeetCode(力扣) 第315题:Count of Smaller Numbers After Self

 

题目看起来很是简单,给定一个数组nums,里面都是一些整数,如今要求打印输出一个新的数组counts,counts数组的每一个元素counts[i]表示nums中第i个元素右边有多少个数小于nums[i]。

 

例如:输入数组是[5, 2, 6, 1],应该输出的结果是[2, 1, 1, 0]。什么意思呢?好比对于5来讲,它的右边有两个数比它小,分别是2和1,因此输出的结果中,第一个元素是2,对于2来讲,右边只有1比它小,因此第二个元素是1,以此类推。

 

理解了问题以后,能够看下如何借用分段树来解决问题。既然是分段树,就须要想一想分段树的每一个节点应该须要包含什么样的信息。

 

对于这道题来讲,由于要统计的是比某个数还要小的数的总和,若是把分段的区间设计成按照数值的大小来划分,并记录下在这个区间中的数的总和的话,那么我就能快速地知道比当前数还要小的数有多少个了。

 

具体实现方式以下:

 

首先,从分段树的根节点开始,根节点记录的是数组里最小值到最大值之间的全部元素的总和,而后分割根节点成左区间和右区间,不断地分割下去,最后分段树成为这样的模样:

 

 

初始化的时候,每一个节点记录的在此区间内的元素数量是0,接下来从数组的最后一位开始往前遍历,每次遍历,判断这个数落在哪一个区间,那么那个区间的数量加一。

 

当遇到1的时候,把它加入到分段树里,此时分段树里各个节点所统计的数量会发生必定的变化:

 

 

同时可得当前所遇到的最小值就是1。

 

接下来看看6,当把6加入到分段树里时,分段树会发生以下变化:

在这个时候,须要求一下比6小的数有多少个?实际上是查询一下分段树,从1到5之间有多少个数。

 

须要从根节点开始查询。因为所要查询的区间是1到5,它没法包含根节点的区间1到6,因此继续往下查询。看左边,很明显,区间1到3被彻底包含在1到5之间,把该节点所统计好的数返回。再看来右边,区间1到5跟区间4到6有交叉,继续往下看,区间4到5彻底被包含在1到5之间,因此能够立刻返回,并把统计的数量相加。

 

最后我可得在当前位置,在6的右边比6小的数只有一个。

 

经过这样的方法,每次把当前的数用分段树进行个数统计,而后再计算出比它小的数便可。算法复杂度是O(nlogn)。

5、树状数组      

 

树状数组也被称为Binary Indexed Tree,一样的,为了了解树状数组,让咱们经过一个问题来好好理解一下这个数据结构吧。

 

假设咱们有一个数组array[0 … n-1], 里面有n个元素,如今咱们要常常对这个数组作两件事:

 

  • 更新数组元素的数值
  • 求数组前k个元素的总和(或者平均值)

 

乍一看,彷佛能够运用分段树来求解,的确,分段树是能够在O(logn)的时间里更新和求解前k个元素的总和。可是,仔细看这个问题,问题只要求求解前k个元素的总和,并不要求任意一个区间,在这种状况下,就能够借用树状数组了,它可让咱们一样在O(logn)的时间里完成上述的操做。并且,相对于分段树的实现,树状数组显得更简单。

 

树状数组的数据结构有如下几个重要的基本特征:

 

a.它是利用数组来表示多叉树的结构,在这一点上和优先队列有些相似,只不过,优先队列是用数组来表示彻底二叉树,而树状数组是多叉树。

b.树状数组的第一个元素是空节点。

c.若是节点tree[y]是tree[x]的父节点,那么须要知足以下条件:

y = x - (x & (-x))

 

因为树状数组所解决的问题跟分段树有些相似,因此不赘述了,力扣(LeetCode)上有不少经典的题目能够用树状数组来解决,好比力扣(LeetCode)308题,求一个动态变化的二维矩阵里,任意子矩阵里的数的总和。

内容摘取自《300分钟搞定算法面试》

第02讲:高级数据结构

主讲人:苏勇,谷歌资深技术工程师

相关文章
相关标签/搜索