第四课 排序算法NB三人组

python数据结构与算法基础 第四课

tags:

  • python
  • 路飞学院

categories:

  • python
  • 排序算法
  • NB 三人组
  • 快速排序
  • 堆排序
  • 归并排序

第一节 NB 三人组之快速排序

1. 快速排序介绍

  1. 快速排序特点:快

  2. 快速排序思路:

    • 取一个元素p (这里我们取第一个元素),使元素p归位;
    • 列表被p分成两部分,左边都比p小,右边都比p大;
    • 一次完成后列表分成两个部分,分别对左右两个列表继续做归位;
    • 递归完成排序
    • 综上分析:需要记录传入列表左边的位置,和传入列表右边的位置。归位后以中间为基准将列表分为[left, mid-1]和[mid+1, right]两个列表进行递归。
      在这里插入图片描述
  3. 实现元素p的归位的思路:

    • 这里指定P元素对应的位置记录为做指针,列表最后位置记录右指针。
    • 取出第一元素P, 从列表右边(指针为right)开始向列表前找。right指针跟着移动
    • 直到找到比P小的元素(或者left和right指针重合),Right指针暂停。把这个元素值放到P第一个元素的位置
    • 然后从左边(指针为left)开始找, 直到找到比P大的元素(或者left和right指针重合),将这个元素放到之前right所在位置。
    • 循环往复。直到left和right指针重合,重合位置就是P归位位置。
  4. 快速排序实现算法如下:

# 快速排序算法
# 首先,实现partition归位函数
def partition(li, left, right):
    '''
    :param li:  接收的列表
    :param left: 列表对应的左边指针
    :param right: 列表对应的右边指针
    :return:返回中间位置的指针给quick_sort
    '''
    # 将第一位置存起来,第一个位置是left。因为之后还有传来列表的切片。
    tmp = li[left]
    while left < right:
        # 右边开始找,找比tmp小的值。
        # 假如li[right] = tmp是因为一直找不到while左右直接碰上,还会自己减一。
        # right就会跑到left左边,这是我们不希望的所以left < right限制一下。
        # 不能去掉li[right] = tmp,列表中可能有其他重复的数字。
        while li[right] >= tmp and left < right:
            right -= 1
        li[left] = li[right]
        # 同理,从左边开始找,找比tmp大的值
        while li[left] <= tmp and left < right:
            left += 1
        li[right] = li[left]

    # 这里左右指针汇合left和right指向同一个位置
    li[left] = tmp
    return left


def quick_sort(li, left, right):
    # 至少有两个元素, 一个元素排什么序呀
    # 这里可千万别傻傻的.(后面都不要出现固定的位置0或len(li)-1,迭代会傻眼的)
    if left < right:
        mid = partition(li, left, right)
        quick_sort(li, left, mid - 1)
        quick_sort(li, mid + 1, right)



if __name__ == "__main__":
    import random
    list = random.sample([i for i in range(100)], 10)
    print(list)
    quick_sort(list, 0, len(list) - 1)
    print(list)
  1. 快速排序实现总结如下:
    1. 快速排序的时间复杂度一般情况为:O(n*logn)

    2. 快速排序的最坏情况:

      • 排序的列表是倒序的,此时间复杂度为:O(n*n)
      • 解决办法:排序前随机选一个数和第一数交换一下(随机化)
    3. 修改递归最大深度

import sys
sys. setrecursionlimit(100000)
  1. 如何把装饰器加到递归函数上(给递归函数加上一个马甲即可)
import time
import functools


def cal_time(func):
  def wrapper(*args, **kwargs):
      t1 = time.time()
      result = func(*args, **kwargs)
      t2 = time.time()
      print(func.__name__)
      print("%s running time is:%s sesc." % (func.__name__, t2 - t1))
      return result
  return wrapper
  
def _quick_sort(li, left, right):
  #递归函数
  pass

@cal_time
def quick_sort(li):
  _quick_sort(li, 0, len(li)-1)

第二节 NB 三人组之堆排序

1. 堆排序相关知识介绍-树的定义

  1. 树是一种数据结构,比如:目录结构。
  2. 树是一种可以递归定义的数据结构
  3. 树的定义:树是由n个节点组成的集合
    • 如果n=0,那这是一棵空树;
    • 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是-棵树。

