七种经典排序算法python实现

最近要考算法设计,所以把排序算法总结一下。经典的排序算法包括:冒泡排序,选择排序,插入排序,快速排序,归并排序,堆排序和希尔排序。全部程序都用python3实现,默认从小到大排序。
参考文章:https://blog.csdn.net/ls5718/article/details/51809866,博主的文章里面有演示动图,不懂的时候可以看下动图。

一、冒泡排序    BubbleSort

介绍:

让两数比较大的值一直滚动到最右侧,类似泡泡一直往上飘,每次滚动都要进行比较

思路:
临近的数字两两进行比较,按照从小到大的顺序进行交换,这样一趟过去,最大的数字就被交换到了最后一位,
然后再从头开始两两比较交换,直到导数第二位时结束

步骤:
1、比较相邻的元素,如果前一个比后一个大,就交换它们两个
2、对第0个到第n-1个数据做同样的工作,这时,最小的数就会‘浮’到数组的最左边位置
3、对所以的元素重复上面的步骤,除了单一个
4、持续每次对越来越少的元素重复上面的步骤,知道没有任何一个数字需要比较

时间复杂度:
O(n**2) 如果未优化的冒泡排序,最好的情况也是O(n**2),优化后的最好情况是O(n)

具体代码:
 
  
def bubbleSort(nums):
    n = len(nums)
    for i in range(1,n): #设置循环次数
        for j in range(n-i): # 每次循环都会确定一次最大值,所以下一次比较就不用再对后i个数比较了
            if nums[j]>nums[j+1]: #和选择排序的区别
                nums[j], nums[j+1] = nums[j+1], nums[j]
    return nums

# 优化:某一趟遍历如果没有数据交换,说明已经排好序了,因此不用再迭代了,用一个标记记录这个状态
def betterBubbleSort(nums):
    n = len(nums)
    flag = True
    for i in range(1,n): #设置循环次数
        if flag == False:
            break
        flag = False
        for j in range(n-i): # 每次循环都会确定一次最大值,所以下一次比较就不用再对后i个数比较了
            if nums[j]>nums[j+1]: #和选择排序的区别
                nums[j], nums[j+1] = nums[j+1], nums[j]
                flag = True
    return nums

二、选择排序    SelectionSort

介绍:
保持最小元素在最左侧,用最左侧的元素依次和右边的元素比较,谁小谁放在左边。
思路:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子序列前面,直到全部记录排序完毕
步骤:1、在未排序序列中找到最小元素,存放在排序序列的起始位置2、再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾3、以此类推,直到所有元素均排序完毕。
时间复杂度:最坏情况:O(n**2),最好情况O(n**2)
具体代码:

def selectSort(nums):
    n = len(nums)
    for i in range(n):
        for j in range(i+1,n): #开始一趟比较,从i+1到数组的末尾,找出最小值
            if nums[i] > nums[j]:
                nums[i], nums[j] = nums[j], nums[i]
    return nums

三、插入排序    InsertionSort

介绍:
由于插入排序其内层循环非常紧凑,对于小规模输入,插入排序是一种非常快的排序算法

思路:
对每个未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

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

时间复杂度:
最坏的情况:数组反向排序 O(n**2);最好的情况:数组已经排序O(n),平均情况:O(n**2)
具体代码:

def insertSort(nums):
    n = len(nums)
    for i in range(1, n):
        temp = nums[i] # 第一个元素认为已经排序了,所以从第二个开始
        index =i
        for j in range(i-1, -1, -1):
            if nums[j] > temp:
                nums[j+1] = nums[j] # 将所以在nums[i]之前的大于nums[i]的都后移一位
                index = j # 用index记录指针的变化
            else:
                break
        nums[index] = temp # 移动完所以大于nums[i]的值后,index刚好指向最靠前一个大于nums[i]的位置
    return nums

四、快速排序    QuickSort

介绍:
快速排序通常比同为O(NlogN)的其他算法更快

思路:
快速排序采用的思想是分治思想。快速排序是找出一个元素(理论上可以随便找一个元素)作为基准(pivot),
然后对数组进行分区操作,是基准左边的元素的值都不大于基准值,基准右边的元素都不小于基准值,如此作为
基准的元素调整到排序后的正确位置。递归快速排序,将其他n-1个元素也调整到排序后的正确位置。最后每个
元素都是在排序后的正确的位置,排序完成。
所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

