算法初级(二)
问题:
问题:给定序列A1,A2,A3,A4,…,An,求A1An,求A1An的一个任意子序列AiAj,使得AiAj的和最大。例如:整数序列【-2,11,-4,13,-5,2,-5,-3,12,-9的最大子序列之和为21(A2~A9)】
上一篇给出的是穷举法,我的简单理解是里面包含了三层for循环,每个循环遍历为N,因此算法的时间复杂度为O(n³)。
专业说法:
从第十行代码得出:
\sum_{i=1}^{n} i*(n-i+1)∑i=1ni∗(n−i+1) = O(n^3n3)
算法二:
穷举法里的每个子序列不需要每次都重新计算一次的,假设sum(i,j) 是A_iAi到A_jAj的和,那么sum(i,j+1)= sum(i,j) + A_{j+1}Aj+1。
代码:
public static int maxSub_2(int[] seq) {
int max = 0;
int n = seq.length;
int sum = 0;
for (int i = 0; i < n; i++) {
sum = 0;
for (int j = i; j < n; j++) {
sum += seq[j];
if(sum > max)
max = sum;
}
}
return max;
}
算法复杂度
专业说法:
从第8,9行代码得出:
\sum_{i=0}^{n-1} (n-i)∑i=0n−1(n−i) = \frac{n(n+1)}{2}2n(n+1) = O(n^2n2)
我的简单理解是里面包含了两层for循环,每个循环遍历为N,因此算法的时间复杂度为O(n^2n2)。
算法三(分治法):
把当前数组分开成2部分,前半部分【-2, 11, -4, 13, -5】,后半部分【2, -5, -3, 12, -9】。这里可以看出前半部分最大子序列之和是20(11-4+13),后半部分最大子序列之和是12(12)。这时候再合并起来看,最大子序列之和应该在这些元素之间产生,那就把中间元素加进去,前半部分为(20-5)15,后半部分为(2-5-3+12)6,那最大子序列之和就是15+6=21.
public static int maxSum(int[] seq,int left,int right) {
if(left == right)
if(seq[left] > 0)
return seq[left];
else
return 0;
int mid = (left+right)/2;
int maxLeftSum = maxSum(seq,left,mid);
int maxRightSum = maxSum(seq,mid+1,right);
int maxLeftBorderSum = 0,leftBorderSum = 0;
for (int i = mid; i >= left; i--) {
leftBorderSum += seq[i];
if(leftBorderSum > maxLeftBorderSum) {
maxLeftBorderSum = leftBorderSum;
}
}
int maxRightBorderSum = 0,rightBorderSum = 0;
for (int i = mid+1; i < right; i++) {
rightBorderSum += seq[i];
if(rightBorderSum > maxRightBorderSum) {
maxRightBorderSum = rightBorderSum;
}
}
return max3(maxLeftSum, maxRightSum, maxLeftBorderSum+maxRightBorderSum);
}
public static int max3(int a,int b ,int c ) {
int max = a>b?a:b;
max = max > c? max:c;
return max;
}
public static int maxSub_3(int[] seq) {
return maxSum(seq, 0, seq.length-1);
}
算法复杂度:
假设T(n)是求解大小为n的最大子序列和问题所花费的时间,那么如果当n=1时,T(1)只执行:
if(left == right)
if(seq[left] > 0)
return seq[left];
else
return 0;
那么T(1) = 1.否则就是调用后面的两个递归:
int maxLeftSum = maxSum(seq,left,mid);
int maxRightSum = maxSum(seq,mid+1,right);
分别求前后两部分。这里我们特殊一下,当n为偶数,那么问题就变成了求两个序列的最大子序列和的问题且每个序列的长度为n/2,故总时间为2T(n/2);其中两个for循环:
for (int i = mid; i >= left; i--) {
leftBorderSum += seq[i];
if(leftBorderSum > maxLeftBorderSum) {
maxLeftBorderSum = leftBorderSum;
}
}
for (int i = mid+1; i < right; i++) {
rightBorderSum += seq[i];
if(rightBorderSum > maxRightBorderSum) {
maxRightBorderSum = rightBorderSum;
}
}
循环共执行n次,因此时间复杂度为O(n)。因此整体时间复杂度:2T(n/2)+O(n)得到方程组:
T(1)=1;T(n)=2T(n/2)+O(n).
为了简化计算,用n代替O(n),由于最终T(n)最终还是要用大O表示,因此并不影响最终的答案。
这样T(n)=2T(n/2)+n且T(1)=1;则:T(2)=4=22,T(4)=12=43,T(8)=32=84,T(16)=80=165…用类推法,当n=2^k2k,则:
T(n)=n*(K+1)=n\log_2nlog2n+n=O(n\log_2nlog2n)
因此算法三的时间复杂度为:O(n\log_2nlog2n)。
附:该算法实现代码比较多,当时当n较大时,与前两个相比,彼此代码的运行速度是不一样的。如下:
public static int[] randomCommon(int min, int max, int n){
if (n > (max - min + 1) || max < min) {
return null;
}
int[] result = new int[n];
int count = 0;
while(count < n) {
int num = (int) (Math.random() * (max - min)) + min;
boolean flag = true;
for (int j = 0; j < n; j++) {
if(num == result[j]){
flag = false;
break;
}
}
if(flag){
result[count] = num;
count++;
}
}
return result;
}
public static void main(String[] args) {
int[] reult1 = randomCommon(-1000,1000,1000);
// for (int i : reult1) {
// System.out.println(i);
// }
long record = System.currentTimeMillis();
System.out.println(maxSub_1(reult1));
System.out.println(System.currentTimeMillis()-record);
record = System.currentTimeMillis();
System.out.println(maxSub_2(reult1));
System.out.println(System.currentTimeMillis()-record);
record = System.currentTimeMillis();
System.out.println(maxSub_3(reult1));
System.out.println(System.currentTimeMillis()-record);
}
}
randomCommon是一个随机生成不重复数组的方法,不打印我都不知道它的顺序,不过我们可以知道它的最大子序列之和和运行时间。
21162
519
21162
5
21162
1
22359
520
22359
6
22359
0
这是我运行两次的结果。可以看到当n=1000时,O(n^3n3)的结果是519和520,O(n^2n2)是5和6,O(n\log_2nlog2n)的结果是1和0。
算法四:
动态规划的思想【百度百科】:动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
代码:
public static int maxSub_4(int[] seq) {
int max = 0;
int n = seq.length;
int sum = 0;
for (int i = 0; i < n; i++) {
sum+=seq[i];
if(sum > max)
max = sum;
else if(sum < 0)
sum = 0;
}
return max;
}
因为只对整个数组只循环一遍因此时间复杂度为O(n)。
先调整n=10000,看下结果:
public static void main(String[] args) {
int n = 10000;
int[] reult1 = randomCommon(-n,n,n);
// for (int i : reult1) {
// System.out.println(i);
// }
long record = System.currentTimeMillis();
System.out.println(maxSub_1(reult1));
System.out.println(System.currentTimeMillis()-record);
record = System.currentTimeMillis();
System.out.println(maxSub_2(reult1));
System.out.println(System.currentTimeMillis()-record);
record = System.currentTimeMillis();
System.out.println(maxSub_3(reult1));
System.out.println(System.currentTimeMillis()-record);
record = System.currentTimeMillis();
System.out.println(maxSub_4(reult1));
System.out.println(System.currentTimeMillis()-record);
}
运行时间稍微有点长。。。
749415
607700
749415
29
749415
1
749415
1
当n=100000时(去掉算法一,时间太长了):
14804002
2047
14804002
17
14804002
2
可以更清楚的看到各代码的时间差异。
到此,结束了,告辞!
附知识点:
- 利用计算机求解问题的实现过程:
(1)确定问题求解的数学模型(或者逻辑结构):对问题进行深入分析,确定处理的数据对象是什么,再考虑根据处理对象的逻辑关系给出其数学模型。
(2)确定存储结构:根据数据对象的逻辑结果及其所需完成的功能,选择一个合适的组织形式将数据对象映射到计算机存储器中。
(3)设计算法(程序):讨论要解决问题的策略,即算法(程序)的确定步骤。
(4)编程并测试结果 - 数据和数据结构
(1)数据:数据是信息的载体
(2)数据元素:数据元素是数据中的一个“个体”,是数据的基本组织单位
(3)数据项:数据项是数据元素的组成部分
(4)数据对象::数据对象是性质相同的数据元素的集合;一般来说,数据对象的数据元素不会是孤立的,而是彼此关联的,这种彼此之间的关系称之为“结构”。 - 数据结构
(1)逻辑结构:集合结构[除了属于一个集合外的特性,数据元素之间毫无关系]、线性结构[一对一关系]、树形结构[一对多关系]、图形结构[多对多关系]…
(2)存储结构:顺序存储、链式存储、索引存储、散列存储…
(3)数据操作:创建、销毁、插入、删除、查找、修改、遍历… - 数据类型
一个数据的数据类型描述了三个方面的内容:存储区域的大小(存储结构)、取值范围(数据集合)和允许的操作。 - 算法的性质:有穷性、确定性、有效性、输入、输出
- 算法的目标:正确性、可读性、健壮性、高效率
- 算法时间复杂度排序:
O(1)<O(\log_2nlog2n)<O(n)<O(n\log_2nlog2n)<O(n^2n2)<O(n^nnn)
指数级:O(a^nan),常见的有O(2^n2n),O(n!n!),O(n^nnn),之间的关系:
O(2^n2n)<O(n!n!)<O(n^nnn)