您的位置:首页 > 编程语言 > Java开发

[Java]常见算法问题(持续学习,更新)

2016-05-08 20:25 609 查看
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;
}


算法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)没多大差别,因为对数增长得很慢。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: