《算法图解》是一本算法入门书,讲解了一些轻松有趣的算法。
第一章:算法简介
二分查找
二分查找算法一次排除一半的可能性,在有序列表查找时,效率比从头开始查找快很多。
大 O 表示法指出了算法的增速。 大 O 表示法指出了最糟情况下的运行时间。除了最糟情况下的运行时间外,还应考虑平均情况的运行时间。
二分查找(Binary Search)是一种在有序数组或列表中查找特定元素的搜索算法。它的基本思想是不断将待查找范围缩小为一半,直到找到目标元素或确定目标元素不存在为止。
二分查找的工作原理如下:
-
初始化: 确定查找范围的起始位置和结束位置,通常为数组或列表的首尾元素索引。
-
中间值计算: 计算查找范围的中间位置的索引,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> m i d = ( s t a r t + e n d ) / 2 mid=(start+end)/2 </math>mid=(start+end)/2。
-
比较目标值: 将目标值与中间值进行比较:
- 如果目标值等于中间值,则查找成功,返回中间值的索引;
- 如果目标值小于中间值,则更新查找范围为左半部分,即将结束位置更新为中间值的前一个索引;
- 如果目标值大于中间值,则更新查找范围为右半部分,即将起始位置更新为中间值的后一个索引。
- 迭代或递归: 重复上述步骤,直到找到目标值或起始位置大于结束位置为止。
二分查找的时间复杂度为 O(logn),其中 n 是待查找范围的大小。这使得二分查找成为一种高效的查找方法,特别适用于有序数组或列表中的查找操作。
值得注意的是,二分查找要求待查找的数组或列表必须是有序的。如果待查找的数据量较小,可以选择简单的线性搜索算法,而对于较大的有序数据集合,二分查找通常是更优的选择。
旅行商问题
旅行商问题(Traveling Salesman Problem,TSP)是一种著名的组合优化问题,它的目标是寻找一条路径,使得旅行商可以经过所有给定城市一次,然后回到起始城市,并且总路程最短。
旅行商问题通常可以描述为以下几个要素:
- 城市集合: 给定一组城市,通常用 N 个点表示,每个点代表一个城市。
- 距离矩阵: 对于每对城市 i 和 j,有一个确定的距离 <math xmlns="http://www.w3.org/1998/Math/MathML"> d i j d_{ij} </math>dij 表示从城市 i 到城市 j 的距离。
- 目标: 旅行商需要从一个起始城市出发,经过所有的城市一次,然后返回起始城市。路径形成一个环路。目标是找到一条路径,使得总路程最短。
旅行商问题是一个组合优化问题,属于NP-hard问题,即没有已知的有效算法可以在多项式时间内解决所有实例。因此,对于大规模的TSP实例,通常需要采用近似算法或启发式算法进行求解。
一些常用的求解方法包括:
- 穷举法: 对于小规模的问题,可以使用穷举法来列举所有可能的路径,并计算每条路径的总路程,从中选择最短的路径。但是随着城市数量的增加,穷举法的计算量会呈指数增长,变得不可行。
- 近似算法: 例如最小生成树法、最近邻法等,这些方法虽然不能保证得到最优解,但是在实际应用中通常能够快速给出一个较好的近似解。
- 启发式算法: 例如模拟退火算法、遗传算法、蚁群算法等,这些方法利用了问题的特定性质,通过迭代优化来寻找较优解,适用于中等规模的TSP实例。
由于旅行商问题在实际应用中具有广泛的应用价值,因此研究者们一直在探索更高效的求解算法和优化方法,以提高求解效率和解的质量。
第二章:选择排序
数组和链表
数组是一段连续的空间,支持随机访问。查找简单,增加、删除麻烦。
链表是一段非连续的空间,只支持顺序访问。头部/尾部增删简单,查找麻烦。
选择排序
选择排序:每次从列表中选出最大值/最小值,将其交换到数组的头部。
第三章:递归
递归让程序更容易理解,它没有性能上的优势。实际上,使用循环可能性能更高。
递归包含基线条件和递归条件。
栈结构:先进后出(Last In First Out,LIFO)。
队列:先进先出(First In First Out,FIFO)。
使用递归可能导致调用栈占用大量的内存。解决方案可以是换成循环,也可以是换成尾递归。
第四章:快速排序
欧几里得算法
分治思想(divide and conquer,D&C):大事化小,小事化了。不断缩小问题的规模。
欧几里得算法:辗转相除法,找最大公约数,解决分土地问题。
快速排序算法
快速排序:选择基数,将数组分为小于基数,大于基数的左右两块区域,再对左右区域分别进行同样的操作。每次遍历一次数组,可以给基数排好序。平均时间复杂度 O(n <math xmlns="http://www.w3.org/1998/Math/MathML"> log \log </math>log n),最坏时间复杂度 O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n^2 </math>n2)。重复排序时,从头/尾选择基数就会导致最差情况出现。随机选择基数可以避免这一情况。
第五章:散列表
散列函数将输入映射到数字。散列函数的特点:
- 确定性:对于相同的输入,散列函数总是产生相同的输出。
- 散列碰撞:散列函数的输入和输出不是唯一对应关系。
- 不可逆性:散列函数是不可逆的,意味着无法从散列值还原出原始输入数据。
- 混淆特性:散列函数具有混淆特性,这意味着输入数据的微小变化会导致输出散列值的大幅变化。
散列表的增删改查都只需要常量级的时间。散列表适合用于:
- 模拟映射关系。
- 防止重复
- 缓存数据
为了避免冲突,需要有:
- 较低的装载因子(书中写的是填装因子)
- 良好的散列函数。
装载因子 = 散列表包含的元素数 / 位置总数。
它反映了散列表的被占用率。装载因子越低,碰撞的概率就越低,因为位置较多。随着数据的填充,装载因子会不断升高。当装载因子较大时,通常就需要调整散列表的长度来避免碰撞了。一个经验规则是,一旦装载因子大于 0.7,就调整散列表的长度。
在 Java 中,HashMap 的装载因子达到 0.75 时会进行扩容。(默认容量为 16,也就是说,填充了 12 个元素后会进行一次扩容。)
第六章:广度优先搜索(breadth-first search,BFS)
广度优先搜索解决两类问题:
- 路径是否存在
- 哪条路径最短
有向图:无向图的边是没方向的,即两个相连的顶点可以互相抵达。 有向图:有向图的边是有方向的,即两个相连的顶点,根据边的方向,只能由一个顶点通向另一个顶点。
可以用队列来实现广度优先搜索:
- 将所有邻居加入队列,从头开始检查。
- 如果这个元素满足条件,出队,搜索结束。
- 如果这个元素不满足条件,将其的邻居添加到队尾,继续搜索。
- 需要一个额外的列表检查当前检查的人是否已经检查过。否则可能会出现循环。
广度优先搜索算法的运行时间:O(V + E),V 为顶点数(vertices),E 为边数(edges)
拓扑排序:多个任务之间有依赖关系,对其的排序称为拓扑排序。
拓扑排序是图论中的一个概念,用于有向无环图(Directed Acyclic Graph,DAG)的顶点排序。在拓扑排序中,如果图中存在一条从顶点 u 到顶点 v 的有向边,那么在排序中顶点 u 必须出现在顶点 v 的前面。换句话说,拓扑排序将图中的顶点排成一个线性序列,使得对于任意一条有向边 (u, v),顶点 u 在序列中出现在顶点 v 的前面。
拓扑排序可用于启动速度优化,将启动时需要执行的任务列出来,任务间的依赖关系表示出来,进行拓扑排序,然后将能并行的任务并行执行,就能提升启动速度。
拓扑排序演示:www.cs.usfca.edu/~galles/vis...
第七章:狄克斯特拉算法(Dijkstra's algorithm)
Dijkstra 算法用于找出加权图中前往 X 的最短路径。
广度优先搜索可以找出最短路径,但如果每条路径花费的时间不一样,那么找出最快的路径就可以用 Dijkstra 算法。
Dijkstra 算法步骤:
- 找出最便宜的节点,即可在最短时间内到达的节点。
- 更新该节点的邻居的开销。
- 重复这个过程,直到图中的每个节点都这样做了。(无需对终点这样做)
- 计算最终路径。
由 Dijkstra 算法的步骤可以看出,Dijkstra 只适用于有向无环图。
更新邻居开销时,同时记录当前到达邻居节点的最便宜的方式的前一个节点,可以称之为父节点。最后回溯整条最短路径时,从父节点层层往上即可。
如果价格相同,将价格相同的多个节点都记录下来,回溯时是不是可以找到所有可能的最短路径?
河边挑水的问题可以用 Dijkstra 算法,也许并不是做对称线的路径最好,可能是经过 C 点最好。因为挑上水之后,桶变重了,之后的路径权重较大。
河边挑水的问题:从 A 点出发,到河边挑水到 B 点,走哪条路最好?
Dijkstra 不适用于带负权边的算法,因为负权边可能导致已经处理过的节点又找到一条到达它的最短路径。
在带负权边的图中,找出最短路径可以用贝尔曼-福德算法(Bellman-Ford algorithm)
模拟,尝试找出 Dijkstra 算法不能用于带环图和带负权边的图的本质原因。
第八章:贪婪算法
每次都取局部最优解,最后获得全局最优解。
有时候,贪婪算法无法获得全局最优解,只能获得一个近似的解。在面对 NP 完全问题(Non-deterministic Polynomial,非确定多项式)时,由于目前没有快速的算法,可以使用贪婪算法获得近似解。
如旅行商问题,是典型的 NP 完全问题。时间复杂度是 O(n!),需要计算出所有的解,才能找出答案。
目前没有找到快速识别 NP 完全问题的办法,一般来讲,涉及"所有组合"的问题通常是 NP 完全问题。
第九章:动态规划
背包问题。每一件物品都可以选择装或者不装。
cell[i][j] = max(cell[i-1][j], cell[i-1][j-当前重量])
cell[i-1][j] 表示不装此物品,cell[i-1][j-当前重量] 表示装入当前物品。
当问题可以分解为彼此独立且离散的子问题时,可使用动态规划。
费曼算法:
- 将问题写下来。
- 好好思考。
- 将答案写下来。
第十章:K 最近邻算法(k-nearest nerghbours,KNN)
毕达哥拉斯公式:
<math xmlns="http://www.w3.org/1998/Math/MathML"> d i s t a n c e = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 distance = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2} </math>distance=(x1−x2)2+(y1−y2)2
多维空间中的距离也是类似的。
计算 K 个最近的邻居,可以用来做分类,以此实现电影推荐系统。还可以用来做回归,以此来预测结果。
余弦相似度:与 KNN 算法不同的是,余弦相似度不计算两个矢量之间的距离,而是比较他们的角度。适合处理每个人的打分标准不同的场景。
使用 KNN 算法,挑选合适的特征很重要。
KNN 算法堪称进入机器学习领域的领路人。可用于图像中的文字识别,思路是浏览大量的图像,将文字的特征提取出来(比如曲线、点、线段),这一步称之为训练。遇到新图像时,提取图像的特征,找出它的最近邻居。
垃圾邮件过滤器使用一种简单的算法------朴素贝叶斯分类器。使用一些数据训练这个分类器,让其知道垃圾邮件中最常出现的单词、句子(如 million,send me your password)。当收到一封新邮件时,对比其与分类器的相似度,预测其是否为垃圾邮件。
第十一章:接下来如何做
树形结构
二叉搜索树
为了更方便地使用二分查找,可以在存储数据时就将其存储成「二分」的结构,二叉搜索树(binary search tree)应运而生。
平衡二叉树
二叉搜索树倾斜时,查询效率不佳,因此,平衡二叉树(Balanced Binary Tree)应运而生。
平衡二叉树是一种通用的概念,它指的是一棵二叉搜索树,其中任何节点的左右子树高度差都受到限制,从而确保树的高度始终保持在对数级别,从而保证了查找、插入和删除操作的时间复杂度为 O(log n)。
AVL 树
平衡二叉搜索树的一种实现是 AVL 树,它是一种特殊的自平衡二叉查找树。 得名于其发明者G. M. Adelson-Velsky 和 E. M. Landis。它在树的每个节点上维护一个平衡因子(即左子树高度与右子树高度的差),并通过旋转操作来保持树的平衡。AVL树的特点是对于树中的每个节点,其平衡因子的绝对值不超过1。
红黑树
红黑树也是一种自平衡的二叉搜索树,它通过在每个节点上增加一个额外的位来存储节点的颜色(红色或黑色),并且满足一组红黑树性质,以确保树的高度保持在对数级别,从而保证了查找、插入和删除操作的时间复杂度为 O(log n)。这些性质包括:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL节点,即空节点)都是黑色的。
- 如果一个节点是红色的,则其两个子节点都是黑色的(即不存在两个相连的红色节点)。
- 从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点(即,路径中的黑色节点数相等)。
红黑树并不是绝对平衡的,而是通过遵循这些性质来保持相对平衡。
伸展树(Splay Tree)
伸展树(Splay Tree)是一种自调整的二叉搜索树,它通过一系列的旋转操作来调整树的结构,以保持最近访问的节点处于树的根位置,从而提高了访问局部性,并且能够在实际应用中取得较好的性能表现。
伸展树的特点包括:
- 自调整性: 伸展树在每次访问或者插入节点时会对树进行重新平衡,将最近访问的节点移动到根位置。这样做的好处是,被频繁访问的节点会被置于靠近根的位置,从而提高了对这些节点的访问速度。
- 局部性: 伸展树通过频繁地进行局部调整,能够将频繁访问的节点聚集在一起,形成了局部性,使得对这些节点的访问更加高效。
- 简单性: 伸展树的实现相对简单,不需要维护额外的平衡因子或颜色标记等信息,只需通过基本的旋转操作来实现自调整。
尽管伸展树在理论上具有一些优点,但是在实际应用中,它的性能并不总是优于其他平衡树结构,如AVL树或红黑树。这是因为伸展树在最坏情况下可能导致树的高度变得很大,从而影响了其性能。然而,对于特定的应用场景,伸展树仍然可能是一种有效的选择,尤其是对于需要频繁访问最近访问节点的情况。
B 树
- B树是一种多路搜索树,用于解决磁盘I/O效率问题。它具有一个特定的阶(order),其中每个节点可以拥有多于两个子节点。
- B树的节点可以容纳大量的键值对,适用于需要频繁读写大量数据的场景,如数据库系统中的索引结构。
B+树
- B+树是B树的一种变体,与B树相比,它具有更好的查找性能和范围查询性能。
- B+树的所有数据都存储在叶子节点上,非叶子节点仅包含键值和指向子节点的指针,这样的设计提高了范围查询的效率。
B*树(B-star tree,读作B星树)
- B*树是B树的另一种变体,旨在减少B树分裂的频率,从而提高插入和删除操作的性能。
- 与B树相比,B*树允许非叶子节点更少的键值,并且具有更大的节点大小,以减少树的高度。
反向索引
搜索引擎的简单实现:将关键字映射到包含它的网页。当用户搜索关键字时,将映射结果呈现给用户。这种数据结构被称为反向索引。
傅里叶变换
傅里叶变换(Fourier Transform)是一种数学工具,用于将一个函数(通常是一个时间域上的信号)转换成另一个域(频率域)的表示。它是以法国数学家约瑟夫·傅里叶的名字命名的,他在19世纪初提出了这个概念。
傅里叶变换的基本思想是,任何一个周期性信号(甚至是非周期性的信号,通过周期扩展)都可以分解成多个不同频率的正弦和余弦函数的叠加。这些正弦和余弦函数的振幅和相位描述了原始信号在频率域中的成分。
傅里叶变换有两种常见的形式:连续傅里叶变换(Continuous Fourier Transform,CFT)和离散傅里叶变换(Discrete Fourier Transform,DFT)。
- 连续傅里叶变换(CFT)适用于连续时间信号,它将一个连续函数转换为一个连续函数。
- 离散傅里叶变换(DFT)适用于离散时间信号,例如在数字信号处理中常见的数字信号。离散傅里叶变换是通过将信号分成离散的时间点来进行计算的。
傅里叶变换在许多领域中都有广泛的应用,包括信号处理、图像处理、通信、音频处理、量子力学等。它可以用来分析信号的频谱特性,去除噪声,滤波,压缩数据等。傅里叶变换的逆变换可以将频域表示的信号重新转换回时间域表示。
简单来说,它可以将一个信号分解成不同频率的成分。
比如,如果你有一段音乐,傅里叶变换可以告诉你这段音乐中有哪些频率的声音成分,以及它们的强度(即振幅)和相位(即音调)。这对于理解信号的特征(音乐识别)、去除噪声、压缩数据(去掉不重要的音符)等都非常有用。
并行算法
并行算法在处理海量数据时,可以获得一些性能提升。并行算法设计起来很难,要确保他们正确工作并实现期望的速度提升也很难。并且,速度的提升不是线性的,也就是说,两个内核无法把算法速度提高一倍。因为需要考虑并行性管理开销和负载均衡的问题。
以并行快速排序算法为例:(排序并不类似于十月怀胎,即使十个人来也需要十个月才能完成。排序是可以并行的,十个月的任务可以让十个人用一个月完成。)
并行快速排序的基本思想是利用多个处理单元同时处理待排序数据的不同部分,从而提高排序的效率。
一种常见的并行快速排序的实现原理:
- 划分阶段: 类似于串行版本的快速排序,首先选择一个基准元素,然后将待排序数据分割成两部分,小于基准元素的部分和大于基准元素的部分。这个过程是并行的,可以由多个处理单元同时处理不同部分的数据。
- 排序阶段: 划分完成后,每个处理单元分别对其负责的部分进行递归排序。这些排序操作也可以并行执行,每个处理单元独立地对其部分进行快速排序。
- 合并阶段: 在所有处理单元都完成了排序后,需要将各个部分合并起来。这一阶段的实现可以采用串行或者并行的方式,取决于具体的并行框架或算法。
并行快速排序的主要挑战之一是如何有效地划分和合并数据,以及如何处理不同处理单元之间的负载平衡问题。通常,各种并行快速排序算法会采用一些技术来解决这些问题,例如动态负载平衡、任务分配策略、局部排序和合并等。
并行快速排序通过同时利用多个处理单元来加速排序过程,可以在一定程度上提高排序的效率,特别是在大规模数据的排序场景中。并行快速排序的时间复杂度为 O(n)。
分布式算法
分布式算法是一种设计用于在分布式系统中执行的算法。在分布式系统中,多个计算节点(或者计算机)通过网络相互连接,共同完成计算任务。
MapReduce 是一种流行的分布式算法。MapReduce算法包含两个主要阶段:Map阶段和Reduce阶段。
- Map阶段: 在Map阶段,原始数据集被划分成多个小数据块,每个数据块由一个Map任务处理。Map任务将输入数据块转换成键值对(key-value pairs),并生成一系列中间键值对作为输出。每个中间键值对包含一个键和与之相关联的一个或多个值。Map阶段的输出结果被分区(partitioned)并分发到多个Reduce任务。
- Reduce阶段: 在Reduce阶段,所有具有相同键的中间键值对被发送到同一个Reduce任务。Reduce任务对这些数据进行汇总处理,并生成最终的输出结果。通常情况下,每个Reduce任务产生一个输出文件,最终的结果由所有Reduce任务的输出组合而成。
概率型数据结构
布隆过滤器
布隆过滤器是一种特殊的数据结构,用于快速判断一个元素是否存在于一个集合中。它通过使用少量的内存空间和多个哈希函数来实现快速查找,常用于大规模数据的判重和过滤操作。虽然它有一定的误判率,但在需要高效判断大量数据是否存在的场景下,布隆过滤器是一种非常有用的工具。
布隆过滤器的核心思想是通过使用多个哈希函数和一个位数组来表示一个集合,从而实现高效的查找。
布隆过滤器的基本原理如下:
- 位数组: 布隆过滤器使用一个长度为m的位数组来表示集合,初始时所有位都初始化为0。
- 多个哈希函数: 布隆过滤器使用多个不同的哈希函数(通常是k个),每个哈希函数可以将集合中的元素映射到位数组中的不同位置。
- 插入操作: 当一个元素要被插入到集合中时,布隆过滤器会对该元素进行k次哈希操作,并将位数组中对应的位置设置为1。
- 查找操作: 当需要判断一个元素是否存在于集合中时,布隆过滤器同样对该元素进行k次哈希操作,然后检查对应的位数组位置是否都为1。如果有任何一个位置为0,则可以确定该元素一定不在集合中;如果所有位置都为1,则可能存在于集合中,但不是确切的证据。
布隆过滤器的优点是空间效率高和查询效率快,因为它只需要使用少量的内存空间,并且不需要存储实际的元素信息,只需要存储哈希值的位置。但是,布隆过滤器也有一定的缺点,即存在一定的误判率(false positive),即有些元素被判断为存在于集合中,但实际上并不存在。
布隆过滤器在实际应用中常被用于缓存、网页爬取、拦截垃圾邮件、数据过滤等场景,尤其是在需要快速判断一个元素是否属于一个大规模集合的情况下,布隆过滤器可以提供高效的解决方案。
HyperLogLog
HyperLogLog是一种概率型数据结构,用于对大规模数据集中的不重复元素进行近似计数。它的设计旨在提供高效的内存利用和快速的近似计数,适用于大规模数据集的基数(不重复元素的数量)估计。
HyperLogLog的基本原理是使用一个位数组和一组哈希函数来表示输入数据集中的不同元素。每个元素经过哈希函数映射到位数组的某个位置,然后根据哈希值的一部分来确定位数组中相应位置的值。最后,通过对位数组进行一些统计和计算操作,可以估计出数据集的基数。
HyperLogLog的特点包括:
- 内存效率高: HyperLogLog使用的内存量很小,并且与输入数据规模无关,因此适用于处理大规模数据集。
- 计数准确性: 虽然HyperLogLog提供的是一种近似估计,但在实践中可以达到很高的准确度,通常可以达到小于1%的误差率。
- 快速计算: HyperLogLog的计算复杂度很低,并且可以通过并行和分布式方式实现高效计算。
HyperLogLog常被用于大规模数据的去重、基数估计、流量分析等场景。例如,在大数据处理和数据挖掘领域中,可以使用HyperLogLog来快速估计一个数据集的去重后的元素数量,而不必存储所有元素的实际值,从而节省内存和计算资源。
密码学
SHA 算法
SHA(Secure Hash Algorithm,安全哈希算法)是一系列密码学哈希函数的家族,用于生成数据的固定大小的哈希值,通常表示为40个十六进制数字。SHA算法是由美国国家安全局(NSA)设计,并由美国国家标准与技术研究所(NIST)发布的一系列标准。
SHA算法的主要特点包括:
- 固定长度输出: SHA算法生成的哈希值具有固定的长度,不受输入数据的长度影响。
- 单向函数: SHA算法是单向函数,即从哈希值无法推导出原始输入数据。这意味着即使知道哈希值,也无法恢复出原始数据。(用其存储密码是相当安全的)
- 唯一性: 不同的输入数据通常会生成不同的哈希值。尽管哈希碰撞(即不同的输入数据生成相同的哈希值)是可能的,但SHA算法设计上尽量避免碰撞的发生。
SHA算法的常见版本包括SHA-1、SHA-256、SHA-384和SHA-512等。它们的区别在于输出的哈希值长度不同,以及使用的轮数和轮函数等细节。
- SHA-1 生成的哈希值长度为 160 位(20 字节)。(有漏洞,已不被使用)
- SHA-256 生成的哈希值长度为 256 位(32 字节)。
- SHA-384 生成的哈希值长度为 384 位(48 字节)。
- SHA-512 生成的哈希值长度为 512 位(64 字节)。
SHA算法在信息安全领域有着广泛的应用,包括数据完整性校验、数字签名、消息认证等。例如,SHA算法常被用于对密码学散列进行数字签名、证书验证、SSL/TLS握手等场景中。需要注意的是,由于SHA-1算法存在安全漏洞,因此在许多应用中已经逐渐被更安全的SHA-256等算法所取代。
bcypt 算法
bcrypt是一种密码哈希函数,通常用于存储用户密码的安全散列。它使用加盐、多轮哈希等技术来增强密码的安全性,以防止常见的哈希攻击,如彩虹表攻击。
bcrypt的主要特点包括:
- 加盐: bcrypt在哈希计算过程中会随机生成一个盐(salt),并将盐与密码一起作为输入进行哈希计算。盐的引入增加了密码的复杂度,防止了针对相同密码的预先计算攻击。
- 多轮哈希: bcrypt使用多轮哈希函数(通常为12轮以上),使得每个密码都需要经过多次哈希计算才能生成最终的哈希值。这增加了破解密码的难度,即使使用了高性能硬件也难以快速破解。
- 可配置性: bcrypt允许调整成本因子(cost factor),以控制哈希计算的复杂度。通过增加成本因子,可以进一步增加哈希计算的复杂度,从而增强密码的安全性。
- 跨平台性: bcrypt算法的实现通常是跨平台的,可以在不同的操作系统和编程语言中使用。
由于上述特点,bcrypt被广泛应用于存储用户密码等安全敏感数据的场景中,如网站用户身份验证、数据库密码存储等。相比于一些传统的哈希算法(如MD5和SHA-1),bcrypt提供了更高的密码安全性,能够更有效地防止密码泄露和哈希攻击。
Diffie-Hellman 算法
Diffie-Hellman算法是一种用于密钥交换的密码学算法,可以让两个通信方在不安全的通信信道上协商出一个共享的密钥,用于加密进一步的通信内容。这种密钥交换方法是非对称加密的一种,意味着通信双方使用不同的密钥进行加密和解密。
Diffie-Hellman算法的基本思想是利用数论中的离散对数问题来实现密钥交换。算法的主要步骤如下:
- 初始化参数: 通信双方首先共同选择一个大素数p和一个原根g,这两个参数是公开的,但通信双方保密的。通常p是一个很大的素数,g是一个原根(即模p时生成了整个剩余系的数)。
- 生成公钥: 每个通信方选择一个私钥(通常表示为a和b),并利用公共参数p和g生成对应的公钥。计算公钥的方法是:A方计算公钥为 <math xmlns="http://www.w3.org/1998/Math/MathML"> A = g a m o d p A=g^a\ mod\ p </math>A=ga mod p,B方计算公钥为 <math xmlns="http://www.w3.org/1998/Math/MathML"> B = g b m o d p B=g^b\ mod\ p </math>B=gb mod p。
- 交换公钥: 通信双方将自己计算得到的公钥发送给对方。
- 计算共享密钥: 通信双方使用对方发送过来的公钥以及自己的私钥计算出共享的密钥。比如,A方使用B方的公钥B和自己的私钥a计算共享密钥 <math xmlns="http://www.w3.org/1998/Math/MathML"> K = B a m o d p K=B^a\ mod\ p </math>K=Ba mod p,而B方使用A方的公钥A和自己的私钥b计算共享密钥 <math xmlns="http://www.w3.org/1998/Math/MathML"> K = A b m o d p K=A^b\ mod\ p </math>K=Ab mod p。由于指数运算的交换性,最终双方计算出来的共享密钥是相同的。
Diffie-Hellman算法的关键点在于,即使通过网络传输的是公钥,但由于离散对数问题的困难性,攻击者无法从公钥中推导出私钥,因此无法计算出共享的密钥,保证了密钥交换的安全性。这种密钥交换方式被广泛应用于安全通信协议中,如SSL/TLS、SSH等。
RSA 算法
RSA算法的全称是"Rivest-Shamir-Adleman"算法,它是由Ron Rivest、Adi Shamir和Leonard Adleman于1977年共同提出的一种非对称加密算法。RSA算法以这三位作者的姓氏首字母命名。
SA算法是一种非对称加密算法,它基于数论中的大素数分解问题,被广泛用于数据加密和数字签名等安全通信领域。
RSA算法的基本原理如下:
- 密钥生成:
- 首先,选择两个不相等的大素数p和q,并计算它们的乘积N(即N = p * q)。
- 然后,计算欧拉函数φ(N) = (p-1) * (q-1)。
- 接着,选择一个整数e,满足1 < e < φ(N),且e与φ(N)互质(即e和φ(N)的最大公约数为1)。
- 最后,计算e的模反元素d,使得(e * d) mod φ(N) = 1。
- 加密:
- 加密时,将明文消息M转换为整数m,且满足0 ≤ m < N。
- 然后,使用公钥(e, N)对m进行加密,得到密文c,计算公式为: <math xmlns="http://www.w3.org/1998/Math/MathML"> c = m e m o d N c=m^e\ mod\ N </math>c=me mod N。
- 解密:
- 解密时,使用私钥(d, N)对密文c进行解密,得到原始消息m,计算公式为: <math xmlns="http://www.w3.org/1998/Math/MathML"> m = c d m o d N m=c^d\ mod\ N </math>m=cd mod N。
RSA算法的安全性基于大数分解的困难性,即在已知N的情况下,要从N中分解出p和q,以便计算出私钥d,是一种困难的数学问题。目前,RSA算法的安全性依赖于大素数的难以分解性,因此在实践中被广泛应用于加密通信、数字签名、身份认证等场景中。
需要注意的是,随着计算能力的增强和量子计算机等新技术的出现,传统的RSA算法可能会面临一些安全性挑战,因此研究者们正在积极探索量子安全的替代方案。
线性规划
线性规划(Linear Programming,LP)是一种数学优化方法,用于求解线性约束条件下的最优解问题。在线性规划中,目标函数和约束条件都是线性的,因此问题可以被描述为在多维空间中的一个多面体中寻找最大(或最小)值的问题。
线性规划问题的解可以通过各种算法求解,包括单纯形法、内点法、对偶法等。线性规划在各种领域都有广泛的应用,包括生产计划、资源分配、物流优化、金融投资、网络流量控制等。
所有的图算法都可以使用线性规划来实现。线性规划是一个宽泛得多的框架,图问题只是其中的一个子集。
Simplex算法是一种用于解决线性规划问题的常用算法,由美国数学家George Dantzig于1947年提出。Simplex算法通过在多维空间中移动从而找到目标函数在约束条件下的最优解。
后记
《算法图解》用简单易懂的语言和图解的方式介绍了一系列常见的算法和数据结构,是一本适合初学者入门的算法入门书籍,但对于想要深入学习算法的读者来说,可能需要配合其他更深入的参考资料。