读书笔记《算法图解》:像小说一样有趣

算法图解

正如封面一样所说的一样 “像小说一样有趣的算法入门书”,其面的插画一样正诠释了其有趣性。 读完这本书,所以个人觉得这本书适合小白作为 “课外读物”一样去阅读,不适合专业的或者想系统学习的人将其作为核心读物。下面是自己读这本书的一些简记,不全面,但基本是本书的结构,仅供参考。

文章目录

      • 算法图解
        • 第1章:算法简介
          • 大O表示法
          • 二分查找
        • 第2章:选择排序
          • 数组和链表
          • 选择排序
        • 第3章:递归
        • 第4章:快速排序
          • D&C
          • 快速排序
        • 第5章:散列表(hash table)
        • 第6章:广度优先搜索(breadth-first search, BFS)
          • BFS
          • 队列
          • BFS 实现
        • 第7章:狄克斯特拉算法
        • 第8章:贪婪算法
          • NP 完全问题
        • 第9章:动态规划
          • 最长公共子串 → \rightarrow 最长公共子序列

第1章:算法简介

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

大O表示法

大 O 表示法是一种特殊的表示法,表示了算法的速度, 表示了最糟糕情况下的运行时间。假设一个列表包含 n n n 个元素,需要从元素中找到目标元素,则最简单的方式是一个一个找,则需要执行 n n n 次操作。使用大 O 表示法,这个的运行时间为 O ( n ) O(n) O(n) (实际上并不能完全将大 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(nlogn)
  • O ( n 2 ) O(n^2) O(n2)
  • O ( n ! ) O(n!) O(n!)

对于这个时间直观的表示如下图
读书笔记《算法图解》:像小说一样有趣_第1张图片

二分查找

输入:一种有序的元素列表
输出:如果需要查找元素在列表中,返回其位置;否则返回null
大 O 表示的运行时间为 O ( log ⁡ n ) O(\log n) O(logn)

## 在列表中参照元素
def binary_search(bucket, item):
    low = 0
    high = len(bucket) - 1
    
    while low <= high:
        mid = (low + high) // 2
        guess = bucket[mid]
        if guess == item:
            return mid
        if guess > item:
            high = mid - 1
            print('guess number {:2d} is bigger'.format(guess))
        else:
            low = mid + 1
            print('guess number {:2d} is smaller'.format(guess))
    return None

if __name__ == '__main__':
    l = [1, 3, 5, 6, 9, 15, 48]
    r = binary_search(l, 5)
    print(r)   

第2章:选择排序

数组和链表

链表:每个元素都存储了下一个元素的地址,从而使一系列随机的内存串起来,其插入数据方便。需要读取所有元素时,链表的效率很高;但当读取链表中某一个元素时比较低,这是因为读取这个元素,必须读取之前的元素,只能顺序访问。

数组:一个连起来的内存存储元素,即插入新元素时,重新分配,支持随机访问。

选择排序

每次从列表中选择一个最小的元素放入一个新的列表,以此类推,取出目标列表的所有元素。
时间复杂度为 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):
    new_arr = []
    for i in range(len(arr)):
        smallest_index = findSmallest(arr)
        new_arr.append(arr.pop(smallest_index))
    return new_arr

if __name__ == '__main__':
    arr = [2, 1, 0, 3, 7, 45, 10]
    new_arr = selectionSort(arr)
    print('sorted : ', new_arr)

第3章:递归

一种优雅的问题解决方法

  • 递归指的是调用自己的函数
  • 每个递归函数都有两个条件:基线条件和递归条件
# 计算阶乘
def factorial(num):
    return 1 if num == 1 else num * factorial(num-1)

第4章:快速排序

分而治之(divide and conquer,D&C),一种著名的递归式问题解决方法,快速排序的基本思想就是 D&C。

D&C

使用 D&C 解决问题的步骤:

  • 找出基线条件
  • 不断将问题分解,直到符合基线条件
# 4.1 计算列表中元素之和
def sum1(arr):
    return arr[0] if len(arr) == 1 else arr.pop(0) + s(arr)

if __name__ == '__main__':
    arr = [2, 4, 6, 8]
    sum1(arr)
# 4.2 编写一个递归函数来计算列表包含元素数
def countList(arr):
    return 0 if arr == [] else 1 + countList(arr[:-1])

if __name__ == '__main__':
    arr = [2, 4, 6, 8]
    nums = countList(arr)
    print(nums)
# 4.3 找出列表中的最大值
def findMax(arr):
    if arr == []:
        return 0
    else:
        return arr[0] if arr[0] > findMax(arr[1:]) else findMax(arr[1:])

if __name__ == '__main__':
    array = [2, 3, 1]
    print(findMax(array))


快速排序
  • 选择基准值
  • 将数组分成两个子数组:小于基准值的元素和大于基准值的元素
  • 对这两个子数组进行快速排序
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)
        # return quickSort(greater) + [pivot] + quickSort(less)  ## 由大到小的顺序
    
if __name__ == '__main__':
    
    arr = [1, 2, -9, 18, -7, -7]
    print('Sorted:', quickSort(arr))

