经典排序算法总结

这一部分我们对面试时涉及到的排序算法进行总结,主要包括插入排序、二分插入排序、希尔排序、选择排序、冒泡排序、鸡尾酒排序、快速排序、堆排序、归并排序、桶排序、计数排序和基数排序,不多说,下面进入主题。

一、插入排序

1)算法简介

插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用O(1)额外空间的排序),因而在从后往前扫描过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。

2)算法描述

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

  1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描

  3. 如果该元素(已排序)大于新元素,将该元素移动下一位置

  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

  5. 将新元素插入到该位置后

  6. 重复步骤2~5

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。

3)算法代码

def insert_sort(nums):
    if not nums:
        return
    n =len(nums)
    for i in range(1, n):
        temp =nums[i]
        j =i-1
        
        while j >=0 and nums[j] >temp:
            nums[j+1] =nums[j]
            j -=1
        nums[j+1] =temp
     return nums
二、二分插入排序

1)算法简介

二分插入排序是一种在直接插入排序算法上进行小改动的排序算法,其与直接排序算法最大的区别在于查找插入位置时使用的是二分查找的方式,在速度上有一定提升。

2)算法描述

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

  1. 从第一个元素开始,该元素可以认为已经被排序

  2. 取出下一个元素,在已经排序的元素序列中二分查找到第一个比它大的数的位置

  3. 将新元素插入到该位置后

  4. 重复上述两步

稳定、空间代价:O(1)、时间代价:插入每个记录需要O(log i)比较,最多移动i+1次,最少2次,最佳情况O(nlogn),最差和平均情况O(n^2)。

二分插入排序是一种稳定的排序。当n较大时,总排序比较次数比直接插入排序的最差情况好得多,但比最好情况差,当元素初始序列接近有序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

3)算法代码

def BinInsert_sort(nums):
    if not nums:
        return 
    
    n =len(nums)
    for i in range(1, n):
        key =nums[i]
        left =0
        right =i-1
        while left <=right:
            middle =(left +right)//2
            if nums[middle] >key:
                right =middle-1
            else:
                left =middle+1
        
        for j in range(i-1, left-1, -1):
            nums[j+1] =nums[j]
        nums[left] =key
        
    return nums
三、希尔排序

1)算法简介

希尔排序,也称递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种高效的改进版本,但其是非稳定排序算法。

2)算法描述

  1. 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组

  2. 所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序

  3. 取第二个增量d2<d1重复上述的分组和排序

  4. 直至所取的增量dt=1(dt<dt-1<......<d2<d1),即所有记录放在同一组中进行直接插入排序为止

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n2),而Hibbard增量的希尔排序的时间复杂度为O(n(5/4)),但是至今仍然没有人能找出希尔排序的精确下界。

3)算法代码

def ShellSort(nums):
    if not nums:
        return
    n =len(nums)
    h =1
    while h<n//3:
        h =3*h+1
    while h>=1:
        for i in range(h, n):
            j =i-h
            temp =nums[i]
            while j>=0 and nums[j]>temp:
                nums[j+h] =nums[j]
                j -=h
            nums[j+h] =temp
        h =h//3
    return nums
四、选择排序

1)算法简介

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

2)算法描述

n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:

  1. 初始状态:无序区为R[1...n],有序区为空

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

  3. 前n-1趟结束,数组有序化了

选择排序的交换操作介于0和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2,交换次数O(n),最好情况是已经有序,交换0次;最坏情况是逆序,交换n-1次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n^2)
平均时间复杂度 O(n^2)
最差空间复杂度 O(n) total, O(1)

3)算法代码

def SelectionSort(nums):
    if not nums:
        return
    n =len(nums)
    for i in range(n):
        min =i
        for j in range(i+1,n):
            if nums[min] >nums[j]:
                min =j
        if min != i:
            nums[min], nums[i] =nums[i], nums[min]
    return nums
五、冒泡排序

1)算法简介

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

2)算法描述

  1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个

  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数

  3. 针对所有的元素重复以上的步骤,除了最后一个

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较

冒泡排序是与插入排序拥有相等的执行时间,但是两种方法在需要的交换次数却很大的不同。在最坏的情况,冒泡排序需要O(n2)次交换,而插入排序只需要最多O(n)次交换。冒泡排序的实现通常会对已经排序好的数列拙劣的执行O(n2),而插入排序只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这种情况下,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微的改进效率,有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差空间复杂度 总共O(n),需要辅助空间O(1)

3)算法代码

def BubbleSort(nums):
    if not nums:
        return
    n =len(nums)
    while n>0:
        for j in range(n-1):
            if nums[j] >nums[j+1]:
                nums[j], nums[j+1] =nums[j+1], nums[j]
        n -=1
    return nums
六、鸡尾酒排序/双向冒泡排序

1)算法简介

鸡尾酒排序等于是冒泡排序的轻微变形,不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。它可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环时只移动一个项目。

