python数据结构与算法-5 列表排序-2快速排序、堆排序、归并排序

目录

一、快速排序

1.1 思路:

1.2 代码:

二、堆排序

2.1 树的概念

2.1.1 二叉树

满二叉树和完全二叉树

二叉树的顺序存储方式(列表存)

2.2 堆排序

2.2.1 堆的向下调整

向下调整代码示例

2.2.2 堆排序过程

2.2.3 堆排序内置模块-heapq

2.2.4 堆排序应用-topk问题

三、归并排序

3.1 归并

3.2 归并排序

四、总结

一、快速排序

1.1 思路:

1、取一个元素p(第一个元素),使元素p归位;
2、列表被p分成两部分,左边都比p小,右边都比p大;
3、递归完成排序

下图动画示例为元素归位过程:

​​​​​​​

以上过程,元素5会把列表分为左右两部分,再对这两部分分别进行以上过程的排序,直至得到的left-right中间没有值,即left>=right为止,也就是说,在如果left<right,以上过程会一直递归。。。

1.2 代码:

# 快速排序
"""步骤:
1、取第一个位置的值为temp,去看剩下的值,小于temp的放到左边,大于temp的放到右边,这样整个列表就被第一个值一分为二
2、再对列表左边进行步骤一,对列表右边进行步骤一,直到左右边界里面没有值,即left>=right结束循环
"""


def partition(lis, left, right):  # 需要知道左右边界
    temp = lis[left]  # 取第一个位置的值为temp,即第一个值就是left上的值

    # 此时left上可以认为没有值了,所以从right开始往前数,直到找到小于temp的值放在left的位置上
    while right > left:  # 只要right与left之间还有值,就一直进行如下循环
        while lis[right] >= temp and right > left:  # 如果right的值大于temp,这个值就不动,right继续往左找
            right -= 1
        lis[left] = lis[right]  # 直到如果right的值小于temp,或者right 与 left重合了还没找到这个值,就退出循环,把找到的这个值放到left位置上,
        while lis[left] <= temp and right > left:  # 如果left的值小于temp,这个值就不动,left继续往右找
            left += 1
        lis[right] = lis[left]  # 直到如果left的值大于temp,或者right 与 left重合了还没找到这个值,就退出循环,把找到的这个值放到right的位置上,
    lis[left] = temp  # 此时right=left,把原来temp的值就放在当前right或left的位置上
    return left  # 返回temp值所在的下标,即把列表一分为二的下标


def quick_sort(lis, left, right):
    if right > left:
        mid = partition(lis, left, right)  # 返回的mid的值,就可以重新确定left和right的范围
        quick_sort(lis, left, mid - 1)  # mid左边--left和right的范围
        quick_sort(lis, mid + 1, right)  # mid右边--left和right的范围
    return lis


item = [5, 1, 4, 3, 6, 2]
print(item)
print(quick_sort(item, 0, len(item) - 1))  # 初始的left和right的范围是整个列表

结果:

快速排序的时间复杂度:O(nlogn)

但是可能会存在最坏的情况,比如对[9,8,7,6,5,4,3,2,1]进行升序排列,那么每次递归情况如下:

[9,8,7,6,5,4,3,2,1]          --9归位

[1,8,7,6,5,4,3,2,9]          --1归位

[1,8,7,6,5,4,3,2,9]          --8归位

[1,2,7,6,5,4,3,8,9]          --2归位

[1,2,7,6,5,4,3,8,9]          --7归位

[1,2,3,6,5,4,7,8,9]          --3归位

[1,2,3,6,5,4,7,8,9]          --6归位

[1,2,3,4,5,6,7,8,9]          --4归位

[1,2,3,4,5,6,7,8,9]          --完成排序

黄色标记为每次递归要排序的列表区域,由此可见,每次一个元素归位完成,剩下待排序的元素只少一个,所以,如果是此类情况,那么时间复杂度就是O(n^{2}),但这种情况出现概率很低

解决方法:在选择第一个元素归位之前,先随机选一个元素与第一个互换

二、堆排序

2.1 树的概念

树:一种数据结构

树是一种可以递归定义的数据结构,有n个节点组成的集合:

如果n=0----空树;

如果n>0----存在一个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。

2.1.1 二叉树

度不超过2的树,即每个节点最多有两个孩子节点,分为左孩子和右孩子

满二叉树和完全二叉树

堆是一个特殊的完全二叉树

二叉树的存储方式:

链式存储方式/顺序存储方式

二叉树的顺序存储方式(列表存)

找规律:

父节点找子节点----若父节点为i,左孩子就是2i+1,右孩子就是2i+2

子节点找父节点----若子节点为i,父节点就是(i-1)//2----要整除

2.2 堆排序

堆是一种特殊的完全二叉树

大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大

小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小

2.2.1 堆的向下调整

根节点的左右子树都是堆,但根节点不满足堆的概念,可以通过一次向下调整来将其变成一个堆,示例如下:

a. 把根节点取出,对比其子节点,找出子节点中大的值,放到根节点位置

b. 再将根节点与空位的下一级子节点对比,若根节点比它们小,则找出它们中大的值,放到上一级的空位,以此类推,直到找到合适根节点的位置

向下调整代码示例
# 堆排序
# 向下调整
def sift(lis, low, high):
    """:param lis:列表
       :param low:根节点位置
       :param high:堆的最后一个元素位置
    """
    i = low  # i最开始指向根节点
    j = 2 * i + 1  # j最开始指向根节点的左孩子
    temp = lis[i]  # 最开始先把根节点存起来

    # 进入循环,判断找根节点应该放到哪里
    while j <= high:  # j要小于等于high,否则就越界了,后面没有数了
        if j + 1 <= high and lis[j + 1] > lis[j]:  # 如果右孩子存在,即j+1也没有越界,且右孩子比较大
            j = j + 1  # 那么j就指向右孩子,这样就可以直接拿根节点与右孩子比较
        if lis[j] > temp:
            lis[i] = lis[j]  # 如果子节点大于根节点的值temp,就把子节点放到根节点的位置上,那么此时i和j都要往下挪一层
            i = j
            j = 2 * i + 1

        else:
            lis[i] = temp  # 如果还是根节点比较大,那么temp就放回lis[i]的地方
            break  # temp归位,循环结束
    else:  # j越界了说明:此时i已经在最后最后一层了,其没有孩子了,就把temp放到i那里
        lis[i] = temp


item = [3,9,8,5,4,7,6,1,2]
print(item)
sift(item, 0, len(item) - 1)
print(item)

结果:

2.2.2 堆排序过程

1、构造堆(大根堆),得到的堆的根节点最大

怎么构造?

