排序算法与python实现

目录

1 交换类排序

1.1 冒泡排序(Bubble Sort)

1.1.1 算法描述

1.1.2 动图演示(以从小到大排序为例)

1.1.3 python 代码

1.1.4 算法评估

1.2 快速排序

1.2.1 算法描述

1.2.2 动图演示(以从小到大排序为例)

1.2.3 python 代码

1.2.4 算法评估

2 插入类排序 

2.1 简单插入排序

2.1.1 算法描述

2.1.2 动图演示(以从小到大排序为例)

2.1.3 python 代码

2.1.4 算法评估

2.2 希尔排序

2.2.1 算法描述

2.2.2 动图演示(以从小到大排序为例)

2.2.3 python 代码

2.2.4 算法评估

3 选择类排序

3.1 选择排序(Selection Sort)

3.1.1 算法描述

3.1.2 动图演示(以从小到大排序为例)

3.1.3 python 代码

3.1.4 算法评估

3.2 堆排序

3.2.1 算法描述

3.2.2 动图演示(以从小到大排序为例)

3.2.3 python 代码

3.2.4 算法评估

4 归并排序

4.1 二路归并排序

4.1.1 算法描述

4.1.2 动图演示(以从小到大排序为例)

4.1.3 python 代码

4.1.4 算法评估

4.2 多路归并排序

5 线性时间非比较类排序

5.1 计数排序

5.1.1 算法描述

5.1.2 动图演示(以从小到大排序为例)

5.1.3 python 代码

5.1.4 算法评估

5.2 基数排序

5.2.1 算法描述

5.2.2 动图演示(以从小到大排序为例)

5.2.3 python 代码

5.2.4 算法评估

5.3 桶排序

5.3.1 算法描述

5.3.2 动图演示(以从小到大排序为例)

5.3.3 python 代码

5.3.4 算法评估


常见的10种排序算法:

排序算法的分类:

(1)分类一:常用的排序算法(主要指面试中)

包含两大类:

    ① 一类是基础比较模型的,也就是排序的过程,是建立在两个数进行对比得出大小基础上,这样的排序算法又可以分为两类:一类是基于数组的,一类是基于树的;基础数组的比较排序算法主要有:冒泡法插入法选择法归并法快速排序法;基础树的比较排序算法主要有:堆排序二叉树排序

    ② 基于非比较模型的排序,主要有桶排序位图排序(个人认为这两个属于同一思路的两个极端)

(2)分类二:

一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序选择排序插入排序归并排序堆排序快速排序等。

另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序基数排序桶排序等。

(3)分类三

十种常见排序算法一般分为以下几种: 

    ① 非线性时间比较类排序:交换类排序(快速排序冒泡排序)、插入类排序(简单插入排序希尔排序)、选择类排序(简单选择排序堆排序)、归并排序(二路归并排序多路归并排序);

    ② 线性时间非比较类排序计数排序基数排序桶排序

总结: 

(1)在比较类排序中,归并排序号称最快,其次是快速排序和堆排序,两者不相伯仲,但是有一点需要注意,数据初始排序状态对堆排序不会产生太大的影响,而快速排序却恰恰相反。

(2)线性时间非比较类排序一般要优于非线性时间比较类排序,但前者对待排序元素的要求较为严格,比如计数排序要求待排序数的最大值不能太大,桶排序要求元素按照hash分桶后桶内元素的数量要均匀。线性时间非比较类排序的典型特点是以空间换时间

相关概念:

① 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

② 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

③ 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

④ 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

⑤ in-place algorithm:在计算机科学中,一个原地算法(in-place algorithm)是一种使用小的,固定数量的额外之空间来转换资料的算法。当算法执行时,输入的资料通常会被要输出的部份覆盖掉。不是原地算法有时候称为非原地(not-in-place)或不得其所(out-of-place)。在计算复杂性理论中,原地算法包含使用O(1)空间复杂度的所有算法,DSPACE(1)类型。这个类型是非常有限的;它与正规语言1相等。

1 交换类排序

1.1 冒泡排序(Bubble Sort)

1.1.1 算法描述

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.1.2 动图演示(以从小到大排序为例)

1.1.3 python 代码

def bubbleSort(L):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    for i in range(length):
        for j in range(length-1-i):
            if L[j] > L[j+1]:
                temp = L[j]
                L[j] = L[j+1]
                L[j+1] = temp
    print (L)
    return L
    pass

