十大排序算法图解+Python实现

目录

一、交换排序

1、冒泡排序(Bubble Sort)

2、快速排序(Quick Sort)

二、插入排序

1、简单插入排序(Insert Sort)

2、希尔排序(Shell Sort)

三、选择排序

1、简单选择排序(Select Sort)

2、堆排序(Heap Sort)

四、归并排序

1、二路归并排序(Two-way Merge Sort)

五、线性时间非比较类排序

1、计数排序(Counting Sort)

2、桶排序(Bucket Sort)

3、基数排序(Radix Sort) 

六、总结


 一、交换排序

1、冒泡排序(Bubble Sort)

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

冒泡排序对n个数据操作n-1轮,每轮找出一个最大(小)值。

操作只对相邻两个数比较与交换,每轮会将一个最值交换到数据列首(尾),像冒泡一样。

每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。

额外空间开销出在交换数据时那一个过渡空间,空间复杂度O(1)

图解:我们以[8,2,5,9,7]这组数字来做示例,上图来战,我们从左往右依次冒泡,将小的往右移动

首先比较第一个数和第二个数的大小,我们发现2比8要小,那么保持原位,不做改动。位置还是8,2,5,9,7。

指针往右移动一格,接着比较:

比较第二个数和第三个数的大小,发现2比5要小,所以位置交换,交换后数组更新为:[8,5,2,9,7]。

  指针再往右移动一格,继续比较:

  比较第三个数和第四个数的大小,发现2比9要小,所以位置交换,交换后数组更新为:[8,5,9,2,7]

  同样,指针再往右移动,继续比较:

  比较第4个数和第5个数的大小,发现2比7要小,所以位置交换,交换后数组更新为:[8,5,9,7,2]

  下一步,指针再往右移动,发现已经到底了,则本轮冒泡结束,处于最右边的2就是已经排好序的数字。

  通过这一轮不断的对比交换,数组中最小的数字移动到了最右边。

  接下来继续第二轮冒泡:

  由于右边的2已经是排好序的数字,就不再参与比较,所以本轮冒泡结束,本轮冒泡最终冒到顶部的数字5也归于有序序列中,现在数组已经变化成了[8,9,7,5,2]。

让我们开始第三轮冒泡吧!

  由于8比7大,所以位置不变,此时第三轮冒泡也已经结束,第三轮冒泡的最后结果是[9,8,7,5,2]

  紧接着第四轮冒泡:

  9和8比,位置不变,即确定了8进入有序序列,那么最后只剩下一个数字9,放在末尾,自此排序结束。

实现思路: 使用双重for循环,内层变量为i, 外层为j,在内层循环中不断的比较相邻的两个值(i, i+1)的大小,如果i+1的值大于i的值,交换两者位置,每循环一次,外层的j增加1,等到j等于n-1的时候,结束循环。

初始代码:

def BubbleSort(lst):
    n=len(lst)
    if n<=1:
        return lst
    for i in range (0,n):
        for j in range(0,n-i-1):
            if lst[j]>lst[j+1]:
                lst[j],lst[j+1]=lst[j+1],lst[j]
    return lst
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=BubbleSort(arr)
print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ')

关键点其实在双重for循环变量的配置,我们来分析一下
第一次循环: j = 0, i~n-2 range(0, n-1)
第二次循环: j = 1, i~n-3 range(0, n-1-1)
第三次循环: j = 2, i~n-4 range(0, n-1-1-1)
—> range(0, n-1-j)

理解这一点后,我们就可以换一种写法来实现了

def BubbleSort(lst):
    n=len(lst)
    if n<=1:
        return lst
    for i in range (n-1,0,-1):
        for j in range(0,i):
            if lst[j]>lst[j+1]:
                lst[j],lst[j+1] = lst[j+1],lst[j]
    return lst
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=BubbleSort(arr)
print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ')

优化:冒泡有一个最大的问题就是这种算法不管不管你有序还是没序,闭着眼睛把你循环比较了再说,无论是第一种写法还是第二种写法,时间复杂度都是O(n ^ 2), 第二种写法也仅仅停留在优化样式的层面,并没有带来性能的提升,果我们输入的本来就是一个有序序列,其实只需要一次循环就够了,比如:[9,8,7,6,5],一个有序的数组,根本不需要排序,它仍然是双层循环一个不少的把数据遍历干净,这其实就是做了没必要做的事情,属于浪费资源。针对这个问题,我们可以设定一个临时遍历来标记该数组是否已经有序,如果有序了就不用遍历了。

