赞
踩
如果这篇文章对您有些用处,请点赞告诉我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 是不是一个常数呢?
确实等于常数。
首先时间复杂度是一个渐进值,当n->无穷时,低阶项对结果影响不大,可以全部去掉。
如:T(n) = 2 * n^2 + 2 * n + 1
当n = 10000时,T(n) = 2 * 10000 * 10000 + 2 * 10000 + 1,其中2 * 10000 + 1 对结果影响不大,可以去掉。
其次时间复杂度比较的是一个增长趋势,首相系数为常量,不是结果变化的主要因素,
且当比较多种时间复杂度时,都从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)从结果来分析排序过程:
(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、从结果分析排序过程
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的情况,
每一个节点就是一次递归,递归的执行顺序为二叉树前序遍历的顺序: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);
- }
用二叉树分析:
这里看出每一层,都只有一个节点,因此总节点数即为树的深度。
时间频度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。
使用二叉树来分析时间复杂度。
递归的执行顺序为二叉树前序遍历的顺序:
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)好。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。