Python实现十大排序算法【小白版】

184 阅读5分钟

常见的内部排序算法有: 插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。

排序.png

一、冒泡排序

bubbleSort.gif 思路: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。较大数再和后一位比较;

每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数;

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

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

def bubbleSort(arr):
    for i in range(1, len(arr)):
        for j in range(0, len(arr)-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

二、选择排序

selectionSort.gif 思路:

  1. 在未排序的数组中找到最小(大)的数,放到起始位置;
  2. 在未排序的数组中找到最小小(大)的数,放到已排序的末尾;
  3. 重复第2步骤;
def selectionSort(arr):
    for i in range(len(arr) - 1):
        # 记录最小数的索引
        minIndex = i
        for j in range(i + 1len(arr)):
            if arr[j] < arr[minIndex]:
                minIndex = j
        # i 不是最小数时,将 i 和最小数进行交换
        if i != minIndex:
            arr[i]arr[minIndex] = arr[minIndex]arr[i]
    return arr

三、插入排序

insertionSort.gif

思路:

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

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

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

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

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

  6. 重复步骤2~5

def insertionSort(arr):
    for i in range(len(arr)):
        preIndex = i - 1
        current = arr[i]
        while preIndex >= 0 and arr[preIndex] > current:
            arr[preIndex+1] = arr[preIndex]
            preIndex-=1
        arr[preIndex+1] = current
    return arr

四、希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

2ec74774ddb8d1b7e58c6e2e4b1cbac3.gif 希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是: 先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。

具体思路:

  1. 计算一个增量(间隔)值

  2. 对元素进行增量元素进行比较,比如增量值为7,那么就对0,7,14,21…个元素进行插入排序

  3. 然后对1,8,15…进行排序,依次递增进行排序

  4. 所有元素排序完后,缩小增量比如为3,然后又重复上述第2,3步

  5. 最后缩小增量至1时,数列已经基本有序,最后一遍普通插入即可

def shell_sort(arr):
    # 取整计算增量(间隔)值
    gap = len(arr) // 2
    while gap > 0:
        # 从增量值开始遍历比较
        for i in range(gap, len(arr)):
            j = i
            current = arr[i]
            # 元素与他同列的前面的每个元素比较,如果比前面的小则互换
            while j - gap >= 0 and current < arr[j - gap]:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = current
        # 缩小增量(间隔)值
        gap //= 2
    return arr

五、归并排序

作为非常典型的分而治之思想的算法应用。

那么什么是分而治之思想?

归并排序采用分治法,把一个复杂的问题不停的去分解成小问题,解决了小问题,就可以倒推解决大问题。

R-Cfz.png 如上图所示有3个步骤:

1.分解:一个列表显示分成2个子列表,2个子列表分别分成2个子列表,不停的分下去,直到最后的列表里只剩1个元素;

2.解决:因为最后的列表都只剩1个元素,他们之间比较大小就非常的方便,然后再从下往上依次用相同的方法解决各个子问题;

3.合并:将解决的问题合并,最终得到结果;

该排序采用了递归的方法,当你不知道某个问题的答案,计算机就会不停的从上往下分解,直到最小的问题得到答案,那么计算机就会从下往上依次返回每个问题的结果。

归并排序的实现有两种方法:

  1. 自上而下归并(由于所有的递归方法都可以采用迭代重写,所以就有了第二种方法);

  2. 自下而上迭代;

用递归法实现归并:

def mergeSort(arr):

    # 因为是递归,所以要有个终止条件,这里的终止条件是最终分成1个元素了就返回只有1个元素的列表
    if(len(arr) <= 1):
        return arr

    # 将列表分1半,比如11//2结果是5
    middle = len(arr)//2

    # 切片,分为左边一部分,右边一部分
    left,right = arr[0:middle],arr[middle:]
    
    # 调用自身函数mergeSort,把左边右边部分传进去,就会继续按照上面代码分成2部分
    # merge函数是用来排序的,并且将排序好的元素放到了新的列表里
    return merge(mergeSort(left),mergeSort(right))

def merge(left,right):

    # 新排序的放到result里
    result = []

    # 当左右两个类标里都有元素时,进行循环
    while left and right:

        # 如果左列表的第1个元素 <= 列表里的第1个元素
        if left[0] <= right[0]:

            # 把左列表的第1个元素用pop函数提取出来,加到新的列表里
            result.append(left.pop(0))
        else:

            # 否则就把右列表的第1个元素提取出来,加到新的列表里
            result.append(right.pop(0))
    
    # 上面循环后,可能某个列表里还有元素,所以继续下面的操作,有的话,直接依次加到新的列表里即可,因为剩下的元素肯定是比已经比较完的大的
    while left:
        result.append(left.pop(0))
    while right:
        result.append(right.pop(0))
    return result

用迭代实现归并:

def myMergeSort(alist):
    n = len(alist)
    i = 1
    while i < n:
        left_start = left_end = right_start = right_end = 0 #初始化游标
        while left_start <= n - i:
            merged = []
            right_start = left_end = left_start + i
            right_end = left_end + i
            if right_end > n:
                right_end = n
            left = alist[left_start:left_end]  # 会需要额外空间开销,可以不要left和right,直接在原列表切片
            right = alist[right_start:right_end]
            while left and right:
                if left[0] < right[0]:  # 小的先添加到结果
                    merged.append(left.pop(0))
                else:
                    merged.append(right.pop(0))
                # print(merged)
            merged.extend(left if left else right)  # 剩余元素添加
            alist[left_start:right_end] = merged  ##中间排序结果返回给alist-类似于return的作用
            # print(alist,i,left_start)
            left_start += i * 2  ##右移游标,依次处理剩余元素
        i *= 2  ##进入下一批次的merge
    return alist

六、快速排序

快速排序使用分治法来把一个数组分为两个子数组。

quickSort.gif

和归并排序一样,快速排序也是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然最坏运行情况的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。

基本思路:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

def quick_sort(array, left, right):
    if left >= right:
        return
    low = left
    high = right
    key = array[low]
    while left < right:
        while left < right and array[right] > key:
            right -= 1
        array[left] = array[right]
        while left < right and array[left] <= key:
            left += 1
        array[right] = array[left]
    array[right] = key
    quick_sort(array, low, left - 1)
    quick_sort(array, left + 1, high)

七、计数排序

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

思路:

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

countingSort.gif

八、桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

桶.gif 桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

基本思路:

  1. 设置一个定量的数组当作空桶;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序;
  4. 从不是空的桶里把排好序的数据拼接起来。
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

九、堆排序

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

如果你不了解堆这种数据结构,可以查看文章---看动画轻松理解堆

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

022e93e6d000484402a7cb5766919811.gif

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

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

基本思路:

  1. 将待排序的数组初始化为大顶堆,该过程即建堆。
  2. 将堆顶元素与最后一个元素进行交换,除去最后一个元素外可以组建为一个新的大顶堆。
  3. 由于第二部堆顶元素跟最后一个元素交换后,新建立的堆不是大顶堆,需要重新建立大顶堆。重复上面的处理流程,直到堆中仅剩下一个元素。
class Solution(object):
   def heap_sort(self, nums):
       i, l = 0, len(nums)
       self.nums = nums
       # 构造大顶堆,从非叶子节点开始倒序遍历,因此是l//2 -1 就是最后一个非叶子节点
       for i in range(l//2-1, -1, -1): 
           self.build_heap(i, l-1)
       # 上面的循环完成了大顶堆的构造,那么就开始把根节点跟末尾节点交换,然后重新调整大顶堆  
       for j in range(l-1, -1, -1):
           nums[0], nums[j] = nums[j], nums[0]
           self.build_heap(0, j-1)

       return nums

   def build_heap(self, i, l): 
       """构建大顶堆"""
       nums = self.nums
       left, right = 2*i+1, 2*i+2 ## 左右子节点的下标
       large_index = i 
       if left <= l and nums[i] < nums[left]:
           large_index = left

       if right <= l and nums[left] < nums[right]:
           large_index = right

       # 通过上面跟左右节点比较后,得出三个元素之间较大的下标,如果较大下表不是父节点的下标,说明交换后需要重新调整大顶堆
       if large_index != i:
           nums[i], nums[large_index] = nums[large_index], nums[i]
           self.build_heap(large_index, l)

十、基数排序

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

基本思路:

  1. 取得数组中的最大数,并取得位数;
  2. 对数位较短的数前面补零;
  3. 分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中;
  4. 收集,再将放置在0~9号桶中的数据按顺序放到数组中;
  5. 重复3~4过程,直到最高位,即可完成排序。
def getbit(num,i):          # 获取元素第i位的数
    return (num % (i * 10) - (num % i)) // i
def getMax(numList):        # 获取数组中的最大值
    if len(numList) == 1:
        return numList[0]
    maxNum = numList[0]
    for i in range(len(numList)):
        if numList[i] > maxNum:
            maxNum = numList[i]
    return maxNum
def radixSort(numList):
    if len(numList) == 0 or len(numList) == 1:
        return numList
    maxNum = getMax(numList)
    bitCount = 0
    index = 1
    while maxNum // index:
        bitCount += 1
        index *= 10
    currentBit = 1 
    # 统计一下最大值的bitCount(有多少位),因为比较多少次,是有最大值的位数决定的
    while currentBit <= 10**(bitCount-1):          # 开始循环的进行每一个位的比较
        res = []
        buckets = [[] for i in range(10)]    # 桶排序
        for i in numList:
            currentBitNum = getbit(i,currentBit)
            buckets[currentBitNum].append(i)
        for i in range(10):
            for j in range(len(buckets[i])):
                res.append(buckets[i][j])
        numList = res
        currentBit *= 10
    return numList

排序算法总结

1.冒泡排序几乎是最差的排序

2.随机数排序时,当数据集非常少时,插入类排序 要比 比较类排序快

只有当n=10时,快排反而比较慢,而插入和希尔排序相对较快,这是因为插入排序和希尔排序都属于插入类型的排序,而快排和冒泡属于交换类排序,数据量少时交换所消耗的资源占比大。

3.基本有序数据排序时,在数据量较少的情况下,插入排序胜过其他排序

在基本有序数据排序结果中,当n=10和n=100中都是插入排序消耗时间更短,因为数据基本有序,所以需要插入的次数比较少,尽管插入排序需要一个一个比较,但因为数据量不大,所以比较所消耗的资源占比不会太大。

4.不管数据是随机还是基本有序,数据量越大,快排的优势越明显

快排果然还是名副其实的快,我们看到当数据集达到十万级别时,冒泡排序已经用时800多秒,而快排只用了0.3秒,相信随着数据量的增大,它们之间的差距也会越来越大。

5.快排优化方案成立

对于大数据集排序先使用快排,使数据集达到基本有序,然后当分区达到一定小的时候使用插入排序,因为插入排序对少量的基本有序数据集性能优于快排!

特别鸣谢:

这或许是东半球讲十大排序算法最好的一篇文章 - 小专栏 (xiaozhuanlan.com)

GitHub热门项目:使用Python实现所有算法 - 知乎 (zhihu.com)