步骤:
1、从数列中挑出一个元素作为基准数
2、分区过程,将比基准数大的放到右边,小于或等于它的数放在左边
3、再对左右分区递归执行第二步,直到各区间只有一个数

算法时间复杂度:O(nlogn)

排序演示:

具体代码:

def QSort(nums, low, high):
    pivot = nums[low] # 取第一个元素为基准值
    #low < high 是安全判断,防止数组越界,只要进行了指针加减都要判断
    while low < high:
        while low < high and nums[high] >= pivot:
            high -= 1
        if low < high:
            nums[low], nums[high] = nums[high], nums[low]
            low += 1
        while low < high and nums[low] <= pivot:
            low += 1
        if low < high:
            nums[low], nums[high] = nums[high], nums[low]
            high -= 1
    return low


def quickSort(nums, low, high):
    if low < high:
        mid = QSort(nums, low, high)
        quickSort(nums, low, mid-1)
        quickSort(nums, mid+1, high)
    return nums

五、归并排序    MergeSort

介绍:

归并排序是采用分治法的一个非常典型的应用。

思路:
归并排序的思想就是先递归分解数组,再合并数组。

先考虑合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了以后相应的指针
就往后移动一位。然后再比较,直至一个数组为空,最后再把另一个数组的剩余部分复制过来即可。

再考虑递归分解,基本思路是将数组分解成left和right,如果这两个数组内部数据是有序的,那么就可以
用上面合并数组的方法将这两个数组合并排序。如何让这两个数组内部有序呢?可以再二分,直至分解出的
小组只有一个元素时为止,此时认为该小组内部已经有序。然后合并排序相邻两个小组即可。

步骤:
1、将待排序数组R[0...n-1]二分分解,分解成N个长度为1的有序数组
2、将相邻的有序数组成对合并,得到N/2个长度为2的有序数组
3、将这些有序数组再次成对合并,得到N/4个长度为4的有序数组
4、重复上面的步骤,最后得到一个长度为N的有序数组。
综上可知,归并排序其实只要做两件事:(1)“分解”--将序列每次折半拆分。(2)“合并”--将划分
后的数组两两合并和排序

时间复杂度: O(NlogN)

具体代码:有两种,上面的是传统的做法,下面是利用python数组的特性
def merge(nums, low, mid, high):
    l = low # 第一段序列下标
    m = mid+1 # 第二段序列下标
    h = 0 # 新序列序列下标
    b = [0]*(high+1) # 新序列
    while l <= mid and m <= high:
    # 把两个子序列的头元素比较,取较小者进入新序列,然后在久序列中指针+1,开始下一次比较
        if nums[l] <= nums[m]:
            b[h] = nums[l]
            h += 1
            l += 1
        else:
            b[h] = nums[m]
            h += 1
            m += 1
    while l <= mid: # 若第一段序列没有扫描完,将其全部赋值到合并序列中
        b[h] = nums[l]
        h += 1
        l += 1
    while m <= high: # 若第二段序列没有扫描完,将其全部赋值到合并序列中
        b[h] = nums[m]
        h += 1
        m += 1
    for i in range(h): # 将分段排序得到的新数组复制到之前的数组
        nums[low+i] = b[i]

def mergeSort(nums, low, high):
    if low < high:
        mid = int((low+high)/2)
        mergeSort(nums, low, mid) # 分解
        mergeSort(nums, mid+1, high)
        merge(nums, low, mid, high) # 合并
    return nums
#---------------------------------------------------------------
#python的方法
def merge_sort(nums):
    if len(nums) <= 1:
        return nums
    mid = int(len(nums)/2) # 二分分解
    left = merge_sort(nums[:mid])
    right = merge_sort(nums[mid:])
    return merge_(left,right) # 合并数组

def merge_(left, right):
    l, r = 0, 0 #定义left和right下标
    result = []
    while l<len(left) and r<len(right):
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    # 将剩余数组直接加到结果数组中
    result += left[l:]
    result += right[r:]
    return result

六、堆排序    HeapSort

介绍:
堆排序在top K 问题中使用比较频繁。堆排序是采用二叉堆的数据结构来实现的,虽然实质上还是
一维数组。二叉堆是一个近似完全二叉树。

二叉堆具有以下性质:
1、父节点的键值总是大于或等于任何一个子节点的键值
2、每个节点的左右子树都是一个二叉堆(都是最大堆或最小堆)

根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆(大顶堆)。
大根堆要求根节点的关键字既大于或等于左子树的关键字值,又大于或等于右子树的关键字值。

