《啊哈!算法》

Monday:

第1章 一大波数组正在靠近------排序

第1节 桶排序

简单的桶排序思想:假设有若干个按大小顺序编号的桶用来存放每个出现数据的次数、假设每个出现数据为小旗子,将每支小旗子插到对应的桶里,通过桶编号大小顺序对小旗子对应数据进行排序。
注意:数据范围为整数0-1000 需要1001个桶
用getchar()或system(“pause”);暂停程序查看输出
在忽略较小常数的合理近似下最后算法时间复杂度为O(M+N),其中M为桶的个数,N为小旗子的支数。
桶排序算法诞生于1956年。

第2节 冒泡排序

第1节中桶简单排序存在问题:1、浪费空间:需要申请最大数据范围的变量个数 2、无法处理小数
冒泡排序基本思想:每次比较两个相邻的元素,如果顺序错误就交换。
每一次冒泡循环称为一趟,冒泡排序的时间复杂度是O(N^2)
最早于1956年开始研究但似乎并没有很好的改进,时间复杂度非常高,适用场合有限。

第3节 快速排序

假设计算机每秒可运行10亿次,对1亿个数进行排序,桶排序只需要0.1秒,而冒泡排序需要1千万秒(115天)。
快速排序的思想:(基于二分思想)
在需要排序的数列中随便找一个数作为“基准数”(一般取两端),用两个变量从左右端开始向里搜索,左右两端搜索结果比较,小的放左边大的放右边(交换)直到这两个搜索变量相遇;这样就按基准数将整个数列分成了两部分(左边全小于基准数、右边全大于基准数);接着对两边分别进行类似的操作,直到穷尽。
快速排序相比于冒泡排序要快的原因是,每次交换是跳跃式的,不像冒泡排序相邻交换,交换距离大得多了,因此总的交换次数就少了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换,因此快速排序的时间复杂度最差是和冒泡排序是一样的,但它的平均时间复杂度是O(NlogN)。
快速排序由C.A.R.Hoare(1980年图灵奖获得者)在1960年提出,之后由有许多人做了进一步的优化。

第4节 算法实践—小哼买书问题

对比了相同问题下不同算法的巨大时间差距。

Tuesday:

第2章 栈、队列、链表

第1节 揭秘QQ号------队列

队列是一种特殊的线性结构,只允许在队列的首部(head)进行删除操作(称为”出队“),而在队列的尾部(tail)进行插入操作(称为“入队”),当队列中没有元素时(head=tail)称为空队列。例如排队买票,”先进先出“原则(First In First Out,FIFO)。
队列的三个基本元素(一个数组,两个变量)可以封装为一个结构体类型:
struct queue
{
int data[100];//队列的主体,用来存储内容
int head;//队首
int tail;//队尾
};
结构体类型通常子啊main函数外面定义(结构体定义末尾有一个;号)。struct是结构体的关键字,queue是结构体名,data、head、tail是这个结构体的三个成员。由结构体定义的新的数据类型可以同时存储结构体内部定义的成员个数那么多个的数据。
结构体变量定义:struct queue q;
q.head=1;
q.tail=1;
(.号称为成员运算符或点号运算符)

第2节 解密回文------栈

队列是先进先出的数据结构;
后进先出的数据结构叫做栈(例如吃桶装薯片,吃最后一片必须吃光之前的薯片)。栈可以用来检测回文(“aha”、“席主席”)、也可以用来进行验证括号的匹配。栈最早由Alan M.Turing(艾伦.图灵)于1946年提出。

第3节 纸牌游戏------小猫钓鱼

用两个队列和一个栈模拟整个游戏(打出的牌是栈、手牌是队列)。
book有记录、登记的意思,很多算法书籍用book标记问题(我们常用flag,没事就立flag)。

第4节 链表

