各种排序算法详解及优劣对比

从时间复杂度和空间复杂度进行考虑。

  1. 相关性:排序时是否需要比较元素

  2. 稳定性:相同元素排序后是否可能打乱

  3. 时间空间复杂度:随着元素增加时间和空间随之变化的函数

一.简单插入排序

       插入排序,顾名思义就是基本操作是插入,不断把一个个元素插入一个序列中,最终得到排序序列。插入过程中需要一个个的处理未排序元素,最简单的方法就是按下标处理。处理一个元素时留下一个空位,如果该空位与已排序序列相连,就可以直接用作该序列的延伸位置,也就是简单插入排序。示意图及代码如下:

1.过程图解

2.代码实现 

def insert_sort(lst):
    for i in range(1,len(lst)):
        x=lst[i]
        j=i
        while j>0 and lst[j-1]<lst[j]:
            lst[j]=lst[j-1]
            j=j-1
        lst[j]=x
            

3.算法分析 

     插入排序的适用场景:一个新元素需要插入到一组已经是有序的数组中,或者是一组基本有序的数组排序

  1. 比较性:排序时元素之间需要比较,所以为比较排序

  2. 稳定性:从代码我们可以看出只有比较元素大于当前元素,比较元素才会往后移动,所以相同元素是不会改变相对顺序

  3. 时间复杂度:插入排序同样需要两次循坏一个一个比较,故时间复杂度也为O(n^2)

  4. 空间复杂度:只需要常数个辅助单元,所以空间复杂度也为O(1)

  5. 记忆方法:想象成在书架中插书:先找到相应位置,将后面的书往后推,再将书插入

4.算法变型

    在插入排序中需要检索元素的插入位置,而且是在有序的序列里检索。这可能使用二分法进行检索元素,因此也称为二分插入排序,但稍微分析可知其不能根本改进算法复杂度,因为虽然检索代价降低了,但仍然还需要顺序移动元素,腾出空位。

二.希尔排序

       希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本,它与插入排序的不同之处在于,它会优先比较距离较远的元素,该方法因D.L.Shell于1959年提出而得名。希尔排序的整体思想是将固定间隔的几个元素之间排序,然后再缩小这个间隔。这样到最后数列就成为了基本有序数列,而前面我们讲过插入排序对基本有序数列排序效果较好。

  1. 计算一个增量(间隔)值

  2. 对元素进行增量元素进行比较,比如增量值为7,那么就对0,7,14,21…个元素进行插入排序

  3. 然后对1,8,15…进行排序,依次递增进行排序

  4. 所有元素排序完后,缩小增量比如为3,然后又重复上述第2,3步

  5. 最后缩小增量至1时,数列已经基本有序,最后一遍普通插入即可

已知的最增量式是由 Sedgewick 提出的 (1, 5, 19, 41, 109,…),该步长的项来自 9 4^i - 9 2^i + 1 和 4^i - 3 2^i + 1 这两个算式。这项研究也表明 “比较在希尔排序中是最主要的操作,而不是交换。 用这样增量式的希尔排序插入排序堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比*快速排序慢。

1.过程图解

2.代码实现 

def shell_sort(arr):
    """希尔排序"""
    # 取整计算增量(间隔)值
    gap = len(arr) // 2
    while gap > 0:
        # 从增量值开始遍历比较
        for i in range(gap, len(arr)):
            j = i
            current = arr[i]
            # 元素与他同列的前面的每个元素比较,如果比前面的小则互换
            while j - gap >= 0 and current < arr[j - gap]:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = current
        # 缩小增量(间隔)值
        gap //= 2
    return arr

3.算法分析

  1. 比较性:排序时元素之间需要比较,所以为比较排序

  2. 稳定性:因为希尔排序是间隔的插入,所以存在相同元素相对顺序被打乱,所以是不稳定排序

  3. 时间复杂度:    最坏时间复杂度O(n^2)平均复杂度为O(n^1.3)

  4. 空间复杂度:只需要常数个辅助单元,所以空间复杂度也为O(1)

  5. 记忆方法:插入排序是每轮都是一小步,希尔排序是先大步后小步,它第一个突破O(n2)的排序算法。联想起阿姆斯特朗登月之后说:这是我个人一小步,却是人类迈出的一大步。

三.简单选择排序

       选择排序是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,所以称为:选择排序

  1. 设第一个元素为比较元素,依次和后面的元素比较,比较完所有元素找到最小的元素,将它和第一个元素互换

  2. 重复上述操作,我们找出第二小的元素和第二个位置的元素互换,以此类推找出剩余最小元素将它换到前面,即完成排序

1.过程图解

2.代码实现

def selection_sort(lst):
    for i in range(0,len(lst)):
        x=lst[i]
        minid=i
        for j in range(i,len(lst)):
           if j<len(lst) and lst[j]<lst[minid]:
                minid=j
                j=j+1
        if i !=minid:
            lst[i]=lst[minid]
            lst[minid]=x

