1.最大子序列和问题的四种算法:O(N^3),O(N^2),O(NlogN),O(N)
算法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; }
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; }
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")); }
分析:循环从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; }
一次迭代中余数并不按一个常数因子递减、但可以证明,在两次迭代后余数最多是原始值的一半,故迭代次数至多是2logN,即O(logN)
定理:如果M>N,则M mod N<M/2