2. 堆排序相关知识介绍-树的相关概念

  1. 根节点、叶子节点
  2. 树的深度(高度)
  3. 节点的度。节点的分差数比如:F的度是三
  4. 树的度。所有节点的分差数最大的数叫数的度。
  5. 孩子节点/父节点
  6. 子树
    在这里插入图片描述

3. 堆排序相关知识介绍-二叉树的介绍

  1. 二叉树:度不超过2的树
  2. 每个节点最多有两个孩子节点
  3. 两个孩子节点被区分为左孩子节点和右孩子节点
  4. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
  5. 完全二叉树:叶子节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。(最后一层若有节点,必从左边开始排。图b,7号节点有值,六号节点没右孩子则不是完全二叉树)
  6. 二叉树的储存方式:
    • 链式存储方式(后面会讲到)
    • 顺序存储方式(用列表储存)
    • 在此堆排序中用顺序储存方式
  7. 二叉树中最常见的操作:
    • 父亲i找孩子:左孩子2i+1, 右孩子2i +2
    • 孩子p找父亲:左右孩子(p-1)//2
      在这里插入图片描述

4. 堆排序相关知识介绍-堆的介绍

  1. 堆是一种特殊的完全二叉树
  2. 大根堆: -棵完全二叉树,满足任一节点都比其孩子节点大
  3. 小根堆: -棵完全二叉树,满足任一节点都比其孩子节点小
  4. 堆的向下调整性质
    • 假设根节点的左右子树都是堆,但根节点不满足堆的性质
    • 可以通过一次向下的调整来将其变成一个堆。
      在这里插入图片描述

5. 堆排序的实现过程

  1. 建立堆。(采用农村包围城市,从低向上慢慢调整)
  2. 得到堆顶元素,为最大元素
  3. 去掉堆顶,将堆最后-个元素放到堆顶,此时可通过一次调整重新使堆有序。
  4. 堆顶元素为第二大元素。
  5. 重复步骤3,直到堆变空。
 '''
    这里出数时,没有必要去在开辟内存空间。重新建一个列表,可以用列表切片划分堆和有序位置(用high来表示最后一个最元素的位置)。
    需要i和j分别指当前层和下面一层
    sift函数中可以合并else分支,但是不利于理解所以不合并了。
    父亲i找孩子:左孩子2*i+1, 右孩子2*i +2
	孩子p找父亲:左右孩子(p-1)//2
'''


def sift(li, low, high):
    '''
    :param li: 待调整的堆列表
    :param low: 待调整的堆的堆顶位置(根节点位置)
    :param high:待调整的堆的最后一个元素位置(防止调整越界)
    :return:
    '''
    i = low
    j = 2 * i + 1
    # 保存需要堆顶元素放到它该放到的位置
    tmp = li[low]
    while j <= high:  # j位置有数就一直循环
        if (j + 1) <= high and li[j + 1] > li[j]:  # 右孩子有并且比左边的孩子更大
            j = j + 1  # 把j指向右边孩子
        if li[j] > tmp:
            li[i] = li[j]
            # j位置就空下来了,朝下继续找合适元素(更新i和j,继续往下找)
            i = j
            j = 2 * i + 1
        else:  # tmp 更大
            li[i] = tmp  # 这个父节点可以放tmp。可以做个官(不一定是最大的官)
            break
    else:
        li[i] = tmp  # 把tmp放到孩子节点上


