9种排序方法及python实现(冒泡,插入,希尔,选择,堆,快速,桶,基数,归并排序)

1. 排序算法分类

排序算法可以分为 外部排序 和 内部排序:
(1)外部排序 (External sorting)是指能够处理极大量数据的排序算法。

通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。

在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。而后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。

(2)内部排序还可以细分:
需要额外内存空间(out-place,即空间复杂度不是 O ( 1 ) O(1) O(1))的算法有:桶排序,基数排序,归并排序; 其他算法一般只需常量的内存空间(in-place,原地排序)。
在这里插入图片描述

以下介绍9种内部排序:

2. 冒泡法

冒泡法的步骤:从左到右遍历,遍历的元素和后一个比较,如果前一个比后一个大,则交换;第一次遍历后,最大的元素在最右的位置。以相同的方式遍历,次大的元素放置在倒数第2的位置;直到需要比较的元素只有1个为止。

import numpy as np
import time
def bubbleSort(arr):
    # 冒泡法
    for i in range(len(arr)-1, 0, -1):
        for j in range(i):
            if arr[j] > arr[j+1]:
                arr[j],arr[j+1] = arr[j+1],arr[j]
    return arr 
def get_arr(num=10):
	# 生成随机的大小为num的数组
    np.random.seed(1)
    arr = np.random.randint(0,num,(num,))
    return arr 
    
if __name__ == "__main__":
    # 冒泡法
    arr = get_arr()
    print('arr',arr)
    arr = bubbleSort(arr)
    print(arr)

运行结果:

arr [5 8 9 5 0 0 1 7 6 9]
#排序后 
arr [0 0 1 5 5 6 7 8 9 9]

3. 插入排序

插入排序,是把当前的数插入到前面排序好的序列相应位置,使得序列保持单调性。

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]

def insertionSort(arr):
    for i in range(len(arr)):
        for j in range(i, 0, -1):
            if arr[j] < arr[j-1]:
                swap(arr, j, j-1)
            else:
                break
    return arr 

arr = insertionSort(arr)

4. 希尔排序

希尔排序对插入排序改进,定义一个步长序列,不再只是相邻的两个元素进行比较。

步长序列最坏情况下复杂度
n / 2 i n/2^i n/2i O ( n 2 ) \mathcal{O} {\displaystyle (n^{2})} O(n2)
2 k − 1 {\displaystyle 2^{k}-1} 2k1 O ( n 3 / 2 ) {\displaystyle {\mathcal {O}}} (n^{3/2}) O(n3/2)
2 i 3 j {\displaystyle 2^{i}3^{j}} 2i3j O ( n log ⁡ 2 n ) {\displaystyle {\mathcal {O}}}( n\log^2 n ) O(nlog2n)
def shellSort(arr):
    # 希尔排序
    gap = len(arr) // 2
    while gap > 0:
        for i in range(gap, len(arr)):
            j = i
            while (j >= gap) and (arr[j] < arr[j-gap]):
                swap(arr, j, j-gap)
                j = j-gap
        gap = gap//2
    return arr

5. 选择排序

选择排序是,遍历一遍,找到最小的元素,然后交换到最左边,重复这个过程。和冒泡法相比,它遍历一遍,只交换一次。

def selectSort(arr):
    for i in range(len(arr)):
        min_idx = i
        for j in range(i+1, len(arr)):
            if arr[j] < arr[min_idx]:
                min_idx = j
        swap(arr, min_idx, i)
    return arr 

6. 堆排序

堆排序使用二叉树的数据结构,需要满足,父节点值不小于儿子节点。
堆排序,通过建立一个最大堆,即根节点的值最大,然后把根节点和最后一个叶节点交换;剩下的数重新建立一个最大堆,获得次大值,继续和叶节点交换;重复这个过程,直到堆的大小为1。

