排序算法

作者: import_hello | 来源:发表于2018-04-11 23:03 被阅读2次

    本文讲述了:选择排序、快速排序法、合并排序法,并对后两种算法进行了比较。

    本文的最新版本位于:https://github.com/iwhales/algorithms_notes
    转载请注明出处:https://www.jianshu.com/u/5e6f798c903a

    排序算法

    1.选择排序法

    在 Python 中分别用循环和递归实现选择排序。 选择排序的时间复杂度是 $O(n^2)$

    1.1. 循环方式

    # -*- coding: utf-8 -*-
    def selection_sort_l(a_list: list):
        """
        选择排序,循环方式
        将数组元素按从小到大的顺序排列
        :param a_list:
        :return:
        """
        for n in range(len(a_list)):
            for i in range(n + 1, len(a_list)):
                if a_list[i] < a_list[n]:
                    a_list[n], a_list[i] = a_list[i], a_list[n]
        return a_list
    
    if __name__ == '__main__':
        print(selection_sort_loop([9, 8, 1, 3, 2, 10, 7]))
    

    1.2. 递归方式

    • 基线条件:数组只包含一个元素,直接返回该数组即可
    • 递归条件:每次从数组中剔除索引为0的最小值,对剩下部分继续执行选择排序
    # -*- coding: utf-8 -*-
    def selection_sort_r(a_list: list):
        """
        选择排序,递归方式
        将数组元素按从小到大的顺序排列
        :param a_list:
        :return:
        """
        if len(a_list) <= 1:  # 基线条件
            return a_list
        # 递归条件
        for i in range(1, len(a_list)):
            if a_list[i] < a_list[0]:
                a_list[0], a_list[i] = a_list[i], a_list[0]
        return [a_list[0]] + selection_sort_r(a_list[1:])
    
    if __name__ == '__main__':
        print(selection_sort_recursive([9, 8, 1, 3, 2, 10, 7]))
    

    2. 快速排序法

    Quicksort

    快速排序采用了分治策略的思想,其速度比选择排序法快得多,C 语言标准库中 qsort 函数就是利用快速排序法实现的。

    快速排序法的速度取决于基准值的选择,最坏时间复杂度为 $O(n^2)$ ,此时调用栈的高度为 $O(n)$;最优时间复杂度为 $O(n\log_{2}n)$ ,此时调用栈的高度为 $O(\log_{2}n)$。

    合并排序(merge sort) 的排序算法,时间复杂度为 $O(n\log_{2}n)$ 。

    快速排序的步骤:

    1. 基准值(pivot):首先从数组中随机选择一个元素作为基准值(pivot)
    2. 分区(partitioning):将比基准值大的元素和比基准值小的元素分别放到两个子数组中,同时将基准值也放到一个数组中,并按数组中平均值的大小排列这三个分组
    3. 递归:对子数组递归调用快速排序函数
    # -*- coding: utf-8 -*-
    # 快速排序算法
    
    def quick_sort(a_list: list):
        if len(a_list) <= 1:  # 基线条件
            return a_list
        #  通过递归缩小问题规模
        pivot = a_list[0]
        less = [i for i in a_list[1:] if i <= pivot]
        greater = [i for i in a_list[1:] if i > pivot]
        return quick_sort(less) + [pivot] + quick_sort(greater)
    
    if __name__ == '__main__':
        print(quick_sort([5, 3, 6, 2, 7, 1, 2, 3]))
        print(quick_sort([]))
    

    3. 合并排序法

    merge sort

    合并排序法的时间复杂度 $$O(n\log_{2}n)$$ 比快速排序法低,但其执行数度比快速排序慢。 合并排序法调用栈的高度为 $$O(\log_{2}n)$$ 。

    算法示意图如下:


    image.png

    自序列的合并过程如下图所示:


    image.png
    # -*- coding: utf-8 -*-
    # 合并排序(merge sort),也称归并排序
    
    def merge_sort(a_list: list):
        result = []
        if len(a_list) < 2:
            return a_list
        mid = len(a_list) // 2
        left = merge_sort(a_list[:mid])
        right = merge_sort(a_list[mid:])
        l = 0
        r = 0
        while l < len(left) and r < len(right):
            if left[l] < right[r]:
                result.append(left[l])
                l += 1
            else:
                result.append(right[r])
                r += 1
        result += left[l:]
        result += right[r:]
        return result
    
    if __name__ == '__main__':
        print(merge_sort([8, 4, 5, 7, 1, 3, 8, 9]))
    

    参考

    4. 快速排序 VS 合并排序

    4.1. 快速排序

    快速排序法的速度取决于基准值的选择。其最坏时间复杂度为 $O(n^2)$ ,此时调用栈的高度为 $O(n)$;最优时间复杂度为 $O(n\log_{2}n)$ ,此时调用栈的高度为 $O(\log_{2}n)$。

    如果总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序,此时便会遇到最糟糕的情况调用栈的高度为 $O(n)$ ,每层调用栈的时间复杂度是 $O(n)$ ,所以最坏时间复杂度为 $O(n^2)$ 。 在这种情况下,数组并没有被分成两半,但其中一个子数组始终为空,这导致调用栈非常长。

    image.png

    现在假设你总是将中间的元素用作基准值,便会遇到最优情况,此时调用栈的高度为 $O(\log_{2}n)$,每层调用栈的时间复杂度是 $O(n)$ ,所以最优时间复杂度为 $O(n\log_{2}n)$ ,

    image.png

    可见最优情况的调用栈比最坏的情况短的多,由于每次都将数组分成两半,所以减少了递归调用的次数,因此很快便可达到基线条件。另外,最佳情况也是平均情况,因为最佳情况的时间复杂度远小于最坏情况的时间复杂度。 因此,只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均操作次数将为 $O(n log n)$ 。快速排序是最快的排序算法之一,也是D&C典范。

    4.2. 合并排序

    合并排序(merge sort) 的排序算法,时间复杂度为 $O(n\log_{2}n)$ ,调用栈的高度为 $\log_{2}n$ 。

    4.3. 常量的作用

    通常无需考虑常量,因为如果两种算法的大 $O$ 运行时间不同,常量将无关紧要。例如,在比较简单查找和二分查找时,常量便几乎无关紧要,因为列表很长时,$\log_{2}n$ 的速度比$O(n)$ 快得多。

    但是,大 $O$ 表示法中的常量有时候非常重要,这就是快速排序比合并排序快的原因所在。 由于快速查找的常量比合并查找小,因此如果它们的时间复杂度都为 $O(n\log_{2}n)$,快速查找的速度将更快。 实际上,快速查找的速度确实更快,相对于遇上最糟情况,它遇上平均情况的可能性要大得多。

    相关文章

      网友评论

        本文标题:排序算法

        本文链接:https://www.haomeiwen.com/subject/fyrtkftx.html