在数列中插入数据需要依次操作插入点某一方向的全部数据平移,很浪费时间,链表就是解决这类问题的,通过在插入点并行的位置设置变量建立与相邻数据的三角关系(形似)。
C语言中可以通过指针和动态分配内存函数malloc来实现。
指针:用来存储一个内存空间的地址
int a;//定义整形变量a
int *p;//定义整形指针变量p *
p=&a;//将整形变量a的地址赋值给整形指针变量p(形象的理解为指针p指向了变量a)
&:取地址符
printf("%d",*p);//其中星号叫做间接访问运算符,用来获取指针p所指向的内存中的值。
C语言中星号有三哥用途:
1、乘号
2、声明指针变量(在定义指针变量时使用)
3、间接访问运算符(取得指针所指向的内存中的值)
在程序中存储除了int a;这种在内存中申请一块区域存储的方式,还有另外一种动态存储方法。
malloc(4);//申请4个字节的内存空间
malloc函数的作用是从内存中申请分配指定字节大小的内存空间
可以通过sizeof(int)这种方式获取类型所占用的字节数,例如malloc(sizeof(int));
从内存中申请了存储空间后怎么来对这个空间进行操作呢?
这里就需要用一个指针来指向这个空间,即存储这个空间的首地址。
另外,malloc函数的返回类型是void类型,在C、C++中void类型可以强制转换为任何其他类型的指针。
这么复杂的办法来存储数据的好处是,当你发布的程序定义了100的整型变量,但有一天需要存储1000个数,除了修改程序重新编译发布一个新版本这么麻烦的方法之外,可以通过malloc在程序运行的过程中根据实际情况来申请空间。
->:叫做结构体指针运算符,也是用来访问结构体内部成员的。
当struct node *p的时候,由于p是一个指针,所以不能使用.号访问内部成员,而要使用->。
如果不释放动态申请的空间,虽然不会报错,但是这样会很不安全,可以了解一下free命令。

第5节 模拟链表

用一个存储数据的数组和另一个存储前一个数组每个数据相邻右侧数据的位置的数组来模拟链表。

Wednesday:

第3章 枚举! 很暴力

第1节 坑爹的奥数

枚举=穷举
变量一一列举很麻烦,通过数组表示可以简化。

第2节 炸弹人

二维字符数组存储约定好的地图,按行列递增搜索可以消灭的敌人数量,求最大。

第3节 火柴棍模式

火柴棍拼成型如:A+B=C的形式,求在给定火柴棍数目的前提下等式成立的个数。枚举A、B、C的话算法的时间复杂度是O(N3),但是C可以通过A+B算出来,所以只枚举A、B的话算法的时间复杂度就降到了O(N2)。

第4节 数的全排列

求123的全排列可以通过三重循环嵌套就可以搞定,但是123456789的循环嵌套就很麻烦,在某些情况下处理数据时过度的循环嵌套是致命的。

Thursday:

第4章 万能的搜索

第1节 不撞南墙不回头------深度优先搜索

深度优先搜索(Depth First Search,DFS)的关键在于解决“当下如何做”,“下一步如何做”和当下该如何做是一样的(通过封装dfs()函数的递归调用来实现)

第2节 解救小哈

可以通过二维数组表示地图,另外建立一个规则数组(方向数组)构建dfs()函数。
深度优先算法的发明人是John E.Hopcroft和Bobert E. Tarjan1971~1972年在斯坦福研究图的连通性(任意两点是否可以相互到达)和平面性(图中所有的边相互不交叉)。电路板布线是平面型的一个实际应用。因为该算法两人获得了1986年图灵奖。
PS:深度优先算法是一种递归的思想,是搜索算法中的一种。我的理解这只是一种规则遍历算法,居然能获得图灵奖 emmm。。。。。。

第3节 层层递进------广度优先搜索

广度优先搜索(Breadth First Search,BFS)在1959年由Edward F.Moore率先在“如何从迷宫中寻找出路”这一问题中提出了广度优先算法。1961年C.Y.Lee在“电路板布线”这一问题中也独立提出了相同的算法。
所谓广度,就是一层一层的,向下遍历。

第4节 再解炸弹人

用广度优先算法实现第三章的问题

第5节 宝岛探险

