一、二分查找
其要求操作的数据集必须是一个有序列表
过程:
每次都取中间值,比大小,再取中间
时间复杂度:
对一个长为 N 的列表查找:
- 对于二分查找,最坏的情况为要查找的结果为紧挨查找开始时两端中的任意一段,这是时间复杂度为 \log_2 N
- 对于顺序查找,最坏的情况为要查找的结果在列表末尾,其时间复杂度为 N
关于时间复杂度的一个知识点
通常在大O表示法中忽略常数,但是在进行时间复杂度的比较时,如果非常数部分相等,这时就需要比较常数部分。
代码实现
def binary_search(list,item):
low = 0
height = len(list) - 1
while low<=height:
mid = (low + height) / 2
guess = list[mid]
if guess == item:
return mid
elif guess < item:
low = mid+1
else:
height = mid-1
return None
二、数组与链表
插入(或删除)时间复杂度
- 数组的插入时间复杂度 O(n)
- 链表的插入时间复杂度 O(1)
读取时间复杂度
- 数组的读取时间复杂度O(1)
- 链表的读取时间复杂度O(n)
三、选择排序
过程:
先遍历 n 个找到应该放在第一位的,然后遍历剩下的 n-1 个找到放在第二位的,依次找到所有。共需要查找(n-1)+(n-2)…+2+1
时间复杂度
最差的情况为 (n-1)+(n-2)…+2+1 ,平均每次为 n/2 共 n 次,所以O(n/2 * n),时间复杂度中会省略常数,所以为O(n^2)
代码实现
# 先定义一个找出数组中最小元素索引的函数
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 = list()
for i in range(len(arr))
smallest_index = findSmallest(arr)
newArr.append(arr.pop(smallest_index))
return newArr
递归
*递归只是让解决方案更加清晰,并没有性能上的优势。*在编写递归函数时,注意应当告诉它如何结束
**注意:**使用递归可能会占用大量的内存,因为要在内存中存放大量的函数调用信息,如果发生了这样的情况有两种方法解决:
- 重新编写函数,使用循环。
- 使用尾递归(一个高级递归主题)
快速排序
过程:
1、选择一个基准值
2、讲数组分为小于基准值和大于基准值的两个数组
3、对这两个数组在进行快速排序
代码实现
def quicksort(arr):
if len(arr) < 2:
return arr
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) + [pviot] + quicksort(greater)
时间复杂度:
- 平均时间复杂度:O(n\logn)
- 最佳时间复杂度:O(\logn)
- 最糟时间复杂度:O(n^2)
散列表
通过散列函数生成一个索引值,存储在列表中,当查询数据是,将相同的值输入,可以直接得到其在列表中的索引值,时间复杂度为O(1)。在 python 中的表现形式为字典 dict()。
小解散列函数
散列函数必须满足的几点定义:
- 相同输入得到的输出值必须是一致的
- 不同输入必须得到不同的值
- 知道列表的长度,不会输出无效的索引值
散列函数的冲突:
当输入不同的值是得到的相同的输出,这是想列表中插入数据,发现该索引上已经有值,这种情况被称为冲突。这时,通常将该索引指向一个链表。用链表来存储输出相同的结果。一个好的散列函数,绝对是冲突最优的。
避免散列函数冲突:
- 较低的装填因子
装填因子为:散列表包含的元素数量 / 位置总数。装填因子越大即效率越低。经验表明:当装填因子值大于 0.7 时,最好调整散列表长度。在调整散列表长度时,先扩大散列表长度,然后对其中已有的重新使用散列函数(hash)确定在新散列表中的位置。这个操作十分费时间。但是就平均来说,算上散列表的长度调整,它的时间复杂度仍然为O(1) - 良好的散列函数
可以了解一下 SHA 函数。
几条总结:
- 可以结合散列函数和数组来创建散列表
- 应当使用可以最大限度的减少冲突的散列函数
- 散列表的查找、插入和删除数据都非常快
- 散列表很适合模拟映射关系
- 当散列因子超过 0.7 时,应当调整散列表长度。
- 散列表可用于缓存数据
- 散列表非常适合用于防止重复
广度优先搜索
广度优先搜索解决了**最短路径问题。**主要解决:
- 从 A 出发,有到 B 的的路径吗?
- 从 A 出发,到 B 点哪条路径最短?
Python 中图的表示
使用 dict 来表示图。dict的每个key为所有图的结点,value 为一个保存了周边结点的 list。
# 例如表示我的朋友圈
graph = dict()
graph['王'] = ['张一','王一','李一']
graph['张一'] = ['张二','王二','李二']
graph['王一'] = ['张三','王三','李三']
graph['李一'] = ['张四','王四','李四']
graph['张二'] = ['张五','王五','李五']
......
广度优先搜索
举例解决问题:在我的关系网中是否可以找到李四?
其搜索的顺序结点依靠一个队列。当开始时,将我的朋友依次加入队列中,然后从队列中取出一个值,判断其是否为李四,则 return,如果不是则将他的朋友加入队列中,再从队列中取出来重复操作。如果队列为空了还未找到,则在我的关系网中找不到李四。
注意:可能会发生一个节点多次入队的情况,如果多次处理可能会形成循环。要避免这种情况,需要在处理这个结点时先判断该结点是否已经处理过,若处理过直接跳过。
代码实现:
# 定义图
graph = dict()
......
使用广度优先算法查找
def search(myName,search_name):
search_deque = deque() # 这是一个双端队列(实质为list)
search_deque += graph[myName]
searched = []
while search_deque:
person = search_deque.popleft()
if person not in searched:
if person == search_name:
return True
else:
search_deque += graph[person]
searched.append(person)
return False
广度优先搜索的运行时间:O(节点数 + 边数),通常记做:O(V+E)
狄克斯特拉算法
广度优先搜索可以确定中图中是否有某结点,要确定到达该结点的最短路径这是狄克斯特拉算法就派上了用场。但是需要注意狄克斯特拉算法只适用于有向、无环、无负权的图。(如果图中有负权,则无法保证在处理某一个节点时它以及获得了最短路径。)
狄克斯特拉算法包含的四个步骤:
- 找出最便宜的节点,即可在最短时间内前往的节点。
- 对于该节点的邻居(指向的节点),检查从该节点经过到达的路径是否更短,如果更短就更新。
- 重复这个过程,直到对图中的所有节点都这样做。
- 计算最终路径。
代码实现:
狄克斯特拉算法的实现需要借助三个散列表和一个数组来实现:
- 用来存储图的散列表 graph。因为图中不仅有每个节点而且也有每条边的权值,所以 graph 需要用一个嵌套的双层散列表来表示:
graph = {
"节点1":{
"节点3":权值, # (指向节点)
"节点4":权值,
....
},
"节点2":{
"节点5":权值,
"节点6":权值,
....
},
}
- 一个存储父节点的散列表。
parents = {
'节点1':'节点1的父节点',
'节点':'节点2的父节点',
......
}
- 一个用来存储节点开销的散列表
将已知当前最小开销的节点存在,对于未知的,可以初始化为一个无穷大的值。
infinity = float('inf')
costs = dict()
costs['a'] = 5
costs['4'] = 7
costs['fin'] = infinity
- 一个用来存储已处理过的节点的数组
processed = []
狄克斯特拉算法实现
# 假设基础信息已经有上述实现
# 从未处理的节点中取出一个开销最小的节点
def find_lowest_cost_node(costs):
lowest_cost = float('inf')
lowest_cost_node = Node
for node in costs:
cost = costs[node]
if cost < lowest_cost and node not in processed:
lowest_cost = cost
lowest_cost_node = node
return lowest_cost_node
def dijkstra():
node = find_lowest_cost_node(costs)
while node:
cost = costs[node] # 获取到当前节点的权值
neighbors = graph[node] # 获取到当前节点指向的所有节点
for n in neighbors.keys():
new_cost = cost + neighbors[n]
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
划重点:
- 广度优先搜索用于在非加权图中查找最短路径
- 狄克斯特拉算法用于在加权图中查找最短路径
- 狄克斯特拉算法仅适用于权重为正数的图
- 如果图中包含负权值,请适用贝尔曼-福德算法
贪婪算法
贪婪算法的优点:简单易行
贪婪算法很多情况下都无法获得最优解
判断 NP 问题的一些蛛丝马迹:
(没有办法明确判断问题是不是 NP 完全问题)
- 元素较少时算法的运行速度非常快,随着元素的增加,速度回变的非常慢
- 涉及“所有组合”的问题通常都是 NP 完全问题
- 不能将问题分解成小问题,必须考虑各种可能的情况。这可能是 NP 完全问题
- 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是 NP 完全问题。
- 如果问题设计集合(如广播台集合)且难以解决,他可能是 NP 问题
- 如果问题可转换为集合覆盖问题或旅行商问题,那他肯定是 NP 完全问题
几点记录:
- 贪婪算法寻求局部最优解,企图以这种方式获得全局最优解
- 对于 NP 完全问题,还没有找到快速的解决方案
- 面临 NP 完全问题,最佳的做法就是使用近似算法
- 贪婪算法易于实现、运行速度快,是不错的近似算法
动态规划
先解决子问题,再逐步解决大问题。当且每个子问题都是离散的,即不依赖其他子问题时,动态规划才管用。
每一个动态规划问题都从表格开始。然后依次行填充(不可列填充)。
在确定每一个单元格的时候,都应当比较其上一个单元格( cell[i-1][j]) 和 当前行商品价值+ 容下当前商品后剩余空间可容纳的最大价值
最长公共子串问题
比较两个字符串的最长公共子串与最长公共子序列问题
最长公共子串:
绘制表格,两个字符串的字符分别为每个表格的横纵坐标,然后对表格进行填充,如果横纵坐标上的两个字符不同,该单元格为 0,如果横纵坐标上的两个字符串相同,则该单元格的值为其左上方单元格的值加一。
最长公共子序列:
最长公共子序列为判断两个字符串有多少个相同字符的问题。
绘制表格,两个字符串的字符分别为每个表格的横纵坐标,然后对表格进行填充。如果单元格横纵坐标上的两个字符相同,则为其左上方单元格加 1,如果不同,则取其上方和左方两个单元格中较大的值。
划重点
- 需要在给定约束条件下优化某种指标时,动态规划很有用
- 问题可以分解为离散子问题时,可以使用动态规划来解决
- 每种动态规划解决方案都依赖于网格
- 单元格中的值通常就是要优化的值
- 每个子单元格都是一个子问题,因此需要考虑如何将问题分解为子问题
- 没有放之四海皆准的计算动态规划解决方案的公式
K 最近邻算法(KNN)
特征抽取:
将元素的每一项特征数字化(应当加上归一化处理)后存储在一个多维的坐标空间中,每个元素在空间中的距离则为其双方的相似度(距离越近的越相似)。在空间邻近值的计算公式(无论是几维空间计算方法都等同二维空间):
sqrt((x2-x1)2+(y2-y1)2+(z2-z1)^2+…)
归一化的必要性:
加入两个用户都比较喜欢同一个类型的电影,但是 A 用户的评分标注较低给出的高分较多,而另一位则给的分数普遍全部偏低,这样就会导致在空间中这两个用户元素距离较远。所以需要对其数据做归一化处理。即对每一位用户使用评价评分,例如求分数在总评分中所占比率?
对位置情况的预测:回归
例如 A 用户将要对某一部影片评分,则可以取该用户的 K 个邻近用户对该影片的评分的平均值座位预测值。
又例如某零售店某产品的明日销量预测,可以可能对结果产生影响的天气、是否工作日、等因素数据花做回归操作来预测。
做 K 邻近时 K 的最佳取值为 sqrt(N),N 为数据总数。
小结:
- KNN 用户分类和回归,需要考虑最近的邻居
- 分类就是编组
- 回归就是预测结果
- 特征抽象意味着将物品装换为一系列可比较的数字
- 能否挑选合适的特征,事关 KNN 算法的成败
展望未来
二叉树
- 在二叉树中查找节点时,平均运行时间为 O(\logn),最糟运行时间为 O(n),
- 数据库中运用的高级数据结构:B树、红黑树、堆、伸展树
傅里叶变换
在音/视频处理、图片处理、甚至是地震预测、DNA 分析中都有很广阔的应用。
并行算法
并行算法调用了多个计算机核心,但其速度增长并不是线性的。因为还要做并行性管理、负载均衡方面的计算。
MapReduce
MapReduce 为一种流行的分布式算法。可以通过 Apache Hadoop 来使用。其基于映射函数和归并函数两个简答理念。
布隆过滤器 和 HyperLogLog
布隆过滤器是一种概率型的数据结构,它提供的答案可能不对。
HyperLogLog是一种类似于布隆过滤器的算法,提供的答案同样可能是有误的。
SHA 算法
SHA 算法是不可逆的、局部不敏感的,多用于密码加密中,目前一直的SHA-0、SHA-1已被发现存在缺陷,可以使用SHA-2、SHA-3.
局部敏感的 Simhash
例如在论文查重时可能就是使用该算法,其主要用于检查两项内容的相似程度。
Diff-Hellman 秘钥交换
Diff-Hellman 目前已经被 RSA 代替。其原本也是使用公钥-私钥这样的秘钥对来进行加密。
线性规划
线性规划是一个很宽泛的框架,图问题只是其中的一个子集。线性规划使用了 Simplex 算法。