def heap_sort(li):
    n = len(li)
    # 先建堆,农村包围城市方法
    for i in range((n-2)//2, -1, -1):
        # i 构建堆时的调整部分的根位置low
        # high位置精准确定比较麻烦,我们用n-1来代替high的作用(防止调整函数中j越界)
        # 农村堆也位于整个对列表中
        sift(li, i, n-1)
    # 构建堆完成,开始挨个出数
    for k in range(n-1, -1, -1):
        li[0], li[k] = li[k], li[0]
        sift(li, 0, k-1)


if __name__ == "__main__":
    import random
    list = random.sample([i for i in range(100)], 10)
    print(list)
    heap_sort(list)
    print(list)

6. 堆排序的分析

  1. 时间复杂度: O(n*logn)
  2. 实际表现比快排序慢一些。
  3. Python内置模块一heapq(小根堆)
  4. 常用函数
    • heapify(x)
    • heappush(heap,item)
    • heappop(heap)
import heapq # 它可以用来实现优先队列
import random


list = random.sample([i for i in range(100)], 10)
print(list)
heapq.heapify(list) # 构建堆的过程(默认小根堆)
for i in range(len(list)):
    # 每次弹出最小的数
    print(heapq.heappop(list), end=",")

7. 堆排序的实际应用

  1. 现在有n个数,设计算法得到前k大的数。(k<n) 如:榜单、热搜榜(按照点击数,收藏数)
  2. 解决思路:
    • 排序后切片 O(nlogn)
    • 排序LowB三人组O(Kn) 如:冒泡冒个k次就可以了
    • 堆排序思路O(nlogk)
  3. 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数,也就是现在k中最小的数。
  4. 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;
  5. 遍历列表所有元素后,倒序弹出堆顶。
# 这里建立的是小根堆
def sift(li, low, high):
    i = low
    j = 2 * i + 1
    tmp = li[low]
    while j <= high:
        if (j + 1) <= high and li[j + 1] < li[j]:
            j = j + 1  # 把j指向右边孩子
        if li[j] < tmp:
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            li[i] = tmp
            break
    else:
        li[i] = tmp


def top(li, k):
    heap = li[0:k]
    # 建立堆
    for i in range((k - 2) // 2, -1, -1):
        sift(heap, i, k - 1)
    # 不断更新堆
    for i in range(k, len(li)-1):
        if li[i] > heap[0]:
            heap[0] = li[i]
            sift(heap, 0, k - 1)
    # 输出堆
    for i in range(k-1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i-1)
    return heap


if __name__ == "__main__":
    import random
    list = random.sample([i for i in range(1000)], 100)
    print(list)
    print(top(list, 10))

第三节 NB 三人组之归并排序

1. 归并

  1. 归并排序非常有用。python中内置排序算法其实也用到了归并排序
  2. 假设现在的列表分两段有序(前提),如何将其合成为一个有序列表。
  3. 比较两段的第一个,小的出数,指针后移,直到一个序列为空。把另外一个序列全部出数。这种操作称为一次归并。
    在这里插入图片描述

2. 归并排序

  1. 分解:将列表越分越小,直至分成一个元素。
  2. 终止条件:一个元素是有序的。
  3. 合并:将两个有序列表归并,列表越来越大。
    在这里插入图片描述
# 假设传过来的列表两段有序
def merge(li, low, mid, high):
    '''
    :param li:  归并的列表
    :param low: 第一个有序列表的起始点
    :param mid: 第一个有序列表的终止点
    :param high: 第二个有序列表的终止点
    :return:
    '''
    # 指针移动, 变量记录指针
    i = low
    j = mid + 1
    ltmp = []
    # 左右两边都有数时
    while i <= mid and j <= high:
        # 因为移动的指针,不能原地排序.开辟列表储存值
        if li[i] < li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1
    # 只有一边有数了
    # 只有左边有数
    while i <= mid:
        ltmp.append(li[i])
        i += 1
    # 只有右边有数
    while j <= high:
        ltmp.append(li[j])
        j += 1
    # 把ltmp 写回到li
    li[low: high+1] = ltmp


# 测试merge函数
# li = [2, 4, 5, 7, 1, 3, 6, 8]
# merge(li, 0, 3, 7)
# print(li)


def merge_sort(li, low, high):
    # 递归直到low和high相等。只有一个元素
    if low < high:
        mid = (low + high) // 2
        merge_sort(li, low, mid)
        merge_sort(li, mid+1, high)
        merge(li, low, mid, high)


if __name__ == "__main__":
    import random
    list = random.sample([i for i in range(100)], 10)
    print(list)
    merge_sort(list, 0, len(list)-1)
    print(list)

3. 归并排序总结

  1. 时间复杂度: O(n*logn)
  2. 空间复杂度:O(n)
  3. python的sort方法内部实现。基于归并排序,结合归并排序和插入排序做了优化的Tmsort.

第四节 NB 三人组总结

  1. 三种排序算法的时间复杂度都是0(nlogn)
  2. 一般情况下,就运行时间而言:快速排序<归并排序<堆排序
  3. 三种排序算法的缺点:
    • 快速排序:极端情况下排序效率低
    • 归并排序:需要额外的内存开销
    • 堆排序:在快的排序算法中相对较慢
  4. 递归也需要用到空间。需要用到系统栈的空间。如下面快速排序的空间复杂度。
  5. 排序算法的稳定性:当两个元素值一样的时候,保持它们的相对位置不变。(排序前在前面,排序后也在前面)
  6. 挨着换的都稳定,飞着换的就不稳定。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值