Floodfill漫水填充法(种子填充法),在计算机图形学(CG)中有着广泛的运用,比如图像分割、物体识别等。另外Windows“画图”软件中的油漆桶工具、Photoshop中的魔术棒选择工具都是基于这个算法实现的。具体的算法是:查找种子周边的点,将与种子点颜色相近的点(可以设置一个阈值)入队作为新种子,并对新入队的种子也进行同样的扩展操作,这样就选取了与最初种子相近颜色的区域。

第6节 水管工游戏

玩游戏

Friday:

第5章 图的遍历

第1节 深度和广度优先究竟是指啥

图是由一些点和连接这些点的直线(边)组成的。假设有一个树状图,访问每个节点的时候用一个数表示这个节点是第几个被访问到的,这个数叫做“时间戳”。
深度优先遍历的主要思想就是:首先以一个未被访问过的节点作为起始节点,沿当前节点的边走到未访问过的顶点;当没有未访问过的节点时,则回到上一个节点,继续试探访问别的节点,直到所有的定点都被访问过。深度优先遍历是沿着图的某一条分支遍历直到末端,然后回溯,再沿河另一条进行同样的遍历,直到穷尽。
广度优先遍历的主要思想就是:首先以一个未被访问过的顶点作为起始顶点,访问其所有相邻的顶点,然后对每个相邻的顶点,再访问它们相邻的未被访问过的顶点,直到穷尽,遍历结束。

第2节 城市地图------图的深度优先遍历

如何表示正无穷:似乎用9999999999大数近似。
图分为有向图和无向图,如果图的每条边规定一个方向,那么得到的图称为有向图,其边称为有向边。在有向图中,与一个点相关联的边有出边和入边之分,而与一个有向边关联的两个点也有始点和终点之分。相反成为无向边。无向图存储在二维数表中的特征是对称。
本节用二维数组存储这个图(顶点和边的关系),这种存储方法叫做图的邻接矩阵表示法。存储图的方法还有很多种,比如邻接表等。求图上两点之间的最短路径,除了使用深度优先算法搜索以外,还可以使用广度优先搜索。

第3节 最少转机------图的广度优先遍历

p143 这里为什么一扩展到5号城市就结束了呢?为什么之前的深度优先搜索却不行呢?
个人理解:深度优先搜索需要包含不能到达终点的路径,而广度优先只要找到终点就能确定从始点到终点的路径。
但是如何判断是最短呢?
广度优先搜索更加适用于所有边的权值相同的情况。

Saturday:

第6章 最短路径

第1节 只有五行的算法------Floyd-Warshall

动态规划的思想
通常将正无穷定义为99999999,因为这样即使两个正无穷相加,其和仍超不过int类型的范围(C语言int类型可以存储的最大正整数是2147483647)。实际应用中最好估计一下数据范围,适当扩大一点就可以了。
该算法还是通过二维数组存储地图求路径最短,最终时间复杂度为O(N^2),在时间复杂度要求不高的时候该算法可行。另外,该算法可以处理带有负权边(边的值为负数)的图,但不能处理带有“负权回路”或者叫(“负权环“)的图。因为带有”负权回路“的图两点之间可能没有最短路径。
五行算法如下:
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j])
e[i][j]=e[i][k]+e[k][j];
此算法由Robert W.Floyd于1962年发表在Communications of the ACM上。同年Stephen Warshall也独立发表了这个算法。Robert W.Floyd这个牛人是朵奇葩,他原本在芝加哥大学读的文学,也是因为当时美国经济不太景气,找工作比较困难,无奈之下到西屋电器公司当了一名计算机操作员,在IBM650机房值夜班,并由此开始了他的计算机生涯。此外他还和J.W.J. Williams于1964年共同发明了著名的堆排序算法HEAPSORT,在1978年获得了图灵奖。

第2节 Dijkstra算法------单源最短路

