读书笔记 -- 算法图解

阅读《算法图解》的随手记,一些概念性的东西和一些简单的代码逻辑

# -*- coding: utf-8 -*-
"""
一些常见的大 O 运行时间,从快到慢:
O(1): 常量时间
O(log n): 对数时间,如二分查找
O(n): 线性时间,如简单查找
O(n * log n): 如快速排序
O(n^2): 如选择排序
O(n!): 如旅行商问题

尾递归
递归语句出现在函数末尾,且不存在于表达式中。
这样可以复用栈(当前栈不有继续存在的意义),而不创建新的栈。
即不是等待满足基线条件的值,往前回溯计算,而是依次计算当前值传向基线条件
例如:求 6 的阶乘
一般递归:1 * 2 * 3 * 4 * 5 * 6
尾递归:6 * 5 * 4 * 3 * 2 * 1

欧几里得算法
即辗转相除法求最大公约数
gcd(a, b)=gcd(b, a mod b)

散列表
良好性能的两大要素:
- 较低的填装因子,一旦填装因子超过 0.7 就应该调整散列表的长度
- 良好的散列函数,尽可能均匀分布

广度优先搜索 -- 非加权图
解决两类问题:
- 从节点 A 是否有前往节点 B 的路径
- 从节点 A 前往节点 B 的最短路径是什么
运行时间:O(V + E),V 为顶点(vertice)数,E 为边数
对于搜索过的节点,不能重复搜索,否则可能无限循环
广度优先搜索不会主动收敛,直到待搜索队列为空

有向图与无向图
有向即单向,无向即双向(环)

狄克斯特拉算法 -- 加权图,有向无环图 DAG(directed acyclic graph)
贝尔曼-富德算法 -- 包含负权边
狄克斯特拉算法不能计算负权边在于,优先计算开销小的节点 且 不重复计算节点开销
即从 A-C 所需开销小于 A-B,则狄克斯特拉算法认为 A-B-C 一定大于 A-B 大于 A-C

近似算法
贪婪算法
每步都是局部最优解,企图最终得到的就是全局最优解 -- 并非任何情况下有行之有效,但易于实现
广度优先搜索、狄克斯特拉算法都属于贪婪算法
NP 完全问题 -- 通常需要寻求近似算法解决

动态规划
在给定约束条件下找到最优解
仅当每个子问题都是独立且离散的,即不依赖其他子问题时,动态规划才管用

字符串相似度比较 -- 动态规划
最长公共子串:两个单词中都有的子串,每个子串的字母数
最长公共子序列:两个单词中都有的序列包含的总的字母数

K 最近邻算法
余弦相似度 -- 距离公式
K 值选取,经验规则:有 N 个用户就选取 sqrt(N) 个邻居

PS:
二叉查找树(binary search tree) -- 对于其中每个节点,左子节点的值都比它小,而右子节点的值都比它大
反向索引 -- 搜索引擎
傅里叶变换
并行算法
MapReduce
布隆过滤器 和 HyperLogLog -- 有概率不正确,但是占用的内存空间十分少
SHA 算法 -- 局部敏感的散列函数
Simhash -- 局部不敏感的散列函数 -- 可用于检查两项内容的相似度
Diffie-Hellman / RSA 秘钥交换
线性规划 Simplex 算法 -- 用于在给定约束条件下最大限度地改善指定的指标
"""


def binary_search(arr, item):
    """二分查找"""
    low = 0
    high = len(arr) - 1

    while low <= high:
        mid = int((low + high) / 2)  # 取小
        guess = arr[mid]
        if guess == item:
            return mid
        if guess > item:
            high = mid - 1
        else:
            low = mid + 1
    return None


def find_smallest(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 selection_sort(arr):
    """选择排序"""
    new_arr = []
    for i in range(len(arr)):
        smallest = find_smallest(arr)
        new_arr.append(arr.pop(smallest))
    return new_arr


def quick_sort(arr):
    """
    快速排序
    递归条件的选择决定了算法的速度
    平均情况:O(n log n)
    最糟情况:O(n^2)
    """
    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 quick_sort(less) + [pivot] + quick_sort(greater)


def find_lowest_cost_node(costs, processed):
    # 在未处理的节点中找出开销最小的节点
    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 = cost
            lowest_cost_node = node
    return lowest_cost_node


def dijkstra():
    """狄克斯特拉算法, Dijkstra's algorithm"""
    # 初始化
    infinity = float("inf")
    graph = {
        "start": {"a": 6, "b": 2},
        "a": {"fin": 1},
        "b": {"a": 3, "fin": 5},
        "fin": {}
    }
    # 存储每个节点的开销
    costs = {
        "a": 6,
        "b": 2,
        "fin": infinity
    }
    # 存储父节点
    parents = {
        "a": "start",
        "b": "start",
        "fin": None
    }
    # 记录处理过的节点
    processed = []

    node = find_lowest_cost_node(costs, processed)
    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, processed)

    print(parents)


if __name__ == '__main__':
    print(binary_search([1, 2, 3, 4, 5], 3))
    print(selection_sort([4, 6, 1, 3, 7]))
    print(quick_sort([10, 7, 3, 5, 9]))
    dijkstra()