【数据结构与算法基础】——算法复杂度

简介: 【数据结构与算法基础】——算法复杂度

前言

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

一、算法效率

       如何去衡量一个算法的好坏?

       算法在编写成可执行程序后,运行时消耗时间和空间资源。因此,一般从时间和空间两个维度来衡量,即时间复杂度和空间复杂度。

时间复杂度和空间复杂度

       时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行时所需要的额外空间。      

       在计算机发展的早期,计算机的存储容量很小。所以对复杂度很是在乎。但是计算机行业的迅速发展,计算机存储容量已经达到很高的程度,所以我们并不用特别关注算法的空间复杂度

二、时间复杂度

       时间复杂度是指算法在执行过程中,所需要的时间资源和问题规模之间的关系,主要衡量算法的运行效率,用来估算算法在不同规模下的运行时间

       时间复杂度用大O的渐进表示法来表示

2.1时间复杂度的计算

       算法的时间复杂度是一个函数式T(N),它定量描述了该算法的运行时间。

实际上,我们计算时间复杂度时,计算主要涉及到以下几个方面

基本操作次数: 时间复杂度的计算通常关注算法中执行的基本操作次数,例如赋值操作、比较操作、算术运算等。通常将这些操作的数量与输入规模相关联。

循环结构: 算法包含循环结构,需要考虑循环的迭代次数以及每次迭代中的基本操作数量。

递归调用: 对于递归算法,需要考虑递归的深度以及每次递归调用的时间复杂度。通常使用递归方程(递归关系式)来表示递归算法的时间复杂度。

分支结构: 如果算法包含分支结构,需要考虑每个分支的执行次数以及分支中的基本操作数量。

输入规模: 时间复杂度的计算通常与输入规模有关。输入规模表示算法操作的数据量或问题的大小,通常用符号n表示。

       说白了,算法复杂度其实就是计算基本操作的执行次数

看一个案例,来计算时间复杂度

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;
  }
}

这里Func函数基础语句执行次数 T(N)=N^2+2N+10

大O的渐进表示法表示就成了 O(N^2)

大O的渐进表示法

1 . 时间复杂度的函数式T(N)中,只保留最高阶项,去掉那些低阶项

2 . 如果最高项存在且不是1,则去掉这个项的常数系数

3 . T(N)中如果没有N的相关项,只要常数项,用常数1来取代所有加法常数

2.2、时间复杂度计算实例

2.2.1、示例一

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

Func1函数基本操作次数

        T(N)= 2N+10

       大O渐进表示法  Func1的时间复杂度为:O(N)

2.2.2、示例二

void Func2(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);
}

Func1函数基本操作次数

       T(N)= M+N

       因为这里无法确定M和N的大小,所以有大O渐进表示法 就为:O(M+N)

2.2.3、示例三

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

Func3函数的基本操作次数

       T(N)=100

       用大O渐进表示法 表示 O(1)

2.2.4、示例四

const char* strchr(const char
  * str, int character)
{
  const char* p_begin = s;
  while (*p_begin != character)
  {
    if (*p_begin == '\0')
      return NULL;
    p_begin++;
  }
  return p_begin;
}

这里计算strchr函数的基本操作次数      

       如果查找的字符在字符串的第一个位置(靠前的位置)则T(N)= 1

       如果查找的字符在字符串最后 则T(N)=  N

       如果查找的字符在字符串中间位置 则T(N)= N/2

这样用大O渐进表示法就要三种情况

情况一: O(1)        情况二: O(N)        情况三: O(N)

这里我们就会发现,有些算法的时间复杂度存在多种情况

       最好情况 : 任意出入规模的最小运行次数(下界)

       最坏情况 : 任意输入规模的最大运行次数(上界)

       平均情况: 任意输入规模的期望运行次数

大O渐进表示法在实际情况中一般关注的是算法的上界,也就是最坏运行情况。

所以这里strchr函数的时间复杂度为:O(N)

2.2.5、示例五

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;
  }
}

冒泡排序的时间复杂度

       如果数组有序为升序,则T(N) = N (最好情况)

       如果数组有序为降序,则T(N) = (N*(N+1))/2 (最坏情况)

因此BubbleSort的时间复杂度为 O(N^2)

2.2.6、示例六

void func4(int n)
{
  int cnt = 1;
  while (cnt < n)
  {
    cnt *= 2;
  }
}

Func4函数

       n=2,执行次数为1

       n=4,执行次数为2

       n=16,执行次数为4

       当执行次数为x时,n=2^x

       所以执行次数 x=

所以时间复杂度为:

这里也可以写成log n

       当N接近无穷大时,底数的大小对结果影响不大。因此,一般情况下不管底数为多少都可以省略不写,写成log n

2.2.7、示例七

long long Fac(size_t N)
{
  if (0 == N)
    return 1;
  return Fac(N - 1) * N;
}

这里每一次调用Fac函数的时间复杂度为O(1)

       而一共有n次递归,所以阶乘递归的时间复杂度为O(N)

三、空间复杂度

       空间复杂度也是一个数学表达式,是对一个算法在运行过程中因为算法的需要额外临时开辟的空间

       空间复杂度表示程序占用了多少bytes的空间,因为常规情况下每个对象大小 差异不会很大,所以空间复杂度计算的是变量的个数

       空间复杂度也使用大O渐进表示法

       这里函数运行时所需要的栈空间(存储参数,局部变量。一些寄存器信息等)在编译期间已经确定好了,空间复杂度主要通过函数在运行时显示申请的额外空间来确定

空间复杂度计算

示例一

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;
  }
}

函数栈帧在编译期间已经确定好了,只需关注函数在运行时额外申请的空间

BubbleSort额外申请的空间有exchange等有限个局部变量,使用了常数个额外的空间

      因此时间复杂度为:O(1)

示例二

long long Fac(size_t N)
{
  if (N == 0)
    return 1;
  return Fac(N - 1) * N;
}

Fac递归调用了N次,额外开辟了N个函数栈帧,每个栈帧使用了常数个空间

       因此时间复杂度为:O(N)

感谢各位大佬支持并指出问题,


相关文章
|
30天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
47 1
|
2月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
97 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
1月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
1月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
1月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
107 23
|
1月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
62 20
|
1月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
53 0
|
2月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
49 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
1月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
53 0
|
2月前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
52 4