dis数组存储1号顶点到其余各个顶点的初始路程,e[2][3]表示由2到3的路程,dis[3]biaoshi 1号顶点到3号顶点的路程,当dis[3]>dis[2]+e[2][3]的时候表示2到3的这条边”松弛”。这就是Dijkstra算法的主要思想:通过“边”来松弛1号顶点到其余各个顶点的路程,并将dis数组里的估计值更新为确定值。
算法的基本思想是:每次找到离源点(上面例子的源点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。基本步骤如下:
1、将所有的顶点分为两部分:
已知最短路程的顶点几何P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book数组来记录哪些点在集合P中。例如对于某个顶点i,如果book[i]为1表示这个顶点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。
2、设置源点s到自己的最短路径为0即dis[s]=0.若存在有源点能直接到达的顶点i,则把dis[i]设为e[s][j]。同时把所有其他(源点不能直接到达的)顶点的最短路径设为∞。
3、在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以点u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v]的值要小,我们可以用新值来替代当前dis[v]中的值。
4、重复第3步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。

该算法的时间复杂度为O(N2),其中每次找到离1号顶点最近的顶点的时间复杂度是O(N),这里我们可以用堆(见下一章)来优化,使得这一部分的时间复杂度降低到O(logN)。另外对于边数M少于N2的稀疏图来说(我们把M远小于N2的图称为稀疏图,而M相对较大的图成为稠密图),我们可以用邻接表来代替邻接矩阵,使得整个时间复杂度优化到O(M+N)logN。请注意!在最坏的情况下M就是N2,这样的话(M+N)logN要比N2还要大。但是大多数情况下并不会有那么多边,因此(M+N)logN要比N2小很多。
通过数组来实现邻接表,并没有使用真正的指针链表,这是一种在实际应用中非常容易实现的方法。用邻接表来存储图的时间空间复杂度是O(M),遍历每一条边的时间复杂度文视O(M)。如果一个图是稀疏图的话,M要远小于N^2。因此稀疏图选用邻接表来存储要比用邻接矩阵来存储好得多。
本节介绍的求最短路径的算法是一种基于贪心策略的算法。每次寻扩展一个路程最短的点,更新与其相邻的点的路程。当所有边权都为正时,由于不会存在一个路程更短的没扩展过的点,所以这个点的路程永远不会再改变,因而保证了算法的正确性。不过根据这个原理,用本算法求最短路径的图是不能有负权边的,因为扩展到负权边的时候会产生更短的路程,有可能就破坏了已经更新的点路程不会有改变的性质。
该算法是由荷兰计算机科学家Edsger Wybe Dijkstra于1959年提出的。其实早在1956年就发现了,当时他正与夫人在一家咖啡厅的阳台上晒太阳喝咖啡。因为当时没有专注于离散算法的专业期刊,直到1959年,他才把这个算法发表在Numerische Mathematik的创刊号上。

第3节 Bellman-Ford------解决负权边

该算法是在思想上和代码实现上都堪称完美的最短路算法,算法十分简单,核心代码只有4行并且可以完美解决带有负权边的图:
for(k=1;k<=n-1;k++)//进行n-1轮松弛
for(i=1;i<=m;i++)//枚举每一条边
if( dis[v[i]] > dis[u[i]] + w[i] )//尝试对每一条边进行松弛
dis[v[i]] = dis[u[i]] + w[i];
Bellman-Ford算法用一句话概括就是:对所有的边进行n-1次“松弛操作”。该算法的时间复杂度是O(NM),这个时间复杂度貌似比Dijkstra还要高,我们还可以对其进行优化。在实际操作中Bellman-Ford算法经常会在未达到n-1轮松弛前就已经计算出最短路,n-1是最大值。因此可以添加一个变量check来标记数组dis在本轮松弛中是否发生了变化,如果没有发生变化,则可以提前跳出循环。
美国应用数学家Richard Bellman于1958年发表了概算法。此外Lester Ford,Jr.在1956年也发表了该算法。因此这个算法叫做Bellman-Ford算法。其实Edward F. Moore在1957年也发表了同样的算法,所以这个算法也称为Bellman-Ford-Moore算法,后面这个家伙就是在“如何从迷宫中寻找出路”问题中提出了广度优先算法。

第4节 Bellman-Ford的队列优化

