文章目录
二分查找
前提:有序元素列表
对于包含n个元素的列表,用二分查找最多需要
log
2
n
{\log _2}n
log2n步,
O
(
log
n
)
O(\log n)
O(logn)
简单查找最多需要
n
n
n步.
O
(
n
)
O(n)
O(n)
def binary_search(list,item):
low=0
high=len(list)-1
while(low<=high):
mid=(low+high)//2# //后是整数
guess=list[mid]
if guess==item:
return mid
if guess < item:
low=mid+1
else:
high=mid-1
return None
my_lism=[1,3,5,7,9]
print(binary_search(my_lism,3))
print(binary_search(my_lism,-1))
第一次:low=0,high=4,mid=(0+4)//2=2,5>3,因此low=0,high=1
第二次:low=0,high=1,mid=(0+1)//2=0,1<3,因此low=1,high=1,还得继续循环,所以条件设置为while(low<=high):
线性时间:猜测的次数与列表长度相同
对数时间:二分查找地运行时间
大O表示法
指出算法速度快慢程度,并非时间,是算法运行时间的增速。是最糟糕情况下的运行时间。
常见的大O运行时间:
O
(
log
n
)
O(\log n)
O(logn):对数时间,如二分查找
O
(
n
)
O( n)
O(n):线性时间,如简单查找
O
(
n
∗
log
n
)
O(n * \log n)
O(n∗logn):如合并排序。快速排序的平均情况,一种较快的排序算法。
O
(
n
2
)
O({n^2})
O(n2):如选择排序,一种比较慢的排序算法。快速排序的最糟情况。
O
(
n
!
)
O({n!})
O(n!):如旅行商问题的解决方案
数组&链表
元素的位置称为索引
常见的数组和链表操作的运行时间
数组 | 链表 | |
---|---|---|
读取 | O ( 1 ) O(1) O(1) | O ( n ) O(n) O(n) |
插入 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
删除 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
O
(
n
)
O(n)
O(n):线性时间
O
(
1
)
O(1)
O(1):常量时间
插入和删除:链表都是最好的选择。删除操作总能成功,因为插入可能会碰到内存空间不够的情况。
数组用的比较多,因为数组能随机访问,而链表是顺序访问。
选择排序
需要的总时间:
O
(
n
2
)
O({n^2})
O(n2)
需求:将数组元素按从小到大的顺序排列。
注意:先编写一个用于找出数组中最小元素的函数。
def findsmallest(arr):
"""找到数组中最小的数"""
smallest=arr[0]
smallest_index=0
for i in range(1,len(arr)):
if arr[i]<smallest:
smallest=arr[i]
smallest_index=i
return smallest_index
def selectionsort(arr):
"""选择排序,将数组按从小到大排序"""
newArr=[]
for i in range(len(arr)):
smallest_index=findsmallest(arr)
newArr.append(arr.pop(smallest_index))#注意这行
return newArr
print(selectionsort([1,4,5,8,6,10]))
递归
递归只是让解决方案更加清晰,并没有性能上的优势。
如果使用循环,程序的性能可能更高 ;如果使用递归,程序可能更容易理解。
每个递归函数都有两部分组成,分别为基线条件(base case)和递归条件(recursive case).
递归条件 :函数调用自己
基线条件:函数不再调用自己
需求:打印3,2,1
def count(i):
"""打印3,2,1"""
print(i)
if i<=1: #基线条件
return
else: #递归条件
count(i-1)
count(3)
栈
**压入:**插入,在最上面添加新的待办事项。
**弹出:**删除并读取最上面的待办事项。
后进先出(LIFO,Last In First Out)
调用栈:
在一个函数中调用另一个栈时,当前函数暂停并处于未完成状态。
**使用栈的代价:**如果栈很高,意味着计算机存储了大量函数调用的信息。
**解决方案:**1.重新编写代码 ,使用循环。2.使用尾递归,但并不是所有语言都支持尾递归。
递归调用栈
需求:x! (factorial)
def fac(x):
if x==1:
return 1
else:
return x*fac(x-1)
print(fac(3))
分而治之
分而治之(divide and conquer,D&C)——一种著名的递归式问题解决方法。
1.找出基线条件,这种条件必须尽可能简单。
2.不断将问题分解(或者缩小规模),直到符合基线条件。
需求1:计算出一个数组内所有数字的总和。
def sum(arr):
total=0
for i in arr: #数组也可以用for..in..。
total+=i
return total
print(sum([1,2,3,4,5]))
用分而治之改进:
def sum(list):
if list==[]:
return 0
else:
return list[0]+sum(list[1:])
print(sum([1,2,3,4,5]))
需求2:编写一个递归函数来计算列表包含的元素数
def count(list):
if list==[]:
return 0
else:
return 1+count(list[1:])
print(count([1,2,3,4,5]))
需求3:找出列表中最大的数字
def max(list):
if len(list)==2:
if list[0]>list[1]:
return list[0]
else:
return list[1]
else:
max_value=max(list[1:])
if list[0]>max_value:
return list[0]
else:
return max_value
print(max([1,4,3,6,8,7]))
缩写版:
def max(list):
if len(list)==2:
return list[0] if list[0]>list[1] else list[1]
else:
max_value=max(list[1:])
return list[0] if list[0]>max_value else max_value
print(max([1,4,3,6,8,7]))
快速排序
def quicksort(arr):
if len(arr)<2:
return arr
else:
pivot=arr[0]
less=[ i for i in arr[1:] if i<=pivot]
greater=[i for i in arr[1:] if i>pivot]
return quicksort(less)+[pivot]+quicksort(greater)
print(quicksort([11,4,2,55,6]))
平均情况
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),最糟情况
O
(
n
2
)
O({n^2})
O(n2).
快速排序速度取决于选择的基准值。
平均情况下,栈长(调用栈的高度)为
O
(
l
o
g
n
)
O(logn)
O(logn),最糟情况下,栈长为
O
(
n
)
O(n)
O(n)。
最佳情况也是平均情况。
快速排序的平均情况和合并排序的运行时间都是 O ( n l o g n ) O(nlogn) O(nlogn),但是快速排序的常量比合并排序小,因此如果它们运行时间都为 O ( n l o g n ) O(nlogn) O(nlogn)时,快速排序的速度将更快。
练习:根据数组包含的元素创建一个乘法表,即如果数组为[2, 3, 7, 8, 10],首先将每个元素 都乘以2,再将每个元素都乘以3,然后将每个元素都乘以7,以此类推。需要多长时间? O ( n 2 ) O({n^2}) O(n2).
散列表
最有用的基本数据结构之一。
散列函数将不同的输入映射到不同的数字。
散列表适用于:
1.模拟映射关系;
2.防止重复;
3.缓存/记住数据,以免服务器再通过处理来生成它们。
python提供的散列表实现为字典,可使用函数dict()
来创建散列表。也可以直接用{}
来创建哈希表。
需求1:创建电话簿。
phone_book=dict()#phone_book={}
phone_book["wzt"]=45323
phone_book["lcy"]=124644
phone_book["zzp"]=44554
print(phone_book["lcy"])
需求2:当某人投过票,踢出去;当某人没投过票,让他投。
voted={}
def checked_voter(name):
if voted.get(name):
print("kick them out")
else:
voted[name]=True
print("let them vote")
checked_voter("Tom")
checked_voter("Mike")
checked_voter("Mike")
缓存
是一种常用的加速方式。所有大型网站都使用缓存,而缓存的数据则存储在散列表中。
冲突
collision:给两个键分配的位置相同。
最简单的办法:如果两个键映射到了同一个位置,就在这个位置存储一个链表。
注意:
1.散列函数很重要。最理想的情况是散列函数将键均匀地映射到散列表的不同位置。
2.如果散列函数存储的链表很长,散列表的速度将急速下降。
在平均情况下,散列表执行各种操作的时间都为
O
(
1
)
O(1)
O(1),即为常量时间。
在最糟情况下,散列表所有操作的运行时间都为
O
(
n
)
O(n)
O(n)。
为避免冲突,需要有:
1.较低的填装因子。
2.良好的散列函数。
填装因子
散列表的填装因子=散列表包含的元素数/位置总数
一旦填装因子超过0.7,就该调整散列表的长度。
**调整长度:**一旦填装因子开始增大,就需要在散列表中添加位置。
平均而言,即便考虑到调整长度所需要的时间,散列表操作所需的时间也为
O
(
1
)
O(1)
O(1)。
良好的散列函数
例如SHA
广度优先搜索(breadth-first search,BFS)
解决最短路径问题(shortest-path problem)的算法。
步骤:
1.使用图来建立问题模型。
2.使用广度优先搜索解决模型。
解决问题:(两类)
1.从节点A出发,有前往节点B的路径么?
2.从节点A出发,前往节点B的哪条路径最短?
在广度优先搜索的执行过程中,搜索范围从起点开始向外延伸,即先检查一度关系,再检查二度关系。
图
图由节点(node)和边(edge)组成。
一个节点可能与众多节点直接相连,这些节点被称为邻居 。
有向图(directed graph)
无向图 (undirected graph)
图的实现
散列表。散列表是无序的,因此添加键-值对的顺序无关紧要。
graph={}
graph["you"]=["ed","wew",]
队列
按添加顺序进行检查。
只支持两种操作:
入队:将一个元素加入队列
出队:从队列中取出一个元素
先进先出(FIFO,First In First Out)
广度优先搜索的实现
在python中,可使用deque
来创建一个双端队列。
from collections import deque
需求:查看你的关系网中是否有芒果代理商
from collections import deque
"""建立图"""
graph={}
graph["you"]=["alice","bob","claire"]
graph["alice"]=["peggy"]
graph["bob"]=["claire","peggy"]
graph["claire"]=["thom"]
graph["anuj"]=[]
graph["thom"]=[]
graph["peggy"]=[]
def search(name):
"""搜索算法"""
# 创建队列
search_queue =deque()
search_queue += graph[name]
searched=[]
# 该队列中是否有卖家
while search_queue:
person = search_queue.popleft()
if person not in searched:
if person_is_seller(person):
print(person.title()+" is the seller")
return True
else:
search_queue += graph[person]
searched.append(person)
return False
def person_is_seller(person):
"""该人是否为卖家"""
return person[-1] == 'm'
search("you")
运行时间
O
(
顶
点
+
边
数
)
=
O
(
V
+
E
)
O(顶点+边数)=O(V+E)
O(顶点+边数)=O(V+E)
V=vertice,E=eage
拓扑排序
树
狄克斯特拉算法(Dijkstra’s algorithm)
找出加权图中前往X的最短路径。
步骤:
1.找出“最便宜”的节点,即可在最短时间内到达的节点。
2.对于该节点的邻居,检查是否有前往它们的更短路径,如果有,就更新其开销。
3.重复这个过程,直到对图中的每个节点都这样做了。
4.计算最终路径。
加权图:带权重的图,可使用广度优先搜索算法
非加权图:不带权重的图,可使用狄克斯特拉算法
无向图中,每条边都是一个环。
狄克斯特拉算法只适用于有向无环图(directed acyclic graph)
注意:如果有负权边,就不能使用迪克斯拉特算法。
因为狄克斯特拉假设:对于处理过的海报节点,没有前往该节点的更短路径。
对于包含负权边的图,可以用贝尔曼-福德算法(Bellman-Ford algorithm)
算法的实现
有三部分,分别为graph,costs,parents
# 图的创建
graph={}
graph["start"]={}
graph["start"]["a"]=6
graph["start"]["b"]=2
graph["a"]={}
graph["a"]["fin"]=1
graph["b"]={}
graph["b"]["a"]=3
graph["b"]["fin"]=5
graph["fin"]={}
# cost的创建,每个节点现有的cost
infinity=float("inf")# 无穷大
costs={}
costs["a"]=6
costs["b"]=2
costs["fin"]=infinity
# parents的创建,每个节点现在的parent
parents={}
parents["a"]="start"
parents["b"]="start"
parents["fin"]=None
processed=[]# 处理过的节点
def find_lowest_cost_node(costs):
"""寻找未处理过的最小cost的节点"""
lowest_cost_node = None
lowest_cost = float("inf")
# if lowest_cost_node not in processed:
for node in costs.keys():
if (costs[node] < lowest_cost)and(node not in processed):
lowest_cost = costs[node]
lowest_cost_node = node
return lowest_cost_node
node=find_lowest_cost_node(costs)
while node is not None:
cost=costs[node]
for n in graph[node].keys():
new_cost=graph[node][n]+cost
if new_cost<costs[n]:
costs[n]=new_cost
parents[n]=node
processed.append(node)
node=find_lowest_cost_node(costs)
print(costs["fin"])#最终开销
print(costs)
print(parents)
贪婪算法
每步都采取最优的做法。即每步都选择局部最优解,最终获得的就是全局最优解。
背包问题
集合覆盖问题
列出每个可能的广播台集合,这成为幂集(power set)。 O ( 2 n ) O({2^n}) O(2n)
用近似算法。
判断近似算法优劣的标准如下:
1.速度有多快。
2.得到的近似解与最优解的接近程度。
在这个例子中,贪婪算法的运行时间为 O ( n 2 ) O({n^2}) O(n2)
集合:类似于列表,不能包含重复的元素。set(arr)
fruits=set(["avocado","tomato","banana"])
vegetable=set(["beets","carrots","tomato"])
print(fruits | vegetable) #并集
print(fruits & vegetable) #交集
print(fruits - vegetable) #差集
广度优先搜索和狄克斯特拉算法属于贪婪算法,因为它们每一步也是采取最优的做法。
实现
# 需要被覆盖的州
states_needed=set(["mt","wa","or","id","nv","ut","ca","az"])
# 广播台及其覆盖的州
stations={}
stations["kone"]=set(["id","nv","ut"])
stations["ktwo"]=set(["wa","id","mt"])
stations["kthree"]=set(["or","nv","ca"])
stations["kfour"]=set(["nv","ut"])
stations["kfive"]=set(["ca","az"])
#最终的最佳广播台们
final_stations=[]
while states_needed:
# 每次找到的最佳覆盖的那个广播台
best_station = None
# 已覆盖的州
states_covered = set()
for station, states in stations.items(): # 遍历每个台和台覆盖的州,该for循环是为了找出每次剩下覆盖的最佳广播台
covered = states_needed & states # 需要被覆盖的州和每个台覆盖的州重合的部分
if len(covered) > len(states_covered): # 如果这个部分大于已覆盖的部分,找出那个最佳广播台
best_station = station
states_covered=covered
states_needed -= states_covered#x需要被覆盖的州减去已经覆盖的州,然后在剩下需要被覆盖的州中找到最佳广播台
final_stations.append(best_station)
print(final_stations)
NP完全问题
旅行商问题
O
(
n
!
)
O(n!)
O(n!)
定义:以难解著称的问题。
如旅行商问题和覆盖集合问题。
用近似算法即可。
如何判定是否是NP完全问题
1.元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变的非茶慢。
2.涉及所有组合问题通常是NP完全问题
3.不能将问题分成小问题,必须考虑各种情况,这可能是NP问题
4.如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能是NP问题
5.如果问题涉及集合(如广播电台集合)且难以解决,它可能是NP完全问题
6.如果问题可转化为集合覆盖问题或商旅问题,那它肯定是NP问题
动态规划
上述的为近似问题,但可能不是最优解。最优解可用动态规划。
动态规划先解决子问题,再逐步解决大问题。
根据动态规划算法的设计,最多只需合并两个子背包。不过子背包中可能又包含子背包。
需要在给定约束条件下优化某种指标时,可使用动态规划来解决。
问题可分解为离散子问题时,可使用动态规划来解决。
背包问题
需要完整地偷商品,而且要根据商品需要考虑的粒度而调整网格。
答案总是在最后的单元格中
旅游行程最优化
需要每个子问题都是离散的,即不依赖于其他子问题时,动态规划才管用。
最长公共子串
注意:
1.每种动态规划解决方案都涉及网格。
2.单元格中的值通常就是你要优化的值。
3.每个单元格都是一个子问题,因此应考虑如何将问题分成子问题,这有助于找出网格的坐标轴。
答案为网格中最大的数字
伪代码如下:
if word_a[i]==word_b[j]:#两个字母相同时
cell[i][j]=cell[i-1][[j-1]]+1
else:
cell[i][j] = 0
最长公共子序列
两个单词中都有的序列包含的字母数
伪代码如下:
if word_a[i]==word_b[j]:
cell[i][j]=cell[i-1][[j-1]]+1
else:
cell[i][j] = max(cell[i-1][j],cell[i][j-1])
动态规划的应用:
1.DNA链的相似性,最长公共序列/
2.git diff 两个文件的差异
3.编辑距离(levenshtein distance)
4.Microsoft Word的断字功能
K最近邻算法
k-nearest neighbours, KNN
可用于分类与回归
分类:编组
回归:预测结果(如一个数字)
推荐系统
特征抽取
回归
计算两位用户的距离时,使用的都是距离公式。
如毕达哥拉斯公式,更常用的是余弦相似度(cosine similarity).
余弦相似度不计算两个矢量的距离,而比较它们的角度。
挑选合适的特征
1.与要推荐的电影紧密相关的特征;
2.不偏不倚的特征(公平的)
机器学习简介
例子:创建推荐系统、OCR、创建垃圾邮件过滤器、预测股票市场
OCR
光学字符识别(optical character recognition)
计算机自动识别其中的文字
步骤:
1.浏览大量的数字图像,将这些数字的 特征提取出来。(线段、点、曲线)
2.遇到新图像时,你提取该图像 的特征,再找出最近的邻居是谁。(可用KNN)
创建垃圾邮件过滤器
用朴素贝叶斯分类器(Naive Bayes classifier)计算出收到的邮件主题的单词在训练的邮件分类中出现的概率来判断是否是垃圾邮件。
基础算法
树
将用户名插入到数组的正确位置而不需要在插入后再重新排序。
二叉查找树(binary search tree查找节点平均运行时间为
O
(
l
o
g
n
)
O(logn)
O(logn),最糟情况下的运行时间为
O
(
n
)
O(n)
O(n)
而有序数组最糟情况的运行时间为
O
(
l
o
g
n
)
O(logn)
O(logn)
有序数组 | 二叉查找树 | |
---|---|---|
查找 | O ( l o g n ) O(logn) O(logn) | O ( l o g n ) O(logn) O(logn) |
插入 | O ( n ) O(n) O(n) | O ( l o g n ) O(logn) O(logn) |
删除 | O ( n ) O(n) O(n) | O ( l o g n ) O(logn) O(logn) |
缺点:不能随机访问
特殊的树:B树、红黑树、堆、延展树。
反向索引
inverted index
一个散列表,将单词映射到包含它的页面。
常用于创建搜索引擎
傅里叶变换
是用于处理信号
并行算法
速度的提升并非线性的
原因:
1.并行性管理开销
2.负载均衡
MapReduce
是一种流行的分布式算法,非常适用于在短时间内内完成海量工作。
基于映射函数和归并函数
布隆过滤器和HyperLogLog
布隆过滤器
概率型数据结构,答案很可能正确。
1.可能出现错报的情况。
2.不可能出现漏报的情况。
优点:占用的存储空间很少。
HyperLogLog
类似于布隆过滤器。
近似地计算集合中不同的元素数。
SHA算法
安全散列算法(secure hash algorithm)
SHA是一个散列函数,生成一个散列值(一个较短的字符串)
应用:比较文件和检查密码。
特点:局部不敏感
当输入稍微不同时,输出的散列值完全不同。
局部敏感的散列函数
Simhash
当输入稍微不同时,输出的散列值只存在细微的差别。
Diff-Hellman密钥交换
对消息进行加密,只有收件人才能看得懂。
优点:
1.双方无需知道加密算法。
2.要破解算法很难。
公钥和私钥。
线性规划
在给定约束情况下最大限度地改善指定的指标。
线性规划是一个宽泛得多的框架,图问题只是其中一个子集。
使用Simplex算法