《算法图解》,Aditya Bhargava 著,袁国忠 译,人民邮电出版社
本书的目的就是要把难懂的技术主题说清楚,让这本算法书易于理解(摘自作者前言)
读后感
《算法图解》是经典的算法书,受到很多人推崇为入门书籍,第一次阅读此书是20年上半年,觉得内容比较浅显,粗略看过,第二次是21年上半年,对于例子部分跳过较多,原理部分详细阅读,并仔细做了纪要
此书作为算法入门书籍的优势在于讲解生动,但是由于算法这个话题太过于广泛,所以阅读起来会觉得书本逻辑跳跃较大,前后章节的关联性时有时无,下面尝试理一理书本的内容逻辑
- 数据结构:数组、链表、散列表、队列、调用栈、二叉查找树
- 算法思想:二分查找、递归算法、分治算法、动态规划、贪婪算法、广度优先算法
- 排序算法:选择排序、快速排序
- 经典算法:迪杰斯特拉算法、K最近邻算法
- 拓展内容:MapReduce、反向索引、傅里叶变换、并行算法、线性规划、加密算法等
以下是知识点纪要,部分代码转化为C++给出
1 二分查找
- 有序列表
- 对数时间
- 大O表示法是最坏情况下的运行时间
- 算法的速度指的是操作数的增速,不以秒为单位
- 旅行商问题的时间复杂度是O(n!)
int binary_search(vector<int>& nums, int item){
int lhs = 0, rhs = nums.size()-1;
while(lhs <= rhs){
int mid = lhs + (rhs - lhs) / 2;
// 猜数字
int guess = nums[mid];
if (guess == item){
return mid;
}
else if (guess > item){
rhs = mid - 1;
}
else{
lhs = mid + 1
}
}
// 数字没找到
return -1;
}
2 选择排序
- 计算机内存均有各自的地址,用于存放数据
- 数组和链表
数组和链表是两种基本的数据结构。
(1)数组是提前申请内存,内存大小是固定的,元素都在一起,优点是便于随机读取,缺点是难以确定合适的内存
(2)链表的每个元素不仅储存值,还储存下一个元素的地址,元素是分开的,优点是便于插入和删除元素,缺点是无法随机读取 - 选择排序的时间复杂度是O(n^2),每轮循环需要找寻最小数
3 递归
- 递归便于理解,但不提高效率
递归的基本做法就是函数调用自己
如果使用循环,程序的性能可能更高,如果使用递归,程序可能更容易理解
递归函数包含基线条件(base case)和递归条件(recursive case),前者是终止条件,避免无限循环,后者是函数调用自己
void countdown(int i){
cout << i << endl;
// base case
if (i <= 0){
return;
}
// recursive case
else{
countdown(i-1);
}
}
- 调用栈(call stack)
基本操作是压入和弹出
思想是后进先出(Last In First Out,LIFO)
递归函数也使用调用栈:递归分解问题是自上而下,分解过程的中间量入栈中,求解问题的时候自下而上,出栈进行依次计算
使用栈的代价是消耗内存
4 快速排序
- 分治思想
分而治之(divide and conquer,D&C),一种著名的递归方法
分治包含两个步骤,找出可以便捷处理的基线条件,不断分解问题直至符合基线条件 - 快速排序
快速排序也是一种分治算法
快速排序的步骤是:(1)设定基准(2)将余下的元素分成小于等于基准和大于基准的两个分区(3)对分区重复(1)和(2),直至分区不多于一个数为止
快排的平均时间复杂度是O(nlogn),其算法速度与基准取值有关
def quicksort(array):
if len(array) < 2:
return array
else:
pivot = array[0]
# 小于基准的子数组
less = [i for i in array[1:] if i <= pivot]
# 大于基准的子数组
greater = [i for i in array[1:] if i > pivot]
return quicksort(less) + [pivot] + quicksort(greater)
print quicksort([10,5,2,3])
5 散列表(hash table)
散列表由键和值组成,散列表将键映射到值
键是唯一的,不重复
缓存/记住数据,以免服务器再通过处理来生成它们
散列函数用于构建键到内存地址的映射,实现O(1)查找
散列函数要将键均匀映射到散列表不同位置
散列函数要尽量减少冲突,就是减少存储的链表长度
6 广度优先搜索(Breadth-first search, BFS)
- 广度优先搜索思想
广度优先搜索用于找寻最短距离,适合用于图查找
广度优先搜索回答两个问题:解的存在性和解的最优性
对于检查过的人,务必不要再去检查,否则可能导致无限循环
建立搜索列表依次进行检查 - 队列
队列思想是先进先出(First In First Out,FIFO)
支持两种操作:入队和出队
7 狄克斯特拉算法
- 加权图和非加权图
带有权重边的关联图称为加权图
不带权重边的关联图称为非加权图 - 有向图和无向图
边为单一方向,称为有向图
边为双向,称为无向图
无向图,每条边都是一个环 - 狄克斯特拉算法
狄克斯特拉算法用于求解加权有向无环图最短路径
且这里的加权图不能是负权,负权需用贝尔曼福德算法(Bellman-Ford algorithm)求解
步骤包括:
(1)找出当前位置(已遍历节点)可到达的代价最小的节点
(2)对于每个节点,检查其邻居,判断是否存在更短路径,如有则更新其开销
(3)重复(1)(2),直至遍历所有节点的可达路径
(4)计算最终路径
8 贪婪算法
-
贪婪算法思想
贪婪算法很简单:每步都采取最优的做法
每步都选择局部最优解,最终得到的不一定是全局最优解
使用贪婪算法可得到非常接近的解
贪婪算法的时间复杂度往往较低
经典问题:任务排序问题、背包问题 -
NP完全问题
NP完全问题的简单定义是,以难解著称的问题,如旅行商问题和集合覆盖问题
旅行商问题的解空间是关于城市个数n的阶乘函数,求解的时间复杂度是阶乘时间,远大于多项式时间,很多非常聪明的人都认为,根本不可能编写出可快速解决这些问题的算法
面临NP完全问题时,最佳的做法是使用近似算法NP完全问题难以判断,常具有的特征包括:
(1)随着元素增加,求解时间迅速增加
(2)所有涉及组合问题
(3)不能划分小问题
(4)涉及序列且难以解决
(5)涉及集合且难以解决
9 动态规划
-
动态规划思想
将问题划分为小问题,并从处理小问题着手
每种动态规划解决方案都涉及网格
动态规划可在给定约束条件下找到最优解
在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决
最优解可能导致背包没装满 -
经典问题:背包问题
背包问题的范式是:给定固定的容量约束,选择最大价值的物件组合
求解思路:双层循环,外层遍历物件,内层遍历容量,判断在该容量下能否装入物件的最大价值
背包问题与排列顺序无关 -
绘制网格
思考下面问题:
- 单元格中的值是什么?
- 如何将这个问题划分为子问题?
- 网格的坐标轴是什么?
单元格中的值通常就是你要优化的值。
每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题
没有放之四海皆准的计算动态规划解决方案的公式 -
应用广泛
git diff指出两个文件的差异,也是使用动态规划实现的
编辑距离(levenshtein distance)指出了两个字符串的相似程度,也是使用动态规划计算得到的
10 K最近邻(k-nearest neighbours,KNN)
-
K最近邻的要素
特征抽取:将物品(如水果或用户)转换为一系列可比较的数字
衡量相似程度:距离公式、余弦相似度
实现目的:推荐系统 -
分类和回归
分类就是编组
回归就是预测结果
KNN用于分类和回归,需要考虑最近的邻居
11 接下来如何做
概述本书未介绍的10种算法以及它们很有用的原因
- 树
二叉查找树(binary search tree)的数据结构可以便于查找
二叉树即每个节点最多只有左右两个子节点,左子节点的值都比它小,而右子节点的值都比它大
二叉查找树中查找节点时,平均运行时间为O(log n),但在最糟的情况下所需时间为O(n)
二叉查找树的插入和删除操作的速度很快,但不支持随机访问
平衡二叉树(红黑树、B树)等避免出现性能不佳的糟糕情况
- 反向索引(inverted index)
反向索引常用于创建搜索引擎
- 傅里叶变换
傅里叶变换的一个绝佳比喻:给它一杯冰沙,它能告诉你其中包含哪些成分
给定一首歌曲,傅里叶变换能够将其中的各种频率分离出来
傅里叶变换非常适合用于处理信号,可使用它来压缩音乐(过滤低频分量)
- 并行算法
并行算法设计起来很难,速度的提升并非线性的
(1)并行性管理开销:分配任务,合并结果
(2)负载均衡:均匀分配任务
-
MapReduce
MapReduce是一种流行的分布式算法,可通过流行的开源工具Apache Hadoop来使用它
分布式算法非常适合用于在短时间内完成海量工作,其中的MapReduce基于两个简单的理念:映射(map)函数和归并(reduce)函数 -
布隆过滤器和HyperLogLog
布隆过滤器是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。为判断网页以前是否已搜集,可不使用散列表,而使用布隆过滤器。使用散列表时,答案绝对可靠,而使用布隆过滤器时,答案却是很可能是正确的
布隆过滤器的优点在于占用的存储空间很少 -
安全散列算法(secure hash algorithm,SHA)
采用一种映射对数据进行变换,可用于加密 -
局部敏感的散列算法
对字符串做细微的修改,Simhash生成的散列值也只存在细微的差别,用于比对文件
- Diffie-Hellman 密钥交换
对消息进行加密
Diffie-Hellman使用两个密钥:公钥和私钥,使用公钥对其进行加密,加密后的消息只有使用私钥才能解密
(1) 双方无需知道加密算法。他们不必会面协商要使用的加密算法。
(2) 要破解加密的消息比登天还难
- 线性规划
线性规划是最简单的最优化算法,用于求解线性约束下的最值问题