Bellman-Ford算法的另一种优化:每次仅对最短路程发生变化了的点的相邻边执行松弛操作。用队列优化的Bellman-Frod算法的关键之处在于:只有那些在前一边松弛中改变了最短路径估计值的顶点,才可能引起它们邻接点最短路程估计值发生改变。因此,用一个队列来存放被成功松弛的顶点,之后支队队列中的点进行处理,这就降低了算法的时间复杂度。
西南交通大学段丁凡在1994年发表的关于最短路径的SPFA快速算法(SPFA,Shortest Path Faster Algorithm),也是基于队列优化的Bellman-Ford算法的。

第5节 最短路径算法对比分析

P177图表
Floyd算法虽然总体时间复杂度高,但是可以处理带有负权边的图(但不能有负权回路)并且均摊到每一点对上,在所有的算法中还是属于较优的。另外,Floyd算法较小的编码复杂度也是它的一大优势。所以,如果要求的是所有点对间的最短路径,或者如果数据范围较小,则Floyd算法比较合适。Dijkstra算法最大的弊端是它无法处理带有负权边以及负权回路的图,但是Dijkstra算法的时间复杂度可以达到O(MlogN)。当边有负权,甚至cun在负权回路时,需要使用Bellman-Ford算法或者队列优化的Bellman-Ford算法。因此我们选择最短路径算法时,要根据实际需求和每一种算法的特性,选择适合的算法。

Sunday

第7章 神奇的树

第1节 开启“树”之旅

树其实就是不包含回路的连通无向图。P180
因为树有着“不包含回路”这个特点,所以树被赋予了很多特性。
1、一棵树中的任意两个节点有且仅有唯一的一条路径联通。
2、一棵树如果有n个结点,那么它一定恰好有n-1条边。
3、在一棵树中加一条边将会构成一个回路。
树这个特殊的数据结构在哪里会用到呢?例如:足球世界杯晋级图,家族的族谱图,公司的组织结构图,书的目录,操作系统中的目录(文件夹)都是一棵树。
这里定义:树是指任意两个结点之间有且仅有一条路径的无向图。或者说,只要是没有回路的连通无向图就是树。
为了确定一棵树的形态,在树中可以指定一个特殊的结点——根。我们子啊对一棵树进行讨论的时候,将树中的每个点成为结点(节点)。有一个根的树叫做有根树(这句好像是废话)。
根又叫做根节点,一棵树有且只有一个根节点。开叉的节点是父节点;如果一个结点没有子节点,那么这个结点称为叶结点;如果一个结点既不是根节点也不是叶结点,则称为内部结点。最后结点还有深度,是指从根到这个结点的层数(根为第一层)。

第2节 二叉树

二叉树是一种特殊的树。它的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根节点、左子树和右子树组成,二左子树和右子树分别是一棵二叉树。
二叉树的使用范围最广;二叉树中还有两种特殊的二叉树,叫做满二叉树和完全二叉树。如果二叉树中每个内部结点都已两个儿子就是满二叉树(或者说满二叉树所有的叶结点都有同样的深度)。满二叉树的严格定义是一棵深度为h且有(2^h-1)个结点的二叉树。如果一棵二叉树除了最右边位置上有一个或几个叶结点缺少以外,其他是丰满的,那么这样的二叉树就是完全二叉树。严格的定义是:若设二叉树的高度为h,除第h层外,其他各层(1~h-1)的结点数都达到最大个数,第h层从右向左连续缺若干结点,则这个二叉树就是完全二叉树(也就是说如果一个结点有右子节点,那么它也一定有左子节点)。其实可以将满二叉树理解成是一种特殊的或者机器完美的完全二叉树。
接下来就是二叉树真正的魅力所在了。完全二叉树中父亲和儿子有着神奇的规律,我们只需用一个一维数组就可以存储完全二叉树。首先按照从上到下,从左到右的顺序对二叉树结点进行编号。可以发现如果完全二叉树的一个父结点编号为k,那么它左儿子编号为2k,右儿子编号为2k+1;如果已知儿子(左或右)的编号是x,那么它父结点的编号就是x/2(这里只取商的证书部分)。在C语言中如果除号“/”两边都是整数的话,那么商也只有整数部分(自动向下取整),即4/2=5/2=2。另外如果一棵完全二叉树有N个结点,那么这个完全二叉树的高度为log2N,简写为logN,即最多有logN层结点。完全二叉树的最典型应用就是堆,见下节。

