8.1.1 排序算法概述及python基本实现(上)

从查找算法的性能可以看出,有序数据可以提高查找速度。对数据进行排序,是数据结构与算法知识基础篇中最后介绍也是最重要的一部分。

面试时考察编程基础,一看字符串、数组处理的一些题目,二看链表、树的基础应用,三看查找、排序各种方法张口就来。

排序算法是和语言无关的,本节重点还是python的实现;另外,排序算法分为几大类,若有不理解之处还要自行研究,本文不对原理详细展开(原理上比较复杂的算法不多,大多数极易理解)。

参考博客

十大经典排序算法最强总结

十大经典排序算法(Python代码实现)

Python实现十大经典排序算法

基本概念

排序甚至有个定义:对于一组记录组成的表,表中每项记录有一项可以用来标识大小关系(称为关键字),也就是说每项记录还有其他附加信息,通过整理表中的数据使之按关键字递增或递减有序排列。

稳定性:当待排序的表中有多个关键字相同的记录,经过排序后,这些具有相同关键字的记录之间的相对次序保持不变,则称该排序方法是稳定的。注意: 稳定性是针对所有输入实例而言的。

外排序与内排序:在排序中,若整个表都是放在内存中处理的,则称之为内排序;若排序过程中要进行数据的内、外存交换,则称之为外排序。

排序算法分类汇总

一般将排序算法分为插入排序、选择排序、交换排序、归并排序、基数排序五大类;其中基数排序属于非比较排序,其他都为比较排序。

比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr之前有多少个元素,则唯一确定了arr在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求

我们看一下主要的十类排序算法的比较,个中细节还是不少,在理解算法原理后,总结如下几条随便谈谈:

  1. 非比较排序/基数排序的平均复杂度均为O(n+k),k为桶的数目,而且都占用额外内存;比较排序除了另有重用的归并排序(用于外排),都不占用额外内存,in-place排序。
  2. 再看比较排序的时间复杂度。从排序算法的演进角度来看,插入、选择、冒泡都是科学家最初设计的排序算法,都是很暴力的,O(n^2);随着计算机科学的演进,算法不断优化,演进了更快的排序算法,图中希尔排序,平均时间复杂度其实为O(n^{1.3}),是很早一批优化的排序算法。而后堆排序、快排序被设计出来,平均复杂度在O(n logn)。ps: 在排序算法更深入的知识中,科学家们还是设计出了更快的方法,指数次幂也再不是整数了。
  3. 综上,排序方法按空间复杂度可分为两类,in-place/ out-place;按时间复杂度分为三类,平方阶、线性对数阶、线性阶。线性阶也是非比较排序都是out-place,算法在时空上一定是平衡的。
  4.  算法的选择问题:没有一种排序是任何情况下 都表现最好的。选择方法时根据 数据规模、初始状态、稳定性要求、时空复杂度限制及关键字结构来决定。
  5. 数据规模较小,可选简单的排序方法(你懂得),往往不会是平方的时间复杂度;数据规模较大时,应选择线性对数的排序大法。目前基于比较的内排序中,快速排序被认为是最好的,要求重点学习。但是快排不稳定,要求稳定性的话,可以选择归并排序,优化的话可以将直接插入与归并结合使用。
  6. 基数排序是线性的,使用的话对数据的结构特征有要求,详细的学习其原理就明白了。

排序算法原理及实现

冒泡排序(Bubble Sort)

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

def bubbleSort(nums):
    for i in range(len(nums) - 1): # 遍历 len(nums)-1 次
        for j in range(len(nums) - i - 1): # 已排好序的部分不用再次遍历
            if nums[j] > nums[j+1]:
                nums[j], nums[j+1] = nums[j+1], nums[j] # Python 交换两个数不用中间变量
    return nums

快速排序(Quick Sort)

快排属于交换排序,跟冒泡排序是类似的交换思路。是由东尼·霍尔所发展的一种排序算法。

快排是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。它是处理大数据最快的排序算法之一,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。它的主要缺点是非常脆弱。

重点学习一下,面试必考。

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,这个分割的数称为pivot;然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

def quickSort(nums, left, right):  # 这种写法的平均空间复杂度为 O(logn) 
    # 分区操作
    def partition(nums, left, right):
        pivot = nums[left]  # 基准值
        while left < right:
            while left < right and nums[right] >= pivot:
                right -= 1
            nums[left] = nums[right]  # 比基准小的交换到前面
            while left < right and nums[left] <= pivot:
                left += 1
            nums[right] = nums[left]  # 比基准大交换到后面
        nums[left] = pivot # 基准值的正确位置,也可以为 nums[right] = pivot
        return left  # 返回基准值的索引,也可以为 return right
    # 递归操作
    if left < right:
        pivotIndex = partition(nums, left, right)
        quickSort2(nums, left, pivotIndex - 1)  # 左序列
        quickSort2(nums, pivotIndex + 1, right) # 右序列
    return nums

插入排序(Insertion Sort)

