算法-1B

续算法-1A后第二节课

贪心(2)-1

上次邓老师讲了一些基本数据结构和贪心思维,这次课主要还是贪心,但提出了很多新的算法及数据结构和解决方案。
算法-1B_第1张图片
这是二叉搜索树,在我们要在一个集合里面找到我们想要的那个元素,遍历一遍无疑是最简单的方案,但像我们之前说的,达到目的的方式办法有很多种,但我们择优选取好的算法,这才是一个程序员应该有的思维,很明显遍历一遍的复杂度是n,但用二叉搜索树的复杂度降到了log n。二叉搜索树的构成想法很简单,我们拿取一个元素放在根节点,然后拿取第二个,比之小的放在元素左边,大的放在右边,再拿取第三个,如此迭代就可以得到一棵二叉搜索树。例如图中我们寻找29,在根节点处看见20,29比20大,则在右边寻找,遇见38,左边,遇见25,右边,则找见了我们的29这个元素,这样的话我们三步就可以搜寻到我们要找到的那个元素。
算法-1B_第2张图片
这是一棵二叉搜索树,符合我们刚刚构造的原则,但我们发现,假如其中树的高度越深,我们寻找所需要的次数可能就越多,那有没有解决方案呢?明显有,那就是BBST平衡二叉搜索树,也就是下面这棵:
算法-1B_第3张图片
我们会发现,这棵平衡二叉搜索树比普通的二叉搜索树更加“好看”,而这种好看则就是我们缩短时间复杂度的关键所在,使树整体的深度越浅,我们搜索的时间越短,应该是log n级别的,这种平衡二叉搜索树不唯一,关键所在是最深的叶子节点和最浅的叶子节点,层数不超过一。
在这里插入图片描述
这是一张很有意思的图,也是一个很有趣的游戏,你可以理解为找小名,比如翼德我们需要找到对应的张飞,云长则是关羽,可以理解为是一种映射,这种关系在我们数据结构里面同样有一种数据结构与之对应,那就是散列(哈希)表。
算法-1B_第4张图片
这是一个很简单的哈希表演示的开始图,我们可以看见0到46号共47个元素,那么这些元素在散列表里有什么用呢?我们往下看
算法-1B_第5张图片
我们可以看见,我们把一个156号元素放进了15号元素,使之关联了起来,那么156为什么和15对应呢?眼尖的同学会发现,这是因为156%47=15.在这里我们用余数的关系,使之相对应。
算法-1B_第6张图片
更多的元素出现了,用余数的方法我们把他们和我们的47个元素对应起来,如果对应的元素不止一个,我们还可以用队列实现多个。这就是散列表的思想,这样的话,比如我们要找156号元素,只要找到与之相对应的15号元素就可以找到我们的156号元素,这样相对应的方法是不是比遍历一遍更加方便呢?而且散列表的一个奥妙在于,我们的除数往往是一个质数,例如这里的47,这里面是很有学问的。
算法-1B_第7张图片
像python里面就有字典,思想和我们的散列表一样,相对应的关系。还有一些语言里面的map,也是这样实现的。比较关键的是,这里的M,通常是质数,比如47和这里的90001!
算法-1B_第8张图片
另一个比较重要的数据结构是优先级队列,我们可以把它想象成一个三角形的树,不过是倒过来的,树的根节点往往是树上所有元素里面最小或最大的那个,这样的话,假如我们在一个集合里面想得到最小的那个,遍历一遍用一般方法的话,是n的复杂度,而优先级队列只要log n。
算法-1B_第9张图片
用个我们熟悉的例子,上节课中的哈夫曼树。在构造我们的哈夫曼树时,我们总需要得到两个最小的元素,构成一个新的元素,遍历一遍找到最小的两个,复杂度为n,需要n-1次,总复杂度为n的平方,而用优先级队列实现的话,找到最小的,只要log n,循环n-1次,那么总复杂度为nlog n。在这里选用优先级队列,是极为方便的,而且c++的库里面有已经封装好的优先级队列,可以直接拿来用。
算法-1B_第10张图片
在这里,邓老师拓展了一下,构造哈夫曼树不用优先级队列也可以达到n
log n的方法,听完后让我直呼神奇。这里,邓老师用了一个栈和队列两种数据结构。首先我们用nlog n的排序方法把各元素排好序最大的最先进栈的方式得到一个顺序栈,栈顶最小,最开始我们需要的两个最小的,肯定就是栈顶的两个元素,拿出来合成后放进空的队列里面,这时候第二次循环,想得到最小的两个,我们肯定考虑的还是栈顶的两个,但还有一种可能,就是这时候队列里的那个新元素,可能在所有元素里面,也可排到前二小,这时候做个简单的大小比较就好,时间是常数的,再往下有两种情况(1)最小的两个还是栈顶两个,我们合成再放进队列,这时候注意,队列里,队前面的肯定小于队后面的,然后再找两个最小的,这时候我们只要盯住栈顶的两个元素和队列最前面两个元素就可(因为我们只要盯着这四个元素,目光里只有这四个,而不考虑其它,这不就是鼠目寸光的嘛?局部的,看起来是愚蠢的,也就是我们的贪心算法,但贪心在一些情况下往往能起到大用处,比如这种情况下在我们想算法时,最开始就应该尝试贪心的方法。),最小的两个元素一定在这四个中间,只要比较大小就好,同样复杂度也是常数的。(2)最小的两个一个是栈顶,另一个是队列里的那个元素,那我们把他们拿出来合成新元素再放进队列里面,这时候又回到了再往下有两种情况这里,继续迭代操作,最后便可得到我们想要的哈夫曼树,时间复杂度是nlog n(排序所用的复杂度)+(n-1)c(n-1次循环的复杂度),这个式子我们就可以看成nlog n。所以这样我们也用nlog n的复杂度得到了哈夫曼树,这种方式甚是神奇!
算法-1B_第11张图片
随后就是最小生成(支撑)树,就是在我们n个元素里,有很多元素与元素之间的连线并且有权重,如何在其中挑选n-1跟连线使所有的点能够联通无环并且总权重最小,得到最小生成树,这是一个麻烦的问题,当然最小生成树可能不是唯一的。
算法-1B_第12张图片
我们有两种极为经典的方法,Prim算法和Kruskal算法。首先我们来看Prim算法,Prim算法的精髓是,所有元素看成集合V,假如我们把这些元素分为两边,U和V\U,两部分元素间有很多连线,这些连线里面最短(权重最小的)的那根一定是我们的最小生成树里面需要的一根。证明方法很简单,两部分元素,我们最终需要一个完整的树,所以这两部分中间一定有连线,我们用反证法,假如不用最小的那一根,走最后的生成树里面,我们把不是最小的那根换成最小的那根,因为它们的作用是相同的,连接两部分元素,所以作用上不会影响,但我们得到了一个总权重更小的最小支撑树,这和我们的本意不符,所以说一定会采用最小的那根。这便是Prim思想。

