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

文章目录

  • 一、优先级队列实现排序
    • 1. 排序实现
    • 2. 选择排序
      • 复杂度分析
      • 应用示例
    • 3. 插入排序
      • 复杂度分析
      • 应用示例
    • 4. 堆排序
      • 复杂度分析
  • 二、完整代码测试

  • 在文章【数据结构Python描述】优先级队列简介及Python手工实现中,对于需要保存的每一条优先级队列键值对形式记录,根据保存在位置列表中的记录是否按键大小进行排序,分别给出了具体实现类SortedPriorityQueueUnsortedPriorityQueue
  • 在文章【数据结构Python描述】树堆(heap)简介和Python手工实现及使用树堆实现优先级队列中,通过使用二叉堆来保存每一条优先级队列的键值对形式记录,给出了综合来看较上述两个实现类更高效的优先级队列实现类HeapPriorityQueue

本文您将看到,有趣的是:如果使用优先级队列实现排序,则根据具体实现类是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()

你可能感兴趣的:(#,数据结构,python,算法,选择排序,插入排序,堆排序)