if __name__ == '__main__':
    L=[2,4,1,10,45,24,48,97,3,5]
    bubbleSort(L)

1.1.4 算法评估

(1)冒泡排序的优点

每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。如上例:第一趟比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,每进行一趟比较,每一趟少比较一次,一定程度上减少了算法的量。

(2)时间复杂度来说:

  1.如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。

  2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:

冒泡排序的最坏时间复杂度为:O(n^2) 。

综上所述:冒泡排序总的平均时间复杂度为:O(n^2) 

空间复杂度:O(1),因为只是用了2个循环变量以及1到2个标志和交换等的中间变量,这个与待排序的记录个数无关。

1.2 快速排序

1.2.1 算法描述

快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

一趟快速排序的算法是:

1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;

2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];

3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;

4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;

5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

概括来说为 挖坑填数+分治法

下面举例来进行说明,主要有三个参数,i为区间的开始地址,j为区间的结束地址,X为当前的开始的值。

第一步,i=0,j=9,X=21

第二步,从j开始由,后向前找,找到比X小的第一个数a[7]=4,此时i=0,j=6,X=21 进行替换

第三步,由前往后找,找到比X大的第一个数a[1]=32,此时i=2,j=6,X=21 

第四步,从j=6开始由,由后向前找,找到比X小的第一个数a[0]=4,此时i=2,j=0,X=21,发现j<=i,所以第一回结束 可以发现21前面的数字都比21小,后面的数字都比21大。 接下来对两个子区间[0,0]和[2,9]重复上面的操作即可。

下面直接给出过程:

i=2,j=6,X=43

i=4,j=6,X=43

i=4,j=5,x=43

i=5,j=5,x=43

然后被分为了两个子区间[2,3]和[5,9]

…最后排序下去就是最终的答案

1.2.2 动图演示(以从小到大排序为例)

http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html

1.2.3 python 代码

#QuickSort by Alvin

def QuickSort(myList,start,end):
    #判断low是否小于high,如果为false,直接返回
    if start < end:
        i,j = start,end
        #设置基准数
        base = myList[i]
        while i < j:
            #如果列表后边的数,比基准数大或相等,则前移一位直到有比基准数小的数出现
            while (i < j) and (myList[j] >= base):
                j = j - 1
            #如找到,则把第j个元素赋值给第i个元素,此时表中i,j个元素相等
            myList[i] = myList[j]

            #同样的方式比较前半区
            while (i < j) and (myList[i] <= base):
                i = i + 1
            myList[j] = myList[i]
        #做完第一轮比较之后,列表被分成了两个半区,并且i=j,需要将这个数设置回base
        myList[i] = base

        #递归前后半区
        QuickSort(myList, start, i - 1)
        QuickSort(myList, j + 1, end)
    return myList

myList = [49,38,65,97,76,13,27,49]
print("Quick Sort: ")
QuickSort(myList,0,len(myList)-1)
print(myList)

1.2.4 算法评估

        快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。

        当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。

每个值的概念:

(1)快速排序的性能取决于递归树的深度。

(2)在最优情况下,递归树深度log2n。(数量级也属于logn)

partition每次均匀划分,如果排序n个关键字,递归树深度为.log2n. + 1(.x.表示对x向下取整),即仅需递归log2n次。

(3)在最坏情况下,递归树深度n。

待排序的序列为正序或者逆序(即有序),每次划分得到的序列只会减少一个记录,另一个为空序列。此时的递归树,是一棵斜树。

(4)在平均情况下,递归树深度数量级也为logn。

平均情况,也就是说基准元素位于第k(1<=k<=n)个位置。

(5)无论划分好坏,每次划分之后都需要进行n次比较;

结论:

快速排序的时间复杂度 = 递归树的深度*每层比较的次数

所以,当最优情况以及一般情况下,时间复杂度为O(N*logN),最坏情况下,时间复杂度为O(N*N)。

2 插入类排序 

2.1 简单插入排序

2.1.1 算法描述

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

2.1.2 动图演示(以从小到大排序为例)

2.1.3 python 代码

def insertSort(L):
    assert(type(L)==type(['']))
    length = len(L)
    if length==0 or length==1:
        return L
    for i in range(1,length):
        value = L[i]
        j = i-1
        while j>=0 and L[j]>value:
            L[j+1] = L[j]
            j-=1
        L[j+1] = value
    return L

if __name__ == '__main__':
    L=[2,4,1,10,45,24,48,97,3,5]
    small=insertSort(L)
    print(small)