贪心(2)-2

算法-1B_第13张图片
例如这个图中有8个元素,及很多的连线,我们需要在其中找到7根连线连接这8个元素,并且这个7根连线的总权重是在可行方案里面最小的那7根。
算法-1B_第14张图片
首先我们分为两部分,A和不含A的部分,我们可以看见两部分间有三根连线,权重分别为4,6,7,按照我们上面的Prim证明,我们选用4这根线。

算法-1B_第15张图片
随后我们用了4把A,B连在了一起,这时候分为A,B和没有A,B的两部分,这两部分中最小的是6,因此我们选用6把D也拉进来。这时候分为A,B,D和没有A,B,D的两部分,也就是上面这幅图,其中我们发现2最小,选用2把G拉进来。
算法-1B_第16张图片
如此循环往复,最终得到我们的7根线,连接八个点,得到我们的最小支撑树。那我们来看看用矩阵储存的方式如何来实现吧。
算法-1B_第17张图片
最开始我们可以看见0到16号元素,也就是17个点,也就意味着我们要选用16条边,矩阵中的数字表示把列和行两个元素可以连接起来,并且权重大小就是数字大小。首先分为0号元素和非0号元素两部分,能和0连接的边里面9最小,所以选用9,连接0和15。粉红色说明是两部分之间的跨边,大红色是最优的选择边,蓝色的边是另一部分里的内部边。矩阵为什么只有一半呢?因为是无相边,另一半是对称的,所以一半就可以展示我们的效果了。
算法-1B_第18张图片
如此往复,但要注意,在执行的过程中,可能会出现灰色的边,这就说明是到了这部分来了的两个点间的连线,哪怕这种连线很小,但也不能选用,因为既然在一边了,说明肯定有连线,再选用必然会出现环,这和我们最小生成树的定义是不符合的。这个很容易搞错,博主在理解时一度没想明白,直到博主自己画了一个图:
算法-1B_第19张图片
这个图中也是类似的实现的,在采用3,和4后,0,1,2三个元素都已经在一起,那么我们往下选最小的,应该是5最小,但并不能选5,因为5连接的两个点,已经在一起了,这时候的5就对应上面图中灰色的部分。
算法-1B_第20张图片
最终得到我们的16条连线,生成我们的最小生成树!这便是Prim算法的矩阵实现过程,在我们写代码时可以用矩阵数组来实现。
算法-1B_第21张图片
但聪明的人肯定知道,一般这种二维数组,往往是复杂度很高的,并不是什么好的解决方案,我们的过程是每次得到符合条件的集合里面最小的那条连线,那这时候你会不会想到我们上面说优先级队列呢?
算法-1B_第22张图片
如果采用优先级队列,是一种很好的方法,每次得到最小的是log n的复杂度,一共n-1次,也就是n*log n的复杂度,这是极好的。但要注意的是,每有一条边被拉上去,我们的优先级队列就应该更新,被拉上之后的两部分间的连线组成新的优先级队列。
算法-1B_第23张图片
Prim算法讲完之后,让我们来看看我们的Kruskal算法来解决最小支撑树问题!
算法-1B_第24张图片
Kruskal算法的思想和Prim有点像,如果你能理解Prim算法,那么理解Kruskal起来也特别方便。Kruskal指出,我们把所有边都拿出来排个序,从小到大排序,然后我们不是要使权重最小嘛?那么最小的那条连线我们必选,它连接了两个点,第二小的连线我们必选,因为它也连接了两个点,那么第三小的边我们必不必选呢?答案是不。为什么呢?联想到我上面讲的灰色数字,你应该能想到,如果这时候三条边组成了一个环怎么办?这时候哪怕第三小的边很小,也不能选,也就是我画的那个图里面的权重为5那条边。那为什么前两个可以必选呢?答案很简单,三边才成环嘛,然后后面的每一条边的是否选择,我们都要判断,它连接的两个点,是不是已经在一起了!最终我们选出n-1条边,这就是Kruskal算法的思想。

