八大排序算法介绍分析及其python实现

方便起见,一下所有代码与解释都默认升序排列。且存储在列表中。
分别实现了1.冒泡排序、2.选择排序、3.插入排序、4.希尔排序、5.归并排序、6.快速排序、7.堆排序

1.冒泡排序:

冒泡排序是一种很简单但是效率不高的方式,要多次遍历列表。
第一趟:两两比较,a[i] > a[i+1]时交换它们的位置,这样遍历完整个序列就能选出将最大值放在最后。如图。
第二趟:除去最大值,选出剩余序列中的最大值(原序列的第二大)放在倒数第二个位置。

第n-1趟
这里写图片描述

实现代码如下:

def bubbleSort(alist):
    for i in range(len(alist)-1):          #这一循环表示要走n-1趟
        for j in range(1, len(alist)-i):   #每一趟需要进行n-i-1次的两两对比
            if alist[j] < alist[j-1]:
                alist[j], alist[j - 1] = alist[j-1], alist[j]
    return alist

第一趟需要比较n-1次,第二趟n-2次,第三趟n-3次….最后一趟1次。所以这是一个等差序列。一共需要 (n1)2/2 ( n − 1 ) 2 / 2 次比较。当然交换的部分就根据原始列表来决定。所以冒泡排序的时间复杂度为O( n2 n 2 )

2.选择排序

选择排序可以看成是冒泡排序的一个改进,原理跟冒泡一样,也是每次只选出一个数放在其正确的位置上。但是相比冒泡而言不用一直做交换。每次遍历只做一个交换。
第一趟:选出最大的值放在最后
第二趟:选出第二大的值放在倒数第二处

这里写图片描述
代码实现:

def selectionSort(alist):
    for i in range(len(alist) - 1):
        positionOfMax = 0
        for j in range(1, len(alist) - i):
            if alist[j] > alist[positionOfMax]:
                positionOfMax = j
        alist[positionOfMax], alist[len(alist)-i-1] = alist[len(alist)-i-1], alist[positionOfMax]
    return alist

选择排序的时间复杂度跟冒泡排序一样,也是O( n2 n 2 )

3.插入排序

插入排序有一个很形象的比喻:打扑克牌时,一张一张的摸牌,每摸一张就把它放在合适的位置,这就保证了手里的牌一直是有序的。插入排序也是这样,始终在列表的较低位置维护一个排好序的子列表。然后将每个新项“插入”回之前的子列表。
这里写图片描述

代码实现:

def insertSort(alist):
    for i in range(1, len(alist)):
        currentValue = alist[i]
        position = i
        while position > 0 and alist[position - 1] > currentValue:
            alist[position] = alist[position - 1]
            position -= 1
        alist[position] = currentValue

同样插入排序的时间复杂度也是 O(n2) O ( n 2 )

4.希尔排序

shell排序(也称递减增量排序)算是插入排序的一个改进。通过将原始列表分解为多个较小的子列表进行插入排序。因为插入排序一次只能将数据移一位,希尔排序可以一次将数据移一大步。shell排序使用增量i(有时称为 gap),通过选择gap来创建子列表。
这里写图片描述
以上图为例:
第一步:设置增量为3,把原始列表分成了3个子列表,第一个是(54,17,44)第二个是(26,77,55)第三个(93,31, 20)
第二步:分别对这三个子列表进行插入排序。排序后的结果分别为(17,44,54)、(26,55,77)和(20,31,93)。实际上整个序列为(17,26,20,44, 55, 31, 54,77,93)

这里写图片描述
最后设置增量为1(即插入排序),对上一步的数据排序。就得到(17, 20, 26, 31, 44, 54, 55, 77, 93)

总结一下,希尔排序的关键在于怎么选取那个增量gap,增量要越来越小,也就是子列表越来越大。最后使用一次插入排序。因为如果列表本身已经排好序,或者大部分都排好了,那么插入排序的效率还是很高的。

代码实现:
设定gap的值由n/2–>n/4–>…–>1
相当于是第一次有n/2个子列表,每个子列表最多比较两个数(边界可能只有一个数)。第二次有n/4个子列表,每个子列表最多比较4个数。依次类推。。。

def shellSort(alist):
    subListCount = len(alist)//2       #gap(子列表的个数)先为n//2

    while subListCount > 0:
        for i in range(subListCount):  #分别比较每一个子列表
            gapInsertSort(alist, i, subListCount)    #调用插入排序,同时将子列表的第一个索引和gap传进去。

        subListCount //= 2             #排完一个循环以后,将子列表个数缩减一半,子列表的扩大一倍。

