• 算法——列表排序和常用排序算法


    一、列表排序

      排序就是将一组“无序”的记录序列调整为“有序”的记录序列。

      列表排序:将无序列表变为有序列表。

        输入:列表

        输出:有序列表

      两种基本的排序方式:升序降序

      python内置的排序函数:sort()。

    二、常见排序算法  

    名称

    复杂度

    说明

    备注

    冒泡排序
    Bubble Sort

    O(N*N)

    将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮

    插入排序

    Insertion sort

    O(N*N)

    逐一取出元素,在已经排序的元素序列中从后向前扫描,放到适当的位置

    起初,已经排序的元素序列为空

    选择排序

    O(N*N)

    首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此递归。

    快速排序

    Quick Sort

    O(n *log2(n))

    先选择中间值,然后把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使用这个过程(递归)。

    堆排序HeapSort

    O(n *log2(n))

    利用堆(heaps)这种数据结构来构造的一种排序算法。堆是一个近似完全二叉树结构,并同时满足堆属性:即子节点的键值或索引总是小于(或者大于)它的父节点。

    近似完全二叉树

    希尔排序

    SHELL

    O(n1+)

    0<£<1

    选择一个步长(Step) ,然后按间隔为步长的单元进行排序.递归,步长逐渐变小,直至为1.

    箱排序
    Bin Sort

    O(n)

    设置若干个箱子,把关键字等于 k 的记录全都装入到第k 个箱子里 ( 分配 ) ,然后按序号依次将各非空的箱子首尾连接起来 ( 收集 ) 。

    分配排序的一种:通过" 分配 " 和 " 收集 " 过程来实现排序。

    1、冒泡排序(Bubble Sort)

      列表每两个相邻的数,如果前面比后面大,则交换这两个数。

      一趟排序完成后,则无序区减少一个数,有序区增加一个数。

      代码关键点:趟、无序区范围

    (1)图示说明

              

      这样排序一趟后,最大的数9,就到了列表最顶成为了有序区,下面的部分则还是无序区。然后在无序区不断重复这个过程,每完成一趟排序,无序区减少一个数,有序区增加一个数。图示最后一张图要开始第六趟排序,排序从第0趟开始计数。剩一个数的时候不需要排序了,因此整个排序排了n-1趟。

    (2)代码示例 

    import random
    
    def bubble_sort(li):
        for i in range(len(li)-1):    # 总共是n-1趟
            for j in range(len(li)-i-1):   # 每一趟都有箭头,从0开始到n-i-1
                if li[j] > li[j+1]:  # 比对箭头指向和箭头后面的那个数的值
                    # 当箭头所指数大于后面的数时交换位置, 升序排列;条件相反则为降序排列
                    li[j], li[j+1] = li[j+1], li[j]
    
    
    li = [random.randint(0, 10000) for i in range(30)]
    print(li)
    bubble_sort(li)
    print(li)
    
    """
    [5931, 5978, 6379, 4217, 9597, 4757, 4160, 3310, 6916, 2463, 9330, 8043, 8275, 5614, 8908, 7799, 9256, 3097, 9447, 9327, 7604, 9464, 417, 927, 1720, 145, 6451, 7050, 6762, 6608]
    [145, 417, 927, 1720, 2463, 3097, 3310, 4160, 4217, 4757, 5614, 5931, 5978, 6379, 6451, 6608, 6762, 6916, 7050, 7604, 7799, 8043, 8275, 8908, 9256, 9327, 9330, 9447, 9464, 9597]
    """
    

      如果要打印出每次排序结果:

    import random
    
    def bubble_sort(li):
        for i in range(len(li)-1):    # 总共是n-1趟
            for j in range(len(li)-i-1):   # 每一趟都有箭头,从0开始到n-i-1
                if li[j] > li[j+1]:  # 比对箭头指向和箭头后面的那个数的值
                    # 当箭头所指数大于后面的数时交换位置, 升序排列;条件相反则为降序排列
                    li[j], li[j+1] = li[j+1], li[j]
            print(li)
    
    
    li = [random.randint(0, 10000) for i in range(5)]
    print(li)
    bubble_sort(li)
    print(li)
    
    """
    [1806, 212, 4314, 1611, 8355]
    [212, 1806, 1611, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    """
    

    (3)算法时间复杂度

      n是列表的长度,算法中也没有发生循环折半的过程,具备两层关于n的循环,因此它的时间复杂度是O(n2)

    (4)冒泡排序优化

      如果在一趟排序过程中没有发生交换就可以认定已经排好序了。因此可做如下优化:

    import random
    
    def bubble_sort(li):
        for i in range(len(li)-1):    # 总共是n-1趟
            exchange = False
            for j in range(len(li)-i-1):   # 每一趟都有箭头,从0开始到n-i-1
                if li[j] > li[j+1]:  # 比对箭头指向和箭头后面的那个数的值
                    # 当箭头所指数大于后面的数时交换位置, 升序排列;条件相反则为降序排列
                    li[j], li[j+1] = li[j+1], li[j]
                    exchange = True   # 如果发生了交换就置为true
            print(li)
            if not exchange:
                # 如果exchange还是False,说明没有发生交换,结束代码
                return
    
    
    # li = [random.randint(0, 10000) for i in range(5)]
    li = [1806, 212, 4314, 1611, 8355]
    bubble_sort(li)
    
    """
    [212, 1806, 1611, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    [212, 1611, 1806, 4314, 8355]
    """
    

      对比前面排序的次数少了很多,算法得到了优化~

    2、选择排序(Selection Sort)

      一趟遍历完记录最小的数,放到第一个位置;再一趟遍历记录剩余列表中的最小的数,继续放置。

      算法关键点:有序区和无序区、无序区最小数的位置

    (1)简单的选择排序

    def select_sort_simple(li):
        li_new = []
        for i in range(len(li)):
            min_val = min(li)   # 找到最小的数,也需要遍历一边O(n)
            li_new.append(min_val)
            li.remove(min_val)   # 按值删除,如果有重复的先删除最左边的,删除之后,后面元素需要向前移动补位,因此也是O(n)
        return li_new
    
    
    li = [3, 2, 4, 1, 5, 6, 8, 7, 9]
    print(select_sort_simple(li))
    """
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    """
    

      注意这里的remove操作和min操作都不是O(1)的操作,都需要进行遍历,因此它的时间复杂度是O(n2)。

      而且前面冒泡排序是原地排序不需要开启一个新的列表,二这个版本的选择排序不是原地排序,多占了一份内存。

    (2)优化后的选择排序

    def select_sort(li):
        # 和冒泡排序类似,在n-1趟完成后,无序区只剩一个数,这个数一定是最大的
        for i in range(len(li)-1):   # i是第几趟
            min_loc = i     # 最小值的位置
            for j in range(i+1, len(li)):   # 遍历无序区,从i开始是自己跟自己比,因此从i+1开始
                if li[j] < li[min_loc]:   # 如果遍历的这个数小于现在min_loc位置上的数
                    min_loc = j     # 修改min_loc的index,循环完后,min_loc一定是无序区最小数的下标
            li[i], li[min_loc] = li[min_loc], li[i]  # 将i和min_loc对应的值进行位置交换
            print(li)   # 打印每趟执行完的排序,分析过程
    
    
    li = [3, 2, 4, 1, 5, 6, 8, 7, 9]
    select_sort(li)
    # print(li)   # [1, 2, 3, 4, 5, 6, 7, 8, 9]
    

      这里只有两层循环,时间复杂度是O(n2)。

    3、插入排序(Insertion Sort)

      元素被分为有序区和无序区两部分。初始时手里(有序区)只有一张牌,每次(从无序区)摸一张牌,插入到手里已有牌的正确位置,直到无序区变空。

    (1)图示说明

      一开始手里的牌只有5

      

      第一张摸到的牌是7,比5大插到5的右边:

      

      第二张摸到的牌是4,需要将5和7的位置向右挪,将4插到最前面:

      

      后面的情况依次类推。

    (2)代码示例

    def insert_sort(li):
        for i in range(1, len(li)):  # i表示摸到牌的下标
            tmp = li[i]  # 摸到的牌
            j = i - 1  # j指得是手里牌的下标
            while li[j] > tmp and j >= 0:  # 循环条件
                """
                循环终止条件:如果手里最后一张牌 <= 摸到的牌  or j == -1
                    比如手里有牌457,新摸到一张6(index=3),当比对5与6时,5<6,满足了循环终止条件,插到列表j+1处,即index=2处.
                    比如手里的牌是4567,新摸到一张3(index=4),一个个比对均比3大,到4与3比较时,由于比4小,再次循环j=-1,满足终止条件插到列表j+1处,即最前面
                """
                li[j + 1] = li[j]  # 通过循环条件,将手里的牌左移
                j -= 1  # 手里的牌对比箭头左移
            li[j + 1] = tmp  # 将摸到的牌插入有序区
            print(li)  # 打印每一趟排序过程
    
    
    li = [3, 2, 4, 1, 5, 6, 9, 6, 8]
    print('原列表', li)
    insert_sort(li)
    
    print('排序结果', li)
    

      这个循环主要是在找插入的位置。

      时间复杂度:O(n2)。

    (3)查看排序算法执行时间和效率

      准备好cal_time.py:

    import time
    
    def cal_time(func):
        def wrapper(*args, **kwargs):
            t1 = time.time()
            result = func(*args, **kwargs)
            t2 = time.time()
            print("%s running time: %s secs." % (func.__name__, t2 - t1))
            return result
    
        return wrapper
    

      检查10000个随机数字排序:

    import random
    from cal_time import *
    
    @cal_time
    def insert_sort(li):
        for i in range(1, len(li)):  # i表示摸到牌的下标
            tmp = li[i]  # 摸到的牌
            j = i - 1  # j指得是手里牌的下标
            while li[j] > tmp and j >= 0:  # 循环条件
                li[j + 1] = li[j]  # 通过循环条件,将手里的牌左移
                j -= 1  # 手里的牌对比箭头左移
            li[j + 1] = tmp  # 将摸到的牌插入有序区
            # print(li)  # 打印每一趟排序过程
    
    li = list(range(10000))
    random.shuffle(li)
    insert_sort(li)
    """
    insert_sort running time: 4.496495723724365 secs.
    """
    

    4、快速排序(Quick Sort)

      快速排序思路:取一个元素p(第一个元素),使元素p归位;列表被p分为两部分,左边都比p小,右边都比p大;递归完成排序。

      算法关键点:归位、递归。

     (1)图示说明

      

    (2)元素归位过程分析

      5要归位,先用一个变量将5存起来,两个箭头表示当前列表的left和right:

      

      列表左边有了一个空位,从右边开始找一个比5小的数填入:

      

      此时右边有了一个空位,右边是给比5大的数准备的,从左边开始找比5大的数填入:

      

      同理,此时左边又有了空位继续从右边开始找比5小的数填过去,以此类推

      

      最后要找比5大的数放到右边去,但是3<5,这时left和right重合了,此时说明位置已经在中间了,将5放回。

       

    (3)归位代码实现

    def partition(li, left, right):
        """
        归位函数
        :param li: 列表
        :param left: 左箭头
        :param right: 右箭头
        :return:
        """
        tmp = li[left]
        while left < right:
            while left < right and li[right] >= tmp:    # 从右边找一个比tmp小的数放过来
                # 注意由于循环条件是li[right] >= tep,在两个箭头相遇时不会退出循环,因此添加left<right条件
                right -= 1   # 如果比tmp大则right往左走一步
            li[left] = li[right]    # 将右边找的数插入到左边空位处
            print(li)  # 打印排序过程
    
            while left<right and li[left] <= tmp:      # 从左边找一个比tmp大的数放入右边的空位
                left += 1    # 如果比tmp小则left往右走一步
            li[right] = li[left]    # 将左边的值写入到右边空位处
            print(li)  # 打印排序过程
    
    
        # 循环终止条件:left>=right
        li[left] = tmp    # 将tmp归位
    
    
    li = [5,7,4,6,3,1,2,9,8]
    print("原列表", li)
    partition(li, 0, len(li)-1)
    print("排序结果", li)
    """
    原列表 [5, 7, 4, 6, 3, 1, 2, 9, 8]
    [2, 7, 4, 6, 3, 1, 2, 9, 8]
    [2, 7, 4, 6, 3, 1, 7, 9, 8]
    [2, 1, 4, 6, 3, 1, 7, 9, 8]
    [2, 1, 4, 6, 3, 6, 7, 9, 8]
    [2, 1, 4, 3, 3, 6, 7, 9, 8]
    [2, 1, 4, 3, 3, 6, 7, 9, 8]
    排序结果 [2, 1, 4, 3, 5, 6, 7, 9, 8]
    """
    

      注意无论从左边找还是从右边找,都需要添加left<right条件,在箭头相遇时跳出循环。还可以注意到每次写入空位,并不是真正的空位,仍由原元素占位在空位出,直到tmp归位,整个列表才没有了重复的元素。

    (4)快速排序代码实现

    def partition(li, left, right):
        """
        归位函数
        :param li: 列表
        :param left: 左箭头
        :param right: 右箭头
        :return:
        """
        tmp = li[left]
        while left < right:
            while left < right and li[right] >= tmp:    # 从右边找一个比tmp小的数放过来
                # 注意由于循环条件是li[right] >= tep,在两个箭头相遇时不会退出循环,因此添加left<right条件
                right -= 1   # 如果比tmp大则right往左走一步
            li[left] = li[right]    # 将右边找的数插入到左边空位处
            print(li)  # 打印排序过程
    
            while left<right and li[left] <= tmp:      # 从左边找一个比tmp大的数放入右边的空位
                left += 1    # 如果比tmp小则left往右走一步
            li[right] = li[left]    # 将左边的值写入到右边空位处
            print(li)  # 打印排序过程
    
    
        # 循环终止条件:left>=right
        li[left] = tmp    # 将tmp归位
        return left
    
    def quick_sort(li, left, right):
        """快速排序两个关键:归位、递归"""
        if left < right:   # 至少有两个元素
            mid = partition(li, left, right)
            quick_sort(li, left, mid-1)
            quick_sort(li, mid+1, right)
    
    
    li = [5,7,4,6,3,1,2,9,8]
    quick_sort(li, 0, len(li)-1)
    print(li)
    

      注意这里使用了partition归位函数和快速排序递归框架完成了快速排序设计。

    (5)快速排序的效率

      快速排序的时间复杂度:O(nlogn),每一层排序的复杂度是O(n),总共有logn层。

    (6)快速排序改写

      想给quick_sort添加装饰器查看排序运行效率,但是递归函数不能添加装饰器,因此需要做如下改写:

    from cal_time import *
    
    def partition(li, left, right):......
    
    def _quick_sort(li, left, right):
        """快速排序两个关键:归位、递归"""
        if left < right:   # 至少有两个元素
            mid = partition(li, left, right)
            _quick_sort(li, left, mid-1)
            _quick_sort(li, mid+1, right)
    
    @cal_time
    def quick_sort(li):
        _quick_sort(li, 0, len(li)-1)
    

    (7)测试验证快排和冒泡排序执行效率

    # -*- coding:utf-8 -*-
    __author__ = 'Qiushi Huang'
    
    import random
    from cal_time import *
    import copy   # 复制模块
    
    
    def partition(li, left, right):
        """
        归位函数
        :param li: 列表
        :param left: 左箭头
        :param right: 右箭头
        :return:
        """
        tmp = li[left]
        while left < right:
            while left < right and li[right] >= tmp:    # 从右边找一个比tmp小的数放过来
                # 注意由于循环条件是li[right] >= tep,在两个箭头相遇时不会退出循环,因此添加left<right条件
                right -= 1   # 如果比tmp大则right往左走一步
            li[left] = li[right]    # 将右边找的数插入到左边空位处
            # print(li)  # 打印排序过程
    
            while left<right and li[left] <= tmp:      # 从左边找一个比tmp大的数放入右边的空位
                left += 1    # 如果比tmp小则left往右走一步
            li[right] = li[left]    # 将左边的值写入到右边空位处
            # print(li)  # 打印排序过程
    
    
        # 循环终止条件:left>=right
        li[left] = tmp    # 将tmp归位
        return left
    
    
    def _quick_sort(li, left, right):
        """快速排序两个关键:归位、递归"""
        if left < right:   # 至少有两个元素
            mid = partition(li, left, right)
            _quick_sort(li, left, mid-1)
            _quick_sort(li, mid+1, right)
    
    
    @cal_time
    def quick_sort(li):
        _quick_sort(li, 0, len(li)-1)
    
    
    @cal_time
    def bubble_sort(li):
        for i in range(len(li)-1):    # 总共是n-1趟
            exchange = False
            for j in range(len(li)-i-1):   # 每一趟都有箭头,从0开始到n-i-1
                if li[j] > li[j+1]:  # 比对箭头指向和箭头后面的那个数的值
                    # 当箭头所指数大于后面的数时交换位置, 升序排列;条件相反则为降序排列
                    li[j], li[j+1] = li[j+1], li[j]
                    exchange = True   # 如果发生了交换就置为true
            # print(li)
            if not exchange:
                # 如果exchange还是False,说明没有发生交换,结束代码
                return
    
    li = list(range(10000))
    random.shuffle(li)
    
    li1 = copy.deepcopy(li)   # 深拷贝
    li2 = copy.deepcopy(li)
    
    quick_sort(li1)
    bubble_sort(li2)
    """
    quick_sort running time: 0.03162503242492676 secs.
    bubble_sort running time: 10.773478269577026 secs.
    """
    print(li1)  # [0, 1, 2, 3, 4,..., 9997, 9998, 9999]
    print(li2)
    冒泡排序和快速排序效率对比

      对比运行时间,可以发现针对10000个元素的数组排序,快速排序的效率比冒泡排序高了几百倍。

      时间复杂度O(nlogn)和O(n2)在数量越大的情况下,效率相差将越来越大。

      快速排序的最好情况时间复杂度是O(n),一般情况时间复杂度是O(nlogn),最坏情况时间复杂度是O(n2)。

    (8)快速排序存在的问题

      首先python有一个递归最大深度的问题,默认是999,修改递归最大深度方法:

    import sys
    
    sys.setrecursionlimit(100000)   # 修改递归最大深度

    虽然可以修改;而且递归会相当消耗一部分的系统资源。

      其次快速排序有一个最坏情况出现:倒序排列的数组,在这种情况下,快速排序无法两边同时排序,每次只能排序一个数字。因此在这种情况下快速排序的时间复杂度是:O(n2)

      加入随机化解决该问题:即不再找第一个元素归位,而是随机找一个值与第一个元素交换,然后继续执行快速排序,就可以解决倒序例子时间复杂度特别高的情况。但是这个方法不能完全避免最坏情况,比如每次随机都恰好选中了最大的一个数,但是这种修改可以让最坏情况无法被设计出来,发生最坏情况的概率也会非常非常小。

    5、堆排序(Heap-Sort)

    算法——堆和堆排序介绍

    6、归并排序(Merge-Sort)

    算法——归并和归并排序 

    三、排序总结 

    1、冒泡排序、选择排序、插入排序

      冒泡排序、选择排序、插入排序的时间复杂度都是O(n2),且都是原地排序。

    2、快速排序、堆排序、归并排序

      快速排序、堆排序、归并排序这三种排序算法的时间复杂度都是O(nlogn)。 但有常数差异。

    (1)一般情况下,就运行时间来比较:

        快速排序(速度最快)< 归并排序 < 堆排序

    (2)三种排序算法的缺点:

      快速排序:极端情况下排序效率低。

      归并排序:需要额外的内存开销。

      堆排序:在快的排序算法中相对较慢。

    3、六种排序算法对比总结

      

    (1)递归占用空间

      递归需要用系统占的空间,快速排序在平均情况下需要递归logn层,所以平均情况下需要消耗O(logn)的空间复杂度;最坏情况下需要递归n层,因此需要消耗O(n)的时间复杂度。

      归并虽然也有递归,但他已经开了一个列表了占用O(n),归并递归需要的空间复杂度是O(logn)小于O(n),因此统计空间复杂度是O(n)。

    (2)排序算法稳定性

      假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

      判断是否算法是否稳定:挨着换的稳定,不挨着换的不稳定

    (3)代码复杂度

      算法是否好写,是否容易理解。

  • 相关阅读:
    管理~资源组织运作
    科学与艺术区别
    概念思维
    拨开文字表象,关注背后事实
    论信息部门与业务部门的关系
    再论信息系统
    linux命令行快捷键记录
    hadoop,帮我解了部分惑的文章
    hadoop运行测试命令遇到的问题
    日志分析及几个小技巧
  • 原文地址:https://www.cnblogs.com/xiugeng/p/9638052.html
Copyright © 2020-2023  润新知