WS分享11-时间复杂度分析JAVA算法(一)

如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O

一、简单比较算法好坏

我们比较哪一个算法好,最直接的方法就是比较执行命令的次数。

计算1 + 2 + 3 + 4 + 5 + ...10

第一种方式 依次累加,计算10次。

第二种方式 使用公式(1 + 10)/ 2 * 10 ,计算3次。

扩展为n的计算。

1 + 2 + 3 + 4 + .....n

第一种方式 依次累加,计算n次。

第二种方式 使用公式 (1 + n) / 2 * n,计算3次。

假定单次计算时间相同,计算次数越少,用时越短,算法越好。

当n 小于3时,第一种方式好, 之后当n越大,第二种方式优势越明显。

二、定义时间复杂度

通过上面的例子,引入时间复杂度概念:

时间频度:一个算法中语句执行次数,记为T(n),如上例中的计算次数。

时间复杂度又称为渐进时间复杂度,考察的是当 n 趋近于无穷大的时候,T(n)的增长趋势。使用大O符号表述。

定义:(来自于百度知道)

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f (n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。

听上去复杂,可以试着反向验证理解,先说结论:

开篇例子中,(1 + n) / 2 * n 的时间复杂度为 O(n ^ 2)

让我们反向验证定义:T(n) / n ^ 2 是不是一个常数呢?

0

确实等于常数。

三、求解时间复杂度步骤

首先时间复杂度是一个渐进值,当n->无穷时,低阶项对结果影响不大,可以全部去掉。

如:T(n) = 2 * n^2 + 2 * n + 1

当n = 10000时,T(n) = 2 * 10000 * 10000 + 2 * 10000 + 1,其中2 * 10000 + 1 对结果影响不大,可以去掉。

其次时间复杂度比较的是一个增长趋势,首相系数为常量,不是结果变化的主要因素,

且当比较多种时间复杂度时,都从0点出发,从函数图上一眼可以比较出好坏,因此需要去掉首相系数。

0

(上图来自百度知道)

综上,求解时间复杂度步骤如下:

(1)T(n)只保留函数的首项,去掉低阶项

(2)去掉首项系数(如果是对数函数换底为2)

如:

T(n) = 3 时间复杂度为: O(1)

T(n) = 2n 时间复杂度为: O(n)

T(n) = 2 * n^2 + 2 * n + 1 时间复杂度为:O(n^2)

T(n) = 2 * logan (这里a是底) 时间复杂度为: O(logn)

(无论底是多少都换为以2为底)

四、插入排序

public class Sort1 implements ISort{

	@Override
	public void sort(int[] arr) {
		for(int i = 0; i < arr.length; i++) {
			for(int j = i; j > 0; j--) {
				if (arr[j] < arr[j - 1]) {
					swap(arr, j , j - 1);
				}
			}
		}
	}
	
	private void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
		print(arr);
	}
	
	private void print(int[] arr) {
		System.out.println(Arrays.toString(arr));
	}
	
	public static void main(String[] args) {
		int[] arr = new int[] {9,8,7,6,5};
		Sort1 sort1 = new Sort1();
		sort1.sort(arr);
	}
}

结果:

[8, 9, 7, 6, 5]

[8, 7, 9, 6, 5]

[7, 8, 9, 6, 5]

[7, 8, 6, 9, 5]

[7, 6, 8, 9, 5]

[6, 7, 8, 9, 5]

[6, 7, 8, 5, 9]

[6, 7, 5, 8, 9]

[6, 5, 7, 8, 9]

[5, 6, 7, 8, 9]

(1)从结果来分析排序过程:

0

(2)求解时间复杂度

public void sort(int[] arr) {
		for(int i = 0; i < arr.length; i++) {
			for(int j = i; j > 0; j--) {
				if (arr[j] < arr[j - 1]) {
					swap(arr, j , j - 1);
				}
			}
		}
	}

首先计算出命令执行的次数,分析如下:

int i = 0 -> i < arr.length -> int j = i -> j > 0 (4次)

i++ -> i < arr.length -> int j = i -> j > 0 -> arr[j] < arr[j - 1] -> swap(arr, j , j - 1);

-> j -- -> j > 0 (8次)

i++ -> i < arr.length -> int j = i -> j > 0 -> arr[j] < arr[j - 1] -> swap(arr, j , j - 1);

