程序员编程艺术系列之经典算法研究


十三个经典算法研究与总结、目录+索引 作者:July 时间:二零一零年十二月末------------二零一一年四月初。 邮箱:zhoulei0907@yahoo.cn 微博:http://weibo.com/julyweibo 出处:http://blog.csdn.net/v_JULY_v 声明:版权所有,侵权定究。 前言: 本人的原创作品,经典算法研究系列,自从去年十二月末至今,已写了近四个月,而本 人开博还不到半年。可以这么说,开博头俩个月一直在整理微软等公司的面试题,而后的四 个月至今,则断断续续,在写此经典算法研究系列。 本经典算法研究系列,如今已写了 22 篇,13 个算法,包括算法理论的研究,算法编程 的实现,很多个算法都后续写了续集,如第二个算法:Dijkstra 算法,便写了 4 篇文章。而 红黑树系列,则更是最后写了 6 篇文章,成为了国内最为经典的红黑树教程。 此过程中,费了不少精力和心血。如在文章的排版上,总是不断的调整,文章内容,也 是不断的修正,尤其在一些算法的实现上,则更显复杂与艰难了,如红黑树的 c,c++实现, sift 算法的步步 c 实现等等。而仅此,还远远不够。虽然,我无愧于心:这些个算法,个人 认为是网上写的最好的,但还是有个别如 KMP 算法,还亟待完善。 不过,个人会继续写下去,同时,本 BLOG 内的此经典算法研究系列,永久更新,永久 维护。估计,最后会写将近 100 篇算法文章。 应众多网友强烈要求,同时也是为了各位以后看着方便,以下是已经写了的十三个算法 集锦,算是一个目录+索引,共计二十二篇文章。任何人有任何问题,欢迎留言评论,或批 评指正。谢谢。以下是十三个经典算法的目录及内容: 十三个经典算法集锦 一、A*搜索算法 一(续)、A*,Dijkstra,BFS 算法性能比较及 A*算法的应用 二、Dijkstra 算法初探 二(续)、彻底理解 Dijkstra 算法 二(再续)、Dijkstra 算法+fibonacci 堆的逐步 c 实现 二(三续)、Dijkstra 算法+Heap 堆的完整 c 实现源码 三、dynamic programming 四、BFS 和 DFS 优先搜索算法 五、红黑树算法的实现与剖析 五(续)、教你透彻了解红黑树 六、教你从头到尾彻底理解 KMP 算法 七、遗传算法 透析 GA 本质 八、再谈启发式搜索算法 九、图像特征提取与匹配之 SIFT 算法 九(续)、sift 算法的编译与实现 九(再续)、教你一步一步用 c 语言实现 sift 算法、上九(再续)、教你一步一步用 c 语言实 现 sift 算法、下 十、从头到尾彻底理解傅里叶变换算法、上 十、从头到尾彻底理解傅里叶变换算法、下 十一、从头到尾彻底解析 Hash 表算法 十二、快速排序算法之所有版本的 c/c++实现 十三、通过浙大上机复试试题学 SPFA 算法 后记 自从本人写这个算法系列以来,总有不少的朋友问我如何学算法,问我怎么会有那么多 的时间来学算法,在此,我愿回复各位俩句话:1、兴趣。2、没有兴趣的东西一般不会占 用我的时间。 非常感谢,各位对我的支持与关注,谢谢大家。完。 版权声明:本人及 CSDN 对本 BLOG 内的此经典算法研究系列,享有全部的版权。 任何人,任何组织或网站转载,必须以超链接形式注明出处。 任何出版社未经本人书面许可,不得出版本博客内任何文章及内容。谢谢。 July、二零一一年四月六日。 一、A*搜索算法 作者:July、二零一一年一月 --------------------------------------------------------------------------------------------------------------------------------- 博主说明: 1、本经典算法研究系列,此系列文章写的不够好之处,还望见谅。 2、本经典算法研究系列,系我参考资料,一篇一篇原创所作,转载必须注明作者本人 July 及出处。 3、本经典算法研究系列,精益求精,不断优化,永久更新,永久勘误。 欢迎,各位,与我一同学习探讨,交流研究。 有误之处,不吝指正。 --------------------------------------------------------------------------------------------------------------------------------- 引言 1968 年,的一篇论文,“P. E. Hart, N. J. Nilsson, and B. Raphael. A formal basis for the heuristic determination of minimum cost paths in graphs. IEEE Trans. Syst. Sci. and Cybernetics, SSC-4(2):100-107, 1968”。从此,一种精巧、高效的算法------A*算法横空出世了,并在相关领 域得到了广泛的应用。 启发式搜索算法 要理解 A*搜寻算法,还得从启发式搜索算法开始谈起。 所谓启发式搜索,就在于当前搜索结点往下选择下一步结点时,可以通过一个启发函数 来进行选择,选择代价最少的结点作为下一步搜索结点而跳转其上(遇到有一个以上代价最 少的结点,不妨选距离当前搜索点最近一次展开的搜索点进行下一步搜索)。 DFS 和 BFS 在展开子结点时均属于盲目型搜索,也就是说,它不会选择哪个结点在下一 次搜索中更优而去跳转到该结点进行下一步的搜索。在运气不好的情形中,均需要试探完整 个解集空间, 显然,只能适用于问题规模不大的搜索问题中。 而与 DFS,BFS 不同的是,一个经过仔细设计的启发函数,往往在很快的时间内就可得到 一个搜索问题的最优解,对于 NP 问题,亦可在多项式时间内得到一个较优解。是的,关键 就是如何设计这个启发函数。 A*搜寻算法 A*搜寻算法,俗称 A 星算法,作为启发式搜索算法中的一种,这是一种在图形平面上, 有多个节点的路径,求出最低通过成本的算法。常用于游戏中的 NPC 的移动计算,或线上 游戏的 BOT 的移动计算上。该算法像 Dijkstra 算法一样,可以找到一条最短路径;也像 BFS 一样,进行启发式的搜索。 A*算法最为核心的部分,就在于它的一个估值函数的设计上: f(n)=g(n)+h(n) 其中 f(n)是每个可能试探点的估值,它有两部分组成: 一部分,为 g(n),它表示从起始搜索点到当前点的代价(通常用某结点在搜索树中的深 度来表示)。 另一部分,即 h(n),它表示启发式搜索中最为重要的一部分,即当前结点到目标结点的 估值,h(n)设计的好坏,直接影响着具有此种启发式函数的启发式算法的是否能称为 A*算法。 一种具有 f(n)=g(n)+h(n)策略的启发式算法能成为 A*算法的充分条件是: 1、搜索树上存在着从起始点到终了点的最优路径。 2、问题域是有限的。 3、所有结点的子结点的搜索代价值>0。 4、h(n)=V5 的路径,V0->V5 的过程中,可以经由 V1,V2,V3, V4 各点达到目的点 V5。下面的问题,即是求此起始顶点 V0->途径任意顶点 V1、V2、V3、 V4->目标顶点 V5 的最短路径。 //是的,图片是引用 rickone 的。 通过上图,我们可以看出::A*算法最为核心的过程,就在每次选择下一个当前搜索点 时,是从所有已探知的但未搜索过点中(可能是不同层,亦可不在同一条支路上),选取 f 值 最小的结点进行展开。 而所有“已探知的但未搜索过点”可以通过一个按 f 值升序的队列(即优先队列)进行排 列。 这样,在整体的搜索过程中,只要按照类似广度优先的算法框架,从优先队列中弹出队 首元素(f 值),对其可能子结点计算 g、h 和 f 值,直到优先队列为空(无解)或找到终止点为 止。 A*算法与广度、深度优先和 Dijkstra 算法的联系就在于:当 g(n)=0 时,该算法类似于 DFS,当 h(n)=0 时,该算法类似于 BFS。且同时,如果 h(n)为 0,只需求出 g(n),即求出起点 到任意顶点 n 的最短路径,则转化为单源最短路径问题,即 Dijkstra 算法。这一点,可以通 过上面的 A*搜索树的具体过程中将 h(n)设为 0 或将 g(n)设为 0 而得到。 A*算法流程: 首先将起始结点 S 放入 OPEN 表,CLOSE 表置空,算法开始时: 1、如果 OPEN 表不为空,从表头取一个结点 n,如果为空算法失败。 2、n 是目标解吗?是,找到一个解(继续寻找,或终止算法)。 3、将 n 的所有后继结点展开,就是从 n 可以直接关联的结点(子结点),如果不在 CLOSE 表中,就将它们放入 OPEN 表,并把 S 放入 CLOSE 表,同时计算每一个后继结点的估价值 f(n), 将 OPEN 表按 f(x)排序,最小的放在表头,重复算法,回到 1。 //OPEN-->CLOSE,起点-->任意顶点 g(n)-->目标顶点 h(n) closedset := the empty set //已经被估算的节点集合 openset := set containing the initial node //将要被估算的节点集合 g_score[start] := 0 //g(n) h_score[start] := heuristic_estimate_of_distance(start, goal) //h(n) f_score[start] := h_score[start] while openset is not empty //若 OPEN 表不为空 x := the node in openset having the lowest f_score[] value //x 为 OPEN 表中最小的 if x = goal //如果 x 是一个解 return reconstruct_path(came_from,goal) // remove x from openset add x to closedset //x 放入 CLSOE 表 for each y in neighbor_nodes(x) if y in closedset continue tentative_g_score := g_score[x] + dist_between(x,y) if y not in openset add y to openset tentative_is_better := true else if tentative_g_score < g_score[y] tentative_is_better := true else tentative_is_better := false if tentative_is_better = true came_from[y] := x g_score[y] := tentative_g_score h_score[y] := heuristic_estimate_of_distance(y, goal) //x-->y-->goal f_score[y] := g_score[y] + h_score[y] return failure function reconstruct_path(came_from,current_node) if came_from[current_node] is set p = reconstruct_path(came_from,came_from[current_node]) return (p + current_node) else return the empty path 与结点写在一起的数值表示那个结点的价值 f(n),当 OPEN 表为空时 CLOSE 表中将求得 从 V0 到其它所有结点的最短路径。 考虑到算法性能,外循环中每次从 OPEN 表取一个元素,共取了 n 次(共 n 个结点), 每次展开一个结点的后续结点时,需 O(n)次,同时再对 OPEN 表做一次排序,OPEN 表大小 是 O(n)量级的,若用快排就是 O(nlogn),乘以外循环总的复杂度是 O(n^2 * logn), 如果每次不是对 OPEN 表进行排序,因为总是不断地有新的结点添加进来,所以不用进 行排序,而是每次从 OPEN 表中求一个最小的,那只需要 O(n)的复杂度,所以总的复杂度为 O(n*n),这相当于 Dijkstra 算法。 本文完。 July、二零一一年二月十日更新。 --------------------------------------------------------------------------------------------------------------------------------- 后续:July、二零一一年三月一日更新。 简述 A*最短路径算法的方法: 目标:从当前位置 A 到目标位置 B 找到一条最短的行走路径。 方法: 从 A 点开始,遍历所有的可走路径,记录到一个结构中,记录内容为(位置点,最小 步数) 当任何第二次走到一个点的时候,判断最小步骤是否小于记录的内容,如果是,则更新 掉原最小步数,一直到所有的路径点都不能继续都了为止,最终那个点被标注的最小步数既 是最短路径, 而反向找跟它相连的步数相继少一个值的点连起来就形成了最短路径,当多个点相同, 则任意取一条即可。 总结: A*算法实际是个穷举算法,也与课本上教的最短路径算法类似。课本上教的是两头往 中间走,也是所有路径都走一次,每一个点标注最短值。(i am sorry。文章,写的差,恳请 各位读者参考此 A*搜寻算法的后续一篇文章:一(续)、A*,Dijkstra,BFS 算法性能比较及 A*算法的应用。谢谢大家。)本文完。 一(续)、A*,Dijkstra,BFS 算法性能比较及 A*算法的应用 --------------------------------------------------------------------------------------------------------------------------------- 作者:July 二零一一年三月十日。 出处:http://blog.csdn.net/v_JULY_v --------------------------------------------------------------------------------------------------------------------------------- 引言: 最短路径的各路算法 A*算法、Dijkstra 算法、BFS 算法,都已在本 BLOG 内有所阐述了。 其中,Dijkstra 算法,后又写了一篇文章继续阐述:二(续)、理解 Dijkstra 算法。但,想必, 还是有部分读者对此类最短路径算法,并非已了然于胸,或者,无一个总体大概的印象。 本文,即以演示图的形式,比较它们各自的寻路过程,让各位对它们有一个清晰而直观 的印象。 我们比较,以下五种算法: 1. A* (使用曼哈顿距离) 2. A* (采用欧氏距离) 3. A* (利用切比雪夫距离) 4. Dijkstra 5. Bi-Directional Breadth-First-Search(双向广度优先搜索) 咱们以下图为例,图上绿色方块代表起始点,红色方块代表目标点,紫色的方块代表障 碍物,白色的方块代表可以通行的路径。 下面,咱们随意摆放起始点绿块,目标点红块的位置,然后,在它们中间随便画一些障 碍物, 最后,运行程序,比较使用上述五种算法,得到各自不同的路径,各自找寻过程中所覆 盖的范围,各自的工作流程,并从中可以窥见它们的效率高低。 A*、Dijkstra、BFS 算法性能比较演示: ok,任意摆放绿块与红块的三种状态: 一、起始点绿块,与目标点红块在同一条水平线上: 各自的搜寻路径为: 1. A*(使用曼哈顿距离) 2. A*(采用欧氏距离) 3. A*(利用切比雪夫距离) 4. Dijkstra 算法. //很明显,Dijkstra 搜寻效率明显差于上述 A* 算法。(看它最后 找到目标点红块所走过的路径,和覆盖的范围,即能轻易看出来,下面的比较,也是基 于同一个道理。看路径,看覆盖的范围,评价一个算法的效率)。 5. Bi-Directional Breadth-First-Search(双向广度优先搜索) 二、起始点绿块,目标点红块在一斜线上: 各自的搜寻路径为: 1. A*(使用曼哈顿距离) 2. A*(采用欧氏距离) 3. A*(利用切比雪夫距离) 4. Dijkstra 算法。 //与上述 A* 算法比较,覆盖范围大,搜寻效率较低 5. Bi-Directional Breadth-First-Search(双向广度优先搜索) 三、起始点绿块,目标点红块被多重障碍物阻挡: 各自的搜寻路径为(同样,还是从绿块到红块): 1. A* (使用曼哈顿距离) 2. A*(采用欧氏距离) 3. A*(利用切比雪夫距离) 4. Dijkstra 5. Bi-Directional Breadth-First-Search(双向广度优先搜索) //覆盖范围同上述 Dijkstra 算法一样很大,效率低下。 A*搜寻算法的高效之处 如上,是不是对 A*、Dijkstra、双向 BFS 算法各自的性能有了个总体大概的印象列?由上 述演示,我们可以看出,在最短路径搜寻效率上,一般有 A*>Dijkstra、双向 BFS,其中 Dijkstra、 双向 BFS 到底哪个算法更优,还得看具体情况。 由上,我们也可以看出,A*搜寻算法的确是一种比较高效的寻路算法。 A*算法最为核心的过程,就在每次选择下一个当前搜索点时,是从所有已探知的但未 搜索过点中(可能是不同层,亦可不在同一条支路上),选取 f 值最小的结点进行展开。 而所有“已探知的但未搜索过点”可以通过一个按 f 值升序的队列(即优先队列)进行排 列。 这样,在整体的搜索过程中,只要按照类似广度优先的算法框架,从优先队列中弹出队 首元素(f 值),对其可能子结点计算 g、h 和 f 值,直到优先队列为空(无解)或找到终止点为 止。 A*算法与广度、深度优先和 Dijkstra 算法的联系就在于:当 g(n)=0 时,该算法类似于 DFS,当 h(n)=0 时,该算法类似于 BFS。且同时,如果 h(n)为 0,只需求出 g(n),即求出起点 到任意顶点 n 的最短路径,则转化为单源最短路径问题,即 Dijkstra 算法。这一点,可以通 过上面的 A*搜索树的具体过程中将 h(n)设为 0 或将 g(n)设为 0 而得到。 BFS、DFS 与 A*搜寻算法的比较 参考了算法驿站上的部分内容: 不管以下论述哪一种搜索,都统一用这样的形式表示:搜索的对象是一个图,它面向一 个问题,不一定有明确的存储形式,但它里面的一个结点都有可能是一个解(可行解),搜 索的目的有两个方面,或者求可行解,或者从可行解集中求最优解。 我们用两张表来进行搜索,一个叫 OPEN 表,表示那些已经展开但还没有访问的结点集, 另一个叫 CLOSE 表,表示那些已经访问的结点集。 蛮力搜索(BFS,DFS) BFS(Breadth-First-Search 宽度优先搜索) 首先将起始结点放入 OPEN 表,CLOSE 表置空,算法开始时: 1、如果 OPEN 表不为空,从表中开始取一个结点 S,如果为空算法失败 2、S 是目标解吗?是,找到一个解(继续寻找,或终止算法);不是到 3 3、将 S 的所有后继结点展开,就是从 S 可以直接关联的结点(子结点),如果不在 CLOSE 表中,就将它们放入 OPEN 表末尾,而把 S 放入 CLOSE 表,重复算法到 1。 DFS(Depth-First-Search 深度优先搜索) 首先将起始结点放入 OPEN 表,CLOSE 表置空,算法开始时: 1、如果 OPEN 表不为空,从表中开始取一个结点 S,如果为空算法失败 2、S 是目标解吗?是,找到一个解(继续寻找,或终止算法);不是到 3 3、将 S 的所有后继结点展开,就是从 S 可以直接关联的结点(子结点),如果不在 CLOSE 表中,就将它们放入 OPEN 表开始,而把 S 放入 CLOSE 表,重复算法到 1。 是否有看出:上述的 BFS 和 DFS 有什么不同? 仔细观察 OPEN 表中待访问的结点的组织形式,BFS 是从表头取结点,从表尾添加结点, 也就是说 OPEN 表是一个队列,是的,BFS 首先让你想到‘队列’;而 DFS,它是从 OPEN 表 头取结点,也从表头添加结点,也就是说 OPEN 表是一个栈! DFS 用到了栈,所以有一个很好的实现方法,那就是递归,系统栈是计算机程序中极重 要的部分之一。用递归也有个好处就是,在系统栈中只需要存结点最大深度那么大的空间, 也就是在展开一个结点的后续结点时可以不用一次全部展开,用一些环境变量记录当前的状 态,在递归调用结束后继续展开。 利用系统栈实现的 DFS 函数 dfs(结点 s) { s 超过最大深度了吗?是:相应处理,返回; s 是目标结点吗?是:相应处理;否则: { s 放入 CLOSE 表; for(c=s.第一个子结点 ;c 不为空 ;c=c.下一个子结点() ) if(c 不在 CLOSE 表中) dfs(c);递归 } } 如果指定最大搜索深度为 n,那系统栈最多使用 n 个单位,它相当于有状态指示的 OPEN 表,状态就是 c,在栈里存了前面搜索时的中间变量 c,在后面的递归结束后,c 继续后移。 在象棋等棋类程序中,就是用这样的 DFS 的基本模式搜索棋局局面树的,因为如果用 OPEN 表,有可能还没完成搜索 OPEN 表就暴满了,这是难于控制的情况。 我们说 DFS 和 BFS 都是蛮力搜索,因为它们在搜索到一个结点时,在展开它的后续结点 时,是对它们没有任何‘认识’的,它认为它的孩子们都是一样的‘优秀’,但事实并非如 此,后续结点是有好有坏的。好,就是说它离目标结点‘近’,如果优先处理它,就会更快 的找到目标结点,从而整体上提高搜索性能。 启发式搜索 为了改善上面的算法,我们需要对展开后续结点时对子结点有所了解,这里需要一个估 值函数,估值函数就是评价函数,它用来评价子结点的好坏,因为准确评价是不可能的,所 以称为估值。打个比方,估值函数就像一台显微镜,一双‘慧眼’,它能分辨出看上去一样 的孩子们的手,哪个很脏,有细菌,哪个没有,很干净,然后对那些干净的孩子进行奖励。 这里相当于是需要‘排序’,排序是要有代价的,而花时间做这样的工作会不会对整体搜索 效率有所帮助呢,这完全取决于估值函数。 排序,怎么排?用哪一个?快排吧,qsort!不一定,要看要排多少结点,如果很少, 简单排序法就很 OK 了。看具体情况了。 排序可能是对 OPEN 表整体进行排序,也可以是对后续展开的子结点排序,排序的目的 就是要使程序有启发性,更快的搜出目标解。 如果估值函数只考虑结点的某种性能上的价值,而不考虑深度,比较有名的就是有序搜 索(Ordered-Search),它着重看好能否找出解,而不看解离起始结点的距离(深度)。 如果估值函数考虑了深度,或者是带权距离(从起始结点到目标结点的距离加权和), 那就是 A*,举个问题例子,八数码问题,如果不考虑深度,就是说不要求最少步数,移动 一步就相当于向后多展开一层结点,深度多算一层,如果要求最少步数,那就需要用 A*。 简单的来说 A*就是将估值函数分成两个部分,一个部分是路径价值,另一个部分是一 般性启发价值,合在一起算估整个结点的价值。 从 A*的角度看前面的搜索方法,如果路径价值为 0 就是有序搜索,如果路径价值就用 所在结点到起始结点的距离(深度)表示,而启发值为 0,那就是 BFS 或者 DFS,它们两刚 好是个反的,BFS 是从 OPEN 表中选一个深度最小的进行展开, 而 DFS 是从 OPEN 表中选一个深度最大的进行展开。当然只有 BFS 才算是特殊的 A*, 所以 BFS 可以求要求路径最短的问题,只是没有任何启发性。 下文稍后,会具体谈 A*搜寻 算法思想。 BFS、DFS、Kruskal、Prim、Dijkstra 算法时间复杂度 上面,既然提到了 A*算法与广度、深度优先搜索算法的联系,那么,下面,也顺便再 比较下 BFS、DFS、Kruskal、Prim、Dijkstra 算法时间复杂度吧: 一般说来,我们知道,BFS,DFS 算法的时间复杂度为 O(V+E), 最小生成树算法 Kruskal、Prim 算法的时间复杂度为 O(E*lgV)。 而 Prim 算法若采用斐波那契堆实现的话,算法时间复杂度为 O(E+V*lgV),当|V|<<|E| 时,E+V*lgV 是一个较大的改进。 //|V|<<|E|,=>O(E+V*lgV) << O(E*lgV),对吧。:D Dijkstra 算法,斐波纳契堆用作优先队列时,算法时间复杂度为 O(V*lgV + E)。 //看到了吧,与 Prim 算法采用斐波那契堆实现时,的算法时间复杂度是一样的。 所以我们,说,BFS、Prime、Dijkstra 算法是有相似之处的,单从各算法的时间复杂度 比较看,就可窥之一二。 A*搜寻算法的思想 ok,既然,A*搜寻算法作为是一种好的、高效的寻路算法,咱们就来想办法实现它吧。 实现一个算法,首先得明确它的算法思想,以及算法的步骤与流程,从我之前的一篇文 章中,可以了解到: A*算法,作为启发式算法中很重要的一种,被广泛应用在最优路径求解和一些策略设 计的问题中。 而 A*算法最为核心的部分,就在于它的一个估值函数的设计上: f(n)=g(n)+h(n) 其中 f(n)是每个可能试探点的估值,它有两部分组成: 一部分,为 g(n),它表示从起始搜索点到当前点的代价(通常用某结点在搜索树中的深 度来表示)。 另一部分,即 h(n),它表示启发式搜索中最为重要的一部分,即当前结点到目标结点的 估值, h(n)设计的好坏,直接影响着具有此种启发式函数的启发式算法的是否能称为 A*算法。 一种具有 f(n)=g(n)+h(n)策略的启发式算法能成为 A*算法的充分条件是: 1、搜索树上存在着从起始点到终了点的最优路径。 2、问题域是有限的。 3、所有结点的子结点的搜索代价值>0。 4、h(n)=next,min = (*Open)->next,minp = (*Open); Node * minx; while(temp->next != NULL) { if((temp->next ->npoint->f) < (min->npoint->f)) { min = temp->next; minp = temp; } temp = temp->next; } minx = min->npoint; temp = minp->next; minp->next = minp->next->next; free(temp); return minx; } //判断是否可解 int Canslove(Node * suc, Node * goal) { int a = 0,b = 0,i,j; for(i = 1; i< 9;i++) for(j = 0;j < i;j++) { if((suc->data[i] > suc->data[j]) && suc->data[j] != 0) a++; if((goal->data[i] > goal->data[j]) && goal->data[j] != 0) b++; } if(a%2 == b%2) return 1; else return 0; } //判断节点是否相等 ,1 相等,0 不相等 int Equal(Node * suc,Node * goal) { for(int i = 0; i < 9; i ++ ) if(suc->data[i] != goal->data[i])return 0; return 1; } //判断节点是否属于 OPEN 表 或 CLOSED 表,是则返回节点地址,否则返回空地址 Node * Belong(Node * suc,Lstack * list) { Lstack temp = (*list) -> next ; if(temp == NULL)return NULL; while(temp != NULL) { if(Equal(suc,temp->npoint))return temp -> npoint; temp = temp->next; } return NULL; } //把节点放入 OPEN 或 CLOSED 表中 void Putinto(Node * suc,Lstack * list) { Stack * temp; temp =(Stack *) malloc(sizeof(Stack)); temp->npoint = suc; temp->next = (*list)->next; (*list)->next = temp; } ///////////////计算 f 值部分-开始////////////////////////////// double Fvalue(Node suc, Node goal, float speed) {//计算 f 值 double Distance(Node,Node,int); double h = 0; for(int i = 1; i <= 8; i++) h = h + Distance(suc, goal, i); return h*speed + suc.g; //f = h + g(speed 值增加时搜索过程以找到目标为优先因此可能 不会返 回最优解) } double Distance(Node suc, Node goal, int i) {//计算方格的错位距离 int k,h1,h2; for(k = 0; k < 9; k++) { if(suc.data[k] == i)h1 = k; if(goal.data[k] == i)h2 = k; } return double(fabs(h1/3 - h2/3) + fabs(h1%3 - h2%3)); } ///////////////计算 f 值部分-结束////////////////////////////// ///////////////////////扩展后继节点部分的函数-开始///////////////// int BelongProgram(Lnode * suc ,Lstack * Open ,Lstack * Closed ,Node goal ,float speed) {//判断子节点是否属于 OPEN 或 CLOSED 表 并作出相应的处理 Node * temp = NULL; int flag = 0; if((Belong(*suc,Open) != NULL) || (Belong(*suc,Closed) != NULL)) { if(Belong(*suc,Open) != NULL) temp = Belong(*suc,Open); else temp = Belong(*suc,Closed); if(((*suc)->g) < (temp->g)) { temp->parent = (*suc)->parent; temp->g = (*suc)->g; temp->f = (*suc)->f; flag = 1; } } else { Putinto(* suc, Open); (*suc)->f = Fvalue(**suc, goal, speed); } return flag; } void Spread(Lnode * suc, Lstack * Open, Lstack * Closed, Node goal, float speed) {//扩展后继节点总函数 int i; Node * child; for(i = 0; i < 4; i++) { if(Canspread(**suc, i+1)) //判断某个方向上的子节点可否扩展 { child = (Node *) malloc(sizeof(Node)); //扩展子节点 child->g = (*suc)->g +1; //算子节点的 g 值 child->parent = (*suc); //子节点父指针指向父节点 Spreadchild(child, i); //向该方向移动空格生成子节点 if(BelongProgram(&child, Open, Closed, goal, speed)) // 判断子节点是否属 于 OPEN 或 CLOSED 表 并作出相应的处理 free(child); } } } ///////////////////////扩展后继节点部分的函数-结束////////////////////////////////// Node * Process(Lnode * org, Lnode * goal, Lstack * Open, Lstack * Closed, float speed) {//总执行函数 while(1) { if((*Open)->next == NULL)return NULL; //判断 OPEN 表是否为空,为空则失败退出 Node * minf = Minf(Open); //从 OPEN 表中取出 f 值最小的节点 Putinto(minf, Closed); //将节点放入 CLOSED 表中 if(Equal(minf, *goal))return minf; //如果当前节点是目标节点,则成功退出 Spread(&minf, Open, Closed, **goal, speed); //当前节点不是目标节点时扩展 当前节点的后继 节点 } } int Shownum(Node * result) {//递归显示从初始状态到达目标状态的移动方法 if(result == NULL)return 0; else { int n = Shownum(result->parent); for(int i = 0; i < 3; i++) { printf("\n"); for(int j = 0; j < 3; j++) { if(result->data[i*3+j] != 0) printf(" %d ",result->data[i*3+j]); else printf(" "); } } printf("\n"); return n+1; } } 后记: 日后,本 BLOG 将陆续实现所有经典的算法。是为记。完。 --------------------------------------------------------------------------------------------------------------------------------- 版权声明: 1、本人对本 BLOG 内所有任何文章和资料享有版权,转载,请注明作者本人,并以链 接形式注明出处。 2、侵犯本人版权相关利益者,个人会在新浪微博、CSDN 迷你博客中永久追踪,给予谴 责。 同时,保留追究法律责任的权利。向您的厚道致敬,谢谢。 July、二零一一年三月十日。 二、Dijkstra 算法初探 July 二零一一年一月 =============================================================================== --------------------------------------------------------------------------------------- 本文主要参考:算法导论 第二版、维基百科。 写的不好之处,还望见谅。 本经典算法研究系列文章,永久勘误,永久更新、永久维护。 July、二零一一年二月十日更新。 ---------------------------------------------------------------------------------------- 一、Dijkstra 算法的介绍 Dijkstra 算法,又叫迪科斯彻算法(Dijkstra), 算法解决的是有向图中单个源点到其他顶点的最短路径问题。 举例来说, 如果图中的顶点表示城市,而边上的权重表示著城市间开车行经的距离, Dijkstra 算法可以用来找到两个城市之间的最短路径。 二、Dijkstra 的算法实现 Dijkstra 算法的输入包含了一个有权重的有向图 G,以及 G 中的一个来源顶点 S。 我们以 V 表示 G 中所有顶点的集合,以 E 表示 G 中所有边的集合。 (u, v) 表示从顶点 u 到 v 有路径相连,而边的权重则由权重函数 w: E → *0, ∞+ 定义。 因此,w(u, v) 就是从顶点 u 到顶点 v 的非负花费值(cost),边的花费可以想像成两 个顶点之间的距离。 任两点间路径的花费值,就是该路径上所有边的花费值总和。 已知有 V 中有顶点 s 及 t,Dijkstra 算法可以找到 s 到 t 的最低花费路径(例如,最 短路径)。 这个算法也可以在一个图中,找到从一个顶点 s 到任何其他顶点的最短路径。 好,咱们来看下此算法的具体实现: Dijkstra 算法的实现一(维基百科): u := Extract_Min(Q) 在顶点集合 Q 中搜索有最小的 d[u] 值的顶点 u。这个顶点被 从集合 Q 中删除并返回给用户。 1 function Dijkstra(G, w, s) 2 for each vertex v in V[G] // 初始化 3 d[v] := infinity 4 previous[v] := undefined 5 d[s] := 0 6 S := empty set 7 Q := set of all vertices 8 while Q is not an empty set // Dijkstra 演算法主體 9 u := Extract_Min(Q) 10 S := S union {u} 11 for each edge (u,v) outgoing from u 12 if d[v] > d[u] + w(u,v) // 拓展边(u,v) 13 d[v] := d[u] + w(u,v) 14 previous[v] := u 如果我们只对在 s 和 t 之间寻找一条最短路径的话,我们可以在第 9 行添加条件如果 满足 u = t 的话终止程序。 现在我们可以通过迭代来回溯出 s 到 t 的最短路径 1 s := empty sequence 2 u := t 3 while defined u 4 insert u to the beginning of S 5 u := previous[u] 现在序列 S 就是从 s 到 t 的最短路径的顶点集. Dijkstra 算法的实现二(算法导论): DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) 2 S ← Ø 3 Q ← V[G] //V*O(1) 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //EXTRACT-MIN,V*O(V),V*O(lgV) 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //松弛技术,E*O(1),E*O(lgV)。 因为 Dijkstra 算法总是在 V-S 中选择―最轻‖或―最近‖的顶点插入到集合 S 中,所以我 们说它使用了贪心策略。 (贪心算法会在日后的博文中详细阐述)。 ================================================ 二零一一年二月九日更新: 此 Dijkstra 算法的最初的时间复杂度为 O(V*V+E),源点可达的话,O(V*lgV+E*lgV) =>O(E*lgV) 当是稀疏图的情况时,E=V*V/lgV,算法的时间复杂度可为 O(V^2)。 但我们知道,若是斐波那契堆实现优先队列的话,算法时间复杂度,则为 O(V*lgV + E)。 三、Dijkstra 算法的执行速度 我们可以用大 O 符号将 Dijkstra 算法的运行时间表示为边数 m 和顶点数 n 的函数。 Dijkstra 算法最简单的实现方法是用一个链表或者数组来存储所有顶点的集合 Q, 所以搜索 Q 中最小元素的运算(Extract-Min(Q))只需要线性搜索 Q 中的所有元素。 这样的话算法的运行时间是 O(E^2)。 对于边数少于 E^2 的稀疏图来说,我们可以用邻接表来更有效的实现迪科斯彻算法。 同时需要将一个二叉堆或者斐波纳契堆用作优先队列来寻找最小的顶点(Extract-Min)。 当用到二叉堆时候,算法所需的时间为 O(( V+E )logE), 斐波纳契堆能稍微提高一些性能,让算法运行时间达到 O(V+ElogE)。(此处一月十六日 修正。) 开放最短路径优先(OSPF, Open Shortest Path First)算法是迪科斯彻算法在网络路由 中的一个具体实现。 与 Dijkstra 算法不同,Bellman-Ford 算法可用于具有负数权值边的图,只要图中不存在 总花费为负值且从源点 s 可达的环路即可用此算法(如果有这样的环路,则最短路径不存 在,因为沿环路循环多次即可无限制的降低总花费)。 与最短路径问题相关最有名的一个问题是旅行商问题(Traveling salesman problem),此 类问题要求找出恰好通过所有标点一次且最终回到原点的最短路径。 然而该问题为 NP-完全的;换言之,与最短路径问题不同,旅行商问题不太可能具有多 项式时间解法。 如果有已知信息可用来估计某一点到目标点的距离,则可改用 A*搜寻算法,以减小最 短路径的搜索范围。 二零一一年二月九日更新: BFS、DFS、Kruskal、Prim、Dijkstra 算法时间复杂度的比较: 一般说来,我们知道,BFS,DFS 算法的时间复杂度为 O(V+E), 最小生成树算法 Kruskal、Prim 算法的时间复杂度为 O(E*lgV)。 而 Prim 算法若采用斐波那契堆实现的话,算法时间复杂度为 O(E+V*lgV),当|V|<<|E| 时,E+V*lgV 是一个较大的改进。 //|V|<<|E|,=>O(E+V*lgV) << O(E*lgV),对吧。:D Dijkstra 算法,斐波纳契堆用作优先队列时,算法时间复杂度为 O(V*lgV + E)。 //看到了吧,与 Prim 算法采用斐波那契堆实现时,的算法时间复杂度是一样的。 所以我们,说,BFS、Prime、Dijkstra 算法是有相似之处的,单从各算法的时间复杂度 比较看,就可窥之一二。 四、图文解析 Dijkstra 算法 ok,经过上文有点繁杂的信息,你还并不对此算法了如指掌,清晰透彻。 没关系,咱们来幅图,就好了。请允许我再对此算法的概念阐述下, Dijkstra 算法是典型最短路径算法,用于计算一个节点到其他所有节点的最短路径。 不过,针对的是非负值权边。 主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。 [Dijkstra 算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。] ok,请看下图: 如下图,设 A 为源点,求 A 到其他各所有一一顶点(B、C、D、E、F)的最短路径。线 上所标注为相邻线段之间的距离,即权值。 (注:此图为随意所画,其相邻顶点间的距离与图中的目视长度不能一一对等) Dijkstra 无向图 算法执行步骤如下表: 是不是对此 Dijkstra 算法有不错的了解了。那么,此文也完了。:D。 ----July、2010 年 12 月 24 日。平安夜。 ============================================================================== 此文,写的实在不怎么样。不过,承蒙大家厚爱,此经典算法研究系列的后续文章,个 人觉得写的还行。 所以,还请,各位可关注此算法系列的后续文章。谢谢。 二零一一年一月四日。 二之续、彻底理解 Dijkstra 算法 作者:July 二零一一年二月十三日。 参考代码:introduction to algorithms,Second Edition。 -------------------------------------------------------------------------------------- 了解什么是 Dijkstra 算法,请参考: 经典算法研究系列:二、Dijkstra 算法初探 http://blog.csdn.net/v_JULY_v/archive/2010/12/24/6096981.aspx 本文由单源最短路径路径问题开始,而后描述 Bellman-Ford 算法,到具体阐述 Dijkstra 算法,阐述详细剖析 Dijkstra 算法的每一个步骤,教你彻底理解此 Dijkstra 算法。 一、单源最短路径问题 我们知道,单源最短路径问题:已知图 G=(V,E),要求找出从某个定源顶点 s<-V,到 每个 v<-V 的最短路径。 简单来说,就是一个图 G 中,找到一个定点 s,然后以 s 为起点,要求找出 s 到图 G 中 其余各个点的最短距离或路径。 此单源最短路径问题有以下几个变形: I、 单终点最短路径问题: 每个顶点 v 到指定终点 t 的最短路径问题。即单源最短路径问题的相对问题。 II、 单对顶点最短路径问题: 给定顶点 u 和 v,找出从 u 到 v 的一条最短路径。 III、每对顶点间最短路径问题: 针对任意每俩个顶点 u 和 v,找出从 u 到 v 的最短路径。 最简单的想法是,将每个顶点作为源点,运行一次单源算法即可以解决这个问题。 当然,还有更好的办法,日后在本 BOlG 内阐述。 二、Bellman-Ford 算法 1、回路问题 一条最短路径不能包含负权回路,也不能包含正权回路。 一些最短路径的算法,如 Dijkstra 算法,要求图中所有的边的权值都是非负的,如在 公路地图上,找一条从定点 s 到目的顶点 v 的最短路径问题。 2、Bellman-Ford 算法 而 Bellman-Ford 算法,则允许输入图中存在负权边,只要不存在从源点可达的负权回 路,即可。 简单的说,图中可以存在负权边,但此条负权边,构不成负权回路,不影响回路的形成。 且,Bellman-Ford 算法本身,便是可判断图中是否存在从源点可达的负权回路, 若存在负权回路,算法返回 FALSE,若不存在,返回 TRUE。 Bellman-Ford 算法的具体描述 BELLMAN-FORD(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) //对每个顶点初始化 ,O(V) 2 for i ← 1 to |V[G]| - 1 3 do for each edge (u, v) ∈ E[G] 4 do RELAX(u, v, w) //针对每个顶点(V-1 个),都运用松弛技术 O(E),计 为 O((v-1)*E)) 5 for each edge (u, v) ∈ E[G] 6 do if d[v] > d[u] + w(u, v) 7 then return FALSE //检测图中每条边,判断是否包含负权回路, //若 d[v]>d[u]+w(u,v),则表示包含,返回 FALSE, 8 return TRUE //不包含负权回路,返回 TRUE Bellman-Ford 算法的时间复杂度,由上可得为 O(V*E)。 3、关于判断图中是否包含负权回路的问题: 根据定理,我们假定,u 是 v 的父辈,或父母,那么 当 G(V,E)是一个有向图或无向图(且不包含任何负权回路),s<-V,s 为 G 的任意一个顶点, 则对任意边(u,v)<-V,有 d[s,v] <= d[s,u]+1 此定理的详细证明,可参考算法导论一书上,第 22 章中引理 22.1 的证明。 或者根据第 24 章中通过三角不等式论证 Bellman-Ford 算法的正确性,也可得出上述 定理的变形。 即假设图 G 中不包含负权回路,可证得 d[v]=$(s,u) <=$(s,u)+w(u,v) //根据三角不等式 =d[u]+w[u,v] 所以,在不包含负权回路的图中,是可以得出 d[v]<=d[u]+w(u,v)。 于是,就不难理解,在上述 Bellman-Ford 算法中, if d[v] > d[u]+w(u,v),=> 包含负权回路,返回 FASLE else if =>不包含负权回路,返回 TRUE。 ok,咱们,接下来,立马切入 Dijkstra 算法。 三、深入浅出,彻底解剖 Dijkstra 算法 I、松弛技术 RELAX 的介绍 Dijkstra 算法使用了松弛技术,对每个顶点 v<-V,都设置一个属性 d[v],用来描述 从源点 s 到 v 的最短路径上权值的上界,称为最短路径的估计。 首先,得用 O(V)的时间,来对最短路径的估计,和对前驱进行初始化工作。 INITIALIZE-SINGLE-SOURCE(G, s) 1 for each vertex v ∈ V[G] 2 do d[v] ← ∞ 3 π[v] ← NIL //O(V) 4 d[s] 0 RELAX(u, v, w) 1 if d[v] > d[u] + w(u, v) 2 then d[v] ← d[u] + w(u, v) 3 π[v] ← u //O(E) 图。 II、Dijkstra 算法 此 Dijkstra 算法分三个步骤, INSERT (第 3 行), EXTRACT-MIN (第 5 行), 和 DECREASE-KEY(第 8 行的 RELAX, 调用此减小关键字的操作)。 DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) //对每个顶点初始化 ,O(V) 2 S ← Ø 3 Q ← V[G] //INSERT,O(1) 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //简单的 O(V*V);二叉/项堆,和 FIB-HEAP 的话,则都为 O(V*lgV)。 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //简单方式:O(E),二叉/项堆,E*O(lgV), FIB-HEAP,E*O(1)。 四、Dijkstra 算法的运行时间 在继续阐述之前,得先声明一个问题,DIJKSTRA(G,w,s)算法中的第 5 行, EXTRACT-MIN(Q),最小优先队列的具体实现。 而 Dijkstra 算法的运行时间,则与此最小优先队列的采取何种具体实现,有关。 最小优先队列三种实现方法: 1、利用从 1 至|V| 编好号的顶点,简单地将每一个 d[v]存入一个数组中对应的第 v 项, 如上述 DIJKSTRA(G,w,s)所示,Dijkstra 算法的运行时间为 O(V^2+E)。 2、如果是二叉/项堆实现最小优先队列的话,EXTRACT-MIN(Q)的运行时间为 O(V*lgV), 所以,Dijkstra 算法的运行时间为 O(V*lgV+E*lgV), 若所有顶点都是从源点可达的话,O((V+E)*lgV)=O(E*lgV)。 当是稀疏图时,则 E=O(V^2/lgV),此 Dijkstra 算法的运行时间为 O(V^2)。 3、采用斐波那契堆实现最小优先队列的话,EXTRACT-MIN(Q)的运行时间为 O(V*lgV), 所以,此 Dijkstra 算法的运行时间即为 O(V*lgV+E)。 综上所述,此最小优先队列的三种实现方法比较如下: EXTRACT-MIN + RELAX I、 简单方式: O(V*V + E*1) II、 二叉/项堆: O(V*lgV + |E|*lgV) 源点可达:O(E*lgV) 稀疏图时,有 E=o(V^2/lgV), => O(V^2) III、斐波那契堆:O(V*lgV + E) 当|V|<<|E|时,采用 DIJKSTRA(G,w,s)+ FIB-HEAP-EXTRACT-MIN(Q), 即斐波那契堆实现最小优先队列的话,优势就体现出来了。 五、Dijkstra 算法 + FIB-HEAP-EXTRACT-MIN(H),斐波那契堆实现最 小优先队列 由以上内容,我们已经知道,用斐波那契堆来实现最小优先队列,可以将运行时间提升 到 O(VlgV+E)。 |V|个 EXTRACT-MIN 操作,每个平摊代价为 O(lgV), |E|个 DECREASE-KEY 操作的每个 平摊时间为 O(1)。 下面,重点阐述 DIJKSTRA(G, w, s)中,斐波那契堆实现最小优先队列的操作。 由上,我们已经知道,DIJKSTRA 算法包含以下的三个步骤: INSERT (第 3 行), EXTRACT-MIN (第 5 行), 和 DECREASE-KEY(第 8 行的 RELAX)。 先直接给出 Dijkstra 算法 + FIB-HEAP-EXTRACT-MIN(H)的算法: DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) 2 S ← Ø 3 Q ← V[G] //第 3 行,INSERT 操作,O(1) 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //第 5 行,EXTRACT-MIN 操作,V*lgV 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //第 8 行,RELAX 操作,E*O(1) FIB-HEAP-EXTRACT-MIN(H) //平摊代价为 O(lgV) 1 z ← min[H] 2 if z ≠ NIL 3 then for each child x of z 4 do add x to the root list of H 5 p[x] ← NIL 6 remove z from the root list of H 7 if z = right[z] 8 then min[H] ← NIL 9 else min[H] ← right[z] 10 CONSOLIDATE(H) 11 n[H] ← n[H] - 1 12 return z 六、Dijkstra 算法 +fibonacci 堆各项步骤的具体分析 ok,接下来,具体分步骤阐述以上各个操作: 第 3 行的 INSERT 操作: FIB-HEAP-INSERT(H, x) //平摊代价,O(1). 1 degree[x] ← 0 2 p[x] ← NIL 3 child[x] ← NIL 4 left[x] ← x 5 right[x] ← x 6 mark[x] ← FALSE 7 concatenate the root list containing x with root list H 8 if min[H] = NIL or key[x] < key[min[H]] 9 then min[H] ← x 10 n[H] ← n[H] + 1 第 5 行的 EXTRACT-MIN 操作: FIB-HEAP-EXTRACT-MIN(H) //平摊代价为 O(lgV) 1 z ← min[H] 2 if z ≠ NIL 3 then for each child x of z 4 do add x to the root list of H 5 p[x] ← NIL 6 remove z from the root list of H 7 if z = right[z] 8 then min[H] ← NIL 9 else min[H] ← right[z] 10 CONSOLIDATE(H) //CONSOLIDATE 算法在下面,给出。 11 n[H] ← n[H] - 1 12 return z 下图是 FIB-HEAP-EXTRACT-MIN 的过程示意图: CONSOLIDATE(H) 1 for i ← 0 to D(n[H]) 2 do A[i] ← NIL 3 for each node w in the root list of H 4 do x ← w 5 d ← degree[x] //子女数 6 while A[d] ≠ NIL 7 do y ← A[d] 8 if key[x] > key[y] 9 then exchange x <-> y 10 FIB-HEAP-LINK(H, y, x) //下面给出。 11 A[d] ← NIL 12 d ← d + 1 13 A[d] ← x 14 min[H] ← NIL 15 for i ← 0 to D(n[H]) 16 do if A[i] ≠ NIL 17 then add A[i] to the root list of H 18 if min[H] = NIL or key[A[i]] < key[min[H]] 19 then min[H] ← A[i] FIB-HEAP-LINK(H, y, x) //y 链接至 x。 1 remove y from the root list of H 2 make y a child of x, incrementing degree[x] 3 mark[y] ← FALSE 第 8 行的 RELAX 的操作,已上已经给出: RELAX(u, v, w) 1 if d[v] > d[u] + w(u, v) 2 then d[v] ← d[u] + w(u, v) 3 π[v] ← u //O(E) 一般来说,在 Dijkstra 算法中,DECREASE-KEY 的调用次数远多于 EXTRACT-MIN 的调用,所以在不增加 EXTRACT-MIN 操作的平摊时间前提下,尽量减小 DECREASE-KEY 操作的平摊时间,都能获得对比二叉堆更快的实现。 以下,是二叉堆,二项堆,斐波那契堆的各项操作的时间复杂度的比较: 操作 二叉堆(最坏) 二项堆(最坏) 斐波那契堆(平摊) ____________________________________________________ MAKE-HEAP Θ(1) Θ(1) Θ(1) INSERT Θ(lg n) O(lg n) Θ(1) MINIMUM Θ(1) O(lg n) Θ(1) EXTRACT-MIN Θ(lg n) Θ(lg n) O(lg n) UNION Θ(n) O(lg n) Θ(1) DECREASE-KEY Θ(lg n) Θ(lg n) Θ(1) DELETE Θ(lg n) Θ(lg n) O(lg n) 斐波那契堆,日后会在本 BLOG 内,更进一步的深入与具体阐述。 且同时,此文,会不断的加深与扩展。 完。 本人 July 对本博客所有任何文章、内容和资料享有版权。 转载务必注明作者本人及出处,并通知本人。谢谢。 July、二零一一年二月十三日。 二之再续、Dijkstra 算法+fibonacci 堆 的逐步 c 实现 作者:JULY、二零一一年三月十八日 出处:http://blog.csdn.net/v_JULY_v ---------------------------------------------------------------------------------- 引言: 来考虑一个问题, 平面上 6 个点,A,B,C,D,E,F,假定已知其中一些点之间的距离, 现在,要求 A 到其它 5 个点,B,C,D,E,F 各点的最短距离。 如下图所示: 经过上图,我们可以轻而易举的得到 A->B,C,D,E,F 各点的最短距离: 目的 路径 最短距离 A=>A, A->A 0 A=>B, A->C->B 3+2=5 A=>C, A->C 3 A=>D, A->C->D 3+3=6 A=>E, A->C->E 3+4=7 A=>F, A->C->D->F 3+3+3=9 我想,如果是单单出上述一道填空题,要你答出 A->B,C,D,E,F 各点的最短距离, 一个小学生,掰掰手指,也能在几分钟之内,填写出来。 我们的问题,当然不是这么简单,上述只是一个具体化的例子而已。 实际上,很多的问题,如求图的最短路径问题,就要用到上述方法,不断比较、不断寻找, 以期找到最短距离的路径,此类问题,便是 Dijkstra 算法的应用了。当然,还有 BFS 算 法,以及更高效的 A*搜寻算法。 A*搜寻算法已在本 BLOG 内有所详细的介绍,本文咱们结合 fibonacci 堆实现 Dijkstra 算法。 即,Dijkstra + fibonacci 堆 c 实现。 我想了下,把一个算法研究够透彻之后,还要编写代码去实现它,才叫真正掌握了一个 算法。本 BLOG 内经典算法研究系列,已经写了 18 篇文章,十一个算法,所以,还有 10 多个算法,待我去实现。 代码风格 实现一个算法,首先要了解此算法的原理,了解此算法的原理之后,便是写代码实现。 在打开编译器之前,我先到网上搜索了一下―Dijkstra 算法+fibonacci 堆实现‖。 发现:网上竟没有过 Dijkstra + fibonacci 堆实现的 c 代码,而且如果是以下几类的 代码,我是直接跳过不看的: 1、没有注释(看不懂)。 2、没有排版(不舒服)。 3、冗余繁杂(看着烦躁)。 fibonacci 堆实现 Dijkstra 算法 ok,闲话少说,咱们切入正题。下面,咱们来一步一步利用 fibonacci 堆实现 Dijkstra 算 法吧。 前面说了,要实现一个算法,首先得明确其算法原理及思想,而要理解一个算法的原理, 又得知道发明此算法的目的是什么,即,此算法是用来干什么的? 由前面的例子,我们可以总结出:Dijkstra 算法是为了解决一个点到其它点最短距离的 问题。 我们总是要找源点到各个目标点的最短距离,在寻路过程中,如果新发现了一个新的 点,发现当源点到达前一个目的点路径通过新发现的点时,路径可以缩短,那么我们就必须 及时更新此最短距离。 ok,举个例子:如我们最初找到一条路径,A->B,这条路径的最短距离为 6,后来找 到了 C 点,发现若 A->C->B 点路径时,A->B 的最短距离为 5,小于之前找到的最短距 离 6,所以,便得此更新 A 到 B 的最短距离:为 5,最短路径为 A->C->B. 好的,明白了此算法是干什么的,那么咱们先用伪代码尝试写一下吧(有的人可能会说, 不是吧,我现在,什么都还没搞懂,就要我写代码了。额,你手头不是有资料么,如果全部 所有的工作,都要自己来做的话,那就是一个浩大的工程了。:D。)。 咱们先从算法导论上,找来 Dijkstra 算法的伪代码如下: DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) //1、初始化结点工作 2 S ← Ø 3 Q ← V[G] //2、插入结点操作 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //3、从最小队列中,抽取最小点工作 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //4、松弛操作。 伪代码毕竟与能在机子上编译运行的代码,还有很多工作要做。 首先,咱们看一下上述伪代码,可以看出,基本上,此 Dijkstra 算法主要分为以下四个 步骤: 1、初始化结点工作 2、插入结点操作 3、从最小队列中,抽取最小点工作 4、松弛操作。 ok,由于第 2 个操作涉及到斐波那契堆,比较复杂一点,咱们先来具体分析第 1、2、 4 个操作: 1、得用 O(V)的时间,来对最短路径的估计,和对前驱进行初始化工作。 INITIALIZE-SINGLE-SOURCE(G, s) 1 for each vertex v ∈ V[G] 2 do d[v] ← ∞ 3 π[v] ← NIL //O(V) 4 d[s] 0 我们根据上述伪代码,不难写出以下的代码: void init_single_source(Graph *G,int s) { for (int i=0;in;i++) { d[i]=INF; pre[i]=-1; } d[s]=0; } 2、插入结点到队列的操作 2 S ← Ø 3 Q ← V[G] //2、插入结点操作 代码: for (i=0;in;i++) S[i]=0; 4、松弛操作。 首先得理解什么是松弛操作: Dijkstra 算法使用了松弛技术,对每个顶点 v<-V,都设置一个属性 d[v],用来描述 从源点 s 到 v 的最短路径上权值的上界,称为最短路径的估计。 RELAX(u, v, w) 1 if d[v] > d[u] + w(u, v) 2 then d[v] ← d[u] + w(u, v) 3 π[v] ← u //O(E) 同样,我们不难写出下述代码: void relax(int u,int v,Graph *G) { if (d[v]>d[u]+G->w[u][v]) { d[v] = d[u]+G->w[u][v]; //更新此最短距离 pre[v]=u; //u 为 v 的父结点 } } 再解释一下上述 relax 的代码,其中 u 为 v 的父母结点,当发现其父结点 d[u] 加上经过路径的距离 G->w[u][v],小于子结点到源点的距离 d[v],便得更新此最短距离。 请注意,说的明白点:就是本来最初 A 到 B 的路径为 A->B,现在发现,当 A 经过 C 到达 B 时,此路径距离比 A->B 更短,当然,便得更新此 A 到 B 的最短路径了,即是: A->C->B,C 即成为了 B 的父结点(如此解释,我相信您已经明朗。:D。)。 即 A=>B <== A->C->B,执行赋值操作。 ok,第 1、2、4 个操作步骤,咱们都已经写代码实现了,那么,接下来,咱们来编写 第 3 个操作的代码:3、从最小队列中,抽取最小点工作。 相信,你已经看出来了,我们需要构造一个最小优先队列,那用什么来构造最小优先 队列列?对了,堆。什么堆最好,效率最高,呵呵,就是本文要实现的 fibonacci 堆。 为什么?ok,请看最小优先队列的三种实现方法比较: EXTRACT-MIN + RELAX I、 简单方式: O(V*V + E*1) II、 二叉/项堆: O(V*lgV + |E|*lgV) 源点可达:O(E*lgV) 稀疏图时,有 E=o(V^2/lgV), => O(V^2) III、斐波那契堆:O(V*lgV + E) 其中,V 为顶点,E 为边。好的,这样我们就知道了:Dijkstra 算法中,当用斐波纳 契堆作优先队列时,算法时间复杂度为 O(V*lgV + E)。 额,那么接下来,咱们要做的是什么列?当然是要实现一个 fibonacci 堆了。可要怎么 实现它,才能用到我们 Dijkstra 算法中列?对了,写成一个库的形式。库?呵呵,是一个类。 ok,以下就是这个 fibonacci 堆的实现: view plaincopy to clipboardprint? 1. //FibonacciHeap.h 2. #ifndef _FIBONACCI_HEAP_H_INCLUDED_ 3. #define _FIBONACCI_HEAP_H_INCLUDED_ 4. 5. #include 6. #include 7. 8. template 9. struct Fib_node 10. { 11. Fib_node* ns_; //后驱结点 12. Fib_node *pt_; //父母结点 13. Fib_node* ps_; //前驱结点 14. Fib_node* fc_; //头结点 15. int rank_; //孩子结点 16. bool marked_; //孩子结点是否删除的标记 17. T* pv_; 18. Fib_node(T* pv = 0) : pv_(pv) { } 19. T& value(void) { return *pv_; } 20. void set_src(T* pv) { pv_ = pv; } 21. }; //Fib_node 的数据结构 22. 23. 24. template 25. Node* merge_tree(Node*a, Node* b, OD small) //合并结点 26. { 27. if(small(b->value(), a->value())) 28. swap(a, b); 29. Node* fc = a->fc_; 30. a->fc_ = b; 31. a->ns_ = a->ps_ = a->pt_ = 0; 32. ++a->rank_; 33. 34. b->pt_ = a; //a 为 b 的父母 35. b->ns_ = fc; //第一个结点赋给 b 的前驱结点 36. b->ps_ = 0; 37. if(fc != 0) 38. fc->ps_ = b; 39. return a; 40. } 41. 42. template 43. void erase_node(Node* me) //删除结点 44. { 45. Node* const p = me->pt_; 46. --p->rank_; 47. if(p->fc_ == me) //如果 me 是头结点 48. { 49. if((p->fc_ = me->ns_) != 0) 50. me->ns_->ps_ = 0; 51. } 52. else 53. { 54. Node *prev = me->ps_; 55. Node *next = me->ns_; //可能为 0 56. prev->ns_ = next; 57. if(next != 0) 58. next->ps_ = prev; 59. } 60. } 61. 62. 63. template 64. Node* merge_fib_heap(Node* a, Node* b, OD small) //调用上述的 merge_tree 合并 fib_heap。 65. { 66. enum {SIZE = 64}; // 67. Node* v[SIZE] = {0}; 68. int k; 69. while(a != 0) 70. { 71. Node* carry = a; 72. a = a->ns_; 73. for(k = carry->rank_; v[k] != 0; ++k) 74. { 75. carry = merge_tree(carry, v[k], small); 76. v[k] = 0; 77. } 78. v[k] = carry; 79. } 80. while(b != 0) 81. { 82. Node* carry = b; 83. b = b->ns_; 84. for(k = carry->rank_; v[k] != 0; ++k) 85. { 86. carry = merge_tree(carry, v[k], small); 87. v[k] = 0; 88. } 89. v[k] = carry; 90. } 91. Node** t = std::remove(v, v+SIZE, (Node*)0); 92. int const n = t - v; 93. if(n > 0) 94. { 95. for(k = 0; k < n - 1; ++k) 96. v[k]->ns_ = v[k+1]; 97. for(k = 1; k < n; ++k) 98. v[k]->ps_ = v[k-1]; 99. v[n-1]->ns_ = v[0]->ps_ = 0; 100. } 101. return v[0]; 102. } 103. 104. template > 105. struct Min_fib_heap //抽取最小结点 106. { 107. typedef Fib_node Node; 108. typedef Node Node_type; 109. 110. Node* roots_; 111. Node* min_; //pointer to the minimum node 112. OD less_; 113. 114. Min_fib_heap(void): roots_(0), min_(0), less_() { } 115. bool empty(void) const { return roots_ == 0; } 116. T& top(void) const { return min_->value(); } 117. 118. void decrease_key(Node* me) //删除 119. { //precondition: root_ not zero 120. if(less_(me->value(), min_->value())) 121. min_ = me; 122. cascading_cut(me); 123. } 124. void push(Node* me) //压入 125. { 126. me->pt_ = me->fc_ = 0; 127. me->rank_ = 0; 128. if(roots_ == 0) 129. { 130. me->ns_ = me->ps_ = 0; 131. me->marked_ = false; 132. roots_ = min_ = me; 133. } 134. else 135. { 136. if(less_(me->value(), min_->value())) 137. min_ = me; 138. insert2roots(me); 139. } 140. } 141. Node* pop(void) //弹出 142. { 143. Node* const om = min_; 144. erase_tree(min_); 145. min_ = roots_ = merge_fib_heap(roots_, min_->fc_, less_); 146. if(roots_ != 0) //find new min_ 147. { 148. for(Node* t = roots_->ns_; t != 0; t = t->ns_) 149. if(less_(t->value(), min_->value())) 150. min_ = t; 151. } 152. return om; 153. } 154. void merge(void) //合并 155. { 156. if(empty()) return; 157. min_ = roots_ = merge_fib_heap(roots_, (Node*)0, less_); 158. for(Node* a = roots_->ns_; a != 0; a = a->ns_) 159. if(less_(a->value(), min_->value() )) 160. min_ = a; 161. } 162. private: 163. void insert2roots(Node* me) //插入 164. { //precondition: 1) root_ != 0; 2) me->value() >= min_->value() 165. me->pt_ = me->ps_ = 0; 166. me->ns_ = roots_; 167. me->marked_ = false; 168. roots_->ps_ = me; 169. roots_ = me; 170. } 171. void cascading_cut(Node* me) //断开 172. { //precondition: me is not a root. that is me->pt_ != 0 173. for(Node* p = me->pt_; p != 0; me = p, p = p->pt_) 174. { 175. erase_node(me); 176. insert2roots(me); 177. if(p->marked_ == false) 178. { 179. p->marked_ = true; 180. break; 181. } 182. } 183. } 184. void erase_tree(Node* me) //删除 185. { 186. if(roots_ == me) 187. { 188. roots_ = me->ns_; 189. if(roots_ != 0) 190. roots_->ps_ = 0; 191. } 192. else 193. { 194. Node* const prev = me->ps_; 195. Node* const next = me->ns_; 196. prev->ns_ = next; 197. if(next != 0) 198. next->ps_ = prev; 199. } 200. } 201. }; //Min_fib_heap 的类 202. 203. 204. template 205. bool is_sorted(Fitr first, Fitr last) 206. { 207. if(first != last) 208. for(Fitr prev = first++; first != last; prev = first++) 209. if(*first < *prev) return false; 210. return true; 211. } 212. template 213. bool is_sorted(Fitr first, Fitr last, OD cmp) 214. { 215. if(first != last) 216. for(Fitr prev = first++; first != last; prev = first++) 217. if(cmp(*first, *prev)) return false; 218. return true; 219. } 由于本 BLOG 日后会具体阐述这个斐波那契堆的各项操作,限于篇幅,在此,就不再 啰嗦解释上述程序了。 ok,实现了 fibonacci 堆,接下来,咱们可以写 Dijkstra 算法的代码了。为了版述清 晰,再一次贴一下此算法的伪代码: DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) 2 S ← Ø 3 Q ← V[G] //第 3 行,INSERT 操作,O(1) 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //第 5 行,EXTRACT-MIN 操作,V*lgV 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //第 8 行,RELAX 操作,E*O(1) 编写的 Dijkstra 算法的 c 代码如下: view plaincopy to clipboardprint? 1. void Dijkstra(int s, T d[], int p[]) 2. { 3. //寻找从顶点 s 出发的最短路径,在 d 中存储的是 s->i 的最短距离 4. //p 中存储的是 i 的父节点 5. if (s < 1 || s > n) 6. throw OutOfBounds(); 7. 8. //路径可到达的顶点列表,这里可以用上述实现的 fibonacci 堆代码。 9. Chain L; 10. 11. ChainIterator I; 12. //初始化 d, p, and L 13. for (int i = 1; i <= n; i++) 14. { 15. d[i] = a[s][i]; 16. 17. if (d[i] == NoEdge) 18. { 19. p[i] = 0; 20. } 21. else 22. { 23. p[i] = s; 24. L.Insert(0,i); 25. } 26. } 27. 28. //更新 d, p 29. while (!L.IsEmpty()) 30. { 31. //寻找最小 d 的点 v 32. int *v = I.Initialize(L); 33. int *w = I.Next(); 34. while (w) 35. { 36. if (d[*w] < d[*v]) 37. v = w; 38. 39. w = I.Next(); 40. } 41. 42. int i = *v; 43. L.Delete(*v); 44. for (int j = 1; j <= n; j++) 45. { 46. if (a[i][j] != NoEdge 47. && (!p[j] || d[j] > d[i] + a[i][j])) //d[i]是父节点 48. { 49. // 刷新更小的 d[j] 50. d[j] = d[i] + a[i][j]; 51. 52. // 如果 j 没有父节点,则添加到 L 53. if (!p[j]) 54. L.Insert(0,j); 55. 56. // 更新父节点 57. p[j] = i; 58. } 59. } 60. } 61. } 更好的代码,还在进一步修正中。日后,等完善好后,再发布整个工程出来。 下面是演示此 Dijkstra 算法的工程的俩张图(0 为源点,4 为目标点,第二幅图中的红 色路径即为所求的 0->4 的最短距离的路径): 完。 版权所有。转载本 BLOG 内任何文章,请以超链接形式注明出处。谢 谢,各位。 二之三续、Dijkstra 算法+Heap 堆的 完整 c 实现源码 作者:JULY、二零一一年三月十八日 出处:http://blog.csdn.net/v_JULY_v。 --------------------------------------------------------------------------------------- 引言: 此文的写作目的很简单,就一个理由,个人认为:上一篇文章,二之再续、Dijkstra 算 法+fibonacci 堆的逐步 c 实现,写的不够好,特此再写 Dijkstra 算法的一个续集,谓之 二之三续。 鉴于读者理解斐波那契堆的难度,本文,以简单的最小堆为示例。同时,本程序也有 参考网友的实现。有任何问题,欢迎指正。 Dijkstra 算法+Heap 堆完整算法思想 在前一篇文章中,我们已经了解到,Dijkstra 算法如下: DIJKSTRA(G, w, s) 1 INITIALIZE-SINGLE-SOURCE(G, s) //1、初始化结点工作 2 S ← Ø 3 Q ← V[G] //2、初始化队列 4 while Q ≠ Ø 5 do u ← EXTRACT-MIN(Q) //3、从最小队列中,抽取最小结点(在此之前,先建 立最小堆) 6 S ← S ∪{u} 7 for each vertex v ∈ Adj[u] 8 do RELAX(u, v, w) //4、松弛操作。 如此,咱们不再赘述,直接即可轻松编写如下 c/c++源码: void dijkstra(ALGraph G,int s,int d[],int pi[],int Q[]) { //Q[]是最小优先队列,Q[1..n]中存放的是图顶点标号,Q[0]中存放堆的大小 //优先队列中有 key 的概念,这里 key 可以从 d[]中取得。比如说,Q[2]的大小(key)为 d[ Q[2] ] initSingleSource(G,s,d,pi); //1、初始化结点工作 //2、初始化队列 Q[0] = G.vexnum; for(int i=1;i<=Q[0];i++) { Q[i] = i-1; } Q[1] = s; Q[s+1] = 0; int u; int v; while(Q[0]!=0) { buildMinHeap(Q,d); //3.1、建立最小堆 u = extractMin(Q,d); //3.2、从最小队列中,抽取最小结点 ArcNode* arcNodePt = G.vertices[u].firstarc; while(arcNodePt!=NULL) { v = arcNodePt->adjvex; relax(u,v,G,d,pi); //4、松弛操作。 arcNodePt = arcNodePt->nextarc; } } } ok,接下来,咱们一步一步编写代码来实现此 Dijkstra 算法,先给出第 1、初始化结 点工作,和 4、松弛操作俩个操作的源码: void initSingleSource(ALGraph G,int s,int d[],int pi[]) { //1、初始化结点工作 for(int i=0;id[u]+getEdgeWeight(G,u,v)) { d[v] = d[u] + getEdgeWeight(G,u,v); pi[v] = u; } } ok,接下来,咱们具体阐述第 3 个操作,3、从最小队列中,抽取最小结点(在此之前, 先建立最小堆)。 Heap 最小堆的建立与抽取最小结点 在我的这篇文章二、堆排序算法里头,对最大堆的建立有所阐述: 2.3.1、建堆(O(N)) BUILD-MAX-HEAP(A) 1 heap-size[A] ← length[A] 2 for i ← |_length[A]/2_| downto 1 3 do MAX-HEAPIFY(A, i) //建堆,怎么建列?原来就是不断的调用 MAX-HEAPIFY(A, i)来建立最大堆。 建最小堆,也是一回事,把上述代码改俩处即可,一,MAX->MIN,二, MAX-HEAPIFY(A, i)->MIN-HEAPIFY(A, i)。如此说来,是不是很简单列,是的,本身 就很简单。 先是建立最小堆的工作: void buildMinHeap(int Q[],int d[]) //建立最小堆 { for(int i=Q[0]/2;i>=1;i--) { minHeapify(Q,d,i); //调用 minHeapify,以保持堆的性质。 } } 然后,得编写 minHeapify 代码,来保持最小堆的性质: void minHeapify(int Q[],int d[],int i) { //smallest,l,r,i 都是优先队列元素的下标,范围是从 1 ~ heap-size[Q] int l = 2*i; int r = 2*i+1; int smallest; if(l<=Q[0] && d[ Q[l] ] < d[ Q[i] ]) { smallest = l; } else { smallest = i; } if(r<=Q[0] && d[ Q[r] ] < d[ Q[smallest] ]) { smallest = r; } if(smallest!=i) { int temp = Q[i]; Q[i] = Q[smallest]; Q[smallest] = temp; minHeapify(Q,d,smallest); } } 你自个比较一下建立最小堆,与建立最大堆的代码,立马看见,如出一辙,不过是改几 个字母而已: MAX-HEAPIFY(A, i) //建立最大堆的代码 1 l ← LEFT(i) 2 r ← RIGHT(i) 3 if l ≤ heap-size[A] and A[l] > A[i] 4 then largest ← l 5 else largest ← i 6 if r ≤ heap-size[A] and A[r] > A[largest] 7 then largest ← r 8 if largest ≠ i 9 then exchange A[i] <-> A[largest] 10 MAX-HEAPIFY(A, largest) ok,最后,便是 3、从最小队列中,抽取最小结点的工作了,如下: int extractMin(int Q[],int d[]) //3、从最小队列中,抽取最小结点 { //摘取优先队列中最小元素的内容,这里返回图中顶点的标号(0 ~ G.vexnum-1), //这些标号是保存在 Q[1..n]中的 if(Q[0]<1) { cout<<"heap underflow!"<arcnum = 0; GPt->vexnum = vn; for(int i=0;ivertices[i].firstarc = NULL; } } void insertArc(ALGraph* GPt,int vhead,int vtail,int w) //增加结点操作 { ArcNode* arcNodePt = new ArcNode; arcNodePt->nextarc = NULL; arcNodePt->adjvex = vtail; arcNodePt->weight = w; ArcNode* tailPt = GPt->vertices[vhead].firstarc; if(tailPt==NULL) { GPt->vertices[vhead].firstarc = arcNodePt; } else { while(tailPt->nextarc!=NULL) { tailPt = tailPt->nextarc; } tailPt->nextarc = arcNodePt; } GPt->arcnum ++; } void displayGraph(ALGraph G) //打印结点 { ArcNode* arcNodePt; for(int i=0;iadjvex<<"("<<"weight"<weight<<")"<< " "; arcNodePt = arcNodePt->nextarc; } cout<adjvex==vtail) { return arcNodePt->weight; } arcNodePt = arcNodePt->nextarc; } return INFINITY; } 主函数测试用例 最后,便是编写主函数测试本程序: int main(){ ALGraph G; ALGraph* GPt = &G; initALGraph(GPt,5); insertArc(GPt,0,1,10); insertArc(GPt,0,3,5); insertArc(GPt,1,2,1); insertArc(GPt,1,3,2); insertArc(GPt,2,4,4); insertArc(GPt,3,1,3); insertArc(GPt,3,2,9); insertArc(GPt,3,4,2); insertArc(GPt,4,2,4); insertArc(GPt,4,0,7); cout<<"显示出此构造的图:"<=0 and xi=xj \ max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj 上面的公式用递归函数不难求得。自然想到 Fibonacci 第 n 项(本微软等 100 题系列 V0.1 版第 19 题)问题的求解中可知, 直接递归会有很多重复计算,所以,我们用从底向上循环求解的思路效率更高。 为了能够采用循环求解的思路,我们用一个矩阵(参考下文文末代码中的 LCS_length) 保存下来当前已经计算好了的 c[i,j], 当后面的计算需要这些数据时就可以直接从矩阵读取。 另外,求取 c[i,j]可以从 c[i-1,j-1] 、c[i,j-1]或者 c[i-1,j]三个方向计算得到, 相当于在矩阵 LCS_length 中是从 c[i-1,j-1],c[i,j-1]或者 c[i-1,j]的某一个各自移动到 c[i,j], 因此在矩阵中有三种不同的移动方向:向左、向上和向左上方,其中只有向左上方移动 时才表明找到 LCS 中的一个字符。 于是我们需要用另外一个矩阵(参考下文文末代码中的 LCS_direction)保存移动的 方向。 步骤三,计算 LCS 的长度 LCS-LENGTH(X, Y) 1 m ← length[X] 2 n ← length[Y] 3 for i ← 1 to m 4 do c[i, 0] ← 0 5 for j ← 0 to n 6 do c[0, j] ← 0 7 for i ← 1 to m 8 do for j ← 1 to n 9 do if xi = yj 10 then c[i, j] ← c[i - 1, j - 1] + 1 11 b[i, j] ← "↖" 12 else if c[i - 1, j] ≥ c[i, j - 1] 13 then c[i, j] ← c[i - 1, j] 14 b[i, j] ← "↑" 15 else c[i, j] ← c[i, j - 1] 16 b[i, j] ← ← 17 return c and b 此过程 LCS-LENGTH 以俩个序列 X = 〈x1, x2, ..., xm〉 和 Y = 〈y1, y2, ..., yn〉 为输入。 它把 c[i,j]值填入一个按行计算表项的表 c[0 ‥ m, 0 ‥ n] 中, 它还维护 b[1 ‥ m, 1 ‥ n] 以简化最优解的构造。 从直觉上看,b[i, j] 指向一个表项,这个表项对应于与在计算 c[i, j]时所选择的最优 子问题的解是相同的。 该程序返回表中 b and c , c[m, n] 包含 X 和 Y 的一个 LCS 的长度。 步骤四,构造一个 LCS, PRINT-LCS(b, X, i, j) 1 if i = 0 or j = 0 2 then return 3 if b[i, j] = "↖" 4 then PRINT-LCS(b, X, i - 1, j - 1) 5 print xi 6 elseif b[i, j] = "↑" 7 then PRINT-LCS(b, X, i - 1, j) 8 else PRINT-LCS(b, X, i, j - 1) 该过程的运行时间为 O(m+n)。 ------------------------------- ok,最后给出此面试第 56 题的代码,请君自看: 参考代码如下: #include "string.h" // directions of LCS generation enum decreaseDir {kInit = 0, kLeft, kUp, kLeftUp}; // Get the length of two strings' LCSs, and print one of the LCSs // Input: pStr1 - the first string // pStr2 - the second string // Output: the length of two strings' LCSs int LCS(char* pStr1, char* pStr2) { if(!pStr1 || !pStr2) return 0; size_t length1 = strlen(pStr1); size_t length2 = strlen(pStr2); if(!length1 || !length2) return 0; size_t i, j; // initiate the length matrix int **LCS_length; LCS_length = (int**)(new int[length1]); for(i = 0; i < length1; ++ i) LCS_length[i] = (int*)new int[length2]; for(i = 0; i < length1; ++ i) for(j = 0; j < length2; ++ j) LCS_length[i][j] = 0; // initiate the direction matrix int **LCS_direction; LCS_direction = (int**)(new int[length1]); for( i = 0; i < length1; ++ i) LCS_direction[i] = (int*)new int[length2]; for(i = 0; i < length1; ++ i) for(j = 0; j < length2; ++ j) LCS_direction[i][j] = kInit; for(i = 0; i < length1; ++ i) { for(j = 0; j < length2; ++ j) { if(i == 0 || j == 0) { if(pStr1[i] == pStr2[j]) { LCS_length[i][j] = 1; LCS_direction[i][j] = kLeftUp; } else LCS_length[i][j] = 0; } // a char of LCS is found, // it comes from the left up entry in the direction matrix else if(pStr1[i] == pStr2[j]) { LCS_length[i][j] = LCS_length[i - 1][j - 1] + 1; LCS_direction[i][j] = kLeftUp; } // it comes from the up entry in the direction matrix else if(LCS_length[i - 1][j] > LCS_length[i][j - 1]) { LCS_length[i][j] = LCS_length[i - 1][j]; LCS_direction[i][j] = kUp; } // it comes from the left entry in the direction matrix else { LCS_length[i][j] = LCS_length[i][j - 1]; LCS_direction[i][j] = kLeft; } } } LCS_Print(LCS_direction, pStr1, pStr2, length1 - 1, length2 - 1); //调用下 面的 LCS_Pring 打印出所求子串。 return LCS_length[length1 - 1][length2 - 1]; //返回长度。 } // Print a LCS for two strings // Input: LCS_direction - a 2d matrix which records the direction of // LCS generation // pStr1 - the first string // pStr2 - the second string // row - the row index in the matrix LCS_direction // col - the column index in the matrix LCS_direction void LCS_Print(int **LCS_direction, char* pStr1, char* pStr2, size_t row, size_t col) { if(pStr1 == NULL || pStr2 == NULL) return; size_t length1 = strlen(pStr1); size_t length2 = strlen(pStr2); if(length1 == 0 || length2 == 0 || !(row < length1 && col < length2)) return; // kLeftUp implies a char in the LCS is found if(LCS_direction[row][col] == kLeftUp) { if(row > 0 && col > 0) LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col - 1); // print the char printf("%c", pStr1[row]); } else if(LCS_direction[row][col] == kLeft) { // move to the left entry in the direction matrix if(col > 0) LCS_Print(LCS_direction, pStr1, pStr2, row, col - 1); } else if(LCS_direction[row][col] == kUp) { // move to the up entry in the direction matrix if(row > 0) LCS_Print(LCS_direction, pStr1, pStr2, row - 1, col); } } 扩展:如果题目改成求两个字符串的最长公共子字符串,应该怎么求? 子字符串的定义和子串的定义类似,但要求是连续分布在其他字符串中。 比如输入两个字符串 BDCABA 和 ABCBDAB 的最长公共字符串有 BD 和 AB,它们的 长度都是 2。 附注:算法导论上指出, 一、最长公共子序列问题的一个一般的算法、时间复杂度为 O(mn)。 然后,Masek 和 Paterson 给出了一个 O(mn/lgn)时间内执行的算法,其中 n<=m, 而且此序列是从一个有限集合中而来。 在输入序列中没有出现超过一次的特殊情况中,Szymansk 说明这个问题可在 O((n+m)lg(n+m))内解决。 二、一篇由 Gilbert 和 Moore 撰写的关于可变长度二元编码的早期论文中有这样的应用: 在所有的概率 pi 都是 0 的情况下构造最优二叉查找树,这篇论文给出一个 O(n^3)时间 的算法。 Hu 和 Tucker 设计了一个算法,它在所有的概率 pi 都是 0 的情况下,使用 O(n)的 时间和 O(n)的空间, 最后,Knuth 把时间降到了 O(nlgn)。 关于此动态规划算法更多可参考 算法导论一书第 15 章 动态规划问题, 至于关于此面试第 56 题的更多,可参考我即将整理上传的答案 V04 版第 41-60 题的 答案。 完、July、二零一零年十二月三十一日。 --------------------------------------------------------------------------------------- 补一网友提供的关于此最长公共子序列问题的 java 算法源码,我自行测试了下,正确: import java.util.Random; public class LCS{ public static void main(String[] args){ //设置字符串长度 int substringLength1 = 20; int substringLength2 = 20; //具体大小可自行设置 // 随机生成字符串 String x = GetRandomStrings(substringLength1); String y = GetRandomStrings(substringLength2); Long startTime = System.nanoTime(); // 构造二维数组记录子问题 x[i]和 y[i]的 LCS 的长度 int[][] opt = new int[substringLength1 + 1][substringLength2 + 1]; // 动态规划计算所有子问题 for (int i = substringLength1 - 1; i >= 0; i--){ for (int j = substringLength2 - 1; j >= 0; j--){ if (x.charAt(i) == y.charAt(j)) opt[i][j] = opt[i + 1][j + 1] + 1; //参考 上文我给的公式。 else opt[i][j] = Math.max(opt[i + 1][j], opt[i][j + 1]); // 参考上文我给的公式。 } } --------------------------------------------------------------------------------------- 理解上段,参考上文我给的公式: 根据上述结论,可得到以下公式, 如果我们记字符串 Xi 和 Yj 的 LCS 的长度为 c[i,j],我们可以递归地求 c[i,j]: / 0 if i<0 or j<0 c[i,j]= c[i-1,j-1]+1 if i,j>=0 and xi=xj \ max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj ------------------------------------------------------------------------------------- System.out.println("substring1:"+x); System.out.println("substring2:"+y); System.out.print("LCS:"); int i = 0, j = 0; while (i < substringLength1 && j < substringLength2){ if (x.charAt(i) == y.charAt(j)){ System.out.print(x.charAt(i)); i++; j++; } else if (opt[i + 1][j] >= opt[i][j + 1]) i++; else j++; } Long endTime = System.nanoTime(); System.out.println(" Totle time is " + (endTime - startTime) + " ns"); } //取得定长随机字符串 public static String GetRandomStrings(int length){ StringBuffer buffer = new StringBuffer("abcdefghijklmnopqrstuvwxyz"); StringBuffer sb = new StringBuffer(); Random r = new Random(); int range = buffer.length(); for (int i = 0; i < length; i++){ sb.append(buffer.charAt(r.nextInt(range))); } return sb.toString(); } } eclipse 运行结果为: substring1:akqrshrengxqiyxuloqk substring2:tdzbujtlqhecaqgwfzbc LCS:qheq Totle time is 818058 ns ------------------------------------------------------- ------------------------------------------------------- /* * x_array,x_length: X 序列及其长度 * y_array,y_length: Y 序列及其长度 * b_array,b_length: 记录 LCS 的元素,b_length=min(x_length,y_length) * 返回 LCS 长度 */ int dynamic_lcs(int *x_array, int x_length, int *y_array, int y_length, int *b_array, int b_length) { int i, j, k, lcs_length; int **c_array=NULL; c_array = malloc(sizeof(int *)*(x_length+1)); for(i=0; i c_array[i] = malloc(sizeof(int)*(y_length+1)); for(i=0; i c_array[i][0] = 0; for(j=0; j c_array[0][j] = 0; k = 0; for(i=0; i { for(j=0; j { if(x_array[i] == y_array[j]) { c_array[i+1][j+1] = c_array[i][j]+1; b_array[k++] = x_array[i]; } else if(c_array[i+1][j] >= c_array[i][j+1]) c_array[i+1][j+1] = c_array[i+1][j]; else c_array[i+1][j+1] = c_array[i][j+1]; } } lcs_length = c_array[x_length][y_length]; for(i=0; i free(c_array[i]); free(c_array); return lcs_length; } 四、教你通透彻底理解:BFS 和 DFS 优先搜索算法 作者:July 二零一一年一月一日 本人参考:算法导论 本人声明:个人原创,转载请注明出处。 ---------------------------------------------------------------------------------------------------------------------- ok,开始。 翻遍网上,关于此类 BFS 和 DFS 算法的文章,很多。但,都说不出个所以然来。 读完此文,我想, 你对图的广度优先搜索和深度优先搜索定会有个通通透透,彻彻底底的认识。 咱们由 BFS 开始: 首先,看下算法导论一书关于 此 BFS 广度优先搜索算法的概述。 算法导论第二版,中译本,第 324 页。 广度优先搜索(BFS) 在 Prime 最小生成树算法,和 Dijkstra 单源最短路径算法中,都采用了与 BFS 算法类似 的思想。 //u 为 v 的先辈或父母。 BFS(G, s) 1 for each vertex u ∈ V [G] - {s} 2 do color[u] ← WHITE 3 d[u] ← ∞ 4 π[u] ← NIL //除了源顶点 s 之外,第 1-4 行置每个顶点为白色,置每个顶点 u 的 d[u]为无穷大, //置每个顶点的父母为 NIL。 5 color[s] ← GRAY //第 5 行,将源顶点 s 置为灰色,这是因为在过程开始时,源顶点已被发现。 6 d[s] ← 0 //将 d[s]初始化为 0。 7 π[s] ← NIL //将源顶点的父顶点置为 NIL。 8 Q ← Ø 9 ENQUEUE(Q, s) //入队 //第 8、9 行,初始化队列 Q,使其仅含源顶点 s。 10 while Q ≠ Ø 11 do u ← DEQUEUE(Q) //出队 //第 11 行,确定队列头部 Q 头部的灰色顶点 u,并将其从 Q 中去掉。 12 for each v ∈ Adj[u] //for 循环考察 u 的邻接表中的每个顶点 v 13 do if color[v] = WHITE 14 then color[v] ← GRAY //置为灰色 15 d[v] ← d[u] + 1 //距离被置为 d[u]+1 16 π[v] ← u //u 记为该顶点的父母 17 ENQUEUE(Q, v) //插入队列中 18 color[u] ← BLACK //u 置为黑色 由下图及链接的演示过程,清晰在目,也就不用多说了: 广度优先遍历演示地址: http://sjjg.js.zwu.edu.cn/SFXX/sf1/gdyxbl.html --------------------------------------------------------------------------------------- ok,不再赘述。接下来,具体讲解深度优先搜索算法。 深度优先探索算法 DFS //u 为 v 的先辈或父母。 DFS(G) 1 for each vertex u ∈ V [G] 2 do color[u] ← WHITE 3 π[u] ← NIL //第 1-3 行,把所有顶点置为白色,所有 π 域被初始化为 NIL。 4 time ← 0 //复位时间计数器 5 for each vertex u ∈ V [G] 6 do if color[u] = WHITE 7 then DFS-VISIT(u) //调用 DFS-VISIT 访问 u,u 成为深度优先森林中一 棵新的树 //第 5-7 行,依次检索 V 中的顶点,发现白色顶点时,调用 DFS-VISIT 访问该顶点。 //每个顶点 u 都对应于一个发现时刻 d[u]和一个完成时刻 f[u]。 DFS-VISIT(u) 1 color[u] ← GRAY //u 开始时被发现,置为白色 2 time ← time +1 //time 递增 3 d[u] <-time //记录 u 被发现的时间 4 for each v ∈ Adj[u] //检查并访问 u 的每一个邻接点 v 5 do if color[v] = WHITE //如果 v 为白色,则递归访问 v。 6 then π[v] ← u //置 u 为 v 的先辈 7 DFS-VISIT(v) //递归深度,访问邻结点 v 8 color[u] <-BLACK //u 置为黑色,表示 u 及其邻接点都已访问完成 9 f [u] ▹ time ← time +1 //访问完成时间记录在 f[u]中。 //完 第 1-3 行,5-7 行循环占用时间为 O(V),此不包括调用 DFS-VISIT 的时间。 对于每个顶点 v(-V,过程 DFS-VISIT 仅被调用依次,因为只有对白色顶点才会调用此 过程。 第 4-7 行,执行时间为 O(E)。 因此,总的执行时间为 O(V+E)。 下面的链接,给出了深度优先搜索的演示系统: 图的深度优先遍历演示系统: http://sjjg.js.zwu.edu.cn/SFXX/sf1/sdyxbl.html ================================================ 最后,咱们再来看深度优先搜索的递归实现与非递归实现 1、DFS 递归实现: void dftR(PGraphMatrix inGraph) { PVexType v; assertF(inGraph!=NULL,"in dftR, pass in inGraph is null\n"); printf("\n===start of dft recursive version===\n"); for(v=firstVertex(inGraph);v!=NULL;v=nextVertex(inGraph,v)) if(v->marked==0) dfsR(inGraph,v); printf("\n===end of dft recursive version===\n"); } void dfsR(PGraphMatrix inGraph,PVexType inV) { PVexType v1; assertF(inGraph!=NULL,"in dfsR,inGraph is null\n"); assertF(inV!=NULL,"in dfsR,inV is null\n"); inV->marked=1; visit(inV); for(v1=firstAdjacent(inGraph,inV);v1!=NULL;v1=nextAdjacent(inGraph,i nV,v1)) //v1 当为 v 的邻接点。 if(v1->marked==0) dfsR(inGraph,v1); } 2、DFS 非递归实现 非递归版本---借助结点类型为队列的栈实现 联系树的前序遍历的非递归实现: 可知,其中无非是分成―探左‖和―访右‖两大块访右需借助栈中弹出的结点进行. 在图的深度优先搜索中,同样可分成―深度探索‖和―回访上层未访结点‖两块: 1、图的深度探索这样一个过程和树的―探左‖完全一致, 只要对已访问过的结点作一个判定即可。 2、而图的回访上层未访结点和树的前序遍历中的―访右‖也是一致的. 但是,对于树而言,是提供 rightSibling 这样的操作的,因而访右相当好实现。 在这里,若要实现相应的功能,考虑将每一个当前结点的下层结点中,如果有 m 个未访问 结点, 则最左的一个需要访问,而将剩余的 m-1 个结点按从左到右的顺序推入一个队列中。 并将这个队列压入一个堆栈中。 这样,当当前的结点的邻接点均已访问或无邻接点需要回访时, 则从栈顶的队列结点中弹出队列元素,将队列中的结点元素依次出队, 若已访问,则继续出队(当当前队列结点已空时,则继续出栈,弹出下一个栈顶的队列), 直至遇到有未访问结点(访问并置当前点为该点)或直到栈为空(则当前的深度优先搜索树停 止搜索)。 将算法通过精简过的 C 源程序的方式描述如下: //dfsUR:功能从一个树的某个结点 inV 发,以深度优先的原则访问所有与它相邻的结点 void dfsUR(PGraphMatrix inGraph,PVexType inV) { PSingleRearSeqQueue tmpQ; //定义临时队列,用以接受栈顶队列及压栈时使用 PSeqStack testStack; //存放当前层中的 m-1 个未访问结点构成队列的堆栈. //一些变量声明,初始化动作 //访问当前结点 inV->marked=1; //当 marked 值为 1 时将不必再访问。 visit(inV); do { flag2=0; //flag2 是一个重要的标志变量,用以、说明当前结点的所有未访问结点的个数,两个以上 的用 2 代表 //flag2:0:current node has no adjacent which has not been visited. //1:current node has only one adjacent node which has not been visited. //2:current node has more than one adjacent node which have not been visited. v1=firstAdjacent(inGraph,inV); //邻接点 v1 while(v1!=NULL) //访问当前结点的所有邻接点 { if(v1->marked==0) //.. { if(flag2==0) //当前结点的邻接点有 0 个未访问 { //首先,访问最左结点 visit(v1); v1->marked=1; //访问完成 flag2=1; // //记录最左儿子 lChildV=v1; //save the current node's first unvisited(has been visited at this time)adjacent node } else if(flag2==1) //当前结点的邻接点有 1 个未访问 { //新建一个队列,申请空间,并加入第一个结点 flag2=2; } else if(flag2==2)//当前结点的邻接点有 2 个未被访问 { enQueue(tmpQ,v1); } } v1=nextAdjacent(inGraph,inV,v1); } if(flag2==2)//push adjacent nodes which are not visited. { //将存有当前结点的 m-1 个未访问邻接点的队列压栈 seqPush(testStack,tmpQ); inV=lChildV; } else if(flag2==1)//only has one adjacent which has been visited. { //只有一个最左儿子,则置当前点为最左儿子 inV=lChildV; } else if(flag2==0) //has no adjacent nodes or all adjacent nodes has been visited { //当当前的结点的邻接点均已访问或无邻接点需要回访时,则从栈顶的队列结点中弹出队 列元素, //将队列中的结点元素依次出队,若已访问,则继续出队(当当前队列结点已空时, //则继续出栈,弹出下一个栈顶的队列),直至遇到有未访问结点(访问并置当前点为该点) 或直到栈为空。 flag=0; while(!isNullSeqStack(testStack)&&!flag) { v1=frontQueueInSt(testStack); //返回栈顶结点的队列中的队首元素 deQueueInSt(testStack); //将栈顶结点的队列中的队首元素弹出 if(v1->marked==0) { visit(v1); v1->marked=1; inV=v1; flag=1; } } } }while(!isNullSeqStack(testStack));//the algorithm ends when the stack is null } ----------------------------- 上述程序的几点说明: 所以,这里应使用的数据结构的构成方式应该采用下面这种形式: 1)队列的实现中,每个队列结点均为图中的结点指针类型. 定义一个以队列尾部下标加队列长度的环形队列如下: struct SingleRearSeqQueue; typedef PVexType QElemType; typedef struct SingleRearSeqQueue* PSingleRearSeqQueue; struct SingleRearSeqQueue { int rear; int quelen; QElemType dataPool[MAXNUM]; }; 其余基本操作不再赘述. 2)堆栈的实现中,每个堆栈中的结点元素均为一个指向队列的指针,定义如下: #define SEQ_STACK_LEN 1000 #define StackElemType PSingleRearSeqQueue struct SeqStack; typedef struct SeqStack* PSeqStack; struct SeqStack { StackElemType dataArea[SEQ_STACK_LEN]; int slot; }; 为了提供更好的封装性,对这个堆栈实现两种特殊的操作 2.1) deQueueInSt 操作用于将栈顶结点的队列中的队首元素弹出. void deQueueInSt(PSeqStack inStack) { if(isEmptyQueue(seqTop(inStack))||isNullSeqStack(inStack)) { printf("in deQueueInSt,under flow!\n"); return; } deQueue(seqTop(inStack)); if(isEmptyQueue(seqTop(inStack))) inStack->slot--; } 2.2) frontQueueInSt 操作用以返回栈顶结点的队列中的队首元素. QElemType frontQueueInSt(PSeqStack inStack) { if(isEmptyQueue(seqTop(inStack))||isNullSeqStack(inStack)) { printf("in frontQueueInSt,under flow!\n"); return '\r'; } return getHeadData(seqTop(inStack)); } =================== ok,本文完。 July、二零一一年一月一日。Happy 2011 new year! 作者声明: 本人 July 对本博客所有任何内容和资料享有版权,转载请注明作者本人 July 及出处。 永远,向您的厚道致敬。谢谢。July、二零一零年十二月二日。 五、红黑树算法的层层剖析与逐步实现 作者 July 二零一零年十二月三十一日 本文主要参考:算法导论第二版 本文主要代码:参考算法导论。 本文图片来源:个人手工画成、算法导论原书。 推荐阅读:Leo J. Guibas 和 Robert Sedgewick 于 1978 年写的关于红黑树的一 篇论文。 -------------------------------------------------------------- 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 --------------------------------------------------------------- 引言: 昨天下午画红黑树画了好几个钟头,总共 10 页纸。 特此,再深入剖析红黑树的算法实现,教你如何彻底实现红黑树算法。 经过我上一篇博文,―教你透彻了解红黑树‖后,相信大家对红黑树已经有了一定的了解。 个人觉得,这个红黑树,还是比较容易懂的。 不论是插入、还是删除,不论是左旋还是右旋,最终的目的只有一个: 即保持红黑树的 5 个性质,不得违背。 再次,重述下红黑树的五个性质: 一般的,红黑树,满足一下性质,即只有满足一下性质的树,我们才称之为红黑树: 1)每个结点要么是红的,要么是黑的。 2)根结点是黑的。 3)每个叶结点,即空结点(NIL)是黑的。 4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。 抓住了红黑树的那 5 个性质,事情就好办多了。 如, 1.红黑红黑,要么是红,要么是黑; 2.根结点是黑; 3.每个叶结点是黑; 4.一个红结点,它的俩个儿子必然都是黑的; 5.每一条路径上,黑结点的数目等同。 五条性质,合起来,来句顺口溜就是:(1)红黑 (2)黑 (3)黑 (4&5)红 ->黑 黑。 本文所有的文字,都是参照我昨下午画的十张纸(即我拍的照片)与算法导论来写的。 希望,你依照此文一点一点的往下看,看懂此文后,你对红黑树的算法了解程度,一定大增 不少。 ok,现在咱们来具体深入剖析红黑树的算法,并教你逐步实现此算法。 此教程分为 10 个部分,每一个部分作为一个小节。且各小节与我给的十张照片一一对应。 一、左旋与右旋 先明确一点:为什么要左旋? 因为红黑树插入或删除结点后,树的结构发生了变化,从而可能会破坏红黑树的性质。 为了维持插入、或删除结点后的树,仍然是一颗红黑树,所以有必要对树的结构做部分 调整,从而恢复红黑树的原本性质。 而为了恢复红黑性质而作的动作包括: 结点颜色的改变(重新着色),和结点的调整。 这部分结点调整工作,改变指针结构,即是通过左旋或右旋而达到目的。 从而使插入、或删除结点的树重新成为一颗新的红黑树。 ok,请看下图: 如上图所示,‘找茬’ 如果你看懂了上述俩幅图有什么区别时,你就知道什么是“左旋”,“右旋”。 在此,着重分析左旋算法: 左旋,如图所示(左->右),以 x->y 之间的链为“支轴”进行, 使 y 成为该新子树的根,x 成为 y 的左孩子,而 y 的左孩子则成为 x 的右孩子。 算法很简单,还有注意一点,各个结点从左往右,不论是左旋前还是左旋后,结点大小 都是从小到大。 左旋代码实现,分三步(注意我给的注释): The pseudocode for LEFT-ROTATE assumes that right[x] ≠ nil[T] and that the root's parent is nil[T]. LEFT-ROTATE(T, x) 1 y ← right[x] ▹ Set y. 2 right[x] ← left[y] //开始变化,y 的左孩子成为 x 的右孩子 3 if left[y] !=nil[T] 4 then p[left[y]] <- x 5 p[y] <- p[x] //y 成为 x 的父母 6 if p[x] = nil[T] 7 then root[T] <- y 8 else if x = left[p[x]] 9 then left[p[x]] ← y 10 else right[p[x]] ← y 11 left[y] ← x //x 成为 y 的左孩子(一月三日修正) 12 p[x] ← y //注,此段左旋代码,原书第一版英文版与第二版中文版,有所出入。 //个人觉得,第二版更精准。所以,此段代码以第二版中文版为准。 左旋、右旋都是对称的,且都是在 O(1)时间内完成。因为旋转时只有指针被改变, 而结点中的所有域都保持不变。 最后,贴出昨下午关于此右旋算法所画的图: 左旋(第 2 张图): //此图有点 bug。第 4 行的注释移到第 11 行。如上述代码所示。(一月三日修正) 二、左旋的一个实例 不做过多介绍,看下副图,一目了然。 LEFT-ROTATE(T, x)的操作过程(第 3 张图): -------------------------------------------------------------------------------------- 提醒,看下文之前,请首先务必明确,区别以下俩种操作: 1.红黑树插入、删除结点的操作 //如插入中,红黑树插入结点操作:RB-INSERT(T, z)。 2.红黑树已经插入、删除结点之后, 为了保持红黑树原有的红黑性质而做的恢复与保持红黑性质的操作。 //如插入中,为了恢复和保持原有红黑性质,所做的工作:RB-INSERT-FIXUP(T, z)。 ok,请继续。 三、红黑树的插入算法实现 RB-INSERT(T, z) //注意我给的注释... 1 y ← nil[T] // y 始终指向 x 的父结点。 2 x ← root[T] // x 指向当前树的根结点, 3 while x ≠ nil[T] 4 do y ← x 5 if key[z] < key[x] //向左,向右.. 6 then x ← left[x] 7 else x ← right[x] // 为了找到合适的插入点,x 探路跟踪路径,直到 x 成为 NIL 为止。 8 p[z] ← y // y 置为 插入结点 z 的父结点。 9 if y = nil[T] 10 then root[T] ← z 11 else if key[z] < key[y] 12 then left[y] ← z 13 else right[y] ← z //此 8-13 行,置 z 相关的指针。 14 left[z] ← nil[T] 15 right[z] ← nil[T] //设为空, 16 color[z] ← RED //将新插入的结点 z 作为红色 17 RB-INSERT-FIXUP(T, z) //因为将 z 着为红色,可能会违反某一红黑性质, //所以需要调用 RB-INSERT-FIXUP(T, z)来保持红黑性质。 17 行的 RB-INSERT-FIXUP(T, z) ,在下文会得到着重而具体的分析。 还记得,我开头说的那句话么, 是的,时刻记住,不论是左旋还是右旋,不论是插入、还是删除,都要记得恢复和保持红黑 树的 5 个性质。 四、调用 RB-INSERT-FIXUP(T, z)来保持和恢复红黑性质 RB-INSERT-FIXUP(T, z) 1 while color[p[z]] = RED 2 do if p[z] = left[p[p[z]]] 3 then y ← right[p[p[z]]] 4 if color[y] = RED 5 then color[p[z]] ← BLACK ▹ Case 1 6 color[y] ← BLACK ▹ Case 1 7 color[p[p[z]]] ← RED ▹ Case 1 8 z ← p[p[z]] ▹ Case 1 9 else if z = right[p[z]] 10 then z ← p[z] ▹ Case 2 11 LEFT-ROTATE(T, z) ▹ Case 2 12 color[p[z]] ← BLACK ▹ Case 3 13 color[p[p[z]]] ← RED ▹ Case 3 14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 15 else (same as then clause with "right" and "left" exchanged) 16 color[root[T]] ← BLACK //第 4 张图略: 五、红黑树插入的三种情况,即 RB-INSERT-FIXUP(T, z)。操作过程(第 5 张): //这幅图有个小小的问题,读者可能会产生误解。图中左侧所表明的情况 2、情况 3 所标的 位置都要标上一点。 //请以图中的标明的 case1、case2、case3 为准。一月三日。 六、红黑树插入的第一种情况(RB-INSERT-FIXUP(T, z)代码的具体分析一) 为了保证阐述清晰,重述下 RB-INSERT-FIXUP(T, z)的源码: RB-INSERT-FIXUP(T, z) 1 while color[p[z]] = RED 2 do if p[z] = left[p[p[z]]] 3 then y ← right[p[p[z]]] 4 if color[y] = RED 5 then color[p[z]] ← BLACK ▹ Case 1 6 color[y] ← BLACK ▹ Case 1 7 color[p[p[z]]] ← RED ▹ Case 1 8 z ← p[p[z]] ▹ Case 1 9 else if z = right[p[z]] 10 then z ← p[z] ▹ Case 2 11 LEFT-ROTATE(T, z) ▹ Case 2 12 color[p[z]] ← BLACK ▹ Case 3 13 color[p[p[z]]] ← RED ▹ Case 3 14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 15 else (same as then clause with "right" and "left" exchanged) 16 color[root[T]] ← BLACK //case1 表示情况 1,case2 表示情况 2,case3 表示情况 3. ok,如上所示,相信,你已看到了。 咱们,先来透彻分析红黑树插入的第一种情况: 插入情况 1,z 的叔叔 y 是红色的。 第一种情况,即上述代码的第 5-8 行: 5 then color[p[z]] ← BLACK ▹ Case 1 6 color[y] ← BLACK ▹ Case 1 7 color[p[p[z]]] ← RED ▹ Case 1 8 z ← p[p[z]] ▹ Case 1 如上图所示,a:z 为右孩子,b:z 为左孩子。 只有 p[z]和 y(上图 a 中 A 为 p[z],D 为 z,上图 b 中,B 为 p[z],D 为 y)都是红色的时 候,才会执行此情况 1. 咱们分析下上图的 a 情况,即 z 为右孩子时 因为 p[p[z]],即 c 是黑色,所以将 p[z]、y 都着为黑色(如上图 a 部分的右边), 此举解决 z、p[z]都是红色的问题,将 p[p[z]]着为红色,则保持了性质 5. ok,看下我昨天画的图(第 6 张): 红黑树插入的第一种情况完。 七、红黑树插入的第二种、第三种情况 插入情况 2:z 的叔叔 y 是黑色的,且 z 是右孩子 插入情况 3:z 的叔叔 y 是黑色的,且 z 是左孩子 这俩种情况,是通过 z 是 p[z]的左孩子,还是右孩子区别的。 参照上图,针对情况 2,z 是她父亲的右孩子,则为了保持红黑性质,左旋则变为情况 3,此时 z 为左孩子, 因为 z、p[z]都为黑色,所以不违反红黑性质(注,情况 3 中,z 的叔叔 y 是黑色的,否 则此种情况就变成上述情况 1 了)。 ok,我们已经看出来了,情况 2,情况 3 都违反性质 4(一个红结点的俩个儿子都是黑 色的)。 所以情况 2->左旋后->情况 3,此时情况 3 同样违反性质 4,所以情况 3->右旋,得到上 图的最后那部分。 注:情况 2、3 都只违反性质 4,其它的性质 1、2、3、5 都不违背。 好的,最后,看下我画的图(第 7 张): 八、接下来,进入红黑树的删除部分。 RB-DELETE(T, z) 1 if left[z] = nil[T] or right[z] = nil[T] 2 then y ← z 3 else y ← TREE-SUCCESSOR(z) 4 if left[y] ≠ nil[T] 5 then x ← left[y] 6 else x ← right[y] 7 p[x] ← p[y] 8 if p[y] = nil[T] 9 then root[T] ← x 10 else if y = left[p[y]] 11 then left[p[y]] ← x 12 else right[p[y]] ← x 13 if y 3≠ z 14 then key[z] ← key[y] 15 copy y's satellite data into z 16 if color[y] = BLACK //如果 y 是黑色的, 17 then RB-DELETE-FIXUP(T, x) //则调用 RB-DELETE-FIXUP(T, x) 18 return y //如果 y 不是黑色,是红色的,则当 y 被删除时,红黑性质仍然得以保持。 不做操作,返回。 //因为:1.树种各结点的黑高度都没有变化。2.不存在俩个相邻的红色结点。 //3.因为入宫 y 是红色的,就不可能是根。所以,根仍然是黑色的。 ok,第 8 张图,不必贴了。 九、红黑树删除之 4 种情况,RB-DELETE-FIXUP(T, x)之代码 RB-DELETE-FIXUP(T, x) 1 while x ≠ root[T] and color[x] = BLACK 2 do if x = left[p[x]] 3 then w ← right[p[x]] 4 if color[w] = RED 5 then color[w] ← BLACK ▹ Case 1 6 color[p[x]] ← RED ▹ Case 1 7 LEFT-ROTATE(T, p[x]) ▹ Case 1 8 w ← right[p[x]] ▹ Case 1 9 if color[left[w]] = BLACK and color[right[w]] = BLACK 10 then color[w] ← RED ▹ Case 2 11 x ← p[x] ▹ Case 2 12 else if color[right[w]] = BLACK 13 then color[left[w]] ← BLACK ▹ Case 3 14 color[w] ← RED ▹ Case 3 15 RIGHT-ROTATE(T, w) ▹ Case 3 16 w ← right[p[x]] ▹ Case 3 17 color[w] ← color[p[x]] ▹ Case 4 18 color[p[x]] ← BLACK ▹ Case 4 19 color[right[w]] ← BLACK ▹ Case 4 20 LEFT-ROTATE(T, p[x]) ▹ Case 4 21 x ← root[T] ▹ Case 4 22 else (same as then clause with "right" and "left" exchanged) 23 color[x] ← BLACK ok,很清楚,在此,就不贴第 9 张图了。 在下文的红黑树删除的 4 种情况,详细、具体分析了上段代码。 十、红黑树删除的 4 种情况 情况 1:x 的兄弟 w 是红色的。 情况 2:x 的兄弟 w 是黑色的,且 w 的俩个孩子都是黑色的。 情况 3:x 的兄弟 w 是黑色的,w 的左孩子是红色,w 的右孩子是黑色。 情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子时红色的。 操作流程图: ok,简单分析下,红黑树删除的 4 种情况: 针对情况 1:x 的兄弟 w 是红色的。 5 then color[w] ← BLACK ▹ Case 1 6 color[p[x]] ← RED ▹ Case 1 7 LEFT-ROTATE(T, p[x]) ▹ Case 1 8 w ← right[p[x]] ▹ Case 1 对策:改变 w、p[z]颜色,再对 p[x]做一次左旋,红黑性质得以继续保持。 x 的新兄弟 new w 是旋转之前 w 的某个孩子,为黑色。 所以,情况 1 转化成情况 2 或 3、4。 针对情况 2:x 的兄弟 w 是黑色的,且 w 的俩个孩子都是黑色的。 10 then color[w] ← RED ▹ Case 2 11 x <-p[x] ▹ Case 2 如图所示,w 的俩个孩子都是黑色的, 对策:因为 w 也是黑色的,所以 x 和 w 中得去掉一黑色,最后,w 变为红。 p[x]为新结点 x,赋给 x,x<-p[x]。 针对情况 3:x 的兄弟 w 是黑色的,w 的左孩子是红色,w 的右孩子是黑色。 13 then color[left[w]] ← BLACK ▹ Case 3 14 color[w] ← RED ▹ Case 3 15 RIGHT-ROTATE(T, w) ▹ Case 3 16 w ← right[p[x]] ▹ Case 3 w 为黑,其左孩子为红,右孩子为黑 对策:交换 w 和和其左孩子 left[w]的颜色。 即上图的 D、C 颜色互换。:D。 并对 w 进行右旋,而红黑性质仍然得以保持。 现在 x 的新兄弟 w 是一个有红色右孩子的黑结点,于是将情况 3 转化为情况 4. 针对情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子时红色的。 17 color[w] ← color[p[x]] ▹ Case 4 18 color[p[x]] ← BLACK ▹ Case 4 19 color[right[w]] ← BLACK ▹ Case 4 20 LEFT-ROTATE(T, p[x]) ▹ Case 4 21 x ← root[T] ▹ Case 4 x 的兄弟 w 为黑色,且 w 的右孩子为红色。 对策:做颜色修改,并对 p[x]做一次旋转,可以去掉 x 的额外黑色,来把 x 变成单独的黑色, 此举不破坏红黑性质。 将 x 置为根后,循环结束。 最后,贴上最后的第 10 张图: ok,红黑树删除的 4 中情况,分析完成。 结语:只要牢牢抓住红黑树的 5 个性质不放,而不论是树的左旋还是右旋, 不论是红黑树的插入、还是删除,都只为了保持和修复红黑树的 5 个性质而已。 顺祝各位, 元旦快乐。完。 July、二零一零年十二月三十日。 ------------------------------------------------------- 扩展阅读:Left-Leaning Red-Black Trees, Dagstuhl Workshop on Data Structures, Wadern, Germany, February, 2008. 直接下载:http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 版权声明 本 BLOG 内的此红黑树系列,总计六篇文章,是整个国内有史以来有关红黑树的最具代 表性,最具完整性,最具参考价值的资料。且,本人对此红黑树系列全部文章,享有版权, 任何人,任何组织,任何出版社不得侵犯本人版权相关利益,违者追究法律责任。谢谢。 五(续)、教你透彻了解红黑树 作者:July、saturnman 2010 年 12 月 29 日 文参考:Google、算法导论、STL 源码剖析、计算机程序设计艺术。 本人声明:个人原创,转载请注明出处。 推荐阅读:Left-Leaning Red-Black Trees, Dagstuhl Workshop on Data Structures, Wadern, Germany, February, 2008. 直接下载:http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf ------------------------------------------------ 红黑树系列,六篇文章于今日已经完成: 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 ------------------------------------------------ 一、红黑树的介绍 先来看下算法导论对 R-B Tree 的介绍: 红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是 Red 或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。 前面说了,红黑树,是一种二叉查找树,既然是二叉查找树,那么它必满足二叉查找树 的一般性质。 下面,在具体介绍红黑树之前,咱们先来了解下 二叉查找树的一般性质: 1.在一棵二叉查找树上,执行查找、插入、删除等操作,的时间复杂度为 O(lgn)。 因为,一棵由 n 个结点,随机构造的二叉查找树的高度为 lgn,所以顺理成章,一般操 作的执行时间为 O(lgn)。 //至于 n 个结点的二叉树高度为 lgn 的证明,可参考算法导论 第 12 章 二叉查找树 第 12.4 节。 2.但若是一棵具有 n 个结点的线性链,则此些操作最坏情况运行时间为 O(n)。 而红黑树,能保证在最坏情况下,基本的动态几何操作的时间均为 O(lgn)。 ok,我们知道,红黑树上每个结点内含五个域,color,key,left,right,p。如果相应 的指针域没有,则设为 NIL。 一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树: 1)每个结点要么是红的,要么是黑的。 2)根结点是黑的。 3)每个叶结点,即空结点(NIL)是黑的。 4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。 下图所示,即是一颗红黑树: 此图忽略了叶子和根部的父结点。 二、树的旋转知识 当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的 性质。 为了保持红黑树的性质,我们可以通过对树进行旋转,即修改树种某些结点的颜色及指 针结构,以达到对红黑树进行 插入、删除结点等操作时,红黑树依然能保持它特有的性质(如上文所述的,五点性质)。 树的旋转,分为左旋和右旋,以下借助图来做形象的解释和介绍: 1.左旋 如上图所示: 当在某个结点 pivot 上,做左旋操作时,我们假设它的右孩子 y 不是 NIL[T],pivot 可以 为树内任意右孩子而不是 NIL[T]的结点。 左旋以 pivot 到 y 之间的链为“支轴”进行,它使 y 成为该孩子树新的根,而 y 的左孩子 b 则成为 pivot 的右孩子。 来看算法导论对此操作的算法实现(以 x 代替上述的 pivot): LEFT-ROTATE(T, x) 1 y ← right[x] ▹ Set y. 2 right[x] ← left[y] ▹ Turn y's left subtree into x's right subtree. 3 p[left[y]] ← x 4 p[y] ← p[x] ▹ Link x's parent to y. 5 if p[x] = nil[T] 6 then root[T] ← y 7 else if x = left[p[x]] 8 then left[p[x]] ← y 9 else right[p[x]] ← y 10 left[y] ← x ▹ Put x on y's left. 11 p[x] ← y 2.右旋 右旋与左旋差不多,再此不做详细介绍。 对于树的旋转,能保持不变的只有原树的搜索性质,而原树的红黑性质则不能保持, 在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。 至于有些书如 STL 源码剖析有对双旋的描述,其实双旋只是单旋的两次应用,并无新 的内容,因此这里就不再介绍了,而且左右旋也是相互对称的,只要理解其中一种旋转就可 以了。 三、红黑树插入、删除操作的具体实现 三、I、ok,接下来,咱们来具体了解红黑树的插入操作。 向一棵含有 n 个结点的红黑树插入一个新结点的操作可以在 O(lgn)时间内完成。 算法导论: RB-INSERT(T, z) 1 y ← nil[T] 2 x ← root[T] 3 while x ≠ nil[T] 4 do y ← x 5 if key[z] < key[x] 6 then x ← left[x] 7 else x ← right[x] 8 p[z] ← y 9 if y = nil[T] 10 then root[T] ← z 11 else if key[z] < key[y] 12 then left[y] ← z 13 else right[y] ← z 14 left[z] ← nil[T] 15 right[z] ← nil[T] 16 color[z] ← RED 17 RB-INSERT-FIXUP(T, z) 咱们来具体分析下,此段代码: RB-INSERT(T, z),将 z 插入红黑树 T 之内。 为保证红黑性质在插入操作后依然保持,上述代码调用了一个辅助程序 RB-INSERT-FIXUP 来对结点进行重新着色,并旋转。 14 left[z] ← nil[T] 15 right[z] ← nil[T] //保持正确的树结构 第 16 行,将 z 着为红色,由于将 z 着为红色可能会违背某一条红黑树的性质, 所以,在第 17 行,调用 RB-INSERT-FIXUP(T,z)来保持红黑树的性质。 RB-INSERT-FIXUP(T, z),如下所示: 1 while color[p[z]] = RED 2 do if p[z] = left[p[p[z]]] 3 then y ← right[p[p[z]]] 4 if color[y] = RED 5 then color[p[z]] ← BLACK ▹ Case 1 6 color[y] ← BLACK ▹ Case 1 7 color[p[p[z]]] ← RED ▹ Case 1 8 z ← p[p[z]] ▹ Case 1 9 else if z = right[p[z]] 10 then z ← p[z] ▹ Case 2 11 LEFT-ROTATE(T, z) ▹ Case 2 12 color[p[z]] ← BLACK ▹ Case 3 13 color[p[p[z]]] ← RED ▹ Case 3 14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 15 else (same as then clause with "right" and "left" exchanged) 16 color[root[T]] ← BLACK ok,参考一网友的言论,用自己的语言,再来具体解剖下上述俩段代码。 为了保证阐述清晰,我再写下红黑树的 5 个性质: 1)每个结点要么是红的,要么是黑的。 2)根结点是黑的。 3)每个叶结点,即空结点(NIL)是黑的。 4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。 在对红黑树进行插入操作时,我们一般总是插入红色的结点,因为这样可以在插入过程 中尽量避免对树的调整。 那么,我们插入一个结点后,可能会使原树的哪些性质改变列? 由于,我们是按照二叉树的方式进行插入,因此元素的搜索性质不会改变。 如果插入的结点是根结点,性质 2 会被破坏,如果插入结点的父结点是红色,则会破坏 性质 4。 因此,总而言之,插入一个红色结点只会破坏性质 2 或性质 4。 我们的回复策略很简单, 其一、把出现违背红黑树性质的结点向上移,如果能移到根结点,那么很容易就能通过 直接修改根结点来恢复红黑树的性质。直接通过修改根结点来恢复红黑树应满足的性质。 其二、穷举所有的可能性,之后把能归于同一类方法处理的归为同一类,不能直接处理 的化归到下面的几种情况, //注:以下情况 3、4、5 与上述算法导论上的代码 RB-INSERT-FIXUP(T, z),相对应: 情况 1:插入的是根结点。 原树是空树,此情况只会违反性质 2。 对策:直接把此结点涂为黑色。 情况 2:插入的结点的父结点是黑色。 此不会违反性质 2 和性质 4,红黑树没有被破坏。 对策:什么也不做。 情况 3:当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色。 此时父结点的父结点一定存在,否则插入前就已不是红黑树。 与此同时,又分为父结点是祖父结点的左子还是右子,对于对称性,我们只要解开一个方向 就可以了。 在此,我们只考虑父结点为祖父左子的情况。 同时,还可以分为当前结点是其父结点的左子还是右子,但是处理方式是一样的。我们 将此归为同一类。 对策:将当前节点的父节点和叔叔节点涂黑,祖父结点涂红,把当前结点指向祖父节点, 从新的当前节点重新开始算法。 针对情况 3,变化前(图片来源:saturnman)[插入 4 节点]: 变化后: 情况 4:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右 子 对策:当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。 如下图所示,变化前[插入 7 节点]: 变化后: 情况 5:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左 子 解法:父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋 如下图所示[插入 2 节点] 变化后: ================================================ 三、II、ok,接下来,咱们最后来了解,红黑树的删除操作: 算法导论一书,给的算法实现: RB-DELETE(T, z) 单纯删除结点的总操作 1 if left[z] = nil[T] or right[z] = nil[T] 2 then y ← z 3 else y ← TREE-SUCCESSOR(z) 4 if left[y] ≠ nil[T] 5 then x ← left[y] 6 else x ← right[y] 7 p[x] ← p[y] 8 if p[y] = nil[T] 9 then root[T] ← x 10 else if y = left[p[y]] 11 then left[p[y]] ← x 12 else right[p[y]] ← x 13 if y 3≠ z 14 then key[z] ← key[y] 15 copy y's satellite data into z 16 if color[y] = BLACK 17 then RB-DELETE-FIXUP(T, x) 18 return y RB-DELETE-FIXUP(T, x) 恢复与保持红黑性质的工作 1 while x ≠ root[T] and color[x] = BLACK 2 do if x = left[p[x]] 3 then w ← right[p[x]] 4 if color[w] = RED 5 then color[w] ← BLACK ▹ Case 1 6 color[p[x]] ← RED ▹ Case 1 7 LEFT-ROTATE(T, p[x]) ▹ Case 1 8 w ← right[p[x]] ▹ Case 1 9 if color[left[w]] = BLACK and color[right[w]] = BLACK 10 then color[w] ← RED ▹ Case 2 11 x p[x] ▹ Case 2 12 else if color[right[w]] = BLACK 13 then color[left[w]] ← BLACK ▹ Case 3 14 color[w] ← RED ▹ Case 3 15 RIGHT-ROTATE(T, w) ▹ Case 3 16 w ← right[p[x]] ▹ Case 3 17 color[w] ← color[p[x]] ▹ Case 4 18 color[p[x]] ← BLACK ▹ Case 4 19 color[right[w]] ← BLACK ▹ Case 4 20 LEFT-ROTATE(T, p[x]) ▹ Case 4 21 x ← root[T] ▹ Case 4 22 else (same as then clause with "right" and "left" exchanged) 23 color[x] ← BLACK 为了保证以下的介绍与阐述清晰,我第三次重写下红黑树的 5 个性质 1)每个结点要么是红的,要么是黑的。 2)根结点是黑的。 3)每个叶结点,即空结点(NIL)是黑的。 4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。 (相信,重述了 3 次,你应该有深刻记忆了。:D) saturnman: 红黑树删除的几种情况: --------------------------------------------------------------------------------------- 博主提醒: 以下所有的操作,是针对红黑树已经删除结点之后, 为了恢复和保持红黑树原有的 5 点性质,所做的恢复工作。 前面,我已经说了,因为插入、或删除结点后, 可能会违背、或破坏红黑树的原有的性质, 所以为了使插入、或删除结点后的树依然维持为一棵新的红黑树, 那就要做俩方面的工作: 1、部分结点颜色,重新着色 2、调整部分指针的指向,即左旋、右旋。 而下面所有的文字,则是针对红黑树删除结点后,所做的修复红黑树性质的工作。 二零一一年一月七日更新。 --------------------------------------------------------------------------------------- (注:以下的情况 3、4、5、6,与上述算法导论之代码 RB-DELETE-FIXUP(T, x) 恢复与 保持 中 case1,case2,case3,case4 相对应。) 情况 1:当前节点是红色 解法,直接把当前节点染成黑色,结束。 此时红黑树性质全部恢复。 情况 2:当前节点是黑色且是根节点 解法:什么都不做,结束 情况 3:当前节点是黑色,且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)。 解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法(我们只讨论当前 节点是其父节点左孩子时的情况)。 然后,针对父节点做一次左旋。此变换后原红黑树性质 5 不变,而把问题转化为兄弟节 点为黑色的情况。 3.变化前: 3.变化后: 情况 4:当前节点是黑色,且兄弟是黑色,且兄弟节点的两个子节点全为黑色。 解法:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当 前节点,重新进入算法。(此变换后性质 5 不变) 4.变化前 4.变化后 情况 5:当前节点颜色是黑色,兄弟节点是黑色,兄弟的左子是红色,右子是黑色。 解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋, 之后重新进入算法。此是把当前的情况转化为情况 6,而性质 5 得以保持。 5.变化前: 5.变化后: 情况 6:当前节点颜色是黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节 点左子的颜色任意。 解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点 右子染成黑色, 之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确。 6.变化前: 6.变化后: 限于篇幅,不再过多赘述。更多,可参考算法导论或下文我写的第二篇文章。 完。 July、二零一零年十二月二十九日初稿。三十日凌晨修订。行文 3 个小时以上。 今下午画红黑树画了好几个钟头,贴俩张图: 红黑树插入的 3 种情况: 红黑树删除的 4 种情况: ok,只贴俩张,更多,参考我写的关于红黑树的第二篇文章: 红黑树算法的层层剖析与逐步实现[推荐] http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx 这篇文章针对算法实现源码分十层,层层、逐层剖析,相信,更清晰易懂。 或者,saturnman 的这篇文 章: http://saturnman.blog.163.com/blog/static/5576112010969420383/。 July、二零一零年十二月三十一日、最后更新。 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 版权声明 本 BLOG 内的此红黑树系列,总计六篇文章,是整个国内有史以来有关红黑 树的最具代表性,最具完整性,最具参考价值的资料。且,本人对此红黑树系列 全部文章,享有版权,任何人,任何组织,任何出版社不得侵犯本人版权相关利 益,违者追究法律责任。谢谢。 附:红黑树研究系列六篇(另四篇): 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 红黑树 c 源码实现与剖析 作者:July 、那谁 时间:二零一一年一月三日 ------------------------------------------------------------------------------------- 前言: 红黑树作为一种经典而高级的数据结构,相信,已经被不少人实现过,但不是因为程序 不够完善而无法运行,就是因为程序完全没有注释,初学者根本就看不懂。 此份红黑树的 c 源码最初从 linux-lib-rbtree.c 而来,后经一网友那谁用 c 写了出来。 在此,向原作者表示敬意。但原来的程序存在诸多问题,且全程序没有任何一行注释。没有 一行注释的程序,令程序的价值大打折扣。 所以,我特把这份源码放到了 windows xp+vc 6.0 上,一行一行的完善修正,一行一 行的给它添加注释,至此,红黑树 c 带完美注释的源码,就摆在了您眼前,有不妥、不正之 处,还望不吝指正。 ------------------------------------------------------ 红黑树的六篇文章: 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 ------------------------------------------------------ ok,咱们开始吧。 相信,经过我前俩篇博文对红黑树的介绍,你应该对红黑树有了透彻的理解了(没看过 的朋友,可事先查上面的倆篇文章,或与此文的源码剖析对应着看)。 本套源码剖析把重点放在红黑树的 3 种插入情况,与红黑树的 4 种删除情况。其余的能 从略则尽量简略。 目录: 一、左旋代码分析 二、右旋 三、红黑树查找结点 四、红黑树的插入 五、红黑树的 3 种插入情况 六、红黑树的删除 七、红黑树的 4 种删除情况 八、测试用例 好的,咱们还是先从树的左旋、右旋代码,开始(大部分分析,直接给注释): view plaincopy to clipboardprint? 1. //一、左旋代码分析 2. /*----------------------------------------------------------- 3. | node right 4. | / \ ==> / \ 5. | a right node y 6. | / \ / \ 7. | b y a b //左旋 8. -----------------------------------------------------------*/ 9. static rb_node_t* rb_rotate_left(rb_node_t* node, rb_node_t* root) 10. { 11. rb_node_t* right = node->right; //指定指针指向 right<--node->right 12. 13. if ((node->right = right->left)) 14. { 15. right->left->parent = node; //好比上面的注释图,node 成为 b 的父母 16. } 17. right->left = node; //node 成为 right 的左孩子 18. 19. if ((right->parent = node->parent)) 20. { 21. if (node == node->parent->right) 22. { 23. node->parent->right = right; 24. } 25. else 26. { 27. node->parent->left = right; 28. } 29. } 30. else 31. { 32. root = right; 33. } 34. node->parent = right; //right 成为 node 的父母 35. 36. return root; 37. } 38. 39. 40. //二、右旋 41. /*----------------------------------------------------------- 42. | node left 43. | / \ / \ 44. | left y ==> a node 45. | / \ / \ 46. | a b b y //右旋与左旋差不多,分析略过 47. -----------------------------------------------------------*/ 48. static rb_node_t* rb_rotate_right(rb_node_t* node, rb_node_t* root) 49. { 50. rb_node_t* left = node->left; 51. 52. if ((node->left = left->right)) 53. { 54. left->right->parent = node; 55. } 56. left->right = node; 57. 58. if ((left->parent = node->parent)) 59. { 60. if (node == node->parent->right) 61. { 62. node->parent->right = left; 63. } 64. else 65. { 66. node->parent->left = left; 67. } 68. } 69. else 70. { 71. root = left; 72. } 73. node->parent = left; 74. 75. return root; 76. } 77. 78. 79. //三、红黑树查找结点 80. //---------------------------------------------------- 81. //rb_search_auxiliary:查找 82. //rb_node_t* rb_search:返回找到的结点 83. //---------------------------------------------------- 84. static rb_node_t* rb_search_auxiliary(key_t key, rb_node_t* root, rb_node_t* * save) 85. { 86. rb_node_t *node = root, *parent = NULL; 87. int ret; 88. 89. while (node) 90. { 91. parent = node; 92. ret = node->key - key; 93. if (0 < ret) 94. { 95. node = node->left; 96. } 97. else if (0 > ret) 98. { 99. node = node->right; 100. } 101. else 102. { 103. return node; 104. } 105. } 106. 107. if (save) 108. { 109. *save = parent; 110. } 111. 112. return NULL; 113. } 114. 115. //返回上述 rb_search_auxiliary 查找结果 116. rb_node_t* rb_search(key_t key, rb_node_t* root) 117. { 118. return rb_search_auxiliary(key, root, NULL); 119. } 120. 121. 122. //四、红黑树的插入 123. //--------------------------------------------------------- 124. //红黑树的插入结点 125. rb_node_t* rb_insert(key_t key, data_t data, rb_node_t* root) 126. { 127. rb_node_t *parent = NULL, *node; 128. 129. parent = NULL; 130. if ((node = rb_search_auxiliary(key, root, &parent))) //调用 rb_search_auxiliary 找到插入结 131. 132. 点的地方 133. { 134. return root; 135. } 136. 137. node = rb_new_node(key, data); //分配结点 138. node->parent = parent; 139. node->left = node->right = NULL; 140. node->color = RED; 141. 142. if (parent) 143. { 144. if (parent->key > key) 145. { 146. parent->left = node; 147. } 148. else 149. { 150. parent->right = node; 151. } 152. } 153. else 154. { 155. root = node; 156. } 157. 158. return rb_insert_rebalance(node, root); //插入结点后,调用 rb_insert_rebalance 修复红黑树 159. 160. 的性质 161. } 162. 163. 164. //五、红黑树的 3 种插入情况 165. //接下来,咱们重点分析针对红黑树插入的 3 种情况,而进行的修复工作。 166. //-------------------------------------------------------------- 167. //红黑树修复插入的 3 种情况 168. //为了在下面的注释中表示方便,也为了让下述代码与我的倆篇文章相对应, 169. //用 z 表示当前结点,p[z]表示父母、p[p[z]]表示祖父、y 表示叔叔。 170. //-------------------------------------------------------------- 171. static rb_node_t* rb_insert_rebalance(rb_node_t *node, rb_node_t *root) 172. { 173. rb_node_t *parent, *gparent, *uncle, *tmp; //父母 p[z]、祖父 p[p[z]]、叔 叔 y、临时结点*tmp 174. 175. while ((parent = node->parent) && parent->color == RED) 176. { //parent 为 node 的父母,且当父母的颜色为红时 177. gparent = parent->parent; //gparent 为祖父 178. 179. if (parent == gparent->left) //当祖父的左孩子即为父母时。 180. //其实上述几行语句,无非就是理顺孩子、父母、祖 父的关系。:D。 181. { 182. uncle = gparent->right; //定义叔叔的概念,叔叔 y 就是父母的右孩子。 183. 184. if (uncle && uncle->color == RED) //情况 1:z 的叔叔 y 是红色的 185. { 186. uncle->color = BLACK; //将叔叔结点 y 着为黑色 187. parent->color = BLACK; //z 的父母 p[z]也着为黑色。解决 z,p[z] 都是红色的问题。 188. gparent->color = RED; 189. node = gparent; //将祖父当做新增结点 z,指针 z 上移俩层,且着 为红色。 190. //上述情况 1 中,只考虑了 z 作为父母的右孩子的情况。 191. } 192. else //情况 2:z 的叔叔 y 是黑色的, 193. { 194. if (parent->right == node) //且 z 为右孩子 195. { 196. root = rb_rotate_left(parent, root); //左旋[结点 z,与父母 结点] 197. tmp = parent; 198. parent = node; 199. node = tmp; //parent 与 node 互换角色 200. } 201. //情况 3:z 的叔叔 y 是黑色的,此时 z 成为了左孩子。 202. //注意,1:情况 3 是由上述情况 2 变化而来的。 203. //......2:z 的叔叔总是黑色的,否则就是情况 1 了。 204. parent->color = BLACK; //z 的父母 p[z]着为黑色 205. gparent->color = RED; //原祖父结点着为红色 206. root = rb_rotate_right(gparent, root); //右旋[结点 z,与祖父结 点] 207. } 208. } 209. 210. else 211. { 212. //这部分是特别为情况 1 中,z 作为左孩子情况,而写的。 213. uncle = gparent->left; //祖父的左孩子作为叔叔结点。[原理还是与上部 分一样的] 214. if (uncle && uncle->color == RED) //情况 1:z 的叔叔 y 是红色的 215. { 216. uncle->color = BLACK; 217. parent->color = BLACK; 218. gparent->color = RED; 219. node = gparent; //同上。 220. } 221. else //情况 2:z 的叔叔 y 是黑色的, 222. { 223. if (parent->left == node) //且 z 为左孩子 224. { 225. root = rb_rotate_right(parent, root); //以结点 parent、 root 右旋 226. tmp = parent; 227. parent = node; 228. node = tmp; //parent 与 node 互换角色 229. } 230. //经过情况 2 的变化,成为了情况 3. 231. parent->color = BLACK; 232. gparent->color = RED; 233. root = rb_rotate_left(gparent, root); //以结点 gparent 和 root 左旋 234. } 235. } 236. } 237. 238. root->color = BLACK; //根结点,不论怎样,都得置为黑色。 239. return root; //返回根结点。 240. } 241. 242. 243. //六、红黑树的删除 244. //------------------------------------------------------------ 245. //红黑树的删除结点 246. rb_node_t* rb_erase(key_t key, rb_node_t *root) 247. { 248. rb_node_t *child, *parent, *old, *left, *node; 249. color_t color; 250. 251. if (!(node = rb_search_auxiliary(key, root, NULL))) //调用 rb_search_auxiliary 查找要删除的 252. 253. 结点 254. { 255. printf("key %d is not exist!\n"); 256. return root; 257. } 258. 259. old = node; 260. 261. if (node->left && node->right) 262. { 263. node = node->right; 264. while ((left = node->left) != NULL) 265. { 266. node = left; 267. } 268. child = node->right; 269. parent = node->parent; 270. color = node->color; 271. 272. if (child) 273. { 274. child->parent = parent; 275. } 276. if (parent) 277. { 278. if (parent->left == node) 279. { 280. parent->left = child; 281. } 282. else 283. { 284. parent->right = child; 285. } 286. } 287. else 288. { 289. root = child; 290. } 291. 292. if (node->parent == old) 293. { 294. parent = node; 295. } 296. 297. node->parent = old->parent; 298. node->color = old->color; 299. node->right = old->right; 300. node->left = old->left; 301. 302. if (old->parent) 303. { 304. if (old->parent->left == old) 305. { 306. old->parent->left = node; 307. } 308. else 309. { 310. old->parent->right = node; 311. } 312. } 313. else 314. { 315. root = node; 316. } 317. 318. old->left->parent = node; 319. if (old->right) 320. { 321. old->right->parent = node; 322. } 323. } 324. else 325. { 326. if (!node->left) 327. { 328. child = node->right; 329. } 330. else if (!node->right) 331. { 332. child = node->left; 333. } 334. parent = node->parent; 335. color = node->color; 336. 337. if (child) 338. { 339. child->parent = parent; 340. } 341. if (parent) 342. { 343. if (parent->left == node) 344. { 345. parent->left = child; 346. } 347. else 348. { 349. parent->right = child; 350. } 351. } 352. else 353. { 354. root = child; 355. } 356. } 357. 358. free(old); 359. 360. if (color == BLACK) 361. { 362. root = rb_erase_rebalance(child, parent, root); //调用 rb_erase_rebalance 来恢复红黑树性 363. 364. 质 365. } 366. 367. return root; 368. } 369. 370. 371. //七、红黑树的 4 种删除情况 372. //---------------------------------------------------------------- 373. //红黑树修复删除的 4 种情况 374. //为了表示下述注释的方便,也为了让下述代码与我的倆篇文章相对应, 375. //x 表示要删除的结点,*other、w 表示兄弟结点, 376. //---------------------------------------------------------------- 377. static rb_node_t* rb_erase_rebalance(rb_node_t *node, rb_node_t *parent, rb _node_t *root) 378. { 379. rb_node_t *other, *o_left, *o_right; //x 的兄弟*other,兄弟左孩子 *o_left,*o_right 380. 381. while ((!node || node->color == BLACK) && node != root) 382. { 383. if (parent->left == node) 384. { 385. other = parent->right; 386. if (other->color == RED) //情况 1:x 的兄弟 w 是红色的 387. { 388. other->color = BLACK; 389. parent->color = RED; //上俩行,改变颜色,w->黑、p[x]->红。 390. root = rb_rotate_left(parent, root); //再对 p[x]做一次左旋 391. other = parent->right; //x 的新兄弟 new w 是旋转之前 w 的某个孩 子。其实就是左旋后 392. 393. 的效果。 394. } 395. if ((!other->left || other->left->color == BLACK) && 396. (!other->right || other->right->color == BLACK)) 397. //情况 2:x 的兄弟 w 是黑色,且 w 的俩个孩子也 398. 399. 都是黑色的 400. 401. { //由于 w 和 w 的俩个孩子都是黑色的,则在 x 和 w 上得去掉一黑色, 402. other->color = RED; //于是,兄弟 w 变为红色。 403. node = parent; //p[x]为新结点 x 404. parent = node->parent; //x<-p[x] 405. } 406. else //情况 3:x 的兄弟 w 是黑色的, 407. { //且,w 的左孩子是红色,右孩子为黑色。 408. if (!other->right || other->right->color == BLACK) 409. { 410. if ((o_left = other->left)) //w 和其左孩子 left[w],颜色交 换。 411. { 412. o_left->color = BLACK; //w的左孩子变为由黑->红色 413. } 414. other->color = RED; //w 由黑->红 415. root = rb_rotate_right(other, root); //再对 w 进行右旋, 从而红黑性质恢复。 416. other = parent->right; //变化后的,父结点的右孩子, 作为新的兄弟结点 417. 418. w。 419. } 420. //情况 4:x 的兄弟 w 是黑色的 421. 422. other->color = parent->color; //把兄弟节点染成当前节点父节点 的颜色。 423. parent->color = BLACK; //把当前节点父节点染成黑色 424. if (other->right) //且 w 的右孩子是红 425. { 426. other->right->color = BLACK; //兄弟节点 w 右孩子染成黑 色 427. } 428. root = rb_rotate_left(parent, root); //并再做一次左旋 429. node = root; //并把 x 置为根。 430. break; 431. } 432. } 433. //下述情况与上述情况,原理一致。分析略。 434. else 435. { 436. other = parent->left; 437. if (other->color == RED) 438. { 439. other->color = BLACK; 440. parent->color = RED; 441. root = rb_rotate_right(parent, root); 442. other = parent->left; 443. } 444. if ((!other->left || other->left->color == BLACK) && 445. (!other->right || other->right->color == BLACK)) 446. { 447. other->color = RED; 448. node = parent; 449. parent = node->parent; 450. } 451. else 452. { 453. if (!other->left || other->left->color == BLACK) 454. { 455. if ((o_right = other->right)) 456. { 457. o_right->color = BLACK; 458. } 459. other->color = RED; 460. root = rb_rotate_left(other, root); 461. other = parent->left; 462. } 463. other->color = parent->color; 464. parent->color = BLACK; 465. if (other->left) 466. { 467. other->left->color = BLACK; 468. } 469. root = rb_rotate_right(parent, root); 470. node = root; 471. break; 472. } 473. } 474. } 475. 476. if (node) 477. { 478. node->color = BLACK; //最后将 node[上述步骤置为了根结点],改为黑色。 479. } 480. return root; //返回 root 481. } 482. 483. 484. //八、测试用例 485. //主函数 486. int main() 487. { 488. int i, count = 100; 489. key_t key; 490. rb_node_t* root = NULL, *node = NULL; 491. 492. srand(time(NULL)); 493. for (i = 1; i < count; ++i) 494. { 495. key = rand() % count; 496. if ((root = rb_insert(key, i, root))) 497. { 498. printf("[i = %d] insert key %d success!\n", i, key); 499. } 500. else 501. { 502. printf("[i = %d] insert key %d error!\n", i, key); 503. exit(-1); 504. } 505. 506. if ((node = rb_search(key, root))) 507. { 508. printf("[i = %d] search key %d success!\n", i, key); 509. } 510. else 511. { 512. printf("[i = %d] search key %d error!\n", i, key); 513. exit(-1); 514. } 515. if (!(i % 10)) 516. { 517. if ((root = rb_erase(key, root))) 518. { 519. printf("[i = %d] erase key %d success\n", i, key); 520. } 521. else 522. { 523. printf("[i = %d] erase key %d error\n", i, key); 524. } 525. } 526. } 527. 528. return 0; 529. } ok,完。 后记: 一、欢迎任何人就此份源码,以及我的前述倆篇文章,进行讨论、提议。 但任何人,引用此份源码剖析,必须得注明作者本人 July 以及出处。 红黑树系列,已经写了三篇文章,相信,教你透彻了解红黑树的目的,应该达到了。 二、本文完整源码,请到此处下载: http://download.csdn.net/source/2958890 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 转载本 BLOG 内任何文章,请以超链接形式注明出处。非常感谢。 一步一图一代码,一定要让你真正彻底明白红黑树 作者:July 二零一一年一月九日 -------------------------------------------------------------------------------------------------------------------------------- 本文参考: I、 The Art of Computer Programming Volume I II、 Introduction to Algorithms, Second Edition III、The Annotated STL Sources IV、 Wikipedia V、 Algorithms In C Third Edition VI、 本人写的关于红黑树的前三篇文章: 第一篇:教你透彻了解红黑树: http://blog.csdn.net/v_JULY_v/archive/2010/12/29/6105630.aspx 第二篇:红黑树算法的层层剖析与逐步实现 http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx 第三篇:教你彻底实现红黑树:红黑树的 c 源码实现与剖析 http://blog.csdn.net/v_JULY_v/archive/2011/01/03/6114226.aspx -------------------------------------------------------------------------------------- 前言: 1、有读者反应,说看了我的前几篇文章,对红黑树的了解还是不够透彻。 2、我个人觉得,如果我一步一步,用图+代码来阐述各种插入、删除情况,可能会更直观 易懂。 3、既然写了红黑树,那么我就一定要把它真正写好,让读者真正彻底明白红黑树。 本文相对我前面红黑树相关的 3 篇文章,主要有以下几点改进: 1.图、文字叙述、代码编写,彼此对应,明朗而清晰。 2.宏观总结,红黑树的性质与插入、删除情况的认识。 3.代码来的更直接,结合图,给你最直观的感受,彻底明白红黑树。 ok,首先,以下几点,你现在应该是要清楚明白了的: I、红黑树的五个性质: 1)每个结点要么是红的,要么是黑的。 2)根结点是黑的。 3)每个叶结点,即空结点(NIL)是黑的。 4)如果一个结点是红的,那么它的俩个儿子都是黑的。 5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。 II、红黑树插入的几种情况: 情况 1,z 的叔叔 y 是红色的。 情况 2:z 的叔叔 y 是黑色的,且 z 是右孩子 情况 3:z 的叔叔 y 是黑色的,且 z 是左孩子 III、红黑树删除的几种情况。 情况 1:x 的兄弟 w 是红色的。 情况 2:x 的兄弟 w 是黑色的,且 w 的俩个孩子都是黑色的。 情况 3:x 的兄弟 w 是黑色的,且 w 的左孩子是红色,w 的右孩子是黑色。 情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子是红色的。 除此之外,还得明确一点: IV、我们知道,红黑树插入、或删除结点后, 可能会违背、或破坏红黑树的原有的性质, 所以为了使插入、或删除结点后的树依然维持为一棵新的红黑树, 那就要做俩方面的工作: 1、部分结点颜色,重新着色 2、调整部分指针的指向,即左旋、右旋。 V、并区别以下俩种操作: 1)红黑树插入、删除结点的操作,RB-INSERT(T, z),RB-DELETE(T, z) 2).红黑树已经插入、删除结点之后, 为了保持红黑树原有的红黑性质而做的恢复与保持红黑性质的操作。 如 RB-INSERT-FIXUP(T, z),RB-DELETE-FIXUP(T, x) 以上这 5 点,我已经在我前面的 2 篇文章,都已阐述过不少次了,希望,你现在已经透彻 明了。 --------------------------------------------------------------------- 本文,着重图解分析红黑树插入、删除结点后为了维持红黑性质而做修复工作的各种情况。 [下文各种插入、删除的情况,与我的第二篇文章,红黑树算法的实现与剖析相对应] ok,开始。 一、在下面的分析中,我们约定: 要插入的节点为,N 父亲节点,P 祖父节点,G 叔叔节点,U 兄弟节点,S 如下图所示,找一个节点的祖父和叔叔节点: node grandparent(node n) //祖父 { return n->parent->parent; } node uncle(node n) //叔叔 { if (n->parent == grandparent(n)->left) return grandparent(n)->right; else return grandparent(n)->left; } 二、红黑树插入的几种情况 情形 1: 新节点 N 位于树的根上,没有父节点 void insert_case1(node n) { if (n->parent == NULL) n->color = BLACK; else insert_case2(n); } 情形 2: 新节点的父节点 P 是黑色 void insert_case2(node n) { if (n->parent->color == BLACK) return; /* 树仍旧有效 */ else insert_case3(n); } 情形 3:父节点 P、叔叔节点 U,都为红色, [对应第二篇文章中,的情况 1:z 的叔叔是红色的。] void insert_case3(node n) { if (uncle(n) != NULL && uncle(n)->color == RED) { n->parent->color = BLACK; uncle(n)->color = BLACK; grandparent(n)->color = RED; insert_case1(grandparent(n)); //因为祖父节点可能是红色的,违反性质 4, 递归情形 1. } else insert_case4(n); //否则,叔叔是黑色的,转到下述情形 4 处理。 此时新插入节点 N 做为 P 的左子节点或右子节点都属于上述情形 3,上图仅显示 N 做为 P 左子的情形。 情形 4: 父节点 P 是红色,叔叔节点 U 是黑色或 NIL; 插入节点 N 是其父节点 P 的右孩子,而父节点 P 又是其父节点的左孩子。 [对应我第二篇文章中,的情况 2:z 的叔叔是黑色的,且 z 是右孩子] void insert_case4(node n) { if (n == n->parent->right && n->parent == grandparent(n)->left) { rotate_left(n->parent); n = n->left; } else if (n == n->parent->left && n->parent == grandparent(n)->right) { rotate_right(n->parent); n = n->right; } insert_case5(n); //转到下述情形 5 处理。 情形 5: 父节点 P 是红色,而叔父节点 U 是黑色或 NIL, 要插入的节点 N 是其父节点的左孩子,而父节点 P 又是其父 G 的左孩子。 [对应我第二篇文章中,情况 3:z 的叔叔是黑色的,且 z 是左孩子。] void insert_case5(node n) { n->parent->color = BLACK; grandparent(n)->color = RED; if (n == n->parent->left && n->parent == grandparent(n)->left) { rotate_right(grandparent(n)); } else { /* 反情况,N 是其父节点的右孩子,而父节点 P 又是其父 G 的右孩子 */ rotate_left(grandparent(n)); } } 三、红黑树删除的几种情况 上文我们约定,兄弟节点设为 S,我们使用下述函数找到兄弟节点: struct node * sibling(struct node *n) //找兄弟节点 { if (n == n->parent->left) return n->parent->right; else return n->parent->left; } 情况 1: N 是新的根。 void delete_case1(struct node *n) { if (n->parent != NULL) delete_case2(n); } 情形 2:兄弟节点 S 是红色 [对应我第二篇文章中,情况 1:x 的兄弟 w 是红色的。] void delete_case2(struct node *n) { struct node *s = sibling(n); if (s->color == RED) { n->parent->color = RED; s->color = BLACK; if (n == n->parent->left) rotate_left(n->parent); //左旋 else rotate_right(n->parent); } delete_case3(n); } 情况 3: 兄弟节点 S 是黑色的,且 S 的俩个儿子都是黑色的。但 N 的父节点 P,是黑色。 [对应我第二篇文章中,情况 2:x 的兄弟 w 是黑色的,且兄弟 w 的俩个儿子都是黑色的。 (这里,父节点 P 为黑)] void delete_case3(struct node *n) { struct node *s = sibling(n); if ((n->parent->color == BLACK) && (s->color == BLACK) && (s->left->color == BLACK) && (s->right->color == BLACK)) { s->color = RED; delete_case1(n->parent); } else delete_case4(n); } 情况 4: 兄弟节点 S 是黑色的、S 的儿子也都是黑色的,但是 N 的父亲 P,是红色。 [还是对应我第二篇文章中,情况 2:x 的兄弟 w 是黑色的,且 w 的俩个孩子都是黑色的。 (这里,父节点 P 为红)] void delete_case4(struct node *n) { struct node *s = sibling(n); if ((n->parent->color == RED) && (s->color == BLACK) && (s->left->color == BLACK) && (s->right->color == BLACK)) { s->color = RED; n->parent->color = BLACK; } else delete_case5(n); } 情况 5: 兄弟 S 为黑色,S 的左儿子是红色,S 的右儿子是黑色,而 N 是它父亲的左儿子。 //此种情况,最后转化到下面的情况 6。 [对应我第二篇文章中,情况 3:x 的兄弟 w 是黑色的,w 的左孩子是红色,w 的右孩子是 黑色。] void delete_case5(struct node *n) { struct node *s = sibling(n); if (s->color == BLACK) if ((n == n->parent->left) && (s->right->color == BLACK) && (s->left->color == RED)) { // this last test is trivial too due to cases 2-4. s->color = RED; s->left->color = BLACK; rotate_right(s); } else if ((n == n->parent->right) && (s->left->color == BLACK) && (s->right->color == RED)) { // this last test is trivial too due to cases 2-4. s->color = RED; s->right->color = BLACK; rotate_left(s); } } delete_case6(n); //转到情况 6。 情况 6: 兄弟节点 S 是黑色,S 的右儿子是红色,而 N 是它父亲的左儿子。 [对应我第二篇文章中,情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子时红色的。] void delete_case6(struct node *n) { struct node *s = sibling(n); s->color = n->parent->color; n->parent->color = BLACK; if (n == n->parent->left) { s->right->color = BLACK; rotate_left(n->parent); } else { s->left->color = BLACK; rotate_right(n->parent); } } //呵呵,画这 12 张图,直接从中午画到了晚上。希望,此文能让你明白。 四、红黑树的插入、删除情况时间复杂度的分析 因为每一个红黑树也是一个特化的二叉查找树, 因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。 然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。 恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和 不超过三次树旋转(对于插入操作是两次)。 虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。 ok,完。 后记: 此红黑树系列,前前后后,已经写了 4 篇文章,如果读者读完了这 4 篇文章, 对红黑树有一个相对之前来说,比较透彻的理解, 那么,也不枉费,我花这么多篇幅、花好几个钟头去画红黑树了。 真正理解一个数据结构、算法,最紧要的还是真正待用、实践的时候体会。 欢迎,各位,将现在、或以后学习、工作中运用此红黑树结构、算法的经验与我分享。 谢谢。:D。 --------------------------------------------------------------------------------------- 作者声明: 本人 July 对本博客所有文章和资料享有版权,转载、或引用任何内容请注明出处。 向您的厚道致敬。谢谢。二零一一年一月九日。 红黑树插入和删除结点的全程演示 作者:July、saturnman。 时间:二零一一年三月二十八日。 出处:http://blog.csdn.net/v_JULY_v。 声明:版权所有,侵权必究。 ---------------------------------------------------------------------------------------------------------------------------- 引言: 目前国内图书市场上,抑或网上讲解红黑树的资料层次不齐,混乱不清,没有一个完 整而统一的阐述。而本人的红黑树系列四篇文章(详见文末的参考文献),虽然从头至尾, 讲的有根有据,层次清晰,然距离读者真正做到红黑树了然于胸,则还缺点什么。 而我们知道,即便在经典的算法导论一书上,也没有把所有的插入、删除情况一一道 尽,直接导致了不少读者的迷惑,而我的红黑树系列第 4 篇文章:一步一图一代码,一定 要让你真正彻底明白红黑树,虽然早已把所有的插入、删除情况都一一道尽了,但也缺了点 东西。 缺点什么东西列?对了,缺的就是一个完完整整的,包含所有插入、删除情况全部过程 的全程演示图,即缺一个例子,缺一个完整的图来从头至尾阐述这一切。 ok,本文,即以 40 幅图来全程演示此红黑树的所有插入,和删除情况。相信,一定会 对您理解红黑树有所帮助。 话不絮烦,下面,本文便以此篇文章:一步一图一代码,一定要让你真正彻底明白红 黑树为纲,从插入一个结点到最后插入全部结点,再到后来一个一个把结点全部删除的情况 一一阐述。 由于为了有个完整统一,红黑树插入和删除情况在此合作成一篇文章。同时,由于本 人的红黑树系列的四篇文章已经把红黑树的插入、删除情况都一一详尽的阐述过了,因此, 有关红黑树的原理,本文不再赘述,只侧重于用图来一一全程演示结点的插入和删除情况。 有任何问题,欢迎指正。 红黑树插入情况全过程演示 通过本人的红黑树系列第 4 篇文章,我们已经知道,红黑树的所有插入情况有以下五 种: 情形 1: 新节点 N 位于树的根上,没有父节点 情形 2: 新节点的父节点 P 是黑色 情形 3:父节点 P、叔叔节点 U,都为红色, [对应第二篇文章中,的情况 1:z 的叔叔是红色的。] 情形 4: 父节点 P 是红色,叔叔节点 U 是黑色或 NIL; 插入节点 N 是其父节点 P 的右孩子,而父节点 P 又是其父节点的左孩子。 [对应我第二篇文章中,的情况 2:z 的叔叔是黑色的,且 z 是右孩子] 情形 5: 父节点 P 是红色,而叔父节点 U 是黑色或 NIL, 要插入的节点 N 是其父节点的左孩子,而父节点 P 又是其父 G 的左孩子。 [对应我第二篇文章中,情况 3:z 的叔叔是黑色的,且 z 是左孩子。] 详细,可参考此红黑树系列第 4 篇文章:一步一图一代码,一定要让你真正彻底明白红 黑树。 首先,各个结点插入与以上的各种插入情况,一一对应起来,如图: 以下的 20 张图,是依次插入这些结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 的全程演示图,已经把所有的 5 种插入情况,都全部涉及到了: 红黑树的一一插入各结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 的全 程演示图完。 红黑树删除情况全过程演示 红黑树的所有删除情况,如下: 情况 1: N 是新的根。 情形 2:兄弟节点 S 是红色 [对应我第二篇文章中,情况 1:x 的兄弟 w 是红色的。] 情况 3: 兄弟节点 S 是黑色的,且 S 的俩个儿子都是黑色的。但 N 的父节点 P,是黑色。 [对应我第二篇文章中,情况 2:x 的兄弟 w 是黑色的,且兄弟 w 的俩个儿子都是黑色的。 (这里,N 的父节点 P 为黑)] 情况 4: 兄弟节点 S 是黑色的、S 的儿子也都是黑色的,但是 N 的父亲 P,是红色。 [还是对应我第二篇文章中,情况 2:x 的兄弟 w 是黑色的,且 w 的俩个孩子都是黑色的。 (这里,N 的父节点 P 为红)] 情况 5: 兄弟 S 为黑色,S 的左儿子是红色,S 的右儿子是黑色,而 N 是它父亲的左儿子。 //此种情况,最后转化到下面的情况 6。 [对应我第二篇文章中,情况 3:x 的兄弟 w 是黑色的,w 的左孩子是红色,w 的右孩子是 黑色。] 情况 6: 兄弟节点 S 是黑色,S 的右儿子是红色,而 N 是它父亲的左儿子。 [对应我第二篇文章中,情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子时红色的。] 接下来,便是一一删除这些点 12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 为例,即,红黑树删除情况全程演示: 各个结点删除与以上的六种情况,一一对应起来,如图 首先,插入 12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 结点后,形成的红 黑树为: 然后,以下的 20 张图,是一一删除这些结点 12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 所得到的删除情况的全程演示图: 红黑树的一一删除各结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17 的全 程演示图完。 参考文献,本人代表作之一:红黑树系列: 1、教你透彻了解红黑树 2、红黑树算法的实现与剖析 3、红黑树的 c 源码实现与剖析 4、一步一图一代码,R-B Tree 5、红黑树插入和删除结点的全程演示 6、红黑树的 c++完整实现源码 7、http://saturnman.blog.163.com/。 全文完。 版权所有。转载本 BLOG 内任何文章,请以超链接形式注明出处。 否则,一经发现,必定永久谴责+追究法律责任。谢谢,各位。 红黑树的 c++完整实现源码 作者:July、saturnman。 时间:二零一一年三月二十九日。 出处:http://blog.csdn.net/v_JULY_v。 声明:版权所有,侵权必究。 ------------------------------------------------------------------------------------------ 前言: 本人的原创作品红黑树系列文章,至此,已经写到第 5 篇了。虽然第三篇文章:红黑树 的 c 源码实现与剖析,用 c 语言完整实现过红黑树,但个人感觉,代码还是不够清晰。特 此,再奉献出一份 c++的完整实现源码,以飨读者。 此份 c++实现源码,代码紧凑了许多,也清晰了不少,同时采取 c++类实现的方式, 代码也更容易维护以及重用。ok,有任何问题,欢迎指正。 版权声明 本 BLOG 内的此红黑树系列,总计六篇文章,是整个国内有史以来有关红黑树的最具代 表性,最具完整性,最具参考价值的资料。且,本人对此红黑树系列全部文章,享有版权, 任何人,任何组织,任何出版社不得侵犯本人版权相关利益,违者追究法律责任。谢谢。 红黑树的 c++完整实现源码 本文包含红黑树 c++实现的完整源码,所有的解释都含在注释中,所有的有关红黑树的 原理及各种插入、删除操作的情况,都已在本人的红黑树系列的前 4 篇文章中,一一阐述。 且在此红黑树系列第五篇文章中:红黑树从头至尾插入和删除结点的全程演示图,把所有 的插入、删除情况都一一展示尽了。 因此,有关红黑树的全部原理,请参考其它文章,重点可参考此文:红黑树算法的实现 与剖析。因此,相关原理,本文不再赘述。 ok,以下,即是红黑树 c++实现的全部源码,先是 RBTree.h,然后是 RBTree.cpp。 RBTree.h view plaincopy to clipboardprint? 1. //file RBTree.h 2. //written by saturnman,20101008。 3. //updated by July,20110329。 4. /*----------------------------------------------- 5. 版权声明: 6. July 和 saturnman 对此份红黑树的 c++实现代码享有全部的版权, 7. 谢绝转载,侵权必究。 8. ------------------------------------------------*/ 9. #ifndef _RB_TREE_H_ 10. #define _RB_TREE_H_ 11. #include 12. #include 13. #include 14. #include 15. using namespace std; 16. 17. template 18. class RB_Tree 19. { 20. private: 21. RB_Tree(const RB_Tree& input){} 22. const RB_Tree& operator=(const RB_Tree& input){} 23. private: 24. enum COLOR{RED,BLACK}; 25. class RB_Node 26. { 27. public: 28. RB_Node() 29. { 30. //RB_COLOR = BLACK; 31. right = NULL; 32. left = NULL; 33. parent = NULL; 34. } 35. COLOR RB_COLOR; 36. RB_Node* right; 37. RB_Node* left; 38. RB_Node* parent; 39. KEY key; 40. U data; 41. }; 42. public: 43. RB_Tree() 44. { 45. this->m_nullNode = new RB_Node(); 46. this->m_root = m_nullNode; 47. this->m_nullNode->right = this->m_root; 48. this->m_nullNode->left = this->m_root; 49. this->m_nullNode->parent = this->m_root; 50. this->m_nullNode->RB_COLOR = BLACK; 51. } 52. 53. bool Empty() 54. { 55. if(this->m_root == this->m_nullNode) 56. { 57. return true; 58. } 59. else 60. { 61. return false; 62. } 63. } 64. 65. //查找 key 66. RB_Node* find(KEY key) 67. { 68. RB_Node* index = m_root; 69. while(index != m_nullNode) 70. { 71. if(keykey) 72. { 73. index = index->left; //比当前的小,往左 74. } 75. else if(key>index->key) 76. { 77. index = index->right; //比当前的大,往右 78. } 79. else 80. { 81. break; 82. } 83. } 84. return index; 85. } 86. 87. //--------------------------插入结点总操作 ---------------------------------- 88. //全部的工作,都在下述伪代码中: 89. /*RB-INSERT(T, z) 90. 1 y ← nil[T] // y 始终指向 x 的父结点。 91. 2 x ← root[T] // x 指向当前树的根结点, 92. 3 while x ≠ nil[T] 93. 4 do y ← x 94. 5 if key[z] < key[x] //向左,向右.. 95. 6 then x ← left[x] 96. 7 else x ← right[x] //为了找到合适的插入点,x 探路跟踪路 径,直到 x 成为 NIL 为止。 97. 8 p[z] ← y //y 置为 插入结点 z 的父结点。 98. 9 if y = nil[T] 99. 10 then root[T] ← z 100. 11 else if key[z] < key[y] 101. 12 then left[y] ← z 102. 13 else right[y] ← z //此 8-13 行,置 z 相关的指 针。 103. 14 left[z] ← nil[T] 104. 15 right[z] ← nil[T] //设为空, 105. 16 color[z] ← RED //将新插入的结点 z 作为红色 106. 17 RB-INSERT-FIXUP(T, z) 107. */ 108. //因为将 z 着为红色,可能会违反某一红黑性质, 109. //所以需要调用下面的 RB-INSERT-FIXUP(T, z)来保持红黑性质。 110. bool Insert(KEY key,U data) 111. { 112. RB_Node* insert_point = m_nullNode; 113. RB_Node* index = m_root; 114. while(index!=m_nullNode) 115. { 116. insert_point = index; 117. if(keykey) 118. { 119. index = index->left; 120. } 121. else if(key>index->key) 122. { 123. index = index->right; 124. } 125. else 126. { 127. return false; 128. } 129. } 130. RB_Node* insert_node = new RB_Node(); 131. insert_node->key = key; 132. insert_node->data = data; 133. insert_node->RB_COLOR = RED; 134. insert_node->right = m_nullNode; 135. insert_node->left = m_nullNode; 136. if(insert_point==m_nullNode) //如果插入的是一颗空树 137. { 138. m_root = insert_node; 139. m_root->parent = m_nullNode; 140. m_nullNode->left = m_root; 141. m_nullNode->right = m_root; 142. m_nullNode->parent = m_root; 143. } 144. else 145. { 146. if(keykey) 147. { 148. insert_point->left = insert_node; 149. } 150. else 151. { 152. insert_point->right = insert_node; 153. } 154. insert_node->parent = insert_point; 155. } 156. InsertFixUp(insert_node); //调用 InsertFixUp 修复红黑树性 质。 157. } 158. 159. //---------------------插入结点性质修复 -------------------------------- 160. //3 种插入情况,都在下面的伪代码中(未涉及到所有全部的插入情况)。 161. /* 162. RB-INSERT-FIXUP(T, z) 163. 1 while color[p[z]] = RED 164. 2 do if p[z] = left[p[p[z]]] 165. 3 then y ← right[p[p[z]]] 166. 4 if color[y] = RED 167. 5 then color[p[z]] ← BLACK ? Case 1 168. 6 color[y] ← BLACK ? Case 1 169. 7 color[p[p[z]]] ← RED ? Case 1 170. 8 z ← p[p[z]] ? Case 1 171. 9 else if z = right[p[z]] 172. 10 then z ← p[z] ? Case 2 173. 11 LEFT-ROTATE(T, z) ? Case 2 174. 12 color[p[z]] ← BLACK ? Case 3 175. 13 color[p[p[z]]] ← RED ? Case 3 176. 14 RIGHT-ROTATE(T, p[p[z]]) ? Case 3 177. 15 else (same as then clause with "right" and "left" exchanged) 178. 16 color[root[T]] ← BLACK 179. */ 180. //然后的工作,就非常简单了,即把上述伪代码改写为下述的 c++代码: 181. void InsertFixUp(RB_Node* node) 182. { 183. while(node->parent->RB_COLOR==RED) 184. { 185. if(node->parent==node->parent->parent->left) // 186. { 187. RB_Node* uncle = node->parent->parent->right; 188. if(uncle->RB_COLOR == RED) //插入情况 1,z 的叔叔 y 是红色的。 189. { 190. node->parent->RB_COLOR = BLACK; 191. uncle->RB_COLOR = BLACK; 192. node->parent->parent->RB_COLOR = RED; 193. node = node->parent->parent; 194. } 195. else if(uncle->RB_COLOR == BLACK ) //插入情况 2:z 的 叔叔 y 是黑色的,。 196. { 197. if(node == node->parent->right) //且 z 是右孩子 198. { 199. node = node->parent; 200. RotateLeft(node); 201. } 202. else //插入情况 3:z 的叔叔 y 是黑色 的,但 z 是左孩子。 203. { 204. node->parent->RB_COLOR = BLACK; 205. node->parent->parent->RB_COLOR = RED; 206. RotateRight(node->parent->parent); 207. } 208. } 209. } 210. else //这部分是针对为插入情况 1 中,z 的父亲现在作为祖父的右孩 子了的情况,而写的。 211. //15 else (same as then clause with "right" and "le ft" exchanged) 212. { 213. RB_Node* uncle = node->parent->parent->left; 214. if(uncle->RB_COLOR == RED) 215. { 216. node->parent->RB_COLOR = BLACK; 217. uncle->RB_COLOR = BLACK; 218. uncle->parent->RB_COLOR = RED; 219. node = node->parent->parent; 220. } 221. else if(uncle->RB_COLOR == BLACK) 222. { 223. if(node == node->parent->left) 224. { 225. node = node->parent; 226. RotateRight(node); //与上述代码相比,左旋 改为右旋 227. } 228. else 229. { 230. node->parent->RB_COLOR = BLACK; 231. node->parent->parent->RB_COLOR = RED; 232. RotateLeft(node->parent->parent); //右旋 改为左旋,即可。 233. } 234. } 235. } 236. } 237. m_root->RB_COLOR = BLACK; 238. } 239. 240. //左旋代码实现 241. bool RotateLeft(RB_Node* node) 242. { 243. if(node==m_nullNode || node->right==m_nullNode) 244. { 245. return false;//can't rotate 246. } 247. RB_Node* lower_right = node->right; 248. lower_right->parent = node->parent; 249. node->right=lower_right->left; 250. if(lower_right->left!=m_nullNode) 251. { 252. lower_right->left->parent = node; 253. } 254. if(node->parent==m_nullNode) //rotate node is root 255. { 256. m_root = lower_right; 257. m_nullNode->left = m_root; 258. m_nullNode->right= m_root; 259. //m_nullNode->parent = m_root; 260. } 261. else 262. { 263. if(node == node->parent->left) 264. { 265. node->parent->left = lower_right; 266. } 267. else 268. { 269. node->parent->right = lower_right; 270. } 271. } 272. node->parent = lower_right; 273. lower_right->left = node; 274. } 275. 276. //右旋代码实现 277. bool RotateRight(RB_Node* node) 278. { 279. if(node==m_nullNode || node->left==m_nullNode) 280. { 281. return false;//can't rotate 282. } 283. RB_Node* lower_left = node->left; 284. node->left = lower_left->right; 285. lower_left->parent = node->parent; 286. if(lower_left->right!=m_nullNode) 287. { 288. lower_left->right->parent = node; 289. } 290. if(node->parent == m_nullNode) //node is root 291. { 292. m_root = lower_left; 293. m_nullNode->left = m_root; 294. m_nullNode->right = m_root; 295. //m_nullNode->parent = m_root; 296. } 297. else 298. { 299. if(node==node->parent->right) 300. { 301. node->parent->right = lower_left; 302. } 303. else 304. { 305. node->parent->left = lower_left; 306. } 307. } 308. node->parent = lower_left; 309. lower_left->right = node; 310. } 311. 312. //--------------------------删除结点总操作 ---------------------------------- 313. //伪代码,不再贴出,详情,请参考此红黑树系列第二篇文章: 314. //经典算法研究系列:五、红黑树算法的实现与剖析: 315. //http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx 。 316. bool Delete(KEY key) 317. { 318. RB_Node* delete_point = find(key); 319. if(delete_point == m_nullNode) 320. { 321. return false; 322. } 323. if(delete_point->left!=m_nullNode && delete_point->right!=m _nullNode) 324. { 325. RB_Node* successor = InOrderSuccessor(delete_point); 326. delete_point->data = successor->data; 327. delete_point->key = successor->key; 328. delete_point = successor; 329. } 330. RB_Node* delete_point_child; 331. if(delete_point->right!=m_nullNode) 332. { 333. delete_point_child = delete_point->right; 334. } 335. else if(delete_point->left!=m_nullNode) 336. { 337. delete_point_child = delete_point->left; 338. } 339. else 340. { 341. delete_point_child = m_nullNode; 342. } 343. delete_point_child->parent = delete_point->parent; 344. if(delete_point->parent==m_nullNode)//delete root node 345. { 346. m_root = delete_point_child; 347. m_nullNode->parent = m_root; 348. m_nullNode->left = m_root; 349. m_nullNode->right = m_root; 350. } 351. else if(delete_point == delete_point->parent->right) 352. { 353. delete_point->parent->right = delete_point_child; 354. } 355. else 356. { 357. delete_point->parent->left = delete_point_child; 358. } 359. if(delete_point->RB_COLOR==BLACK && !(delete_point_child==m _nullNode && delete_point_child->parent==m_nullNode)) 360. { 361. DeleteFixUp(delete_point_child); 362. } 363. delete delete_point; 364. return true; 365. } 366. 367. //---------------------删除结点性质修复 ----------------------------------- 368. //所有的工作,都在下述 23 行伪代码中: 369. /* 370. RB-DELETE-FIXUP(T, x) 371. 1 while x ≠ root[T] and color[x] = BLACK 372. 2 do if x = left[p[x]] 373. 3 then w ← right[p[x]] 374. 4 if color[w] = RED 375. 5 then color[w] ← BLACK ? Case 1 376. 6 color[p[x]] ← RED ? Case 1 377. 7 LEFT-ROTATE(T, p[x]) ? Case 1 378. 8 w ← right[p[x]] ? Case 1 379. 9 if color[left[w]] = BLACK and color[right[w]] = BLACK 380. 10 then color[w] ← RED ? Case 2 381. 11 x p[x] ? Case 2 382. 12 else if color[right[w]] = BLACK 383. 13 then color[left[w]] ← BLACK ? Case 3 384. 14 color[w] ← RED ? Case 3 385. 15 RIGHT-ROTATE(T, w) ? Case 3 386. 16 w ← right[p[x]] ? Case 3 387. 17 color[w] ← color[p[x]] ? Case 4 388. 18 color[p[x]] ← BLACK ? Case 4 389. 19 color[right[w]] ← BLACK ? Case 4 390. 20 LEFT-ROTATE(T, p[x]) ? Case 4 391. 21 x ← root[T] ? Case 4 392. 22 else (same as then clause with "right" and "left" exc hanged) 393. 23 color[x] ← BLACK 394. */ 395. //接下来的工作,很简单,即把上述伪代码改写成 c++代码即可。 396. void DeleteFixUp(RB_Node* node) 397. { 398. while(node!=m_root && node->RB_COLOR==BLACK) 399. { 400. if(node == node->parent->left) 401. { 402. RB_Node* brother = node->parent->right; 403. if(brother->RB_COLOR==RED) //情况 1:x 的兄弟 w 是红色 的。 404. { 405. brother->RB_COLOR = BLACK; 406. node->parent->RB_COLOR = RED; 407. RotateLeft(node->parent); 408. } 409. else //情况 2:x 的兄弟 w 是黑色的, 410. { 411. if(brother->left->RB_COLOR == BLACK && brother- >right->RB_COLOR == BLACK) 412. //且 w 的俩个孩子都是黑色的。 413. { 414. brother->RB_COLOR = RED; 415. node = node->parent; 416. } 417. else if(brother->right->RB_COLOR == BLACK) 418. //情况 3:x 的兄弟 w 是黑色的,w 的右孩子是黑色(w 的左孩子是红色)。 419. { 420. brother->RB_COLOR = RED; 421. brother->left->RB_COLOR = BLACK; 422. RotateRight(brother); 423. } 424. else if(brother->right->RB_COLOR == RED) 425. //情况 4:x 的兄弟 w 是黑色的,且 w 的右孩子时红色 的。 426. { 427. brother->RB_COLOR = node->parent->RB_COLOR; 428. node->parent->RB_COLOR = BLACK; 429. brother->right->RB_COLOR = BLACK; 430. RotateLeft(node->parent); 431. node = m_root; 432. } 433. } 434. } 435. else //下述情况针对上面的情况 1 中,node 作为右孩子而阐述 的。 436. //22 else (same as then clause with "right" and "left" exchanged) 437. //同样,原理一致,只是遇到左旋改为右旋,遇到右旋改为左旋, 即可。其它代码不变。 438. { 439. RB_Node* brother = node->parent->left; 440. if(brother->RB_COLOR == RED) 441. { 442. brother->RB_COLOR = BLACK; 443. node->parent->RB_COLOR = RED; 444. RotateRight(node->parent); 445. } 446. else 447. { 448. if(brother->left->RB_COLOR==BLACK && brother->r ight->RB_COLOR == BLACK) 449. { 450. brother->RB_COLOR = RED; 451. node = node->parent; 452. } 453. else if(brother->left->RB_COLOR==BLACK) 454. { 455. brother->RB_COLOR = RED; 456. brother->right->RB_COLOR = BLACK; 457. RotateLeft(brother); 458. } 459. else if(brother->left->RB_COLOR==RED) 460. { 461. brother->RB_COLOR = node->parent->RB_COLOR; 462. node->parent->RB_COLOR = BLACK; 463. brother->left->RB_COLOR = BLACK; 464. RotateRight(node->parent); 465. node = m_root; 466. } 467. } 468. } 469. } 470. m_nullNode->parent = m_root; //最后将 node 置为根结点, 471. node->RB_COLOR = BLACK; //并改为黑色。 472. } 473. 474. // 475. inline RB_Node* InOrderPredecessor(RB_Node* node) 476. { 477. if(node==m_nullNode) //null node has no predecessor 478. { 479. return m_nullNode; 480. } 481. RB_Node* result = node->left; //get node's left child 482. while(result!=m_nullNode) //try to find node's left subtree's right most node 483. { 484. if(result->right!=m_nullNode) 485. { 486. result = result->right; 487. } 488. else 489. { 490. break; 491. } 492. } //after while loop result==null or result's ri ght child is null 493. if(result==m_nullNode) 494. { 495. RB_Node* index = node->parent; 496. result = node; 497. while(index!=m_nullNode && result == index->left) 498. { 499. result = index; 500. index = index->parent; 501. } 502. result = index; // first right parent or null 503. } 504. return result; 505. } 506. 507. // 508. inline RB_Node* InOrderSuccessor(RB_Node* node) 509. { 510. if(node==m_nullNode) //null node has no successor 511. { 512. return m_nullNode; 513. } 514. RB_Node* result = node->right; //get node's right node 515. while(result!=m_nullNode) //try to find node's right subtree's left most node 516. { 517. if(result->left!=m_nullNode) 518. { 519. result = result->left; 520. } 521. else 522. { 523. break; 524. } 525. } //after while loop result==n ull or result's left child is null 526. if(result == m_nullNode) 527. { 528. RB_Node* index = node->parent; 529. result = node; 530. while(index!=m_nullNode && result == index->right) 531. { 532. result = index; 533. index = index->parent; 534. } 535. result = index; //first parent's left or null 536. } 537. return result; 538. } 539. 540. //debug 541. void InOrderTraverse() 542. { 543. InOrderTraverse(m_root); 544. } 545. void CreateGraph(string filename) 546. { 547. //delete 548. } 549. void InOrderCreate(ofstream& file,RB_Node* node) 550. { 551. //delete 552. } 553. void InOrderTraverse(RB_Node* node) 554. { 555. if(node==m_nullNode) 556. { 557. return; 558. } 559. else 560. { 561. InOrderTraverse(node->left); 562. cout<key<right); 564. } 565. } 566. ~RB_Tree() 567. { 568. clear(m_root); 569. delete m_nullNode; 570. } 571. private: 572. // utility function for destructor to destruct object; 573. void clear(RB_Node* node) 574. { 575. if(node==m_nullNode) 576. { 577. return ; 578. } 579. else 580. { 581. clear(node->left); 582. clear(node->right); 583. delete node; 584. } 585. } 586. private: 587. RB_Node *m_nullNode; 588. RB_Node *m_root; 589. }; 590. #endif /*_RB_TREE_H_*/ RBTree.cpp view plaincopy to clipboardprint? 1. //file RBTree.cpp 2. //written by saturnman,20101008。 3. //updated by July,20110329。 4. 5. //所有的头文件都已补齐,现在您可以直接复制此份源码上机验证了(版权所有,侵权必究)。 6. //July、updated,2011.05.06。 7. #include 8. #include 9. #include 10. #include 11. #include 12. #include"RBTree.h" //如果.h 文件,和 cpp 文件放在一个文件里,此句去掉 13. using namespace std; 14. 15. int main() 16. { 17. RB_Tree tree; 18. vector v; 19. 20. for(int i=0;i<20;++i) 21. { 22. v.push_back(i); 23. } 24. random_shuffle(v.begin(),v.end()); 25. copy(v.begin(),v.end(),ostream_iterator(cout," ")); 26. cout<parent == NULL || node->parent == m_nullNode){ 12. printf("root:%d\n", node->data); 13. }else if(node->parent->left == node){ 14. printf("left:%d, parent:%d\n", node->data, node->parent->data); 15. }else if(node->parent->right == node){ 16. printf("right:%d, parent:%d\n", node->data, node->parent->data); 17. } 18. 19. _printNode(node->left); 20. _printNode(node->right); 21. } 2、改写 RBTree.cpp 文件,如下: view plaincopy to clipboardprint? 1. //file RBTree.cpp 2. //written by saturnman,20101008。 3. //updated by July,20110329。 4. 5. //所有的头文件都已补齐,现在您可以直接复制此份源码上机验证了(版权所有,侵权必究)。 6. //July、updated,2011.05.06。 7. #include 8. #include 9. #include 10. #include 11. #include 12. //#include"RBTree.h" //如果.h 文件,和 cpp 文件放在一个文件里,此句去掉 13. using namespace std; 14. 15. int main() 16. { 17. RB_Tree tree; 18. 19. tree.Insert(12, 12); 20. tree.Insert(1, 1); 21. tree.Insert(9, 9); 22. tree.Insert(2, 2); 23. tree.Insert(0, 0); 24. tree.Insert(11, 11); 25. tree.Insert(7, 7); 26. 27. 28. tree.Delete(9); 29. 30. tree.PrintTree(); 31. /*vector v; 32. 33. for(int i=0;i<20;++i) 34. { 35. v.push_back(i); 36. } 37. random_shuffle(v.begin(),v.end()); 38. copy(v.begin(),v.end(),ostream_iterator(cout," ")); 39. cout< #include using namespace std; int match(const string& target,const string& pattern) { int target_length = target.size(); int pattern_length = pattern.size(); int target_index = 0; int pattern_index = 0; while(target_index < target_length && pattern_index < pattern_length) { if(target[target_index]==pattern[pattern_index]) { ++target_index; ++pattern_index; } else { target_index -= (pattern_index-1); pattern_index = 0; } } if(pattern_index == pattern_length) { return target_index - pattern_length; } else { return -1; } } int main() { cout< #include using namespace std; void compute_overlay(const string& pattern) { const int pattern_length = pattern.size(); int *overlay_function = new int[pattern_length]; int index; overlay_function[0] = -1; for(int i=1;i=0 && pattern[i]!=pattern[index+1]) { index = overlay_function[index]; } if(pattern[i]==pattern[index+1]) { overlay_function[i] = index + 1; } else { overlay_function[i] = -1; } } for(i=0;i #include #include using namespace std; int kmp_find(const string& target,const string& pattern) { const int target_length = target.size(); const int pattern_length = pattern.size(); int * overlay_value = new int[pattern_length]; overlay_value[0] = -1; int index = 0; for(int i=1;i=0 && pattern[index+1]!=pattern[i]) { index = overlay_value[index]; } if(pattern[index+1]==pattern[i]) { overlay_value[i] = index +1; } else { overlay_value[i] = -1; } } //match algorithm start int pattern_index = 0; int target_index = 0; while(pattern_indexT:终止 输出解。 好的,看下遗传算法的伪代码实现: ▲Procedures GA: 伪代码 begin initialize P(0); t = 0; //t 是进化的代数,一代、二代、三代... while(t <= T) do for i = 1 to M do //M 是初始种群的个体数 Evaluate fitness of P(t); //计算 P(t)中各个个体 的适应度 end for for i = 1 to M do Select operation to P(t); //将选择算子作用于群体 end for for i = 1 to M/2 do Crossover operation to P(t); //将交叉算子作用于群体 end for for i = 1 to M do Mutation operation to P(t); //将变异算子作用于群体 end for for i = 1 to M do P(t+1) = P(t); //得到下一代群体P(t + 1) end for t =t + 1; //终止条件判断 t≦T:t← t+1 转到步骤 2 end while end 二、深入遗传算法 1、智能优化算法概述 智能优化算法又称现代启发式算法,是一种具有全局优化性能、通用性强且适合于并行 处理的算法。 这种算法一般具有严密的理论依据,而不是单纯凭借专家经验,理论上可以在一定的时 间内找到最优解或近似最优解。 遗传算法属于智能优化算法之一。 常用的智能优化算法有: 遗传算法 、模拟退火算法、禁忌搜索算法、粒子群算法、蚁群算法。 (本经典算法研究系列,日后将陆续阐述模拟退火算法、粒子群算法、蚁群算法。) 2、遗传算法概述 遗传算法是由美国的 J. Holland 教授于 1975 年在他的专著《自然界和人工系统的适应 性》中首先提出的。 借鉴生物界自然选择和自然遗传机制的随机化搜索算法。 模拟自然选择和自然遗传过程中发生的繁殖、交叉和基因突变现象。 在每次迭代中都保留一组候选解,并按某种指标从解群中选取较优的个体,利用遗传算 子(选择、交叉和变异)对这些个体进行组合,产生新一代的候选解群,重复此过程,直到满 足某种收敛指标为止。 基本遗传算法(Simple Genetic Algorithms,GA)又称简单遗传算法或标准遗传算法), 是由 Goldberg 总结出的一种最基本的遗传算法,其遗传进化操作过程简单,容易理解,是 其它一些遗传算法的雏形和基础。 3、基本遗传算法的组成 (1)编码(产生初始种群) (2)适应度函数 (3)遗传算子(选择、交叉、变异) (4)运行参数 接下来,咱们分门别类,分别阐述着基本遗传算法的五个组成部分: 1、编码 遗传算法(GA)通过某种编码机制把对象抽象为由特定符号按一定顺序排成的串。 正如研究生物遗传是从染色体着手,而染色体则是由基因排成的串。 基本遗传算法(SGA)使用二进制串进行编码。 初始种群:基本遗传算法(SGA)采用随机方法生成若干个个体的集合,该集合称为初 始种群。 初始种群中个体的数量称为种群规模。 2、适应度函数 遗传算法对一个个体(解)的好坏用适应度函数值来评价,适应度函数值越大,解的质 量越好。 适应度函数是遗传算法进化过程的驱动力,也是进行自然选择的唯一标准, 它的设计应结合求解问题本身的要求而定。 3.1、选择算子 遗传算法使用选择运算对个体进行优胜劣汰操作。 适应度高的个体被遗传到下一代群体中的概率大;适应度低的个体,被遗传到下一代群 体中的概率小。 选择操作的任务就是从父代群体中选取一些个体,遗传到下一代群体。 基本遗传算法(SGA)中选择算子采用轮盘赌选择方法。 Ok,下面就来看下这个轮盘赌的例子,这个例子通俗易懂,对理解选择算子帮助很大。 轮盘赌选择方法 轮盘赌选择又称比例选择算子,其基本思想是: 各个个体被选中的概率与其适应度函数值大小成正比。 设群体大小为 N,个体 xi 的适应度为 f(xi),则个体 xi 的选择概率为: 轮盘赌选择法可用如下过程模拟来实现: (1)在[0, 1]内产生一个均匀分布的随机数 r。 (2)若 r≤q1,则染色体 x1 (3)若 qk-1width, init_img->height ) ) / log(2) - 2; gauss_pyr = build_gauss_pyr( init_img, octvs, intvls, sigma ); dog_pyr = build_dog_pyr( gauss_pyr, octvs, intvls ); storage = cvCreateMemStorage( 0 ); features = scale_space_extrema( dog_pyr, octvs, intvls, contr_thr, curv_thr, storage ); calc_feature_scales( features, sigma, intvls ); if( img_dbl ) adjust_for_img_dbl( features ); calc_feature_oris( features, gauss_pyr ); compute_descriptors( features, gauss_pyr, descr_width, descr_hist_bins ); /* sort features by decreasing scale and move from CvSeq to array */ cvSeqSort( features, (CvCmpFunc)feature_cmp, NULL ); n = features->total; *feat = calloc( n, sizeof(struct feature) ); *feat = cvCvtSeqToArray( features, *feat, CV_WHOLE_SEQ ); for( i = 0; i < n; i++ ) { free( (*feat)[i].feature_data ); (*feat)[i].feature_data = NULL; } cvReleaseMemStorage( &storage ); cvReleaseImage( &init_img ); release_pyr( &gauss_pyr, octvs, intvls + 3 ); release_pyr( &dog_pyr, octvs, intvls + 2 ); return n; } 这个函数是上述函数一的重载,作用是一样的,实际上函数一只不过是使用默认参数调用了 函数二,核心的代码都是在函数二中实现的。 sift 创始人 David Lowe 的完整代码,包括他的论文,请到此处下载: http://www.cs.ubc.ca/~lowe/keypoints 日后,本 BLOG 内,会具体剖析下上述 David Lowe 的 Sift 算法代码。 Rob Hess 维护的 sift 库: http://blogs.oregonstate.edu/hess/code/sift/ 还可,参考这里: sift 图像特征提取与匹配算法代码(友人,onezeros 博客): http://blog.csdn.net/onezeros/archive/2011/01/05/6117704.aspx 完。 本人 July 对本博客所有任何文章、内容和资料享有版权。 转载务必注明作者本人及出处,并通知本人。July、二零一一年二月十五日。 九之续、sift 算法的编译与实现 作者:July 、二零一一年三月一日。 代码:Rob Hess 维护的 sift 库,July updated。 环境:windows xp+vc6.0。 条件:opencv1.0、gsl-1.8.exe 昨日,下载了 Rob Hess 的 sift 库,将其源码粗略的看了看,想要编译时,遇到了不少 问题,先修改了下代码,然后下载 opencv、gsl。最后,几经周折,才最终编译成功。 以下便是 sift 源码库编译后的效果图: 为了给有兴趣实现 sift 算法的朋友提供个参考,特整理此文如下。要了解什么是 sift 算法,请参考:九、图像特征提取与匹配之 SIFT 算法。ok,咱们下面,就来利用 Rob Hess 维护的 sift 库来实现 sift 算法: 首先,请下载 Rob Hess 维护的 sift 库: http://blogs.oregonstate.edu/hess/code/sift/ 下载 Rob Hess 的这个压缩包后,如果直接解压缩,直接编译,那么会出现下面的错 误提示: 编译提示:error C1083: Cannot open include file: 'cxcore.h': No such file or directory,找不到这个头文件。 这个错误,是因为你还没有安装 opencv,因为:cxcore.h 和 cv.h 是开源的 OPEN CV 头文件,不是 VC++的默认安装文件,所以你还得下载 OpenCV 并进行安装。然后,可以在 OpenCV 文件夹下找到你所需要的头文件了。 据网友称,截止 2010 年 4 月 4 日,还没有在 VC6.0 下成功使用 opencv2.0 的案例。 所以,如果你是 VC6.0 的用户请下载 opencv1.0 版本。vs 的话,opencv2.0,1.0 任意下 载。 以下,咱们就以 vc6.0 为平台举例,下载并安装 opencv1.0 版本、gsl 等。当然, 你也可以用 vs 编译,同样下载 opencv(具体版本不受限制)、gsl 等。 请按以下步骤操作: 一、下载 opencv1.0 http://sourceforge.net/projects/opencvlibrary/files/opencv-win/1.0/OpenCV _1.0.exe/download 二、安装 opencv1.0,配置 Windows 环境变量 1、安装注意:假如你是将 OpenCV 安装到 C:\Program Files\OpenCV(如果你安装 的时候选择不是安装在 C 盘,则下面所有对应的 C 盘都改为你所安装在的那个―X 盘‖,即 可),在安装时选择"将\OpenCV\bin 加入系统变量",打上―勾‖。( Add\OpenCV\bin to the systerm PATH。这一步确认选上了之后,下面的检查环境变量的步骤,便可免去) 2、检查环境变量。为了确保上述步骤中,加入了系统变量,在安装 opencv1.0 成功后, 还得检查 C:\Program Files\OpenCV\bin 是否已经被加入到环境变量 PATH,如果没有, 请加入。 3、最后是配置 Visual C++ 6.0。 全局设置 菜单 Tools->Options->Directories:先设置 lib 路径,选择 Library files,在下方 填入路径: C:\Program Files\OpenCV\lib 然后选择 include files,在下方填入路径(参考下图): C:\Program Files\OpenCV\cxcore\include C:\Program Files\OpenCV\cv\include C:\Program Files\OpenCV\cvaux\include C:\Program Files\OpenCV\ml\include C:\Program Files\OpenCV\otherlibs\highgui C:\Program Files\OpenCV\otherlibs\cvcam\include 最后选择 source files,在下方填入路径: C:\Program Files\OpenCV\cv\src C:\Program Files\OpenCV\cxcore\src C:\Program Files\OpenCV\cvaux\src C:\Program Files\OpenCV\otherlibs\highgui C:\Program Files\OpenCV\otherlibs\cvcam\src\windows 项目设置 每创建一个将要使用 OpenCV 的 VC Project,都需要给它指定需要的 lib。菜单: Project->Settings,然后将 Setting for 选为 All Configurations,然后选择右边的 link 标签,在 Object/library modules 附加上: cxcore.lib cv.lib ml.lib cvaux.lib highgui.lib cvcam.lib 当然,你不需要这么多 lib,你可以只添加你需要的 lib(见下图) 三、下载 gsl,gsl 也是一个库,也需要下载: http://sourceforge.net/projects/gnuwin32/files/gsl/1.8/gsl-1.8.exe/dow nload。在编译时候 GSL 也是和 OpenCV 一样要把头文件和 lib 的路径指定好。 四、配置 gsl 将 C:\WinGsl\bin 中的 WinGsl.dll 和 WinGslD.dll 复制到 C:\VC6.0\Bin;将整个 Gsl 目录复制到 C:\VC6.0\Bin 下;lib 目录下的所有.lib 文件全部复制到 C:\VC6.0\Lib 下。 然后,在 tools-options-directories 中,将 C:\WinGsl 下的 lib,gsl 分别加入到 库文件和头文件的搜索路径中。 以下是可能会出现的错误情况处理: I、OpenCV 安装后―没有找到 cxcore100.dll‖的错误处理 在安装时选择―将\OpenCV\bin 加入系统变量‖(Add\OpenCV\bin to the systerm PATH)。 但该选项并不一定能成功添加到系统变量,如果编写的程序在运行时出现―没有 找到 cxcore100.dll,因为这个应用程序未能启动。重新安装应用程序可能会修复此问题。‖ 的错误。 手动在我的电脑->属性->高级->环境变量->系统变量->path 添加 c:\program files\opencv\bin;添加完成后需要重启计算机。 II、vc6.0 下配置了一下,可是编译程序时遇到如下一个错误: Linking... LINK : fatal error LNK1104: cannot open file"odbccp32.libcxcore.lib" 可能是:在工程设置的时候添加连接库时没加空格或.来把两个文件名(odbccp32.lib cxcore.lib)分开。注意每一次操作后,记得保存。 若经过以上所有的步骤之后,如果还不能正常编译,那就是还要稍微修改下你下载的 Rob Hess 代码。ok,日后,若有空,再好好详细剖析下此 sift 的源码。最后,祝你编译 顺利。 完。 版权声明:原创文章,若需转载,请标明出处。谢谢。 九之再续、教你一步一步用 c 语言实现 sift 算法、上 作者:July、二零一一年三月十二日 出处:http://blog.csdn.net/v_JULY_v 参考:Rob Hess 维护的 sift 库 环境:windows xp+vc6.0 条件:c 语言实现。 说明:本 BLOG 内会陆续一一实现所有经典算法。 引言: 在我写的关于 sift 算法的前倆篇文章里头,已经对 sift 算法有了初步的介绍:九、图 像特征提取与匹配之 SIFT 算法,而后在:九(续)、sift 算法的编译与实现里,我也简单 记录下了如何利用 opencv,gsl 等库编译运行 sift 程序。 但据一朋友表示,是否能用 c 语言实现 sift 算法,同时,尽量不用到 opencv,gsl 等 第三方库之类的东西。而且,Rob Hess 维护的 sift 库,也不好懂,有的人根本搞不懂是 怎么一回事。 那么本文,就教你如何利用 c 语言一步一步实现 sift 算法,同时,你也就能真正明白 sift 算法到底是怎么一回事了。 ok,先看一下,本程序最终运行的效果图,sift 算法分为五个步骤(下文详述),对应 以下第二--第六幅图: sift 算法的步骤 要实现一个算法,首先要完全理解这个算法的原理或思想。咱们先来简单了解下,什 么叫 sift 算法: sift,尺度不变特征转换,是一种电脑视觉的算法用来侦测与描述影像中的局部性特征, 它在空间尺度中寻找极值点,并提取出其位置、尺度、旋转不变量,此算法由 David Lowe 在 1999 年所发表,2004 年完善总结。 所谓,Sift 算法就是用不同尺度(标准差)的高斯函数对图像进行平滑,然后比较平滑 后图像的差别,差别大的像素就是特征明显的点。 以下是 sift 算法的五个步骤: 一、建立图像尺度空间(或高斯金字塔),并检测极值点 首先建立尺度空间,要使得图像具有尺度空间不变形,就要建立尺度空间,sift 算法采 用了高斯函数来建立尺度空间,高斯函数公式为: G(x,y,e) = [1/2*pi*e^2] * exp[ -(x^2 + y^2)/2e^2] 上述公式 G(x,y,e),即为尺度可变高斯函数。 而,一个图像的尺度空间 L(x,y,e) ,定义为原始图像 I(x,y)与上述的一个可变尺度的 2 维高斯函数 G(x,y,e) 卷积运算。 即,原始影像I(x,y)在不同的尺度e下,与高斯函数G(x,y,e)进行卷积,得到L(x,y,e), 如下: L(x,y,e) = G(x,y,e)*I(x,y) 以上的(x,y)是空间坐标, e,是尺度坐标,或尺度空间因子,e 的大小决定平滑 程度,大尺度对应图像的概貌特征,小尺度对应图像的细节特征。大的 e 值对应粗糙尺度(低 分辨率),反之,对应精细尺度(高分辨率)。 尺度,受 e 这个参数控制的表示。而不同的 L(x,y,e)就构成了尺度空间,具体计算 的时候,即使连续的高斯函数,都被离散为(一般为奇数大小)(2*k+1) *(2*k+1)矩阵, 来和数字图像进行卷积运算。 随着 e 的变化,建立起不同的尺度空间,或称之为建立起图像的高斯金字塔。 但,像上述 L(x,y,e) = G(x,y,e)*I(x,y)的操作,在进行高斯卷积时,整个图像就 要遍历所有的像素进行卷积(边界点除外),于此,就造成了时间和空间上的很大浪费。 为了更有效的在尺度空间检测到稳定的关键点,也为了缩小时间和空间复杂度,对上述 的操作作了一个改建:即,提出了高斯差分尺度空间(DOG scale-space)。利用不同尺 度的高斯差分与原始图像 I(x,y)相乘 ,卷积生成。 D(x,y,e) = ((G(x,y,ke) - G(x,y,e)) * I(x,y) = L(x,y,ke) - L(x,y,e) DOG 算子计算简单,是尺度归一化的 LOG 算子的近似。 ok,耐心点,咱们再来总结一下上述内容: 1、高斯卷积 在组建一组尺度空间后,再组建下一组尺度空间,对上一组尺度空间的最后一幅图像进 行二分之一采样,得到下一组尺度空间的第一幅图像,然后进行像建立第一组尺度空间那样 的操作,得到第二组尺度空间,公式定义为 L(x,y,e) = G(x,y,e)*I(x,y) 图像金字塔的构建:图像金字塔共 O 组,每组有 S 层,下一组的图像由上一组图像降采 样得到,效果图,图 A 如下(左为上一组,右为下一组): 2、高斯差分 在尺度空间建立完毕后,为了能够找到稳定的关键点,采用高斯差分的方法来检测那些 在局部位置的极值点,即采用俩个相邻的尺度中的图像相减,即公式定义为: D(x,y,e) = ((G(x,y,ke) - G(x,y,e)) * I(x,y) = L(x,y,ke) - L(x,y,e) 效果图,图 B: SIFT 的精妙之处在于采用图像金字塔的方法解决这一问题,我们可以把两幅图像想象 成是连续的,分别以它们作为底面作四棱锥,就像金字塔,那么每一个 截面与原图像相似, 那么两个金字塔中必然会有包含大小一致的物体的无穷个截面,但应用只能是离散的,所以 我们只能构造有限层,层数越多当然越好,但处理时 间会相应增加,层数太少不行,因为 向下采样的截面中可能找不到尺寸大小一致的两个物体的图像。 咱们再来具体阐述下构造 D(x,y,e)的详细步骤: 1、首先采用不同尺度因子的高斯核对图像进行卷积以得到图像的不同尺度空间,将这 一组图像作为金子塔图像的第一层。 2、接着对第一层图像中的 2 倍尺度图像(相对于该层第一幅图像的 2 倍尺度)以 2 倍 像素距离进行下采样来得到金子塔图像的第二层中的第一幅图像,对该图像采用不同尺度因 子的高斯核进行卷积,以获得金字塔图像中第二层的一组图像。 3、再以金字塔图像中第二层中的 2 倍尺度图像(相对于该层第一幅图像的 2 倍尺度) 以 2 倍像素距离进行下采样来得到金字塔图像的第三层中的第一幅图像,对该图像采用不 同尺度因子的高斯核进行卷积,以获得金字塔图像中第三层的一组图像。这样依次类推,从 而获得了金字塔图像的每一层中的一组图像,如下图所示: 4、对上图得到的每一层相邻的高斯图像相减,就得到了高斯差分图像,如下述第一幅 图所示。下述第二幅图中的右列显示了将每组中相邻图像相减所生成的高斯差分图像的结果, 限于篇幅,图中只给出了第一层和第二层高斯差分图像的计算(下述俩幅图统称为图 2): 5、因为高斯差分函数是归一化的高斯拉普拉斯函数的近似,所以可以从高斯差分金字 塔分层结构提取出图像中的极值点作为候选的特征点。对 DOG 尺度空间每个点与相邻尺 度和相邻位置的点逐个进行比较,得到的局部极值位置即为特征点所处的位置和对应的尺度。 二、检测关键点 为了寻找尺度空间的极值点,每一个采样点要和它所有的相邻点比较,看其是否比它的 图像域和尺度域的相邻点大或者小。如下图,图 3 所示,中间的检测点和它同尺度的 8 个 相邻点和上下相邻尺度对应的 9×2 个点共 26 个点比较,以确保在尺度空间和二维图像空 间都检测到极值点。 因为需要同相邻尺度进行比较,所以在一组高斯差分图像中只能检测到两个尺度的极 值点(如上述第二幅图中右图的五角星标识),而其它尺度的极值点检测则需要在图像金字 塔的上一层高斯差分图像中进行。依次类推,最终在图像金字塔中不同层的高斯差分图像中 完成不同尺度极值的检测。 当然这样产生的极值点并不都是稳定的特征点,因为某些极值点响应较弱,而且 DOG 算子会产生较强的边缘响应。 三、关键点方向的分配 为了使描述符具有旋转不变性,需要利用图像的局部特征为给每一个关键点分配一个 方向。利用关键点邻域像素的梯度及方向分布的特性,可以得到梯度模值和方向如下: 其中,尺度为每个关键点各自所在的尺度。 在以关键点为中心的邻域窗口内采样,并用直方图统计邻域像素的梯度方向。梯度直 方图的范围是 0~360 度,其中每 10 度一个方向,总共 36 个方向。 直方图的峰值则代表了该关键点处邻域梯度的主方向,即作为该关键点的方向。 在计算方向直方图时,需要用一个参数等于关键点所在尺度 1.5 倍的高斯权重窗对方 向直方图进行加权,上图中用蓝色的圆形表示,中心处的蓝色较重,表示权值最大,边缘处 颜色潜,表示权值小。如下图所示,该示例中为了简化给出了 8 方向的方向直方图计算结 果,实际 sift 创始人 David Lowe 的原论文中采用 36 方向的直方图。 方向直方图的峰值则代表了该特征点处邻域梯度的方向,以直方图中最大值作为该关 键点的主方向。为了增强匹配的鲁棒性,只保留峰值大于主方向峰值 80%的方向作为该关 键点的辅方向。因此,对于同一梯度值的多个峰值的关键点位置,在相同位置和尺度将会有 多个关键点被创建但方向不同。仅有 15%的关键点被赋予多个方向,但可以明显的提高关 键点匹配的稳定性。 至此,图像的关键点已检测完毕,每个关键点有三个信息:位置、所处尺度、方向。 由此可以确定一个 SIFT 特征区域。 四、特征点描述符 通过以上步骤,对于每一个关键点,拥有三个信息:位置、尺度以及方向。接下来就 是为每个关键点建立一个描述符,使其不随各种变化而改变,比如光照变化、视角变化等等。 并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。 首先将坐标轴旋转为关键点的方向,以确保旋转不变性。 接下来以关键点为中心取 8×8 的窗口。 上图,图 5 中左部分的中央黑点为当前关键点的位置,每个小格代表关键点邻域所在 尺度空间的一个像素,箭头方向代表该像素的梯度方向,箭头长度代表梯度模值,图中蓝色 的圈代表高斯加权的范围(越靠近关键点的像素梯度方向信息贡献越大)。 然后在每 4×4 的小块上计算 8 个方向的梯度方向直方图,绘制每个梯度方向的累加值, 即可形成一个种子点,如图 5 右部分所示。此图中一个关键点由 2×2 共 4 个种子点组成, 每个种子点有 8 个方向向量信息。这种邻域方向性信息联合的思想增强了算法抗噪声的能 力,同时对于含有定位误差的特征匹配也提供了较好的容错性。 实际计算过程中,为了增强匹配的稳健性,Lowe 建议对每个关键点使用 4×4 共 16 个种子点来描述,这样对于一个关键点就可以产生 128 个数据,即最终形成 128 维的 SIFT 特征向量。此时 SIFT 特征向量已经去除了尺度变化、旋转等几何变形因素的影响,再继续 将特征向量的长度归一化,则可以进一步去除光照变化的影响。 五、最后一步:当两幅图像的 SIFT 特征向量生成后,下一步我们采用关键点特征向量 的欧式距离来作为两幅图像中关键点的相似性判定度量。取上图中,图像 A 中的某个关键 点,并找出其与图像 B 中欧式距离最近的前两个关键点,在这两个关键点中,如果最近的 距离除以次近的距离少于某个比例阈值,则接受这一对匹配点。降低这个比例阈值,SIFT 匹配点数目会减少,但更加稳定。 sift 算法的逐步 c 实现 ok,上文搅了那么多的理论,如果你没有看懂它,咋办列?没关系,下面,咱们来一步 一步实现此 sift 算法,即使你没有看到上述的理论,慢慢的,你也会明白 sift 算法到底是 怎么一回事,sift 算法到底是怎么实现的...。 yeah,请看: 前期工作: 在具体编写核心函数之前,得先做几个前期的准备工作: 0、头文件: view plaincopy to clipboardprint? 1. #ifdef _CH_ 2. #pragma package 3. #endif 4. 5. #ifndef _EiC 6. #include 7. 8. #include "stdlib.h" 9. #include "string.h" 10. #include "malloc.h" 11. #include "math.h" 12. #include 13. #include 14. #include 15. #include 16. #include 17. #include 18. #include 19. #endif 20. 21. #ifdef _EiC 22. #define WIN32 23. #endif 1、定义几个宏,及变量,以免下文函数中,突然冒出一个变量,而您却不知道怎么一回事: view plaincopy to clipboardprint? 1. #define NUMSIZE 2 2. #define GAUSSKERN 3.5 3. #define PI 3.14159265358979323846 4. 5. //Sigma of base image -- See D.L.'s paper. 6. #define INITSIGMA 0.5 7. //Sigma of each octave -- See D.L.'s paper. 8. #define SIGMA sqrt(3)//1.6// 9. 10. //Number of scales per octave. See D.L.'s paper. 11. #define SCALESPEROCTAVE 2 12. #define MAXOCTAVES 4 13. int numoctaves; 14. 15. #define CONTRAST_THRESHOLD 0.02 16. #define CURVATURE_THRESHOLD 10.0 17. #define DOUBLE_BASE_IMAGE_SIZE 1 18. #define peakRelThresh 0.8 19. #define LEN 128 20. 21. // temporary storage 22. CvMemStorage* storage = 0; 2、然后,咱们还得,声明几个变量,以及建几个数据结构(数据结构是一切程序事物的基 础麻,:D。): view plaincopy to clipboardprint? 1. //Data structure for a float image. 2. typedef struct ImageSt { /*金字塔每一层*/ 3. 4. float levelsigma; 5. int levelsigmalength; 6. float absolute_sigma; 7. CvMat *Level; //CvMat 是 OPENCV 的矩阵类,其元素可以是图像的象素值 8. } ImageLevels; 9. 10. typedef struct ImageSt1 { /*金字塔每一阶梯*/ 11. int row, col; //Dimensions of image. 12. float subsample; 13. ImageLevels *Octave; 14. } ImageOctaves; 15. 16. ImageOctaves *DOGoctaves; 17. //DOG pyr,DOG 算子计算简单,是尺度归一化的 LoG 算子的近似。 18. 19. ImageOctaves *mag_thresh ; 20. ImageOctaves *mag_pyr ; 21. ImageOctaves *grad_pyr ; 22. 23. //keypoint 数据结构,Lists of keypoints are linked by the "next" field. 24. typedef struct KeypointSt 25. { 26. float row, col; /* 反馈回原图像大小,特征点的位置 */ 27. float sx,sy; /* 金字塔中特征点的位置*/ 28. int octave,level;/*金字塔中,特征点所在的阶梯、层次*/ 29. 30. float scale, ori,mag; /*所在层的尺度 sigma,主方向 orientation (range [-PI,PI]), 以及幅值*/ 31. float *descrip; /*特征描述字指针:128 维或 32 维等*/ 32. struct KeypointSt *next;/* Pointer to next keypoint in list. */ 33. } *Keypoint; 34. 35. //定义特征点具体变量 36. Keypoint keypoints=NULL; //用于临时存储特征点的位置等 37. Keypoint keyDescriptors=NULL; //用于最后的确定特征点以及特征描述字 3、声明几个图像的基本处理函数: 图像处理基本函数,其实也可以用 OPENCV 的函数代替,但本文,咱们选择了用 c 语 言实现,尽量不用第三方库的东西,所以,还得自己编写这些函数: view plaincopy to clipboardprint? 1. CvMat * halfSizeImage(CvMat * im); //缩小图像:下采样 2. CvMat * doubleSizeImage(CvMat * im); //扩大图像:最近临方法 3. CvMat * doubleSizeImage2(CvMat * im); //扩大图像:线性插值 4. float getPixelBI(CvMat * im, float col, float row);//双线性插值函数 5. void normalizeVec(float* vec, int dim);//向量归一化 6. CvMat* GaussianKernel2D(float sigma); //得到 2 维高斯核 7. void normalizeMat(CvMat* mat) ; //矩阵归一化 8. float* GaussianKernel1D(float sigma, int dim) ; //得到 1 维高斯核 9. 10. //在具体像素处宽度方向进行高斯卷积 11. float ConvolveLocWidth(float* kernel, int dim, CvMat * src, int x, int y) ; 12. //在整个图像宽度方向进行 1D 高斯卷积 13. void Convolve1DWidth(float* kern, int dim, CvMat * src, CvMat * dst) ; 14. //在具体像素处高度方向进行高斯卷积 15. float ConvolveLocHeight(float* kernel, int dim, CvMat * src, int x, int y) ; 16. //在整个图像高度方向进行 1D 高斯卷积 17. void Convolve1DHeight(float* kern, int dim, CvMat * src, CvMat * dst); 18. //用高斯函数模糊图像 19. int BlurImage(CvMat * src, CvMat * dst, float sigma) ; 算法核心 本程序中,sift 算法被分为以下五个步骤及其相对应的函数(可能表述与上,或与前俩 篇文章有所偏差,但都一个意思): view plaincopy to clipboardprint? 1. //SIFT 算法第一步:图像预处理 2. CvMat *ScaleInitImage(CvMat * im) ; //金字塔初始化 3. 4. //SIFT 算法第二步:建立高斯金字塔函数 5. ImageOctaves* BuildGaussianOctaves(CvMat * image) ; //建立高斯金字塔 6. 7. //SIFT 算法第三步:特征点位置检测,最后确定特征点的位置 8. int DetectKeypoint(int numoctaves, ImageOctaves *GaussianPyr); 9. void DisplayKeypointLocation(IplImage* image, ImageOctaves *GaussianPyr); 10. 11. //SIFT 算法第四步:计算高斯图像的梯度方向和幅值,计算各个特征点的主方向 12. void ComputeGrad_DirecandMag(int numoctaves, ImageOctaves *GaussianPyr); 13. 14. int FindClosestRotationBin (int binCount, float angle); //进行方向直方图统 计 15. void AverageWeakBins (double* bins, int binCount); //对方向直方图滤波 16. //确定真正的主方向 17. bool InterpolateOrientation (double left, double middle,double right, double *degreeCorrection, double *peakValue); 18. //确定各个特征点处的主方向函数 19. void AssignTheMainOrientation(int numoctaves, ImageOctaves *GaussianPyr,Imag eOctaves *mag_pyr,ImageOctaves *grad_pyr); 20. //显示主方向 21. void DisplayOrientation (IplImage* image, ImageOctaves *GaussianPyr); 22. 23. //SIFT 算法第五步:抽取各个特征点处的特征描述字 24. void ExtractFeatureDescriptors(int numoctaves, ImageOctaves *GaussianPyr); 25. 26. //为了显示图象金字塔,而作的图像水平、垂直拼接 27. CvMat* MosaicHorizen( CvMat* im1, CvMat* im2 ); 28. CvMat* MosaicVertical( CvMat* im1, CvMat* im2 ); 29. 30. //特征描述点,网格 31. #define GridSpacing 4 主体实现 ok,以上所有的工作都就绪以后,那么接下来,咱们就先来编写 main 函数,因为你一 看主函数之后,你就立马能发现 sift 算法的工作流程及其原理了。 (主函数中涉及到的函数,下一篇文章:一、教你一步一步用 c 语言实现 sift 算法、下, 咱们自会一个一个编写): view plaincopy to clipboardprint? 1. int main( void ) 2. { 3. //声明当前帧 IplImage 指针 4. IplImage* src = NULL; 5. IplImage* image1 = NULL; 6. IplImage* grey_im1 = NULL; 7. IplImage* DoubleSizeImage = NULL; 8. 9. IplImage* mosaic1 = NULL; 10. IplImage* mosaic2 = NULL; 11. 12. CvMat* mosaicHorizen1 = NULL; 13. CvMat* mosaicHorizen2 = NULL; 14. CvMat* mosaicVertical1 = NULL; 15. 16. CvMat* image1Mat = NULL; 17. CvMat* tempMat=NULL; 18. 19. ImageOctaves *Gaussianpyr; 20. int rows,cols; 21. 22. #define Im1Mat(ROW,COL) ((float *)(image1Mat->data.fl + image1Mat->step/size of(float) *(ROW)))[(COL)] 23. 24. //灰度图象像素的数据结构 25. #define Im1B(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3] 26. #define Im1G(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3+1] 27. #define Im1R(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3+2] 28. 29. storage = cvCreateMemStorage(0); 30. 31. //读取图片 32. if( (src = cvLoadImage( "street1.jpg", 1)) == 0 ) // test1.jpg einstein.pg m back1.bmp 33. return -1; 34. 35. //为图像分配内存 36. image1 = cvCreateImage(cvSize(src->width, src->height), IPL_DEPTH_8U,3); 37. grey_im1 = cvCreateImage(cvSize(src->width, src->height), IPL_DEPTH_8U,1); 38. DoubleSizeImage = cvCreateImage(cvSize(2*(src->width), 2*(src->height)), I PL_DEPTH_8U,3); 39. 40. //为图像阵列分配内存,假设两幅图像的大小相同,tempMat 跟随 image1 的大小 41. image1Mat = cvCreateMat(src->height, src->width, CV_32FC1); 42. //转化成单通道图像再处理 43. cvCvtColor(src, grey_im1, CV_BGR2GRAY); 44. //转换进入 Mat 数据结构,图像操作使用的是浮点型操作 45. cvConvert(grey_im1, image1Mat); 46. 47. double t = (double)cvGetTickCount(); 48. //图像归一化 49. cvConvertScale( image1Mat, image1Mat, 1.0/255, 0 ); 50. 51. int dim = min(image1Mat->rows, image1Mat->cols); 52. numoctaves = (int) (log((double) dim) / log(2.0)) - 2; //金字塔阶数 53. numoctaves = min(numoctaves, MAXOCTAVES); 54. 55. //SIFT 算法第一步,预滤波除噪声,建立金字塔底层 56. tempMat = ScaleInitImage(image1Mat) ; 57. //SIFT 算法第二步,建立 Guassian 金字塔和 DOG 金字塔 58. Gaussianpyr = BuildGaussianOctaves(tempMat) ; 59. 60. t = (double)cvGetTickCount() - t; 61. printf( "the time of build Gaussian pyramid and DOG pyramid is %.1f\n", t/( cvGetTickFrequency()*1000.) ); 62. 63. #define ImLevels(OCTAVE,LEVEL,ROW,COL) ((float *)(Gaussianpyr[(OCTAVE)].Octa ve[(LEVEL)].Level->data.fl + Gaussianpyr[(OCTAVE)].Octave[(LEVEL)].Level->st ep/sizeof(float) *(ROW)))[(COL)] 64. //显示高斯金字塔 65. for (int i=0; iwidth, mosaicVertical1->hei ght), IPL_DEPTH_8U,1); 95. cvConvertScale( mosaicVertical1, mosaicVertical1, 255.0, 0 ); 96. cvConvertScaleAbs( mosaicVertical1, mosaic1, 1, 0 ); 97. 98. // cvSaveImage("GaussianPyramid of me.jpg",mosaic1); 99. cvNamedWindow("mosaic1",1); 100. cvShowImage("mosaic1", mosaic1); 101. cvWaitKey(0); 102. cvDestroyWindow("mosaic1"); 103. //显示 DOG 金字塔 104. for ( i=0; iwidth, mosaicVertical1->he ight), IPL_DEPTH_8U,1); 140. cvConvertScale( mosaicVertical1, mosaicVertical1, 255.0/(max_val-min_val), 0 ); 141. cvConvertScaleAbs( mosaicVertical1, mosaic2, 1, 0 ); 142. 143. // cvSaveImage("DOGPyramid of me.jpg",mosaic2); 144. cvNamedWindow("mosaic1",1); 145. cvShowImage("mosaic1", mosaic2); 146. cvWaitKey(0); 147. 148. //SIFT 算法第三步:特征点位置检测,最后确定特征点的位置 149. int keycount=DetectKeypoint(numoctaves, Gaussianpyr); 150. printf("the keypoints number are %d ;\n", keycount); 151. cvCopy(src,image1,NULL); 152. DisplayKeypointLocation( image1 ,Gaussianpyr); 153. 154. cvPyrUp( image1, DoubleSizeImage, CV_GAUSSIAN_5x5 ); 155. cvNamedWindow("image1",1); 156. cvShowImage("image1", DoubleSizeImage); 157. cvWaitKey(0); 158. cvDestroyWindow("image1"); 159. 160. //SIFT 算法第四步:计算高斯图像的梯度方向和幅值,计算各个特征点的主方向 161. ComputeGrad_DirecandMag(numoctaves, Gaussianpyr); 162. AssignTheMainOrientation( numoctaves, Gaussianpyr,mag_pyr,grad_pyr); 163. cvCopy(src,image1,NULL); 164. DisplayOrientation ( image1, Gaussianpyr); 165. 166. // cvPyrUp( image1, DoubleSizeImage, CV_GAUSSIAN_5x5 ); 167. cvNamedWindow("image1",1); 168. // cvResizeWindow("image1", 2*(image1->width), 2*(image1->height) ); 169. cvShowImage("image1", image1); 170. cvWaitKey(0); 171. 172. //SIFT 算法第五步:抽取各个特征点处的特征描述字 173. ExtractFeatureDescriptors( numoctaves, Gaussianpyr); 174. cvWaitKey(0); 175. 176. //销毁窗口 177. cvDestroyWindow("image1"); 178. cvDestroyWindow("mosaic1"); 179. //释放图像 180. cvReleaseImage(&image1); 181. cvReleaseImage(&grey_im1); 182. cvReleaseImage(&mosaic1); 183. cvReleaseImage(&mosaic2); 184. return 0; 185. } 更多见下文:一、教你一步一步用 c 语言实现 sift 算法、下。本文完。 版权声明: 本文版权归本人和 CSDN 共同拥有。转载,请注明出处及作者本人。 侵犯版权者,无论任何人,任何网站,1、永久追踪,2、永久谴责,3、永久追究法律责 任的权利。 July、二零一一年三月十二日声明。 九之再续、教你一步一步用 c 语言实现 sift 算法、下 作者:July、二零一一年三月十二日 出处:http://blog.csdn.net/v_JULY_v。 参考:Rob Hess 维护的 sift 库 环境:windows xp+vc6.0 条件:c 语言实现。 说明:本 BLOG 内会陆续一一实现所有经典算法。 --------------------------------------------------------------------------------------- 本文接上,教你一步一步用 c 语言实现 sift 算法、上,而来: 函数编写 ok,接上文,咱们一个一个的来编写 main 函数中所涉及到所有函数,这也是本文的关 键部分: view plaincopy to clipboardprint? 1. //下采样原来的图像,返回缩小 2 倍尺寸的图像 2. CvMat * halfSizeImage(CvMat * im) 3. { 4. unsigned int i,j; 5. int w = im->cols/2; 6. int h = im->rows/2; 7. CvMat *imnew = cvCreateMat(h, w, CV_32FC1); 8. 9. #define Im(ROW,COL) ((float *)(im->data.fl + im->step/sizeof(float) *(ROW))) [(COL)] 10. #define Imnew(ROW,COL) ((float *)(imnew->data.fl + imnew->step/sizeof(float) *(ROW)))[(COL)] 11. for ( j = 0; j < h; j++) 12. for ( i = 0; i < w; i++) 13. Imnew(j,i)=Im(j*2, i*2); 14. return imnew; 15. } 16. 17. //上采样原来的图像,返回放大 2 倍尺寸的图像 18. CvMat * doubleSizeImage(CvMat * im) 19. { 20. unsigned int i,j; 21. int w = im->cols*2; 22. int h = im->rows*2; 23. CvMat *imnew = cvCreateMat(h, w, CV_32FC1); 24. 25. #define Im(ROW,COL) ((float *)(im->data.fl + im->step/sizeof(float) *(ROW))) [(COL)] 26. #define Imnew(ROW,COL) ((float *)(imnew->data.fl + imnew->step/sizeof(float) *(ROW)))[(COL)] 27. 28. for ( j = 0; j < h; j++) 29. for ( i = 0; i < w; i++) 30. Imnew(j,i)=Im(j/2, i/2); 31. 32. return imnew; 33. } 34. 35. //上采样原来的图像,返回放大 2 倍尺寸的线性插值图像 36. CvMat * doubleSizeImage2(CvMat * im) 37. { 38. unsigned int i,j; 39. int w = im->cols*2; 40. int h = im->rows*2; 41. CvMat *imnew = cvCreateMat(h, w, CV_32FC1); 42. 43. #define Im(ROW,COL) ((float *)(im->data.fl + im->step/sizeof(float) *(ROW))) [(COL)] 44. #define Imnew(ROW,COL) ((float *)(imnew->data.fl + imnew->step/sizeof(float) *(ROW)))[(COL)] 45. 46. // fill every pixel so we don't have to worry about skipping pixels later 47. for ( j = 0; j < h; j++) 48. { 49. for ( i = 0; i < w; i++) 50. { 51. Imnew(j,i)=Im(j/2, i/2); 52. } 53. } 54. /* 55. A B C 56. E F G 57. H I J 58. pixels A C H J are pixels from original image 59. pixels B E G I F are interpolated pixels 60. */ 61. // interpolate pixels B and I 62. for ( j = 0; j < h; j += 2) 63. for ( i = 1; i < w - 1; i += 2) 64. Imnew(j,i)=0.5*(Im(j/2, i/2)+Im(j/2, i/2+1)); 65. // interpolate pixels E and G 66. for ( j = 1; j < h - 1; j += 2) 67. for ( i = 0; i < w; i += 2) 68. Imnew(j,i)=0.5*(Im(j/2, i/2)+Im(j/2+1, i/2)); 69. // interpolate pixel F 70. for ( j = 1; j < h - 1; j += 2) 71. for ( i = 1; i < w - 1; i += 2) 72. Imnew(j,i)=0.25*(Im(j/2, i/2)+Im(j/2+1, i/2)+Im(j/2, i/2+1)+Im(j/2+1, i /2+1)); 73. return imnew; 74. } 75. 76. //双线性插值,返回像素间的灰度值 77. float getPixelBI(CvMat * im, float col, float row) 78. { 79. int irow, icol; 80. float rfrac, cfrac; 81. float row1 = 0, row2 = 0; 82. int width=im->cols; 83. int height=im->rows; 84. #define ImMat(ROW,COL) ((float *)(im->data.fl + im->step/sizeof(float) *(ROW )))[(COL)] 85. 86. irow = (int) row; 87. icol = (int) col; 88. 89. if (irow < 0 || irow >= height 90. || icol < 0 || icol >= width) 91. return 0; 92. if (row > height - 1) 93. row = height - 1; 94. if (col > width - 1) 95. col = width - 1; 96. rfrac = 1.0 - (row - (float) irow); 97. cfrac = 1.0 - (col - (float) icol); 98. if (cfrac < 1) 99. { 100. row1 = cfrac * ImMat(irow,icol) + (1.0 - cfrac) * ImMat(irow,icol+1); 101. } 102. else 103. { 104. row1 = ImMat(irow,icol); 105. } 106. if (rfrac < 1) 107. { 108. if (cfrac < 1) 109. { 110. row2 = cfrac * ImMat(irow+1,icol) + (1.0 - cfrac) * ImMat(irow+1,icol+1) ; 111. } else 112. { 113. row2 = ImMat(irow+1,icol); 114. } 115. } 116. return rfrac * row1 + (1.0 - rfrac) * row2; 117. } 118. 119. //矩阵归一化 120. void normalizeMat(CvMat* mat) 121. { 122. #define Mat(ROW,COL) ((float *)(mat->data.fl + mat->step/sizeof(float) *(RO W)))[(COL)] 123. float sum = 0; 124. 125. for (unsigned int j = 0; j < mat->rows; j++) 126. for (unsigned int i = 0; i < mat->cols; i++) 127. sum += Mat(j,i); 128. for ( j = 0; j < mat->rows; j++) 129. for (unsigned int i = 0; i < mat->rows; i++) 130. Mat(j,i) /= sum; 131. } 132. 133. //向量归一化 134. void normalizeVec(float* vec, int dim) 135. { 136. unsigned int i; 137. float sum = 0; 138. for ( i = 0; i < dim; i++) 139. sum += vec[i]; 140. for ( i = 0; i < dim; i++) 141. vec[i] /= sum; 142. } 143. 144. //得到向量的欧式长度,2-范数 145. float GetVecNorm( float* vec, int dim ) 146. { 147. float sum=0.0; 148. for (unsigned int i=0;idata.fl + mat->step/sizeof(float) *(RO W)))[(COL)] 189. float s2 = sigma * sigma; 190. int c = dim / 2; 191. //printf("%d %d\n", mat.size(), mat[0].size()); 192. float m= 1.0/(sqrt(2.0 * CV_PI) * sigma); 193. for (int i = 0; i < (dim + 1) / 2; i++) 194. { 195. for (int j = 0; j < (dim + 1) / 2; j++) 196. { 197. //printf("%d %d %d\n", c, i, j); 198. float v = m * exp(-(1.0*i*i + 1.0*j*j) / (2.0 * s2)); 199. Mat(c+i,c+j) =v; 200. Mat(c-i,c+j) =v; 201. Mat(c+i,c-j) =v; 202. Mat(c-i,c-j) =v; 203. } 204. } 205. // normalizeMat(mat); 206. return mat; 207. } 208. 209. //x 方向像素处作卷积 210. float ConvolveLocWidth(float* kernel, int dim, CvMat * src, int x, int y) 211. { 212. #define Src(ROW,COL) ((float *)(src->data.fl + src->step/sizeof(float) *(RO W)))[(COL)] 213. unsigned int i; 214. float pixel = 0; 215. int col; 216. int cen = dim / 2; 217. //printf("ConvolveLoc(): Applying convoluation at location (%d, %d)\n", x, y); 218. for ( i = 0; i < dim; i++) 219. { 220. col = x + (i - cen); 221. if (col < 0) 222. col = 0; 223. if (col >= src->cols) 224. col = src->cols - 1; 225. pixel += kernel[i] * Src(y,col); 226. } 227. if (pixel > 1) 228. pixel = 1; 229. return pixel; 230. } 231. 232. //x 方向作卷积 233. void Convolve1DWidth(float* kern, int dim, CvMat * src, CvMat * dst) 234. { 235. #define DST(ROW,COL) ((float *)(dst->data.fl + dst->step/sizeof(float) *(RO W)))[(COL)] 236. unsigned int i,j; 237. 238. for ( j = 0; j < src->rows; j++) 239. { 240. for ( i = 0; i < src->cols; i++) 241. { 242. //printf("%d, %d\n", i, j); 243. DST(j,i) = ConvolveLocWidth(kern, dim, src, i, j); 244. } 245. } 246. } 247. 248. //y 方向像素处作卷积 249. float ConvolveLocHeight(float* kernel, int dim, CvMat * src, int x, int y) 250. { 251. #define Src(ROW,COL) ((float *)(src->data.fl + src->step/sizeof(float) *(RO W)))[(COL)] 252. unsigned int j; 253. float pixel = 0; 254. int cen = dim / 2; 255. //printf("ConvolveLoc(): Applying convoluation at location (%d, %d)\n", x, y); 256. for ( j = 0; j < dim; j++) 257. { 258. int row = y + (j - cen); 259. if (row < 0) 260. row = 0; 261. if (row >= src->rows) 262. row = src->rows - 1; 263. pixel += kernel[j] * Src(row,x); 264. } 265. if (pixel > 1) 266. pixel = 1; 267. return pixel; 268. } 269. 270. //y 方向作卷积 271. void Convolve1DHeight(float* kern, int dim, CvMat * src, CvMat * dst) 272. { 273. #define Dst(ROW,COL) ((float *)(dst->data.fl + dst->step/sizeof(float) *(RO W)))[(COL)] 274. unsigned int i,j; 275. for ( j = 0; j < src->rows; j++) 276. { 277. for ( i = 0; i < src->cols; i++) 278. { 279. //printf("%d, %d\n", i, j); 280. Dst(j,i) = ConvolveLocHeight(kern, dim, src, i, j); 281. } 282. } 283. } 284. 285. //卷积模糊图像 286. int BlurImage(CvMat * src, CvMat * dst, float sigma) 287. { 288. float* convkernel; 289. int dim = (int) max(3.0f, 2.0 * GAUSSKERN * sigma + 1.0f); 290. CvMat *tempMat; 291. // make dim odd 292. if (dim % 2 == 0) 293. dim++; 294. tempMat = cvCreateMat(src->rows, src->cols, CV_32FC1); 295. convkernel = GaussianKernel1D(sigma, dim); 296. 297. Convolve1DWidth(convkernel, dim, src, tempMat); 298. Convolve1DHeight(convkernel, dim, tempMat, dst); 299. cvReleaseMat(&tempMat); 300. return dim; 301. } 五个步骤 ok,接下来,进入重点部分,咱们依据上文介绍的 sift 算法的几个步骤,来一一实现 这些函数。 为了版述清晰,再贴一下,主函数,顺便再加强下对 sift 算法的五个步骤的认识: 1、SIFT 算法第一步:图像预处理 CvMat *ScaleInitImage(CvMat * im) ; //金字塔初始化 2、SIFT 算法第二步:建立高斯金字塔函数 ImageOctaves* BuildGaussianOctaves(CvMat * image) ; //建立高斯金字塔 3、SIFT 算法第三步:特征点位置检测,最后确定特征点的位置 int DetectKeypoint(int numoctaves, ImageOctaves *GaussianPyr); 4、SIFT 算法第四步:计算高斯图像的梯度方向和幅值,计算各个特征点的主方向 void ComputeGrad_DirecandMag(int numoctaves, ImageOctaves *GaussianPyr); 5、SIFT 算法第五步:抽取各个特征点处的特征描述字 void ExtractFeatureDescriptors(int numoctaves, ImageOctaves *GaussianPyr); ok,接下来一一具体实现这几个函数: SIFT 算法第一步 SIFT 算法第一步:扩大图像,预滤波剔除噪声,得到金字塔的最底层-第一阶的第一层: view plaincopy to clipboardprint? 1. CvMat *ScaleInitImage(CvMat * im) 2. { 3. double sigma,preblur_sigma; 4. CvMat *imMat; 5. CvMat * dst; 6. CvMat *tempMat; 7. //首先对图像进行平滑滤波,抑制噪声 8. imMat = cvCreateMat(im->rows, im->cols, CV_32FC1); 9. BlurImage(im, imMat, INITSIGMA); 10. //针对两种情况分别进行处理:初始化放大原始图像或者在原图像基础上进行后续操作 11. //建立金字塔的最底层 12. if (DOUBLE_BASE_IMAGE_SIZE) 13. { 14. tempMat = doubleSizeImage2(imMat);//对扩大两倍的图像进行二次采样,采样率为 0.5, 采用线性插值 15. #define TEMPMAT(ROW,COL) ((float *)(tempMat->data.fl + tempMat->step/sizeof( float) * (ROW)))[(COL)] 16. 17. dst = cvCreateMat(tempMat->rows, tempMat->cols, CV_32FC1); 18. preblur_sigma = 1.0;//sqrt(2 - 4*INITSIGMA*INITSIGMA); 19. BlurImage(tempMat, dst, preblur_sigma); 20. 21. // The initial blurring for the first image of the first octave of the pyr amid. 22. sigma = sqrt( (4*INITSIGMA*INITSIGMA) + preblur_sigma * preblur_sigm a ); 23. // sigma = sqrt(SIGMA * SIGMA - INITSIGMA * INITSIGMA * 4); 24. //printf("Init Sigma: %f\n", sigma); 25. BlurImage(dst, tempMat, sigma); //得到金字塔的最底层-放大 2 倍的图像 26. cvReleaseMat( &dst ); 27. return tempMat; 28. } 29. else 30. { 31. dst = cvCreateMat(im->rows, im->cols, CV_32FC1); 32. //sigma = sqrt(SIGMA * SIGMA - INITSIGMA * INITSIGMA); 33. preblur_sigma = 1.0;//sqrt(2 - 4*INITSIGMA*INITSIGMA); 34. sigma = sqrt( (4*INITSIGMA*INITSIGMA) + preblur_sigma * preblur_sigma ); 35. //printf("Init Sigma: %f\n", sigma); 36. BlurImage(imMat, dst, sigma); //得到金字塔的最底层:原始图像大小 37. return dst; 38. } 39. } SIFT 算法第二步 SIFT 第二步,建立 Gaussian 金字塔,给定金字塔第一阶第一层图像后,计算高斯金 字塔其他尺度图像, 每一阶的数目由变量 SCALESPEROCTAVE 决定,给定一个基本图像,计算它的高斯金字 塔图像,返回外部向量是阶梯指针,内部向量是每一个阶梯内部的不同尺度图像。 view plaincopy to clipboardprint? 1. //SIFT 算法第二步 2. ImageOctaves* BuildGaussianOctaves(CvMat * image) 3. { 4. ImageOctaves *octaves; 5. CvMat *tempMat; 6. CvMat *dst; 7. CvMat *temp; 8. 9. int i,j; 10. double k = pow(2, 1.0/((float)SCALESPEROCTAVE)); //方差倍数 11. float preblur_sigma, initial_sigma , sigma1,sigma2,sigma,absolute_sigma,sig ma_f; 12. //计算金字塔的阶梯数目 13. int dim = min(image->rows, image->cols); 14. int numoctaves = (int) (log((double) dim) / log(2.0)) - 2; //金字塔阶 数 15. //限定金字塔的阶梯数 16. numoctaves = min(numoctaves, MAXOCTAVES); 17. //为高斯金塔和 DOG 金字塔分配内存 18. octaves=(ImageOctaves*) malloc( numoctaves * sizeof(ImageOctaves) ); 19. DOGoctaves=(ImageOctaves*) malloc( numoctaves * sizeof(ImageOctaves) ); 20. 21. printf("BuildGaussianOctaves(): Base image dimension is %dx%d\n", (int)(0.5 *(image->cols)), (int)(0.5*(image->rows)) ); 22. printf("BuildGaussianOctaves(): Building %d octaves\n", numoctaves); 23. 24. // start with initial source image 25. tempMat=cvCloneMat( image ); 26. // preblur_sigma = 1.0;//sqrt(2 - 4*INITSIGMA*INITSIGMA); 27. initial_sigma = sqrt(2);//sqrt( (4*INITSIGMA*INITSIGMA) + preblur_sigma * preblur_sigma ); 28. // initial_sigma = sqrt(SIGMA * SIGMA - INITSIGMA * INITSIGMA * 4); 29. 30. //在每一阶金字塔图像中建立不同的尺度图像 31. for ( i = 0; i < numoctaves; i++) 32. { 33. //首先建立金字塔每一阶梯的最底层,其中 0 阶梯的最底层已经建立好 34. printf("Building octave %d of dimesion (%d, %d)\n", i, tempMat->cols,tempM at->rows); 35. //为各个阶梯分配内存 36. octaves[i].Octave= (ImageLevels*) malloc( (SCALESPEROCTAVE + 3) * sizeof(I mageLevels) ); 37. DOGoctaves[i].Octave= (ImageLevels*) malloc( (SCALESPEROCTAVE + 2) * sizeo f(ImageLevels) ); 38. //存储各个阶梯的最底层 39. (octaves[i].Octave)[0].Level=tempMat; 40. 41. octaves[i].col=tempMat->cols; 42. octaves[i].row=tempMat->rows; 43. DOGoctaves[i].col=tempMat->cols; 44. DOGoctaves[i].row=tempMat->rows; 45. if (DOUBLE_BASE_IMAGE_SIZE) 46. octaves[i].subsample=pow(2,i)*0.5; 47. else 48. octaves[i].subsample=pow(2,i); 49. 50. if(i==0) 51. { 52. (octaves[0].Octave)[0].levelsigma = initial_sigma; 53. (octaves[0].Octave)[0].absolute_sigma = initial_sigma; 54. printf("0 scale and blur sigma : %f \n", (octaves[0].subsample) * ((octav es[0].Octave)[0].absolute_sigma)); 55. } 56. else 57. { 58. (octaves[i].Octave)[0].levelsigma = (octaves[i-1].Octave)[SCALESPEROCTAVE ].levelsigma; 59. (octaves[i].Octave)[0].absolute_sigma = (octaves[i-1].Octave)[SC ALESPEROCTAVE].absolute_sigma; 60. printf( "0 scale and blur sigma : %f \n", ((octaves[i].Octave)[0].absolut e_sigma) ); 61. } 62. sigma = initial_sigma; 63. //建立本阶梯其他层的图像 64. for ( j = 1; j < SCALESPEROCTAVE + 3; j++) 65. { 66. dst = cvCreateMat(tempMat->rows, tempMat->cols, CV_32FC1);//用于存储高斯 层 67. temp = cvCreateMat(tempMat->rows, tempMat->cols, CV_32FC1);//用于存储 DOG 层 68. // 2 passes of 1D on original 69. // if(i!=0) 70. // { 71. // sigma1 = pow(k, j - 1) * ((octaves[i-1].Octave)[j-1].levelsigma) ; 72. // sigma2 = pow(k, j) * ((octaves[i].Octave)[j-1].levelsigma); 73. // sigma = sqrt(sigma2*sigma2 - sigma1*sigma1); 74. sigma_f= sqrt(k*k-1)*sigma; 75. // } 76. // else 77. // { 78. // sigma = sqrt(SIGMA * SIGMA - INITSIGMA * INITSIGMA * 4)*pow(k,j) ; 79. // } 80. sigma = k*sigma; 81. absolute_sigma = sigma * (octaves[i].subsample); 82. printf("%d scale and Blur sigma: %f \n", j, absolute_sigma); 83. 84. (octaves[i].Octave)[j].levelsigma = sigma; 85. (octaves[i].Octave)[j].absolute_sigma = absolute_sigma; 86. //产生高斯层 87. int length=BlurImage((octaves[i].Octave)[j-1].Level, dst, sigma_f);//相应 尺度 88. (octaves[i].Octave)[j].levelsigmalength = length; 89. (octaves[i].Octave)[j].Level=dst; 90. //产生 DOG 层 91. cvSub( ((octaves[i].Octave)[j]).Level, ((octaves[i].Octave)[j-1] ).Level, temp, 0 ); 92. // cvAbsDiff( ((octaves[i].Octave)[j]).Level, ((octaves[i].Octave )[j-1]).Level, temp ); 93. ((DOGoctaves[i].Octave)[j-1]).Level=temp; 94. } 95. // halve the image size for next iteration 96. tempMat = halfSizeImage( ( (octaves[i].Octave)[SCALESPEROCTAVE].Level ) ) ; 97. } 98. return octaves; 99. } SIFT 算法第三步 SIFT 算法第三步,特征点位置检测,最后确定特征点的位置检测 DOG 金字塔中的局 部最大值,找到之后,还要经过两个检验才能确认为特征点:一是它必须有明显的差异,二 是他不应该是边缘点,(也就是说,在极值点处的主曲率比应该小于某一个阈值)。 view plaincopy to clipboardprint? 1. //SIFT 算法第三步,特征点位置检测, 2. int DetectKeypoint(int numoctaves, ImageOctaves *GaussianPyr) 3. { 4. //计算用于 DOG 极值点检测的主曲率比的阈值 5. double curvature_threshold; 6. curvature_threshold= ((CURVATURE_THRESHOLD + 1)*(CURVATURE_THRESHOLD + 1))/ CURVATURE_THRESHOLD; 7. #define ImLevels(OCTAVE,LEVEL,ROW,COL) ((float *)(DOGoctaves[(OCTAVE)].Octav e[(LEVEL)].Level->data.fl + DOGoctaves[(OCTAVE)].Octave[(LEVEL)].Level->step /sizeof(float) *(ROW)))[(COL)] 8. 9. int keypoint_count = 0; 10. for (int i=0; i= CONTRAST_THRESHOLD ) 22. { 23. 24. if ( ImLevels(i,j,m,n)!=0.0 ) //1、首先是非零 25. { 26. float inf_val=ImLevels(i,j,m,n); 27. if(( (inf_val <= ImLevels(i,j-1,m-1,n-1))&& 28. (inf_val <= ImLevels(i,j-1,m ,n-1))&& 29. (inf_val <= ImLevels(i,j-1,m+1,n-1))&& 30. (inf_val <= ImLevels(i,j-1,m-1,n ))&& 31. (inf_val <= ImLevels(i,j-1,m ,n ))&& 32. (inf_val <= ImLevels(i,j-1,m+1,n ))&& 33. (inf_val <= ImLevels(i,j-1,m-1,n+1))&& 34. (inf_val <= ImLevels(i,j-1,m ,n+1))&& 35. (inf_val <= ImLevels(i,j-1,m+1,n+1))&& //底层的小尺度 9 36. 37. (inf_val <= ImLevels(i,j,m-1,n-1))&& 38. (inf_val <= ImLevels(i,j,m ,n-1))&& 39. (inf_val <= ImLevels(i,j,m+1,n-1))&& 40. (inf_val <= ImLevels(i,j,m-1,n ))&& 41. (inf_val <= ImLevels(i,j,m+1,n ))&& 42. (inf_val <= ImLevels(i,j,m-1,n+1))&& 43. (inf_val <= ImLevels(i,j,m ,n+1))&& 44. (inf_val <= ImLevels(i,j,m+1,n+1))&& //当前层 8 45. 46. (inf_val <= ImLevels(i,j+1,m-1,n-1))&& 47. (inf_val <= ImLevels(i,j+1,m ,n-1))&& 48. (inf_val <= ImLevels(i,j+1,m+1,n-1))&& 49. (inf_val <= ImLevels(i,j+1,m-1,n ))&& 50. (inf_val <= ImLevels(i,j+1,m ,n ))&& 51. (inf_val <= ImLevels(i,j+1,m+1,n ))&& 52. (inf_val <= ImLevels(i,j+1,m-1,n+1))&& 53. (inf_val <= ImLevels(i,j+1,m ,n+1))&& 54. (inf_val <= ImLevels(i,j+1,m+1,n+1)) //下一层大尺度 9 55. ) || 56. ( (inf_val >= ImLevels(i,j-1,m-1,n-1))&& 57. (inf_val >= ImLevels(i,j-1,m ,n-1))&& 58. (inf_val >= ImLevels(i,j-1,m+1,n-1))&& 59. (inf_val >= ImLevels(i,j-1,m-1,n ))&& 60. (inf_val >= ImLevels(i,j-1,m ,n ))&& 61. (inf_val >= ImLevels(i,j-1,m+1,n ))&& 62. (inf_val >= ImLevels(i,j-1,m-1,n+1))&& 63. (inf_val >= ImLevels(i,j-1,m ,n+1))&& 64. (inf_val >= ImLevels(i,j-1,m+1,n+1))&& 65. 66. (inf_val >= ImLevels(i,j,m-1,n-1))&& 67. (inf_val >= ImLevels(i,j,m ,n-1))&& 68. (inf_val >= ImLevels(i,j,m+1,n-1))&& 69. (inf_val >= ImLevels(i,j,m-1,n ))&& 70. (inf_val >= ImLevels(i,j,m+1,n ))&& 71. (inf_val >= ImLevels(i,j,m-1,n+1))&& 72. (inf_val >= ImLevels(i,j,m ,n+1))&& 73. (inf_val >= ImLevels(i,j,m+1,n+1))&& 74. 75. (inf_val >= ImLevels(i,j+1,m-1,n-1))&& 76. (inf_val >= ImLevels(i,j+1,m ,n-1))&& 77. (inf_val >= ImLevels(i,j+1,m+1,n-1))&& 78. (inf_val >= ImLevels(i,j+1,m-1,n ))&& 79. (inf_val >= ImLevels(i,j+1,m ,n ))&& 80. (inf_val >= ImLevels(i,j+1,m+1,n ))&& 81. (inf_val >= ImLevels(i,j+1,m-1,n+1))&& 82. (inf_val >= ImLevels(i,j+1,m ,n+1))&& 83. (inf_val >= ImLevels(i,j+1,m+1,n+1)) 84. ) ) //2、满足 26 个中极值点 85. { 86. //此处可存储 87. //然后必须具有明显的显著性,即必须大于 CONTRAST_THRESHOLD=0.02 88. if ( fabs(ImLevels(i,j,m,n))>= CONTRAST_THRESHOLD ) 89. { 90. //最后显著处的特征点必须具有足够的曲率比,CURVATURE_THRESHOLD=10.0,首先 计算 Hessian 矩阵 91. // Compute the entries of the Hessian matrix at the extrema locatio n. 92. /* 93. 1 0 -1 94. 0 0 0 95. -1 0 1 *0.25 96. */ 97. // Compute the trace and the determinant of the Hessian. 98. //Tr_H = Dxx + Dyy; 99. //Det_H = Dxx*Dyy - Dxy^2; 100. float Dxx,Dyy,Dxy,Tr_H,Det_H,curvature_ratio; 101. Dxx = ImLevels(i,j,m,n-1) + ImLevels(i,j,m,n+1)-2.0*ImLevels(i,j,m ,n); 102. Dyy = ImLevels(i,j,m-1,n) + ImLevels(i,j,m+1,n)-2.0*ImLevels(i,j,m ,n); 103. Dxy = ImLevels(i,j,m-1,n-1) + ImLevels(i,j,m+1,n+1) - ImLevels(i,j ,m+1,n-1) - ImLevels(i,j,m-1,n+1); 104. Tr_H = Dxx + Dyy; 105. Det_H = Dxx*Dyy - Dxy*Dxy; 106. // Compute the ratio of the principal curvatures. 107. curvature_ratio = (1.0*Tr_H*Tr_H)/Det_H; 108. if ( (Det_H>=0.0) && (curvature_ratio <= curvature_threshold) ) / /最后得到最具有显著性特征的特征点 109. { 110. //将其存储起来,以计算后面的特征描述字 111. keypoint_count++; 112. Keypoint k; 113. /* Allocate memory for the keypoint. */ 114. k = (Keypoint) malloc(sizeof(struct KeypointSt)); 115. k->next = keypoints; 116. keypoints = k; 117. k->row = m*(GaussianPyr[i].subsample); 118. k->col =n*(GaussianPyr[i].subsample); 119. k->sy = m; //行 120. k->sx = n; //列 121. k->octave=i; 122. k->level=j; 123. k->scale = (GaussianPyr[i].Octave)[j].absolute_sigma; 124. }//if >curvature_thresh 125. }//if >contrast 126. }//if inf value 127. }//if non zero 128. }//if >contrast 129. } //for concrete image level col 130. }//for levels 131. }//for octaves 132. return keypoint_count; 133. } 134. 135. //在图像中,显示 SIFT 特征点的位置 136. void DisplayKeypointLocation(IplImage* image, ImageOctaves *GaussianPyr) 137. { 138. 139. Keypoint p = keypoints; // p 指向第一个结点 140. while(p) // 没到表尾 141. { 142. cvLine( image, cvPoint((int)((p->col)-3),(int)(p->row)), 143. cvPoint((int)((p->col)+3),(int)(p->row)), CV_RGB(255,255,0), 144. 1, 8, 0 ); 145. cvLine( image, cvPoint((int)(p->col),(int)((p->row)-3)), 146. cvPoint((int)(p->col),(int)((p->row)+3)), CV_RGB(255,255,0), 147. 1, 8, 0 ); 148. // cvCircle(image,cvPoint((uchar)(p->col),(uchar)(p->row)), 149. // (int)((GaussianPyr[p->octave].Octave)[p->level].absolute_sigma), 150. // CV_RGB(255,0,0),1,8,0); 151. p=p->next; 152. } 153. } 154. 155. // Compute the gradient direction and magnitude of the gaussian pyramid ima ges 156. void ComputeGrad_DirecandMag(int numoctaves, ImageOctaves *GaussianPyr) 157. { 158. // ImageOctaves *mag_thresh ; 159. mag_pyr=(ImageOctaves*) malloc( numoctaves * sizeof(ImageOctaves) ); 160. grad_pyr=(ImageOctaves*) malloc( numoctaves * sizeof(ImageOctaves) ); 161. // float sigma=( (GaussianPyr[0].Octave)[SCALESPEROCTAVE+2].absolute_sigma ) / GaussianPyr[0].subsample; 162. // int dim = (int) (max(3.0f, 2 * GAUSSKERN *sigma + 1.0f)*0.5+0.5); 163. #define ImLevels(OCTAVE,LEVEL,ROW,COL) ((float *)(GaussianPyr[(OCTAVE)].Oct ave[(LEVEL)].Level->data.fl + GaussianPyr[(OCTAVE)].Octave[(LEVEL)].Level->s tep/sizeof(float) *(ROW)))[(COL)] 164. for (int i=0; idata.fl + Mag->step/sizeof(float) *(RO W)))[(COL)] 179. #define ORI(ROW,COL) ((float *)(Ori->data.fl + Ori->step/sizeof(float) *(RO W)))[(COL)] 180. #define TEMPMAT1(ROW,COL) ((float *)(tempMat1->data.fl + tempMat1->step/siz eof(float) *(ROW)))[(COL)] 181. #define TEMPMAT2(ROW,COL) ((float *)(tempMat2->data.fl + tempMat2->step/siz eof(float) *(ROW)))[(COL)] 182. for (int m=1;m<(GaussianPyr[i].row-1);m++) 183. for(int n=1;n<(GaussianPyr[i].col-1);n++) 184. { 185. //计算幅值 186. TEMPMAT1(m,n) = 0.5*( ImLevels(i,j,m,n+1)-ImLevels(i,j,m,n-1) ); //dx 187. TEMPMAT2(m,n) = 0.5*( ImLevels(i,j,m+1,n)-ImLevels(i,j, m-1,n) ); //dy 188. MAG(m,n) = sqrt(TEMPMAT1(m,n)*TEMPMAT1(m,n)+TEMPMAT2(m, n)*TEMPMAT2(m,n)); //mag 189. //计算方向 190. ORI(m,n) =atan( TEMPMAT2(m,n)/TEMPMAT1(m,n) ); 191. if (ORI(m,n)==CV_PI) 192. ORI(m,n)=-CV_PI; 193. } 194. ((mag_pyr[i].Octave)[j-1]).Level=Mag; 195. ((grad_pyr[i].Octave)[j-1]).Level=Ori; 196. cvReleaseMat(&tempMat1); 197. cvReleaseMat(&tempMat2); 198. }//for levels 199. }//for octaves 200. } SIFT 算法第四步 view plaincopy to clipboardprint? 1. //SIFT 算法第四步:计算各个特征点的主方向,确定主方向 2. void AssignTheMainOrientation(int numoctaves, ImageOctaves *GaussianPyr,Imag eOctaves *mag_pyr,ImageOctaves *grad_pyr) 3. { 4. // Set up the histogram bin centers for a 36 bin histogram. 5. int num_bins = 36; 6. float hist_step = 2.0*PI/num_bins; 7. float hist_orient[36]; 8. for (int i=0;i<36;i++) 9. hist_orient[i]=-PI+i*hist_step; 10. float sigma1=( ((GaussianPyr[0].Octave)[SCALESPEROCTAVE].absolute_sigma) ) / (GaussianPyr[0].subsample);//SCALESPEROCTAVE+2 11. int zero_pad = (int) (max(3.0f, 2 * GAUSSKERN *sigma1 + 1.0f)*0.5+0.5); 12. //Assign orientations to the keypoints. 13. #define ImLevels(OCTAVES,LEVELS,ROW,COL) ((float *)((GaussianPyr[(OCTAVES)]. Octave[(LEVELS)].Level)->data.fl + (GaussianPyr[(OCTAVES)].Octave[(LEVELS)]. Level)->step/sizeof(float) *(ROW)))[(COL)] 14. 15. int keypoint_count = 0; 16. Keypoint p = keypoints; // p 指向第一个结点 17. 18. while(p) // 没到表尾 19. { 20. int i=p->octave; 21. int j=p->level; 22. int m=p->sy; //行 23. int n=p->sx; //列 24. if ((m>=zero_pad)&&(m=zero_pad)&&(nrows)); 31. //分配用于存储 Patch 幅值和方向的空间 32. #define MAT(ROW,COL) ((float *)(mat->data.fl + mat->step/sizeof(float) *(ROW )))[(COL)] 33. 34. //声明方向直方图变量 35. double* orienthist = (double *) malloc(36 * sizeof(double)); 36. for ( int sw = 0 ; sw < 36 ; ++sw) 37. { 38. orienthist[sw]=0.0; 39. } 40. //在特征点的周围统计梯度方向 41. for (int x=m-dim,mm=0;x<=(m+dim);x++,mm++) 42. for(int y=n-dim,nn=0;y<=(n+dim);y++,nn++) 43. { 44. //计算特征点处的幅值 45. double dx = 0.5*(ImLevels(i,j,x,y+1)-ImLevels(i,j,x,y-1)); //dx 46. double dy = 0.5*(ImLevels(i,j,x+1,y)-ImLevels(i,j,x-1,y)); //dy 47. double mag = sqrt(dx*dx+dy*dy); //mag 48. //计算方向 49. double Ori =atan( 1.0*dy/dx ); 50. int binIdx = FindClosestRotationBin(36, Ori); //得到离 现有方向最近的直方块 51. orienthist[binIdx] = orienthist[binIdx] + 1.0* mag * MAT(mm,nn);//利用高 斯加权累加进直方图相应的块 52. } 53. // Find peaks in the orientation histogram using nonmax suppression. 54. AverageWeakBins (orienthist, 36); 55. // find the maximum peak in gradient orientation 56. double maxGrad = 0.0; 57. int maxBin = 0; 58. for (int b = 0 ; b < 36 ; ++b) 59. { 60. if (orienthist[b] > maxGrad) 61. { 62. maxGrad = orienthist[b]; 63. maxBin = b; 64. } 65. } 66. // First determine the real interpolated peak high at the maximum bin 67. // position, which is guaranteed to be an absolute peak. 68. double maxPeakValue=0.0; 69. double maxDegreeCorrection=0.0; 70. if ( (InterpolateOrientation ( orienthist[maxBin == 0 ? (36 - 1) : (maxB in - 1)], 71. orienthist[maxBin], orienthist[(maxBin + 1) % 36], 72. &maxDegreeCorrection, &maxPeakValue)) == false) 73. printf("BUG: Parabola fitting broken"); 74. 75. // Now that we know the maximum peak value, we can find other keypoint 76. // orientations, which have to fulfill two criterias: 77. // 78. // 1. They must be a local peak themselves. Else we might add a very 79. // similar keypoint orientation twice (imagine for example the 80. // values: 0.4 1.0 0.8, if 1.0 is maximum peak, 0.8 is still added 81. // with the default threshhold, but the maximum peak orientation 82. // was already added). 83. // 2. They must have at least peakRelThresh times the maximum peak 84. // value. 85. bool binIsKeypoint[36]; 86. for ( b = 0 ; b < 36 ; ++b) 87. { 88. binIsKeypoint[b] = false; 89. // The maximum peak of course is 90. if (b == maxBin) 91. { 92. binIsKeypoint[b] = true; 93. continue; 94. } 95. // Local peaks are, too, in case they fulfill the threshhold 96. if (orienthist[b] < (peakRelThresh * maxPeakValue)) 97. continue; 98. int leftI = (b == 0) ? (36 - 1) : (b - 1); 99. int rightI = (b + 1) % 36; 100. if (orienthist[b] <= orienthist[leftI] || orienthist[b] <= orienthist[ rightI]) 101. continue; // no local peak 102. binIsKeypoint[b] = true; 103. } 104. // find other possible locations 105. double oneBinRad = (2.0 * PI) / 36; 106. for ( b = 0 ; b < 36 ; ++b) 107. { 108. if (binIsKeypoint[b] == false) 109. continue; 110. int bLeft = (b == 0) ? (36 - 1) : (b - 1); 111. int bRight = (b + 1) % 36; 112. // Get an interpolated peak direction and value guess. 113. double peakValue; 114. double degreeCorrection; 115. 116. double maxPeakValue, maxDegreeCorrection; 117. if (InterpolateOrientation ( orienthist[maxBin == 0 ? (36 - 1) : (maxB in - 1)], 118. orienthist[maxBin], orienthist[(maxBin + 1) % 36], 119. °reeCorrection, &peakValue) == false) 120. { 121. printf("BUG: Parabola fitting broken"); 122. } 123. 124. double degree = (b + degreeCorrection) * oneBinRad - PI; 125. if (degree < -PI) 126. degree += 2.0 * PI; 127. else if (degree > PI) 128. degree -= 2.0 * PI; 129. //存储方向,可以直接利用检测到的链表进行该步主方向的指定; 130. //分配内存重新存储特征点 131. Keypoint k; 132. /* Allocate memory for the keypoint Descriptor. */ 133. k = (Keypoint) malloc(sizeof(struct KeypointSt)); 134. k->next = keyDescriptors; 135. keyDescriptors = k; 136. k->descrip = (float*)malloc(LEN * sizeof(float)); 137. k->row = p->row; 138. k->col = p->col; 139. k->sy = p->sy; //行 140. k->sx = p->sx; //列 141. k->octave = p->octave; 142. k->level = p->level; 143. k->scale = p->scale; 144. k->ori = degree; 145. k->mag = peakValue; 146. }//for 147. free(orienthist); 148. } 149. p=p->next; 150. } 151. } 152. 153. //寻找与方向直方图最近的柱,确定其 index 154. int FindClosestRotationBin (int binCount, float angle) 155. { 156. angle += CV_PI; 157. angle /= 2.0 * CV_PI; 158. // calculate the aligned bin 159. angle *= binCount; 160. int idx = (int) angle; 161. if (idx == binCount) 162. idx = 0; 163. return (idx); 164. } 165. 166. // Average the content of the direction bins. 167. void AverageWeakBins (double* hist, int binCount) 168. { 169. // TODO: make some tests what number of passes is the best. (its clear 170. // one is not enough, as we may have something like 171. // ( 0.4, 0.4, 0.3, 0.4, 0.4 )) 172. for (int sn = 0 ; sn < 2 ; ++sn) 173. { 174. double firstE = hist[0]; 175. double last = hist[binCount-1]; 176. for (int sw = 0 ; sw < binCount ; ++sw) 177. { 178. double cur = hist[sw]; 179. double next = (sw == (binCount - 1)) ? firstE : hist[(sw + 1) % binCount ]; 180. hist[sw] = (last + cur + next) / 3.0; 181. last = cur; 182. } 183. } 184. } 185. 186. // Fit a parabol to the three points (-1.0 ; left), (0.0 ; middle) and 187. // (1.0 ; right). 188. // Formulas: 189. // f(x) = a (x - c)^2 + b 190. // c is the peak offset (where f'(x) is zero), b is the peak value. 191. // In case there is an error false is returned, otherwise a correction 192. // value between [-1 ; 1] is returned in 'degreeCorrection', where -1 193. // means the peak is located completely at the left vector, and -0.5 just 194. // in the middle between left and middle and > 0 to the right side. In 195. // 'peakValue' the maximum estimated peak value is stored. 196. bool InterpolateOrientation (double left, double middle,double right, doubl e *degreeCorrection, double *peakValue) 197. { 198. double a = ((left + right) - 2.0 * middle) / 2.0; //抛物线捏合系数 a 199. // degreeCorrection = peakValue = Double.NaN; 200. 201. // Not a parabol 202. if (a == 0.0) 203. return false; 204. double c = (((left - middle) / a) - 1.0) / 2.0; 205. double b = middle - c * c * a; 206. if (c < -0.5 || c > 0.5) 207. return false; 208. *degreeCorrection = c; 209. *peakValue = b; 210. return true; 211. } 212. 213. //显示特征点处的主方向 214. void DisplayOrientation (IplImage* image, ImageOctaves *GaussianPyr) 215. { 216. Keypoint p = keyDescriptors; // p 指向第一个结点 217. while(p) // 没到表尾 218. { 219. float scale=(GaussianPyr[p->octave].Octave)[p->level].absolute_sigma; 220. float autoscale = 3.0; 221. float uu=autoscale*scale*cos(p->ori); 222. float vv=autoscale*scale*sin(p->ori); 223. float x=(p->col)+uu; 224. float y=(p->row)+vv; 225. cvLine( image, cvPoint((int)(p->col),(int)(p->row)), 226. cvPoint((int)x,(int)y), CV_RGB(255,255,0), 227. 1, 8, 0 ); 228. // Arrow head parameters 229. float alpha = 0.33; // Size of arrow head relative to the length of the vector 230. float beta = 0.33; // Width of the base of the arrow head relative to the length 231. 232. float xx0= (p->col)+uu-alpha*(uu+beta*vv); 233. float yy0= (p->row)+vv-alpha*(vv-beta*uu); 234. float xx1= (p->col)+uu-alpha*(uu-beta*vv); 235. float yy1= (p->row)+vv-alpha*(vv+beta*uu); 236. cvLine( image, cvPoint((int)xx0,(int)yy0), 237. cvPoint((int)x,(int)y), CV_RGB(255,255,0), 238. 1, 8, 0 ); 239. cvLine( image, cvPoint((int)xx1,(int)yy1), 240. cvPoint((int)x,(int)y), CV_RGB(255,255,0), 241. 1, 8, 0 ); 242. p=p->next; 243. } 244. } SIFT 算法第五步 SIFT 算法第五步:抽取各个特征点处的特征描述字,确定特征点的描述字。描述字是 Patch 网格内梯度方向的描述,旋转网格到主方向,插值得到网格处梯度值。 一个特征点可以用 2*2*8=32 维的向量,也可以用 4*4*8=128 维的向量更精确的进行 描述。 view plaincopy to clipboardprint? 1. void ExtractFeatureDescriptors(int numoctaves, ImageOctaves *GaussianPyr) 2. { 3. // The orientation histograms have 8 bins 4. float orient_bin_spacing = PI/4; 5. float orient_angles[8]={-PI,-PI+orient_bin_spacing,-PI*0.5, -orient_bin_ spacing, 6. 0.0, orient_bin_spacing, PI*0.5, PI+orient_bin_spacing}; 7. //产生描述字中心各点坐标 8. float *feat_grid=(float *) malloc( 2*16 * sizeof(float)); 9. for (int i=0;ioctave].Octave)[p->level].absolute_sigma; 32. 33. float sine = sin(p->ori); 34. float cosine = cos(p->ori); 35. //计算中心点坐标旋转之后的位置 36. float *featcenter=(float *) malloc( 2*16 * sizeof(float)); 37. for (int i=0;isx); 44. featcenter[i*2*GridSpacing+j+1]=((-sine * x + cosine * y) + p->sy); 45. } 46. } 47. // calculate sample window coordinates (rotated along keypoint) 48. float *feat=(float *) malloc( 2*256 * sizeof(float)); 49. for ( i=0;i<64*GridSpacing;i++,i++) 50. { 51. float x=feat_samples[i]; 52. float y=feat_samples[i+1]; 53. feat[i]=((cosine * x + sine * y) + p->sx); 54. feat[i+1]=((-sine * x + cosine * y) + p->sy); 55. } 56. //Initialize the feature descriptor. 57. float *feat_desc = (float *) malloc( 128 * sizeof(float)); 58. for (i=0;i<128;i++) 59. { 60. feat_desc[i]=0.0; 61. // printf("%f ",feat_desc[i]); 62. } 63. //printf("\n"); 64. for ( i=0;i<512;++i,++i) 65. { 66. float x_sample = feat[i]; 67. float y_sample = feat[i+1]; 68. // Interpolate the gradient at the sample position 69. /* 70. 0 1 0 71. 1 * 1 72. 0 1 0 具体插值策略如图示 73. */ 74. float sample12=getPixelBI(((GaussianPyr[p->octave].Octave)[p->level]).Lev el, x_sample, y_sample-1); 75. float sample21=getPixelBI(((GaussianPyr[p->octave].Octave)[p->level]).Lev el, x_sample-1, y_sample); 76. float sample22=getPixelBI(((GaussianPyr[p->octave].Octave)[p->level]).Lev el, x_sample, y_sample); 77. float sample23=getPixelBI(((GaussianPyr[p->octave].Octave)[p->level]).Lev el, x_sample+1, y_sample); 78. float sample32=getPixelBI(((GaussianPyr[p->octave].Octave)[p->level]).Lev el, x_sample, y_sample+1); 79. //float diff_x = 0.5*(sample23 - sample21); 80. //float diff_y = 0.5*(sample32 - sample12); 81. float diff_x = sample23 - sample21; 82. float diff_y = sample32 - sample12; 83. float mag_sample = sqrt( diff_x*diff_x + diff_y*diff_y ); 84. float grad_sample = atan( diff_y / diff_x ); 85. if(grad_sample == CV_PI) 86. grad_sample = -CV_PI; 87. // Compute the weighting for the x and y dimensions. 88. float *x_wght=(float *) malloc( GridSpacing * GridSpacing * size of(float)); 89. float *y_wght=(float *) malloc( GridSpacing * GridSpacing * size of(float)); 90. float *pos_wght=(float *) malloc( 8*GridSpacing * GridSpacing * sizeof(fl oat));; 91. for (int m=0;m<32;++m,++m) 92. { 93. float x=featcenter[m]; 94. float y=featcenter[m+1]; 95. x_wght[m/2] = max(1 - (fabs(x - x_sample)*1.0/GridSpacing), 0); 96. y_wght[m/2] = max(1 - (fabs(y - y_sample)*1.0/GridSpacing), 0); 97. 98. } 99. for ( m=0;m<16;++m) 100. for (int n=0;n<8;++n) 101. pos_wght[m*8+n]=x_wght[m]*y_wght[m]; 102. free(x_wght); 103. free(y_wght); 104. //计算方向的加权,首先旋转梯度场到主方向,然后计算差异 105. float diff[8],orient_wght[128]; 106. for ( m=0;m<8;++m) 107. { 108. float angle = grad_sample-(p->ori)-orient_angles[m]+CV_PI; 109. float temp = angle / (2.0 * CV_PI); 110. angle -= (int)(temp) * (2.0 * CV_PI); 111. diff[m]= angle - CV_PI; 112. } 113. // Compute the gaussian weighting. 114. float x=p->sx; 115. float y=p->sy; 116. float g = exp(-((x_sample-x)*(x_sample-x)+(y_sample-y)*(y_sample-y))/(2 *feat_window*feat_window))/(2*CV_PI*feat_window*feat_window); 117. 118. for ( m=0;m<128;++m) 119. { 120. orient_wght[m] = max((1.0 - 1.0*fabs(diff[m%8])/orient_bin_spacing),0) ; 121. feat_desc[m] = feat_desc[m] + orient_wght[m]*pos_wght[m]*g*mag_sample; 122. } 123. free(pos_wght); 124. } 125. free(feat); 126. free(featcenter); 127. float norm=GetVecNorm( feat_desc, 128); 128. for (int m=0;m<128;m++) 129. { 130. feat_desc[m]/=norm; 131. if (feat_desc[m]>0.2) 132. feat_desc[m]=0.2; 133. } 134. norm=GetVecNorm( feat_desc, 128); 135. for ( m=0;m<128;m++) 136. { 137. feat_desc[m]/=norm; 138. printf("%f ",feat_desc[m]); 139. } 140. printf("\n"); 141. p->descrip = feat_desc; 142. p=p->next; 143. } 144. free(feat_grid); 145. free(feat_samples); 146. } 147. 148. //为了显示图象金字塔,而作的图像水平拼接 149. CvMat* MosaicHorizen( CvMat* im1, CvMat* im2 ) 150. { 151. int row,col; 152. CvMat *mosaic = cvCreateMat( max(im1->rows,im2->rows),(im1->cols+im2->cols ),CV_32FC1); 153. #define Mosaic(ROW,COL) ((float*)(mosaic->data.fl + mosaic->step/sizeof(flo at)*(ROW)))[(COL)] 154. #define Im11Mat(ROW,COL) ((float *)(im1->data.fl + im1->step/sizeof(float) *(ROW)))[(COL)] 155. #define Im22Mat(ROW,COL) ((float *)(im2->data.fl + im2->step/sizeof(float) *(ROW)))[(COL)] 156. cvZero(mosaic); 157. /* Copy images into mosaic1. */ 158. for ( row = 0; row < im1->rows; row++) 159. for ( col = 0; col < im1->cols; col++) 160. Mosaic(row,col)=Im11Mat(row,col) ; 161. for ( row = 0; row < im2->rows; row++) 162. for ( col = 0; col < im2->cols; col++) 163. Mosaic(row, (col+im1->cols) )= Im22Mat(row,col) ; 164. return mosaic; 165. } 166. 167. //为了显示图象金字塔,而作的图像垂直拼接 168. CvMat* MosaicVertical( CvMat* im1, CvMat* im2 ) 169. { 170. int row,col; 171. CvMat *mosaic = cvCreateMat(im1->rows+im2->rows,max(im1->cols,im2->cols), CV_32FC1); 172. #define Mosaic(ROW,COL) ((float*)(mosaic->data.fl + mosaic->step/sizeof(flo at)*(ROW)))[(COL)] 173. #define Im11Mat(ROW,COL) ((float *)(im1->data.fl + im1->step/sizeof(float) *(ROW)))[(COL)] 174. #define Im22Mat(ROW,COL) ((float *)(im2->data.fl + im2->step/sizeof(float) *(ROW)))[(COL)] 175. cvZero(mosaic); 176. 177. /* Copy images into mosaic1. */ 178. for ( row = 0; row < im1->rows; row++) 179. for ( col = 0; col < im1->cols; col++) 180. Mosaic(row,col)= Im11Mat(row,col) ; 181. for ( row = 0; row < im2->rows; row++) 182. for ( col = 0; col < im2->cols; col++) 183. Mosaic((row+im1->rows),col)=Im22Mat(row,col) ; 184. 185. return mosaic; 186. } ok,为了版述清晰,再贴一下上文所述的主函数(注,上文已贴出,此是为了版述清晰, 重复造轮): view plaincopy to clipboardprint? 1. int main( void ) 2. { 3. //声明当前帧 IplImage 指针 4. IplImage* src = NULL; 5. IplImage* image1 = NULL; 6. IplImage* grey_im1 = NULL; 7. IplImage* DoubleSizeImage = NULL; 8. 9. IplImage* mosaic1 = NULL; 10. IplImage* mosaic2 = NULL; 11. 12. CvMat* mosaicHorizen1 = NULL; 13. CvMat* mosaicHorizen2 = NULL; 14. CvMat* mosaicVertical1 = NULL; 15. 16. CvMat* image1Mat = NULL; 17. CvMat* tempMat=NULL; 18. 19. ImageOctaves *Gaussianpyr; 20. int rows,cols; 21. 22. #define Im1Mat(ROW,COL) ((float *)(image1Mat->data.fl + image1Mat->step/size of(float) *(ROW)))[(COL)] 23. 24. //灰度图象像素的数据结构 25. #define Im1B(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3] 26. #define Im1G(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3+1] 27. #define Im1R(ROW,COL) ((uchar*)(image1->imageData + image1->widthStep*(ROW)) )[(COL)*3+2] 28. 29. storage = cvCreateMemStorage(0); 30. 31. //读取图片 32. if( (src = cvLoadImage( "street1.jpg", 1)) == 0 ) // test1.jpg einstein.pg m back1.bmp 33. return -1; 34. 35. //为图像分配内存 36. image1 = cvCreateImage(cvSize(src->width, src->height), IPL_DEPTH_8U,3); 37. grey_im1 = cvCreateImage(cvSize(src->width, src->height), IPL_DEPTH_8U,1); 38. DoubleSizeImage = cvCreateImage(cvSize(2*(src->width), 2*(src->height)), I PL_DEPTH_8U,3); 39. 40. //为图像阵列分配内存,假设两幅图像的大小相同,tempMat 跟随 image1 的大小 41. image1Mat = cvCreateMat(src->height, src->width, CV_32FC1); 42. //转化成单通道图像再处理 43. cvCvtColor(src, grey_im1, CV_BGR2GRAY); 44. //转换进入 Mat 数据结构,图像操作使用的是浮点型操作 45. cvConvert(grey_im1, image1Mat); 46. 47. double t = (double)cvGetTickCount(); 48. //图像归一化 49. cvConvertScale( image1Mat, image1Mat, 1.0/255, 0 ); 50. 51. int dim = min(image1Mat->rows, image1Mat->cols); 52. numoctaves = (int) (log((double) dim) / log(2.0)) - 2; //金字塔阶数 53. numoctaves = min(numoctaves, MAXOCTAVES); 54. 55. //SIFT 算法第一步,预滤波除噪声,建立金字塔底层 56. tempMat = ScaleInitImage(image1Mat) ; 57. //SIFT 算法第二步,建立 Guassian 金字塔和 DOG 金字塔 58. Gaussianpyr = BuildGaussianOctaves(tempMat) ; 59. 60. t = (double)cvGetTickCount() - t; 61. printf( "the time of build Gaussian pyramid and DOG pyramid is %.1f\n", t/( cvGetTickFrequency()*1000.) ); 62. 63. #define ImLevels(OCTAVE,LEVEL,ROW,COL) ((float *)(Gaussianpyr[(OCTAVE)].Octa ve[(LEVEL)].Level->data.fl + Gaussianpyr[(OCTAVE)].Octave[(LEVEL)].Level->st ep/sizeof(float) *(ROW)))[(COL)] 64. //显示高斯金字塔 65. for (int i=0; iwidth, mosaicVertical1->hei ght), IPL_DEPTH_8U,1); 95. cvConvertScale( mosaicVertical1, mosaicVertical1, 255.0, 0 ); 96. cvConvertScaleAbs( mosaicVertical1, mosaic1, 1, 0 ); 97. 98. // cvSaveImage("GaussianPyramid of me.jpg",mosaic1); 99. cvNamedWindow("mosaic1",1); 100. cvShowImage("mosaic1", mosaic1); 101. cvWaitKey(0); 102. cvDestroyWindow("mosaic1"); 103. //显示 DOG 金字塔 104. for ( i=0; iwidth, mosaicVertical1->he ight), IPL_DEPTH_8U,1); 140. cvConvertScale( mosaicVertical1, mosaicVertical1, 255.0/(max_val-min_val), 0 ); 141. cvConvertScaleAbs( mosaicVertical1, mosaic2, 1, 0 ); 142. 143. // cvSaveImage("DOGPyramid of me.jpg",mosaic2); 144. cvNamedWindow("mosaic1",1); 145. cvShowImage("mosaic1", mosaic2); 146. cvWaitKey(0); 147. 148. //SIFT 算法第三步:特征点位置检测,最后确定特征点的位置 149. int keycount=DetectKeypoint(numoctaves, Gaussianpyr); 150. printf("the keypoints number are %d ;\n", keycount); 151. cvCopy(src,image1,NULL); 152. DisplayKeypointLocation( image1 ,Gaussianpyr); 153. 154. cvPyrUp( image1, DoubleSizeImage, CV_GAUSSIAN_5x5 ); 155. cvNamedWindow("image1",1); 156. cvShowImage("image1", DoubleSizeImage); 157. cvWaitKey(0); 158. cvDestroyWindow("image1"); 159. 160. //SIFT 算法第四步:计算高斯图像的梯度方向和幅值,计算各个特征点的主方向 161. ComputeGrad_DirecandMag(numoctaves, Gaussianpyr); 162. AssignTheMainOrientation( numoctaves, Gaussianpyr,mag_pyr,grad_pyr); 163. cvCopy(src,image1,NULL); 164. DisplayOrientation ( image1, Gaussianpyr); 165. 166. // cvPyrUp( image1, DoubleSizeImage, CV_GAUSSIAN_5x5 ); 167. cvNamedWindow("image1",1); 168. // cvResizeWindow("image1", 2*(image1->width), 2*(image1->height) ); 169. cvShowImage("image1", image1); 170. cvWaitKey(0); 171. 172. //SIFT 算法第五步:抽取各个特征点处的特征描述字 173. ExtractFeatureDescriptors( numoctaves, Gaussianpyr); 174. cvWaitKey(0); 175. 176. //销毁窗口 177. cvDestroyWindow("image1"); 178. cvDestroyWindow("mosaic1"); 179. //释放图像 180. cvReleaseImage(&image1); 181. cvReleaseImage(&grey_im1); 182. cvReleaseImage(&mosaic1); 183. cvReleaseImage(&mosaic2); 184. return 0; 185. } 最后,再看一下,运行效果(图中美女为老乡+朋友,何姐 08 年照): 完。 版权声明: 1、本文版权归本人和 CSDN 共同拥有。转载,请注明出处及作者本人。 2、版权侵犯者,无论任何人,任何网站,1、永久追踪,2、永久谴责,3、永久追究法律 责任的权利。 July、二零一一年三月十二日声明。 十、从头到尾彻底理解傅里叶变换算法、上 作者:July、dznlong 二零一一年二月二十日 推荐阅读:The Scientist and Engineer's Guide to Digital Signal Processing,By Steven W. Smith, Ph.D。此书地址:http://www.dspguide.com/pdfbook.htm。 博主说明: I、本文中阐述离散傅里叶变换方法,是根据此书:The Scientist and Engineer's Guide to Digital Signal Processing , By Steven W. Smith, Ph.D. 而翻译而成的,此书地址: http://www.dspguide.com/pdfbook.htm。 II、同时,有相当一部分内容编辑整理自 dznlong 的博客,也贴出其博客地址,向原创 的作者表示致敬:http://blog.csdn.net/dznlong 。这年头,真正静下心写来原创文章的人,很 少了。 -------------------------------------------------------------------------------------------------------------------------------- 从头到尾彻底理解傅里叶变换算法、上 前言 第一部分、 DFT 第一章、傅立叶变换的由来 第二章、实数形式离散傅立叶变换(Real DFT) 从头到尾彻底理解傅里叶变换算法、下 第三章、复数 第四章、复数形式离散傅立叶变换 前言: ―关于傅立叶变换,无论是书本还是在网上可以很容易找到关于傅立叶变换的描述,但是大 都是些故弄玄虚的文章,太过抽象,尽是一些让人看了就望而生畏的公式的罗列,让人很难 能够从感性上得到理解‖---dznlong, 那么,到底什么是傅里叶变换算法列?傅里叶变换所涉及到的公式具体有多复杂列? 傅里叶变换(Fourier transform)是一种线性的积分变换。因其基本思想首先由法国学者 傅里叶系统地提出,所以以其名字来命名以示纪念。 哦,傅里叶变换原来就是一种变换而已,只是这种变换是从时间转换为频率的变化。这 下,你就知道了,傅里叶就是一种变换,一种什么变换列?就是一种从时间到频率的变化或 其相互转化。 ok,咱们再来总体了解下傅里叶变换,让各位对其有个总体大概的印象,也顺便看看 傅里叶变换所涉及到的公式,究竟有多复杂: 以下就是傅里叶变换的 4 种变体(摘自,维基百科) 连续傅里叶变换 一般情况下,若―傅里叶变换‖一词不加任何限定语,则指的是―连续傅里叶变换‖。连续傅 里叶变换将平方可积的函数 f(t)表示成复指数函数的积分或级数形式。 这是将频率域的函数 F(ω)表示为时间域的函数 f(t)的积分形式。 连续傅里叶变换的逆变换 (inverse Fourier transform)为: 即将时间域的函数 f(t)表示为频率域的函数 F(ω)的积分。 一般可称函数 f(t)为原函数,而称函数 F(ω)为傅里叶变换的像函数,原函数和像函数构 成一个傅里叶变换对(transform pair)。 除此之外,还有其它型式的变换对,以下两种型式亦常被使用。在通信或是信号处理方面, 常以 来代换,而形成新的变换对: 或者是因系数重分配而得到新的变换对: 一种对连续傅里叶变换的推广称为分数傅里叶变换(Fractional Fourier Transform)。 分数傅里叶变换(fractional Fourier transform,FRFT)指的就是傅里叶变换(Fourier transform,FT)的广义化。 分数傅里叶变换的物理意义即做傅里叶变换 a 次,其中 a 不一定要为整数;而做了 分数傅里叶变换之后,信号或输入函数便会出现在介于时域(time domain)与频域 (frequency domain)之间的分数域(fractional domain)。 当 f(t)为偶函数(或奇函数)时,其正弦(或余弦)分量将消亡,而可以称这时的变 换为余弦变换(cosine transform)或正弦变换(sine transform). 另一个值得注意的性质是,当 f(t)为纯实函数时,F(−ω) = F*(ω)成立. 傅里叶级数 连续形式的傅里叶变换其实是傅里叶级数 (Fourier series)的推广,因为积分其实是一 种极限形式的求和算子而已。对于周期函数,其傅里叶级数是存在的: 其中 Fn 为复幅度。对于实值函数,函数的傅里叶级数可以写成: 其中 an 和 bn 是实频率分量的幅度。 离散时域傅里叶变换 离散傅里叶变换是离散时间傅里叶变换(DTFT)的特例(有时作为后者的近似)。DTFT 在时域上离散,在频域上则是周期的。DTFT 可以被看作是傅里叶级数的逆变换。 离散傅里叶变换 离散傅里叶变换(DFT),是连续傅里叶变换在时域和频域上都离散的形式,将时域信 号的采样变换为在离散时间傅里叶变换(DTFT)频域的采样。在形式上,变换两端(时域 和频域上)的序列是有限长的,而实际上这两组序列都应当被认为是离散周期信号的主值序 列。即使对有限长的离散信号作 DFT,也应当将其看作经过周期延拓成为周期信号再作变 换。在实际应用中通常采用快速傅里叶变换以高效计算 DFT。 为了在科学计算和数字信号处理等领域使用计算机进行傅里叶变换,必须将函数 xn 定义 在离散点而非连续域内,且须满足有限性或周期性条件。这种情况下,使用离散傅里叶变换 (DFT),将函数 xn 表示为下面的求和形式: 其中 Xk 是傅里叶幅度。直接使用这个公式计算的计算复杂度为 O(n*n),而快速傅 里叶变换(FFT)可以将复杂度改进为 O(n*lgn)。(后面会具体阐述 FFT 是如何将复杂 度降为 O(n*lgn)的。)计算复杂度的降低以及数字电路计算能力的发展使得 DFT 成为 在信号处理领域十分实用且重要的方法。 下面,比较下上述傅立叶变换的 4 种变体, 如上,容易发现:函数在时(频)域的离散对应于其像函数在频(时)域的周期性。反 之连续则意味着在对应域的信号的非周期性。也就是说,时间上的离散性对应着频率上的周 期性。同时,注意,离散时间傅里叶变换,时间离散,频率不离散,它在频域依然是连续的。 如果,读到此,你不甚明白,大没关系,不必纠结于以上 4 种变体,继续往下看,你自 会豁然开朗。(有什么问题,也恳请提出,或者批评指正) ok, 本文,接下来,由傅里叶变换入手,后重点阐述离散傅里叶变换、快速傅里叶算法, 到最后彻底实现 FFT 算法,全篇力求通俗易懂、阅读顺畅,教你从头到尾彻底理解傅里叶 变换算法。由于傅里叶变换,也称傅立叶变换,下文所称为傅立叶变换,同一个变换,不同 叫法,读者不必感到奇怪。 第一部分、DFT 第一章、傅立叶变换的由来 要理解傅立叶变换,先得知道傅立叶变换是怎么变换的,当然,也需要一定的高等数 学基础,最基本的是级数变换,其中傅立叶级数变换是傅立叶变换的基础公式。 一、傅立叶变换的提出 傅立叶是一位法国数学家和物理学家,原名是 Jean Baptiste Joseph Fourier(1768-1830), Fourier 于 1807 年在法国科学学会上发表了一篇论文,论文里描 述运用正弦曲线来描述温度分布,论文里有个在当时具有争议性的决断:任何连续周期信号 都可以由一组适当的正弦曲线组合而成。 当时审查这个论文拉格朗日坚决反对此论文的发表,而后在近 50 年的时间里,拉格朗 日坚持认为傅立叶的方法无法表示带有棱角的信号,如在方波中出现非连续变化斜率。直到 拉格朗日死后 15 年这个论文才被发表出来。 谁是对的呢?拉格朗日是对的:正弦曲线无法组合成一个带有棱角的信号。但是,我 们可以用正弦曲线来非常逼近地表示它,逼近到两种表示方法不存在能量差别,基于此,傅 立叶是对的。 为什么我们要用正弦曲线来代替原来的曲线呢?如我们也还可以用方波或三角波来代 替呀,分解信号的方法是无穷多的,但分解信号的目的是为了更加简单地处理原来的信号。 用正余弦来表示原信号会更加简单,因为正余弦拥有原信号所不具有的性质:正弦曲 线保真度。一个正余弦曲线信号输入后,输出的仍是正余弦曲线,只有幅度和相位可能发生 变化,但是频率和波的形状仍是一样的。且只有正余弦曲线才拥有这样的性质,正因如此我 们才不用方波或三角波来表示。 二、傅立叶变换分类 根据原信号的不同类型,我们可以把傅立叶变换分为四种类别: 1、非周期性连续信号 傅立叶变换(Fourier Transform) 2、周期性连续信号 傅立叶级数(Fourier Series) 3、非周期性离散信号 离散时域傅立叶变换(Discrete Time Fourier Transform) 4、周期性离散信号 离散傅立叶变换(Discrete Fourier Transform) 下图是四种原信号图例(从上到下,依次是 FT,FS,DTFT,DFT): 这四种傅立叶变换都是针对正无穷大和负无穷大的信号,即信号的的长度是无穷大的, 我们知道这对于计算机处理来说是不可能的,那么有没有针对长度有限的傅立叶变换呢? 没有。因为正余弦波被定义成从负无穷小到正无穷大,我们无法把一个长度无限的信号组合 成长度有限的信号。 面对这种困难,方法是:把长度有限的信号表示成长度无限的信号。如,可以把信号 无限地从左右进行延伸,延伸的部分用零来表示,这样,这个信号就可以被看成是非周期性 离散信号,我们可以用到离散时域傅立叶变换(DTFT)的方法。也可以把信号用复制的方 法进行延伸,这样信号就变成了周期性离散信号,这时我们就可以用离散傅立叶变换方法 (DFT)进行变换。本章我们要讲的是离散信号,对于连续信号我们不作讨论,因为计算 机只能处理离散的数值信号,我们的最终目的是运用计算机来处理信号的。 但是对于非周期性的信号,我们需要用无穷多不同频率的正弦曲线来表示,这对于计 算机来说是不可能实现的。所以对于离散信号的变换只有离散傅立叶变换(DFT)才能被 适用,对于计算机来说只有离散的和有限长度的数据才能被处理,对于其它的变换类型只有 在数学演算中才能用到,在计算机面前我们只能用 DFT 方法,后面我们要理解的也正是 DFT 方法。 这里要理解的是我们使用周期性的信号目的是为了能够用数学方法来解决问题,至于 考虑周期性信号是从哪里得到或怎样得到是无意义的。 每种傅立叶变换都分成实数和复数两种方法,对于实数方法是最好理解的,但是复数 方法就相对复杂许多了,需要懂得有关复数的理论知识,不过,如果理解了实数离散傅立叶 变换(real DFT),再去理解复数傅立叶变换就更容易了,所以我们先把复数的傅立叶变换 放到一边去,先来理解实数傅立叶变换,在后面我们会先讲讲关于复数的基本理论,然后在 理解了实数傅立叶变换的基础上再来理解复数傅立叶变换。 还有,这里我们所要说的变换(transform)虽然是数学意义上的变换,但跟函数变换是 不同的,函数变换是符合一一映射准则的,对于离散数字信号处理(DSP),有许多的变 换:傅立叶变换、拉普拉斯变换、Z 变换、希尔伯特变换、离散余弦变换等,这些都扩展了 函数变换的定义,允许输入和输出有多种的值,简单地说变换就是把一堆的数据变成另一堆 的数据的方法。 三、一个关于实数离散傅立叶变换(Real DFT)的例子 先来看一个变换实例,下图是一个原始信号图像: 这个信号的长度是 16,于是可以把这个信号分解 9 个余弦波和 9 个正弦波(一个长度 为 N 的信号可以分解成 N/2+1 个正余弦信号,这是为什么呢?结合下面的 18 个正余弦图, 我想从计算机处理精度上就不难理解,一个长度为 N 的信号,最多只能有 N/2+1 个不同频 率,再多的频率就超过了计算机所能所处理的精度范围),如下图: 9 个余弦信号: 9 个正弦信号: 把以上所有信号相加即可得到原始信号,至于是怎么分别变换出9种不同频率信号的, 我们先不急,先看看对于以上的变换结果,在程序中又是该怎么表示的,我们可以看看下面 这个示例图: 上图中左边表示时域中的信号,右边是频域信号表示方法, 从左向右,-->,表示正向转换(Forward DFT),从右向左,<--,表示逆向转换(Inverse DFT),用小写 x[]表示信号在每个时间点上的幅度值数组, 用大写 X[]表示每种频率的副度 值数组(即时间 x-->频率 X), 因为有 N/2+1 种频率,所以该数组长度为 N/2+1, X[]数组又分两种,一种是表示余弦波的不同频率幅度值:Re X[], 另一种是表示正弦波的不同频率幅度值:Im X[], Re 是实数(Real)的意思,Im 是虚数(Imagine)的意思,采用复数的表示方法把正余 弦波组合起来进行表示,但这里我们不考虑复数的其它作用,只记住是一种组合方法而已, 目的是为了便于表达(在后面我们会知道,复数形式的傅立叶变换长度是 N,而不是 N/2+1)。 如此,再回过头去,看上面的正余弦各 9 种频率的变化,相信,问题不大了。 第二章、实数形式离散傅立叶变换(Real DFT) 上一章,我们看到了一个实数形式离散傅立叶变换的例子,通过这个例子能够让我们先 对傅立叶变换有一个较为形象的感性认识,现在就让我们来看看实数形式离散傅立叶变换的 正向和逆向是怎么进行变换的。在此,我们先来看一下频率的多种表示方法。 一、 频域中关于频率的四种表示方法 1、序号表示方法,根据时域中信号的样本数取 0 ~ N/2,用这种方法在程序中使用起来可 以更直接地取得每种频率的幅度值,因为频率值跟数组的序号是一一对应的: X[k],取值范 围是 0 ~ N/2; 2、分数表示方法,根据时域中信号的样本数的比例值取 0 ~ 0.5: X[ƒ],ƒ = k/N,取值 范围是 0 ~ 1/2; 3、用弧度值来表示,把 ƒ 乘以一个 2π 得到一个弧度值,这种表示方法叫做自然频率 (natural frequency):X[ω],ω = 2πƒ = 2πk/N,取值范围是 0 ~ π; 4、以赫兹(Hz)为单位来表示,这个一般是应用于一些特殊应用,如取样率为 10 kHz 表示 每秒有 10,000 个样本数:取值范围是 0 到取样率的一半。 二、 DFT 基本函数 ck[i] = cos(2πki/N) sk[i] = sin(2πki/N) 其中 k 表示每个正余弦波的频率,如为 2 表示在 0 到 N 长度中存在两个完整的周期, 10 即有 10 个周期,如下图: 上图中至于每个波的振幅(amplitude)值(Re X[k],Im X[k])是怎么算出来的,这个是 DFT 的核心,也是最难理解的部分,我们先来看看如何把分解出来的正余弦波合成原始信 号(Inverse DFT)。 三、 合成运算方法(Real Inverse DFT) DFT 合成等式(合成原始时间信号,频率-->时间,逆向变换): 如果有学过傅立叶级数,对这个等式就会有似曾相识的感觉,不错!这个等式跟傅立叶级数 是非常相似的: 当然,差别是肯定是存在的,因为这两个等式是在两个不同条件下运用的,至于怎么 证明 DFT 合成公式,这个我想需要非常强的高等数学理论知识了,这是研究数学的人的工 作,对于普通应用者就不需要如此的追根究底了,但是傅立叶级数是好理解的,我们起码可 以从傅立叶级数公式中看出 DFT 合成公式的合理性。 _ _ DFT合成等式中的Im X[k]和Re X[k]跟之前提到的Im X[k]和Re X[k]是不一样的, 下面是转换方法(关于此公式的解释,见下文): 但 k 等于 0 和 N/2 时,实数部分的计算要用下面的等式: 上面四个式中的 N 是时域中点的总数,k 是从 0 到 N/2 的序号。 为什么要这样进行转换呢?这个可以从频谱密度(spectral density)得到理解,如下图 就是个频谱图: 这是一个频谱图,横坐标表示频率大小,纵坐标表示振幅大小,原始信号长度为 N(这 里是 32),经 DFT 转换后得到的 17 个频率的频谱,频谱密度表示每单位带宽中为多大 的振幅,那么带宽是怎么计算出来的呢?看上图,除了头尾两个,其余点的所占的宽度是 2/N,这个宽度便是每个点的带宽,头尾两个点的带宽是 1/N,而 Im X[k]和 Re X[k]表示 的是频谱密度,即每一个单位带宽的振幅大小,但 表示 2/N(或 1/N) 带宽的振幅大小,所以 分别应当是 Im X[k]和 Re X[k]的 2/N(或 1/N)。 频谱密度就象物理中物质密度,原始信号中的每一个点就象是一个混合物,这个混合物 是由不同密度的物质组成的,混合物中含有的每种物质的质量是一样的,除了最大和最小两 个密度的物质外,这样我们只要把每种物质的密度加起来就可以得到该混合物的密度了,又 该混合物的质量是单位质量,所以得到的密度值跟该混合物的质量值是一样的。 至于为什么虚数部分是负数,这是为了跟复数 DFT 保持一致,这个我们将在后面会知 道这是数学计算上的需要(Im X[k]在计算时就已经加上了一个负号(稍后,由下文,便可 知), 再加上负号,结果便是正的,等于没有变化)。 如果已经得到了 DFT 结果,这时要进行逆转换,即合成原始信号,则可按如下步骤进 行转换: 1、先根据上面四个式子计算得出 的值; 2、再根据 DFT 合成等式得到原始信号数据。 下面是用 BASIC 语言来实现的转换源代码: 100 ‗DFT 逆转换方法 110 ‗/XX[]数组存储计算结果(时域中的原始信号) 120 ‗/REX[]数组存储频域中的实数分量,IMX[]为虚分量 130 ‗ 140 DIM XX[511] 150 DIM REX[256] 160 DIM IMX[256] 170 ‗ 180 PI = 3.14159265 190 N% = 512 200 ‗ 210 GOSUB XXXX ‗转到子函数去获取 REX[]和 IMX[]数据 220 ‗ 230 ‗ 240 ‗ 250 FOR K% = 0 TO 256 260 REX[K%] = REX[K%] / (N%/2) 270 IMX[K%] = -IMX[K%] / (N%/2) 280 NEXT k% 290 ‗ 300 REX[0] = REX[0] / N 310 REX[256] = REX[256] / N 320 ‗ 330 ‗ 初始化 XX[]数组 340 FOR I% = 0 TO 511 350 XX[I%] = 0 360 NEXT I% 370 ‗ 380 ‗ 390 ‗ 400 ‗ 410 ‗ 420 FOR K% =0 TO 256 430 FOR I%=0 TO 511 440 ‗ 450 XX[I%] = XX[I%] + REX[K%] * COS(2 * PI * K% * I% / N%) 460 XX[I%] = XX[I%] + IMX[K%] * SIN(2 * PI * K% * I% / N%) 470 ‗ 480 NEXT I% 490 NEXT K% 500 ‗ 510 END 上面代码中 420 至 490 换成如下形式也许更好理解,但结果都是一样的: 420 FOR I% =0 TO 511 430 FOR K%=0 TO 256 440 ‗ 450 XX[I%] = XX[I%] + REX[K%] * COS(2 * PI * K% * I% / N%) 460 XX[I%] = XX[I%] + IMX[K%] * SIN(2 * PI * K% * I% / N%) 470 ‗ 480 NEXT I% 490 NEXT K% 四、 分解运算方法(DFT) 有三种完全不同的方法进行 DFT:一种方法是通过联立方程进行求解, 从代数的角度 看,要从 N 个已知值求 N 个未知值,需要 N 个联立方程,且 N 个联立方程必须是线性独立 的,但这是这种方法计算量非常的大且极其复杂,所以很少被采用;第二种方法是利用信号 的相关性(correlation)进行计算,这个是我们后面将要介绍的方法;第三种方法是快速 傅立叶变换(FFT),这是一个非常具有创造性和革命性的的方法,因为它大大提高了运算 速度,使得傅立叶变换能够在计算机中被广泛应用,但这种算法是根据复数形式的傅立叶变 换来实现的,它把 N 个点的信号分解成长度为 N 的频域,这个跟我们现在所进行的实域 DFT 变换不一样,而且这种方法也较难理解,这里我们先不去理解,等先理解了复数 DFT 后, 再来看一下 FFT。有一点很重要,那就是这三种方法所得的变换结果是一样的,经过实践证 明,当频域长度为 32 时,利用相关性方法进行计算效率最好,否则 FFT 算法效率较高。现 在就让我们来看一下相关性算法。 利用第一种方法、信号的相关性(correlation)可以从噪声背景中检测出已知的信号,我们 也可以利用这个方法检测信号波中是否含有某个频率的信号波:把一个待检测信号波乘以另 一个信号波,得到一个新的信号波,再把这个新的信号波所有的点进行相加,从相加的结果 就可以判断出这两个信号的相似程度。如下图: 上面 a 和 b 两个图是待检测信号波,图 a 很明显可以看出是个 3 个周期的正弦信号波, 图 b 的信号波则看不出是否含有正弦或余弦信号,图 c 和 d 都是个 3 个周期的正弦信号波, 图 e 和 f 分别是 a、b 两图跟 c、d 两图相乘后的结果,图 e 所有点的平均值是 0.5,说明 信号 a 含有振幅为 1 的正弦信号 c,但图 f 所有点的平均值是 0,则说明信号 b 不含有信号 d。这个就是通过信号相关性来检测是否含有某个信号的方法。 第二种方法:相应地,我也可以通过把输入信号和每一种频率的正余弦信号进行相乘 (关联操作),从而得到原始信号与每种频率的关联程度(即总和大小),这个结果便是我 们所要的傅立叶变换结果,下面两个等式便是我们所要的计算方法: 第二个式子中加了个负号,是为了保持复数形式的一致,前面我们知道在计算 时又加了个负号,所以这只是个形式的问题,并没有实际意义,你也可以把负号去掉,并在 计算 时也不加负号。 这里有一点必须明白一个正交的概念:两个函数相乘,如果结果中的每个点的总和为 0, 则可认为这两个函数为正交函数。要确保关联性算法是正确的,则必须使得跟原始信号相乘 的信号的函数形式是正交的,我们知道所有的正弦或余弦函数是正交的,这一点我们可以通 过简单的高数知识就可以证明它,所以我们可以通过关联的方法把原始信号分离出正余弦信 号。当然,其它的正交函数也是存在的,如:方波、三角波等形式的脉冲信号,所以原始信 号也可被分解成这些信号,但这只是说可以这样做,却是没有用的。 下面是实域傅立叶变换的 BASIC 语言代码: 到此为止,我们对傅立叶变换便有了感性的认识了吧。但要记住,这只是在实域上的 离散傅立叶变换,其中虽然也用到了复数的形式,但那只是个替代的形式,并无实际意义, 现实中一般使用的是复数形式的离散傅立叶变换,且快速傅立叶变换是根据复数离散傅立叶 变换来设计算法的,在后面我们先来复习一下有关复数的内容,然后再在理解实域离散傅立 叶变换的基础上来理解复数形式的离散傅立叶变换。 更多见下文:十、从头到尾彻底理解傅里叶变换算法、下(July、dznlong) 本人 July 对本博客所有任何文章、内容和资料享有版权。 转载务必注明作者本人及出处,并通知本人。二零一一年二月二十一日。 十、从头到尾彻底理解傅里叶变换算法、下 作者:July、dznlong 二零一一年二月二十二日 推荐阅读:The Scientist and Engineer's Guide to Digital Signal Processing, By Steven W. Smith, Ph.D。此书地址:http://www.dspguide.com/pdfbook.htm。 --------------------------------------------------------------------------------------- 从头到尾彻底理解傅里叶变换算法、上 前言 第一部分、 DFT 第一章、傅立叶变换的由来 第二章、实数形式离散傅立叶变换(Real DFT) 从头到尾彻底理解傅里叶变换算法、下 第三章、复数 第四章、复数形式离散傅立叶变换 前期回顾,在上一篇:十、从头到尾彻底理解傅里叶变换算法、上里,我们讲了傅立 叶变换的由来、和实数形式离散傅立叶变换(Real DFT)俩个问题, 本文接上文,着重讲下复数、和复数形式离散傅立叶变换等俩个问题。 第三章、复数 复数扩展了我们一般所能理解的数的概念,复数包含了实数和虚数两部分,利用复数 的形式可以把由两个变量表示的表达式变成由一个变量(复变量)来表达,使得处理起来更加 自然和方便。 我们知道傅立叶变换的结果是由两部分组成的,使用复数形式可以缩短变换表达式, 使得我们可以单独处理一个变量(这个在后面的描述中我们就可以更加确切地知道),而且 快速傅立叶变换正是基于复数形式的,所以几乎所有描述的傅立叶变换形式都是复数的形式。 但是复数的概念超过了我们日常生活中所能理解的概念,要理解复数是较难的,所以 我们在理解复数傅立叶变换之前,先来专门复习一下有关复数的知识,这对后面的理解非常 重要。 一、 复数的提出 在此,先让我们看一个物理实验:把一个球从某点向上抛出,然后根据初速度和时间来 计算球所在高度,这个方法可以根据下面的式子计算得出: 其中 h 表示高度,g 表示重力加速度(9.8m/s2),v 表示初速度,t 表示时间。现在反 过来,假如知道了高度,要求计算到这个高度所需要的时间,这时我们又可以通过下式来计 算: (多谢 JERRY_PRI 提出: 1、根据公式 h=-(gt2/2)+Vt(gt 后面的 2 表示 t 的平方),我们可以讨论最终情况, 也就是说小球运动到最高点时,v=gt,所以,可以得到 t=sqt(2h/g) 且在您给的公式中,根号下为 1-(2h)/g,化成分数形式为(g-2h)/g,g 和 h 不能直接做加 减运算。 2、g 是重力加速度,单位是 m/s2,h 的单位是 m,他们两个相减的话在物理上没有意 义,而且使用您给的那个公式反向回去的话推出的是 h=-(gt2/2)+gt 啊(gt 后面的 2 表 示 t 的平方)。 3、直接推到可以得出 t=v/g±sqt((v2-2hg)/g2)(v 和 g 后面的 2 都表示平方),那 么也就是说当 v2<2hg 时会产生复数,但是如果从实际的 v2 是不可能小于 2hg 的,所以 我感觉复数不能从实际出发去推到,只能从抽象的角度说明一下。 经过计算我们可以知道,当高度是 3 米时,有两个时间点到达该高度:球向上运动时 的时间是 0.38 秒,球向下运动时的时间是 1.62 秒。但是如果高度等于 10 时,结果又是 什么呢?根据上面的式子可以发现存在对负数进行开平方运算,我们知道这肯定是不现实的。 第一次使用这个不一般的式子的人是意大利数学家 Girolamo Cardano (1501-1576),两个世纪后,德国伟大数学家 Carl Friedrich Gause(1777-1855)提 出了复数的概念,为后来的应用铺平了道路,他对复数进行这样表示:复数由实数(real) 和虚数(imaginary)两部分组成,虚数中的根号负 1 用 i 来表示(在这里我们用 j 来表示, 因为 i 在电力学中表示电流的意思)。 我们可以把横坐标表示成实数,纵坐标表示成虚数,则坐标中的每个点的向量就可以 用复数来表示,如下图: 上图中的 ABC 三个向量可以表示成如下的式子: A = 2 + 6j B = -4 – 1.5j C = 3 – 7j 这样子来表达方便之处在于运用一个符号就能把两个原来难以联系起来的数组合起来 了,不方便的是我们要分辨哪个是实数和哪个是虚数,我们一般是用 Re( )和 Im( )来表示 实数和虚数两部分,如: Re A = 2 Im A = 6 Re B = -4 Im B = -1.5 Re C = 3 Im C = -7 复数之间也可以进行加减乘除运算: 这里有个特殊的地方是 j2 等于-1,上面第四个式子的计算方法是把分子和分母同时乘 以 c – dj,这样就可消去分母中的 j 了。 复数也符合代数运算中的交换律、结合律、分配律: A B = B A (A + B) + C = A + (B + C) A(B + C) = AB + AC 二、 复数的极坐标表示形式 前面提到的是运用直角坐标来表示复数,其实更为普遍应用的是极坐标的表示方法, 如下图: 上图中的 M 即是数量积(magnitude),表示从原点到坐标点的距离,θ 是相位角(phase angle),表示从 X 轴正方向到某个向量的夹角,下面四个式子是计算方法: 我们还可以通过下面的式子进行极坐标到直角坐标的转换: a + jb = M (cosθ + j sinθ) 上面这个等式中左边是直角坐标表达式,右边是极坐标表达式。 还有一个更为重要的等式——欧拉等式(欧拉,瑞士的著名数学家,Leonhard Euler, 1707-1783): ejx = cos x + j sin x 这个等式可以从下面的级数变换中得到证明: 上面中右边的两个式子分别是 cos(x)和 sin(x)的泰勒(Taylor)级数。 这样子我们又可以把复数的表达式表示成指数的形式了: a + jb = M ejθ (这便是复数的两个表达式) 指数形式是数字信号处理中数学方法的支柱,也许是因为用指数形式进行复数的乘除 运算极为简单的缘故吧: 三、复数是数学分析中的一个工具 为什么要使用复数呢?其实它只是个工具而已,就如钉子和锤子的关系,复数就象那 锤子,作为一种使用的工具。我们把要解决的问题表达成复数的形式(因为有些问题用复数 的形式进行运算更加方便),然后对复数进行运算,最后再转换回来得到我们所需要的结果。 有两种方法使用复数,一种是用复数进行简单的替换,如前面所说的向量表达式方法 和前一节中我们所讨论的实域 DFT,另一种是更高级的方法:数学等价(mathematical equivalence),复数形式的傅立叶变换用的便是数学等价的方法,但在这里我们先不讨论 这种方法,这里我们先来看一下用复数进行替换中的问题。 用复数进行替换的基本思想是:把所要分析的物理问题转换成复数的形式,其中只是 简单地添加一个复数的符号 j,当返回到原来的物理问题时,则只是把符号 j 去掉就可以了。 有一点要明白的是并不是所有问题都可以用复数来表示,必须看用复数进行分析是否 适用,有个例子可以看出用复数来替换原来问题的表达方式明显是谬误的:假设一箱的苹果 是 5 美元,一箱的桔子是 10 美元,于是我们把它表示成 5 + 10j,有一个星期你买了 6 箱苹果和 2 箱桔子,我们又把它表示成 6 + 2j,最后计算总共花的钱是(5 + 10j)(6 + 2j) = 10 + 70j,结果是买苹果花了 10 美元的,买桔子花了 70 美元,这样的结果明显是错 了,所以复数的形式不适合运用于对这种问题的解决。 四、用复数来表示正余弦函数表达式 对于象 M cos (ωt + φ)和 A cos(ωt ) + B sin(ωt )表达式,用复数来表示,可以变 得非常简洁,对于直角坐标形式可以按如下形式进行转换: 上式中余弦幅值 A 经变换生成 a,正弦幅值 B的相反数经变换生成 b:A <=> a,B<=> -b,但要注意的是,这不是个等式,只是个替换形式而已。 对于极坐标形式可以按如下形式进行转换: 上式中,M <=> M,θ<=>φ。 这里虚数部分采用负数的形式主要是为了跟复数傅立叶变换表达式保持一致,对于这种 替换的方法来表示正余弦,符号的变换没有什么好处,但替换时总会被改变掉符号以跟更高 级的等价变换保持形式上的一致。 在离散信号处理中,运用复数形式来表示正余弦波是个常用的技术,这是因为利用复 数进行各种运算得到的结果跟原来的正余弦运算结果是一致的,但是,我们要小心使用复数 操作,如加、减、乘、除,有些操作是不能用的,如两个正弦信号相加,采用复数形式进行 相加,得到的结果跟替换前的直接相加的结果是一样的,但是如果两个正弦信号相乘,则采 用复数形式来相乘结果是不一样的。幸运的是,我们已严格定义了正余弦复数形式的运算操 作条件: 1、参加运算的所有正余弦的频率必须是一样的; 2、运算操作必须是线性的,如两个正弦信号可以进行相加减,但不能进行乘除,象信号的 放大、衰减、高低通滤波等系统都是线性的,象平方、缩短、取限等则不是线性的。要记住 的是卷积和傅立叶分析也只有线性操作才可以进行。 下图是一个相量变换(我们把正弦或余弦波变成复数的形式称为相量变换,Phasor transform)的例子,一个连续信号波经过一个线性处理系统生成另一个信号波,从计算过 程我们可以看出采用复数的形式使得计算变化十分的简洁: 在第二章中我们描述的实数形式傅立叶变换也是一种替换形式的复数变换,但要注意的 是那还不是复数傅立叶变换,只是一种代替方式而已。下一章、即,第四章,我们就会知道 复数傅立叶变换是一种更高级的变换,而不是这种简单的替换形式。 第四章、复数形式离散傅立叶变换 复数形式的离散傅立叶变换非常巧妙地运用了复数的方法,使得傅立叶变换变换更加 自然和简洁,它并不是只是简单地运用替换的方法来运用复数,而是完全从复数的角度来分 析问题,这一点跟实数 DFT 是完全不一样的。 一、 把正余弦函数表示成复数的形式 通过欧拉等式可以把正余弦函数表示成复数的形式: cos( x ) = 1/2 e j(-x) + 1/2 ejx sin( x ) = j (1/2 e j(-x) - 1/2 ejx) 从这个等式可以看出,如果把正余弦函数表示成复数后,它们变成了由正负频率组成 的正余弦波,相反地,一个由正负频率组成的正余弦波,可以通过复数的形式来表示。 我们知道,在实数傅立叶变换中,它的频谱是 0 ~ π(0 ~ N/2),但无法表示-π~ 0 的 频谱,可以预见,如果把正余弦表示成复数形式,则能够把负频率包含进来。 二、 把变换前后的变量都看成复数的形式 复数形式傅立叶变换把原始信号 x[n]当成是一个用复数来表示的信号,其中实数部分 表示原始信号值,虚数部分为 0,变换结果 X[k]也是个复数的形式,但这里的虚数部分是 有值的。 在这里要用复数的观点来看原始信号,是理解复数形式傅立叶变换的关键(如果有学 过复变函数则可能更好理解,即把 x[n]看成是一个复数变量,然后象对待实数那样对这个 复数变量进行相同的变换)。 三、 对复数进行相关性算法(正向傅立叶变换) 从实数傅立叶变换中可以知道,我们可以通过原始信号乘以一个正交函数形式的信号, 然后进行求总和,最后就能得到这个原始信号所包含的正交函数信号的分量。 现在我们的原始信号变成了复数,我们要得到的当然是复数的信号分量,我们是不是可 以把它乘以一个复数形式的正交函数呢?答案是肯定的,正余弦函数都是正交函数,变成如 下形式的复数后,仍旧还是正交函数(这个从正交函数的定义可以很容易得到证明): cos x + j sin x, cos x – j sin x,…… 这里我们采用上面的第二个式子进行相关性求和,为什么用第二个式子呢?,我们在后 面会知道,正弦函数在虚数中变换后得到的是负的正弦函数,这里我们再加上一个负号,使 得最后的得到的是正的正弦波,根据这个于是我们很容易就可以得到了复数形式的 DFT 正 向变换等式: 这个式子很容易可以得到欧拉变换式子: 其实我们是为了表达上的方便才用到欧拉变换式,在解决问题时我们还是较多地用到正 余弦表达式。 对于上面的等式,我们要清楚如下几个方面(也是区别于实数 DFT 的地方): 1、X[k]、x[n]都是复数,但 x[n]的虚数部分都是由 0 组成的,实数部分表示原始信号; 2、k 的取值范围是 0 ~ N-1 (也可以表达成 0 ~ 2π),其中 0 ~ N/2(或 0 ~ π)是正频 部分,N/2 ~ N-1(π~ 2π)是负频部分,由于正余弦函数的对称性,所以我们把 –π~ 0 表示成 π~ 2π,这是出于计算上方便的考虑。 3、其中的 j 是一个不可分离的组成部分,就象一个等式中的变量一样,不能随便去掉,去 掉之后意义就完全不一样了,但我们知道在实数 DFT 中,j 只是个符号而已,把 j 去掉,整 个等式的意义不变; 4、下图是个连续信号的频谱,但离散频谱也是与此类似的,所以不影响我们对问题的分析: 上面的频谱图把负频率放到了左边,是为了迎合我们的思维习惯,但在实际实 现中我们一般是把它移到正的频谱后面的。 从上图可以看出,时域中的正余弦波(用来组成原始信号的正余弦波)在复数 DFT 的 频谱中被分成了正、负频率的两个组成部分,基于此等式中前面的比例系数是 1/N(或 1/2π), 而不是 2/N,这是因为现在把频谱延伸到了 2π,但把正负两个频率相加即又得到了 2/N,又 还原到了实数 DFT 的形式,这个在后面的描述中可以更清楚地看到。 由于复数 DFT 生成的是一个完整的频谱,原始信号中的每一个点都是由正、负两个频 率组合而成的,所以频谱中每一个点的带宽是一样的,都是 1/N,相对实数 DFT,两端带 宽比其它点的带宽少了一半;复数 DFT 的频谱特征具有周期性:-N/2 ~ 0 与 N/2 ~ N-1 是一样的,实域频谱呈偶对称性(表示余弦波频谱),虚域频谱呈奇对称性(表示正弦波频 谱)。 四、 逆向傅立叶变换 假设我们已经得到了复数形式的频谱 X[k],现在要把它还原到复数形式的原始信号 x[n],当然应该是把 X[k]乘以一个复数,然后再进行求和,最后得到原始信号 x[n],这个 跟 X[k]相乘的复数首先让我们想到的应该是上面进行相关性计算的复数: cos(2πkn/N) – j si(2πkn/N), 但其中的负号其实是为了使得进行逆向傅立叶变换时把正弦函数变为正的符号,因为虚 数 j 的运算特殊性,使得原来应该是正的正弦函数变为了负的正弦函数(我们从后面的推导 会看到这一点),所以这里的负号只是为了纠正符号的作用,在进行逆向 DFT 时,我们可 以把负号去掉,于是我们便得到了这样的逆向 DFT 变换等式: x[n] = X[k] (cos(2πkn/N) + j sin(2πkn/N)) 我们现在来分析这个式子,会发现这个式其实跟实数傅立叶变换是可以得到一样结果的。 我们先把 X[k]变换一下: X[k] = Re X[k] + j Im X[k] 这样我们就可以对 x[n]再次进行变换,如: x[n] = (Re X[k] + j Im X[k]) (cos(2πkn/N) + j sin(2πkn/N)) = ( Re X[k] cos(2πkn/N) + j Im X[k] cos(2πkn/N) +j Re X[k] sin(2πkn/N) - Im X[k] sin(2πkn/N) ) = ( Re X[k] (cos(2πkn/N) + j sin(2πkn/N)) + ---------------------(1) Im X[k] ( - sin(2πkn/N) + j cos(2πkn/N))) ---------------(2) 这时我们就把原来的等式分成了两个部分,第一个部分是跟实域中的频谱相乘,第二 个部分是跟虚域中的频谱相乘,根据频谱图我们可以知道,Re X[k]是个偶对称的变量,Im X[k]是个奇对称的变量,即 Re X[k] = Re X[- k] Im X[k] = - Im X[-k] 但 k 的范围是 0 ~ N-1,0~N/2 表示正频率,N/2~N-1 表示负频率,为了表达方便 我们把 N/2~N-1 用-k 来表示,这样在从 0 到 N-1 的求和过程中对于(1)和(2)式分别有 N/2 对的 k 和-k 的和,对于(1)式有: Re X[k] (cos(2πkn/N) + j sin(2πkn/N)) + Re X[- k] (cos( - 2πkn/N) + j sin( -2πkn/N)) 根据偶对称性和三角函数的性质,把上式化简得到: Re X[k] (cos(2πkn/N) + j sin(2πkn/N)) + Re X[ k] (cos( 2πkn/N) - j sin( 2πkn/N)) 这个式子最后的结果是: 2 Re X[ k] cos(2πkn/N)。 再考虑到求 Re X[ k]等式中有个比例系数 1/N,把 1/N 乘以 2,这样的结果不就是跟 实数 DFT 中的式子一样了吗? 对于(2)式,用同样的方法,我们也可以得到这样的结果: -2 Im X[k] sin(2πkn/N) 注意上式前面多了个负符号,这是由于虚数变换的特殊性造成的,当然我们肯定不能 把负符号的正弦函数跟余弦来相加,还好,我们前面是用 cos(2πkn/N) – j sin(2πkn/N) 进行相关性计算,得到的 Im X[k]中有个负的符号,这样最后的结果中正弦函数就没有负 的符号了,这就是为什么在进行相关性计算时虚数部分要用到负符号的原因(我觉得这也许 是复数形式 DFT 美中不足的地方,让人有一种拼凑的感觉)。 从上面的分析中可以看出,实数傅立叶变换跟复数傅立叶变换,在进行逆变换时得到 的结果是一样的,只不过是殊途同归吧。本文完。(July、dznlong) 本人 July 对本博客所有任何文章、内容和资料享有版权。 转载务必注明作者本人及出处,并通知本人。二零一一年二月二十二日。 十一、从头到尾彻底解析 Hash 表算法 作者:July、wuliming、pkuoliver 出处:http://blog.csdn.net/v_JULY_v。 说明:本文分为三部分内容, 第一部分为一道百度面试题 Top K 算法的详解;第二部分为关于 Hash 表算法的详细 阐述;第三部分为打造一个最快的 Hash 表算法。 --------------------------------------------------------------------------------------- 第一部分:Top K 算法详解 问题描述 百度面试题: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串 的长度为 1-255 字节。 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是 1 千万,但如果 除去重复后,不超过 3 百万个。一个查询串的重复度越高,说明查询它的用户越多,也就 是越热门。),请你统计最热门的 10 个查询串,要求使用的内存不能超过 1G。 必备知识: 什么是哈希表? 哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问 的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的 速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 哈希表的做法其实很简单,就是把 Key 通过一个固定的算法函数既所谓的哈希函数转 换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将 value 存储在以该数字为下标的数组空间里。 而当使用哈希表进行查询的时候,就是再次使用哈希函数将 key 转换为对应的数组下 标,并定位到该空间获取 value,如此一来,就可以充分利用到数组的定位性能进行数据定 位(文章第二、三部分,会针对 Hash 表详细阐述)。 问题解析: 要统计最热门查询,首先就是要统计每个 Query 出现的次数,然后根据统计结果,找 出 Top 10。所以我们可以基于这个思路分两步来设计该算法。 即,此问题的解决分为以下俩个步骤: 第一步:Query 统计 Query 统计有以下俩个方法,可供选择: 1、直接排序法 首先我们最先想到的的算法就是排序了,首先对这个日志里面的所有 Query 都进行排 序,然后再遍历排好序的 Query,统计每个 Query 出现的次数了。 但是题目中有明确要求,那就是内存不能超过 1G,一千万条记录,每条记录是 225Byte, 很显然要占据 2.55G 内存,这个条件就不满足要求了。 让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候, 我们可以采用外排序的方法来进行排序,这里我们可以采用归并排序,因为归并排序有一个 比较好的时间复杂度 O(NlgN)。 排完序之后我们再对已经有序的 Query 文件进行遍历,统计每个 Query 出现的次数, 再次写入文件中。 综合分析一下,排序的时间复杂度是 O(NlgN),而遍历的时间复杂度是 O(N),因此 该算法的总体时间复杂度就是 O(N+NlgN)=O(NlgN)。 2、Hash Table 法 在第 1 个方法中,我们采用了排序的办法来统计每个 Query 出现的次数,时间复杂度 是 NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢? 题目中说明了,虽然有一千万个 Query,但是由于重复度比较高,因此事实上只有 300 万的 Query,每个 Query255Byte,因此我们可以考虑把他们都放进内存中去,而现在只 是需要一个合适的数据结构,在这里,Hash Table 绝对是我们优先的选择,因为 Hash Table 的查询速度非常的快,几乎是 O(1)的时间复杂度。 那么,我们的算法就有了:维护一个 Key 为 Query 字串,Value 为该 Query 出现次 数的 HashTable,每次读取一个 Query,如果该字串不在 Table 中,那么加入该字串,并 且将 Value 值设为 1;如果该字串在 Table 中,那么将该字串的计数加一即可。最终我们 在 O(N)的时间复杂度内完成了对该海量数据的处理。 本方法相比算法 1:在时间复杂度上提高了一个数量级,为 O(N),但不仅仅是时间 复杂度上的优化,该方法只需要 IO 数据文件一次,而算法 1 的 IO 次数较多的,因此该算 法 2 比算法 1 在工程上有更好的可操作性。 第二步:找出 Top 10 算法一:普通排序 我想对于排序算法大家都已经不陌生了,这里不在赘述,我们要注意的是排序算法的 时间复杂度是 NlgN,在本题目中,三百万条记录,用 1G 内存是可以存下的。 算法二:部分排序 题目要求是求出 Top 10,因此我们没有必要对所有的 Query 都进行排序,我们只需 要维护一个 10 个大小的数组,初始化放入 10 个 Query,按照每个 Query 的统计次数由 大到小排序,然后遍历这 300 万条记录,每读一条记录就和数组最后一个 Query 对比,如 果小于这个 Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前的 Query。 最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10 了。 不难分析出,这样,算法的最坏时间复杂度是 N*K, 其中 K 是指 top 多少。 算法三:堆 在算法二中,我们已经将时间复杂度由 NlogN 优化到 NK,不得不说这是一个比较大的 改进了,可是有没有更好的办法呢? 分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是 K,因为要把元素 插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,一 次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了 logK,可是, 随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法二有 了改进。 基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构 呢?回答是肯定的,那就是堆。 借助堆结构,我们可以在 log 量级的时间内查找和调整/移动。因此到这里,我们的算法 可以改进为这样,维护一个 K(该题目中是 10)大小的小根堆,然后遍历 300 万的 Query, 分别和根元素进行对比。 思想与上述算法二一致,只是算法在算法三,我们采用了最小堆这种数据结构代替数组, 把查找目标元素的时间复杂度有 O(K)降到了 O(logK)。 那么这样,采用堆数据结构,算法三,最终的时间复杂度就降到了 N‘logK,和算法二 相比,又有了比较大的改进。 总结: 至此,算法就完全结束了,经过上述第一步、先用 Hash 表统计每个 Query 出现的次 数,O(N);然后第二步、采用堆数据结构找出 Top 10,N*O(logK)。所以,我们最 终的时间复杂度是:O(N) + N'*O(logK)。(N 为 1000 万,N‘为 300 万)。如 果各位有什么更好的算法,欢迎留言评论。第一部分,完。 第二部分、Hash 表 算法的详细解析 什么是 Hash Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做 预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种 转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散 列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的 消息压缩到某一固定长度的消息摘要的函数。 HASH 主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的 128 位的编码,这些编码值叫做 HASH 值. 也可以说,hash 就是找到一种数据内容和数据 存放地址之间的映射关系。 数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删 除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构? 答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释 的是最常用的一种方法——拉链法,我们可以理解为―链表的数组‖,如图: 左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个 链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去, 也是根据这些特征,找到正确的链表,再从链表中找出这个元素。 元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较 常用的: 1,除法散列法 最直观的一种,上图使用的就是这种散列法,公式: index = value % 16 学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。 2,平方散列法 求 index 是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的 CPU 来说,估计 我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式: index = (value * value) >> 28 (右移,除以 2^28。记法:左移变大,是乘。 右移变小,是除。) 如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元 素的值算出来的 index 都是 0——非常失败。也许你还有个问题,value 如果很大,value * value 不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获 取相乘结果,而是为了获取 index。 3,斐波那契(Fibonacci)散列法 平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿 value 本身当作乘数呢?答案是肯定的。 1,对于 16 位整数而言,这个乘数是 40503 2,对于 32 位整数而言,这个乘数是 2654435769 3,对于 64 位整数而言,这个乘数是 11400714819323198485 这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄 金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610 , 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。 对我们常见的 32 位整数而言,公式: index = (value * 2654435769) >> 28 如果用这种斐波那契散列法的话,那上面的图就变成这样了: 很明显,用斐波那契散列法调整之后要比原来的取摸散列法好很多。 适用范围 快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。 基本原理及要点 hash 函数选择,针对字符串,整数,排列,具体相应的 hash 方法。 碰撞处理,一种是 open hashing,也称为拉链法;另一种就是 closed hashing,也称开 地址法,opened addressing。 扩展 d-left hashing 中的 d 是多个的意思,我们先简化这个问题,看一看 2-left hashing。 2-left hashing 指的是将一个哈希表分成长度相等的两半,分别叫做 T1 和 T2,给 T1 和 T2 分别配备一个哈希函数,h1 和 h2。在存储一个新的 key 时,同 时用两个哈希函数进 行计算,得出两个地址 h1[key]和 h2[key]。这时需要检查 T1 中的 h1[key]位置和 T2 中的 h2[key]位置,哪一个 位置已经存储的(有碰撞的)key 比较多,然后将新 key 存储 在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个 key,就把新 key 存储在左边的 T1 子表中,2-left 也由此而来。在查找一个 key 时,必须进行两次 hash, 同时查找两个位置。 问题实例(海量数据处理) 我们知道 hash 表在海量数据处理中有着广泛的应用,下面,请看另一道百度面试题: 题目:海量日志数据,提取出某日访问百度次数最多的那个 IP。 方案:IP 的数目还是有限的,最多 2^32 个,所以可以考虑使用 hash 将 ip 直接存入内存, 然后进行统计。 第三部分、最快的 Hash 表算法 接下来,咱们来具体分析一下一个最快的 Hasb 表算法。 我们由一个简单的问题逐步入手:有一个庞大的字符串数组,然后给你一个单独的字 符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?有一个方法最简单, 老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这 样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它 真的能工作,但...也只能如此了。 最合适的算法自然是使用 HashTable(哈希表),先介绍介绍其中的基本知识,所谓 Hash,一般是一个整数,通过某种算法,可以把一个字符串"压缩" 成一个整数。当然,无 论如何,一个 32 位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的 Hash 值相等的可能非常小,下面看看在 MPQ 中的 Hash 算法: 函数一、以下的函数生成一个长度为 0x500(合 10 进制数:1280)的 cryptTable[0x500] void prepareCryptTable() { unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; for( index1 = 0; index1 < 0x100; index1++ ) { for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) { unsigned long temp1, temp2; seed = (seed * 125 + 3) % 0x2AAAAB; temp1 = (seed & 0xFFFF) << 0x10; seed = (seed * 125 + 3) % 0x2AAAAB; temp2 = (seed & 0xFFFF); cryptTable[index2] = ( temp1 | temp2 ); } } } 函数二、以下函数计算 lpszFileName 字符串的 hash 值,其中 dwHashType 为 hash 的类型,在下面的函数三、GetHashTablePos 函数中调用此函数二,其可以取的值为 0、 1、2;该函数返回 lpszFileName 字符串的 hash 值: unsigned long HashString( char *lpszFileName, unsigned long dwHashType ) { unsigned char *key = (unsigned char *)lpszFileName; unsigned long seed1 = 0x7FED7FED; unsigned long seed2 = 0xEEEEEEEE; int ch; while( *key != 0 ) { ch = toupper(*key++); seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; } return seed1; } Blizzard 的这个算法是非常高效的,被称为"One-Way Hash"( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。举个例子,字符串 "unitneutralacritter.grp"通过这个算法得到的结果是 0xA26067F3。 是不是把第一个算法改进一下,改成逐个比较字符串的 Hash 值就可以了呢,答案是, 远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,例如 1024,每一个 Hash 值通过取模运算 (mod) 对应到数组中的一个位置,这样,只要比较 这个字符串的哈希值对应的位置有没有被占用,就可以得到最后的结果了,想想这是什么速 度?是的,是最快的 O(1),现在仔细看看这个算法吧: typedef struct { int nHashA; int nHashB; char bExists; ...... } SOMESTRUCTRUE; 一种可能的结构体定义? 函数三、下述函数为在 Hash 表中查找是否存在目标字符串,有则返回要查找字符串的 Hash 值,无则,return -1. int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable ) //lpszString 要在 Hash 表中查找的字符串,lpTable 为存储字符串 Hash 值的 Hash 表。 { int nHash = HashString(lpszString); //调用上述函数二,返回要查找字符串 lpszString 的 Hash 值。 int nHashPos = nHash % nTableSize; if ( lpTable[nHashPos].bExists && !strcmp( lpTable[nHashPos].pString, lpszString ) ) { //如果找到的 Hash 值在表中存在,且要查找的字符串与表中对应位置的字符串相同, return nHashPos; //则返回上述调用函数二后,找到的 Hash 值 } else { return -1; } } 看到此,我想大家都在想一个很严重的问题:―如果两个字符串在哈希表中对应的位置 相同怎么办?‖,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我 首先想到的就是用―链表‖,感谢大学里学的数据结构教会了这个百试百灵的法宝,我遇到的 很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的 字符串就 OK 了。事情到此似乎有了完美的结局,如果是把问题独自交给我解决,此时我可 能就要开始定义数据结构然后写代码了。 然而 Blizzard 的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表 中不是用一个哈希值而是用三个哈希值来校验字符串。 MPQ 使用文件名哈希表来跟踪内部的所有文件。但是这个表的格式与正常的哈希表有 一些不同。首先,它没有使用哈希作为下标,把实际的文件名存储在表中用于验证,实际上 它根本就没有存储文件名。而是使用了 3 种不同的哈希:一个用于哈希表的下标,两个用 于验证。这两个验证哈希替代了实际文件名。 当然了,这样仍然会出现 2 个不同的文件名哈希到 3 个同样的哈希。但是这种情况发 生的概率平均是:1:18889465931478580854784,这个概率对于任何人来说应该都是 足够小的。现在再回到数据结构上,Blizzard 使用的哈希表没有使用链表,而采用"顺延" 的方式来解决问题,看看这个算法: 函数四、lpszString 为要在 hash 表中查找的字符串;lpTable 为存储字符串 hash 值的 hash 表;nTableSize 为 hash 表的长度: int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize ) { const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; int nHash = HashString( lpszString, HASH_OFFSET ); int nHashA = HashString( lpszString, HASH_A ); int nHashB = HashString( lpszString, HASH_B ); int nHashStart = nHash % nTableSize; int nHashPos = nHashStart; while ( lpTable[nHashPos].bExists ) { /*如果仅仅是判断在该表中时候存在这个字符串,就比较这两个 hash 值就可以了,不 用对 *结构体中的字符串进行比较。这样会加快运行的速度?减少 hash 表占用的空间?这 种 *方法一般应用在什么场合?*/ if ( lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB ) { return nHashPos; } else { nHashPos = (nHashPos + 1) % nTableSize; } if (nHashPos == nHashStart) break; } return -1; } 上述程序解释: 1.计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验) 2. 察看哈希表中的这个位置 3. 哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回-1。 4. 如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返 回其 Hash 值。 5. 移到下一个位置,如果已经移到了表的末尾,则反绕到表的开始位置起继续查询 6. 看看是不是又回到了原来的位置,如果是,则返回没找到 7. 回到 3 ok,这就是本文中所说的最快的 Hash 表算法。什么?不够快?:D。欢迎,各位批评指正。 --------------------------------------------------------------------------------------- 补充 1、一个简单的 hash 函数: /*key 为一个字符串,nTableLength 为哈希表的长度 *该函数得到的 hash 值分布比较均匀*/ unsigned long getHashIndex( const char *key, int nTableLength ) { unsigned long nHash = 0; while (*key) { nHash = (nHash<<5) + nHash + *key++; } return ( nHash % nTableLength ); } 补充 2、一个完整测试程序: 哈希表的数组是定长的,如果太大,则浪费,如果太小,体现不出效率。合适的数组 大小是哈希表的性能的关键。哈希表的尺寸最好是一个质数。当然,根据不同的数据量,会 有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈 希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的 2 倍时,我们 就需要扩大哈希表尺寸,一般是扩大一倍。 下面是哈希表尺寸大小的可能取值: 17, 37, 79, 163, 331, 673, 1361, 2729, 5471, 10949, 21911, 43853, 87719, 175447, 350899, 701819, 1403641, 2807303, 5614657, 11229331, 22458671, 44917381, 89834777, 179669557, 359339171, 718678369, 1437356741, 2147483647 以下为该程序的完整源码,已在 linux 下测试通过: view plaincopy to clipboardprint? 1. #include 2. #include //多谢 citylove 指正。 3. //crytTable[]里面保存的是 HashString 函数里面将会用到的一些数据,在 prepareCryptTable 4. //函数里面初始化 5. unsigned long cryptTable[0x500]; 6. 7. //以下的函数生成一个长度为 0x500(合 10 进制数:1280)的 cryptTable[0x500] 8. void prepareCryptTable() 9. { 10. unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; 11. 12. for( index1 = 0; index1 < 0x100; index1++ ) 13. { 14. for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) 15. { 16. unsigned long temp1, temp2; 17. 18. seed = (seed * 125 + 3) % 0x2AAAAB; 19. temp1 = (seed & 0xFFFF) << 0x10; 20. 21. seed = (seed * 125 + 3) % 0x2AAAAB; 22. temp2 = (seed & 0xFFFF); 23. 24. cryptTable[index2] = ( temp1 | temp2 ); 25. } 26. } 27. } 28. 29. //以下函数计算 lpszFileName 字符串的 hash 值,其中 dwHashType 为 hash 的类型, 30. //在下面 GetHashTablePos 函数里面调用本函数,其可以取的值为 0、1、2;该函数 31. //返回 lpszFileName 字符串的 hash 值; 32. unsigned long HashString( char *lpszFileName, unsigned long dwHashType ) 33. { 34. unsigned char *key = (unsigned char *)lpszFileName; 35. unsigned long seed1 = 0x7FED7FED; 36. unsigned long seed2 = 0xEEEEEEEE; 37. int ch; 38. 39. while( *key != 0 ) 40. { 41. ch = toupper(*key++); 42. 43. seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); 44. seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; 45. } 46. return seed1; 47. } 48. 49. //在 main 中测试 argv[1]的三个 hash 值: 50. //./hash "arr\units.dat" 51. //./hash "unit\neutral\acritter.grp" 52. int main( int argc, char **argv ) 53. { 54. unsigned long ulHashValue; 55. int i = 0; 56. 57. if ( argc != 2 ) 58. { 59. printf("please input two arguments\n"); 60. return -1; 61. } 62. 63. /*初始化数组:crytTable[0x500]*/ 64. prepareCryptTable(); 65. 66. /*打印数组 crytTable[0x500]里面的值*/ 67. for ( ; i < 0x500; i++ ) 68. { 69. if ( i % 10 == 0 ) 70. { 71. printf("\n"); 72. } 73. 74. printf("%-12X", cryptTable[i] ); 75. } 76. 77. ulHashValue = HashString( argv[1], 0 ); 78. printf("\n----%X ----\n", ulHashValue ); 79. 80. ulHashValue = HashString( argv[1], 1 ); 81. printf("----%X ----\n", ulHashValue ); 82. 83. ulHashValue = HashString( argv[1], 2 ); 84. printf("----%X ----\n", ulHashValue ); 85. 86. return 0; 87. } 致谢: 1、http://blog.redfox66.com/。 2、http://blog.csdn.net/wuliming_sc/。完。 版权所有,法律保护。转载,请以链接形式,注明出处。 十二、快速排序算法所有版本的 c/c++实现 作者:July、二零一一年三月二十日。 出处:http://blog.csdn.net/v_JULY_v。 -------------------------------------------------------------------------------------- 前言: 相信,经过本人之前写的前俩篇关于快速排序算法的文章:第一篇、一、快速排序算 法,及第二篇、一之续、快速排序算法的深入分析,各位,已经对快速排序算法有了足够 的了解与认识。但仅仅停留在对一个算法的认识层次上,显然是不够的,即便你认识的有多 透彻与深入。最好是,编程实现它。 而网上,快速排序的各种写法层次不清,缺乏统一、整体的阐述与实现,即,没有个 一锤定音,如此,我便打算自己去实现它了。 于是,今花了一个上午,把快速排序算法的各种版本全部都写程序一一实现了一下。 包括网上有的,没的,算法导论上的,国内教材上通用的,随机化的,三数取中分割法的, 递归的,非递归的,所有版本都用 c/c++全部写了个遍。 鉴于时间仓促下,一个人考虑问题总有不周之处,以及水平有限等等,不正之处,还 望各位不吝赐教。不过,以下,所有全部 c/c++源码,都经本人一一调试,若有任何问题, 恳请指正。 ok,本文主要分为以下几部分内容: 第一部分、递归版 一、算法导论上的单向扫描版本 二、国内教材双向扫描版 2.1、Hoare 版本 2.2、Hoare 的几个变形版本 三、随机化版本 四、三数取中分割法 第二部分、非递归版 好的,请一一细看。 第一部分、快速排序的递归版本 一、算法导论上的版本 在我写的第二篇文章中,我们已经知道: ―再到后来,N.Lomuto 又提出了一种新的版本,此版本....,即优化了 PARTITION 程序, 它现在写在了 算法导论 一书上‖: 快速排序算法的关键是 PARTITION 过程,它对 A[p..r]进行就地重排: PARTITION(A, p, r) 1 x ← A[r] //以最后一个元素,A[r]为主元 2 i ← p - 1 3 for j ← p to r - 1 //注,j 从 p 指向的是 r-1,不是 r。 4 do if A[j] ≤ x 5 then i ← i + 1 6 exchange A[i] <-> A[j] 7 exchange A[i + 1] <-> A[r] //最后,交换主元 8 return i + 1 然后,对整个数组进行递归排序: QUICKSORT(A, p, r) 1 if p < r 2 then q ← PARTITION(A, p, r) //关键 3 QUICKSORT(A, p, q - 1) 4 QUICKSORT(A, q + 1, r) 根据上述伪代码,我们不难写出以下的 c/c++程序: 首先是,PARTITION 过程: int partition(int data[],int lo,int hi) { int key=data[hi]; //以最后一个元素,data[hi]为主元 int i=lo-1; for(int j=lo;j A[j] //7 exchange A[i + 1] <-> A[r] 去掉此最后的步骤 8 return i //返回 i,不再返回 i+1. 望读者思考,后把结果在评论里告知我。 我这里简单论述下:上述请读者思考版本,只是代码做了以下三处修改而已:1、 j 从 p->r;2、去掉最后的交换步骤;3、返回 i。首先,无论是我的版本,还是算法导论 上的原装版本,都是准确无误的,且我都已经编写程序测试通过了。但,其实这俩种写法, 思路是完全一致的。 为什么这么说列?请具体看以下的请读者思考版本, int partition(int data[],int lo,int hi) //请读者思考 { int key=data[hi]; //以最后一个元素,data[hi]为主元 int i=lo-1; for(int j=lo;j<=hi;j++) //.... { if(data[j]<=key) //如果让 j 从 lo 指向 hi,那么当 j 指到 hi 时,是一定会有 A[j]<=x 的 { i=i+1; swap(&data[i],&data[j]); } } //swap(&data[i+1],&data[hi]); //事实是,应该加上这句,直接交换,即可。 return i; // } 我们知道当 j 最后指到了 r 之后,是一定会有 A[j]<=x 的(即=),所以这个 if 判断 就有点多余,没有意义。所以应该如算法导论上的版本那般,最后直接交换 swap(&data[i+1],&data[hi]); 即可,返回 i+1。所以,总体说来,算法导论上的版本 那样写,比请读者思考版本更规范,更合乎情理。ok,请接着往下阅读。 当然,上述 partition 过程中,也可以去掉 swap 函数的调用,直接写在分割函数里: int partition(int data[],int lo,int hi) { int i,j,t; int key = data[hi]; //还是以最后一个元素作为哨兵,即主元元素 i = lo-1; for (j =lo;j<=hi;j++) if(data[j] A[j] 11 else return j 同样,根据以上伪代码,不难写出以下的 c/c++代码: view plaincopy to clipboardprint? 1. //此处原来的代码有几点错误,后听从了 Joshua 的建议,现修改如下: 2. int partition(int data[],int lo,int hi) //。 3. { 4. int key=data[lo]; 5. int l=lo-1; 6. int h=hi+1; 7. for(;;) 8. { 9. do{ 10. h--; 11. }while(data[h]>key); 12. 13. do{ 14. l++; 15. }while(data[l]key) //不能加 ―=‖ h--; while(data[l]| |<-h 交换 l 和 h 单元后重新又回到: 2 3 4 5 6 2 l->| |<-h 而第一个程序不存在这种情况,因为它总是在 l 和 h 调整后比较,也就是 l 终究会大于等于 h。 相信,你已经看出来了,上述的第一个程序中 partition 过程的返回值 h 并不是枢纽 元的位置,但是仍然保证了 A[p..j] <= A[j+1...q]。 这种方法在效率上与以下将要介绍的 Hoare 的几个变形版本差别甚微,只不过是上述代 码相对更为紧凑点而已。 2.2、Hoare 的几个变形版本 ok,可能,你对上述的最初的霍尔排序 partition 过程,理解比较费力,没关系,我再 写几种变形,相信,你立马就能了解此双向扫描是怎么一回事了。 int partition(int data[],int lo,int hi) //双向扫描。 { int key=data[lo]; //以第一个元素为主元 int l=lo; int h=hi; while(lhi) return; while(i!=j) { while(data[j]>=temp && j>i) j--; if(j>i) data[i++]=data[j]; while(data[i]<=temp && j>i) i++; if(j>i) data[j--]=data[i]; } data[i]=temp; //2.temp。同上,返回的是枢纽元素,即主元元素。 QuickSort(data,lo,i-1); //递归左边 QuickSort(data,i+1,hi); //递归右边 } 或者,如下: view plaincopy to clipboardprint? 1. void quicksort (int[] a, int lo, int hi) 2. { 3. // lo is the lower index, hi is the upper index 4. // of the region of array a that is to be sorted 5. int i=lo, j=hi, h; 6. 7. // comparison element x 8. int x=a[(lo+hi)/2]; 9. 10. // partition 11. do 12. { 13. while (a[i]x) j--; 15. if (i<=j) 16. { 17. h=a[i]; a[i]=a[j]; a[j]=h; 18. i++; j--; 19. } 20. } while (i<=j); 21. 22. // recursion 23. if (lo=key&&ipivot) --r; if(l>=r) break; temp = data[l]; data[l] = data[r]; data[r] = temp; ++l; --r; } if(l==r) l++; if(lo pivot) j--; if (i <= j) { tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; i++; j--; } } } 上述演示过程,如下图所示(取中间元素为主元,第一趟排序): 三、快速排序的随机化版本 以下是完整测试程序,由于给的注释够详尽了,就再做多余的解释了: //交换两个元素值,咱们换一种方式,采取引用―&‖ void swap(int& a , int& b) { int temp = a; a = b; b = temp; } //返回属于[lo,hi)的随机整数 int rand(int lo,int hi) { int size = hi-lo+1; return lo+ rand()%size; } //分割,换一种方式,采取指针 a 指向数组中第一个元素 int RandPartition(int* data, int lo , int hi) { //普通的分割方法和随机化分割方法的区别就在于下面三行 swap(data[rand(lo,hi)], data[lo]); int key = data[lo]; int i = lo; for(int j=lo+1; j<=hi; j++) { if(data[j]<=key) { i = i+1; swap(data[i], data[j]); } } swap(data[i],data[lo]); return i; } //逐步分割排序 void RandQuickSortMid(int* data, int lo, int hi) { if(lo int RandPartition(T data[],int lo,int hi) { T v=data[lo]; while(lo=v) hi--; data[lo]=data[hi]; while(lo void QuickSort(T data[],int lo,int hi) { stack st; int key; do{ while(lo 2. 00040 class quick_sort_range: private no_assign { 3. 00041 4. 00042 inline size_t median_of_three(const RandomAccessIterator &array, s ize_t l, size_t m, size_t r) const { 5. 00043 return comp(array[l], array[m]) ? ( comp(array[m], array[r]) ? m : ( comp( array[l], array[r]) ? r : l ) ) 6. 00044 : ( comp(array[r], array[m]) ? m : ( comp( array[r], array[l] ) ? r : l ) ); 7. 00045 } 8. 00046 9. 00047 inline size_t pseudo_median_of_nine( const RandomAccessIterator &a rray, const quick_sort_range &range ) const { 10. 00048 size_t offset = range.size/8u; 11. 00049 return median_of_three(array, 12. 00050 median_of_three(array, 0, offset, offse t*2), 13. 00051 median_of_three(array, offset*3, offset *4, offset*5), 14. 00052 median_of_three(array, offset*6, offset *7, range.size - 1) ); 15. 00053 16. 00054 } 17. 00055 18. 00056 public: 19. 00057 20. 00058 static const size_t grainsize = 500; 21. 00059 const Compare ∁ 22. 00060 RandomAccessIterator begin; 23. 00061 size_t size; 24. 00062 25. 00063 quick_sort_range( RandomAccessIterator begin_, size_t size_, const Compare &comp_ ) : 26. 00064 comp(comp_), begin(begin_), size(size_) {} 27. 00065 28. 00066 bool empty() const {return size==0;} 29. 00067 bool is_divisible() const {return size>=grainsize;} 30. 00068 31. 00069 quick_sort_range( quick_sort_range& range, split ) : comp(range.co mp) { 32. 00070 RandomAccessIterator array = range.begin; 33. 00071 RandomAccessIterator key0 = range.begin; 34. 00072 size_t m = pseudo_median_of_nine(array, range); 35. 00073 if (m) std::swap ( array[0], array[m] ); 36. 00074 37. 00075 size_t i=0; 38. 00076 size_t j=range.size; 39. 00077 // Partition interval [i+1,j-1] with key *key0. 40. 00078 for(;;) { 41. 00079 __TBB_ASSERT( i 3. void _isort( InPos posBegin_, InPos posEnd_, ValueType* ) 4. { 5. /*************************************************************************** * 6. * 伪代码如下: 7. * for i = [1, n) 8. * t = x 9. * for( j = i; j > 0 && x[j-1] > t; j-- ) 10. * x[j] = x[j-1] 11. * x[j] = x[j-1] 12. *************************************************************************** */ 13. if( posBegin_ == posEnd_ ) 14. { 15. return; 16. } 17. 18. /// 循环迭代,将每个元素插入到合适的位置 19. for( InPos pos = posBegin_; pos != posEnd_; ++pos ) 20. { 21. ValueType Val = *pos; 22. InPos posPrev = pos; 23. InPos pos2 = pos; 24. /// 当元素比前一个元素大时,交换 25. for( ;pos2 != posBegin_ && *(--posPrev) > Val ; --pos2 ) 26. { 27. *pos2 = *posPrev; 28. } 29. *pos2 = Val; 30. } 31. } 32. 33. /// 快速排序 1,平均情况下需要 O(nlogn)的时间 34. template< typename InPos > 35. inline void qsort1( InPos posBegin_, InPos posEnd_ ) 36. { 37. /*************************************************************************** * 38. * 伪代码如下: 39. * void qsort(l, n) 40. * if(l >= u) 41. * return; 42. * m = l 43. * for i = [l+1, u] 44. * if( x < x[l] 45. * swap(++m, i) 46. * swap(l, m) 47. * qsort(l, m-1) 48. * qsort(m+1, u) 49. *************************************************************************** */ 50. if( posBegin_ == posEnd_ ) 51. { 52. return; 53. } 54. 55. /// 将比第一个元素小的元素移至前半部 56. InPos pos = posBegin_; 57. InPos posLess = posBegin_; 58. for( ++pos; pos != posEnd_; ++pos ) 59. { 60. if( *pos < *posBegin_ ) 61. { 62. swap( *pos, *(++posLess) ); 63. } 64. } 65. 66. /// 把第一个元素插到两快元素中央 67. swap( *posBegin_, *(posLess) ); 68. 69. /// 对前半部、后半部执行快速排序 70. qsort1(posBegin_, posLess); 71. qsort1(++posLess, posEnd_); 72. }; 73. 74. /// 快速排序 2,原理与 1 基本相同,通过两端同时迭代加快平均速度 75. template 76. void qsort2( InPos posBegin_, InPos posEnd_ ) 77. { 78. if( distance(posBegin_, posEnd_) <= 0 ) 79. { 80. return; 81. } 82. 83. InPos posL = posBegin_; 84. InPos posR = posEnd_; 85. 86. while( true ) 87. { 88. /// 找到不小于第一个元素的数 89. do 90. { 91. ++posL; 92. }while( *posL < *posBegin_ && posL != posEnd_ ); 93. 94. /// 找到不大于第一个元素的数 95. do 96. { 97. --posR; 98. } while ( *posR > *posBegin_ ); 99. 100. /// 两个区域交叉时跳出循环 101. if( distance(posL, posR) <= 0 ) 102. { 103. break; 104. } 105. /// 交换找到的元素 106. swap(*posL, *posR); 107. } 108. 109. /// 将第一个元素换到合适的位置 110. swap(*posBegin_, *posR); 111. /// 对前半部、后半部执行快速排序 2 112. qsort2(posBegin_, posR); 113. qsort2(++posR, posEnd_); 114. } 115. 116. /// 当元素个数小与 g_iSortMax 时使用插入排序,g_iSortMax 是根据 STL 库选取的 117. const int g_iSortMax = 32; 118. /// 该排序算法是快速排序与插入排序的结合 119. template 120. void qsort3( InPos posBegin_, InPos posEnd_ ) 121. { 122. if( distance(posBegin_, posEnd_) <= 0 ) 123. { 124. return; 125. } 126. 127. /// 小与 g_iSortMax 时使用插入排序 128. if( distance(posBegin_, posEnd_) <= g_iSortMax ) 129. { 130. return isort(posBegin_, posEnd_); 131. } 132. 133. /// 大与 g_iSortMax 时使用快速排序 134. InPos posL = posBegin_; 135. InPos posR = posEnd_; 136. 137. while( true ) 138. { 139. do 140. { 141. ++posL; 142. }while( *posL < *posBegin_ && posL != posEnd_ ); 143. 144. do 145. { 146. --posR; 147. } while ( *posR > *posBegin_ ); 148. 149. if( distance(posL, posR) <= 0 ) 150. { 151. break; 152. } 153. swap(*posL, *posR); 154. } 155. swap(*posBegin_, *posR); 156. qsort3(posBegin_, posR); 157. qsort3(++posR, posEnd_); 158. } 版权所有。转载本 BLOG 内任何文章,请以超链接形式注明出处。 否则,一经发现,必定永久谴责+追究法律责任。谢谢,各位。 十三、通过浙大上机复试试题学 SPFA 算法 作者:July、sunbaigui。二零一一年三月二十五日。 出处:http://blog.csdn.net/v_JULY_v。 --------------------------------------------------------------------------------------- 前言: 本人不喜欢写诸如―如何学算法‖此类的文章,一来怕被人认为是自以为是,二来话题太 泛,怕扯得太远,反而不着边际。所以,一直不打算写怎么学习算法此类的文章。 不过,鉴于读者的热心支持与关注,给出以下几点小小的建议,仅供参考: 1、算法,浩如烟海,找到自己感兴趣的那个分支,或那个点来学习,然后,一往无前 的深入探究下去。 2、兴趣第一,一切,由着你的兴趣走,忌浮躁。 3、思维敏捷。给你一道常见的题目,你的头脑中应该立刻能冒出解决这道问题的最适 用的数据结构,以及算法。 4、随兴趣,多刷题。ACM 题。poj,面试题,包括下文将出现的研究生复试上机考试 题,都可以作为你的编程练习题库。 5、多实践,多思考。学任何一个算法,反复研究,反复思考,反复实现。 6、数据结构是一切的基石。不必太过专注于算法,一切算法的实现,原理都是依托数 据结构来实现的。弄懂了一个数据结构,你也就通了一大片算法。 7、学算法,重优化。 8、学习算法的高明之处不在于某个算法运用得有多自如,而在于通晓一个算法的内部 原理,运作机制,及其来龙去脉。 ok,话不再多。希望,对你有用。 接下来,咱们来通过最近几年的浙大研究生复试上机试题,来学习或巩固常用的算法。 浙大研究生复试 2010 年上机试题-最短路径问题 问题描述: 给你 n 个点,m 条无向边,每条边都有长度 d 和花费 p,给你起点 s 终点 t,要求输出 起点到终点的最短距离及其花费,如果最短距离有多条路线,则输出花费最少的。 输入:输入 n,m,点的编号是 1~n,然后是 m 行,每行 4 个数 a,b,d,p,表示 a 和 b 之间有一条边,且其长度为 d,花费为 p。最后一行是两个数 s,t;起点 s,终点 t。n 和 m 为 0 时输入结束。 (1 O(V^2) III、斐波那契堆:O(V*lgV + E) 是的,由上,我们已经看出来了,Dijkstra 算法最快的实现是,采用斐波那契堆作最小 优先队列,算法时间复杂度,可达到 O(V*lgV + E)。 但是列?如果题目有时间上的限制列?vlgv+e 的时间复杂度,能否一定满足要求?我们试 图寻找一种解决此最短路径问题更快的算法。 这个时候,我们想到了 Bellman-Ford 算法:求单源最短路,可以判断有无负权回路(若 有,则不存在最短路),时效性较好,时间复杂度 O(VE)。不仅时效性好于上述的 Dijkstra 算法,还能判断回路中是否有无负权回路。 既然,想到了 Bellman-Ford 算法,那么时间上,是否还能做进一步的突破。对了,我 们中国人自己的算法--SPFA 算法:SPFA 算法,Bellman-Ford 的队列优化,时效性相对 好,时间复杂度 O(kE)。(k<map[1005]; 4. int N,M,S,T; 建个数据结构: view plaincopy to clipboardprint? 1. struct Node 2. { 3. int x,y,z; 4. Node(int a=0,int b=0,int c=0):x(a),y(b),z(c){} 5. }; 以下是关键代码 view plaincopy to clipboardprint? 1. //sunbaigui: 2. //首先将起点弹入队列,用 used 数组标记 i 节点是否在队列中, 3. //然后从队列中弹出节点,判断从这个弹出节点能到达的每个节点的距离是否小于已得到的距 离, 4. //如果是则更新距离,然后将其弹入队列,修改 used 数组。 5. void spfa() 6. { 7. queueq; //构造一个队列 8. q.push(S); 9. memset(used,false,sizeof(used)); 10. int i; 11. for(i=1;i<=N;i++) 12. d[i][0]=d[i][1]=-1; 13. d[S][0]=d[S][1]=0; //初始化 14. while(!q.empty()) 15. { 16. int node=q.front(); 17. q.pop(); 18. used[node]=false; 19. int t,dis,p; 20. for(i=0;id[node][0]+dis) 26. { 27. d[t][0]=d[node][0]+dis; //松弛操作 28. d[t][1]=d[node][1]+p; 29. if(!used[t]) 30. { 31. used[t]=true; 32. q.push(t); //t 点不在当前队列中,放入队尾。 33. } 34. } 35. else if(d[t][0]!=-1&&d[t][0]==d[node][0]+dis) 36. { 37. if(d[t][1]>d[node][1]+p) 38. { 39. d[t][1]=d[node][1]+p; 40. if(!used[t]) 41. { 42. q.push(t); 43. used[t]=true; 44. } 45. } 46. } 47. } 48. 49. } 50. } 主函数测试用例: view plaincopy to clipboardprint? 1. int main() 2. { 3. while(scanf("%d %d",&N,&M)!=EOF) 4. { 5. if(N==0&&M==0)break; 6. int s,t,dis,p; 7. int i; 8. for(i=1;i<=N;i++) 9. map[i].clear(); 10. while(M--) 11. { 12. scanf("%d %d %d %d",&s,&t,&dis,&p); 13. map[s].push_back(Node(t,dis,p)); 14. map[t].push_back(Node(s,dis,p)); 15. } 16. scanf("%d %d",&S,&T); 17. spfa(); 18. printf("%d %d\n",d[T][0],d[T][1]); 19. } 20. return 0; 21. } 运行结果,是: 最后,总结一下(Lunatic Princess): (1)对于稀疏图,当然是 SPFA 的天下,不论是单源问题还是 APSP 问题,SPFA 的效 率都是最高的,写起来也比 Dijkstra 简单。对于无向图的 APSP 问题还可以加入优化使效 率提高 2 倍以上。 (2)对于稠密图,就得分情况讨论了。单源问题明显还是 Dijkstra 的势力范围,效率 比 SPFA 要高 2-3 倍。APSP 问题,如果对时间要求不是那么严苛的话简简单单的 Floyd 即可满足要求,又快又不容易写错;否则就得使用 Dijkstra 或其他更高级的算法了。如果 是无向图,则可以把 Dijkstra 扔掉了,加上优化的 SPFA 绝对是必然的选择。 稠密图 稀疏图 有负权边 --------------------------------------------------------------------------------------- 单源问题 Dijkstra+heap SPFA(或 Dijkstra+heap,根据稀疏程度) SPFA APSP(无向图) SPFA(优化)/Floyd SPFA(优化) SPFA(优化) APSP(有向图) Floyd SPFA (或 Dijkstra+heap,根据稀疏程度) SPFA 完。 版权所有。转载本 BLOG 内任何文章,请以超链接形式注明出处。 否则,一经发现,必定永久谴责+追究法律责任。谢谢,各位。 非常感谢网友花明月黯的制作。同时,本人对以上所有内容享有著作权及版权, 严禁用于任何商业用途,违者必永久追究法律责任。 July、2011.05.17。 联系 July E-mail: zhoulei0907@yahoo.cn Blog: http://blog.csdn.net/v_JULY_v Weibo: http://weibo.com/julyweibo
还剩345页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

_ygr

贡献于2013-07-27

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf