【常见算法Python描述】优先级队列应用之实现选择排序、插入排序和堆排序

本文您将看到,有趣的是:如果使用优先级队列实现排序,则根据具体实现类是UnsortedPriorityQueueSortedPriorityQueue、还是HeapPriorityQueue,则该排序实现将分别天然地是选择排序插入排序以及堆排序

一、优先级队列实现排序

1. 排序实现

对于包含的对象元素均可比较(即对象实现了__lt__()__gt__()等方法中的至少一个,用于重载<>等运算符)的集合collection2sort,下面提供可将集合collection2sort中的元素按非降序(非严格单调递增)排列后得到的序列。

下面给出的排序实现十分简单:

  • 首先将集合中的元素挨个插入初始为空的优先级队列;
  • 然后重复调用优先级队列的remove_min()方法以非降序的方式获取每个元素。
from positional_list import PositionalList
from priority_queue import UnsortedPriorityQueue, SortedPriorityQueue
from heap_priority_queue import HeapPriorityQueue


def priority_queue_sort(collection2sort: PositionalList, sort_algorithm):
    """
    使用优先级队列实现选择排序或插入排序
    :param collection2sort: 待排序集合
    :param sort_algorithm: 指定使用选择排序或插入排序,仅可以字符串形式指定为'selection'或'insertion'
    :return: None
    """
    length = len(collection2sort)
    if sort_algorithm == 'selection':
        queue = UnsortedPriorityQueue()
    elif sort_algorithm == 'insertion':
        queue = SortedPriorityQueue()
    elif sort_algorithm == 'heapsort':
        queue = HeapPriorityQueue()
    else:
        raise ValueError('请指定正确的排序算法!')
    for _ in range(length):  # 排序第一阶段:添加待排序集合的元素至优先级队列
        element = collection2sort.delete(collection2sort.first())
        queue.add(element, element)  # 将待排序集合collection2sort中的每个元素既当做键也当做值添加至优先级队列
    for _ in range(length):  # 排序第二阶段:利用优先级队列性质对集合进行排序
        key, value = queue.remove_min()  # 依次获得优先级队列中剩下的最小元素
        collection2sort.add_last(value)

上述实现中,需要注意的是:

  • 待排序集合collection2sort是一个位置列表,其典型特征是对任意给定位置进行增(如:add_last()删(delete()操作的最坏时间复杂度都是 O ( 1 ) O(1) O(1)
  • 由于优先级队列的每一条记录都需要是键值对形式,因此在调用其add(k, v)方法时将待排序集合的元素既当作键也当做值。

2. 选择排序

对于上述实现的priority_queue_sort(),如果使用时指定sort_algorithm参数为'selection',则queue引用UnsortedPriorityQueue的实例对象,由【数据结构Python描述】优先级队列简介及Python手工实现中的分析可知:

  • 第一阶段:循环每次迭代的最坏时间复杂度都是 O ( 1 ) O(1) O(1)
  • 第二阶段:循环每次迭代的最坏时间复杂度和当前优先级队列的长度呈正比。

由上述分析可知,此时priority_queue_sort()的效率瓶颈在于重复选择优先级队列中键最小的记录。因此,这时priority_queue_sort()所实现的算法通常被称为选择排序

复杂度分析

对于选择排序的最坏时间复杂度,瓶颈在于第二阶段重复调用remove_min()方法,由于优先级队列的长度在每次调用remove_min()后减1直到为0,因此循环的第一次迭代的最坏时间复杂度为 ( n ) (n) (n),第二次为 O ( n − 1 ) O(n-1) O(n1)等等,因此循环的最坏总时间复杂度为:
O ( n + ( n − 1 ) + ⋅ ⋅ ⋅ + 2 + 1 ) = O ( ∑ i = 1 n i ) = O ( n ( n + 1 ) / 2 ) O(n+(n-1)+\cdot\cdot\cdot+2+1)=O(\sum\nolimits_{i=1}^{n}{i})=O(n(n+1)/2) O(n+(n1)++2+1)=O(i=1ni)=O(n(n+1)/2)

因此,第二阶段的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),则算法整体的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)

应用示例

关于选择排序的应用,下表显示了当待排序集合为(7, 4, 8, 2, 5, 3)时,完成排序的每一个步骤及结果:

待排序集合优先级队列
输入(7, 4, 8, 2, 5, 3)( )
第一阶段(a)(4, 8, 2, 5, 3)(7)
(b)(8, 2, 5, 3)(7, 4)
(c)(2, 5, 3)(7, 4, 8)
(d)(5, 3)(7, 4, 8, 2)
(e)(3)(7, 4, 8, 2, 5)
(f)( )(7, 4, 8, 2, 5, 3)
第二阶段(a)(2)(7, 4, 8, 5, 3)
(b)(2, 3)(7, 4, 8, 5)
(c)(2, 3, 4)(7, 8, 5)
(d)(2, 3, 4, 5)(7, 8)
(e)(2, 3, 4, 5, 7)(8)
(f)(2, 3, 4, 5, 7, 8)( )

3. 插入排序

对于上述实现的priority_queue_sort(),如果使用时指定sort_algorithm参数为'insertion',则queue引用SortedPriorityQueue的实例对象,由【数据结构Python描述】优先级队列简介及Python手工实现中的分析可知:

  • 第一阶段:循环每次迭代的最坏时间复杂度和调用add(k, v)方法前优先级队列的长度呈正比;
  • 第二阶段:循环每次迭代的最坏时间复杂度都是 O ( 1 ) O(1) O(1),即第二阶段循环的总最坏时间复杂度为 O ( n ) O(n) O(n)

实际上,因为SortedPriorityQueueadd(k, v)方法的实现插入排序的实现基本一致,因此此时priority_queue_sort()通常被称为插入排序

复杂度分析

由上述分析可知,当使用的优先级队列为SortedPriorityQueue,排序算法priority_queue_sort()的效率瓶颈在第一阶段每次调用add(k, v)。因此,类似地,第一阶段总最坏时间复杂度为:

O ( 1 + 2 + ⋅ ⋅ ⋅ + ( n − 1 ) + n ) = O ( ∑ i = 1 n i ) = O ( n ( n + 1 ) / 2 ) O(1+2+\cdot\cdot\cdot+(n-1)+n)=O(\sum\nolimits_{i=1}^{n}{i})=O(n(n+1)/2) O(1+2++(n1)+n)=O(i=1ni)=O(n(n+1)/2)

因此,算法整体的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),然而需要注意的是,不同于选择排序插入排序最好时间复杂度为 O ( n ) O(n) O(n)(此时待排序集合基本为非降序排列),但是选择排序因为总是要遍历所有剩下的未排序元素,因此其时间复杂度总是 O ( n 2 ) O(n^2) O(n2)

应用示例

关于插入排序的应用,下表显示了当待排序集合为(7, 4, 8, 2, 5, 3)时,完成排序的每一个步骤及结果:

待排序集合优先级队列
输入(7, 4, 8, 2, 5, 3)( )
第一阶段(a)(4, 8, 2, 5, 3)(7)
(b)(8, 2, 5, 3)(4, 7)
(c)(2, 5, 3)(4, 7, 8)
(d)(5, 3)(2, 4, 7, 8)
(e)(3)(2, 4, 5, 7, 8)
(f)( )(2, 3, 4, 5, 7, 8)
第二阶段(a)(2)(3, 4, 5, 7, 8)
(b)(2, 3)(4, 5, 7, 8)
(c)(2, 3, 4)(5, 7, 8)
(d)(2, 3, 4, 5)(7, 8)
(e)(2, 3, 4, 5, 7)(8)
(f)(2, 3, 4, 5, 7, 8)( )

4. 堆排序

针对基于堆实现的优先级队列各方法的复杂度分析,使用二叉堆来实现优先级队列,最大优点是其ADT方法的最坏时间复杂度均为 l o g ( n ) log(n) log(n)或更高效。

因此基于二叉堆实现的优先级队列如HeapPriorityQueue非常适合对优先级队列所有方法效率要求较高的场合,如上述分两个阶段(第一阶段依赖优先级队列的add(k, v)方法,第二阶段依赖于remove_min()方法)的priority_queue_sort算法。

复杂度分析

若使用HeapPriorityQueue来实现priority_queue_sort,则:

  • 第一阶段由之前讨论可知,因为第 i − 1 i-1 i1次调用add(k, v)后优先级队列中已有 i i i条记录,所以第 i i i次调用add(k, v)操作的最坏时间复杂度为 l o g ( i ) log(i) log(i),因此该阶段总的最坏时间复杂度为:
    l o g ( 1 ) + l o g ( 2 ) + ⋅ ⋅ ⋅ + l o g ( n − 1 ) + l o g ( n ) = l o g ( n ! ) log(1)+log(2)+\cdot\cdot\cdot+log(n-1)+log(n)=log(n!) log(1)+log(2)++log(n1)+log(n)=log(n!)
    而:
    l o g ( n ! ) < l o g ( n n ) = n l o g ( n ) log(n!)\lt{log(n^n)}=nlog(n) log(n!)<log(nn)=nlog(n)
    即第一阶段的最坏时间复杂度小于 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))
  • 第二阶段由之前讨论可知,因为第 j − 1 j-1 j1次调用remove_min()后优先级队列中还有 n − j + 1 n-j+1 nj+1条记录,所以第 j j j次调用remove_mn()操作时的最坏时间复杂度为 l o g ( n − j + 1 ) log(n-j+1) log(nj+1),因此该阶段总的最坏时间复杂度为:
    l o g ( n ) + l o g ( n − 1 ) + ⋅ ⋅ ⋅ + l o g ( 2 ) + l o g ( 1 ) = l o g ( n ! ) log(n)+log(n-1)+\cdot\cdot\cdot+log(2)+log(1)=log(n!) log(n)+log(n1)++log(2)+log(1)=log(n!)
    因此,类似地,第二阶段的最坏时间复杂度小于 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)),故综合来看堆排序的最坏时间复杂度小于 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))

