算法的主题思想:算法
1.优秀的算法由于可以解决实际问题而变得更为重要;数组
2.高效算法的代码也能够很简单;网络
3.理解某个实现的性能特色是一个挑战;数据结构
4.在解决同一个问题的多种算法之间进行选择时,科学方法是一种重要的工具;工具
5.迭代式改进可以让算法的效率愈来愈高效;性能
1. 动态连通性ui
动态链接:输入是一对整数对的序列,其中每一个整数表明某种类型的对象(或触点),咱们将整数对p q 解释为意味着p链接到q。咱们假设“链接到”是等价关系:spa
对称性:若是p链接到q,则q 链接到p。设计
传递性:若是p链接到q且q 链接到r,则p链接到r。
自反性:p与p链接。
等价关系将对象划分为多个等价类 或链接的组件。等价类称为连通份量或份量。
咱们的目标是编写一个程序,以从序列中过滤掉多余的对:当程序从输入中读取整数对 p q时,只有在该对点不等价的状况下,才应将对写入到输出中,而且将p链接到q。若是等价,则程序应忽略整数对pq 并继续读取下对。code
动态连通性问题的应用:
1.网络
2.变量名等价性
3.数学集合
在更高的抽象层次上,能够将输入的全部整数看作属于不一样的数学集合。
2. 定义问题
设计算法的第一个任务就是精确地定义问题。
算法解决的问题越大,它完成任务所需的时间和空间可能越多。咱们不可能预先知道这其间的量化关系,一般只会在发现解决问题很困难,或是代价巨大,或是发现算法所提供的信息比原问题所须要的更加有用时修改问题。例如,连通性问题只要求咱们的程序可以判断出给定的整数对是否相连,但并无要求给出二者之间的通路上的全部链接。这样的要求更难,并会得出另外一组不一样的算法。
为了定义和说明问题,先设计一份API 来封装基本操做: 初始化,链接两个触点,查找某个触点的份量 ,判断两个触点是否属于同一份量,份量的数量:
/// <summary> /// 动态连通API /// </summary> public interface IUnionFind { /// <summary> /// 链接 /// </summary> /// <param name="p"></param> /// <param name="q"></param> void Union(int p, int q); /// <summary> /// 查找触点 p 的份量标识符 /// </summary> /// <param name="p"></param> /// <returns></returns> int Find(int p); /// <summary> /// 判断两个触点是否处于同一份量 /// </summary> /// <param name="p"></param> /// <param name="q"></param> /// <returns></returns> bool Connected(int p, int q); /// <summary> /// 连通份量的数量 /// </summary> /// <returns></returns> int Count(); }
为解决动态连通性问题设计算法的任务转化为实现这份API:
1. 定义一种数据结构表示已知的链接;
2. 基于此数据结构高效的实现API的方法;
数据结构的性质会直接影响算法的效率。这里,以触点为索引,触点和链接份量都是用 int 值表示,将会使用份量中某个触点的值做为份量的标识符。因此,一开始,每一个触点都是只含有本身的份量,份量标识符为触点的值。由此,能够初步实现一部分方法:
public class FirstUnionFind:IUnionFind { private int[] id;//* 份量id 以触点做为索引 private int count;//份量数量 public FirstUnionFind(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一个 i 做为触点,第二个 i 做为触点的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { } public void Union(int p, int q) { } }
Union-find 的成本模型 是数组的访问次数(不管读写)。
3. quick-find算法实现
quick-find 算法是保证当且仅当 id[p] 等于 id[q] 时,p 和 q 是连通的。也就是说,在同一个连通份量中的全部触点在 id[ ] 中的值所有相等。
因此 Find 方法只需返回 id[q],Union 方法须要先判断 Find(p) 是否等于 Find(q) ,若相等直接返回;若不相等,须要将 q 所在的连通份量中全部触点的 id [ ] 值所有更新为 id[p]。
public class QuickFindUF: IUnionFind { private int[] id;//* 份量id 以触点做为索引 private int count;//份量数量 public QuickFindUF(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一个 i 做为触点,第二个 i 做为触点的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { return id[p]; } public void Union(int p, int q) { var pID = Find(p); var qID = Find(q); if (pID == qID) return; for (var i = 0; i < id.Length; i++) { if (id[i] == qID) id[i] = pID; } count--; //连通份量减小 } public void Show() { for(var i = 0;i<id.Length;i++) Console.WriteLine("索引:"+i+",值:"+ id[i] ); Console.WriteLine("连通份量数量:"+count); } }
算法分析
Find() 方法只需访问一次数组,因此速度很快。可是对于处理大型问题,每对输入 Union() 方法都须要扫描整个数组。
每一次归并两个份量的 Union() 方法访问数组的次数在 N+3 到 2N+1 之间。由代码可知,两次 Find 操做访问两次数组,扫描数组会访问N次,改变其中一个份量中全部触点的值须要访问 1 到 N - 1 次(最好状况是该份量中只有一个触点,最坏状况是该份量中有 N - 1个触点),2+N+N-1。
若是使用quick-find 算法来解决动态连通性问题而且最后只获得一个连通份量,至少须要调用 N-1 次Union() 方法,那么至少须要 (N+3)(N-1) ~ N^2 次访问数组,是平方级别的。
4. quick-union算法实现
quick-union 算法重点提升 union 方法的速度,它也是基于相同的数据结构 -- 已触点为索引的 id[ ] 数组,可是 id[ ] 的值是同一份量中另外一触点的索引(名称),也多是本身(根触点)——这种联系成为连接。
在实现 Find() 方法时,从给定触点,连接到另外一个触点,知道到达根触点,即连接指向本身。同时修改 Union() 方法,分别找到 p q 的根触点,将其中一个根触点连接到根触点。
public class QuickUnionUF : IUnionFind { private int[] id; private int count; public QuickUnionUF(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一个 i 做为触点,第二个 i 做为触点的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { while (p != id[p]) p = id[p]; return p; } public void Union(int p, int q) { var pRoot = Find(p); var qRoot = Find(q); if (pRoot == qRoot) return; id[pRoot] =qRoot;
count--; //连通份量减小 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("连通份量数量:" + count); } }
森林表示
id[ ] 数组用父连接的形式表示一片森林,用节点表示触点。不管从任何触点所对应的节点随着连接查找,最后都将到达含有该节点的根节点。初始化数组以后,每一个节点的连接都指向本身。
算法分析
定义:一棵树的大小是它的节点的数量。树中一个节点的深度是它到根节点的路径上连接数。树的高度是它的全部节点中的最大深度。
quick-union 算法比 quick-find 算法更快,由于它对每对输入不须要遍历整个数组。
分析quick-union 算法的成本比 quick-find 算法的成本要困难,由于quick-union 算法依赖于输入的特色。在最好的状况下,find() 方法只需访问一次数组就能够获得一个触点的份量表示;在最坏状况下,须要 2i+1 次数组访问(i 时触点的深度)。由此得出,该算法解决动态连通性问题,在最佳状况下的运行时间是线性级别,最坏状况下的输入是平方级别。解决了 quick-find 算法中 union() 方法老是线性级别,解决动态连通性问题老是平方级别。
quick-union 算法中 find() 方法访问数组的次数为 1(到达根节点只需访问一次) 加上 给定触点所对应节点的深度的两倍(while 循环,一次读,一次写)。union() 访问两次 find() ,若是两个触点不在同一份量还需加一次写数组。
假设输入的整数对是有序的 0-1, 0-2,0-3 等,N-1 对以后N个触点将所有处于相同的集合之中,且获得的树的高度为 N-1。由上可知,对于整数对 0-i , find() 访问数组的次数为 2i + 1,所以,处理 N 对整数对所需的全部访问数组的总次数为 3+5+7+ ......+(2N+1) ~ n^2
5.加权 quick-union 算法实现
简单改动就能够避免 quick-union算法 出现最坏状况。quick-union算法 union 方法是随意将一棵树链接到另外一棵树,改成老是将小树链接到大树,这须要记录每一棵树的大小,称为加权quick-union算法。
代码:
public class WeightedQuickUnionUF: IUnionFind { int[] sz;//以触点为索引的 各个根节点对应的份量树大小 private int[] id; private int count; public WeightedQuickUnionUF(int n) { count = n; id = new int[n]; sz = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一个 i 做为触点,第二个 i 做为触点的值 sz[i] = 1; } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { while (p != id[p]) p = id[p]; return p; } public void Union(int p, int q) { var pRoot = Find(p); var qRoot = Find(q); if (pRoot == qRoot) return; if (sz[pRoot] < sz[qRoot]) { id[pRoot] = qRoot; } else { id[qRoot] = pRoot; } count--; //连通份量减小 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("连通份量数量:" + count); } }
算法分析
加权 quicj-union 算法最坏的状况:
这种状况,将要被归并的树的大小老是相等的(且老是 2 的 冥),都含有 2^n 个节点,高度都正好是 n 。当归并两个含有 2^n 个节点的树时,获得的树含有 2 ^ n+1 个节点,高度增长到 n+1 。
节点大小: 1 2 4 8 2^k = N
高 度: 0 1 2 3 k
k = logN
因此加权 quick-union 算法能够保证对数级别的性能。
对于 N 个触点,加权 quick-union 算法构造的森林中的任意节点的深度最多为logN。
对于加权 quick-union 算法 和 N 个触点,在最坏状况下 find,connected 和 union 方法的成本的增加量级为 logN。
对于动态连通性问题,加权 quick-union 算法 是三种算法中惟一能够用于解决大型问题的算法。加权 quick-union 算法 处理 N 个触点和 M 条链接时最多访问数组 c M logN 次,其中 c 为常数。
三个算法处理一百万个触点运行时间对比:
三个算法性能特色:
6.最优算法 - 路径压缩
在检查节点的同时将它们直接链接到根节点。
实现:为 find 方法添加一个循环,将在路径上的全部节点都直接连接到根节点。彻底扁平化的树。
研究各类基础问题的基本步骤:
1. 完整而详细地定义问题,找出解决问题所必须的基本抽象操做并定义一份API。
2. 简洁地实现一种初级算法,给出一个精心组织的开发用例并使用实际数据做为输入。
3. 当实现所能解决的问题的最大规模达不到指望时决定改进仍是放弃。
4. 逐步改进实现,经过经验性分析和数学分析验证改进后的效果。
5. 用更高层次的抽象表示数据结构或算法来设计更高级的改进版本。
6. 若是可能尽可能为最坏状况下的性能提供保证,但在处理普通数据时也要有良好的性能。
7.在适当的时候将更细致的深刻研究留给有经验的研究者并解决下一个问题。