[Java]常见算法问题(持续学习,更新)
2016-05-08 20:25
609 查看
1.最大子序列和问题的四种算法:O(N^3),O(N^2),O(NlogN),O(N)
算法1:
算法2:
算法3:
测试:
结果:
D:\java\practice4>javac MaxSub.java
D:\java\practice4>java MaxSub
315569581
315569581
315569581
D:\java\practice4>
讨论:
第三种算法的执行时间分析:
T(1)=1
T(N)=2T(N-1)+O(N)
为了简化计算,以N代替O(N),观察:
T(2)=2*2,T(4)=4*3,T(8)=8*4,T(16)=16*5,
若N=2^k,则T(N)=N*(k+1)=N*logN+N=O(NlogN)
这个分析假设N为偶数,否则N/2就不确定了。当N不是2的幂时,需要复杂一些的分析,但大O的结果是不变的。
算法4:
分析:一个负值绝不是一个最大子列的头;一个值为负的子列绝不是一个最大子列的前缀;如果arr[j]是第一个使一个子列为负的值(而不是什么小于前面子列的值,因为包含arr[j]及后面值的子列仍有可能大于前面子列)(值为负的前缀子列),那么我们可以把头直接推进到j+1(也就是代码中直接将sum赋值为0的情况),所以我们一个最优解也不会错过!------->简单,大气地分析,确定最优想法的每一个简单事实,累积和排除后,就是最优正确答案,不要钻牛角尖!
附带优点:只对数据进行一次扫描,一旦a[i]被读入并处理,它就不需要再被记忆。如果数组在磁盘上或互联网上被传输,可以按顺序读入,主存中不必存储数组的任何部分。在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案(其他算法不具备!),具有这种特性的算法叫做联机算法。仅需要常量空间,并以线性时间运行的联机算法,几乎是完美的算法!
2.O(logN)讨论与折半查找
分析算法最混乱的方面在于对数,某些分治算法(如例子1算法3)将以O(NlogN)的时间运行。对数最常出现的规律概括为以下法则:
如果一个算法用常数时间(O(1))将问题削减为其一部分(通常是1/2),该算法就是O(logN)
如果使用常数时间只把问题减少一个常数数量(如减少1),那么它就是O(N)的
折半查找:在一个已预先排序的集合中查找Ai=X,如果X不在集合中则返回-1
实现:
测试:字符串类型
结果:10
分析:循环从high-low=N-1开始到high-low<=-1结束,每次循环后值至少折半,循环次数最多为「log(N-1) +2(向上取整),因此运行时间为O(logN).
折半查找提供了O(logN)时间内的数据结构的contains操作,但所有其他操作特别是insert都需要O(N)的时间。在数据稳定(不允许插入和删除)的情况下它是非常有用的,只需一次排序,此后访问会很快。顺序查找则需要多的多的时间。
欧几里得算法计算最大公约数:
结果:gcd(1989,1590)的余数序列:399,393,6,3,0,故gcd(1989,1590)=3
一次迭代中余数并不按一个常数因子递减、但可以证明,在两次迭代后余数最多是原始值的一半,故迭代次数至多是2logN,即O(logN)
定理:如果M>N,则M mod N<M/2
幂运算的高效递归算法及分析
明显的算法是N-1次乘法自乘,但递归算法效果更好:N<=1为基准情形,若N为偶数,则X^N=X^(N/2)*X^(N/2),若N为奇数,则X^N=X^(N/2)*X^(N/2)*X
如X^62只用到9次乘法
所需乘法次数最多为2logN,因为每次把问题分半最多需要两次乘法(N为奇数的情况)
下列程序,第5,6行不是必须的,因为当N=1时,第10行将做同样的事情,并且第10行还可写成:
(注:n-1为偶数,按上面的情形计算)
程序:
测试:
结果:9765625
讨论:
把第8行改为:
会影响效率,会有两个大小为N/2的递归调用而不是一个。其运行时间不再是O(logN).
改为:
或
都是错误的,因为当N=2时递归调用有一个将以2作为第二个参数,Calc(2,2)将产生无限循环。
3.运行时间猜想与检验:一个例子
当N扩大一倍时,线性程序运行时间乘以因子2,二次程序乘以4,三次程序乘以8,对数时间运行的程序时间只多加一个常数(根据对数运算法则
),而以O(NlogN)的运行时间为2倍稍多一些。
如果低阶项系数较大,而N又不足够大,则不易观察清楚。
验证一个程序是否是O(f(N))的另一个常用技巧是以2的倍数隔开的N的某个范围计算比值T(N)/f(N),T(N)为观察到的运行时间。如果理想近似,则比值收敛于一个正常数;如果f(N)估计过大,则收敛于0;如果过低或O(f(N))是错的,则发散。
例子:计算随机选取的<=N的两个互异正整数互素的概率:(N增大时,结果趋近于6/PI^2)
结果:
D:\java\practice4>javac Gailv.java
D:\java\practice4>java Gailv
6
0.6082443036567745
D:\java\practice4>
在一台计算机上分别计算T(N)/N^2,T(N)/N^3,T(N)/(N^2*logN):
发现最后一列是最合适的。注意O(N^2)和O(N^2*logN)没多大差别,因为对数增长得很慢。
算法1:
public static int Method1(int[] arr)//O(N^3) { int max=0; for(int i=0;i<arr.length;i++){//子列起始坐标 for(int j=i;j<arr.length;j++){//子列终坐标 int sum=0; for(int k=i;k<=j;k++) sum+=arr[k]; if (sum>max) { max=sum; } } } return max; }
算法2:
public static int Method2(int[] arr)//O(N^2) { int max=0; for(int i=0;i<arr.length;i++){ int sum=0;//它在这,和上一个位置不同! for(int j=i;j<arr.length;j++){ sum+=arr[j];//只要保证a[j]遍历了每个元素,子列加长只不断加上后面元素即可,避免重复计算 if (sum>max) { max=sum; } } } return max; }
算法3:
public static int Method3(int[] arr,int left,int right) { if(left==right)//Base Case if(arr[left]>0) return arr[left]; else return 0; int center=(left+right)/2;//最自然简化实用正确地去思考,不要去想细节! int maxLeftSum=Method3(arr,left,center);//递归 int maxRightSum=Method3(arr,center+1,right); //包含最后元素的左边最大与包含第一元素的右边最大 int maxLeftBorderSum=0,leftBorderSum=0; for(int i=center;i>=left;i--) { leftBorderSum+=arr[i]; if(leftBorderSum>maxLeftBorderSum) maxLeftBorderSum=leftBorderSum; } int maxRightBorderSum=0,rightBorderSum=0; for(int i=center+1;i<=right;i++) { rightBorderSum+=arr[i]; if(rightBorderSum>maxRightBorderSum) maxRightBorderSum=rightBorderSum; } return max3(maxLeftSum,maxRightSum,maxLeftBorderSum+maxRightBorderSum); } public static int max3(int x,int y,int z) { return x>y?(x>z?x:z):(y>z?y:z); }
测试:
public static void main(String[] args) { int[] arr=new int[]{121234,435,3942,786,354542,34244324,765,799078,3455,242343243,35355345,2342432}; System.out.println(Method1(arr)); System.out.println(Method2(arr)); System.out.println(Method3(arr,0,arr.length-1)); }
结果:
D:\java\practice4>javac MaxSub.java
D:\java\practice4>java MaxSub
315569581
315569581
315569581
D:\java\practice4>
讨论:
第三种算法的执行时间分析:
T(1)=1
T(N)=2T(N-1)+O(N)
为了简化计算,以N代替O(N),观察:
T(2)=2*2,T(4)=4*3,T(8)=8*4,T(16)=16*5,
若N=2^k,则T(N)=N*(k+1)=N*logN+N=O(NlogN)
这个分析假设N为偶数,否则N/2就不确定了。当N不是2的幂时,需要复杂一些的分析,但大O的结果是不变的。
算法4:
public static int Method4(int[] arr) { int max=0,sum=0; for(int j=0;j<arr.length;j++) { sum+=arr[j]; if(sum>max){ max=sum; }else if(sum<0){ sum=0; } } return max; }结果一致,时间为O(N).
分析:一个负值绝不是一个最大子列的头;一个值为负的子列绝不是一个最大子列的前缀;如果arr[j]是第一个使一个子列为负的值(而不是什么小于前面子列的值,因为包含arr[j]及后面值的子列仍有可能大于前面子列)(值为负的前缀子列),那么我们可以把头直接推进到j+1(也就是代码中直接将sum赋值为0的情况),所以我们一个最优解也不会错过!------->简单,大气地分析,确定最优想法的每一个简单事实,累积和排除后,就是最优正确答案,不要钻牛角尖!
附带优点:只对数据进行一次扫描,一旦a[i]被读入并处理,它就不需要再被记忆。如果数组在磁盘上或互联网上被传输,可以按顺序读入,主存中不必存储数组的任何部分。在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案(其他算法不具备!),具有这种特性的算法叫做联机算法。仅需要常量空间,并以线性时间运行的联机算法,几乎是完美的算法!
2.O(logN)讨论与折半查找
分析算法最混乱的方面在于对数,某些分治算法(如例子1算法3)将以O(NlogN)的时间运行。对数最常出现的规律概括为以下法则:
如果一个算法用常数时间(O(1))将问题削减为其一部分(通常是1/2),该算法就是O(logN)
如果使用常数时间只把问题减少一个常数数量(如减少1),那么它就是O(N)的
折半查找:在一个已预先排序的集合中查找Ai=X,如果X不在集合中则返回-1
实现:
public static <AnyType extends Comparable<? super AnyType>> int binarySearch(AnyType[] a,AnyType x) { int low=0,high=a.length-1; while(low<=high) { int mid=(low+high)/2; if(a[mid].compareTo(x)<0) low=mid+1; else if(a[mid].compareTo(x)>0) high=mid-1; else return mid; } return -1; }
测试:字符串类型
public static void main(String[] args) { String[] arr=new String[]{"a","abc","abcde","abde","adx","bdxc","def","fx","fxck","jaxp","jdbc","jsp","jvm"}; System.out.println(binarySearch(arr,"jdbc")); }
结果:10
分析:循环从high-low=N-1开始到high-low<=-1结束,每次循环后值至少折半,循环次数最多为「log(N-1) +2(向上取整),因此运行时间为O(logN).
折半查找提供了O(logN)时间内的数据结构的contains操作,但所有其他操作特别是insert都需要O(N)的时间。在数据稳定(不允许插入和删除)的情况下它是非常有用的,只需一次排序,此后访问会很快。顺序查找则需要多的多的时间。
欧几里得算法计算最大公约数:
public static long gcd(long m,long n) { while(n!=0) { long rem=m%n; m=n; n=rem; } return m; }
结果:gcd(1989,1590)的余数序列:399,393,6,3,0,故gcd(1989,1590)=3
一次迭代中余数并不按一个常数因子递减、但可以证明,在两次迭代后余数最多是原始值的一半,故迭代次数至多是2logN,即O(logN)
定理:如果M>N,则M mod N<M/2
幂运算的高效递归算法及分析
明显的算法是N-1次乘法自乘,但递归算法效果更好:N<=1为基准情形,若N为偶数,则X^N=X^(N/2)*X^(N/2),若N为奇数,则X^N=X^(N/2)*X^(N/2)*X
如X^62只用到9次乘法
所需乘法次数最多为2logN,因为每次把问题分半最多需要两次乘法(N为奇数的情况)
下列程序,第5,6行不是必须的,因为当N=1时,第10行将做同样的事情,并且第10行还可写成:
return Calc(x,n-1)*x;
(注:n-1为偶数,按上面的情形计算)
程序:
public static long Calc(long x,int n) { if(n==0) return 1; if(n==1) return x; if(n%2==0) return Calc(x*x,n/2); else return Calc(x*x,n/2)*x; }
测试:
public static void main(String[] args) { System.out.println(Calc(5,10)); }
结果:9765625
讨论:
把第8行改为:
return Calc(x,n/2)*Calc(x,n/2);
会影响效率,会有两个大小为N/2的递归调用而不是一个。其运行时间不再是O(logN).
改为:
return Calc(Calc(x,2),n/2);
或
return Calc(Calc(x,n/2),2);
都是错误的,因为当N=2时递归调用有一个将以2作为第二个参数,Calc(2,2)将产生无限循环。
3.运行时间猜想与检验:一个例子
当N扩大一倍时,线性程序运行时间乘以因子2,二次程序乘以4,三次程序乘以8,对数时间运行的程序时间只多加一个常数(根据对数运算法则
),而以O(NlogN)的运行时间为2倍稍多一些。
如果低阶项系数较大,而N又不足够大,则不易观察清楚。
验证一个程序是否是O(f(N))的另一个常用技巧是以2的倍数隔开的N的某个范围计算比值T(N)/f(N),T(N)为观察到的运行时间。如果理想近似,则比值收敛于一个正常数;如果f(N)估计过大,则收敛于0;如果过低或O(f(N))是错的,则发散。
例子:计算随机选取的<=N的两个互异正整数互素的概率:(N增大时,结果趋近于6/PI^2)
class Gailv { public static long gcd(long i,long j) { while(j!=0) { long r=i%j; i=j; j=r; } return i; } public static double Match(int n) { int sum=0,sum1=0; for(int i=1;i<=n;i++) { for(int j=i+1;j<=n;j++)//遍历n以内所有随机对 { sum++;//总随机对个数 if(gcd(i,j)==1) sum1++; } } return (double)sum1/sum; } public static void main(String[] args) { System.out.println(gcd(678,96)); System.out.println(Match(3000)); } }
结果:
D:\java\practice4>javac Gailv.java
D:\java\practice4>java Gailv
6
0.6082443036567745
D:\java\practice4>
在一台计算机上分别计算T(N)/N^2,T(N)/N^3,T(N)/(N^2*logN):
发现最后一列是最合适的。注意O(N^2)和O(N^2*logN)没多大差别,因为对数增长得很慢。
相关文章推荐
- java mysql 数据类型对照
- 安装JDK
- java - 输入的字符串中是否包含中文
- 20145107 《Java程序设计》第五次实验报告
- 20145107 《Java程序设计》第十周学习总结
- 给大一的学弟学妹们培训java web的后台开发讨论班计划
- java容器总结
- JAVA聊天室代码
- java提示框
- 20145315 《Java程序设计》第十周学习总结
- 20145315 《Java程序设计》实验五实验报告
- 20145311 《Java程序设计》第十周学习总结
- 大数相乘(java)
- JAVA的String 类
- JDK安装与环境变量配置
- Java线程和线程池学习笔记
- SpringMVC原理
- Java设计模式之模板方法设计模式
- 20145334 第五次 java 实验报告
- java新手之每日学习篇