贪心(2)-3

算法-1B_第25张图片
我们接着用图来看Kruskal算法,图的左边是17个在对角线上的方块,表示17个点,而且每个都是独立的,图的右边是点与点之间的边了。右边先选出全局最小的4,变成大红色,左边表示由权重为4的连线连接起来的8和12两个点。
算法-1B_第26张图片
下一步点8和点12在一起了,变成了同一列(表示已经连接起来了),我们再找第二小的连线,又是一个大红色的4,左边是由权重为4的连线连接起来的0和14两个点(也变成大红色表示要连起来了)。
算法-1B_第27张图片
过程就是如此,一直依次选出最小的,但一定用那个最小的吗?这可不一定,比如上图中明显左边5条绿线是一部分,右边三条绿线也已经连成了一部分,接下来最小的是粉红色的线,但我们并不采用,采用比红还要大一些的蓝色的线,这是因为红色的线构成了环,并不能被我们采用,所以选用除了红线外,符合条件的最小的线,也就是蓝线,并连接起来。
算法-1B_第28张图片
如此继续重复操作,右边灰色的表示是权重比较小的线但并不采用,因为是可以使之前的连接的点与点之间构成环。
算法-1B_第29张图片
最终我们得到右边一列,也就是表示已经17个点都连接起来了。
算法-1B_第30张图片
在用Kruskal算法时,我们会遇见一个很现实的问题,就是一直困扰我们的那个灰色的连线,在kruskal算法中,我们遇见灰色点是,你咋知道它是个一个灰色的点而不采用,有同学会说,那就看它连接的两个点是不是在一起了,也就是是否已经连接起来了,是一个部分。那么我们人好判断,但计算机怎么知道我们的两个点是不是同一部分呢?这是我们就推出并查集的概念,并查集可以很好的解决这个问题。就是我们的每一个部分选一个代表,类似于每个班有一个班长,你要想知道某两个同学A和B(点)是不是一个班(已经连接在一起的),那就看它们的班长是不是同一个人,如果A说我班长是C,B说我班长也是C那么这不就能说明他们是同一个班吗?上图中,寻找1号元素,我们得到,它班长是8,查4号元素,8号元素皆是如此。
算法-1B_第31张图片
如图,合并4和7号元素,但合并后,两个元素是一个班级了,他们头上挂的班长名字只能是一个人,那么右图中,四号头上的帽子变成了4。
算法-1B_第32张图片
再慢慢合并,这个过程中,合并两个部分的话,其实只要两个部分中任一元素和另一个元素中任一元素合并,就可以达到两个部分合并的效果,如图合并6和1的效果和合并5和4的效果是一样的。
算法-1B_第33张图片
合并过程中,如果用数组存储一个一个的元素的话,如果需要合并加入新元素,那是相当麻烦的,因为第一节课里我们就讲过,数字的特点是易查询,难增删。这时候不如采用链表来存储。
算法-1B_第34张图片
一个元素最开始的班长是自己,在合并的时候,有一个会改变,如上图中8号元素和11号元素一样,11把自己的头上帽子改变了,变成了8,表示要找到自己的最终班长,先找到8,找到8后发现8头上的帽子也是8,当一个元素自己头上的帽子就是自己后,说明这个元素就是这个集体的班长,这也是我们判断一个元素是不是班长的依据。
算法-1B_第35张图片
如图,合并第一和第三这两个集体时,只要把一个集体的班长头上的帽子改掉,把帽子换成另一个班的班长,表示他已经不是班长,他也属于另一个班了,同样,它下面的元素们,通过这个“曾经”的班长,最终都能找到新的班长,也就是两个集合合并后的效果,只有一个班长。
算法-1B_第36张图片
但这时候又会出现一个问题,在判断两个元素是不是一个集体,也就是是不是一个班时,需要查询他们的班长是不是同一个,在查询班长时,如果该元素上面的人太多,找到最终班长是一个很麻烦的事,时间复杂度将变得很高,那有解决方案吗?有的,这里提出两个优化,一是在每次合并两个集体时,总让小集体加入大集体,因为这样可以使链表组成的树的深度不再增加,树的深度是查询需要时间的指标。用数组查询的复杂度为1,但合并,一个集体完全加入到另一个集体(数组)中的复杂度为n,总复杂度为n。用链表的话,查询的复杂度为log n,合并最多也是log n。这是合并很快,总复杂度也很快。
算法-1B_第37张图片
第二个技巧就是路径压缩在查询班长时,查询很麻烦,但是我们查询到了之后,把这一路上所有元素的帽子都换成最终的班长,这样下次在查询时,之间可以查到最终的班长,这种方式是非常快的。
算法-1B_第38张图片
如图这样,从右边到左边,左边图中,大家之间知道自己班长是谁并指向他,下次查询时将特别方便,这是路径压缩的方法。可以设置反向引用,或者借助递归的方法达到该目的。

结尾

这次课学习了:二叉搜索树,散列表,优先级队列,最小生成树,Prim算法,Kruskal算法,并查集,及并查集优化的概念。希望博主好好掌握,万事开头难,一定要坚持并努力下去吖~

你可能感兴趣的:(算法,数据结构)