当前位置:   article > 正文

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

用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为底)

四、插入排序

  1. public class Sort1 implements ISort{
  2. @Override
  3. public void sort(int[] arr) {
  4. for(int i = 0; i < arr.length; i++) {
  5. for(int j = i; j > 0; j--) {
  6. if (arr[j] < arr[j - 1]) {
  7. swap(arr, j , j - 1);
  8. }
  9. }
  10. }
  11. }
  12. private void swap(int[] arr, int i, int j) {
  13. int temp = arr[i];
  14. arr[i] = arr[j];
  15. arr[j] = temp;
  16. print(arr);
  17. }
  18. private void print(int[] arr) {
  19. System.out.println(Arrays.toString(arr));
  20. }
  21. public static void main(String[] args) {
  22. int[] arr = new int[] {9,8,7,6,5};
  23. Sort1 sort1 = new Sort1();
  24. sort1.sort(arr);
  25. }
  26. }

结果:

[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)求解时间复杂度

  1. public void sort(int[] arr) {
  2. for(int i = 0; i < arr.length; i++) {
  3. for(int j = i; j > 0; j--) {
  4. if (arr[j] < arr[j - 1]) {
  5. swap(arr, j , j - 1);
  6. }
  7. }
  8. }
  9. }

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

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)

五、二分插入排序

  1. @Override
  2. public void sort(int[] arr) {
  3. for(int i = 1; i < arr.length; i++) {
  4. int left = 0;
  5. int right = i - 1;
  6. int cur = arr[i];
  7. while(left <= right) {
  8. int mid = (left + right) >> 1;
  9. if(cur < arr[mid]) {
  10. right = mid - 1;
  11. } else {
  12. left = mid + 1;
  13. }
  14. }
  15. for(int j = i - 1;j >= left; j--) {
  16. arr[j + 1] = arr[j];
  17. print(arr);
  18. }
  19. arr[left] = cur;
  20. print(arr);
  21. }
  22. }

结果:

[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)二分查找到插入的位置

  1. ......
  2. while(left <= right) {
  3. int mid = (left + right) >> 1;
  4. if(cur < arr[mid]) {
  5. right = mid - 1;
  6. } else {
  7. left = mid + 1;
  8. }
  9. }
  10. ......

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

 

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

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

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开始)

方法一:双递归

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

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

这里使用二叉树分析, 我们先算下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)

方法二:单递归

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

用二叉树分析:

0

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

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

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

方法三:循环

  1. public static int fn3(int n) {
  2. int a = 1, b = 1;
  3. int temp;
  4. for(int i = 2; i < n; i++) {
  5. temp = a;
  6. a = a + b;
  7. b = temp;
  8. }
  9. return a;
  10. }

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

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

七、最大子数组

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

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

方法一:暴力枚举

  1. public static int maxSubarray1(int[] arr) {
  2. if(arr.length == 0) {
  3. return -1;
  4. }
  5. int max = arr[0];
  6. for (int i = 0; i < arr.length; i++) {
  7. int sum = 0;
  8. for (int j = i; j < arr.length; j++) {
  9. sum += arr[j];
  10. if (sum > max) {
  11. max = sum;
  12. }
  13. }
  14. }
  15. return max;
  16. }

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

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

方法二:分治法

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

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

  1. int maxMidLeftSum = arr[mid];
  2. int midLeftSum = 0;
  3. for (int i = mid; i >= 0; i--) {
  4. midLeftSum += arr[i];
  5. if (midLeftSum > maxMidLeftSum) {
  6. maxMidLeftSum = midLeftSum;
  7. }
  8. }
  9. int maxMidRightSum = arr[mid + 1];
  10. int midRightSum = 0;
  11. for (int i = mid + 1; i <= right; i++) {
  12. midRightSum += arr[i];
  13. if (midRightSum > maxMidRightSum) {
  14. maxMidRightSum = midRightSum;
  15. }
  16. }
  17. int maxSum = maxLeftSum;
  18. if (maxRightSum > maxSum) {
  19. maxSum = maxRightSum;
  20. }
  21. if ((maxMidLeftSum + maxMidRightSum) > maxSum) {
  22. maxSum = maxMidLeftSum + maxMidRightSum;
  23. }
  24. return maxSum;

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

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

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

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

完整代码如下:

  1. public static int maxSubarray2(int[] arr, int left, int right) {
  2. if (arr.length == 0) {
  3. return -1;
  4. }
  5. if (left == right) {
  6. return arr[left];
  7. } else {
  8. int mid = (left + right) >> 1;
  9. int maxLeftSum = maxSubarray2(arr, left, mid);
  10. int maxRightSum = maxSubarray2(arr, mid + 1, right);
  11. int maxMidLeftSum = arr[mid];
  12. int midLeftSum = 0;
  13. for (int i = mid; i >= 0; i--) {
  14. midLeftSum += arr[i];
  15. if (midLeftSum > maxMidLeftSum) {
  16. maxMidLeftSum = midLeftSum;
  17. }
  18. }
  19. int maxMidRightSum = arr[mid + 1];
  20. int midRightSum = 0;
  21. for (int i = mid + 1; i <= right; i++) {
  22. midRightSum += arr[i];
  23. if (midRightSum > maxMidRightSum) {
  24. maxMidRightSum = midRightSum;
  25. }
  26. }
  27. int maxSum = maxLeftSum;
  28. if (maxRightSum > maxSum) {
  29. maxSum = maxRightSum;
  30. }
  31. if ((maxMidLeftSum + maxMidRightSum) > maxSum) {
  32. maxSum = maxMidLeftSum + maxMidRightSum;
  33. }
  34. return maxSum;
  35. }
  36. }

假设数组为{-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)好。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/398850?site
推荐阅读
相关标签
  

闽ICP备14008679号