《算法图解》——记录阅读

一、大O表示法

1.1.常见的五种大O运行时间

O(log n),也叫对数时间,这样的算法包括二分查找。

O(n),也叫线性时间,这样的算法包括简单查找。

O(n * log n),这样的算法包括快速排序——一种速度较快的排序算法。

O(n^2),这样的算法包括选择排序——一种速度较慢的排序算法。

O(n!),这样的算法包括接下来的旅行商问题的解决方案——一种非常慢的算法。

1.2.启示

· 算法的速度指的并非时间,而是操作数的增速。

· 谈论算法的速度时,我们说的是随着输入的增加,其运行时间将以什么样的速度增加。

· 算法的运行时间用大O表示法表示。

· O(log n)比O(n)快,当需要搜索的元素越多时,前者比后者快得越多。

二、选择排序

2.1.数组和链表

链表的优势在插入和删除元素方面,数组的优势在可以随机地读取元素。

有一种混合结构:链表数组,数组中的元素时链表。

2.2.选择排序

简单的就是对数组进行排序,假如是从小到大,每一次找到最小的元素,然后数组中拿出来并添加到新的数组中,依次循环最终得到从小到大排序的数组,时间复杂度为O(n^2)。

三、递归

Leigh Caldwell:如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解。如何选择要看什么对你来说更重要。

3.1.基线条件和递归条件

基线条件是指函数不再调用自己,从而避免形成无限循环。

递归条件是指函数调用自己。

def countdown(i):
    prin i
    if i<=0:    # 基线条件
        return
    else:       # 递归条件 
        countdown(i-1)

3.2.栈

编程的一个重要概念——调用栈。栈是一种简单的数据结构:先进后出。

计算机在内部使用被称为调用栈的栈。实例:

def greet(name):
    print "hello"
    greet2(name)
    print “hello2”
    bye()

def greet2(name):
    print "greet hello"

def bye():
    print "OK bye"

在调用greet("me")时,计算机为该函数分配一块内存,将涉及的变量存储到内存中。当使用greet2()函数时,计算机使用一个栈来表示这些内存块,当greet2函数调用返回时,栈顶的内存块被弹开了也就是greet2函数被释放了。调用另一个函数时,当前函数暂停并处于未完成状态。接着继续调用bye函数,然后在返回,弹开栈顶内存。这个栈用于存储多个函数的变量,被称为调用栈

3.3.递归调用栈

def fact(x):
    if x == 1:
        return 1
    else:
        return x * fact(x-1)

当递归调用栈时,首先每次调用递归时都会在栈顶放一个内存块,不断累加放在栈顶,当触发基线条件时会不断return数值,这时会逐渐从栈顶释放内存块。

虽然栈很方便,但是存储详尽的信息可能占用大量的内存。如果栈很高,就意味着计算机存储了大量的函数调用信息。这种情况下有两种选择:

        (1)重新编写代码,使用循环。

        (2)使用尾递归。这是一个高级递归主题,暂时没学。

四、快速排序

4.1.D&C算法

快速排序/快速排序是一种排序算法,速度比选择排序快很多,实属优雅代码的典范。

使用D&C算法解决问题有两个步骤:

        (1)找出基线条件,这种条件必须尽可能简单。

        (2)不断将问题分解,知道附和基线条件。

4.2.快速排序

快速排序也使用了D&C算法,如果要对数组进行排序:

Step1.选取一个基准值,得到两个子数组,一个是比基准值小的元素组成的数组,一个是比基准值大的元素组成的数组。

Step2.只需要对这两个子数组进行快速排序。

// 快速排序代码
void quicksort(vector<int> vect)
{
    if(vect.size()<2)
    {
        return vect;
    }
    else{
        int pivot = vect[0];
        vector<int> less, greater;
        for(int i=0; i<vect.size(); i++)
        {
            if(vect[i] < pivot)
                less.push_back(vect[i]);
            else:
                greater.push_back(vect[i]);
        }
        return quicksort(less) + pivot + quicksort(greater);     
    }
}

4.3.再谈大O表示法

