十大排序算法(python)

十大排序算法

排序算法介绍

非线性时间比较类排序:

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

线性时间非比较类排序:

不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。
在这里插入图片描述

相关概念

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

交换排序

1冒泡排序(Bubble Sort)

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

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

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

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

额外空间开销出在交换数据时那一个过渡空间,空间复杂度O(1)
在这里插入图片描述

def buddle_sort(arr):
    n=len(arr)
    for i in range(n-1):
        count=0
        for j in range(0,n-i-1):
            if arr[j]>arr[j+1]:
                arr[j],arr[j+1]=arr[j+1],arr[j]
                count+=1
        if count==0:
            break
if __name__=="__main__":
    li=[5,9,6,3,4,2,7]
    print(li)
    buddle_sort(li)
    print(li)
	

2 快速排序(Quick Sort)

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

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

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

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

额外空间开销出在暂存基准值,O(logn)次划分需要O(logn)个,空间复杂度O(logn)
在这里插入图片描述

def quick_sort(arr,first,last):
    if first>=last:
        return
    n=len(arr)
    left=first
    right=last
    mid_value=arr[first]
    while left<right:
        while left<right and arr[right]>=mid_value:
            right-=1
        arr[left]=arr[right]
        while left<right and arr[left]<mid_value:
            left+=1
        arr[right]=arr[left]
    arr[left]=mid_value
    quick_sort(arr,first,left-1)
    quick_sort(arr,left+1,last)

if __name__=="__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    quick_sort(li,0,len(li)-1)
    print(li)

插入排序

3 简单插入排序(Insert Sort)

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

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

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

额外空间开销出在数据移位时那一个过渡空间,空间复杂度O(1)。在这里插入图片描述

def insert_sort(arr):
    n=len(arr)
    for i in range(1,n):
        j=i
        while j>0:
            if arr[j]<arr[j-1]:
                arr[j-1],arr[j]=arr[j],arr[j-1]
                j-=1
            else:
                break


if __name__=="__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    insert_sort(li)
    print(li)

4希尔排序(Shell Sort)

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

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    希尔排序是插入排序的高效实现(大家可以比对一下插入排序和希尔排序的代码),对简单插入排序减少移动次数优化而来。

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

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

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

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

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

在这里插入图片描述
(1)希尔排序(shell sort)这个排序方法又称为缩小增量排序,是1959年D·L·Shell提出来的。该方法的基本思想是:设待排序元素序列有n个元素,首先取一个整数increment(小于n)作为间隔将全部元素分为increment个子序列,所有距离为increment的元素放在同一个子序列中,在每一个子序列中分别实行直接插入排序。然后缩小间隔increment,重复上述子序列划分和排序工作。直到最后取increment=1,将所有元素放在同一个子序列中排序为止。
(2)由于开始时,increment的取值较大,每个子序列中的元素较少,排序速度较快,到排序后期increment取值逐渐变小,子序列中元素个数逐渐增多,但由于前面工作的基础,大多数元素已经基本有序,所以排序速度仍然很快。
(3)希尔排序举例:
在这里插入图片描述
第一趟取increment的方法是:n/3向下取整+1=3(关于increment的取法之后会有介绍)。将整个数据列划分为间隔为3的3个子序列,然后对每一个子序列执行直接插入排序,相当于对整个序列执行了部分排序调整。图解如下: 在这里插入图片描述
第二趟将间隔increment= increment/3向下取整+1=2,将整个元素序列划分为2个间隔为2的子序列,分别进行排序。图解如下:在这里插入图片描述
第3趟把间隔缩小为increment= increment/3向下取整+1=1,当增量为1的时候,实际上就是把整个数列作为一个子序列进行插入排序,图解如下: 在这里插入图片描述
到increment=1时,就是对整个数列做最后一次调整,因为前面的序列调整已经使得整个序列部分有序,所以最后一次调整也变得十分轻松,这也是希尔排序性能优越的体现。
关于希尔排序increment(增量)的取法。
增量increment的取法有各种方案。最初shell提出取increment=n/2向下取整,increment=increment/2向下取整,直到increment=1。但由于直到最后一步,在奇数位置的元素才会与偶数位置的元素进行比较,这样使用这个序列的效率会很低。后来Knuth提出取increment=n/3向下取整+1.还有人提出都取奇数为好,也有人提出increment互质为好。应用不同的序列会使希尔排序算法的性能有很大的差异。

def shell_sort(arr):
    n=len(arr)
    gap=n//2
    while gap>0:
        for i in range(1,n):
            j=i
            while j>0:
                if arr[j]<arr[j-gap]:
                    arr[j-gap],arr[j]=arr[j],arr[j-gap]
                    j-=gap
                else:
                    break
        gap//=2


if __name__=="__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    shell_sort(li)
    print(li)



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


选择排序

5.简单选择排序(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)。
在这里插入图片描述

def select_sort(arr):
    n=len(arr)
    for i in range(n-1):
        min_index=i
        for j in range(i+1,n):
            if arr[min_index]>arr[j]:
                min_index=j
        arr[i],arr[min_index]=arr[min_index],arr[i]


if __name__=="__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    select_sort(li)
    print(li)