def gapInsertSort(alist, start, gap):  #实际是一个插入排序
    for i in range(start+gap, len(alist), gap):   #待排序的列表为[start, start+gap, start+2*gap...]
        currentValue = alist[i]                   #要将alist[i]放置到正确的位置
        position = i
        while position >= gap and currentValue < alist[position - gap]:    #待排数字是小于position-gap处的数时,那么position-gap就应该放到position处。一直到待排数字是大于当前比较数字,说明待排数已经到了它正确的位置上了
            alist[i] =  alist[position - gap]
            position = position - gap

        alist[position] = currentValue    #最后将待排数放到它正确的位置上

希尔排序的时间复杂度跟gap的选择有关。
这里写图片描述

5.归并排序

归并算法是一个比较高效的算法,采用了分而治之的思想。用了递归的思想,不断地将列表一分为二,直到不能分为止,然后进行排序。
这里写图片描述
上图为拆分的过程:
1.将原列表分为两部分。
2.将分好的子列表,再各分成两部分。
…一直到不能分为止

这里写图片描述
然后是合并的过程:
首先排最小的子列表,排好以后进行合并。合并的过程是根据拆分的顺序,反着合并。最终得到排好的列表。

代码如下:
用递归的方式:

def mergeSort(alist):
    if len(alist) > 1:
        midPosition = len(alist)//2     #切分
        left = alist[:midPosition]
        right = alist[midPosition:]

        mergeSort(left)
        mergeSort(right)
        i = 0                     #左列表的索引
        j = 0                     #右列表的索引
        k = 0                     #合并列表的索引
        while i < len(left) and j < len(right):    #这一部分就相当于合并操作了
            if left[i] < right[j]:       #在right和left中选出最小的放在alist上。因为right和left都是排好序的,因此只需要在他们各自最低位中找。
                alist[k] = left[i]
                i += 1
            elif left[i] > right[j]:
                alist[k] = right[j]
                j += 1
            k += 1
        while i < len(left):          #left中还有剩余
            alist[k] = left[i]
            i += 1
            k += 1
        while j < len(right):        #right中还有剩余
            alist[k] = right[j]
            j += 1
            k += 1
        return alist

分析复杂度:
第一步是拆分,假设列表长度为n,第一次分了2个,第二次分了 22 2 2 个。。。第i次分了 2i 2 i 个,所以 2i 2 i = n,就能得出i = log2n l o g 2 n 次。大小为 n 的列表的合并需要 n 个操作,也就是说每一层需要n次。一共拆了 log2n l o g 2 n 次,也就是有 log2n l o g 2 n 层,所以归并排序是一种 O(nlog2n) O ( n l o g 2 n ) 算法。

6.快速排序

快速排序也使用了分而治之的思想,但不同的是它不需要占用额外的存储,也就是不用像归并那样分成不同的块。

第一步:在列表中选择一个值,称为枢纽值。假设将列表第一项54选为第一个枢纽值,如图
这里写图片描述

第二步:将列表分为两部分,其中<54的在左边,>=54移到右边。但是怎么移到很关键,

  • 1.定义左右两个位置标记。如图leftmark和rightmark分别指向除枢纽值以外的两个端点
  • 2.然后向右移动leftmark,直到leftmark所指的值大于等于枢纽值’54’。如图中’93’就是左位置找到的第一个大于’54’的值
  • 3.定位到rightmark,开始向左移动rightmark,直到rightmark所指的值小于枢纽值’54’,如图中’20’
  • 4.交换‘93’和‘20’,这样就把小的换到左边,大的换到右边了

这里写图片描述

第三步:重复第二步,直到leftmark>rightmark,这时就交换rightmark的值和枢纽值‘54’。这样就以‘54’位分隔点将列表分成了两个部分了。
这里写图片描述
第四步:用同样的方式对两个子列表排序。直到子列表的长度<=1。

代码:

def quickSort(alist):
    quickSortCore(alist, 0, len(alist) - 1)     


def quickSortCore(alist, first, last):     
    if first < last:
        splitPoint = partition(alist, first, last)    #对alist的first-last这一段排序,得到分割点位置

        quickSortCore(alist, first, splitPoint-1)
        quickSortCore(alist, splitPoint + 1, last)


def partition(alist, first, last):
    pivotValue = alist[first]    #默认以子列表的第一个数为枢纽值

    leftMark = first + 1         #左端点
    rightMark = last             #右端点

    while leftMark < rightMark:
        while alist[leftMark] < pivotValue:
            leftMark += 1
        while alist[rightMark] > pivotValue:
            rightMark -= 1

        if leftMark < rightMark:
            alist[leftMark], alist[rightMark] = alist[rightMark], alist[leftMark]
            rightMark -= 1
            leftMark += 1

    alist[rightMark], alist[first] = alist[first], alist[rightMark]

    return rightMark

7.堆排序