快速排序的速度取决于选择的基准值,在最糟糕的情况下,其运行时间为 O ( n 2 ) O(n^2) O(n2), 在平均情况下,快速排序的运行时间为 O ( n log ⁡ n ) O(n \log n) O(nlogn)
合并排序的运行时间恒为 O ( n log ⁡ n ) O(n \log n) O(nlogn) ,如果快速排序的运行时间也为 O ( n log ⁡ n ) O(n \log n) O(nlogn),然而由于大 O 记法忽略了常量的原因,快速排序算法比合并排序快。

第5章:散列表(hash table)

数组和链表都被直接映射到内存,但散列表更为复杂,它使用散列函数来确定元素的存储位置。
Python提供的散列表实现为字典

散列表适用场景:

  • 模拟映射关系

  • 防止重复

  • 缓存数据,以免服务器再通过处理来生成它们

    冲突:给两个键分配的位置相同

在平均情况下,散列表执行各种操作的时间都为 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) 被称为常量时间。

填装因子度量的是散列表中有多少位置是空的,公式表示为: 散 列 表 包 含 的 元 素 数 位 置 总 数 \frac{散列表包含的元素数}{位置总数}

第6章:广度优先搜索(breadth-first search, BFS)

图模拟一组连接,由节点和边组成。

BFS

可解的问题类型:

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

队列支持的操作:入队和出队
队列是一种先进先出(First In First Out, FIFO)的数据结构, 而栈是一种后进先出(Last In First Out, LIFO) 的数据结构。

BFS 实现
from collections import deque

def person_is_seller(name):
    return name[-1] == 'y'

def bfs(graph):
    search_queue = deque()
    search_queue += graph["you"]
    searched = []
    while search_queue:
        person = search_queue.popleft()
        if not person in searched:
            if person_is_seller(person):
                print(person +" is seller")
                return True
            else:
                search_queue += graph[person]
                searched.append(person)
    return False

if __name__ == '__main__':
    graph = {}
    graph['you'] = ["alice","bob","claire"]
    graph['bob'] = ["anuj","peggy"]
    graph["alice"] = ["peggy"]
    graph["claire"] = ["thom","jooy"]
    graph["anuj"] = []
    graph["peggy"] = []
    graph["thom"] = []
    graph["jooy"] = []
    
    flag = bfs(graph) 

广度优先搜索的运行时间为 O ( 人 数 + 边 数 ) O(人数+边数) O(+)

第7章:狄克斯特拉算法

要计算非加权图中的最短路径,可以使用广度优先搜索算法;如若解决的是加权图,则可用狄克斯特拉算法。狄克斯特拉算法包含4个步骤:

  • 找出 “最便宜” 的节点,即可在最短时间内到达的节点
  • 更新该节点的邻居的开销
  • 重复这个过程,直至对途中的每个节点进行这样的操作
  • 计算最终路径

狄克斯特拉算法算法只适用于有向无环图

第8章:贪婪算法

贪婪算法:每步都采取最优的做法。

# 电台覆盖问题
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, stayes_for_station in stations.items():
        covered = states_needed & stayes_for_station
        if len(covered) > len(states_covered):
            best_station = station
            states_covered = covered
    final_stations.append(best_station)
    states_needed -= states_covered
    
final_stations
NP 完全问题

涉及 “所有组合” 的问题通常是 NP完全问题。
如果问题可转换为集合覆盖问题或旅行商问题,那它坑爹是 NP 完全问题

第9章:动态规划

动态规划解决的问题的条件是是每个子问题是离散的,即相互之间不依赖

最长公共子串 → \rightarrow 最长公共子序列

费曼算法(Feynman algorithm)算法的步骤:

  • 将问题写下来
  • 好好思考
  • 将答案写下来
# Longest Common Substring
def longest_common_substring(str1, str2):
    cell = np.zeros(shape=(len(str1), len(str2)))
    for idx_a, a in enumerate(str1):
        for idx_b, b  in enumerate(str2):
            if a == b:
                cell[idx_a][idx_b] = cell[idx_a-1][idx_b-1] + 1

    return cell

if __name__ == '__main__':
    import numpy as np
    str1 = 'fosh'
    str2 = 'forh'
    cell = longest_common_substring(str1, str2)
    print(cell)

# Longest Common Subsequence
def logest_common_subsequence(str1, str2):
    cell = np.zeros(shape=(len(str1), len(str2)))
    for idx_a, a in enumerate(str1):
        for idx_b, b  in enumerate(str2):
            if a == b:
                cell[idx_a][idx_b] = cell[idx_a-1][idx_b-1] + 1
            else:
                cell[idx_a][idx_b] = max(cell[idx_a-1][idx_b], cell[idx_a][idx_b-1])
    return cell
if __name__ == '__main__':
    import numpy as np
    str1 = 'fosh'
    str2 = 'fish'
    cell = logest_common_subsequence(str1, str2)
    print(cell)

你可能感兴趣的:(简记,python,算法,数据结构)