2.1.4 算法评估

        插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。       

        时间复杂度,插入排序的时间复杂度就是判断比较次数有多少,而比较次数与待排数组的初始顺序有关。当待排数组有序时,没有移动操作,此时复杂度为O(N);当待排数组是逆序时,比较次数达到最大--对于下标 i 处的元素,需要比较 i-1 次,总的比较次数:1+2+...+N-1。由于仍然需要两层循环,插入排序的时间复杂度仍然为O(n*n)

        定理:N个互异数的数组的平均逆序数是 N(N-1)/4,可知:基于相邻元素之间的比较和交换的算法的时间复杂度的一个下界为O(N^2)

        空间复杂度:可以看出,算法中只用到了一个临时变量,故空间复杂度为O(1)

       比较次数:在第一轮排序中,插入排序最多比较一次;在第二轮排序中插入排序最多比较二次;以此类推,最后一轮排序时,最多比较N-1次,因此插入排序的最多比较次数为1+2+...+N-1=N*(N-1)/2。尽管如此,实际上插入排序很少会真的比较这么多次,因为一旦发现左侧有比目标元素小的元素,比较就停止了,因此,插入排序平均比较次数为N*(N-1)/4

  移动次数:插入排序的移动次数与比较次数几乎一致,但移动的速度要比交换的速度快得多。

  综上,插入排序的速度约比冒泡排序快一倍(比较次数少一倍),比选择排序还要快一些,对于基本有序的数据,插入排序的速度会很快,是简单排序中效率最高的排序算法。

2.2 希尔排序

2.2.1 算法描述

        1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

2.2.2 动图演示(以从小到大排序为例)

2.2.3 python 代码

代码1:

#!/usr/bin/env python
# coding:utf-8

def shellSort(nums):
    # 设定步长
    step = int(len(nums)/2)
    while step > 0:
        for i in range(step, len(nums)):
            # 类似step = step/2插入排序, 当前值与指定步长之前的值比较, 符合条件则交换位置
            while i >= step and nums[i-step] > nums[i]:
                nums[i], nums[i-step] = nums[i-step], nums[i]
                i -= step
        step = int(step/2)
    return nums

if __name__ == '__main__':
    nums = [2,4,1,10,45,24,48,97,3,5]
    small=shellSort(nums)
    print(small)

其中,交换两个变量:

a=2
b=4
a,b=b,a
print(a)
print(b)

结果为:4  2  

代码2:

def ShellInsetSort(array, len_array, dk):  # 直接插入排序
    for i in range(dk, len_array):  # 从下标为dk的数进行插入排序
        position = i
        current_val = array[position]  # 要插入的数

        index = i
        j = int(index / dk)  # index与dk的商
        index = index - j * dk

        # position>index,要插入的数的下标必须得大于第一个下标
        while position > index and current_val < array[position-dk]:
            array[position] = array[position-dk]  # 往后移动
            position = position-dk
        else:
            array[position] = current_val


def ShellSort(array, len_array):  # 希尔排序
    dk = int(len_array/2)  # 增量
    while(dk >= 1):
        ShellInsetSort(array, len_array, dk)
        print(">>:",array)
        dk = int(dk/2)

if __name__ == "__main__":
    array = [49, 38, 65, 97, 76, 13, 27, 49, 55, 4]
    print(">:", array)
    ShellSort(array, len(array))

2.2.4 算法评估

对希尔排序的时间复杂度分析很困难,在特定情况下可以准确的估算排序码的比较次数和元素移动的次数,但要想弄清楚排序码比较次数和元素移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。 

从增量的初始值选取,到逐渐变为1,将所有用过的增量组成一个序列,就是增量序列。而希尔排序的增量序列选择直接影响它的时间复杂度。最简单的增量就是希尔鼓励使用的希尔增量。增量初始值选择为N/2(N为数组长度),然后每次将增量除以2,得到下一个增量。所以它的增量序列为{N/2, (N / 2)/2, ..., 1}。除了希尔增量还有Hibbard 增量Knuth增量等等都是很复杂的数学公式。

使用希尔增量,在最坏的情况下时间复杂度仍为O(n^2),而使用hibbard增量在最坏的情况下却为O(n3/2)。

3 选择类排序

3.1 选择排序(Selection Sort)

3.1.1 算法描述

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 选择排序是不稳定的排序方法

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

