python数据结构与算法基础 第四课
tags:
- python
- 路飞学院
categories:
- python
- 排序算法
- NB 三人组
- 快速排序
- 堆排序
- 归并排序
文章目录
第一节 NB 三人组之快速排序
1. 快速排序介绍
-
快速排序特点:快
-
快速排序思路:
- 取一个元素p (这里我们取第一个元素),使元素p归位;
- 列表被p分成两部分,左边都比p小,右边都比p大;
- 一次完成后列表分成两个部分,分别对左右两个列表继续做归位;
- 递归完成排序
- 综上分析:需要记录传入列表左边的位置,和传入列表右边的位置。归位后以中间为基准将列表分为[left, mid-1]和[mid+1, right]两个列表进行递归。

-
实现元素p的归位的思路:
- 这里指定P元素对应的位置记录为做指针,列表最后位置记录右指针。
- 取出第一元素P, 从列表右边(指针为right)开始向列表前找。right指针跟着移动
- 直到找到比P小的元素(或者left和right指针重合),Right指针暂停。把这个元素值放到P第一个元素的位置
- 然后从左边(指针为left)开始找, 直到找到比P大的元素(或者left和right指针重合),将这个元素放到之前right所在位置。
- 循环往复。直到left和right指针重合,重合位置就是P归位位置。
-
快速排序实现算法如下:
# 快速排序算法
# 首先,实现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)
- 快速排序实现总结如下:
-
快速排序的时间复杂度一般情况为:O(n*logn)
-
快速排序的最坏情况:
- 排序的列表是倒序的,此时间复杂度为:O(n*n)
- 解决办法:排序前随机选一个数和第一数交换一下(随机化)
-
修改递归最大深度
-
import sys
sys. setrecursionlimit(100000)
- 如何把装饰器加到递归函数上(给递归函数加上一个马甲即可)
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. 堆排序相关知识介绍-树的定义
- 树是一种数据结构,比如:目录结构。
- 树是一种可以递归定义的数据结构
- 树的定义:树是由n个节点组成的集合
- 如果n=0,那这是一棵空树;
- 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是-棵树。
2. 堆排序相关知识介绍-树的相关概念
- 根节点、叶子节点
- 树的深度(高度)
- 节点的度。节点的分差数比如:F的度是三
- 树的度。所有节点的分差数最大的数叫数的度。
- 孩子节点/父节点
- 子树

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

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. 堆排序的分析
- 时间复杂度: O(n*logn)
- 实际表现比快排序慢一些。
- Python内置模块一heapq(小根堆)
- 常用函数
- 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. 堆排序的实际应用
- 现在有n个数,设计算法得到前k大的数。(k<n) 如:榜单、热搜榜(按照点击数,收藏数)
- 解决思路:
- 排序后切片 O(nlogn)
- 排序LowB三人组O(Kn) 如:冒泡冒个k次就可以了
- 堆排序思路O(nlogk)
- 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数,也就是现在k中最小的数。
- 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;
- 遍历列表所有元素后,倒序弹出堆顶。
# 这里建立的是小根堆
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. 归并
- 归并排序非常有用。python中内置排序算法其实也用到了归并排序
- 假设现在的列表分两段有序(前提),如何将其合成为一个有序列表。
- 比较两段的第一个,小的出数,指针后移,直到一个序列为空。把另外一个序列全部出数。这种操作称为一次归并。

2. 归并排序
- 分解:将列表越分越小,直至分成一个元素。
- 终止条件:一个元素是有序的。
- 合并:将两个有序列表归并,列表越来越大。

# 假设传过来的列表两段有序
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. 归并排序总结
- 时间复杂度: O(n*logn)
- 空间复杂度:O(n)
- python的sort方法内部实现。基于归并排序,结合归并排序和插入排序做了优化的Tmsort.
第四节 NB 三人组总结
- 三种排序算法的时间复杂度都是0(nlogn)
- 一般情况下,就运行时间而言:快速排序<归并排序<堆排序
- 三种排序算法的缺点:
- 快速排序:极端情况下排序效率低
- 归并排序:需要额外的内存开销
- 堆排序:在快的排序算法中相对较慢
- 递归也需要用到空间。需要用到系统栈的空间。如下面快速排序的空间复杂度。
- 排序算法的稳定性:当两个元素值一样的时候,保持它们的相对位置不变。(排序前在前面,排序后也在前面)
- 挨着换的都稳定,飞着换的就不稳定。

224

被折叠的 条评论
为什么被折叠?



