Python---简析八大排序算法

前言

1 、排序的概念

排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。
排序分为内部排序外部排序
若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序
反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序

2 、排序分类

八大排序算法均属于内部排序。如果按照策略来分类,大致可分为:交换排序、插入排序、选择排序、归并排序和基数排序。如下图所示:

《Python---简析八大排序算法》

3 、算法分析

下表给出各种排序的基本性能,具体分析请参看各排序的详解:

《Python---简析八大排序算法》

一、插入排序

插入排序分为两种:直接插入排序和希尔排序

直接插入排序

基本思想:
直接插入排序的核心思想就是:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
因此,从上面的描述中我们可以发现,直接插入排序可以用两个循环完成:

  • 第一层循环:遍历待比较的所有数组元素
  • 第二层循环:将本轮选择的元素与已经排好序的元素相比较。
    如果:selected > ordered,那么将二者交换
def insert_sort(array):
    for i in range(len(array)):
        for j in range(i):
            if array[i] < array[j]:
                array.insert(j, array.pop(i))
                break
    return array

希尔排序

基本思想:
将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身还是使用数组进行排序。

希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

def shell_sort(alist):
    n = len(alist)
    # 初始步长
    gap = n // 2
    while gap > 0:
        # 按步长进行插入排序
        for i in range(gap,n):
            j = i
            # 原地插入排序,之前的插入排序另辟空间存储的
            while j>=gap and alist[j-gap] > alist[j]:
                alist[j-gap], alist[j] = alist[j], alist[j-gap]
                j -= gap
        # 得到新的步长
        gap = gap //2
    return alist

最坏情况O(n^2) ,平均O(nlog(n))~O(n2),最好O(n1.3) ,不稳定。

二、选择排序

选择排序分为简单选择排序和堆排序

简单选择排序

基本思想:
比较+交换。

  1. 从待排序序列中,找到关键字最小的元素;
  2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
  3. 从余下的 N – 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
    因此我们可以发现,简单选择排序也是通过两层循环实现。
  • 第一层循环:依次遍历序列当中的每一个元素
  • 第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
def select_sort(array):
    for i in range(len(array)):
        x = i  # min index
        for j in range(i, len(array)):
            if array[j] < array[x]:
                x = j
        array[i], array[x] = array[x], array[i]
    return array

堆排序

基本思想:
堆排序可以按照以下步骤来完成:

  1. 首先将序列构建成为大顶堆;
    (这样满足了大顶堆那条性质:位于根节点的元素一定是当前序列的最大值)
  2. 取出当前大顶堆的根节点,将其与序列末尾元素进行交换;
    (此时:序列末尾的元素为已排序的最大值;由于交换了元素,当前位于根节点的堆并不一定满足大顶堆的性质)
  3. 对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;
  4. 重复2.3步骤,直至堆中只有1个元素为止

堆排序的时间复杂度分为两个部分一个是建堆的时候所耗费的时间,一个是进行堆调整的时候所耗费的时间。而堆排序则是调用了建堆和堆调整。
建堆是一个线性过程,从len/2-0一直调用堆调整的过程,相当于o(h1)+o(h2)+…+o(hlen/2)这里的h表示节点深度,len/2表示节点深度,对于求和过程,结果为线性的O(n) 堆调整为一个递归的过程,调整堆的过程时间复杂度与堆的深度有关系,相当于lgn的操作。 因为建堆的时间复杂度是O(n),调整堆的时间复杂度是lgn,所以堆排序的时间复杂度是O(nlgn)。