快速排序的平均时间为O(n log n),在最糟的情况下,其运行时间为O(n^2)

此外还有一种合并排序的排序算法,其运行时间总是O(n log n)。

在两个算法运行时间都为O(n log n)时,表面上看着是相等的,实际上是有快慢之分的。大O表示法中O(n)其实是这样的c*n,c是固定的时间量,也被称为常量。通常不考虑这个常量,如果两种算法的大O运行时间不同,这种常量将无关紧要,如果大O算法的运行时间相同,则需要考虑这个常量。

最佳情况也是平均情况,而最遭情况有可能是O(n^2)(如果平均是O(n log n))

五、散列表

* 学习散列表——最有用的基本数据结构之一。散列表用途广泛。

* 学习散列表的内部机制:实现、冲突、散列函数。

5.1.散列函数

散列函数就是一对一映射,x-->f(x),或者说是键值对。

* 散列函数总是将同样的输入映射到相同的索引。

* 散列函数将不同的输入映射到不同的索引。

* 散列函数知道数组有多大,只返回有效的索引。

结合散列函数和数组组成的被称为散列表的数据结构。散列表又称为散列映射、映射、字典和关联数组。

在python中是字典{key:value},在C++中是map<type1, type2> name;

5.2.应用案例

* 将散列表用于查找。

* 防止重复。

* 将散列表用作缓存。缓存是一种常用的加速方式,所有大型网站都使用缓存,而缓存的数据则存储在散列表中!

5.3.冲突

当将两个键值对存放到同一个位置时,会引起冲突。因此:

*** 散列函数很重要。最理想的情况是,散列函数将键均匀地映射到散列表的不同位置。

*** 如果散列表存储的链表很长,散列表的速度将急剧下降,然而,如果使用的散列函数很好,这些列表就不会很长。

5.4.性能

在平均情况下,散列表的的查找速度和数组一样快,而插入和删除速度和链表一样快,因此它兼具两者的优点。

在最糟情况下,散列表所有操作的运行时间都为O(n)——线性时间,这真的很慢。

因此在使用散列表时,避开最糟情况至关重要。为此,需要避免冲突。而要避免冲突,需要有:

        &&& 较低的填装因子。

        &&& 良好的散列函数。

散列表的填装因子很容易计算:散列表包含的元素数 / 位置总数。散列表使用数组来存储数据,因此你需要计算数组中被占用的位置数。

良好的散列函数让数组中的值呈均匀分布。糟糕的散列函数让值扎堆,导致大量的冲突。

5.5.小结

### 可以结合散列函数和数组来创建散列表。

### 冲突很糟糕,应使用可以最大限度减少冲突的散列函数。

### 散列表的查找、插入和删除速度都非常快。

### 散列表适用于模拟映射关系。

### 一旦填装因子超过0.7,就该调整散列表的长度。

### 散列表可用于缓存数据。

### 散列表非常适用于防止重复。

六、广度优先搜索

广度优先搜索可以找出两样东西之间的最短距离。

6.1.图介绍

解决最短路径问题的算法被称为广度优先搜索。要确定最短路径有两个步骤:

        (1)使用图来建立问题模型。

        (2)使用广度优先搜索解决问题。

什么是图?图模拟一组连接,图是由节点和边组成的。一个节点可能与众多节点直接相连,这些节点被称为邻居。

6.2.广度优先搜索

广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。

        *** 第一类问题:从节点A出发,有前往节点B的路径吗?

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

需要按照顺序依次检查,有一个可实现这种目的的数据结构——队列(queue)。

6.3.队列(queue)

队列的工作原理与现实生活中的队列完全相同。队列类似于栈,不能随机地访问队列中的元素。队列只支持两种操作:入队和出队。

如果将两个元素假如队列,先加入的元素将在后加入的元素之前出队。因此,可使用队列来表示查找名单。

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

6.4.实现算法

算法工作原理:

也可以使用双端队列(deque)。

简单概要:首先分梯度,你(也就是起点)的“朋友”是第一梯度,你朋友的朋友是第二梯度,依次类推。首先只把你的朋友放在queue里,循环queue如果找到终点则返回,如果在该朋友没找到就把该朋友的朋友放入queue中,一直迭代循环。注:如果某个人已经被循环过了就打上标签记录它已被循环过了,就不在对它进行循环,下次碰到它时直接跳过即可。

6.5.使用时间

使用了一个队列,其中包含检查每个节点。将一个节点添加到队列需要的时间是固定的,即为O(1),依次对每个人都这样做需要总时间为O(节点)。所以广度优先搜索的运行时间为O(节点+边数),通常写成O(V+E)。

6.6.总结

*** 需要按照加入顺序检查搜索列表中的节点,否则找到的就不是最短路径,因此搜索列表必须是队列。

*** 对于检查过的节点,务必不要再去检查,否则可能导致无限循环。

七、狄克斯特拉算法

通过广度优先搜索算法可以找到从A-->B的最短路径,但不一定是最快路径。如果给这个路段加上时间,你将发现有更快的路径。

如果需要找出最快路径,可以使用狄克斯特拉算法

7.1.使用狄克斯特拉算法

狄克斯特拉算法包含四个步骤:

        (1)找出“最便宜”的节点,即可在最短时间内到达的节点。

        (2)更新该节点的邻居的开销,其含义将稍后介绍。

        (3)重复这个过程,直到对图中的每个节点都这样做(该重复过程是除去已经前往的节点)。

        (4)计算最终路径。

                    

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

7.2.狄克斯特拉算法自述步骤

*1* 首先列出每个节点的开销。

*2* 进行不断循环更新节点开销。

*3* 找出最便宜的节点,然后计算前往该节点的各个邻居的开销。然后再次执行找出除第刚才那个最便宜的节点外最便宜的节点,再次计算前往该节点的各个邻居的开销依次类推。最终找到前往终点的时间最短的路径。

如果有负权边则不可使用狄克斯特拉算法

7.3.算法实现

int graph[m][n];   // 表示地图
int costs[m][n];    // 表示到某个节点的花费
int parents[m][n]; // 表示存储该节点的父节点

node = find_lowest_cost_node(costs); // 在未处理之前找到开销最小的节点