二、完整代码测试

下面是给定相同的待排序集合后,分别测试选择排序、插入排序和堆排序的测试代码和结果:

from positional_list import PositionalList
from priority_queue import UnsortedPriorityQueue, SortedPriorityQueue
from heap_priority_queue import HeapPriorityQueue
from random import randint
from copy import deepcopy
from timeit import timeit


def priority_queue_sort(collection2sort: PositionalList, sort_algorithm):
    """
    使用优先级队列实现选择排序或插入排序
    :param collection2sort: 待排序集合
    :param sort_algorithm: 指定使用选择排序或插入排序,仅可以字符串形式指定为'selection'或'insertion'
    :return: None
    """
    length = len(collection2sort)
    if sort_algorithm == 'selection':
        queue = UnsortedPriorityQueue()
    elif sort_algorithm == 'insertion':
        queue = SortedPriorityQueue()
    elif sort_algorithm == 'heapsort':
        queue = HeapPriorityQueue()
    else:
        raise ValueError('请指定正确的排序算法!')
    for _ in range(length):  # 排序第一阶段:添加待排序集合的元素至优先级队列
        element = collection2sort.delete(collection2sort.first())
        queue.add(element, element)  # 将待排序集合collection2sort中的每个元素既当做键也当做值添加至优先级队列
    for _ in range(length):  # 排序第二阶段:利用优先级队列性质对集合进行排序
        key, value = queue.remove_min()  # 依次获得优先级队列中剩下的最小元素
        collection2sort.add_last(value)


collection2sort1 = PositionalList()
for i in range(10):
    collection2sort1.add_last(randint(0, 10))
print('待排序集合collection2sort1 = ', collection2sort1)  # 待排序集合collection2sort1 =  [2, 10, 2, 3, 8, 0, 9, 6, 2, 9]

# 将待排序集合进行深拷贝,确保执行三种排序的输入是一致的
collection2sort2 = deepcopy(collection2sort1)
collection2sort3 = deepcopy(collection2sort1)

sort_algorithm1 = 'selection'
sort_algorithm2 = 'insertion'
sort_algorithm3 = 'heapsort'


def main():
    # 测试选择排序执行
    print('排序前collection2sort1 = ', collection2sort1)  # 排序前collection2sort1 =  [2, 10, 2, 3, 8, 0, 9, 6, 2, 9]
    print('选择排序所用时间为:', timeit(stmt='priority_queue_sort(collection2sort1, sort_algorithm1)',
                               setup='from __main__ import priority_queue_sort,'
                                     'collection2sort1,'
                                     'sort_algorithm1',
                               number=10000))  # 选择排序所用时间为: 1.2219319
    print('排序后collection2sort1 = ', collection2sort1)  # 排序后collection2sort1 =  [0, 2, 2, 2, 3, 6, 8, 9, 9, 10]

    # 测试插入排序执行
    print('排序前collection2sort2 = ', collection2sort2)  # 排序前collection2sort2 =  [2, 10, 2, 3, 8, 0, 9, 6, 2, 9]
    print('插入排序所用时间为:', timeit(stmt='priority_queue_sort(collection2sort2, sort_algorithm2)',
                               setup='from __main__ import priority_queue_sort,'
                                     'collection2sort2,'
                                     'sort_algorithm2',
                               number=10000))  # 插入排序所用时间为: 0.7265602999999998
    print('排序后collection2sort2 = ', collection2sort2)  # 排序后collection2sort2 =  [0, 2, 2, 2, 3, 6, 8, 9, 9, 10]

    # 测试堆排序执行
    print('排序前collection2sort3 = ', collection2sort3)  # 排序前collection2sort3 =  [2, 10, 2, 3, 8, 0, 9, 6, 2, 9]
    print('堆排序所用时间为:', timeit(stmt='priority_queue_sort(collection2sort3, sort_algorithm3)',
                              setup='from __main__ import priority_queue_sort,'
                                    'collection2sort3,'
                                    'sort_algorithm3',
                              number=10000))  # 堆排序所用时间为: 0.6814350999999998
    print('排序后collection2sort3 = ', collection2sort3)  # 排序后collection2sort3 =  [0, 2, 2, 2, 3, 6, 8, 9, 9, 10]


if __name__ == '__main__':
    main()

©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页