b站视频:路飞IT学城
清华计算机博士带你学习Python算法+数据结构_哔哩哔哩_bilibili
文章目录
- #01 算法入门概念
- #02 估计算法运行效率与时间复杂度
- #03 简单判断时间复杂度
- #04 空间复杂度
- #05 递归
- #06 汉诺塔问题
- #07 顺序查找
- #08 二分查找介绍
- #09 二分查找代码
- #10 二分查找与线性查找的比较
#01 算法入门概念
# 算法(Algorithm):一个计算过程,解决问题的方法
# 程序 = 数据结构 + 算法 数据结构 静态 算法 动态
#02 估计算法运行效率与时间复杂度
# 用什么方式体现算法运行的快慢? (不能使用时间)
# for循环执行n次 n在算法里叫做问题的规模(问题的复杂度)
# 时间复杂度:用来评估算法运行效率的一个式子
print('Hello World') # 注:时间复杂度定义为 O(1) 1个print 1就是一个单位 基本操作是1
for i in range(n): # 注:O(n) 运行n次
print('Hello World')
for i in range(n): # 注:O(n2)
for j in range(n):
print('Hello World')
for i in range(n): # 注:O(n3)
for j in range(n):
for k in range(n):
print('Hello World')
print('Hello World') # 注:O(1) 打印1次和3次差距不大 没有上升到n这么大
print('Hello Python') # 注:表示近似
print('Hello Algorithm')
#--------------------------
for i in range(n): # 注:O(n2) 忽略 n 忽略小单位
print('Hello World') # 注:类比生活的问几个小时 不涉及分钟
for j in range(n):
print('Hello World')
#--------------------------
while n > 1:
print(n) # 注:问题规模少一半 但不能写n/2
n = n // 2
# 时间复杂度记为O(log2n)或 O(logn) # 注:以对数形式表示
# 把2省略掉,依据是计算机里面都是二进制的
# 当算法过程出现循环折半的时候, 复杂度式子中会出现logn
# 注:当算法出现循环减半的时候,就一定会出现logn (问题规模缩小一半)
# n=64 输出: 2**6=64
# 64 log2(64)=6
# 32
# 16
# 8
# 4
# 2
### 时间复杂度小节
# 时间复杂度是用来估计算法运行时间的一个式子(单位)。
# 一般来说,时间复杂度高的算法比复杂度低的算法慢。
# 注:为什么是一般来说,1因为和机器有关系,2因为和n有关系
# 常见的时间复杂度(按效率排序)
# O(1)<O(logn)<O(n)<O(nlogn)<O(n**2)<O(n**2logn)<O(n**3)
# 理解:O(1) 只执行1次或几次
# <O(logn)<O(n) logn比n小 log64 < 6
# n < n方 < n立方
# O(n)<O(nlogn)<O(n**2)<O(n**2logn)<O(n**3)
# 复杂问题的时间复杂度 O(n!) O(2**n) O(n**n) … n的阶层…… 非常难解决的问题
#可以把logn想成0.5n (自己的理解)
一般来说,时间复杂度高的算法比复杂度低的算法慢。
O(1)<O(logn)<O(n)<O(nlogn)<O(n**2)<O(n**2logn)<O(n**3)
#03 简单判断时间复杂度
### 如何简单快速地判断算法复杂度
# 快速判断算法复杂度(适用于绝大多数简单情况):
# 确定问题规模n # 注:循环次数 列表进行排序就是列表的长度
# 循环减半过程→logn # 注:循环折半的情况
# k层关于n的循环→n**k # 注:几层循环就是n的几次方 列表长度的循环次数
# 复杂情况:根据算法执行过程判断
快速判断算法复杂度(适用于绝大多数简单情况):
确定问题规模n
循环减半过程→logn
k层关于n的循环→n**k
#04 空间复杂度
### 空间复杂度
# 空间复杂度:用来评估算法内存占用大小的式子
# 空间复杂度的表示方式与时间复杂度完全一样
# 算法使用了几个变量:O(1) # 注:1个变量是0(1) 2个变量也是O(1)
# 算法使用了长度为n的一维列表:O(n)
# 算法使用了m行n列的二维列表:O(mn) # 注:m*n n行n列 n*n
# “空间换时间” (目前公司常采取的策略)
# 注:研究1个算法,时间比空间重要 (远远) 因为现在内存造价便宜了 让用户等待时间到短
# 注:规则 空间换时间 所以有很多分布式允许 1个机器上运算的东西放到多个机器上运算
#注:跑起来看一看 涉及到n的问题,n不同,内存占用就不一样
空间复杂度:用来评估算法内存占用大小的式子
空间复杂度的表示方式与时间复杂度完全一样:
# 算法使用了几个变量:O(1)
# 算法使用了⻓度为n的一维列表:O(n)
# 算法使用了m行n列的二维列表:O(mn)
“空间换时间” (目前公司常采取的策略)
#05 递归
#复习:递归
### 递归的两个特点:
# 调用自身
# 结束条件
#---------------------------------------------------------
def func1(x): # 注:不是合法递归 因为没有结束条件 (死递归)
print(x) # 注:比如x=3进去,会无线循环下去
func1(x-1)
#---------------------------------------------------------
def func2(x): # 注:不是合法递归
if x>0: # 注:看似合法条件 但是最后是x+1
print(x) # 注:2,3,4,5……
func2(x+1)
#---------------------------------------------------------
def func3(x): # 注:合法递归
if x>0:
print(x)
func3(x-1)
# 传进去x=3 会打印3 2 1
#---------------------------------------------------------
def func4(x):
if x>0:
func4(x-1)
print(x)
# 传进去x=3 会打印1 2 3
#---------------------------------------------------------
# 可以用框表示函数的调用 这样好理解
# func3先是打印 后是递归 先小后大
# func4先递归 后打印 先大后小 最里层print 是 x=1 先打印
### 递归的两个特点:
# 调用自身
# 结束条件
#06 汉诺塔问题
### 递归实例:汉诺塔问题
# 大梵天创造世界的时候做了三根金刚石柱子,在一根 柱子上从下往上按照大小顺序摞着64片⻩金圆盘。
# 大梵天命令婆罗门把圆盘从下面开始按大小顺序重新 摆放在另一根柱子上。
# 在小圆盘上不能放大圆盘,在三根柱子之间一次只能 移动一个圆盘。
# 64根柱子移动完毕之日,就是世界毁灭之时。
# 注:小圆盘只能放在大圆盘的上面 不能放在下面
#------------------------------------------------------------
### n=2时:
# 1.把小圆盘从A移动到B
# 2.把大圆盘从A移动到C
# 3.把小圆盘从B移动到C
#------------------------------------------------------------
### n个盘子时: # 注:把它看作是递归的过程
# 1.把n-1个圆盘从A经过C移动到B
# 2.把第n个圆盘从A移动到C
# 3.把n-1个小圆盘从B经过A移动到C
#------------------------------------------------------------
### 注:把它看作是递归的过程
#注:把上面 n - 1 个盘子看成整体
#注:把下面 1 个盘子看成1个部分
#n 个盘子时
# 1.把 n-1 个圆盘从A经过C移动到B # 注:中间的过程 不管怎么经过C 第1、3步 移动 n-1个盘子
# 2.把第n个圆盘从A移动到C # 注:最底下的盘子 从A移动到C 只有第2步 是移动一步的情况
# 3.把n-1个小圆盘从B经过A移动到C # 注:第1、3步 移动 n-1个盘子
#注:它不是一个只能移动一个盘子的合法的步骤,但是它是一个比原问题规模小了1的同样的1个问题 (n-1)
#注:递归:第1步和第3步是原问题的递归的子问题 (比它规模更小的问题)
#-----------------------------------------------------------------------
#注:(n, a, b, c) --> 把n个盘子从a移动到c 中间的b是经过b 第3个参数是经过
#注:参数意义:n把几个盘子 a 从a,b 经过b,c 移动到c
def hanoi(n, a, b, c): # 注:参数n 表示 n个盘子 ; a、b、c表示这3个参数表示这三个盘子的名字
if n > 0: # 注:递归终止条件是 n = 0 的时候 一步都不需要移动(没有盘子)
# 注:第一步 (n-1, a, c, b) --> n-1个盘子 从a经过 b 移动到c
hanoi(n-1, a, c, b) # 注:把n-1个盘子从A经过C移动到B 调用自己 n-1个盘子 从a经过b移动到c
# print("#%d: moving from %s to %s." % (num, a, c)) # 注:第2步 最大的盘子 从a移动到c
print("moving from %s to %s." % (a, c))
hanoi(n-1, b, a, c) # 注:n-1个盘子 从 b 经过 a 移动到 c
#注:每次移动都只是柱子最上面那个
hanoi(3, 'A', 'B', 'C')
#结果为
# moving from A to C.
# moving from A to B.
# moving from C to B.
# moving from A to C.
# moving from B to A.
# moving from B to C.
# moving from A to C.
#注:理解
#当 n = 0 时 什么都不打印
#当 n = 1 时 打印 from a to c 只打印中间那句话 从a --> c (理解时可以忽略中间b)
#当 n = 2 时 先把1个盘子从a-->b ,再把1个盘子从 a-->c, 再把 从b -->c
#当 n = 3 是 前面3步是2个盘子的问题 后面3步也是2个盘子的问题
#-----------------------------------------------------------------------
###假设n个盘子 移动需要 h(n)次
# 先需要h(n-1)步 + 1步 +h(n-1)步 h大概是2的n次方
# 汉诺塔移动次数的递推式:h(x)=2h(x-1)+1
# h(64)=18446744073709551615
# 假设婆罗⻔每秒钟搬一个盘子,则总共需要5800亿年
汉诺塔问题是一个递归的举例,方便理解递归
#07 顺序查找
###列表查找
# 什么是列表查找
# 顺序查找
# 二分查找
#查找
# 查找:在一些数据元素中,通过一定的方法找出与给定关键字相同的数据元素的过程。
# 列表查找(线性表查找):从列表中查找指定元素 输入:列表、待查找元素
# 输出:元素下标(未找到元素时一般返回None或-1) # 注:因为下标一般从0 开始 所以返回-1表示没有查到 或返回None
#
# 内置列表查找函数:index()
#注:列表查找是1个算法
#---------------------------------------------------------
### 顺序查找 (Linear Search)
# 顺序查找:也叫线性查找,从列表第一个元素开始,顺序进行搜索,直到找到元素或搜索到列表最后一个元素为止。
#注:就是从头走到尾 中间找到停掉 返回下标 或者走到最后一个 找不到了 返回-1或者None
# 时间复杂度:O(n)
# 示例:线性搜索/顺序查找
def Linear_search(li, val): # 注:li列表 ;val待查找的元素
for ind, v in enumerate(li): # 注:因为要返回个下标 所以用 enumerate index和值都需要
if v == val: # 注:如果v == 我们要找的那个值 那就返回 它的index
return ind
else: # 如果for循环结束后还没有找到 返回None
return None
# 注:时间复杂度是 O(n)
#n 就是列表的长度
#第2步:没有循环减半的过程
#第3步:有1个跟n 相关的循环 所以时间复杂度是O(n)
'''
补:
以下是 enumerate() 方法的语法:
enumerate(sequence, [start=0])
以下展示了使用 enumerate() 方法的实例:
>>> seasons = ['Spring', 'Summer', 'Fall', 'Winter']
>>> list(enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]
>>> list(enumerate(seasons, start=1)) # 小标从 1 开始
[(1, 'Spring'), (2, 'Summer'), (3, 'Fall'), (4, 'Winter')]
'''
#顺序查找是把列表从头到尾走了一遍 所以最多走n步 复杂度0(n)
def linear_search(data_set, value):
for i in range(range(data_set)):
if data_set[i] == value:
return i
return # 理解为 默认返回None
首先理解什么是查找,
输入:列表、待查找元素
输出:元素下标 (index())
1、线性搜索/顺序查找 O(n)
就是从到到尾的匹配找到就输出,没找到就下一个
2、二分查找 O(logn)
列表是有序的,然后每次折半找,比中间值呗,超好理解
#08 二分查找介绍
# 二分查找:又叫折半查找,从有序列表的初始候选区li[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。
#注:比如公司要选模特 身高必须175
#注:从头找 是顺序查找
#注:按大小个站好,量中间那个人的身高,假如是165,说明175的人在165后面
# 可以让左边所以人走了,量过的那个人 也可以走了 还剩大概50个人 再找50个人中间的
# 他是178 , 所以178后面那些人又不要了,
# 还剩165 - 178之间的那些人,依次找中间的,每一次让整个人数缩小一半
# 剩下1个人如果都没有 说明整个都没有
#留在这里的人叫候选区(值可能出现在这里)
#-------------------------------------------------------------------------------
### 从列表中查找元素3:
# 1 2 3 4 5 6 7 8 9
# left mid right
#注:传入的是 列表和要找的元素3
#注:维护候选区 用2个变量 列表不要了 不能把不要的值扔掉
#注:初始时 left = 0 (指向第1个元素) right = n-1 (指向最后1个元素)
#注:求中间的元素 (left + right) / 2 = mid 地板除
#判断中间元素5 和3 进行 比较 发现比3 大 候选区在左边
#注:如何维护候选区?: 让right到4这边来 (mid=5的左边1个)
#即 right = mid - 1 循环了 再计算新的 mid = (0 + 3) / 2 = 1 (下标计算) 结果 地板除
#发现 mid = 2 比 3 小 说明在mid 的右边
#所以 left = mid + 1 移动left 到 mid 右边
#注:left = 2 ;right = 3 (下标) (2 + 3) / 2 = 2 所以 mid 还是只想3 索引是2
#注:mid 索引是2 mid 和我们要查的元素一样了 那就终止算法 输出mid的值 3 (输出下标)
#注:可以理解为 2 和 6 中间的数 是 4 所以(2 + 6) / 2 = 4
#如果最后1步 mid的值不为3 为 4 , 比想要的值3 大 那么 right = mid -1 候选区就没有值了
#left小于right 的时候 候选区有值
#left = rigth 的时候 也有值 就1个值
#left 如果大于 right 说明候选区没有值 , 就结束算法 说明找不到了
#09 二分查找代码
def binary_search(li, val): # li 列表 val待查找的值
left = 0
right = len(li) - 1
while left <= right: # 注:循环条件 说明候选区有值
mid = (left + right) // 2 # 注:中间值(下标) 是整除2
if li[mid] == val: # 注:== 找到的情况 每次对比都是对比的li[mid]
return mid # 注:返回下标 mid
elif li[mid] > val: # 注:代表 待查找的值val在 mid左边
right = mid - 1 # 注:更新候选区了 这种情况就可以继续循环
else: # 注:第3种情况 li[mid] < val 待查找的值在mid右侧
left = mid + 1 # 注:更新候选区 继续循环
else:
return None # 注:如果找不到的情况 即不满足 left <= right 时
li = [1,2,3,4,5,6,7,8,9]
print(binary_search(li,3)) # 注:调用二分查找 查3
#结果为 2
def bin_search(data_set, value):
low = 0
high = len(data_set) - 1
while low <= high:
mid = (low+high)//2
if data_set[mid] == value:
return mid
elif data_set[mid] > value:
high = mid - 1
else:
low = mid + 1
#注:算法执行过程中 通过 left 和 right 维护候选区
#注:不管什么情况 都会是整个候选区缩小一半 最后缩小到1个或者2个
#所以 时间复杂度:O(logn) logn 比n 小
#所以二分查找比线性查找 效率高
列表有序、折半找,这里写代码思路:
定义左边数,右边数,中间数为这两个数和 、、2然后和val比
先比是不是,再比大了咋办,小了咋办
#10 二分查找与线性查找的比较
#写的 cal_time的模块 里面有个函数 这个函数是装饰器
import time
def cal_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print("%s running time: %s secs." % (func.__name__, t2 - t1))
return result
return wrapper
#-----------------------------------------------------
上面是一个记时装饰器
'''
补:args和kwargs组合起来可以传入任意的参数
args 是 arguments 的缩写,表示位置参数;kwargs 是 keyword arguments 的缩写,表示关键字参数
arguments 英[ˈɑːgjʊmənts]
美[ˈɑrgjəmənts]
n. 争论; 辩论; 论据; 论点; 参数;
简单理解:*arg,**kwargs 单独用分别是可变参数,可变关键字参数,合在一起,代表任何参数
'''
from cal_time import * # 注:导入写的模块
#注:对这2个函数加了2个装饰器
@cal_time
def linear_search(li, val):
for ind, v in enumerate(li):
if v == val:
return ind
else:
return None
@cal_time
def binary_search(li, val):
left = 0
right = len(li) - 1
while left <= right:
mid = (left + right) // 2
if li[mid] == val:
return mid
elif li[mid] > val:
right = mid - 1
else:
left = mid + 1
else:
return None
li = list(range(10000000)) # 注:大一点的数组
linear_search(li, 38900000)
binary_search(li, 38900000)
#结果为
# 2
# linear_search running time: 0.601691722869873 secs.
# binary_search running time: 0.0 secs.
#二分查找肯定非常快 2的32次方长度的列表 最多只要查32、33次 但线性查找要查42亿次
#列表内置函数 index() 就是线性查找
#因为二分查找有个要求:列表必须是有序的列表,但是 列表不一定是有序的,它只能用线性查找
#二分查找必须先排序 排序的时间复杂度大于O(n)
#有序的用二分查找,无序 如果以后要一直查 先排序 以后直接查 (注意排序的时间复杂度)