必须掌握的计算机基础算法

一、二分查找

其要求操作的数据集必须是一个有序列表

过程:

每次都取中间值,比大小,再取中间

时间复杂度:

对一个长为 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 算法。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值