算法图解笔记

研一的寒假,在家好好地补一下算法,先看的算法图解,从2022/1/26到2022/2/9

算法简介

算法是一组完成任务的指令

二分查找

二分查找必须有序,思路为:找中间,若没找到则往靠近的一边找,终止条件为左大于右或者右小于左(这里我用的python3,与书上python2不同)

arr_list = [1,6,8,11,24,33,57,89,90,99]
def binary_search(arr,num):
    left = 0
    right = len(arr)-1
    index = -1
    while left<=right:
        mid = (left+right)//2 # 这里python2和python3语法稍微不同
        if arr[mid]==num:
            index = mid
            break
        elif arr[mid]>num:
            right = mid - 1
        else:
            left = mid + 1
    return index

result = binary_search(arr_list,8)
print(result) # 2

大O表示法

大O表示法指出最糟情况下的运行时间

常见大O运行时间:

  • O(log n):对数时间,如二分查找
  • O(n):线性时间,如简单查找
  • O(n * log n):如快速排序
  • O(n2):如选择排序
  • O(n!):如旅行商问题

研究算法运行时间,并不是从具体时间度量考虑,而是从增速的角度度量

选择排序

数组和链表

数组擅长读取,因为数组支持随机访问

链表擅长插入和删除,请注意插入和删除操作为O(1)是有条件的,是说的第一个元素和最后一个元素,因为通常都会记录头和尾

冒泡排序

def bubbleSort(arr):
    for i in range(len(arr)-1):
        for j in range(len(arr)-i-1):
            if arr[j]>arr[j+1]:
                temp = arr[j+1]
                arr[j+1] = arr[j]
                arr[j] = temp
    return arr

print(bubbleSort([9,7,4,8,1,3,2,6,0,5]))

选择排序

def selectSort(arr):
    for i in range(10):
        max_num = -1
        k = -1
        for j in range(10-i):
            if max_num <= arr[j]:
                max_num = arr[j]
                k = j
        temp = arr[9-i]
        arr[9-i] = arr[k]
        arr[k] = temp
    return arr

print(selectSort([9,7,4,8,1,3,2,6,0,5]))

递归

递归就是函数调用自己,递归有基线条件(跳出循环)和递归条件,栈是存储内存块的一种数据结构

函数调用都进入了调用栈

快速排序

分而治之(divide and conquer,D&C):著名的递归式问题解决方法

分治的思想:

  • 找出基线条件,这种条件必须尽可能简单
  • 不断将问题分解(或者说缩小规模),直到符合基线条件

分治举例1

比如一个1680*640大小的矩形,想要全部分成正方形大小的矩形,目的是矩形最大,如可以全部分成1*1大小的矩形,也可以分成2*2大小的矩形

那么首先可以变成640*640,那么现在问题就变成了从400*640来求这个最大矩形,那么进而变为240*400大小,再变为160*240大小,再变为80*160大小,那么这个矩形大小就变为80*80

问题分析

此问题中,基线条件:找出可容纳的最大矩形

问题分解:不断划分矩形去找最大矩形

分治举例2

对一个数组进行求和,如果用循环,那么遍历就可以,如果用分治,则思想如下:

将数组分为第一个数和剩下的数组,剩下的数组则可以继续进行划分为一个数和另外的数组,只需要把这些数相加即可

def sum(arr):
    total = 0
    if len(arr) != 0:
        temp = arr.pop(0)
        total = temp + sum(arr)
    return total

print(sum([1,2,3,4,5,6,7,8,9,10]))

快速排序

对一个数组进行快速排序,先找一个基准,然后分成3部分内容:小于基准的数组、基准、大于基准的数组

def quickSort(arr):
    if len(arr)<2:
        return arr
    else:
        pivot = arr[0]
        smaller = [i for i in arr[1:] if i<=pivot]
        bigger = [i for i in arr[1:] if i>pivot]
        return quickSort(smaller)+[pivot]+quickSort(bigger)
    
print(quickSort([9,7,4,8,1,3,2,6,0,5]))

快速排序中,最糟情况为O(n2),平均情况为O(n log n),合并排序时间复杂度为O(n log n),这样看好像只要选合并排序就好。但是平均情况下,快速排序要优于合并排序(复杂度相同情况下,常量决定)

快速排序是最快的排序算法之一,是D&C典范

散列表

散列函数:给出一个数据,返回一个数字

散列表:散列函数结合数组的数据结构

在python中,散列表为字典

字典举例

book = dict()
book["apple"] = 0.67
book["milk"] = 1.49
book["avacado"] = 1.49
print(book)

# {'apple': 0.67, 'milk': 1.49, 'avacado': 1.49}

缓存

缓存的数据一般用散列表进行存储

冲突

散列表是散列函数+数组的数据结构,如果两个数据经过散列函数得到的值是相同的,那么在数组中会冲突

如果冲突,在同一个位置处存储一个链表,但设计时最好不要让链表太长

填装因子

$填装因子=\frac{散列表包含的元素数}{位置总数}$

填装因子大于1意味着商品数量超过了数组的位置数,一旦填装因子开始增大,则需要在散列表中添加位置,称为调整长度,通常是将数组增长一倍

一半填装因子大于0.7,就需要调整散列表的长度

优缺点

查找、删除、插入速度都非常快

散列表适合模拟映射关系

散列表适合用于防止重复

广度优先搜索

解决最短路径问题的算法被称为广度优先搜索(BFS)

图由节点和边组成,图用于模拟不同的东西是如何相连的

广度优先搜索

BFS解决两类问题:

  • 第一类问题:从节点A出发,有前往节点B的路径吗?
  • 第二类问题:从节点A出发,前往节点B的哪条路径最短

