数学基础:
定义:
如果只是小量输入的情况,那么花费大量时间去努力设计聪明的算法恐怕并不值得。因此,好的算法应该是因地制宜的,不能盲目。
算法分析的基本策略是从内部(或最深层部分)向外展开的。
正常的用递归解法求解Fib的算法之所以缓慢,是因为有大量的多余的工作量,重复计算较多,可以通过保留一个简单的数组并使用一个for循环将运行时间降下来。
接下来,我们比较四种不同的求解最大子序列和问题的算法。
算法1。
public static int maxSubSum1(int[] a){
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
for (int j = i; j < a.length; j++) {
int thisSum = 0;
for (int k = i; k<= j; k++) {
thisSum+=a[k];
}
if(thisSum>maxSum){
maxSum = thisSum;
}
}
}
return maxSum;
}
//该算法的思想比较直接。i从0到length,j从i到length,再用k(位于i与j之间)将i与j之间的sum计算出来。这样取最大的sum即为所求。
时间复杂度为N的3次方。
算法2.
public static int maxSubSum2(int[] a){
int maxSum = 0;
for (int i = 0; i < a.length; i++) {
int thisSum = 0;
for (int j = i; j < a.length; j++) {
thisSum+=a[j];
if(thisSum>maxSum){
maxSum = thisSum;
}
}
}
return maxSum;
}
//这里的算法2只是1的一个小改动,将原来的:
for (int k = i; k<= j; k++) {
thisSum+=a[k];
}
换成了:
thisSum+=a[j];并将thisSum提出(显然的)
//事实上也应该是这样。最大子序列和问题的本质,就是每个子序列的首与尾的位置不固定,那么我们只需要循环两次,每次得到确定首尾的sum值,然后取最大就可以了
时间复杂度为N的2次方。
“分治”策略:
把问题分成两个大致相等的子问题,然后递归地对他们求解,这是分。
“治”是将两个子问题的解合到一起,并可能做些少量的附加工作,最后得到整个问题的解。
在我们的例子中,最大子序列只可能在3处出现。1.整个出现在输入数据的左半部2.整个出现在输入数据的右半部。针对这两种情况都可以用递归求解。3.最大和可以通过求出前半部分最大和(包含前半部分最后一个元素)以及后半部分的最大和(包含后半部分的第一个元素)而得到,然后将这两个和加在一起。
算法3.
public static int maxSubSum3(int[] a,int left,int right){
//基本情况
if(left==right){
if(a[left]>0){
return a[left];//最大子序列
}else{
return 0;//不选取该值作为最大子序列的一部分,所以应该返回0
}
}
//分治
int center = (left+right)/2;
int leftMaxSum = maxSubSum3(a, left, center);
int rightMaxSum = maxSubSum3(a, center+1, right);
int leftBorderSum = 0,leftBorderMaxSum=0;
//固定一端center,达到前面的最大子序列
for (int i = center; i >= left; i--) {
leftBorderSum+=a[i];
if(leftBorderSum>leftBorderMaxSum){
leftBorderMaxSum = leftBorderSum;
}
}
int rightBorderSum = 0,rightBorderMaxSum=0;
//固定一端center,达到后面的最大子序列
for (int i = center+1; i <= right; i++) {
rightBorderSum+=a[i];
if(rightBorderSum>rightBorderMaxSum){
rightBorderMaxSum = rightBorderSum;
}
}
int twoBorderMaxSum = leftBorderMaxSum+rightBorderMaxSum;
int maxSum1 = Math.max(leftMaxSum,rightMaxSum);
int maxSum = Math.max(maxSum1, twoBorderMaxSum);
return maxSum;
}
时间复杂度为NlogN.都是用递归,Fib低效而本例却比较高效,是因为每次递归降低问题的维度不一样,前者维度下降慢,后者则是对半的下降。
算法4.
public static int maxSubSum4(int[] a){
int maxSum =0,thisSum = 0;
for (int i = 0; i < a.length; i++) {
thisSum+=a[i];
if(thisSum>maxSum){
maxSum = thisSum;
}
if(thisSum<0){
thisSum = 0;
}
}
return maxSum;
}
//分析原因,注意,像算法1和算法2一样,j代表当前序列的终点,而i代表当前序列的起点。碰巧的是,如果我们确实不需要知道最佳的子序列在哪里,那么i的使用可以脱离程序进行优化,我们要改进算法2。
//一个重要结论是,如果a[i]是负的,那么它不可能代表最优序列的起点。类似的,任何负的子序列不可能是最优子序列的前缀。
时间复杂度为N。
该算法的一个附带优点是,它只对数据进行一次扫描,一旦a[i]被读入并被处理,他就不再需要被记忆。因此,如果数组在磁盘上,他就可以被顺序读入,在主存中不必存储数组的任何部分。不仅如此,在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案(其他算法不具有这种特性)。具有这种特性的算法叫做联机算法(on-line algorithm)。仅需要常量空间并以线性时间运行的联机算法几乎是完美的算法。
运行时间中的对数:
分析算法最混乱的方面集中在对数上,我们已经看到某些分治算法将以NlogN时间运行。除分治算法外,对数最常出现的规律概括为下列一般法则:
如果一个算法用常数时间将问题的大小削减为其一部分(通常是一半),那么该算法就是logN。
另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算法就是N的。
具有对数特点的三个例子:
1.对分查找(Binary Search),在数据稳定(即不允许插入操作和删除操作)的应用中,Binary Search非常有用。此时输入数据需要一次排序(前提),但是此后访问就很快了。
2.计算最大公因数的欧几里得算法。
public static long gcd(long M,long N){
while(N!=0){
long rem = M%N;
M = N;
N = rem;
}
return M;
}
3.幂运算