堆排序需要理解堆的含义,它的一些函数,例如下沉、上浮、创建堆等,这些可以查看链接

具体的实现过程就是,给定一个列表,首先将它转为为堆。因为找的图为降序,所以用以降序来讲。如果是降序的话,那么需要转换为最小堆。
然后依次进行删除根节点(最小的值)操作,删除当前堆的根节点后,用当前最末的节点代替根节点,进行下沉操作,这样就又能将最小的节点放置到根节点。相当于依次将最小的节点放在后面,这样的话,最后的列表将是一个降序排列。
这里写图片描述
如上图所示:
这是一个最小堆,根节点为最小值,
1. 取出最小值5,将最末的27放置到根节点,形成新的一个二叉树
2. 对27进行下沉操作,一直到形成一个新的最小堆,这时根节点处又是最小值9.
3. 依次进行步骤2的操作。直到取出所有点。

代码实现:


def heapSort(alist):
    def maxChild(i, end):
        if i * 2 + 1 > len(alist) or i*2+1>end:
            return 2 * i
        return 2 * i if (alist[2 * i] < alist[2 * i + 1]) else 2 * i + 1

    def percDown(start, end):
        while start * 2 <= end:
            mc = maxChild(start, end)
            if alist[start] > alist[mc]:
                alist[start], alist[mc] = alist[mc], alist[start]
            start = mc

    def buildHeap(alist):
        i = len(alist) // 2
        alist.insert(0, 0)
        while i > 0:
            percDown(i, len(alist) - 1)
            i -= 1
#程序从这里开始运行
    buildHeap(alist)                  #将alist构建成一个最小堆
    for i in range(len(alist)-1, 1, -1):  #依次取出最小的值放在后面。再调整二叉树为一个新的最小堆
        alist[i], alist[1] = alist[1], alist[i]
        percDown(1, i-1)
    return alist[1:]

print(heapSort([6, 5, 9, 7, 3, 19, 0, 7, 2]))

结果为
这里写图片描述
以上代码解释:
1. 先构建一个最小堆。
2. 那么第一个节点,一定是最小的。所以把第一个节点和最后一个节点交换位置。
3. 交换完以后,应该固定住最后一个节点,因为它已经放在了正确的位置上了。然后再调整二叉树,使其成为一个新的最小堆。调整的方式是,把第一个节点当做新加入的,然后进行下沉。
4. 再取出新的二叉堆的根节点,与倒数第二个节点交换。。。
5. 重复进行以上步骤,使得最后只剩下一个点为止。

8 基数排序

基数排序用于整数排序。原理是将整数按位数切割成不同的数字,然后按位比较。
实现过程:将所有待比较的数统一为相同的位数长度,位数短的前面补零,然后开始依次按位比较。基数排序分为LSD(Least significant digital)和MSD(Most significant digital)。LSD从键值最右(最低位)开始,MSD从键值最左边(最高位)开始。

例如:给321,109,584,123,052,063,765,814排序
用LSD方式:这些数字一共有3位,也就是说会排3次
第一次:排个位。
Q[0]:
Q[1]:321
Q[2]:052
Q[3]:123, 063
Q[4]:584, 814
Q[5]:765
Q[6]:
Q[7]:
Q[8]:
Q[9]:109
结果:321,052,123,063,584,814,765

第二次:排十位
Q[0]:
Q[1]:814
Q[2]:321,123
Q[3]:
Q[4]:
Q[5]:052
Q[6]:063,765
Q[7]:
Q[8]:584
Q[9]:
结果:814,321,123,052,063,765,584

第三次:排百位
Q[0]:052,063
Q[1]:123
Q[2]:
Q[3]:321
Q[4]:
Q[5]:584
Q[6]:
Q[7]:765
Q[8]:814
Q[9]:
结果:052,063,123,321,584,765,814

通常把Q[0]….Q[9]称为桶,所以这种排序方法也称为桶排序。
基数排序的时间复杂度是O(k*n),其中n是排序元素个数,k是数字位数。

import math

def RadixSort(alist, radix = 10):
    if max(alist) == 0:
        K = 1
    else:
        K = int(math.ceil(math.log(max(alist), radix)))      #位数
    for i in range(0, K):
        bucket = [[] for i in range(radix)]                  #建立radix个桶
        for val in alist:
            bucket[val % (radix**(i+1)) // (radix**i)].append(val) #获得val的第i位数字,并将val放到对应的桶
        del alist[:]
        for each in bucket:
            alist.extend(each)          #合并桶

    return alist

print(RadixSort([321,109 ,584,123, 52, 63,765,814]))

稳定性

定义:相同大小的元素,在排序前后的相对位置不变,那么这种排序算法就是稳定的。

总结

排序方法时间复杂度和空间复杂度以及稳定性总结:
这里写图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值