第 4 章 算法 进阶
4.1 排序
4.1.1 排序算法简介
排序:把无序的队列变成有序的队列
排序算法:排序算法是一种将一串
无规律数据
依照特定顺序
进行排列的一种方法或思路
。排序算法的稳定性:队列中有相同的元素,排序前后,这两个相同元素的顺序有没有发生变化。
特点:
输入:无序队列 输出:有序队列
应用场景:
各种排行榜 - 服不服排行榜
各种表格 - 座位表
排序算法的关键点:
有序队列:有序区刚开始没有任何数据,逐渐变多
无序队列:有序区刚开始存放素所有数据,逐渐变空
排序算法的稳定性
队列中相同元素排序前后
- 没有发生变化,这表示算法有稳定性
发生变化,表示没有稳定性
常见的排序算法:
三基冒插选,中快高堆归,其基希尔桶,冒快会归并
常见排序算法 | 算法 |
---|---|
基础 | 冒泡、插入、选择 |
中级 | 快速 |
高级 | 堆、归并 |
其他 | 基数、希尔、桶 |
4.1.2 冒泡排序
冒泡排序
: 相邻的元素两两比较,升序的话:
大的在右,小的在左
,降序的话,反之。经过数次比较循环,最终达到一个从小到大(升序)
或者从大到小(降序)
的有序序列.这个算法由于类似于气泡从水底冒出,所以叫“冒泡”排序。
过程跟踪
: 在整个冒泡排序过程中,有一个标识指向两个元素的最大值,当这个最大值移动的时候,标识也会随之移动,这就叫做:
过程跟踪
。
特点:
-
- 元素替换:相邻元素从小到大: 左比右大,数据先交换位置,大的和右侧的元素继续比较 左比右小,数据不交换位置,大的和右侧的元素继续比较 左右相等,数据不交换位置,大的和右侧的元素继续比较 - 比较次数:无序队列元素个数 - 1 - 冒泡次数:无序队列元素个数 - 1 - 冒泡次数和比较次数关系: - 冒泡次数:1 ===> n - 比较次数:n ===> 1 - 过程跟踪:alist[i]=最大值
冒泡排序的分析:
>- 元素替换:最基本元素比较(列表的下表)
>
>- 内层比较循环:每次冒泡排序,内层的元素比较次数
>
> 比较次数 + 元素范围(下标)
>
>- 外层冒泡循环:执行多少次冒泡排序
>
> 冒泡次数、冒泡次数和元素比较次数关系
>
>- 不替换情况:特殊情况
>
> 计数器
冒泡排序的实践:
def bubble_sort(alist):
"""冒泡排序"""
# 获取列表元素的总数量
n = len(alist)
# 3.冒泡排序循环范围
# 冒泡排序只关注排序次数
for j in range(n - 1, 0, -1):
# 开始比较前,定义计数器 count 的初始值为 0
count = 0
# 2.内层的数据比较循环范围
for i in range(j):
# 1.相邻元素替换
if alist[i] > alist[i + 1]:
alist[i], alist[i + 1] = alist[i + 1], alist[i]
# 数据替换完毕,计数器加 1
count += 1
# 4.不替换情况
# 如果计数器的值为 0,表示没有发生任何替换,那么就退出当前循环
if count == 0:
break
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
bubble_sort(li)
print(li)
时间复杂度
最优时间复杂度:O(n)
最坏时间复杂度:O(n^2)
稳定性:稳定
拓展:降序<
4.1.3 选择排序
简单直观
从无序队列里面挑选最小的元素,和无序队列头部元素替换(放到有序队列中),最终全部元素形成一个有序的队列。
选择排序的原理:
选择排序的主要特点与
元素替换
有关。每次移动一个元素,就有一个元素放到有序队列,n个元素的无序队列最多进行(n-1)次交换,就可以形成有序队列。
如果整个队列已排序,那么它不会被移动。
选择排序的分析:
比较循环:无序队列查找最小元素
mix表示同意最小元素
cur标识用于遍历所有元素
⚠️注意:过程跟踪:mix永远指向最小的,cur指向的元素负责
对比
。mix 和 cur 标签的初始化地址是相邻的:
mix标签所在元素的下标是
j
,那么cur标签所在元素的下标是j+1
元素替换:最小元素和无序队列第一个元素替换位置
**选择循环:**需要多少次替换,才能形成有序队列
选择排序的实践
def selection_sort(alist):
"""选择排序"""
# 获取列表元素的总数量
n = len(alist)
# 3. 选择循环
# 定义一个外围循环次数
# 排序次数范围的确定
for j in range(n - 1):
# 定义min_index 初始值
min_index = j
# 1.比较循环
# cur 标签元素下标移动范围(1,n-1)
for i in range(j + 1, n):
# 找到最小的元素
if alist[i] < alist[min_index]:
min_index = i
# 2.元素替换
# 保证最新的 min_index 不在无序队列首位,那么就将它和无序队列的首个元素进行替换
if min_index != j:
# mix 标签元素和无序队列首位置元素替换
alist[j], alist[min_index] = alist[min_index], alist[j]
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
selection_sort(li)
print(li)
关键点:
1、mix标签初始化:min_index = j
2、比较循环的范围:for i in range(j+1,n)
3、元素替换的条件:if min_index != j
4、排序次数范围的确定:for j in range(n-1)
时间复杂度
最优时间复杂度:O(n^2)
最坏时间复杂度:O(n^2)
稳定性:看情况
4.1.4 插入排序
先定义一个
有序队列
,然后把无序队列中的第一个元素
放到有序队列的合适位置
,重复操作,直至形成一个完整的有序队列
插入排序原理
1、构建有序序列
2、选择无序队列的第一个元素,先放在有序队列末尾,然后进行冒泡排序,放到指定的位置
3、循环2步,直到无序队列中所有元素全部进入有序队列的合适位置
特点:
(1)插入 ?冒泡
-
联系:插入排序的有序队列用到了冒泡算法
-
区别:升序情况下,
冒泡:有序队列在后,无序队列在前(无序队列 + 有序队列);
插入:有序队列在前,无序队列在后(有序队列 + 无序队列)
(2)插入?选择
- 选择:遍历未排序队列,将最小的元素移动到有序队列的末尾;
- 插入:把无序队列的
第一个元素
放到有序队列,通过使用冒泡算法,移动到合适的位置。
插入排序分析
元素替换:有序队列中元素比较替换
比较循环:每次排序,有序队列元素比较替换的次数
排序循环:需要进行多少次排序
插入排序实践
def insert_sort(alist):
"""插入排序"""
# 无序队列元素数量
n = len(alist)
# 3.插入排序循环
# 有序队列循环的次数
for i in range(n):
# 2.比较循环次数的确定
# 有序队列末尾元素下标为i,范围(0,i]
for j in range(i, 0, -1):
# 1.元素替换
# 有序列表的两个元素进行比较
if alist[j] < alist[j - 1]:
# 大小值元素替换
alist[j], alist[j - 1] = alist[j - 1], alist[j]
# 条件不满足,大小元素不替换
else:
break
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
insert_sort(li)
print(li)
关键点:
1、元素替换:if alist[j] < alist[j-1]
2、比较循环:for j in range(i,0,-1)
3、插入循环:for i in range(n):
时间复杂度
最优时间复杂度:O(n)
最坏时间复杂度:O(n^2)
稳定性:稳定
4.1.5 希尔排序
希尔排序(Shell Sort)是
插入排序
的一种。也称缩小增量排序
,是插入排序算法的一种高效的改进版本。
希尔排序原理:
两两一组、四四一组、八八一组…,直到所有元素为一组,进行排序
特点:
下标增量分组,对小组元素进行插入排序
下标增量的特点:
第一次分组,gap=n/2 ,
从第二次分组,gap=gap/2,
最后一次分组gap=1
整个分组过程就是:递归
希尔排序分析:
元素替换:分组队列中元素比较替换
下标的范围必须大于0
比较次数:每次分组后,同时有几组在进行比较
插入排序
分组次数:需要进行多少次分组
希尔排序实践:
def shell_sort(alist):
"""希尔排序"""
# 获取列表的长度
n = len(alist)
# 3.递归分组循环
# 获取下标偏移量gap(取整)
gap = n // 2
# 只要gap在合理范围内,就一直分组下去
while gap >= 1:
# 2.比较循环(多少组进行插入排序)
# 指定i下标的取值范围
for i in range(gap, n):
# 1.元素替换
# 对移动元素的下标进行条件判断
# 下标的范围必须大于0
while (i - gap) >= 0:
# 组内大小元素进行替换
if alist[i] < alist[i - gap]:
alist[i], alist[i - gap] = alist[i - gap], alist[i]
# 修改i 的属性重新指向原始元素(过程跟踪)
i = i - gap
# 否则的话,不进行替换
else:
break
# 没执行完一次分组内的插入排序,对gap进行/2细分
gap = gap // 2
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
shell_sort(li)
print(li)
1、元素替换:
下标范围:while (i - gap) >= 0:
替换条件:if alist[i] < alist[i-gap]:
过程跟踪:i = i - gap
2、比较循环:
元素的范围:for i in range(gap,n):
3、递归分组循环
偏移量初始值:gap = n // 2
递归循环的退出条件:while gap >= 1
gap偏移量规律:gap = gap // 2
时间复杂度
最优时间复杂度:O(nlogn)~O(n^2)
最坏时间复杂度:O(n^2)
稳定性:不稳定
4.1.6 快速排序
快速排序
,又称划分交换排序
,从无序队列中挑取一个元素,把无序队列分割成独立的两部分
,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,
整个排序过程可以递归进行
,以此达到整个数据变成有序序列。挑元素、划分组、分组重复前两步
快速排序原理
挑元素划分组,整体递归分组
特点:
1、因为是无序队列,所以位置可以随机挑
2、临时划分一个
空间
,存放我们挑选出来的中间元素3、左标签位置空,移动右标签,反之一样
4、重复3,直到左右侧标签指向同一个位置,
5、把临时存放的中间元素,
归位
左手右手一个慢动作,右手左手慢动作重播
整体划分特点:
1、递归拆分
2、拆分到最后,所有小组内的元素个数都是1
递归拆分到不能再拆
快速排序分析
序列切割
三个基本标签:
mid:指定要切割的临时中间数字
left:从队列左侧推进的标签
right:从队列右侧推进的标签
left永远小于right
右侧推进
左侧推进
停止推进 (即元素归位)
递归切分
递归拆分:小组边界的确定 和 递归功能实现
左侧边界start:
0
右侧边界end:left-1
左侧边界start:
left+1
右侧边界end:len(alist)-1
递归退出条件
快速排序实践
# 2.递归切分
# 2.1 小组边界确定
# 增加两个参数,左边界 start,右边界 end
def quick_sort(alist, start, end):
"""快速排序"""
# 2.3定义递归退出条件
if start < end:
# 1.序列切割
# 1.1定义三个基本标签
# 因为 mid 指定的是传入列表的左边界元素
mid = alist[start]
left = start
right = end
# 定义拆分条件
while left < right:
# 1.2 右侧推进
# 如果right元素 > mid值,right标签左移
while right > left and alist[right] >= mid:
right -= 1
# 如果right元素 < mid值,left标签元素设置为right标签元素
alist[left] = alist[right]
# 1.3 左侧推进
# 如果left元素 < mid值,left标签右移
while left < right and alist[left] < mid:
left += 1
# 如果left元素 > mid值,right标签元素设置为left标签元素
alist[right] = alist[left]
# 1.4 停止标签(元素归位)
# 退出循环,表示 left和right 标签合并在一起了
# 获取中间值
alist[left] = mid
# 2.2 递归功能的实现
# 函数自调用
# 对切割后左边的子部分进行快速排序
quick_sort(alist, start, left - 1)
# 对切割后右边的子部分进行快速排序
quick_sort(alist, left + 1, end)
if __name__ == "__main__":
li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
print(li)
quick_sort(li, 0, len(li) - 1)
print(li)
序列切割:
1、挑中间元素:mid = alist[start]
2、右推进:while right > left and alist[right] >= mid:
3、左推进:while left < right and alist[left] < mid:
4、推进循环:while left < right:
5、元素归位:alist[left] = mid
递归拆分:
1、小组边界确定:left = start、right = end
2、递归退出条件:if start < end:
3、函数自调用:quick_sort(alist, start, end)
时间复杂度
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
稳定性:不稳定
4.1.7 归并排序
归并排序是采用
分治法
的一个非常典型的应用。将无序队列
拆成
两个小组,组内
元素排序
,然后组间
元素逐个比较
,把小元素依次放到新队列
中。关键字:拆分、排序、组间、小、新队列
分组排序,合并新队列
归并排序原理
分组排序阶段:
1、将无序队列alist,拆分成成两个小组A和B,
2、分别对两个小组进行同样的
冒泡排序
3、用标签left和right,分别对小组A和B进行管理
合并新队列阶段:
4、两个标签所在的元素比较大小,
5、将小的元素放到一个新队列中,然后小元素所在的标签向
右移
6、多次执行4和5,最终肯定有一个小组先为空 7、把不为空的小组元素,按顺序全部移到新队列的末尾
8、无序队列中的所有元素就在新队列中形成有序队列了
特点:
两个阶段:分组排序 + 合并
合并策略:组间比较、新增小,小移标
两种情况:
分两组合并排序
递归分组合并排序:层级分组、排序、层级合并
归并排序分析
分组实现
首次分组
正常分组
不能分组:空队列/队列只有一个元素
递归分组
合并分组
合并分组的排序
准备工作
组标签:定义两个标签l和r,分别是分组列表的首 位下标值0
组长度
空队列
空列表增加数据
空列表添加数据
一组空,另一组剩余元素按顺序一次性添加到空列
归并排序实践
def fen_zu(alist):
"""分组"""
# 获取当前序列的长度
n = len(alist)
# 1.首次分组
# 1.2 不能分组
# 队列异常情况
if n <= 1:
return alist
# 1.1 正常分组
# 把当前列表分成两部分,使用切片方式h获取两部分内容
mid = n // 2
# 左半部分
# left = alist[:mid]
# 右半部分
# right = alist[mid:]
# 2.递归分组
# 左半边数据
zuo = fen_zu(alist[:mid])
# 右半边数据
you = fen_zu(alist[mid:])
# 3.合并分组
# 将分组后的数据交个一个合并数据的函数去处理
return merge(zuo, you)
def merge(zuo, you):
"""归并排序"""
# 3.1 准备工作
# (1)组标签
# 定义标签 l 和 r 在两组的位置
l, r = 0, 0
# (3)空队列
# 定义一个空列表
result = []
# (2)组长度
# 获取两个分组的长度
zuo_len = len(zuo)
you_len = len(you)
# 指定标签的有效范围
while l < zuo_len and r < you_len:
# 3.2 空列表添加数据
# 判断两侧标签指定的数据大小
if zuo[l] <= you[r]:
# 将左侧小数据追加到新队列
result.append(zuo[l])
# left标签右移一位
l += 1
else:
# 将右侧小数据追加到新队列
result.append(you[r])
# right标签右移一位
r += 1
# 3.3 一组为空,另一组剩余元素按顺序添加到新队列
# 将左侧的剩余内容,一次性添加到 result 表中
result += zuo[l:]
# 将右侧的剩余内容,一次性添加到 result 表中
result += you[r:]
# 返回 result 表
return result
if __name__ == "__main__":
li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
print("处理前: %s" % li)
sort_list = fen_zu(li)
print("处理后: %s" % li)
print("新列表: %s" % sort_list)
关键点:
1、异常分组:if n <= 1:
2、递归分组:fen_zu(alist[:mid])
3、分组合并:merge(zuo, you)
1、数据比较条件:while l < zuo_len and r < you_len:
2、小元素移动:result.append(zuo[l])
3、小元素标签处理:l += 1
4、异常情况:result += zuo[l:]
5、最终效果:return result
时间复杂度
最优时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
稳定性:稳定
4.1.8 堆排序
堆是采用
顺序表
存储的一种近似完全二叉树
的结构。父节点和子结点关系(父找子):
左子节点位置:
2i + 1
右子节点位置:2i + 2
堆分类
堆分类 | |
---|---|
大顶堆 | 任一节点都比其孩子节点大 最大值 堆顶元素 alist[0] |
小顶堆 | 任一节点都比其孩子节点小 |
它是指利用堆这种树结构所设计的一种排序算法。
将无序列表先构造一个有特点的堆,然后利用列表的特点快速定位最大/小的元素,将其放到一个队列中。
特点:
无序队列构建一个堆,堆顶和堆尾元素替换位置
重新构建堆,堆顶和堆尾元素替换位置,…
头尾替换,恢复堆后再继续
堆排序原理
构建一个堆:从
最后一个有子节点的节点
开始构建,下标为[n/2-1]堆的调整:移除堆顶元素,用
队列中最后一个元素
填补,自上而下进行调整
1 将无序列表构造
为一个标准的大顶堆
2 将堆顶元素和堆尾元素进行替换
相当于将最大元素放到了有序序列
剩余的无序序列少了一个
3 将剩余的无序序列重新调整为标准的大顶堆
4 重复2-3 最终形成一个有序序列
堆排序分析
堆的调整:
准备工作
队列参数:data
堆顶元素的确定:传入一个堆顶队列的下标low
堆顶元素的临时存放空间:tmp
无序队列中元素的最大下标值:high
选择新的堆顶节点
选大的子节点
最大子节点跟移除的堆顶元素进行比较
堆顶元素排序
堆的构造
堆顶元素输出到有序队列
步骤
1 无序序列构造标准大顶堆
2 堆顶和堆尾元素替换
3 剩余无序序列调整大顶堆
4 重复2-3
堆排序实践
关键点
1 无序序列构造标准大顶堆
从最后一个包含子节点的父节点开始构造
n/2 -1
顺序:从下向上来构造的
n/2 -1 n/2 -2 n/2 -3 。。。0
表达式:
range(int(n/2)-1,-1,-1)
2 堆顶和堆尾元素替换
无序列表是alist
堆顶位置 0
假设堆尾下标是z
替换:
alist[0],alist[z] = alist[z],alist[0]
3 剩余无序序列调整大顶堆
3.1 把临时堆顶元素放到临时空间tmp
tmp = alist[0]
3.2 选择一个最大的子节点
3.3 最大子节点和临时堆顶元素比较
如果临时堆顶元素大,那么归位
如果子节点大,那么最大子节点放到堆顶位置
假设子节点下标为j alist[0] = alist[j]
3.4 沿着破坏的路径继续调整下去
i = j
j = 2*i + 1
原则:
从上到下
调整的时候,:
对象 alist
堆顶位置 low
调整的范围 high
4 重复2-3
在替换的过程中,堆尾元素的下标z变化是:
n-1 n-2 n-3 n-4 ..... 0
表达式:
range(n-1,-1,-1)
def sift(alist, low, high):
"""堆的调整"""
# 1.准备工作:堆顶元素的标签 + 临时队列
# 指定移除的堆顶位置元素下标为 i
i = low
# 将移除的堆顶元素存放到一个临时队列tmp
tmp = alist[i]
# 2.选择新的堆顶节点
# 假设左子节点是大节点,下标是 j
j = 2 * i + 1
# j 下标的操作范围
# 无序队列中元素的最大下标值
# 左侧子节点小于堆的最大范围值
while j <= high:
# 2.1 选取最大子节点
# 左右子节点进行比较
if j + 1 <= high and alist[j] < alist[j + 1]:
# 过程跟踪,保证通过 j 找到最大元素
# 上移节点标号 j 指向右侧节点
j += 1
# 2.2 最大子节点 和 原堆顶节点比较
# 子结点 > 原堆顶节点
if alist[j] > tmp:
# 将子节点元素移动到堆顶元素
alist[i] = alist[j]
# 因为子节点位置空了,相当于堆顶节点移除了,又要重复操作,所以需要更新 i 和 j 的值
i = j
j = 2 * i + 1
# 如果最大的子节点小于移除的堆顶元素,终止该操作即可
else:
break
# 临时堆顶元素归位
# 设置堆顶节点为原来的内容
alist[i] = tmp
def heap_sort(alist):
"""堆排序"""
# 获取当前列表长度
n = len(alist)
# 1.无序列表构造标准大堆顶
# 对所有父节点进行堆的调整,而且是降序排列(从下往上构造)
# 最后一个元素是n,其父元素尾n/2-1
for i in range(int(n / 2) - 1, -1, -1):
sift(alist, i, n - 1)
# 堆顶元素排序
# 指定最小元素的范围
for z in range(n - 1, -1, -1):
# 2.堆顶元素与堆尾元素替换
# 队列中最大元素和最小元素进行替换
alist[0], alist[z] = alist[z], alist[0]
# 3.剩余无序列表调整大顶堆(从上到下)
# 调整新队列
# 替换完毕后,重新调整堆结构,新的堆结构元素个数变成了 i-1 个
sift(alist, 0, z - 1)
# 返回最终的有序队列
return alist
if __name__ == '__main__':
a = [0, 2, 6, 98, 34, 5, 23, 11, 89, 100, 7]
print("排序之前:%s" % a)
c = heap_sort(a)
print("排序之后:%s" % c)
时间复杂度
成本:
最优: O(nlogn)
最坏: O(nlogn)
稳定性:不稳定
4.1.9 排序总结
技术:
冒小左移,选追加
插入合适,分希尔
快速两半,归新列
顺表构造首尾堆
成本:
冒泡 插入 选择 希尔 堆 归并 快速 系统
技术总结
冒泡排序 | 在无序队列中选择最小的移动到最左侧 |
选择排序 | 定一个有序队列,从无序队列中选择最小的元素追加到有序队列的末尾 |
插入排序 | 定一个有序队列,从无序队列中选择第一个元素,插入到到有序队列的合适位置 |
希尔排序 | 通过对无序队列进行分组,然后再采用插入的排序方法 |
快速排序 | 指定一个元素,将无序队列拆分为大小两部分,然后层级递进,最终实现有序队列 |
归并排序 | 是将无序队列拆分,然后小组内排序,组间元素比较后在新队列中进行排序 |
堆 排 序 | 顺序表方式构造堆,首尾替换调整堆 |
冒小左移 选追加,插入合适 分希尔,快速两半 归新列,顺表构造首尾堆
成本总结
排序方法 | 时间复杂度 | 稳定性 | 代码复杂度 | ||
---|---|---|---|---|---|
最坏情况 | 平均情况 | 最好情况 | |||
冒泡排序 | O(n2) | O(n^2) | O(n) | 稳定 | 简单 |
选择排序 | O(n2) | O(n^2) | O(n^2) | 不稳定 | 简单 |
插入排序 | O(n2) | O(n^2) | O(n^2) | 稳定 | 简单 |
希尔排序 | O(n2) | O(nlogn~n^2) | O(nlogn~n^2) | 不稳定 | 中下等 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | 不稳定 | 中等 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | 中等 |
快速排序 | O(n2) | O(nlogn) | O(nlogn) | 不稳定 | 中下等 |
4.2 搜索
4.2.1 搜索简介
搜索是在队列中找到一个
特定元素
的算法过程。搜索的常见方法:
顺序查找 / 二分法查找 /二叉树查找 / 哈希查找
4.2.2 二分查找
简介
二分查找又称
折半查找
,适用于不经常变动
而查找频繁
的有序
列表。优点:比较次数少、查找速度快、平均性能好
缺点:要求待查表为有序表,且插入删除困难
原理
1.找中间,比较
2.二分比较
3、重复1-2过程,如果最终能找到,表示查找成功True,否则的话,查找不成功False
4.2.3 递归二分实践
递归二分查找分析
列表是否为空
列表中间元素匹配
匹配成功
匹配失败
递归二分查找
二分查找实践
def binary_search(alist, item):
"""二分查找"""
# 获取列表的长度
n = len(alist)
# 1.列表是否为空
if n == 0:
return False
# 2.列表中间元素匹配
# 列表从中间切开
mid = n // 2
# 2.1判断是否匹配
# 匹配成功
# 判断中间值是否是我们想要的值,是的话返回 True
if alist[mid] == item:
return True
# 2.2匹配失败
# 查找的元素小于队列中间值
elif item < alist[mid]:
# 3.二分查找
# 左侧二分递归查找
return binary_search(alist[:mid], item)
# 查找的元素大于队列中间值
else:
# 右侧二分递归查找
return binary_search(alist[mid + 1:], item)
if __name__ == '__main__':
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42]
print(binary_search(testlist, 3))
print(binary_search(testlist, 13))
4.3 二叉树
4.3.1 二叉树遍历
遍历简介
遍历是指对树中所有结点的信息的访问一次
且仅访问一次
。
遍历方法
树的遍历模式 | 特点 |
---|---|
广度优先遍历(队列) | 查看数据: 从上到下,分层查看,每层 从左向右依次查看 ,直至所有数据查看完毕添加数据: 从上到下,分层添加,每层从 左向右依次添加 。 |
深度优先遍历(递归) | 首先递归方法看最深的分支元素, 再看其他的节点元素。 |
三种深度遍历方法: 先序遍历(preorder)访问顺序:根节点->左子树->右子树 中序遍历(inorder)访问顺序:左子树->根节点->右子树 后序遍历(postorder)访问顺序:左子树->右子树->根节点 |
4.3.2 查询实践(广度优先)
二叉树没有数据
二叉树有数据
class BaseNode(object):
"""定义节点的基本属性"""
def __init__(self, item):
# 节点存储的内容
self.item = item
# 左侧子节点的索引值
self.lsub = None
# 右侧子节点的索引值
self.rsub = None
class Tree(object):
"""二叉树遍历"""
def __init__(self, node=None):
"""定义树的基本属性:根节点"""
# 不能写成self.__root,因为肯定需要看
self.root = node
# 添加结点
def add(self, elem):
# 新节点
# 定义要添加到树结构中的节点信息
node = BaseNode(elem)
# 1.如果根结点不存在数据
# 如果树是空,则对根节点赋值,对应的特点:从上到下
if self.root == None:
self.root = node
# 如果根节点存在数据
else:
# 2.待处理父节点队列
# 使用一个临时列表来存储我们要处理的元素: 对应的特点-从左到右(待处理父节点队列)
queue = []
# 先把根节点放到我们要处理的临时队列中
queue.append(self.root)
# 给父节点添加数据
# 只要处理队列中有要处理的数据,那就要一直处理下去
while queue:
# 3.确定要操作的父节点
# 从队列的头部获取要处理的元素
cur = queue.pop(0)
# 4.左侧子结点是否有数据
# 判断要处理的左侧子节点为空
if cur.lsub == None:
# 把接受到的item放到左侧节点位置
cur.lsub = node
# 添加完毕后,退出
return
# 左侧子节点不为空,有数据
# 左侧节点有数据,把该数据追加到待处理父节点队列末尾
else:
queue.append(cur.lsub)
# 5.右侧子结点是否为空
if cur.rsub == None:
# 把接受到的item放到右侧节点位置
cur.rsub = node
# 添加完毕后,退出
return
# 右侧子节点不为空,有数据
# 右侧节点有数据,把该数据追加到待处理父节点队列末尾
else:
queue.append(cur.rsub)
def breadth_search(self):
"""二叉树广度优先遍历"""
# 1.二叉树没有数据
if self.root is None:
return
# 2.二叉树有数据
else:
# 2.1创建一个待处理父节点队列
queue = []
# 把根节点放入
queue.append(self.root)
# 2.2 判断临时队列是否为空
# 判断临时队列不为空,表示还有未查看的父节点
while queue:
# 获取父节点并打印信息
node = queue.pop(0)
print(node.item, end=' ')
# 左侧节点不为空,将其放入待处理队列
if node.lsub is not None:
queue.append(node.lsub)
# 右侧节点不为空,将其放入待处理队列
if node.rsub is not None:
queue.append(node.rsub)
# 修复 print 功能
print("")
关键点
1、空树处理: if self.root == None:
2、树非空,待处理父节点队列 queue.append(self.root)
3、左右侧子节点处理:
不空,将其放入待处理父节点队列:queue.append(node.lsub)
4.3.3 查询实践(深度优先)
递归查询
先序遍历
pdef preorder(self, root):
"""
深度优先遍历(1):先序遍历
根节点===>左节点===>右节点
"""
# 根节点没有数据,为空
if root is None:
return
# 根节点有数据
# 打印根节点内容
print(root.item, end=' ')
self.preorder(root.lsub)
self.preorder(root.rsub)
中序遍历
def inorder(self, root):
"""
深度优先遍历(1):中序遍历
左节点===>根节点===>右节点
"""
# 根节点没有数据,为空
if root is None:
return
# 根节点有数据
# 打印根节点内容
self.inorder(root.lsub)
print(root.item, end=' ')
self.inorder(root.rsub)
后序遍历
def postorder(self, root):
"""
深度优先遍历(1):后序遍历
左节点===>右节点===>根节点
"""
# 根节点没有数据,为空
if root is None:
return
# 根节点有数据
# 打印根节点内容
self.postorder(root.lsub)
self.postorder(root.rsub)
print(root.item, end=' ')
4.3.4 二叉树反推(拓展)
反推原理:先
根
中定边
,往复树两边
先序 + 中序 / 中序 + 后序