def BubbleSort(lst):
    n=len(lst)
    count = 0
    if n<=1:
        return lst
    for i in range (n-1,0,-1):
        for j in range(0,i):
            if lst[j] > lst[j+1]:
                lst[j],lst[j+1] = lst[j+1],lst[j]
                count+=1
    if count == 0:
        return lst
    return lst
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=BubbleSort(arr)
print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ')

2、快速排序(Quick Sort)

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

快速排序基于选择划分,是简单选择排序的优化。

每次划分将数据选到基准值两边,循环对两边的数据进行划分,类似于二分法。

算法的整体性能取决于划分的平均程度,即基准值的选择,此处衍生出快速排序的许多优化方案,甚至可以划分为多块。

基准值若能把数据分为平均的两块,划分次数O(logn),每次划分遍历比较一遍O(n),时间复杂度O(nlogn)。

额外空间开销出在暂存基准值,O(logn)次划分需要O(logn)个,空间复杂度O(logn)

图解:快速排序的核心思想也是分治法,分而治之。它的实现方式是每次从序列中选出一个基准值,其他数依次和基准值做比较,比基准值大的放右边,比基准值小的放左边,然后再对左边和右边的两组数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后都变成单个元素,整个数组就成了有序的序列。

我们以[8,2,5,0,7,4,6,1]这组数字来进行演示

  首先,我们随机选择一个基准值:

与其他元素依次比较,大的放右边,小的放左边:

然后我们以同样的方式排左边的数据:

继续排0和1:

由于只剩下一个数,所以就不用排了,现在的数组序列是下图这个样子:

右边以同样的操作进行,即可排序完成。

代码:

def QuickSort(lst):
    # 此函数完成分区操作
    def partition(arr, left, right):
        key = left  # 划分参考数索引,默认为第一个数为基准数,可优化
        while left < right:
            # 如果列表后边的数,比基准数大或相等,则前移一位直到有比基准数小的数出现
            while left < right and arr[right] >= arr[key]:
                right -= 1
            # 如果列表前边的数,比基准数小或相等,则后移一位直到有比基准数大的数出现
            while left < right and arr[left] <= arr[key]:
                left += 1
            # 此时已找到一个比基准大的书,和一个比基准小的数,将他们互换位置
            (arr[left], arr[right]) = (arr[right], arr[left])
 
        # 当从两边分别逼近,直到两个位置相等时结束,将左边小的同基准进行交换
        (arr[left], arr[key]) = (arr[key], arr[left])
        # 返回目前基准所在位置的索引
        return left
 
    def quicksort(arr, left, right):  
        if left >= right:
            return
        # 从基准开始分区
        mid = partition(arr, left, right)
        # 递归调用
        # print(arr)
        quicksort(arr, left, mid - 1)
        quicksort(arr, mid + 1, right)
 
    # 主函数
    n = len(lst)
    if n <= 1:
        return lst
    quicksort(lst, 0, n - 1)
    return lst
 
print("<<< Quick Sort >>>")
x = input("请输入待排序数列:\n")
y = x.split()
arr = []
for i in y:
    arr.append(int(i))
arr = QuickSort(arr)
# print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i, end=' ')

快速排序的时间复杂度和归并排序一样,O(n log n),但这是建立在每次切分都能把数组一刀切两半差不多大的前提下,如果出现极端情况,比如排一个有序的序列,如[9,8,7,6,5,4,3,2,1],选取基准值9,那么需要切分n-1次才能完成整个快速排序的过程,这种情况下,时间复杂度就退化成了O(n^2),当然极端情况出现的概率也是比较低的。

所以说,快速排序的时间复杂度是O(nlogn),极端情况下会退化成O(n^2),为了避免极端情况的发生,选取基准值应该做到随机选取,或者是打乱一下数组再选取。

二、插入排序

1、简单插入排序(Insert Sort)

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