def heapSort(arr):
    def siftDown(arr, index, length=None):
        '''sift_down'''
        if length is None:
            length = len(arr)
        # 最大堆;子节点总小于父节点, O(n)
        while True:
            left = 2*index + 1
            right = 2*index + 2
            max_idx = left
            if left >= length:
                break
            if right<length and arr[right] > arr[left]:
                max_idx = right
            if arr[index] < arr[max_idx]:
                arr[index], arr[max_idx] = arr[max_idx],arr[index]
                index = max_idx
            else:
                break
    # 初始化最大堆 O(n)
    for idx in range(len(arr)//2-1, -1, -1):
        siftDown(arr, idx, len(arr))

    # 堆排序,交换最大值和最后一个叶节点, O(nlogn)
    for length in range(len(arr)-1, -1, -1):
        arr[length], arr[0] = arr[0], arr[length]
        siftDown(arr, 0, length)
    return arr 

7. 快速排序

快速排序是常用的排序算法。思想是,选择一个基准,遍历一遍,把数分成两堆,比基准小的放在数组左边,比基准大的放在数组右边,最后,交换基准到两者的分界处;对基准左边和右边的子数组重复进行快速排序,直到子数组只有1个元素。

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]
    
def select_base(arr,low,high):
    # 取三个数的中位数作为基准
    mid = (low + high) // 2
    if (arr[low] - arr[mid])*(arr[low] - arr[high]) < 0:
        return low
    if (arr[mid] - arr[low])*(arr[mid] - arr[high]) < 0:
        return mid
    return high
    
def qsort(arr, low, high):
    left, right = low, high
    if left < right:
        while left < right:
            pivot = select_base(arr, low, high)
            # 基准放在最左边low
            swap(arr, low, pivot)
            # 从右到左遍历,选择比基准小的元素
            while left < right and arr[right] >= arr[low]:
                right -= 1
            # 从左到右遍历,选择比基准大的元素
            while left < right and arr[left] <= arr[low]:
                left += 1
            swap(arr, left, right)
        # 最终 left = right,该位置元素不大于基准
        swap(arr, low, right)
        qsort(arr, low, right-1)
        qsort(arr,right+1, high)
        
def quickSort(arr):
    qsort(arr,0,len(arr)-1)

quickSort(arr)

直到一提是,常见选择基准可以取最左边的元素,但是可能最左边的元素是数组的最小值或者最大值,导致左右子数组的数量相差悬殊,排序效率低的情形。因此,取三个数的平均值作为基准。

此外,在遍历数组时,这里使用了双指针两个方向进行遍历,但如单链表只有一个方向进行遍历,实现的话定义两个指针,一个指针记录两堆的分界,一个指针向前遍历直到找到比基准小的值,然后和分界后一个元素交换,更新两堆的分界。

8. 桶排序和计数排序

桶排序是,把值映射函数后,得到索引,根据索引放入相应的有大小排序的桶,然后桶内的元素进行排序(如插入排序),然后合并所有的桶,获得排序后的序列。

如果桶的数量足够多,和序列的范围一样,那么,每个桶放入和桶本身值一样的数值,记录数量。最后遍历所有的桶,按照数量来打印桶的值,得到排序后的序列,这也叫计数排序

在这里插入图片描述

def bucketSort(arr, radix=10):
    '''radix,基数代表一个桶最大容纳多少个不同的数;
    当radix=1, 桶排序相当于计数排序.'''
    def get_index(val, min_a):
        return (val - min_a) // radix
    max_a = max(arr)
    min_a = min(arr)
    bucket_num = (max_a - min_a)//radix + 1
    bucket = [[] for _ in range(bucket_num)]
    # 放入桶中
    for val in arr:
        index = get_index(val, min_a)
        bucket[index].append(val)
    index = 0
    for bk in bucket:
        # 桶内进行排序
        bk = insertionSort(bk)
        for val in bk:
            arr[index] = val
            index += 1
    return arr

这里桶排序有点像哈希表,都是经过散列函数(映射函数),把值放入相应的键(桶)中。和哈希表不同之处,在于桶排序要求的映射函数必须是单调函数,这样才有意义,哈希表的散列函数需要满足不同的值映射后的值尽可能不一样。

