数据结构和算法分析 引论+算法分析
数学知识复习
级数运算
常用的有:
递归算法
递归一般可以条件性的拆分为:
- 基准情况:不用递归的那一部分。
- 不管推进:递归调用的递归是朝着一个基准情况的方向在推进。
一个简单递归的例子:打印出正整数。基于一个只能打印一个0-9的数字的方法,打印正整数。
如果定义可以打印0-9的数字的方法为g(x),打印正整数的那么递归的推进表达式可写为为:
f(x) = f(x/10) + g(x%10)
其中g(x)是基准情况。
代码实现为:
-
package com.zjf;
-
-
public class Recursive {
-
-
public static void main(String[] args) {
-
printInt(15623);
-
}
-
-
public static void printInt(int i)
-
{
-
if(i > 10)
-
{
-
printInt(i/10);
-
}
-
printDigit(i%10);
-
-
}
-
public static void printDigit(int i )
-
{
-
if(9>= i && i >= 0)
-
{
-
System.out.print(i);
-
}
-
else
-
{
-
throw new IllegalArgumentException();
-
}
-
}
-
-
}
注意,一个递归f(x),在递归调用f(x)的时候,必须要终止条件。如上面的 if(i > 10)。否则将会陷入死循环。
泛型类型限界
Comparable接口是泛型接口,涉及子类和超类的的情况下,我们一般在需要对比的超类级别上定义对比方法,子类如果覆盖了超类的对比方法,那么这个超类的不同子类之间就不能比较了,代码如下:
-
package com.zjf;
-
-
-
public class GenericTest {
-
-
public static void main(String[] args) {
-
Person zjf = new Person("zjf",30);
-
Man zdw = new Man("zdw",27);
-
Woman xhj = new Woman("xhj",30);
-
System.out.println(zjf.compareTo(zdw));
-
System.out.println(zdw.compareTo(xhj));
-
}
-
-
}
-
-
class Person implements Comparable<Person>{
-
private String name;
-
private Integer age;
-
-
public Person(String name, Integer age) {
-
super();
-
this.name = name;
-
this.age = age;
-
}
-
-
@Override
-
public int compareTo(Person o) {
-
return this.age.compareTo(o.age);
-
}
-
}
-
-
class Man extends Person{
-
-
public Man(String name, Integer age) {
-
super(name, age);
-
}
-
-
}
-
-
class Woman extends Person{
-
-
public Woman(String name, Integer age) {
-
super(name, age);
-
}
-
-
}
运行结果没有问题,由于都是使用Person的对比方法,所以不同子类之间可以对比。
如果此时Woman实现了Comparable< Woman>,那么Woman和Man就不能相互比较了。
我们来实践一下:
报错了,错误信息是:The interface Comparable cannot be implemented more than once with different arguments:
Comparable<Person> and Comparable<Woman>
因为在java中,Comparable<Person> and Comparable<Woman>和在运行时是一样的,都是Comparable,所以我们不能Woman继承了Person,其实已经实现了Comparable,这里等于再次实现了一次,编译上是通不过的。
那么如果我们不再次实现Comparable,只是试图重写并覆盖超类的compareTo方法呢,如下:
-
class Woman extends Person {
-
-
public Woman(String name, Integer age) {
-
super(name, age);
-
}
-
-
@Override
-
public int compareTo(Person o) {
-
// TODO Auto-generated method stub
-
return super.compareTo(o);
-
}
-
}
这样,即使要重写,参数也只能是Person类型。这里可以做强制转换。但是很不优雅了。
同样,如果使用不加泛型的Comparable,那么所有的参数都是Object类型的,也不是很优雅。
由于Java的泛型不能区分Comparable<Person> and Comparable<Woman>,所以只能如此了。
不再纠结这个问题,现在,基于上面的设计,如果我们需要定义一个static的方法,用于实现取一个数组最大值的工具方法。
第一种方法,使用非泛型方法:
-
public static Comparable findMax(Comparable[] arr)
-
{
-
return ...;
-
}
这种方法,返回类型只能是Comparable的,在使用的时候要强制转换。
下面使用泛型方法重写:
最简单的方法,使用<T extends Comparable>,代码如下:
-
public static <T extends Comparable> T findMax(T[] arr)
-
{
-
T max = arr[0];
-
for(int i = 1; i<arr.length; i++)
-
{
-
if(max.compareTo(arr[i]) < 0)
-
{
-
max = arr[i];
-
}
-
}
-
return max;
-
}
按照作者的说法,这种写法不太优雅。作者进一步又觉得改造成:
<T extends Comparable<T>>
但是这个不能表示Man,因为Man实现的 是Comparable<Person>,而不是Comparable<Man>.
如下:
所以作者又进一步改成了:
<T extends Comparable<? super T>>
这样作者就满足了。。
事实上,我在实验的时候,如通过如下代码试验:
-
public static void main(String[] args) {
-
Person zjf = new Person("zjf",30);
-
Man zdw = new Man("zdw",27);
-
Woman xhj = new Woman("xhj",30);
-
Person[] c = new Person[]{zjf,zdw,xhj};
-
findMax(c);
-
}
上面的几种写法,编译器都是一样认可的。因为我声明数组的时候是用的Person。对于编译器来说,一个 Person对象满足了 <T extends Comparable>,<T extends Comparable<T>> , <T extends Comparable<? super T>>三种。
思考:什么时候使用泛型方法?
当一组操作针对多种类型参数时使用,比如上面的findMax方法,它要对一组继承自Comparable接口的数据进行处理。特别参数中是对数组和集合进行操作,其实结果返回的是集合中的具体类型
运行时间计算
一个简单的例子:
-
public static long getSum(int n){
-
long sum = 0;
-
for(int i = 0;i< n;i++)
-
{
-
sum += n*n*n;
-
}
-
return n;
-
}
时间复杂度在意的是随着N的扩大,极限情况下的复杂度。这里假设第5行的时间复杂度为10,第2行的为1,那么这方法的时间复杂度为10N + 1,我们记为O(N)。
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
最大子序列和问题求解
最大子序列和:求一个数组中所有子序列中和最大的那个和。
如-1,2,5,-6,3。最大子序列是2,5。和是7.
以下所有算法都假设
- 第一种算法:
-
public static int getBigSum(int[] arr){
-
int sum = arr[0];
-
for(int i =0; i< arr.length; i++)
-
{
-
for(int j = i; j < arr.length; j++)
-
{
-
int sumTemp = 0;
-
for(int x = i; x <=j; x++ )
-
{
-
sumTemp += arr[x];
-
}
-
if(sumTemp > sum)
-
{
-
sum = sumTemp;
-
}
-
}
-
}
-
return sum;
-
}
我们来看复杂度:
第一层循环次数为N
第二层循环次数为(1到N) = N + (N-1) + (N-2) + …1 = N*(N+1)/2 ≈ N2/2。
第三层循环的次数为:(1到N) + (1到N-1) + (1到N-2) … + (1到1) = N2/2 + (N-1)2/2 + … =(N*(N+1)*(2N+1)/6)/2 = N3/6
最终的算法复杂度是取第三层执行的次数,也就是O(N3)。
- 第二种算法:
-
ublic static int getBigSum(int[] arr){
-
int sum = arr[0];
-
for(int i =0; i< arr.length; i++)
-
{
-
for(int j = i; j < arr.length; j++)
-
{
-
int sumTemp = 0;
-
for(int x = i; x <=j; x++ )
-
{
-
sumTemp += arr[x];
-
}
-
if(sumTemp > sum)
-
{
-
sum = sumTemp;
-
}
-
}
-
}
-
return sum;
-
}
我们来看复杂度:
第一层循环次数为N
第二层循环次数为(1到N) = N + (N-1) + (N-2) + …1 = N*(N+1)/2 ≈ N2/2。
所以复杂度为O(N2)。
- 第三种算法:
-
public static int getBigSum3(int[] arr)
-
{
-
return getBigSumRec(arr,0,arr.length-1);
-
}
-
-
/**
-
*
-
* @param arr 数组
-
* @param left 左下标
-
* @param right 右下标
-
* @return
-
*/
-
public static int getBigSumRec(int[] arr,int left,int right){
-
//基准情况
-
if(left == right)
-
{
-
return arr[left];
-
}
-
//拆分为两半
-
int center = (left + right)/2;
-
//现在 有两种情况
-
//一种是最大值序列不包含中间的center下标的值 那么就是递归左侧getBigSumRec(arr,left,center)或者右侧getBigSumRec(arr,center + 1,right)
-
//一种是最大值序列包含中间的center下标的值 肯定不是上面两种情况了,而是:
-
//从center向左遍历到left的最大序列值 + center向右表里到right的最大值序列
-
//递归计算左侧
-
int maxLeftSum = getBigSumRec(arr,left,center);
-
//递归计算右侧
-
int maxRightSum = getBigSumRec(arr,center + 1,right);
-
-
int maxLeftBorderSum = arr[center];
-
int leftBorderSum = 0;
-
for(int i = center; i >= left; i--)
-
{
-
leftBorderSum += arr[i];
-
if(leftBorderSum > maxLeftBorderSum )
-
{
-
maxLeftBorderSum = leftBorderSum;
-
}
-
}
-
-
int maxRightBorderSum = arr[center+1];
-
int rightBorderSum = 0;
-
for(int i = center + 1; i <= right; i++)
-
{
-
rightBorderSum += arr[i];
-
if(rightBorderSum > maxRightBorderSum )
-
{
-
maxRightBorderSum = rightBorderSum;
-
}
-
}
-
return max3(maxLeftSum,maxRightSum,maxLeftBorderSum + maxRightBorderSum);
-
}
-
//返回三个数值中的最大值
-
private static int max3(int i, int j, int k) {
-
int max = i;
-
if(j > max)
-
{
-
max = j;
-
}
-
if(k > max)
-
{
-
max = k;
-
}
-
return max;
-
}
因为第一层循环是是折半递归,所以复杂度为logN,第二层循环是两个for循环,复杂度为N。所以整个复杂度为O(NlogN)。
- 第三种算法:
-
public static int getBigSum4(int[] arr){
-
//最大序列和
-
int sum = arr[0];
-
//局部序列和
-
int sumTemp = 0;
-
//解释一下上面的两个初始值的设置
-
//因为只是sum对比和赋值 所以初始值不能设置为0 否则如果全是负值 那么结果会是0
-
//因为sumTemp的值是根据+计算出来的 所以初始这可以设置为0
-
-
for(int i =0; i< arr.length; i++)
-
{
-
sumTemp += arr[i];
-
if(sumTemp > sum )
-
{
-
sum = sumTemp;
-
}
-
//如果局部序列和为负值 那么我们就可以舍弃它 重新开始一个局部序列了
-
//因为一个和为负值的局部序列和 不管下一个数值是正负 累加后都会小于下一个数值 所以我们直接从下一个数值开始一个新的序列
-
if(sumTemp < 0)
-
{
-
sumTemp = 0;
-
}
-
}
-
return sum;
-
}
很明显,时间复杂度为O(N)。
对数复杂度
如果一个算法用常数时间O(1)将问题的大小削减为其一部分,通常为1/2,那么该算法的复杂度就是O(logN)。
最简答的例子就是折半查找(二分查找):
-
public static <T extends Comparable<? super T>> int binarySerch(T[] arr, T t) {
-
int left = 0;
-
int right = arr.length - 1;
-
while(left <= right)
-
{
-
int middle = (left + right)/2;
-
if(arr[middle].compareTo(t) > 0)
-
{
-
right = middle - 1;
-
}
-
else if(arr[middle].compareTo(t) < 0)
-
{
-
left = middle + 1;
-
}
-
else
-
{
-
return middle;
-
}
-
}
-
return -1;
-
}
复杂度为O(logN)。
另外一个例子,幂运算:
-
public static long pow(long x, int y) {
-
long result = 0;
-
if (y == 0) {
-
return 1;
-
}
-
if (y == 1) {
-
return x;
-
}
-
// 偶数
-
if (y % 2 == 0) {
-
long temp = pow(x, y / 2);
-
return temp * temp;
-
//书上写的是 return power(x*x,y/2)
-
}
-
// 偶数
-
if (y % 2 == 1) {
-
long temp = pow(x, y / 2);
-
return temp * temp * x;
-
//书上写的是 return power(x*x,y/2) * x
-
}
-
return result;
-
}
时间复杂度为O(logN)
如果使用y遍循环,做x*x,那么时间复杂度是O(N)。