插入排序的思想和我们打扑克摸牌的时候一样,从牌堆里一张一张摸起来的牌都是乱序的,我们会把摸起来的牌插入到左手中合适的位置,让左手中的牌时刻保持一个有序的状态。那如果我们不是从牌堆里摸牌,而是左手里面初始化就是一堆乱牌呢? 一样的道理,我们把牌往手的右边挪一挪,把手的左边空出一点位置来,然后在乱牌中抽一张出来,插入到左边,再抽一张出来,插入到左边,再抽一张,插入到左边,每次插入都插入到左边合适的位置,时刻保持左边的牌是有序的,直到右边的牌抽完,则排序完毕。

简单插入排序同样操作n-1轮,每轮将一个未排序树插入排好序列。

开始时默认第一个数有序,将剩余n-1个数逐个插入。插入操作具体包括:比较确定插入位置,数据移位腾出合适空位

每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。

额外空间开销出在数据移位时那一个过渡空间,空间复杂度O(1)。

图解:

数组初始化:[8,2,5,9,7],我们把数组中的数据分成两个区域,已排序区域和未排序区域,初始化的时候所有的数据都处在未排序区域中,已排序区域是空。

第一轮,从未排序区域中随机拿出一个数字,既然是随机,那么我们就获取第一个,然后插入到已排序区域中,已排序区域是空,那么就不做比较,默认自身已经是有序的了。(当然了,第一轮在代码中是可以省略的,从下标为1的元素开始即可)

第二轮,继续从未排序区域中拿出一个数,插入到已排序区域中,这个时候要遍历已排序区域中的数字挨个做比较,比大比小取决于你是想升序排还是想倒序排,这里排升序:

第三轮,排5:

第四轮,排9:

第五轮,排7

  排序结束。

代码:

def InsertSort(lst):
    n=len(lst)
    if n<=1:
        return lst
    for i in range(1,n):
        j=i
        target=lst[i]            #每次循环的一个待插入的数
        while j>0 and target<lst[j-1]:       #比较、后移,给target腾位置
            lst[j]=lst[j-1]
            j=j-1
        lst[j]=target            #把target插到空位
    return lst
 
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=InsertSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ')

2、希尔排序(Shell Sort)

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

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

希尔排序是插入排序的高效实现(大家可以比对一下插入排序和希尔排序的代码),对简单插入排序减少移动次数优化而来。

简单插入排序每次插入都要移动大量数据,前后插入时的许多移动都是重复操作,若一步到位移动效率会高很多。

若序列基本有序,简单插入排序不必做很多移动操作,效率很高。

希尔排序将序列按固定间隔划分为多个子序列,在子序列中简单插入排序,先做远距离移动使序列基本有序;逐渐缩小间隔重复操作,最后间隔为1时即简单插入排序。

希尔排序对序列划分O(n)次,每次简单插入排序O(logn),时间复杂度O(nlogn)

额外空间开销出在插入过程数据移动需要的一个暂存,空间复杂度O(1)


图解:希尔排序这个名字,来源于它的发明者希尔,也称作“缩小增量排序”,是插入排序的一种更高效的改进版本。我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。

待排序数组 10个数据:

假设计算出的排序区间为4,那么我们第一次比较应该是用第5个数据与第1个数据相比较。

调换后的数据为[7,2,5,9,8,10,1,15,12,3],然后指针右移,第6个数据与第2个数据相比较。

指针右移,继续比较。

如果交换数据后,发现减去区间得到的位置还存在数据,那么继续比较,比如下面这张图,12和8相比较,原地不动后,指针从12跳到8身上,继续减去区间发现前面还有一个下标为0的数据7,那么8和7相比较。

比较完之后的效果是7,8,12三个数为有序排列。

 

  当最后一个元素比较完之后,我们会发现大部分值比较大的数据都似乎调整到数组的中后部分了。

  假设整个数组比较长的话,比如有100个数据,那么我们的区间肯定是四五十,调整后区间再缩小成一二十还会重新调整一轮,直到最后区间缩小为1,就是真正的排序来了。

指针右移,继续比较:

  重复步骤,即可完成排序,重复的图就不多画了。

  我们可以发现,当区间为1的时候,它使用的排序方式就是插入排序。

代码:

def ShellSort(lst):
    def shellinsert(arr,d):
        n=len(arr)
        for i in range(d,n):
            j=i-d
            temp=arr[i]             #记录要出入的数
            while(j>=0 and arr[j]>temp):    #从后向前,找打比其小的数的位置
                arr[j+d]=arr[j]                 #向后挪动
                j-=d
            if j!=i-d:
                arr[j+d]=temp
    n=len(lst)
    if n<=1:
        return lst
    d=n//2
    while d>=1:
        shellinsert(lst,d)
        d=d//2
    return lst
 
 
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=ShellSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

三、选择排序

1、简单选择排序(Select Sort)

选择排序的思路是这样的:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。至于选大还是选小,这个都无所谓,你也可以每次选择最大的拎出来排,也可以每次选择最小的拎出来的排,只要你的排序的手段是这种方式,都叫选择排序。

  • 初始状态:无序区为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趟结束,数组有序化了。

简单选择排序同样对数据操作n-1轮,每轮找出一个最大(小)值。

操作指选择,即未排序数逐个比较交换,争夺最值位置,每轮将一个未排序位置上的数交换成已排序数,即每轮选一个最值。

每轮操作O(n)次,共O(n)轮,时间复杂度O(n^2)。

额外空间开销出在交换数据时那一个过渡空间,空间复杂度O(1)。

图解:我们还是以[8,2,5,9,7]这组数字做例子。

  第一次选择,先找到数组中最小的数字2,然后和第一个数字交换位置。(如果第一个数字就是最小值,那么自己和自己交换位置,也可以不做处理,就是一个if的事情)

 

  第二次选择,由于数组第一个位置已经是有序的,所以只需要查找剩余位置,找到其中最小的数字5,然后和数组第二个位置的元素交换。

 

  第三次选择,找到最小值7,和第三个位置的元素交换位置。

 

  第四次选择,找到最小值8,和第四个位置的元素交换位置。

  最后一个到达了数组末尾,没有可对比的元素,结束选择。

  如此整个数组就排序完成了。

代码:

def SelectSort(lst):
    n=len(lst)
    if n<=1:
        return lst
    for i in range(0,n-1):
        minIndex=i
        for j in range(i+1,n):          #比较一遍,记录索引不交换
            if lst[j]<lst[minIndex]:
                minIndex=j
        if minIndex!=i:                     #按索引交换
            (lst[minIndex],lst[i])=(lst[i],lst[minIndex])
    return lst
 
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=SelectSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ')

 优化:既然第一次找到了最大数,那是否能在第一次顺便把最小数也找出来,然后把最小数排到最后去,答案当然是可以。而且如果经过一轮比较,发现最小值和最大值相等,那就说明实际上剩下的值都是相等的了。