第3节 堆------神奇的优先队列

所有父结点都比子结点要小的二叉树成为最小堆,相反成为最大堆。
加入有14个数要找出其中最小的数,最简单的话就是将这14个数从头到尾依次扫一遍,时间复杂度是O(14),也就是O(N)。
如果删除最小的数后再添加一个新数,那么整个时间复杂度就是O(N^2)。更好的方法是用堆这个特殊的数据结构:首先按照最小堆的要求将这14个数放入完全二叉树,显然最小的数在堆顶。删除最小数把新加的数放在堆顶进行向下调整(选择一个较小的儿子与它进行交换,直到找到合适的位置为止,使其重新符合最小堆的特性)。这样仅进行3次比较就找出了重新调整后的最小数,时间复杂度为O(3),这恰好是O(log2 14)即O(log2N)。假如现在有1亿个数原来的方法需要运行1亿的平方次,现在只需要1亿*log1亿次,即27亿次。假设计算机每秒运算10亿次,原来方法需要115天,最小堆的方法只需要2.7秒(算法的伟大就是在数据量指数级增长的情况下极大的降低时间带价)。
如果只需要新增一个值,只需要直接将新元素插入到队尾,再根据情况判断新元素是否需要上移,知道满足堆特性为止(插入一个元素的时间复杂度为O(logN))。
说了半天最重要的问题其实是如何建立这个堆。可以从空的堆结构中依次往堆中插入每一个元素,直到所有数都被插入未知(这个过程的时间复杂度为O(NlogN))。还有一种更快的方法来建立堆,就是用一个一维数组存储完全二叉树,将所有数据按默认顺序放入完全二叉树中再进行调整直到满足完全二叉树的特性。
核心代码只有两行:
for(i=n/2;i>=1;i–)
siftdown(i);
用这种方法建立一个堆的时间复杂度是O(N)。
堆还有一个作用就是堆排序,与快速排序一样,堆排序的时间复杂度也是O(NlogN),堆排序的实现很简单,比如要从小到大进行排序,可以先建立最小堆,每次删除顶部元素输出或者放入一个新的数组中,直到堆空为止。
堆排序还有一种更好的方法。P197
最后总结一下。像这样支持插入元素和寻找最值元素的数据结构称为优先队列。如果使用普通队列来实现这两个功能,那么寻找最大元素需要枚举整个队列,这样的时间复杂度比较高。如果是已排序好的数组,那么插入一个元素则需要移动很多元素,时间复杂度依旧很高。而堆就是一种优先队列的实现,可以很好的解决种种操作。
如果求一个数列中第K小的数,只需要建立一个大小为K的最大堆,堆顶就是第K小的数,这种方法的时间复杂度是O(NlogK)。
堆排序算法是由J.W.J. Williams在1964年发明,同年由Robert W. Floyd提出了建立堆的线性时间算法。

第4节 擒贼先擒王------并查集

上一篇讲了树在优先队列中的应用------堆的实现。本篇用一个例子方便理解进行说明。
并查集也称为不相交集数据结构。此算法的发展经历了十多年,其中Robert E. Tarjan做出了很大的贡献。在此之前John E. Hopcroft 和Jeffrey D. Ullman也进行了大量的分析(发明深度优先搜索的两个人------1986年的图灵奖得主)。牛人从来都不闲着,他们到处交流,寻找合作伙伴,一起改变世界。

到此本周阅读挑战结束,还有本书其余两章略读一下。现在正考虑是否应该建立专栏学习数据结构。

猜你喜欢

转载自blog.csdn.net/qq_33838170/article/details/83739257
今日推荐