3.算法分析

       选择排序冒泡排序很类似,但是选择排序每轮比较只会有一次交换,而冒泡排序会有多次交换,交换次数比冒泡排序少,就减少cpu的消耗,所以在数据量小的时候可以用选择排序,实际适用的场合非常少

  1. 比较性:因为排序时元素之间需要比较,所以是比较排序

  2. 稳定性:因为存在任意位置的两个元素交换,比如[5,  8, 5, 2],第一个5会和2交换位置,所以改变了两个5原来的相对顺序,所以为不稳定排序

  3. 时间复杂度:我们看到选择排序同样是双层循环n*(n-1)),所以时间复杂度也为:O(n^2)

  4. 空间复杂度:只需要常数个辅助单元,所以空间复杂度也为O(1)

  5. 记忆方法:选择对象要先选最小的,因为嫩,哈哈

四.冒泡排序

        冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,一层一层的将较大的元素往后移动,其现象和气泡在上升过程中慢慢变大类似,故成为冒泡排序

  1. 从第一个和第二个开始比较,如果第一个比第二个大,则交换位置,然后比较第二个和第三个,逐渐往后

  2. 经过第一轮后最大的元素已经排在最后,所以重复上述操作的话第二大的则会排在倒数第二的位置。

  3. 那重复上述操作n-1次即可完成排序,因为最后一次只有一个元素所以不需要比较

1.过程图解

2.代码实现 

def bubble_sort(lst):
    for i in range(len(lst)):
        for j in range(1,len(lst)-i):
            if lst[j]<lst[j-1]:
                lst[j-1],lst[j]=lst[j],lst[j-1]

3.算法分析

      冒泡排序是一种简单直接暴力的排序算法,为什么说它暴力?因为每一轮比较可能多个元素移动位置,而元素位置的互换是需要消耗资源的,所以这是一种偏慢的排序算法,仅适用于对于含有较少元素的数列进行排序。

  1. 稳定性:我们从代码中可以看出只有前一个元素大于后一个元素才可能交换位置,所以相同元素的相对顺序不可能改变,所以它是稳定排序

  2. 比较性:因为排序时元素之间需要比较,所以是比较排序

  3. 时间复杂度:因为它需要双层循环n*(n-1)),所以平均时间复杂度为O(n^2)

  4. 空间复杂度:只需要常数个辅助单元,所以空间复杂度为O(1),我们把空间复杂度为O(1)的排序成为原地排序(in-place)

  5. 记忆方法:想象成气泡,一层一层的往上变大

五.归并排序

       归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序适用于子序列有序的数据排序。归并排序是分治法的典型应用。分治法(Divide-and-Conquer):将原问题划分成 n 个规模较小而结构与原问题相似的子问题;递归地解决这些问题,然后再合并其结果,就得到原问题的解。从下图看分解后的数列很像一个二叉树。

  1. 使用递归将源数列使用二分法分成多个子列

  2. 申请空间将两个子列排序合并然后返回

  3. 将所有子列一步一步合并最后完成排序

1.过程图解

2.代码实现 

def merge_sort(arr):
    """归并排序"""
    if len(arr) == 1:
        return arr
    # 使用二分法将数列分两个
    mid = len(arr) // 2
    left = arr[:mid]
    right = arr[mid:]
    # 使用递归运算
    return marge(merge_sort(left), merge_sort(right))


def marge(left, right):
    """排序合并两个数列"""
    result = []
    # 两个数列都有值
    while len(left) > 0 and len(right) > 0:
        # 左右两个数列第一个最小放前面
        if left[0] <= right[0]:
            result.append(left.pop(0))
        else:
            result.append(right.pop(0))
    # 只有一个数列中还有值,直接添加
    result += left
    result += right
    return result

3.算法分析

  1. 比较性:排序时元素之间需要比较,所以为比较排序

  2. 稳定性:我们从代码中可以看到当左边的元素小于等于右边的元素就把左边的排前面,而原本左边的就是在前面,所以相同元素的相对顺序不变,故为稳定排序

  3. 时间复杂度:    复杂度为O(nlog^n)

  4. 空间复杂度:在合并子列时需要申请临时空间,而且空间大小随数列的大小而变化,所以空间复杂度为O(n)

  5. 记忆方法:所谓归并肯定是要先分解,再合并

六.快速排序 (1)

     算法中利用两个下标和 j ,其初值分别是序列的第一个和最后一个记录的位置。在划分过程中,他们的值交替地作为空位和下一被见哈记录的下表。然后取出第一个记录,设其排序值为K,作为划分标准。

交替进行下述操作:

  1. 从右向左逐个检查 j 一边的记录,检查中 j 的值不断减一,直到找到一个关键字小于K的记录,将其存入 i 所指的空位。注意移动记录后j变成空位,i 值加一后指向下一需要检查的记录。

  2. 从左向右逐个检查 i 一边的记录,检查中 i 的值不断加一,直到找到一个关键字小于K的记录,将其存入 j 所指的空位。注意移动记录后i变成空位,j 值减一后指向下一需要检查的记录。

重复交替上述两步,直到 i 不再小于 j 为止。当划分结束时,i 与 j 的值相等,指向表中的空位,将记录R存入空位。

1.代码实现