2)算法描述

  1. 依次比较相邻的两个数,将小数放在前面,大数放在后面

  2. 第一趟可得到:将最大数放到最后一位

  3. 第二趟可得到:将第二大的数放到倒数第二位

  4. 如此下去,重复以上过程,直至最终完成排序

鸡尾酒排序最糟或是平均所花费的次数都是O(n^2),但是如果序列在一开始已经大部分排序过的话,会接近O(n)。

最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)

3)算法代码

def CoktailSort(nums):
    if not nums:
        return
    n =len(nums)-1
    i =0
    while i<n:
        #将最小的数排到前面
        for j in range(n, i, -1):
            if nums[j] <nums[j-1]:
                nums[j], nums[j-1] =nums[j-1], nums[j]
        i +=1
        #将最大的数排到后面
        for j in range(i, n):
            if nums[j] >nums[j+1]:
                nums[j], nums[j+1] =nums[j+1], nums[j]
        n -=1
    return nums
七、快速排序

1)算法简介

快速排序是由东尼 霍尔所发展的一种排序算法,其基本思想是,通过一趟排序将待排记录分隔成独立两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

2)算法描述

快速排序使用分治法来把一个串分为两个子串:

  1. 从数列中挑出一个元素,称为基准(pivot)

  2. 重新排列数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列中间位置。这个称为分区(partition)操作。

  3. 递归的把小于基准值元素的子数列和大于基准值元素的子数列排序

递归到最底部情形是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次迭代中,它至少会把一个元素摆到它最后的位置去。

在平均状况下,排序n个项目要O(nlogn)次比较,在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其它O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率的被实现出来。

最差时间复杂度 O(n^2)
最优时间复杂度 O(nlogn)
平均时间复杂度 O(nlogn)
最差空间复杂度 根据实现的方式不同而不同

3)算法代码

def QuickSort(nums, left, right):
    if not nums:
        return
    if left <right:
        i, j =left, right
        #准备以本次最左边的元素值为标准进行划分,先保存其值
        pivot =nums[left]
        while i !=j:
            #从右向左找第1个小于标准值的位置j
            while nums[j] >pivot and i <j:
                j -=1
            if i <j:
                nums[i] =nums[j] #将第j个元素置于左端并重置i
                i +=1
            #从左向右找第1个大于标准值的位置i
            while nums[i] <pivot and i <j:
                i +=1
            if i <j:
                nums[j] =nums[i]
                j -=1
        nums[i] =pivot
        QuickSort(nums, left, i-1)
        QuickSort(nums, i+1, right)
    return nums
八、堆排序

1)算法简介

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

2)算法描述

  1. 什么是堆?

    我们这里提到的堆一般都指的是二叉堆,它满足二个特性:

    • 父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值

    • 每个父节点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)

  2. 什么是堆调整?

    这是为了保持堆的特性而做的一个操作。堆某一个节点为根的子树做堆调整,其实就是将该根节点进行“下沉”操作,一直下沉到合适的位置,使得刚才的子树满足堆的性质。

    例如,对最大堆的堆调整会这么做:

    • 在对应的数组元素A[i],左孩子A[left(i)],和右孩子A[right(i)]中找到最大的那一个,将其下标存储在largest中

    • 如果A[i]已经就是最大的元素,则程序直接结束

    • 否则,i的某个子节点为最大的元素,将A[largest]与A[i]交换

    • 再从交换的子节点开始,重复1,2,3步,直至叶子节点,算完成一次堆调整

    这里需要提一下的是,一般做一次堆调整的时间复杂度为log(n)。

  3. 如何建堆?

    建堆是一个通过不断堆调整,使得整个二叉树中的数满足堆性质的操作。在数组中的话,我们一般从下标为n/2的数开始做堆调整,一直到下标为0的数(因为下标大于n/2的数都是叶子节点,其子树已经满足堆的性质了)

  4. 如何进行堆排序?

    堆排序是在上述3中对数组建堆的操作之后完成的。数组存储成堆的形式之后,第一次将A[0]与A[n-1]交换,再对A[0...n-2]重新恢复堆。第二次将A[0]与A[n-2]交换,再对A[0...n-3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。

最差时间复杂度 O(nlogn)
最优时间复杂度 O(nlogn)
平均时间复杂度 O(nlogn)
最差空间复杂度 O(n)

3)算法代码

def heapify(arr, n, i):
    largest =i
    l =2*i +1
    r =2*i +2
    
    if l <n and arr[i] <arr[l]:
        largest =l
    
    if r <n and arr[largest] <arr[r]:
        largest =r
        
    if largest != i:
        arr[i], arr[largest] =arr[largest], arr[i]
        heapify(arr, n, largest)
        
def heapSort(arr):
    n =len(arr)
    #构造大顶堆
    for i in range(n, -1, -1):
        heapify(arr, n, i)
        
    #一个个交换元素
    for i in range(n-1, 0, -1):
        arr[i], arr[0] =arr[0], arr[i]
        heapify(arr, i, 0)
    
    return arr
九、归并排序