查找最短路径

先在一度关系中搜索,如果一度关系中没有想要的结果,再在二度关系中搜索

广度优先搜索不仅查找A到B的路径,而且找到的是最短的路径

广度优先搜索中,需要满足按添加顺序进行检查,这时需要队列

队列

队列是一种先进先出(FIFO)的数据结构,栈是一种后进先出(LIFO)的数据结构

有向图&无向图

有向图有箭头,无向图无箭头

广度优先举例

# 创建图
graph = {}
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
# 创建队列
from collections import deque
search_queue = deque()
search_queue += graph["you"]
# 进行搜索
def person_is_seller(name):
    return name[-1] == "m"
while search_queue:
    person = search_queue.popleft()
    if person_is_seller(person):
        print(person+" is a mango seller!")
        break
    else:
        search_queue += graph[person]

这个例子中,使用的判断语句比较简单,并且如果是双向循环,则可能导致无限循环,故最好再添加一个列表来只进行一次判断

graph = {}
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
from collections import deque
def person_is_seller(name):
    return name[-1] == "m"
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+" is a mango seller!")
                break
            else:
                search_queue += graph[person]
search("you")
# thom is a mango seller

运行时间

广度优先搜索的时间为:O(V+E)(verticle+edge)

某任务必须在某任务之后,这被称为拓扑排序

树是图的子集,树都是图

狄克斯特拉算法

广度优先搜索解决最短路径问题(段数),狄克斯特拉算法解决最快路径问题(总权重)

算法过程

  • 找出最便宜的节点,即可在最短时间内前往的节点
  • 对该节点的邻居,检查是否有前往他们的更短路径,如果有,则更新其开销
  • 重复上述两步,直到对图里的每个节点都如此
  • 计算最终路径

带权重的为加权图,不带权重的图为非加权图

计算非加权图的最短路径,可使用BFS,加权图的最短路径,可使用狄克斯特拉算法

绕环的路径不可能是最短的路径

无向图意味着两个节点彼此指向对方,其实就是环

在无向图中,每条边都是一个环,狄克斯特拉算法只适用于有向无环图(DAG)

狄克斯特拉算法关键理念:找出图中最便宜的节点,并确保没有到该节点的更便宜的路径

负权边

如果有负权边,就不能使用狄克斯特拉算法,因为负权边会导致狄克斯特拉算法不管用

对于含有负权边的图,可使用贝尔曼-福德算法

# 整个的图
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"] = {}
# 整个的代价
infinity = float("inf")
costs = {}
costs["a"] = 6
costs["b"] = 2
costs["fin"] = infinity
# 父节点
parents = {}
parents["a"] = "start"
parents["b"] = "start"
parents["fin"] = None
# 处理过程
processed = []
def find_lowest_cost_node(costs):
    lowest_cost = float("inf")
    lowest_cost_node = None
    for node in costs:
        cost = costs[node]
        if cost < lowest_cost and node not in processed:
            lowest_cost_node = node
    return lowest_cost_node

node = find_lowest_cost_node(costs)
while node is not None:
    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)
print(costs)
# {'a': 5, 'b': 2, 'fin': 6}
print(parents)
# {'a': 'b', 'b': 'start', 'fin': 'a'}

贪婪算法

贪婪算法:每一步都选择局部最优解,最终得到的就是全局最优解

贪婪算法并非任何情况下有效,但易于实现

问题描述

有几个地方(8个州),然后有若干个广播电台(5个),每个广播电台能播放若干地方(1-3个州),选出若干广播电台,要求如下:

  • 每个广播电台包含的州尽可能多
  • 合起来的电台能够覆盖所有的州

实现

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 = set()
while states_needed:
#     best_station = None
    states_covered = set()
    for station,states in stations.items():
        covered = states_needed & states
        if len(covered) > len(states_covered):
            best_station = station
            states_covered = covered
    
    states_needed -= states_covered
    final_stations.add(best_station)
    
print(final_stations)
# {'kthree', 'ktwo', 'kone', 'kfive'}

结果可能有不同,因为我们实现的是先找到广播最多的州的电台,然后覆盖所有州

贪婪算法复杂度

此例中,n个广播电台,每次对电台进行循环,每次循环时选择最大覆盖,那么复杂度为O(n2)

NP完全问题

多项式复杂程度的非确定性问题,以难解著称的问题

NP完全问题:

  • 组合问题大概率是NP完全问题
  • 序列问题、集合问题、覆盖问题、旅行商问题等大概率是NP完全问题

面对NP完全问题时,最佳做法是使用近似算法

动态规划

动态规划从小问题着手,逐步解决大问题

特点

  • 在约束条件下找到最优解,背包问题中在背包容量给定情况下偷到价值最高商品
  • 问题可分解为彼此独立且离散的子问题时,可使用动态规划解决
  • 每种动态规划解决方案都涉及网格
  • 每个网格值都是要优化的值,背包问题中,单元格的值为商品的价值
  • 每个单元格都是一个子问题,因此需要考虑如何将问题分成子问题

费曼学习法

Concept (概念)、Teach (教给别人)、Review (回顾)、Simplify (简化)

费曼算法

(1)将问题写下来

(2)好好思考

(3)将答案写下来

最长公共序列和最长公共子串稍有不同

K最近邻算法

计算距离时,可以使用距离公式,也可以使用预先相似度

其他

并行算法

并行算法涉及很难,确保正确工作并实现期望的速度提升也很难,原因如下:

  • 并行性管理开销
  • 负载均衡

posted on 2022-02-09 13:06  lpzju  阅读(97)  评论(0编辑  收藏  举报

导航