贪心算法-Dijkstra优化

引言

上一篇Dijkstra算法伪代码第7行:在每次刷新d(v)之后,找出最小的d(v),这个地方具体用什么方法是没有说的,不同的找最小的方法和所涉及的数据结构的时间复杂度也是不一样的。
最直接暴力的思路就是一个for循环遍历找出最小的d(v),但时间复杂度太高,需要遍历所有点,总的时间复杂度就是 O ( n 2 ) O(n^2) O(n2),点很多的时候,这个算法就很慢了。
因此,我们需要在如何找最小值这进行优化。

Dijkstra优化

我们来看一下存储d(v)的数据结构需要支持什么。在Dijkstra算法中,需要支持可以insert一个值、extract最小值、upadte一个值、delete一个值,而且需要动态调整变化的大小顺序。

优先队列(Priority Queue,PQ)可以满足上面的需求。

常见的优先队列应用场景:
Dijkstra’s shortest path algorithm
Prim’s MST algorithm
Huffman coding
A∗ searching algorithm
HeapSort

采用优先队列来存储d(v)值,即PQ集合(优先级就是node对应的d(v)值,后面统一用key(v)),一开始构造队列的插入操作要执行n次,获取最小key(v)在循环中要n次,更新队列中key(v)要m次(m是边的数量),更新了key(v)后可能需要调整队列。(ExtractMin包含取最小值和从队列中删除两个操作)

伪代码如下:
在这里插入图片描述
通过分析,我们发现主要影响该算法时间复杂度的是队列的三个操作Insert,ExtractMin,DecreaseKey,如果我们构成优先队列用的是普通的无序数组或链表,那么每次Insert的复杂度就只有O(1),但ExtractMin就要O(n)(每次都要遍历数组、链表),如果是有序数组或链表,Insert需要O(n)(遍历查找合适插入位置),ExtractMin只用O(1)。所以选择不同的数据结构构造队列,会导致算法时间复杂度不同。下表列出了四种实现队列的不同数据结构方式:
在这里插入图片描述

一般实际问题中边的数量m>点的数量n

最小堆(二叉堆)

最小堆就是任意一个节点的值总是小于等于其孩子节点的值,且堆必须是完全二叉树。实现可以通过链表或数组两种方式。

我们重点看看Dijkstra算法涉及的三个操作,为什么最小堆可以降低这三个操作时间复杂度?

  • Insert:向堆中插入元素包括2个步骤,第一是插入(将元素接到链表末尾或数组末尾),第二是维护(调整元素位置形成新堆)。调整的过程可以发现与对比交换的次数有关,新插入的节点若比父节点大,则无需交换,若比父节点小,则交换两者位置,过程中只需与父节点比较,因为如果小于父节点,肯定小于另一个子节点,一直对比,最多对比logn次,所以Insert的复杂度是O(logn)。

举例:Insert(7)
在这里插入图片描述

  • ExtractMin:前面说了这里包含两个步骤:一是返回堆顶(即最小元素),二是将堆顶元素删除(加入到已遍历点集合中),直接返回堆顶的复杂度是O(1),重点是删除堆顶元素之后要维护堆耗费的时间。如果我们直接删除堆顶元素,再从其左右子节点中挑选较小者作为父节点,最后可能导致堆不成形,意味着我们要重新构造一次堆,但是如果我们先交换堆顶和尾部元素,再删除,则可以保持堆。

举例:ExtractMin()
在这里插入图片描述

  • DecreaseKey:更新Key值,对比某一节点,如果更新了其Key值,则同样类似插入操作,与父节点对比,时间复杂度依然是O(logn)。

举例:DecreaseKey( ptr, 7 )
在这里插入图片描述

合起来,利用最小堆实现优先队列的时间复杂度是:
O ( n ∗ l o g n ( n 次 I n s e r t ) + n ∗ l o g n ( n 次 E x t r a c t M i n ) + m ∗ l o g n ( m 次 D e c r e a s e K e y ) ) = O ( m l o g n ) O(n*logn(n次Insert) + n*logn(n次ExtractMin)+m*logn(m次DecreaseKey))= O(mlogn) O(nlogn(nInsert)+nlogn(nExtractMin)+mlogn(mDecreaseKey))=O(mlogn)

二项堆(二项树)

二项堆在二叉堆的基础上优化了合并堆耗费的时间(从O(n)到O(logn))。
二叉堆采用单棵树的方式存储,而在二项堆中采用多棵树表示。

二项树的定义:

  • 单个节点可以作为一个二项堆
  • 一个二项堆必由两个大小相同的二项树按照规则(将树根小的接在另一棵树的根节点下)

举例:下图是多颗二项树,看起来长得很歪,但其实长得很正。我们按顺序,每棵分层看,就是二项式、杨辉三角,这也就是为什么叫做二项树的原因。
1
1 1
1 2 1
1 3 3 1

在这里插入图片描述
而二项堆由多个二项树构成(森林),将只含一个节点的二项树称作 B 0 B_0 B0 ,将两个 B 0 B_0 B0组合成的二项树称作 B 1 B_1 B1,以此类推。把每棵二项树的根节点用链表连起来就是二项堆。(B就是级别)

一个堆中不能有两个一样级别的树,如果一个堆中出现两棵级别一样的树,就将它们合并。因此一个二项堆中若容纳n棵树,合并过后最多只会有 l o g 2 n + 1 log_2 n+1 log2n+1棵树,最高树的高度是 l o g 2 n log_2 n log2n

我们简单分析一下二项堆时间复杂度:

  • 合并(Union)
    • 没有大小相同的树:直接放在一起
    • 有大小相同的树:将大小相同的合并,直到没有大小相同为止,两个二项树变成一个耗费O(1),合并森林依据树的总数量不超过O(logn)
  • 插入(Insert)
    • 若原先二项堆中无 B 0 B_0 B0,直接放入
    • 若有,两个 B 0 B_0 B0构成 B 1 B_1 B1 ,如果还有就不断合并,耗费O(logn)
  • 取出最小(ExtractMin)
    • 先获取最小,是从不超过logn棵树中对比根节点,O(logn),取走根后,一棵二项树拆成两棵二项树,可能会与其他合并,合并还是O(logn),总时间O(logn)

可以发现,二项堆相比二叉堆,降低了Union操作的时间复杂度。

二项堆Insert和Union的时间是logn,事实上,我们还可以进一步确定其时间复杂度可以到达O(1)。
我们来看个例子,当上一次发生多次合并耗费O(logn)的时间之后,下一次只耗费O(1)的时间。
在这里插入图片描述
假设某一次耗费O(logn)了,在这之后不断插入元素的时间消耗,显然有O(1),O(2),O(1),O(3),O(1)…我们发现,在很长一段时间内,插入耗费的时间都是常数阶的。因此,我们不能单一地根据一次时间消耗是O(logn)而断言时间复杂度是O(logn),需要考虑一串操作的整体期望时间复杂度。通过均摊分析可以证明是O(1)。

总结

谈到了优先队列,引入了两个新的数据结构:二叉堆与二项堆,它们的设计是我们在算法设计过程中的另一种优化,从数据结构上优化算法。

  • 二叉堆
    可以快速获取队列最小值,且拥有快速的插入和删除速度
  • 二项堆
    在二叉堆的基础上,降低了堆合并和插入操作耗费的时间

我们从一开始使用线性链表实现优先队列,发现求最小太慢,改为有序链表,发现插入太慢,然后变成部分有序,采用二叉堆,然后从只用一颗树变成多棵树的二项堆,逐步放松了限制条件,减少了许多冗余计算,优化了问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值