def select_sort(a):
    count_iter = 0
    count_swap = 0
    for i in range(len(a) // 2):   # 从第一个数开始进行迭代
        maxindex = i
        minindex = -i - 1
        minorign = minindex
        for j in range(i + 1, len(a) - i):   # 依次找出第一个最大的数的下标
            count_iter += 1
            if a[maxindex] < a[j]:
                maxindex = j
            if a[minindex] > a[-j - 1]:
                minindex = -j - 1
        if a[maxindex] == a[minindex]:   # 如果相等,剩余元素相同
            break
        if i != maxindex:   # 判断该数字位置是否在正确的位置,否就进行替换
            a[maxindex], a[i] = a[i], a[maxindex]
            count_swap += 1
            if (i == minindex) or (i == (len(a) + minindex)):  # 判断最小值是否被交换过
                minindex = maxindex
        if minorign != minindex:
            a[minindex], a[minorign] = a[minorign], a[minindex]
            count_swap += 1
    print("The end list is:\n{},count_swap:{},count_iter:{}".format(a,count_swap,count_iter))

if __name__ == '__main__':
    list = []
    for i in range(10):
        list.append(random.randrange(1,101))
    print("my original list:\n{}".format(list))
    select_sort(list)

双层循环,时间复杂度和冒泡一模一样,都是O(n^2)。是表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

2、堆排序(Heap Sort)

堆排序顾名思义,是利用堆这种数据结构来进行排序的算法。

  如果你了解堆这种数据结构,你应该知道堆是一种优先队列,两种实现,最大堆和最小堆,由于我们这里排序按升序排,所以就直接以最大堆来说吧。

  我们完全可以把堆(以下全都默认为最大堆)看成一棵完全二叉树,但是位于堆顶的元素总是整棵树的最大值,每个子节点的值都比父节点小,由于堆要时刻保持这样的规则特性,所以一旦堆里面的数据发生变化,我们必须对堆重新进行一次构建。

  既然堆顶元素永远都是整棵树中的最大值,那么我们将数据构建成堆后,只需要从堆顶取元素不就好了吗? 第一次取的元素,是否取的就是最大值?取完后把堆重新构建一下,然后再取堆顶的元素,是否取的就是第二大的值? 反复的取,取出来的数据也就是有序的数据。

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
     

堆排序的初始建堆过程比价复杂,对O(n)级别个非叶子节点进行堆调整操作O(logn),时间复杂度O(nlogn);之后每一次堆调整操作确定一个数的次序,时间复杂度O(nlogn)。合起来时间复杂度O(nlogn)

额外空间开销出在调整堆过程,根节点下移交换时一个暂存空间,空间复杂度O(1)

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

我们以[8,2,5,9,7,3]这组数据来演示。

  首先,将数组构建成堆。

既然构建成堆结构了,那么接下来,我们取出堆顶的数据,也就是数组第一个数,9,取法是将数组的第一位和最后一位调换,然后将数组的待排序范围-1,

现在的待排序数据是[3,8,5,2,7],我们继续将待排序数据构建成堆。

取出堆顶数据,这次就是第一位和倒数第二位交换了,因为待排序的边界已经减1。

继续构建堆

从堆顶取出来的数据最终形成一个有序列表,重复的步骤就不再赘述了,我们来看一下代码实现。

代码:

def  HeapSort(lst):
    def heapadjust(arr,start,end):  #将以start为根节点的堆调整为大顶堆
        temp=arr[start]
        son=2*start+1
        while son<=end:
            if son<end and arr[son]<arr[son+1]:  #找出左右孩子节点较大的
                son+=1
            if temp>=arr[son]:       #判断是否为大顶堆
                break
            arr[start]=arr[son]     #子节点上移
            start=son                     #继续向下比较
            son=2*son+1
        arr[start]=temp             #将原堆顶插入正确位置
#######
    n=len(lst)
    if n<=1:
        return lst
    #建立大顶堆
    root=n//2-1    #最后一个非叶节点(完全二叉树中)
    while(root>=0):
        heapadjust(ls,root,n-1)
        root-=1
    #掐掉堆顶后调整堆
    i=n-1
    while(i>=0):
        (lst[0],lst[i])=(lst[i],lst[0])  #将大顶堆堆顶数放到最后
        heapadjust(lst,0,i-1)    #调整剩余数组成的堆
        i-=1
    return lst
#########
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=HeapSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

四、归并排序

1、二路归并排序(Two-way Merge Sort)

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

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

图解:归并算法的核心思想是分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕。

我们以[8,2,5,9,7]这组数字来举例

 

首先,一刀切两半:

再切:

再切:

粒度切到最小的时候,就开始归并

数据量设定的比较少,是为了方便图解,数据量为单数,是为了让你看到细节,下面我画了一张更直观的图可能你会更喜欢:

代码:

def MergeSort(lst):
    #合并左右子序列函数
    def merge(arr,left,mid,right):
        temp=[]     #中间数组
        i=left          #左段子序列起始
        j=mid+1   #右段子序列起始
        while i<=mid and j<=right:
            if arr[i]<=arr[j]:
                temp.append(arr[i])
                i+=1
            else:
                temp.append(arr[j])
                j+=1
        while i<=mid:
            temp.append(arr[i])
            i+=1
        while j<=right:
            temp.append(arr[j])
            j+=1
        for i in range(left,right+1):    #  !注意这里,不能直接arr=temp,他俩大小都不一定一样
            arr[i]=temp[i-left]
    #递归调用归并排序
    def mSort(arr,left,right):
        if left>=right:
            return
        mid=(left+right)//2
        mSort(arr,left,mid)
        mSort(arr,mid+1,right)
        merge(arr,left,mid,right)
 
    n=len(lst)
    if n<=1:
        return lst
    mSort(lst,0,n-1)
    return lst
 
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=MergeSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

五、线性时间非比较类排序

1、计数排序(Counting Sort)

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

计数排序用待排序的数值作为计数数组(列表)的下标,统计每个数值的个数,然后依次输出即可。

计数数组的大小取决于待排数据取值范围,所以对数据有一定要求,否则空间开销无法承受。

计数排序只需遍历一次数据,在计数数组中记录,输出计数数组中有记录的下标,时间复杂度为O(n+k)。

额外空间开销即指计数数组,实际上按数据值分为k类(大小取决于数据取值),空间复杂度O(k)。

图解:计数排序是一种非基于比较的排序算法,我们之前介绍的各种排序算法几乎都是基于元素之间的比较来进行排序的,计数排序的时间复杂度为O(n+m),m指的是数据量,说的简单点,计数排序算法的时间复杂度约等于O(n),快于任何比较型的排序算法。

以下以[3,5,8,2,5,4]这组数字来演示。

  首先,我们找到这组数字中最大的数,也就是8,创建一个最大下标为8的空数组arr。

 

  遍历数据,将数据的出现次数填入arr中对应的下标位置中。

 

  遍历arr,将数据依次取出即可。

代码:

def CountSort(lst):
    n=len(lst)
    num=max(lst)
    count=[0]*(num+1)
    for i in range(0,n):
        count[lst[i]]+=1
    arr=[]
    for i in range(0,num+1):
        for j in range(0,count[i]):
            arr.append(i)
    return arr
 
 
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=CountSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

2、桶排序(Bucket Sort)

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。 

桶排序实际上是计数排序的推广,但实现上要复杂许多。

桶排序先用一定的函数关系将数据划分到不同有序的区域(桶)内,然后子数据分别在桶内排序,之后顺次输出。

当每一个不同数据分配一个桶时,也就相当于计数排序。

假设n个数据,划分为k个桶,桶内采用快速排序,时间复杂度为O(n)+O(k * n/k*log(n/k))=O(n)+O(n*(log(n)-log(k))),

显然,k越大,时间复杂度越接近O(n),当然空间复杂度O(n+k)会越大,这是空间与时间的平衡。
图解:桶排序可以看成是计数排序的升级版,它将要排的数据分到多个有序的桶里,每个桶里的数据再单独排序,再把每个桶的数据依次取出,即可完成排序。

我们拿一组计数排序啃不掉的数据[500,6123,1700,10,9999]来举例。

  第一步,我们创建10个桶,分别来装0-1000、1000-2000、2000-3000、3000-4000、4000-5000、5000-6000、6000-7000、7000-8000、8000-9000区间的数据。

 第二步,遍历原数组,对号入桶。

第三步,对桶中的数据进行单独排序,只有第一个桶中的数量大于1,显然只需要排第一个桶。

最后,依次将桶中的数据取出,排序完成。

 代码:

def BucketSort(lst):
    ##############桶内使用快速排序
    def QuickSort(lst):
        def partition(arr,left,right):
            key=left         #划分参考数索引,默认为第一个数,可优化
            while left<right:
                while left<right and arr[right]>=arr[key]:
                    right-=1
                while left<right and arr[left]<=arr[key]:
                    left+=1
                (arr[left],arr[right])=(arr[right],arr[left])
            (arr[left],arr[key])=(arr[key],arr[left])
            return left
 
        def quicksort(arr,left,right):   #递归调用
            if left>=right:
                return
            mid=partition(arr,left,right)
            quicksort(arr,left,mid-1)
            quicksort(arr,mid+1,right)
        #主函数
        n=len(lst)
        if n<=1:
            return lst
        quicksort(lst,0,n-1)
        return lst
    ######################
    n=len(lst)
    big=max(lst)
    num=big//10+1
    bucket=[]
    buckets=[[] for i in range(0,num)]
    for i in lst:
        buckets[i//10].append(i)     #划分桶
    for i in buckets:                       #桶内排序
        bucket=QuickSort(i)
    arr=[]
    for i in buckets:
        if isinstance(i, list):
            for j in i:
                arr.append(j)
        else:
            arr.append(i)
    for i in range(0,n):
        lst[i]=arr[i]
    return lst
    
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=BucketSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

3、基数排序(Radix Sort) 

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

图解:基数排序是一种非比较型整数排序算法,其原理是将数据按位数切割成不同的数字,然后按每个位数分别比较。

假设说,我们要对100万个手机号码进行排序,应该选择什么排序算法呢?排的快的有归并、快排时间复杂度是O(nlogn),计数排序和桶排序虽然更快一些,但是手机号码位数是11位,那得需要多少桶?内存条表示不服。

  这个时候,我们使用基数排序是最好的选择。

我们以[892, 846, 821, 199, 810,700]这组数字来做例子演示。

  首先,创建十个桶,用来辅助排序。

 

  先排个位数,根据个位数的值将数据放到对应下标值的桶中。

 

  排完后,我们将桶中的数据依次取出。

  那么接下来,我们排十位数。

 

  最后,排百位数。

 排序完成。

代码:

import math
def RadixSort(lst):
    def getbit(x,i):       #返回x的第i位(从右向左,个位为0)数值
        y=x//pow(10,i)
        z=y%10
        return z
    def CountSort(lst):
        n=len(lst)
        num=max(lst)
        count=[0]*(num+1)
        for i in range(0,n):
            count[lst[i]]+=1
        arr=[]
        for i in range(0,num+1):
            for j in range(0,count[i]):
                arr.append(i)
        return arr
    Max=max(lst)
    for k in range(0,int(math.log10(Max))+1):             #对k位数排k次,每次按某一位来排
        arr=[[] for i in range(0,10)]
        for i in lst:                 #将ls(待排数列)中每个数按某一位分类(0-9共10类)存到arr[][]二维数组(列表)中
            arr[getbit(i,k)].append(i)
        for i in range(0,10):         #对arr[]中每一类(一个列表)  按计数排序排好
            if len(arr[i])>0:
                arr[i]=CountSort(arr[i])
        j=9
        n=len(lst)
        for i in range(0,n):     #顺序输出arr[][]中数到ls中,即按第k位排好
            while len(arr[j])==0:
                j-=1
            else:
                ls[n-1-i]=arr[j].pop()   
    return lst    
    
x=input("请输入待排序数列:\n")
y=x.split()
arr=[]
for i in  y:
    arr.append(int(i))
arr=RadixSort(arr)
#print(arr)
print("数列按序排列如下:")
for i in arr:
    print(i,end=' ') 

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

六、总结

十种常见排序算法可以分为两大类:

  • 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
  • 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

相关概念 

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

 others:

1.存在即有理。十种排序算法在时间、空间复杂度,实现难度,稳定性等指标上存在较大差异,但并没有最好最坏之说,适合的才是最好的。

2.三种O(n^2)平均时间复杂度的排序算法在空间复杂度、稳定性方面表现较好,甚至在特定情况下即便考虑时间复杂度也是最佳选择。

3.堆排序初始建堆过程较复杂,仅建堆时间复杂度就达到O(nlogn),但之后的排序开销稳定且较小,所以适合大量数据排序。

4.希尔排序性能看似很好,但实际上他的整体性能受步长选取影响较大,插入排序本质也使他受数据影响较大。

5.归并排序在平均和最坏情况下时间复杂度都表现良好O(nlogn),但昂贵的空间开销大O(n)。

6.快速排序大名鼎鼎,又有个好名字,但最坏情况下时间复杂度直逼O(n^2),远不如堆排序和归并排序。

7.基于比较排序的算法(如前七种)时间复杂度O(nlogn)已是下限。

8.三种线性时间复杂度排序算法虽然在速度上有决定性的优势,但也付出了沉重的空间代价,有时数据的特点让这种空间代价变得无法承受。所以他们的应用对数据本身有着特定的要求。

9.关于稳定性,希尔排序、快速排序和堆排序这三种排序算法无法保障。三种算法因为划分(子序列、大小端、左右孩子)后各自处理无法保证等值数据的原次序。
 

附:

c++实现:https://blog.csdn.net/aiya_aiya_/article/details/88370855

Java实现:https://www.cnblogs.com/jztan/p/5878630.html

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值