9. 基数排序

基数排序是,先根据各个数的低位,进行排序;排序后的数组,根据次低位进行排序,重复直到完成最高位的比较,输出排序的序列。

def radixSort(arr,radix=10):
    '''基数排序,radix为基数'''
    K = len(str(max(arr)))
    for i in range(K):
        bucket = [[] for _ in range(10)]
        for val in arr:
            if len(str(val))-i >= 1:
                num = int(str(val)[len(str(val))-i-1])
                bucket[num].append(val)
            else:
                bucket[0].append(val)
        # 合并桶
        del arr
        arr = []
        for bk in bucket:
            arr.extend(bk)
    return arr 

这里为了方便,使用了10为基数。基数代表值由什么进制表示。radix 可以取2,当时取位的时候,也得先把值转化成2进制:

val%(radix**i)//(radix**(i-1))

这里 i i i 代表取值 val 在 radix 进制下的第几位。

10. 归并排序

归并排序是通过不断的划分成两个子数组,直到子数组只有1个值(相当于排序好了),然后不断合并两个排序好的子数组,直到合并成原来大小的数组。

def merge(arr, left, mid, right):
    l = left
    r = mid+1
    n_arr = [0 for _ in range(right-left+1)]
    index = 0
    while l <= mid and r<=right:
        if arr[l] > arr[r]:
            n_arr[index] = arr[r]
            r += 1
        else:
            n_arr[index] = arr[l]
            l += 1
        index += 1
    while l <= mid:
        n_arr[index] = arr[l]
        index += 1
        l += 1
    while r <= right:
        n_arr[index] = arr[r]
        index += 1
        r += 1
    # 拷贝排序完成的arr
    for i in range(len(n_arr)):
        arr[left+i] = n_arr[i]
        
def mSort(arr, left, right):
    if left < right:
        mid = (left+right)//2
        mSort(arr, left, mid)
        mSort(arr, mid+1, right)
        merge(arr, left, mid, right)
        
def mergeSort(arr):
    mSort(arr, 0, len(arr)-1)
    return arr
arr = mergeSort(arr)

11. 总结

不同排序方法的复杂度:
在这里插入图片描述

实践测试,选择大小为10000的随机数组,经过这几种算法排序,花费的时间:
比如:

import time
# 冒泡法
arr = get_arr()
start = time.time()
arr = bubbleSort(arr)
end = time.time()
print('冒泡法use {:.4f}'.format(end-start))

运行结果:

# O(n + k)
桶排序use 0.0160
# O(n*k)
基数排序use 0.0748
# O(nlogn)
归并排序use 0.0917
# O(nlogn)
快排use 0.1123
# O(nlogn)
堆排序use 0.1207
# O(nlogn)
希尔排序use 0.1466

# O(n^2)
选择排序use 11.3514
# O(n^2)
插入排序use 17.5552
# O(n^2)
冒泡法use 21.4438

此外,排序算法的稳定性,是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。比如,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
因此,类选择排序(选择,希尔,堆以及快排)都是不稳定排序。


最近开通了个公众号,主要分享推荐系统,风控等算法相关的内容,感兴趣的伙伴可以关注下。
在这里插入图片描述


参考:

  1. (个人blog)排序方法 冒泡排序 快速排序 归并排序 java;
  2. csdn LeetCode分类刷题(三):排序(Sort);
  3. 菜鸟教程 冒泡排序;
  4. 菜鸟教程 快速排序
  5. 菜鸟教程 插入排序
  6. 菜鸟教程 希尔排序;
  7. 菜鸟教程 选择排序;
  8. 菜鸟教程 归并排序
  9. 菜鸟教程 计数排序;
  10. 菜鸟教程 堆排序
  11. wiki 希尔排序;
  12. wiki 基数排序;
  13. wiki 桶排序
  14. wiki 堆排序
  15. wiki 外部排序
  16. 极客 桶排序
  17. 稳定排序和不稳定排序;
  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rosefunR

你的赞赏是我创作的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值