数据结构 | 算法的时间复杂度和空间复杂度【详解】(一)

简介: 什么是数据结构?数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

1. 什么是数据结构?

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

2. 什么是算法?

算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

3. 算法效率

  • 算法效率是指算法执行的时间,算法执行时间需通过依据该算法编制的程序在计算机上运行时所消耗的时间来度量。在现在的计算机硬件环境中,比较少需要考虑这个问题了,特别是pc机的编程,内存空间越来越大,所以被考虑得也越来越少,不过一个好的程序员,都应该对自己的程序有要求,每一个for比别人少一次判断1000个for就能够少掉很多的运行时间。所以能够理解,能够大概的去运用"效率度量"还是有很大意义的。


  • 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算 机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

4. 时间复杂度

4.1 时间复杂度的概念

  • 时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

实际中我们计算时间复杂度的时候其实并不需要计算精确的执行次数,而只需要知道大概执行次数,所以这里我们引入了大O的渐进表示法。其中大O符号是用于描述函数渐进行为的数学符号。

案例1:

请计算一下Func1中++count语句总共执行了多少次?

  • 如果用一个函数F(N)来解释的话,怎么解释?【F(N) = N*N + 2N + 10】这是一个函数式
    这并不方便我们进行比较,能不能简化一下?

这里的时间复杂度是要取影响最大的项

大O渐进表示法:估算–>O(N^2)–>O(N)

我们这里不关心硬件,相同的配置NN^2的程序要快

void Func1(int N)
{
  int count = 0;
  for (int i = 0; i < N; ++i)
  {
    for (int j = 0; j < N; ++j)
    {
      ++count;
    }
  }
  for (int k = 0; k < 2 * N; ++k)
  {
    ++count;
  }
  int M = 10;
  while (M--)
  {
    ++count;
  }
  printf("%d\n", count);
}

Func1 执行的基本操作次数 :

  • N = 10 F(N) = 130
  • N = 100 F(N) = 10210
  • N = 1000 F(N) = 1002010

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

4.2 推导大O阶的方法:

大 O 表示法是一种计算机科学和算法分析中常用的渐进表示法,用于描述算法的时间复杂度和空间复杂度。它帮助我们衡量算法在输入规模增加时,运行时间或内存使用的增长趋势,而不需要精确地测量或比较具体的运行时间。


大 O 表示法使用 “O(f(n))” 的形式,其中 “f(n)” 表示输入规模 “n” 的函数。这意味着当输入规模足够大时,算法的运行时间或内存使用将以 “f(n)” 函数的方式增长。以下是一些常见的大 O 表示法及其含义:

  • O(1):常数时间复杂度
    表示算法的运行时间不受输入规模的影响,无论输入有多大,运行时间都是固定的。
  • O(log n):对数时间复杂度
    表示算法的运行时间以对数方式增长,通常见于二分查找等分治算法。
  • O(n):线性时间复杂度
    表示算法的运行时间与输入规模线性增长,随着输入规模增加,运行时间也会线性增加。
  • O(n log n):线性对数时间复杂度
    表示算法的运行时间随着输入规模的对数线性增加,常见于快速排序和归并排序等算法。
  • O(n^2):二次时间复杂度
    表示算法的运行时间与输入规模的平方成正比,通常见于嵌套循环等情况。
  • O(n^k):多项式时间复杂度
    表示算法的运行时间与输入规模的 k 次方成正比,其中 k 为常数。
  • O(2^n):指数时间复杂度
    表示算法的运行时间随着输入规模的指数级增长,通常表示算法的效率非常低。
  • O(n!):阶乘时间复杂度
    表示算法的运行时间随着输入规模的阶乘级增长,通常表示算法的效率非常低。
  • 大 O 表示法的目的是为了提供算法效率的一种抽象概念,让人们能够更容易比较不同算法的性能,并估计它们在大规模输入下的行为。通过了解算法的大 O 复杂度,开发者可以选择合适的算法来解决问题,以便在实际应用中获得更好的性能。

7fde103c3478e1c517fcf4ddb09965ec_c25bd55bbbdd4c63bb5a7b4ba3dda425.png

用大O的渐进表示法以后,Func1的时间复杂度为:

  • N = 10 F(N) = 100
  • N = 100 F(N) = 10000
  • N = 1000 F(N) = 1000000

4.3 常见时间复杂度计算举例:

接下来就看看下面这几个案例,相信你看完就有一定的了解

实例2:

计算Func2的时间复杂度?

  • 我们前期就一点一点的算,F(N) = 2N + 10
  • 时间复杂度是什么?【O(N)】
  • 就是取最大的那一项,当N无限大的时候N2N没有区别,所以系数要去掉
  • 时间复杂度不是算次数,是算量级
void Func2(int N)
{
  int count = 0;
  for (int k = 0; k < 2 * N; ++k)
  {
    ++count;
  }
  int M = 10;
  while (M--)
  {
    ++count;
  }
  printf("%d\n", count);
}

实例3:

计算Func3的时间复杂度?

  • 如果不确定M和N的关系,可能M很大,可能N很大–> O(M+N)或者O(max(M,N))
  • 如果N远大于M–>o(n)
  • 如果M远大于N–>O(M)
  • 如果N和M差不多大–>O(N) or O(M)
void Func3(int N, int M)
{
  int count = 0;
  for (int k = 0; k < M; ++k)
  {
    ++count;
  }
  for (int k = 0; k < N; ++k)
  {
    ++count;
  }
  printf("%d\n", count);
}

实例4:

计算Func4的时间复杂度?

  • 这里可以看到k循环了100次,那么他是O(100)吗?【不是!】是O(1)

注意:O(1)并不是代表一次,而是常数次!

void Func4(int N)
{
  int count = 0;
  for (int k = 0; k < 100; ++k)
  {
    ++count;
  }
  printf("%d\n", count);
}

小结一下:

  • 大O阶的方法
  • 用常数1取代运行时间中的所有加法常数
  • 在修改后的运行次数函数中,只保留最高阶项,取决定性的那一项
  • 如果最高阶项存在且系数是不唯1的常系数,则去除最高项的系数,得到的结果就是大O阶
  • 如果最终结果是O(1),则表示常数次,并不是代表一次
  • 还有就是没有小o阶~~

实例5:

计算strchr的时间复杂度?

const char* strchr(const char* str, int character);
while (*str) {
  if (*str == character)
    return str;
  ++str;
}
  • 这个函数有可能有的同学没有见过,这是C语言库里面的一个函数strchr,是定位字符串中出现的第一个字符
  • 可能在前面的位置找到了(最好),肯在中间的位置找到了(平均),还有肯在最后的位置找到了(最坏)

小结一下:

  • 最坏情况:
  • 任意输入规模的最大运行次数(上界)
  • 平均情况:
  • 任意输入规模的期望运行次数
  • 最好情况:
  • 任意输入规模的最小运行次数(下界)
  • 一般我们比较关心的是一个算法的最坏情况
  • 如果有最好,最坏,平均,那么时间复杂度是一个保守的估算,是取最坏,这是最好的!!!

实例6:

计算BubbleSort的时间复杂度?

  • 很多同学一眼看他是O(N^2),那么他是O(N^2)吗?接下来接着看
void BubbleSort(int* a, int n)
{
  assert(a);
  for (size_t end = n; end > 0; --end)
  {
    int exchange = 0;
    for (size_t i = 1; i < end; ++i)
    {
      if (a[i - 1] > a[i])
      {
        Swap(&a[i - 1], &a[i]);
        exchange = 1;
      }
    }
    if (exchange == 0)
      break;
  }
}
  • 再来看一个,这个相比上一个的时间复杂度是什么?
  • 可能有点同学一看这个循环有两层那么他是O(N^2),它是O(N^2)吗?–>继续接着往下看!
int PartSort1(int* a, int left, int right) {
  int keyi = left;
  while (left < right) {
    while (left < right && a[right] >= a[keyi]) {
      --right;
    }
    while (left < right && a[left <= a[keyi]]) {
      ++left;
    }
    Swap(&a[left], &a[right]);
  }
  Swap(&a[left], &a[right]);
}

我们上面都是看的循环,但是我们要看思想,不能只看循环,第一个是–>O(N^2)第二个是O(N)

你算对了吗?

  • 首先我们先看第一个冒泡排序的时间复杂度,如图:

  • 再来看第二个 PartSort1,当leftright相遇时,时间复杂度是O(N)

小结一下:

  • 时间复杂度不能数代码中的循环
  • 而是需要根据思想进行灵活计算!!!

数据结构 | 算法的时间复杂度和空间复杂度【详解】(二):https://developer.aliyun.com/article/1426602

相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
69 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
1月前
|
搜索推荐 算法
插入排序算法的平均时间复杂度解析
【10月更文挑战第12天】 插入排序是一种简单直观的排序算法,通过不断将未排序元素插入到已排序部分的合适位置来完成排序。其平均时间复杂度为$O(n^2)$,适用于小规模或部分有序的数据。尽管效率不高,但在特定场景下仍具优势。
|
30天前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
32 4
|
1月前
|
算法
[数据结构] -- 时间复杂度和空间复杂度
[数据结构] -- 时间复杂度和空间复杂度
15 0
|
1月前
|
机器学习/深度学习 搜索推荐 算法
探索数据结构:初入算法之经典排序算法
探索数据结构:初入算法之经典排序算法
|
1月前
|
算法 Java 索引
数据结构与算法学习十五:常用查找算法介绍,线性排序、二分查找(折半查找)算法、差值查找算法、斐波那契(黄金分割法)查找算法
四种常用的查找算法:顺序查找、二分查找(折半查找)、插值查找和斐波那契查找,并提供了Java语言的实现代码和测试结果。
19 0
|
15天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
6天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
15 1
|
8天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
11天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。

热门文章

最新文章