-> j -- -> j > 0 -> arr[j] < arr[j - 1] -> swap(arr, j , j - 1);

......

如此不易计算,将所有常量级操作全部当做1次计算(最坏情况内层循环每次都需要交换),现在的问题简化为计算循环次数。

时间频度T(n) = 1 + 2 + 3 + 4 + ... + n - 1 = n * (n - 1) / 2

时间复杂度为 O(n^2)

五、二分插入排序

@Override
	public void sort(int[] arr) {
		for(int i = 1; i < arr.length; i++) {
			int left = 0;
			int right = i - 1;
			int cur = arr[i];
			while(left <= right) { 
				int mid = (left + right) >> 1;
				if(cur < arr[mid]) {
					right = mid - 1;
				} else {
					left = mid + 1;
				}
			}
			
			for(int j = i - 1;j >= left; j--) {
				arr[j + 1] = arr[j];
				print(arr);
			}
			arr[left] = cur;
			print(arr);
		}
		
	}

结果:

[9, 9, 7, 6, 5]

[8, 9, 7, 6, 5]

[8, 9, 9, 6, 5]

[8, 8, 9, 6, 5]

[7, 8, 9, 6, 5]

[7, 8, 9, 9, 5]

[7, 8, 8, 9, 5]

[7, 7, 8, 9, 5]

[6, 7, 8, 9, 5]

[6, 7, 8, 9, 9]

[6, 7, 8, 8, 9]

[6, 7, 7, 8, 9]

[6, 6, 7, 8, 9]

[5, 6, 7, 8, 9]

1、从结果分析排序过程

0

2、求解时间复杂度

(1)二分查找到插入的位置

......
            while(left <= right) { 
				int mid = (left + right) >> 1;
				if(cur < arr[mid]) {
					right = mid - 1;
				} else {
					left = mid + 1;
				}
			}
......

T(n) = log1 + log2 + log3 + ...... + logn= logn! <= nlogn

 

(2)从插入位置到当前元素之前,整体向后移动1位

......
            for(int j = i - 1;j >= left; j--) {
				arr[j + 1] = arr[j];
				print(arr);
			}
......

T(n) = 1 + 2 + 3 + ...... + (n - 1) = n * (n - 1) / 2

(3)两部分相加

时间频度T(n) = n * (n - 1) / 2 + nlogn

时间复杂度为O(n ^ 2)

六、递归

研究斐波那契数列

1,1,2,3,5,8,......

求第n项。(n从1开始)

方法一:双递归

public static int fn(int n) {
		if(n < 3) {
			return 1;
		}
		return fn(n - 2) + fn(n - 1);
	} 

求解递归的时间频度函数,即计算次数,同样可以简化为递归次数。

这里使用二叉树分析, 我们先算下n = 6的情况,

0

每一个节点就是一次递归,递归的执行顺序为二叉树前序遍历的顺序:6,4,2,3,1,2,5,3,1,2,4,2,3,1,2

考虑最坏情况下,即为满二叉树。

先计算深度 m = n - 1 (从n 到 1)

再计算满二叉树节点数 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + .....2 ^ (m - 1) = 2 ^ m - 1 (等比数列求和公式a1(1-q^n)/(1-q)  )

带入深度m,总结点数为 2 ^ (n - 1) - 1,因此时间复杂度为O(2 ^ n)

方法二:单递归

public static int fn2(int a, int b, int n) {
		if(n < 3) {
			return a;
		}
		return fn2(a + b, a, n - 1);
	}

用二叉树分析:

0

这里看出每一层,都只有一个节点,因此总节点数即为树的深度。

时间频度T(n) = n - 1

时间复杂度为O(n),比方法一O(n^2)要好。

方法三:循环

public static int fn3(int n) {
		int a = 1, b = 1;
		int temp;
		for(int i = 2; i < n; i++) {
			temp = a;
			a = a + b;
			b = temp;
		}
		return a;
	} 

时间频度T(n) = n - 2

时间复杂度为O(n),和方式二相同

七、最大子数组

数组{-1,2,-3, 4,5,-6}中,求解最大的连续子数组和是多少?

这里肉眼可以看到是2 + (-3) + 4 + 5 = 9

方法一:暴力枚举