---农村包围城市,先从最小的(最下面)开始构造堆,一步步往上构造((每次构造就是对这个堆进行一次向下调整),那样我们得到的整个堆就是一个大根堆

怎么转化成代码?

---从最小的堆往上依次调用向下调整函数,直至到最上面

最小的堆,我们假设堆的最下面的子节点下标为i(列表长度-1),那么最小的堆的父节点就是(i-1)//2,若列表长度为n,那么父节点就是(n-2)//2,此例的位置就是4的下标

那么循环就是 for i in range((n-2)//2,-1,-1):# 从最小的堆循环往上,一直到根节点

    for i in range((n-2)//2,-1,-1):
        sift(lis,i,n-1)  #low是每个堆的根节点,所以就是i,high的作用的防止越界,那么为了避免麻烦,我们把high设为堆的最后一个值的下标,也可起到相同作用

大根堆构造完成!!

2、得到堆顶元素,为最大元素,为了节省空间,我们把其放到堆的最后

3、将堆最后一个元素放到堆顶,也就是堆顶和最后一个元素互换位置,此时堆顶那部分可能是不满足大根堆要求的,所以可通过一次向下调整使堆有序

由于最开始构造的堆就是大根堆,即满足任一节点都比其孩子节点大,所以,一次向下调整能使一个最大值放到根节点上

4、堆顶为第二大元素,放到倒数第二个

5、重复步骤三(向下调整),直到堆变空

步骤2-4重复n次,每次把最大的按顺序放到下面,这样我们就把列表排好序了

def heap_sort(lis):
    # 构造堆
    n = len(lis)
    for i in range((n - 2) // 2, -1, -1):
        sift(lis, i, n - 1)  # low是每个堆的根节点,所以就是i,high的作用的防止越界,那么为了避免麻烦,我们把high设为堆的最后一个值的下标,也可起到相同作用

    # 堆构造完成!
    # 接下来开始
    for i in range(n - 1, -1, -1):  # i从最后一个元素一直往上
        lis[0], lis[i] = lis[i], lis[0]  # 把堆顶和i指向的元素交换
        sift(lis, 0, i - 1)  # 堆整个堆进行向下调整,但是不需包括最后一个,因为最后一个已经排好序了


item = [3,5,8,4,9,7,2,1,6]
print(item)
heap_sort(item)
print(item)

结果如下:

时间复杂度:O(nlogn)

2.2.3 堆排序内置模块-heapq

heapq.heapfiy(lis)  ----建小根堆

heapq.heappop(heap)---弹出并返回堆中的最小项,保持堆不变。如果堆是空的,则引发IndexError。若这个堆不是小根堆,进行heappop操作并不会弹出list中最小的值,而是弹出第一个值,弹出后,列表就不存在这个元素了。

示例如下:

import heapq

item = [3, 5, 8, 4, 9, 7, 2, 1, 6]
print(item)
heapq.heapify(item)  # 建立小根堆
print(item)
for i in range(len(item)):
    a = heapq.heappop(item)  #挨个弹出最小元素,前提是这个堆是小根堆
    print(a, end=",")
print(item)

结果:

2.2.4 堆排序应用-topk问题

对于n个数,设计算法得到前k大的数。(k<n)

解决思路:

排序后切片                                   时间复杂度:O(nlogn)

冒泡排序/选择排序/插入排序        时间复杂度:O(kn)   

堆排序                                          时间复杂度:O(nlogk)  

堆排序思路:

1、先取出列表中前k个数,建立一个小根堆(循环调用向下调整建立)

2、对剩余列表中的数进行遍历,如果lis[i]比堆顶的值还小,这个值不要,lis[i]比堆顶的值大,那么lis[i]替换堆顶,替换后做一次向下调整

3、倒序弹出堆顶(小根堆,堆顶是最小的值,所以每次把堆顶与最后一个值调换,然后进行一次向下调整,那么堆顶又变成了最小的值,重复此步骤,就会得到一个从大到小的列表)

代码示例:

1、向下调整代码:

跟大根堆向下调整代码相比,只改变了两个位置的比较关系

# 向下调整--小根堆
def sift(lis, low, high):
    i = low
    j = 2 * i + 1
    tmp = lis[i]

    while j <= high:
        if j + 1 <= high and lis[j + 1] < lis[j]:
            j = j + 1
        if lis[j] < tmp:
            lis[i] = lis[j]
            i = j
            j = 2 * i + 1
        else:
            lis[i] = tmp
            break
    else:
        lis[i] = tmp

2、堆排序实现代码

def topk(lis, k):
    heap = lis[0:k]  # 取出列表中前k个数
    # 1、建立小根堆
    for i in range((k - 2) // 2, -1, -1):
        sift(heap, i, k - 1)
    # 2、遍历列表剩下的数,若lis[i]小于堆顶,这个值不要,lis[i]大于堆顶,那么lis[i]替换堆顶
    for i in range(k, len(lis)):
        if lis[i] > heap[0]:
            heap[0] = lis[i]
            sift(heap, 0, k - 1)
    # 3、倒序弹出堆顶
    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)
    return heap



item = [i for i in range(100)]
random.shuffle(item)
print(item)
heap = topk(item, 10)
print(heap)

结果:

三、归并排序

3.1 归并

先讲一下归并的意思

        假设一个列表分为两段,这两段分别有序,如[1,3,5,7,2,4,6,8,9],那么如何把它合并成一个有序列表?动画示例如下:

代码如下:

# 归并
def merge(lis, low, high, mid):
    """
    :param low:整个列表开始位置
    :param high:整个列表结束位置
    :param mid:可以指第一段有序列表的最后,也可以指第二段有序列表的初始,此处按前者写代码
    """
    new_lis = []
    i = low
    j = mid + 1
    while i <= mid and j <= high:
        if lis[i] < lis[j]:
            new_lis.append(lis[i])
            i += 1
        else:
            new_lis.append(lis[j])
            j += 1
    while i <= mid:
        new_lis.append(lis[i])
        i += 1
    while j <= high:
        new_lis.append(lis[j])
        j += 1

    lis[low:high + 1] = new_lis


item = [1, 3, 5, 7, 2, 4, 6, 8, 9]
print(item)
merge(item, 0, 8, 3)
print(item)

结果:

3.2 归并排序

过程理解:

一个列表,一直分段,直至分成每个列表都只有一个元素,那么一个元素是有序的,再两两归并,如下图:

归并排序代码:

# 归并
def merge(lis, low, high, mid):
    """
    :param low:整个列表开始位置
    :param high:整个列表结束位置
    :param mid:可以指第一段有序列表的最后,也可以指第二段有序列表的初始,此处按前者写代码
    """
    new_lis = []
    i = low
    j = mid + 1
    while i <= mid and j <= high:
        if lis[i] < lis[j]:
            new_lis.append(lis[i])
            i += 1
        else:
            new_lis.append(lis[j])
            j += 1
    while i <= mid:
        new_lis.append(lis[i])
        i += 1
    while j <= high:
        new_lis.append(lis[j])
        j += 1

    lis[low:high + 1] = new_lis

#分解列表,再进行归并
def merge_sort(lis, low, high):
    if low < high:  #只要low和high中间有值,即当列表分解成单个元素时,不再递归
        mid = (low + high) // 2
        merge_sort(lis, low, mid)
        merge_sort(lis, mid + 1, high)
        merge(lis, low, high, mid)


item = [i for i in range(99)]
random.shuffle(item)
print(item)
merge_sort(item, 0, len(item) - 1, )
print(item)

结果:

时间复杂度:O(nlogn)

空间复杂度:O(n)

四、总结

三种排序算法时间复杂度都是O(nlogn)

一般情况下,运行时间上:快速排序<归并排序<堆排序

缺点:

快速排序:极端情况下排序效率低
归并排序:需要额外的内存开销
堆排序:在快的排序算法中相对较慢


递归会消耗空间​​​​​​​

稳定性:当两个元素值一样,保证其相对位置不变,挨个换的都是稳定的,跳着换的都是不稳定的,python中的sort排序是基于归并排序写的,是稳定的排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值