while(node != nullptr)
{
    neighbors = graph[node]; // 得到该节点的所有未遍历过的邻居
    for(auto i: neighbors)
    {
        new_cost = cost + neighbors[n];
        if(costs[n] > new_cost)
            costs[n] = new_cost; parents[n] = node; 
    }
    // 如果已经遍历过了就标记为处理过
    node find(lowest_cost_node(costs);
}

 7.3.总结

*** 广度优先搜索用于在非加权图中查找最短路径。

*** 狄克斯特拉算法用于在加权图中查找最短路径。

*** 仅当权重为正时狄克斯特拉算法才管用。

*** 如果图中包含负权边,可使用贝尔曼-福德算法

八、贪婪算法

对于那些没有快速算法的问题(NP完全问题),可使用贪婪算法。学习近似算法,使它们可快速找到NP完全问题的近似解;学习贪婪策略——一种非常简单的问题解决策略。

贪婪算法很简单:就是每步都采取最优的做法。

8.1.NP 完全问题

为解决集合覆盖问题,你必须计算每个可能的集合。

NP完全问题的简单定义是,以难解著称的问题,如旅行商问题和集合覆盖问题。很多聪明的人都认为,根本不可能编写出可快速解决这些问题的算法。

如何识别NP完全问题呢?要判断问题是不是NP完全问题很难,易于解决的问题和NP完全问题的差别通常很小。

没办法判断问题是不是NP完全问题,但有一些蛛丝马迹可循的。

        &&& 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。

        &&& 涉及“所有组合”的问题通常是NP问题。

        &&& 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。

        &&& 如果问题涉及序列且难以解决,它可能就是NP完全问题。

        &&& 如果问题涉及集合且难以解决,可能就是NP完全问题。

        &&& 如果问题可转化为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。

8.2.总结

        *** 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。

        *** 对于NP完全问题,还没有找到快速解决方案。

        *** 面临NP完全问题时,最佳的做法是使用近似算法。

        *** 贪婪算法易于实现、运行速度快,是不错的近似算法。

九、动态规划

学习动态规划,这是一种解决棘手问题的方法,它将问题分成小问题,并先着手解决这些小问题。

9.1.最长公共子串和最长公共子序列

&&& 动态规划可帮助在给定约束条件下找到最优解,

&&& 在问题可分解问彼此独立且离散的子问题时,就可适用动态规划解决。

&&& 每种动态规划解决方案都涉及网格。

如果你不知道该使用什么样的公式。就使用费曼算法

        (1)将问题写下来。

        (2)好好思考。

        (3)将答案写下来。

    

9.2.总结

*** 需要在给定约束条件下优化某种指标时,动态规划很有用。

*** 问题可分解为离散子问题时,可使用动态规划来解决。

*** 每种动态规划解决方案都涉及网格。

*** 单元格中的值通常就是你要优化的值。

*** 每个单元格都是一个子问题,因此你需要考虑如何将问题分解为子问题。

*** 没有放之四海而皆准的计算动态规划解决方案的公式。

十、K最近邻算法(KNN)

K最近邻算法能够进行创建分类系统、学习特征抽取、学习回归等。

* 创建分类系统就是编组。依靠特征提取,根据系统参数导入数学模型中,计算得出模型的各个参数。

* 回归就是预测结果。余弦相似度能够在数据相似时解决使用距离公式不能解决的问题。

10.1.特征选取

* 不偏不倚的特征。

* 要与分类参数紧密相关的特征。

10.2.总结

这章后续讲解的是机器学习相关的知识,由于已经学过了就不做过多解释。

十一、接下来如何做

本章将介绍10种算法以及它们的用处。

11.1.树

二分查找仅在数组有序才有用。因此出现了一种名为二叉查找树的数据结构。

                                                

在二叉查找树种查找节点时,平均运行时间为O(log n),但在最糟情况虚啊所以需要时间为O(n);而在有序数组种查找时,在最糟情况下所需时间也只有O(log n),因此在查找方面有序数组比二叉查找树更佳。但在插入和删除操作要快得多。

                                                   

11.2.反向索引

搜索引擎的工作原理:假如有三个网页

                                       

我们根据这些内容创建一个散列表。这个表的键为单词,值为包含指定单词的页面。

假设用户搜索hi,在这种情况下,搜索引擎需要检查哪些页面包含hi,搜索引擎发现页面A和B包含hi,将这些页面作为搜索结果呈现给用户。

这种就是一种很有用的数据结构:一个散列表,将单词映射到它的页面。这种被称为反向索引,常用于创建搜索引擎

11.3.傅里叶变换

绝妙、优雅且应用广泛的算法少之又少,傅里叶就是一个。

傅里叶变换的一个绝佳的比喻:“给它一杯冰沙,它能告诉你其中包含哪些成分”。换言之,给定一首歌,傅里叶变换能将其中的各种频率分离出来。

除此之外,傅里叶变换还用来作地震预测和DNA分析,傅里叶变换的用途极其广泛。

11.4.并行算法

为提高算法的速度,需要让它们能够在多个内核中并行地执行。

并行算法设计卡里很难,因为不能保证实现期望的速度运行和提升。有一点是确定的,那就是速度的提升并非线性的,而这其中的原因有两个。

        (1)并行性管理开销。加入要对一个包含1000个元素的数组进行排序,如何在两个内核之间分配这项任务呢?如果让每个内核对500个元素进行排序,再将两个排好序的数组合并成一个数组,那么合并也是需要时间的。

        (2)负载均衡。假设需要完成10个任务,因此你给每个内核都分配5个任务。但分配给内核A的任务都很容易,10s就完成了,而分配给内核B的任务都很难,1分钟才完成。这意味着有50s左右,内核B在忙死忙活,而内核A却闲得很!究竟该如何均匀分配工作呢?

要改善性能和可扩展性,并行算法可能是不错的选择。

11.5.MapReduce

分布式算法是一种特殊的并行算法。在并行算法只需两到四个内核时,完全可以在笔记本电脑上运行它,但如果需要数百个内核呢?这种情况下,可让算法在多台计算机上运行。可在开源工具Apache Hadoop使用MapReduce——一种流行的分布式算法。

MapReduce基于两个简单的理念:映射(map)函数归并(reduce)函数

映射函数很简单,接受一个数组,并对其中的每个元素执行同样的处理。

               

归并函数的理念在于将很多项归并为一项。映射是将一个数组转换为另一个数组,而归并是将一个数组转换为一个元素。

                                                             

MapReduce使用这两个简单概念在多台计算机上执行数据查询。数据集很大,包含数十亿行时,使用MapReduce只需几分钟就可获得查询结果,而传统数据集可能要耗费数小时。

11.6.布隆过滤器和HyperLogLog

布隆过滤器是一种概率型数据结构,它提供的答案有可能不对,但很可能是正确的。当判断网页以前是否已搜集过,可不用散列表,而使用,布隆过滤器。使用散列表时,答案绝对可靠,而使用布隆过滤器时,答案却是很可能是正确的。

        (1)可能出现的报错的情况:Google可能指出“这个网站已搜集”,但实际上并没有搜集。

        (2)不可能出现漏报的情况:如果布隆过滤器说“这个网站未搜集”,就肯定未搜集。

布隆过滤器的优点在于占用的存储空间很少。

HyperLogLog是一种类似于布隆过滤器的算法。HyperLogLog近似地计算集合中不同的元素数,与布隆过滤器一样,它不能给出准确的答案,但也八九不离十,而占用的内存空间却少得多。

面临海量数据且只要求答案八九不离十时,可考虑使用概率型算法!

11.7.SHA算法

SHA算法是另一种散列函数是安全散列算法函数。给定一个字符串,SHA返回其散列值。SHA是一个散列函数,它生成一个散列值——一个较短的字符串。

其他的散列表生成的是数组索引,这只能进行搜寻,当需要判断连个文件是否相同时很难做到。但可以用SHA来判断这两个文件是否相同,这在比较超大型文件时很有用。可直接计算它们的SHA散列值,再进行结果对比。这样就不需要把原有的超大型文件进行传输,只需要传输SHA散列值文件即可。

此外SHA还让你能在不知道原始字符串的情况下对其进行比较。这样就防止了文件泄漏等问题。

最后SHA实际上是一系列算法:SHA-0、SHA-1、SHA-2和SHA-3。最安全的密码散列函数是bcrypt,但没有任何一种东西是完美的!

11.8.局部敏感的散列算法

SHA有局部不敏感一个重要特征,当修改其中一个字符时,在计算其散列值,结果截然不同,这使得攻击者无法通过比较散列值是否类似来破解密码。

但有时候,你希望结果相反,即希望散列函数是局部敏感的。这种情况下可以使用Simhash。如果你对字符串做细微的修改,Simhash生成的散列值也只存在细微的差别。这让你能够通过比较散列值来判读两个字符串的相似程度,这很有用!

11.9.Diffie-Hellman 密钥交换

Diffie-Hellman算法以优雅的方式解决了一个古老的问题:如何对消息进行加密,以便只有收件人才能看懂呢?

Diffie-Hellman算法解决了两个问题:

        (1)双方无需知道加密算法。它们不必会面协商要使用的加密算法。

        (2)要破解加密的消息比登天还难。

Diffie-Hellman使用两个密钥:公钥和私钥。有人向你发送消息时,他使用公钥对其进行加密,加密后的消息只有使用私钥才能解密。只要只有你知道私钥,就只有你才能解密消息。

Diffie-Hellman算法及其替代者RSA依然被广泛使用。

11.10.线性规划

线性规划用于给定约束条件下最大限度地改善指定的指标。

这本书很大篇幅在讨论最优化问题,虽然表面上与线性规划无关,但所有的图算法都可使用线性规划来实现!!!

十二、总结

花费了几天看完了这本书,收获颇多,入门算法了,再接再厉我们一起加油!愿今年能找到理想的工作!!!

  • 40
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值