public static int maxSubarray1(int[] arr) {
		if(arr.length == 0) {
			return -1;
		}

		int max = arr[0];
		for (int i = 0; i < arr.length; i++) {
			int sum = 0;
			for (int j = i; j < arr.length; j++) {
				sum += arr[j];
				if (sum > max) {
					max = sum;
				}
			}
		}
		return max;
	}

时间频度T(n) = n + n - 1 + n - 2 + .......2 + 1 = (1 + n) * n / 2

时间复杂度为O(n ^ 2)

方法二:分治法

将数组从中间分开,分为三种情况最大子数组完全在左边,完全在右边,或包含中点。

a、如果是包含中点的情况,我们需要从中点开始向两边暴力枚举找到最大子数组。

            int maxMidLeftSum = arr[mid];
			int midLeftSum = 0;
			for (int i = mid; i >= 0; i--) {
				midLeftSum += arr[i];
				if (midLeftSum > maxMidLeftSum) {
					maxMidLeftSum = midLeftSum;
				}
			}

			int maxMidRightSum = arr[mid + 1];
			int midRightSum = 0;
			for (int i = mid + 1; i <= right; i++) {
				midRightSum += arr[i];
				if (midRightSum > maxMidRightSum) {
					maxMidRightSum = midRightSum;
				}
			}

			int maxSum = maxLeftSum;
			if (maxRightSum > maxSum) {
				maxSum = maxRightSum;
			}
			if ((maxMidLeftSum + maxMidRightSum) > maxSum) {
				maxSum = maxMidLeftSum + maxMidRightSum;
			}
			return maxSum;

b、如果最大子数组全部在左边,减小右边距,缩小范围,继续递归。

int maxLeftSum = maxSubarray2(arr, left, mid);

c、如果最大子数组全部在右边,加大左边距,缩小范围,继续递归。

int maxRightSum = maxSubarray2(arr, mid + 1, right);

完整代码如下:

public static int maxSubarray2(int[] arr, int left, int right) {
		if (arr.length == 0) {
			return -1;
		}

		if (left == right) {
			return arr[left];
		} else {
			int mid = (left + right) >> 1;
			int maxLeftSum = maxSubarray2(arr, left, mid);
			int maxRightSum = maxSubarray2(arr, mid + 1, right);

			int maxMidLeftSum = arr[mid];
			int midLeftSum = 0;
			for (int i = mid; i >= 0; i--) {
				midLeftSum += arr[i];
				if (midLeftSum > maxMidLeftSum) {
					maxMidLeftSum = midLeftSum;
				}
			}

			int maxMidRightSum = arr[mid + 1];
			int midRightSum = 0;
			for (int i = mid + 1; i <= right; i++) {
				midRightSum += arr[i];
				if (midRightSum > maxMidRightSum) {
					maxMidRightSum = midRightSum;
				}
			}

			int maxSum = maxLeftSum;
			if (maxRightSum > maxSum) {
				maxSum = maxRightSum;
			}
			if ((maxMidLeftSum + maxMidRightSum) > maxSum) {
				maxSum = maxMidLeftSum + maxMidRightSum;
			}
			return maxSum;
		}
	}

假设数组为{-1,2,-3, 4,5,-6},起始时left为0,right为5。

使用二叉树来分析时间复杂度。

0

递归的执行顺序为二叉树前序遍历的顺序:

left:0, right:5

left:0, right:2

left:0, right:1

left:0, right:0

left:1, right:1

left:2, right:2

left:3, right:5

left:3, right:4

left:3, right:3

left:4, right:4

left:5, right:5

 而执行包含中点情况的代码的是非叶子节点(图中打钩节点)。

分析过后得出两个结论:

(1)最大子数组完全在左侧或右侧的情况,递归的次数为二叉树的节点数。

假设最坏情况为满二叉树时,起始左侧索引为0,右侧索引为n(图中的5)

二叉树的深度m为logn + 2

二叉树的总节点数为2^0 + 2^1 + 2^2 + 2^3 +.....2^(m -1) = 2^m - 1 = 4n -1

(2)最大子数组包含中点的情况,在非叶子节点执行。最坏情况下每层节点执行循环的次数的和都是n。

执行次数为:(m - 1) * n = (logn + 1) * n

综上,

时间频度T(n) = 4n -1 + (logn + 1) * n = nlogn + 5n - 1

时间复杂度为O(nlogn)比第一种方式O(n ^ 2)好。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值