步骤:
1、构造最大堆(Build_Max_Heap):若数组下标范围为0~N,考虑到单独一个元素是大根堆,则
从下标N/2开始的元素均为大根堆。于是只要从N/2-1开始,向前一次构造大根堆,这样就能保证,
构造到某个节点时,它的左右子树都已经是大根堆。
2、堆排序(HeapSort):由于堆是用数组模拟的。得到一个大根堆后,数组内部并不是有序的。因此需要将
堆化数组有序化。思想是移除根节点,并做最大堆调整的递归运算。第一次将heap[0]与heap[n-1]交换,
再对heap[0...n-2]做最大堆调整。第二次将heap[0]与heap[n-2]交换,再对heap[n-3]做最大堆调整。
重复该操作直至heap[0]和heap[1]交换。由于每次都是将最大的数并入到后面的有序区间,故操作完后整个
数组就是有序的了。
3、最大堆调整(Max_Heapify):该方法是提供给上述两个过程调用的。目的是将堆的末端子节点做调整,
使得子节点永远小于父节点。

时间复杂度:O(NlogN)
具体代码:

def heapSort(nums):
    n = len(nums)
    first = int(n/2 -1) # 最后一个非叶子节点
    #构建最大堆
    for start in range(first, -1, -1):
        percDown(nums, start, n-1)
    #循环,每次把根节点和最后一个节点调换位置
    for end in range(n-1, 0, -1):
        nums[0], nums[end] = nums[end], nums[0] #根节点是最大值,所以直接放在最后
        percDown(nums, 0, end-1) # end-1 最后一位已经是最大的了,不用加入判断了
    return nums

#最大堆调整:将堆的末端子节点做调整,使得子节点永远小于父节点
#start为当前需要调整最大堆的位置,end为调整边界
def percDown(nums, start, end):
    root = start
    while True:
        # 调整节点的子节点 想象一下树的结构,乘以2到达下一层
        child = root*2 + 1
        if child > end : break
        if child+1 <= end and nums[child] < nums[child+1]: #如果子节点小于后一位数,就+1 使用较大的子节点判断
            child = child+1
        if nums[root] < nums[child]: # 如果根节点小于子节点,就调换位置
            nums[root], nums[child] = nums[child], nums[root]
            root = child # 根节点和子节点调换了位置,这个时候把根节点的下标改为子节点下标,判断更改后的子节点是否符合父节点的要求
        else:
            break

七、希尔排序    ShellSort

介绍:
希尔排序,也称递减增量排序算法,实质是分组插入排序。由 Donald Shell 于1959年提出。
希尔排序是非稳定排序算法。虽然思想可以理解,不过编码过程很绕,以后需要的时候再回来多看看。

思路:
将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)
来进行。最后整个表就只有一列了。将数组转换至表是为了更好的理解这算法,算法本身还是使用数组进行排序
假如,假设有这样一组数[ 13 ,14, 94, 33, 82, 25, 59, 94, 65, 23, 45, 27, 73, 25, 39, 10 ]
如果我们以步长5开始进行排序,我们可以通过将这列表放在有5列的表中来更好的描述算法,
这样它们看起来应该是这样的:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。
这是10 已经移动到正确的位置了,然后再以步长3进行排序
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后是
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以步长1进行排序(此时就是简单的插入排序了)。

时间复杂度:O(n**2) 使用Hibbard增量的最坏情况:O(n^3/2)
具体代码:
def shellSort(nums):
    n = len(nums)
    step = round(n/2) # 初始步长为数组的一半,round是用来四舍五入的
    #步长从N/2开始,每次减半,直至为1
    while step > 0:
        for i in range(step, n):#设置步长step,则每次就像是nums[i]和nums[i+step]对比,进行插入排序,就向前面说的列进行比较
            temp = nums[i]
            j = i
            while j >= step and nums[j-step] > temp: #插入排序一样,调换位置
                nums[j] = nums[j-step]
                j = j - step
            nums[j] = temp
        step = round(step/2) # 重新设置步长
    return nums

总结

排序算法稳定性表示两个值相同的元素在排序前后是否有位置变化。如果前后位置变化,则排序算法是不稳定的,否则是稳定的。稳定性的定义符合常理,两个值相同的元素无需再次交换位置,交换位置是做了一次无用功。

下面是七种经典排序算法指标对比情况:

猜你喜欢

转载自blog.csdn.net/qq_36387683/article/details/80569810