插入排序如同打扑克一样,每次将后面的牌插到前面已经排好序的牌中。插入排序有一种优化算法,叫做拆半插入。因为前面是局部排好的序列,因此可以用二分查找的方法将牌插入到正确的位置,而不是从后往前一一比对。折半查找只是减少了比较次数,但是元素的移动次数不变,所以时间复杂度仍为 O(n^2) !

def insertionSort(nums):
    for i in range(len(nums) - 1):  # 遍历 len(nums)-1 次
        curNum, preIndex = nums[i+1], i  # 假设第一个数排好的, curNum 保存当前待插入的数
        while preIndex >= 0 and curNum < nums[preIndex]: # 寻找curNum在已排好序列中的位置,将比 curNum 大的元素向后移动
            nums[preIndex + 1] = nums[preIndex]
            preIndex -= 1
        nums[preIndex + 1] = curNum  # 待插入的数的正确位置   
    return nums

希尔排序(Shell Sort)

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

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

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

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。希尔排序的核心在于增量序列的设定。既可以提前设定好增量序列,也可以动态的定义增量序列。希尔排序的性能分析很是复杂,取决于增量序列的选取。由于最后一个增量必须是1,那么增量的选取可以是 d^{_{i+1}} = \left \lfloor d_{i}/2 \right \rfloor。最后分析出来,其时间复杂度为 O(n^1.3),总之比直接插入排序快很多啦。

这种排序方法应用较少,了解一下即可。

def shellSort(nums):
    lens = len(nums)
    gap = 1  
    gap //= 2  # 增量
    while gap > 0:
        for i in range(gap, lens):
            curNum, preIndex = nums[i], i - gap  # curNum 保存当前待插入的数
            while preIndex >= 0 and curNum < nums[preIndex]:
                nums[preIndex + gap] = nums[preIndex] # 将比 curNum 大的元素向后移动
                preIndex -= gap
            nums[preIndex + gap] = curNum  # 待插入的数的正确位置
        gap //= 2  # 下一个间隔
    return nums

选择排序(Selection Sort)

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。选择排序每次选出最小的元素,因此需要遍历 n-1 次。实在是太暴力无脑了。

工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

可以看出,选择排序中每趟总是从无序区中选择选出全局最小(最大)的关键字,所以,直接选择排序和堆排序适合于从大量记录中选择一部分排序记录

def selectionSort(nums):
    for i in range(len(nums) - 1):  # 遍历 len(nums)-1 次
        minIndex = i
        for j in range(i + 1, len(nums)):
            if nums[j] < nums[minIndex]:  # 更新最小值索引
                minIndex = j  
        nums[i], nums[minIndex] = nums[minIndex], nums[i] # 把最小数交换到前面
    return nums

堆排序(Heap Sort)

堆排序是指利用堆这种数据结构所设计的一种排序算法。鉴于之前已学习了堆,理解起来很方便。不过堆排序属于选择排序,这是如何理解呢?我们进行堆排序时,是in-place操作的,我们还是要关注一下在原数组中,进行堆排序的原理。

堆排序思想:将待排序的序列构造成一个大顶堆,然后依次将堆顶元素移走并重新调整剩余的n-1个元素为大顶堆,与直接选择排序很类似。

堆排序的关键是构造初始堆,将数组看成是一棵完全二叉树的顺序存储结构,从 i = \left \lfloor n/2 \right \rfloor \rightarrow 1 大者上浮,小者筛选下去,此时根结点为最大值,将其放到数组最后,即与最后一个叶子结点交换。由于最大元素归位,待排序的元素个数减少一个;如此反复建堆。

# 最大堆
def heapSort(nums):
    # 调整堆
    def adjustHeap(nums, i, size):
        # 非叶子结点的左右两个孩子
        lchild = 2 * i + 1
        rchild = 2 * i + 2
        # 在当前结点、左孩子、右孩子中找到最大元素的索引
        largest = i 
        if lchild < size and nums[lchild] > nums[largest]: 
            largest = lchild 
        if rchild < size and nums[rchild] > nums[largest]: 
            largest = rchild 
        # 如果最大元素的索引不是当前结点,把大的结点交换到上面,继续调整堆
        if largest != i: 
            nums[largest], nums[i] = nums[i], nums[largest] 
            # 第 2 个参数传入 largest 的索引是交换前大数字对应的索引
            # 交换后该索引对应的是小数字,应该把该小数字向下调整
            adjustHeap(nums, largest, size)
    # 建立堆
    def builtHeap(nums, size):
        for i in range(len(nums)//2)[::-1]: # 从倒数第一个非叶子结点开始建立最大堆
            adjustHeap(nums, i, size) # 对所有非叶子结点进行堆的调整
        # print(nums)  # 第一次建立好的最大堆
    # 堆排序 
    size = len(nums)
    builtHeap(nums, size) 
    for i in range(len(nums))[::-1]: 
        # 每次根结点都是最大的数,最大数放到后面
        nums[0], nums[i] = nums[i], nums[0] 
        # 交换完后还需要继续调整堆,只需调整根节点,此时数组的 size 不包括已经排序好的数
        adjustHeap(nums, 0, i) 
    return nums  # 由于每次大的都会放到后面,因此最后的 nums 是从小到大排列

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值