3.1.2 动图演示(以从小到大排序为例)

3.1.3 python 代码

def selectSort(L):
    assert (type(L) == type(['']))
    length = len(L)
    if length == 0 or length == 1:
        return L

    def _min(s):
        smallest = s
        for i in range(s, length):
            if L[i] < L[smallest]:
                smallest = i
        return smallest

    for i in range(length):
        smallest = _min(i)
        if i != smallest:
            temp = L[smallest]
            L[smallest] = L[i]
            L[i] = temp
    print(L)
    return L


if __name__ == '__main__':
    L=[2,4,1,10,45,24,48,97,3,5]
    small=selectSort(L)
    print(small)

结果:

[1, 2, 3, 4, 5, 10, 24, 45, 48, 97]

3.1.4 算法评估

    选择排序是不稳定的排序方法。例如,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。 

时间复杂度:简单选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有 N 个元素,则比较次数永远都是N (N - 1) / 2。

移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0。当序列反序时,移动次数最多,为3N (N - 1) /  2

所以,综上,简单排序的时间复杂度为 O(N^2)。

表现最稳定的排序算法之一,因为无论什么数据进去都是 O(N^2)时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

3.2 堆排序

3.2.1 算法描述

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

        (1)堆

        堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如图:

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中如下图:

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

      (2)堆排序

        堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

步骤一: 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

1.假设给定无序序列结构如下

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

b.重新调整结构,使其继续满足堆定义

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

简单总结下堆排序的基本思路:

  a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

3.2.2 动图演示(以从小到大排序为例)

3.2.3 python 代码

def heapSort(L):
    assert (type(L) == type(['']))
    length = len(L)
    if length == 0 or length == 1:
        return L

    def sift_down(L, start, end):
        root = start
        while True:
            child = 2 * root + 1
            if child > end: break
            if child + 1 <= end and L[child] < L[child + 1]:  #左孩子与右孩子进行比较,若右孩子大,则指向右孩子
                child += 1
            if L[root] < L[child]:
                L[root], L[child] = L[child], L[root]
                root = child
            else:
                break

    for start in range(int((len(L) - 2) / 2), -1, -1): #循环构造初始堆
        sift_down(L, start, len(L) - 1)

    for end in range(len(L) - 1, 0, -1): #进行n-1趟堆排序,每一趟堆排序的元素个数减1
        L[0], L[end] = L[end], L[0]
        sift_down(L, 0, end - 1)
    return L

if __name__ == '__main__':
    L=[2,4,1,10,45,24,48,97,3,5]
    print(heapSort(L))

3.2.4 算法评估

      堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

        由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

        堆排序是就地排序,辅助空间为O(1).

        它是不稳定的排序方法。

4 归并排序

4.1 二路归并排序

4.1.1 算法描述

        归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 

        二路归并的基本思路:将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2(最后一个有序序列的长度可能为1)的有序表;将这些有序序列再次两两归并,得到n/4个长度为4(最后一个有序序列的长度可能小于4)的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

4.1.2 动图演示(以从小到大排序为例)

分而治之,例如:

1.分阶段

        可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程,递归深度为logn。

2、治阶段

再来看看阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

4.1.3 python 代码

# -*- coding:utf-8 -*-

def MergeSort(input_list):
    '''
    函数说明:归并排序(升序)
    Parameters:
        input_list - 待排序列表
    Returns:
        sorted_list - 升序排序好的列表
    '''

    def merge(input_list, left, mid, right, temp):
        '''
        函数说明:合并函数
        Parameters:
            input_list - 待合并列表
            left - 左指针
            right - 右指针
            temp - 临时列表
        Returns:
            无
        '''
        i = left
        j = mid + 1
        k = 0

        while i <= mid and j <= right: #在第一段和第二段均未扫描完时循环
            if input_list[i] <= input_list[j]: #将第一段的元素放入temp中
                temp[k] = input_list[i]
                i += 1
            else:  #将第二段的元素放入temp中
                temp[k] = input_list[j]
                j += 1
            k += 1

        while i <= mid:  #将第一段余下元素复制到temp中
            temp[k] = input_list[i]
            i += 1
            k += 1
        while j <= right:   #将第二段余下元素复制到temp中
            temp[k] = input_list[j]
            j += 1
            k += 1

        k = 0
        while left <= right:    #将temp复制回input_list中
            input_list[left] = temp[k]
            left += 1
            k += 1

    def merge_sort(input_list, left, right, temp):
        if left >= right:
            return;
        mid = (right + left) // 2  #取商,折半分解
        merge_sort(input_list, left, mid, temp)
        merge_sort(input_list, mid + 1, right, temp)
        merge(input_list, left, mid, right, temp)

    if len(input_list) == 0:
        return []
    sorted_list = input_list
    temp = [0] * len(sorted_list)
    merge_sort(sorted_list, 0, len(sorted_list) - 1, temp)
    return sorted_list