1)算法简介

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

2)算法描述

  1. Divide:把长度为n的输入序列分成两个长度为n/2的子序列

  2. Conquer:对这两个子序列分别采用归并排序

  3. Combine:将两个排序好的子序列合并成一个最终的排序序列

归并排序的效率是比较高的,设数列长度为n,将数列分开成小数列一共要logn步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(n),故一共为O(nlogn)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(nlogn)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。

3)算法代码

def mergesort(seq):
    if len(seq) <=1:
        return seq
    mid =len(seq)//2
    #分别对左右两个列表进行处理,分别返回两个排序好的列表
    left =mergesort(seq[:mid])
    right =mergesort(seq[mid:])
    #对排序好的列表合并,产生一个新的排序好的列表
    return merge(left, right)
           
def merge(left, right):
    #合并两个已排序好的列表,产生一个新的已排序号列表
    res =[]
    i =0
    j =0
    #对两个列表中的元素两两对比
    #将最小的元素,放到res中,并对当前列表下标加1
    while i <len(left) and j <len(right):
        if left[i] < right[j]:
            res.append(left[i])
            i +=1
        else:
            res.append(right[j])
            j +=1
            
    res +=left[i:]
    res +=right[j:]
    
    return res
十、桶排序

1)算法简介

桶排序(Bucket sort)或所谓的箱排序,工作原理是将数组分到有限数量的桶子里,每个桶子再个别排序(有可能使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是稳定的,且在大多数情况下常见排序里最快的一种,比快排还要快,缺点是非常耗空间,基本上是最耗空间的一种排序算法,而且只能在某些情形下使用。

2)算法描述

  1. 设置一个定量的数组当作空桶子

  2. 寻访串行,并且把项目一个一个放到对应的桶子里

  3. 对每个不是空的桶子进行排序

  4. 从不是空的桶子里把项目放回原来的串行中

桶排序最好情况下使用线性时间O(n),很显然桶排序的时间复杂度,取决于对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n);很显然,桶划分的越大,各个桶之间的数据越少,排序所用的时间也会越小,但相应的空间消耗就会增大。可以证明,即使选用插入排序作为桶内排序的方法,桶排序的平均时间复杂度为线性。

3)算法代码

def bucket_sort(a):
    #初始化桶元素为0
    buckets =[0] *(max(a) -min(a) +1)
    for  i in range(len(a)):
        #遍历数组a,在桶的相应位置累加值
        buckets[a[i] -min(a)] +=1
        
    b =[]
    for i in range(len(buckets)):
        if buckets[i] !=0:
            b +=[i+min(a)]*buckets[i]
            
    return b
十一、计数排序

1)算法简介

计数排序是一种稳定的排序算法,计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置,它只能对整数进行排序。

2)算法描述

  1. 找出待排序的数组最大和最小的元素

  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项

  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

当输入的元素是n个0到k之间的整数时,它的运行时间是O(n+k),计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。

3)算法代码

def count_sort(a, k): #k =max(a)
    n =len(a)
    b =[0 for i in range(n)] #设置输出序列并初始化为0
    c =[0 for i in range(k+1)] #设置计数序列并初始化为0
    for j in a:
        c[j] =c[j] +1
    
    for i in range(1, len(c)):
        c [i] =c[i] +c[i-1]
        
    for j in a:
        b[c[j] -1] =j
        c[j] =c[j] -1
        
    return b
十二、基数排序

1)算法简介

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

2)算法描述

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零

  2. 从最低位开始,依次进行一次排序

  3. 这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列

基数排序的时间复杂度是O(kn),其中n是排序元素个数,k是数字位数

3)算法代码

def radix_sort(list, d=3): #默认三位数,如果是四位数,则d=4,以此类推
    for i in range(d): #d轮排序
        s =[[] for k in range(10)]   #因每一位数字都是0-9,建10个桶
        for j in list:
            s[int(j/(10**i))%10].append(j)
        re =[a for b in s for a in b]
        
    return re
更多精彩,关注“咋家”

推荐阅读更多精彩内容

  • 查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会...
    JourWon阅读 1,620评论 0 34
  • 本文主要整理了九种经典的内部排序算法。 1.冒泡排序 原理: 冒泡排序是一种简单的排序算法。它重复地走访过要排序的...
    爱听故事的人想会讲故事阅读 950评论 0 2
  • 学习Go语言第二周,本周任务尝试实现七大经典排序算法以及分析算法复杂度、优劣及应用场景等,七大经典算法分别为冒泡排...
    BigLong阅读 1,255评论 0 13
  • 1 一个男人,不愿意带相处多年的女朋友回家,是不是意味着,他不够爱她? 倒是身为男人的高强,在心里替许曼提出了这个...
    大圣72阅读 86评论 0 0
  • 家长角度:引导孩子建立梦想,逐步通过行动达成目标,让孩子时刻体验到自主和充实的感觉; 孩子角度:给孩子设定目标和任...
    简简单_a1ba阅读 448评论 0 0