def heap_sort(array):
    def heap_adjust(parent):
        child = 2 * parent + 1  # left child
        while child < len(heap):
            if child + 1 < len(heap):
                if heap[child + 1] > heap[child]:
                    child += 1  # right child
            if heap[parent] >= heap[child]:
                break
            heap[parent], heap[child] = \
                heap[child], heap[parent]
            parent, child = child, 2 * child + 1
    heap, array = array.copy(), []
    for i in range(len(heap) // 2, -1, -1):
        heap_adjust(i)
    while len(heap) != 0:
        heap[0], heap[-1] = heap[-1], heap[0]
        array.insert(0, heap.pop())
        heap_adjust(0)
    return array

三、归并排序

基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型的应用。它的基本操作是:将已有的子序列合并,达到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序其实要做两件事:

分解—-将序列每次折半拆分

合并—-将划分后的序列段两两排序合并
因此,归并排序实际上就是两个操作,拆分+合并

合并:
L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。

首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[]
重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[]
此时将temp[]中的元素复制给L[],则得到的L[first…last]有序

分解:
在这里,我们采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列
分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束。

def merge_sort(nums):  # 变成二次树
    if len(nums) == 1:
        return nums
    mid = len(nums) // 2
    left = nums[:mid]
    right = nums[mid:]
    a = merge_sort(left)
    b = merge_sort(right)
    return merge(a, b)
def merge(left, right):  # 整合排序
    c = []
    j = k = 0
    while len(left) > j and len(right) > k:
        if left[j] <= right[k]:
            c.append(left[j])
            j += 1
        else:
            c.append(right[k])
            k += 1
    if j == len(left):
        for i in right[k:]:
            c.append(i)
    else:
        for i in left[j:]:
            c.append(i)
    return c

四、基数排序

基本思想:
通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。

分配:我们将L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中

收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列L[ ]
对新形成的序列L[]重复执行分配和收集元素中的十位、百位…直到分配完该序列中的最高位,则排序结束
根据上述“基数排序”的展示,我们可以清楚的看到整个实现的过程

def radix_sort(array):
    bucket, digit = [[]], 0
    while len(bucket[0]) != len(array):
        bucket = [[], [], [], [], [], [], [], [], [], []]
        for i in range(len(array)):
            num = (array[i] // 10 ** digit) % 10
            bucket[num].append(array[i])
        array.clear()
        for i in range(len(bucket)):
            array += bucket[i]
        digit += 1
    return array

五、交换排序

交换排序分为两种:冒泡排序和快速排序

冒泡排序

基本思想:

  • 将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;
    ( 第一轮结束后,序列最后一个元素一定是当前序列的最大值;)
  • 对序列当中剩下的n-1个元素再次执行步骤1。
  • 对于长度为n的序列,一共需要执行n-1轮比较
    (利用while循环可以减少执行次数)
def bubble_sort(array):
    for i in range(len(array)):
        for j in range(i, len(array)):
            if array[i] > array[j]:
                array[i], array[j] = array[j], array[i]
    return array

快速排序

基本思想:挖坑填数+分治法

从序列当中选择一个基准数(pivot)
在这里我们选择序列当中第一个数最为基准数
将序列当中的所有数依次遍历,比基准数大的位于其右侧,比基准数小的位于其左侧
重复步骤1.2,直到所有子集当中只有一个元素为止。
用伪代码描述如下:
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中

def quick_sort(array):
    sys.setrecursionlimit(10 ** 8)
    def recursive(begin, end):
        if begin > end:
            return
        l, r = begin, end
        pivot = array[l]
        while l < r:
            while l < r and array[r] > pivot:
                r -= 1
            while l < r and array[l] <= pivot:
                l += 1
            array[l], array[r] = array[r], array[l]
        array[l], array[begin] = pivot, array[l]
        recursive(begin, l - 1)
        recursive(r + 1, end)

    recursive(0, len(array) - 1)
    return array

六、总结

  • (1)若n较小(如n≤50),可采用直接插入或直接选择排序。
     当记录规模较小时,直接插入排序较好;否则如果直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
  • (2)若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
  • (3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
     快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
     堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
     若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。

七、参考文献

[1] 李春葆. 数据结构教程[M].北京市:清华大学出版社,2013.

[2] li563868273.各种排序的比较和使用场景分析.CSDN,2016.

[3] LeeLom. 数据结构常见的八大排序算法.简书,2016.

[4] woider. Python 八大排序算法速度比较.博客园,2016.

    原文作者:Wayne_Dream
    原文地址: https://www.jianshu.com/p/70af69f0fbe9
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