堆排序

选择排序最大的问题,就是不能知道待排序数据是否已经有序,比较了所有数据也没有在比较中确定数据的顺序。

堆排序对简单选择排序进行了改进。

准备知识

  • 堆排序是利用 堆进行排序的
  • 堆是一种完全二叉树
  • 堆有两种类型: 大根堆 小根堆

两种类型的概念如下:

  • 大根堆:每个结点的值都大于或等于左右孩子结点
  • 小根堆:每个结点的值都小于或等于左右孩子结点

因为比较抽象,所以专门花了两个图表示

在这里插入图片描述
在这里插入图片描述
完全二叉树 是 一种除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐的树,向左对齐指的是:在这里插入图片描述
像这样的树就不是完全二叉树:在这里插入图片描述
如果给上面的大小根堆的根节点从1开始编号,则满足下面关系(下图就满足这个关系):
在这里插入图片描述
如果把这些数字放入数组中,则如下图所示:其中,上面的数字是数组下标值,第一个元素占位用。
在这里插入图片描述
堆排序算法详解+Python实现
了解了堆。下面我们来看下堆排序的思想是怎样的(以大根堆为例):

  • 首先将待排序的数组构造出一个大根堆
  • 取出这个大根堆的堆顶节点(最大值),与堆的最下最右的元素进行交换,然后把剩下的元素再构造出一个大根堆
  • 重复第二步,直到这个大根堆的长度为1,此时完成排序。

下面通过图片来看下,第二个步骤是如何进行的:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
参考

from collections import deque
def swap(arr,i,j):
    arr[i],arr[j]=arr[j],arr[i]
    return arr
def heap_adjust(arr,start,end):

    temp=arr[start]
    i=start
    j=2*i
    while j <=end:
        if (j<end) and (arr[j]<arr[j+1]):
            j+=1
        if temp<arr[j]:
            arr[i]=arr[j]
            i=j
            j=2*i
        else:
            break
    arr[i]=temp
def heap_sort(arr):
    length=len(arr)-1
    start=length//2

    for i in range(start):
        heap_adjust(arr,start-i,length)

    for i in range(length-1):
        swap(arr,1,length-i)
        heap_adjust(arr,1,length-i-1)
    return [arr[i] for i in range(1,len(arr))]
def main():
    arr=deque([50, 16, 30, 10, 60, 90, 2, 80, 70])
    arr.appendleft(0)
    print(heap_sort(arr))
if __name__=="__main__":
    main()

归并排序

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

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

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

在这里插入图片描述

def merge_sort(arr):
    n=len(arr)
    if n<=1:
        return arr
    mid=n//2
    left=merge_sort(arr[:mid])
    right=merge_sort(arr[mid:])
    return merge(left,right)
def merge(left,right):
    l,r=0,0
    res=[]
    while l<len(left) and r<len(right):
        if left[l]<right[r]:
            res.append(left[l])
            l+=1
        else:
            res.append(right[r])
            r+=1
    res+=right[r:]
    res+=left[l:]
    return res


if __name__=="__main__":
    li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(li)
    sorted_merge=merge_sort(li)
    print(sorted_merge)

线性时间非比较类排序

8 计数排序

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

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

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

额外空间开销即指计数数组,实际上按数据值分为k类(大小取决于数据取值),空间复杂度O(k)。
在这里插入图片描述

def count_sort(arr):
    n=len(arr)
    num=max(arr)
    count=[0]*(num+1)

    for i in range(n):
        count[arr[i]]+=1
    arr=[]
    for i in range(num+1):
        for j in range(count[i]):
            arr.append(i)
    return arr

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

9 桶排序(Bucket Sort)

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。
    桶排序实际上是计数排序的推广,但实现上要复杂许多。

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

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


def bucket_sort(s):
    """桶排序"""
    min_num = min(s)
    max_num = max(s)
    # 桶的大小
    bucket_range = (max_num-min_num) / len(s)
    # 桶数组
    count_list = [ [] for i in range(len(s) + 1)]
    # 向桶数组填数
    for i in s:
        count_list[int((i-min_num)//bucket_range)].append(i)
    s.clear()
    # 回填,这里桶内部排序直接调用了sorted
    for i in count_list:
        for j in sorted(i):
            s.append(j)

if __name__ == '__main__':
    a = [3.2,6,8,4,7,6,7,9]
    bucket_sort(a)
    print(a)

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

基数排序(Radix Sort)

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);
    在这里插入图片描述
def radix_sort(s):
    """基数排序"""
    i = 0 # 记录当前正在排拿一位,最低位为1
    max_num = max(s)  # 最大值
    j = len(str(max_num))  # 记录最大值的位数
    while i < j:
        bucket_list =[[] for _ in range(10)] #初始化桶数组
        for x in s:
            bucket_list[int(x / (10**i)) % 10].append(x) # 找到位置放入桶数组
        print(bucket_list)
        s.clear()
        for x in bucket_list:   # 放回原序列
            for y in x:
                s.append(y)
        i += 1

if __name__ == '__main__':
    a = [334,5,67,345,7,345345,99,4,23,78,45,1,3453,23424]
    radix_sort(a)
    print(a)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值