算法是为求解一个问题需要遵循的,被清楚指定的简单指令的集合,判断一个算法的好坏,其平均运行时间一般是难以得到的,但是最坏情况的运行时间容易得出,一般用最坏情况下的运行时间来衡量一个算法的好坏。
1 最大子序列和问题
这里实验了三个算法复杂度分别是O( N 2 N^2 N2)、O(N log N \log{N} logN)和O(N)的算法
1.1 二次解法
即进行两次遍历求最大子序列和的方式进行处理。
/**
* 两个for循环,时间大小为O(N*N)
* @param arr
* @return
*/
public static int firstMethod (int [] arr) {
int maxSum = 0;
for (int i = 0 ; i < arr.length; i++ ) {
int thisSum = 0;
for (int j = i; j < arr.length; j++) {
thisSum += arr[j];
if (thisSum > maxSum) {
maxSum = thisSum;
}
}
}
return maxSum;
}
1.2 二分法
- 分治法解决最大子序列和问题
- 1、将数组分成两部分,找出前半部分的最大子序列和(max1)和后半部分的最大子序列和(max2)
- 2、找出包含最后一个元素的前半部分的最大序列和
- 3、找出包含第一个元素的后半部分的最大序列和
- 4、将第2步和第3步中的两个和相加,得出max3
- 5、返回max1、max2和max3三个数中的最大者
/**
* 分治法解决最大子序列和问题
* 1、将数组分成两部分,找出前半部分的最大子序列和(max1)和后半部分的最大子序列和(max2)
* 2、找出包含最后一个元素的前半部分的最大序列和
* 3、找出包含第一个元素的后半部分的最大序列和
* 4、将第2步和第3步中的两个和相加,得出max3
* 5、返回max1、max2和max3三个数中的最大者
* @param arr 数组
* @param left 开始下标
* @param right 结束下表
* @return 返回传入数组的最大子序列和
*/
public static int divide (int [] arr, int left, int right) {
//
if (left == right) {
if (arr[left] > 0) {
return arr[left];
} else {
return 0;
}
}
int center = (left + right) / 2;
int maxLeftSum = divide(arr, left, center);
int maxRightSum = divide(arr, center + 1, right);
// 找出包含最后一个元素的前半部分的最大序列和
int maxLeftBorderSum = 0;
int thisLeftSum = 0;
for (int i = center; i >= left; i--) {
thisLeftSum += arr[i];
if (thisLeftSum > maxLeftBorderSum) {
maxLeftBorderSum = thisLeftSum;
}
}
// 找出包含的一个元素的后半部分的最大序列和
int maxRightBorderSum = 0;
int thisRightSum = 0;
for (int i = center + 1; i <= right; i++) {
thisRightSum += arr[i];
if (thisRightSum > maxRightBorderSum) {
maxRightBorderSum = thisRightSum;
}
}
// 返回maxLeftSum、maxRIghtSum、maxLeftBorderSum + maxRightBorderSum三个数中的最大者
int max = maxLeftSum;
if (maxRightSum > maxLeftSum) {
max = maxRightSum;
}
if (maxLeftBorderSum + maxRightBorderSum > max) {
max = maxLeftBorderSum + maxRightBorderSum;
}
return max;
}
这里来看一下这个二分法的算法时间分析,令T(N)是求解大小为N的最大序列和问题所花费的时间;两个递归行,每行花费T(N/2)时间,两个循环刚好将整个数组遍历一次,即可以看成花费了O(N)的时间,最后得出结论T(N) = 2T(N/2) + N,
求解这个方程:
T(N) = 2T(N/2) + N, 两边同时除以N,得到,
T
(
N
)
N
\frac{T(N)}{N}
NT(N) =
T
(
N
/
2
)
N
/
2
\frac{T(N/2)}{N/2}
N/2T(N/2) + 1,依次可以得到这些等式:
- T ( N / 2 ) N / 2 \frac{T(N/2)}{N/2} N/2T(N/2) = T ( N / 4 ) N / 4 \frac{T(N/4)}{N/4} N/4T(N/4) + 1
-
T
(
N
/
4
)
N
/
4
\frac{T(N/4)}{N/4}
N/4T(N/4) =
T
(
N
/
8
)
N
/
8
\frac{T(N/8)}{N/8}
N/8T(N/8) + 1
… -
T
(
2
)
2
\frac{T(2)}{2}
2T(2) =
T
(
1
)
1
\frac{T(1)}{1}
1T(1) + 1
左边从N到2,一共有logN个这样的的等式,将logN个这样的等式左右两边分别相加,可以得到, T ( N ) N \frac{T(N)}{N} NT(N) = T ( 1 ) 1 \frac{T(1)}{1} 1T(1) + logN, 从而会有:
T(N) = NlogN + N
1.3 最优解法
只需要遍历一次数组,即可获取最大子序列和
public static int bestMethod (int [] arr) {
int maxSum = 0;
int thisSum = 0;
for (int j = 0; j < arr.length; j++) {
thisSum += arr[j];
if (thisSum > maxSum) {
maxSum = thisSum;
} else if (thisSum < 0) { // 第i个数到第j个数的序列和小于0时,则可以舍弃这些数了,循环可以直接从j+1开始
thisSum = 0;
}
}
return maxSum;
}
这个算法实际上是改进的算法1,理解这个算法最关键的点在于,在算法1的基础上,当 A i A_i Ai和 A j A_j Aj之间的序列和小于0时,那么循环就可以直接推进到 A j + 1 A_{j+1} Aj+1.
2、欧几里得算法
计算两个整数的最大公因数,该算法的关键在于算法表述连续计算余数,并把余数赋给n,直到余数是0为止
public static int gcd (int m, int n){
while (n != 0) {
int rem = m % n; // rem 表述余数
m = n;
n = rem;
}
return m;
}
3、二分法求幂运算
二分法求 x n x^n xn的值, 根据指数的运算可以得出: x n x^n xn = x n / 2 ∗ x n / 2 x^{n/2} * x^{n/2} xn/2∗xn/2 = ( x ∗ x ) n / 2 ({x*x})^{n/2} (x∗x)n/2 ,即可以看成把求解 x n x^n xn的问题大小缩小了一般,当然,在程序中,当n为奇数时, x n x^n xn = ( x ∗ x ) ( n − 1 ) / 2 ∗ x ({x*x})^{(n - 1)/2}* x (x∗x)(n−1)/2∗x ,
public static long pow(long x, int n) {
if (n == 0) return 1;
if (n == 1) return x;
if (n%2 == 0) {
return pow(x * x, n/2);
} else {
// 当n为奇数时,n/2 的值与(n -1)/ 2的相同
return pow(x * x, n/2) * x;
}
}