def quick_sort(lst):
    qsort_rec(lst,0,len(lst)-1)

#递归框架
def qsort_rec(lst,1,r):
    if 1>=r :return
    i=1
    j=r
    pivot=lst[i]
    while i<j:
        while i<j and lst[j]>=pivot:
            j=j-1
        if i<j:
           lst[i]=lst[j]
            i=i+1
        while i<j and lst[i]<=pivot:
            i=i+1
        if i<j:
            lst[j]=lst[i]
            j-=1
    lst[i]=pivot
    qsort_rec(lst,1,i-1)
    qsort_rec(lst,i+1,r)_
    

六.快速排序 (2)

快排的实现方式多种多样,写一种容易理解的:分治+迭代,只需要三步:

  1. 在数列之中,选择一个元素作为”基准”(pivot),或者叫比较值。

  2. 数列中所有元素都和这个基准值进行比较,如果比基准值小就移到基准值的左边,如果比基准值大就移到基准值的右边

  3. 以基准值左右两边的子列作为新数列,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。

1.过程图解

2.代码实现 


def quick_sort(arr):
    """快速排序"""
    if len(arr) < 2:
        return arr
    # 选取基准,随便选哪个都可以,选中间的便于理解
    mid = arr[len(arr) // 2]
    # 定义基准值左右两个数列
    left, right = [], []
    # 从原始数组中移除基准值
    arr.remove(mid)
    for item in arr:
        # 大于基准值放右边
        if item >= mid:
            right.append(item)
        else:
            # 小于基准值放左边
            left.append(item)
    # 使用迭代进行比较
    return quick_sort(left) + [mid] + quick_sort(right)

3.算法分析 

  1. 稳定性:快排是一种不稳定排序,比如基准值的前后都存在与基准值相同的元素,那么相同值就会被放在一边,这样就打乱了之前的相对顺序

  2. 比较性:因为排序时元素之间需要比较,所以是比较排序

  3. 时间复杂度:快排的时间复杂度为O(nlogn)

  4. 空间复杂度:排序时需要另外申请空间,并且随着数列规模增大而增大,其复杂度为:O(nlogn)

  5. 归并排序与快排 :归并排序与快排两种排序思想都是分而治之,但是它们分解和合并的策略不一样:归并是从中间直接将数列分成两个,而快排是比较后将小的放左边大的放右边,所以在合并的时候归并排序还是需要将两个数列重新再次排序,而快排则是直接合并不再需要排序,所以快排比归并排序更高效一些,可以从示意图中比较二者之间的区别。

4.快排优化

        快速排序有一个缺点就是对于小规模的数据集性能不是很好。可能有人认为可以忽略这个缺点不计,因为大多数排序都只要考虑大规模的适应性就行了。但是快速排序算法使用了分治技术,最终来说大的数据集都要分为小的数据集来进行处理,所以快排分解到最后几层性能不是很好,所以我们就可以使用扬长避短的策略去优化快排:

  1. 先使用快排对数据集进行排序,此时的数据集已经达到了基本有序的状态

  2. 然后当分区的规模达到一定小时,便停止快速排序算法,而是改用插入排序,因为我们之前讲过插入排序在对基本有序的数据集排序有着接近线性的复杂度,性能比较好。

  3. 这一改进被证明比持续使用快速排序算法要有效的多。

5.快排总结

  • 快排基本思想是?

  • 从数据集中选取一个基准,然后让数据集的每个元素和基准值比较,小于基准值的元素放入左边分区大于基准值的元素放入右边分区,最后以左右两边分区为新的数据集进行递归分区,直到只剩一个元素。

  • 快排有什么优点,有什么缺点:?

  • 分治思想的排序在处理大数据集量时效果比较好,小数据集性能差些。

  • 那该如何优化?

  • 对大规模数据集进行快排,当分区的规模达到一定小时改用插入排序,插入排序在小数据规模时排序性能较好。

七.排序算法总结

    

1.冒泡排序几乎是最差的排序

2.随机数排序时,当数据集非常少时,插入类排序 要比 比较类排序快

 只有当n=10时,快排反而比较慢,而插入和希尔排序相对较快,这是因为插入排序和希尔排序都属于插入类型的排序,而快排和冒泡属于交换类排序,数据量少时交换所消耗的资源占比大。

3.基本有序数据排序时,在数据量较少的情况下,插入排序胜过其他排序

在基本有序数据排序结果中,当n=10和n=100中都是插入排序消耗时间更短,因为数据基本有序,所以需要插入的次数比较少,尽管插入排序需要一个一个比较,但因为数据量不大,所以比较所消耗的资源占比不会太大。

4.不管数据是随机还是基本有序,数据量越大,快排的优势越明显

快排果然还是名副其实的快,我们看到当数据集达到十万级别时,冒泡排序已经用时800多秒,而快排只用了0.3秒,相信随着数据量的增大,它们之间的差距也会越来越大。

5.快排优化方案成立

对于大数据集排序先使用快排,使数据集达到基本有序,然后当分区达到一定小的时候使用插入排序,因为插入排序对少量的基本有序数据集性能优于快排!

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值