if __name__ == '__main__':
    input_list = [6, 4, 8, 9, 2, 3, 1]
    print('排序前:', input_list)
    sorted_list = MergeSort(input_list)
    print('排序后:', sorted_list)

4.1.4 算法评估

(1)、归并排序算法的性能

排序(7):归并排序

其中,log2n为以2为底,n的对数。

(2)、时间复杂度

归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(n*log2n)

(3)、空间复杂度

由前面的算法说明可知,算法处理过程中,需要一个大小为n的临时存储空间用以保存合并序列。

(4)、算法稳定性

在归并排序中,相等的元素的顺序不会改变,所以它是稳定的算法。

(5)、归并排序和堆排序、快速排序的比较

若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。

若从稳定性来考虑,应选取归并排序,因为堆排序和快速排序都是不稳定的。

若从平均情况下的排序速度考虑,应该选择快速排序。 

4.2 多路归并排序

        将k个已经排序的数组归并成一个大的排序的结果数组。我们常用的两种方法一种是建立一个最小k堆,然后每次取最小的元素,也就是堆顶的元素。然后再调整。还有一种就是建立一棵胜者树。其实思路也类似,每次取最顶端的元素,这个元素就是胜者,也就是最小的那个元素。然后从胜者树所在的叶节点对应的序列里取下一个元素。然后再进行比较向上调整。这两种方法都有一个需要注意的地方就是需要根据当前的操作节点来确定该节点所处的序列。从某种角度来说,k路归并算法对于处理大规模的数据有非常重要的意义。

5 线性时间非比较类排序

5.1 计数排序

5.1.1 算法描述

        计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序堆排序

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

5.1.2 动图演示(以从小到大排序为例)

5.1.3 python 代码

#! /usr/bin/env python
#coding=utf-8

#计数排序
# 对每一个输入元素x,确定小于x的元素个数。利用这一信息,就可以直接把x放到它输出数组中的位置上了。
# 例如:如果有17个元素小于x,则x就应该放在第18个输出位置上。
# 当有几个元素相同时,这一方案要略作修改。

def CountingSort(a, b, k):
    #c=[0]*(k+1) #let c[0...k] be an all 0 array
    #c=[0 for i in range(0,k+1)]
    c=[]
    for i in range(k+1):
        c.append(0)
    for j in range(len(a)):
        c[a[j]] = c[a[j]] + 1
    for i in range(1, k+1):
        c[i] = c[i] + c[i-1]
    for j in range(len(a)-1, -1, -1):
        b[c[a[j]]-1] = a[j]#!!!!!减一是关键
        c[a[j]] = c[a[j]] - 1
    print (b)

if __name__ == '__main__':
    a=[2, 5, 3, 0, 2, 3, 0, 3]
    #b=[0]*len(a)
    b=[None for i in range(len(a))]
    CountingSort(a, b, max(a))

5.1.4 算法评估

       计数排序是是牺牲空间复杂度来使时间复杂度达到线性增长。 计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k)空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。但是计数排序对输入有限制,并不是所有情况下都能使用这种排序算法。

5.2 基数排序

5.2.1 算法描述

       基数排序属于“分配式排序”(distribution sort),是非比较类线性时间排序的一种,又称“桶子法”(bucket sort),顾名思义,它是透过键值的部分信息,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。

       基数排序(以整形为例),将整形10进制按每位拆分,然后从低位到高位依次比较各个位。主要分为两个过程:
  (1)分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中(比如64,个位为4,则放入4号桶中);
  (2)收集,再将放置在0~9号桶中的数据按顺序放到数组中;
  重复(1)(2)过程,从个位到最高位(比如32位无符号整形最大数4294967296,最高位为第10位)。

算法描述:

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

        基数排序的方式可以采用最低位优先LSD(Least significant digital)最高位优先MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。 

以【520 350 72 383 15 442 352 86 158 352】序列为例,排序过程描述如下: 

第一步:

第二步:

第三步:

5.2.2 动图演示(以从小到大排序为例)

5.2.3 python 代码

import math

def sort(a, radix=10):
    """a为整数列表, radix为基数"""
    K = int(math.ceil(math.log(max(a), radix)))  # 用K位数可表示任意整数
    print(K)
    bucket = [[] for i in range(radix)]  # 不能用 [[]]*radix
    print(bucket)
    for i in range(1, K + 1):  # K次循环
        for val in a:
            bucket[int(val % (radix ** i) / (radix ** (i - 1)))].append(val)  # 析取整数第K位数字 (从低到高)

        del a[:]
        for each in bucket:
            a.extend(each)  # 桶合并
        bucket = [[] for i in range(radix)]
    return a

if __name__ == '__main__':
    L = [520, 350, 72, 383, 15, 442, 352, 86, 158, 352]
    print(sort(L))


5.2.4 算法评估

       时间复杂度:在基数推序过程中,共进行了d遍(例如三位数字的d=3)的分配和收集,每一遍分配和收集的时间为O(n+r),所以基数排序的时间复杂度为O(d(n+r))。
        空间复杂度:基数排序中一越排序需要的辅助存储空间为r(创建r个队列,十进制中,r=10),但以后的各题排序中重
复使用这些空间,所以总的空间复杂度为O(r)
        另外,基数排序中使用的是队列,排在后面的关键字只能排在前面相同关键字的后面,相对位置不会发生改变,它是一种稳定的排序方法。

5.3 桶排序

5.3.1 算法描述

桶排序也是分配排序的一种,但其是基于比较排序的,这也是与基数排序最大的区别所在。

思想:桶排序算法想法类似于散列表。首先要假设待排序的元素输入符合某种均匀分布,例如数据均匀分布在[ 0,1)区间上,则可将此区间划分为10个小区间,称为,对散布到同一个桶中的元素再排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

要求:待排序数长度一致。

排序过程: 
(1)设置一个定量的数组当作空桶子; 
(2)寻访序列,并且把记录一个一个放到对应的桶子去; 
(3)对每个不是空的桶子进行排序。 
(4)从不是空的桶子里把项目再放回原来的序列中。

例如:待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序。

5.3.2 动图演示(以从小到大排序为例)

5.3.3 python 代码

代码一:

# coding:utf-8

def bucket_sort(array, n):
    # 1.创建n个空桶
    new_list = [[] for _ in range(n)]

    # 2.把arr[i] 插入到bucket[n*array[i]]
    for data in array:
        index = int(data * n)
        new_list[index].append(data)

    # 3.桶内排序
    for i in range(n):
        new_list[i].sort()

    # 4.产生新的排序后的列表
    index = 0
    for i in range(n):
        for j in range(len(new_list[i])):
            array[index] = new_list[i][j]
            index += 1
    return array


def main():
    array = [0.897, 0.565, 0.656, 0.1234, 0.665, 0.3434]
    n = len(array)
    array = bucket_sort(array, n)
    print(array)


if __name__ == '__main__':
    main()

代码二:
 

def bucketSort(nums):
    # 选择一个最大的数
    max_num = max(nums)
    # 创建一个元素全是0的列表, 当做桶
    bucket = [0]*(max_num+1)
    # 把所有元素放入桶中, 即把对应元素个数加一
    for i in nums:
        bucket[i] += 1

    # 存储排序好的元素
    sort_nums = []
    # 取出桶中的元素
    for j in range(len(bucket)):
        if bucket[j] != 0:
            for y in range(bucket[j]):
                sort_nums.append(j)

    return sort_nums

nums = [5,6,3,2,1,65,2,0,8,0]
print (bucketSort(nums))

5.3.4 算法评估

然后遍历数组A,读入Ai时,S[Ai]增一。所有输入被读进后,扫描数组S得出排好序的表。该算法时间花费O(M+N),空间上不能原地排序。

时间复杂度: 

N个关键字进行桶排序的时间复杂度分为两个部分: 

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,对于N个待排数据M个桶,平均每个桶[N/M]个数据,则桶内排序的时间复杂度为 ∑i=1MO(Ni∗logNi)=O(N∗logNM) 。其中Ni 为第i个桶的数据量。 因此,平均时间复杂度为线性的O(N+C)C为桶内排序所花费的时间。当每个桶只有一个数,则最好的时间复杂度为:O(N)

猜你喜欢

转载自blog.csdn.net/weixin_39910711/article/details/82598091