【数据结构与算法】揭秘数据结构与算法中的复杂度:你真的了解它们吗?

大家好,我是小卡皮巴拉 

文章目录 

目录

一.数据结构前言

1.1 数据结构

1.2 算法

1.3 数据结构和算法的重要性

二.算法效率 

2.1 复杂度的概念 

2.2 复杂度的重要性

三.时间复杂度

3.1 大O的渐进表示法 

3.2 时间复杂度计算示例

3.2.1 示例1

3.2.2 示例2 

3.2.3 示例3 

3.2.4 示例4 

3.2.5 示例5 

3.2.6 示例6 

3.2.7 示例7 

四.空间复杂度 

4.1 空间复杂度计算示例

4.1.1 示例1

4.1.2 示例2 

五. 常见复杂度对比

常见的时间复杂度类型

六. 复杂度算法题 

后记 

共勉!!!


每篇前言 

博客主页:小卡皮巴拉 

咱的口号:🌹小比特,大梦想🌹

作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。

一.数据结构前言

1.1 数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用,所以我们要学各式各样的数据结构,如:线性表、树、图、哈希等

1.2 算法

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

1.3 数据结构和算法的重要性

数据结构与算法是计算机科学的基础,它们对于提高系统性能、优化资源利用、增强开发效率以及灵活解决问题至关重要。数据结构决定了数据的存储和访问方式,而算法则提供了解决问题的步骤和方法。两者共同决定了计算机程序的效率和可靠性,是计算机科学领域中不可或缺的核心概念。

以最功利的角度来说,目前互联网行业在对于程序员的面试中都必定需要手撕算法题来作为面试筛选的一大标准,这一切的一切,都告诉我们数据结构与算法是无比重要的,我们需要以最认真的态度来学习它,并付出辛勤的汗水去练习它。

这两个图就是我们需要肝到的境界:

 

 

二.算法效率 

如何衡量⼀个算法的好坏呢?

案例:旋转数组. - 力扣(LeetCode)

思路:循环K次将数组所有元素向后移动⼀位

void rotate(int* nums, int numsSize, int k) 
{
   while(k--)
   {
       int end = nums[numsSize-1];
       for(int i = numsSize - 1;i > 0 ;i--)
       {
           nums[i] = nums[i-1];
       }
       nums[0] = end;
   }
}

该代码点击执行可以通过,然而点击提交却无法通过,那该如何衡量其好与坏呢?

为了衡量算法的好坏,我们引入复杂度的概念。

2.1 复杂度的概念 

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量⼀个算法的好 坏,⼀般是从时间和空间两个维度来衡量的,即时间复杂度空间复杂度

时间复杂度主要衡量⼀个算法的运行快慢,而空间复杂度主要衡量⼀个算法运行所需要的额外空间。 在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注⼀个算法的空间复杂度。 

2.2 复杂度的重要性

复杂度的重要性不言而喻,很多面试题,笔试题中都会涉及到,所以我们更要深入的理解它,学习好它,掌握好它,为以后的面试,笔试,以及算法题打好基础。

三.时间复杂度

定义:在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运行时间。 这个T(N)函数式计算了程序的执行次数

那么我们通过程序代码或者理论思想计算出程序的执行次数的函数式T(N),假设每句指令执行时间基本⼀样(实际中有差别,但是微乎其微),那么执行次数和运行时间就是等比正相关, 这样也脱离了具体的编译运行环境。执行次数就可以代表程序时间效率的优劣。比如解决⼀个问题的算法a程序T(N) = N,算法b程序T(N) = N^2,那么算法a的效率一定优于算法b。

下面我们通过一个案例来加以佐证:

// 请计算⼀下Func1中++count语句总共执⾏了多少
次?
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;
   }
}

 Func1 执行的基本操作次数: T (N) = N^2 + 2 ∗ N + 10

• N = 10     T(N) = 130

• N = 100   T(N) = 10210

• N = 1000 T(N) = 1002010

通过对N取值分析,对结果影响最大的一项是 N^2

实际中我们计算时间复杂度时,计算的也不是程序的精确的执行次数,精确执行次数计算起来还是很麻烦的(不同的⼀句程序代码,编译出的指令条数都是不⼀样的),计算出精确的执行次数意义也不大, 因为我们计算时间复杂度只是想比较算法程序的增长量级,也就是当N不断变大时T(N)的差别,上面我们已经看到了当N不断变大时常数和低阶项对结果的影响很小,所以我们只需要计算程序能代表增长量级的大概执行次数复杂度的表示通常使用大O的渐进表示法。 

3.1 大O的渐进表示法 

大O符号(Big O notation):是用于描述函数渐进行为的数学符号 

推导大O阶规则:

1. 时间复杂度函数式T(N)中,只保留最高阶项,去掉那些低阶项,因为当N不断变大时, 低阶项对结果影响越来越小,当N无穷大时,就可以忽略不计了。

2. 如果最高阶项存在且不是1,则去除这个项目的常数系数,因为当N不断变大,这个系数 对结果影响越来越小,当N无穷大时,就可以忽略不计了。

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

通过以上方法,可以得到 Func1 的时间复杂度为: O(N)

3.2 时间复杂度计算示例

3.2.1 示例1

// 计算Func2的时间复杂度?
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);
}

Func2执行的基本操作次数:

T (N) = 2N + 10

根据推导规则第3条得出

Func2的时间复杂度为: O(N)

3.2.2 示例2 

// 计算Func3的时间复杂度?
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);
}

Func3执行的基本操作次数:

T (N) = M + N

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

3.2.3 示例3 

// 计算Func4的时间复杂度?
void Func4(int N)
{
   int count = 0;
   for (int k = 0; k < 100; ++ k)
   {
      ++count;
   }
   printf("%d\n", count);
}

 Func4执行的基本操作次数:

T (N) = 100

根据推导规则第1条得出

Func2的时间复杂度为: O(1)

3.2.4 示例4 

// 计算strchr的时间复杂度?
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执行的基本操作次数:

1)若要查找的字符在字符串第⼀个位置,则: T (N) = 1

2)若要查找的字符在字符串最后的⼀个位置, 则: T (N) = N

3)若要查找的字符在字符串中间位置,则: T (N) = 2 N

因此:strchr的时间复杂度分为:

最好情况: O(1)

最坏情况: O(N)

平均情况: O(N)

总结 

通过上面我们会发现,有些算法的时间复杂度存在最好、平均和最坏情况。

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

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

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

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

3.2.5 示例5 

// 计算BubbleSort的时间复杂度?
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执行的基本操作次数:

1)若数组有序,则: T (N) = N

2)若数组有序且为降序,则: T (N) = 2 N ∗ (N + 1)

3)若要查找的字符在字符串中间位 置,则:

因此:BubbleSort的时间复杂度取最差情况为: O(N )

3.2.6 示例6 

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

当n=2时,执行次数为1

当n=4时,执行次数为2

当n=16时,执行次数为4

假设执行次数为 x ,则 2 ^ x = n

因此执行次数: x = log n

因此:func5的时间复杂度取最差情况为: O(log n)

3.2.7 示例7 

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
   if(0 == N)
      return 1;
   return Fac(N-1)*N;
}

调用⼀次Fac函数的时间复杂度为 O(1)

而在Fac函数中,存在n次递归调用Fac函数

因此: 阶乘递归的时间复杂度为: O(n)

四.空间复杂度 

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

空间复杂度不是程序占用了多少bytes的空间,因为常规情况每个对象大小差异不会很大,所以空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。 

注意:函数运行时所需要的栈空间(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定 

4.1 空间复杂度计算示例

4.1.1 示例1

// 计算BubbleSort的空间复杂度?
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)

4.1.2 示例2 

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
   if(N == 0)
      return 1;
   return Fac(N-1)*N;
}

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

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

五. 常见复杂度对比

常见的时间复杂度类型

  • 常数时间复杂度 O(1):算法的运行时间不随输入数据规模n的变化而变化,例如访问数组中的某个元素。
  • 线性时间复杂度 O(n):算法的运行时间与输入数据规模n成正比,例如遍历一个数组。
  • 平方时间复杂度 O(n^2):算法的运行时间与输入数据规模的平方成正比,例如嵌套循环遍历二维数组。
  • 对数时间复杂度 O(log n):算法的运行时间与输入数据规模的对数成正比,例如二分查找。
  • 指数时间复杂度 O(2^n):算法的运行时间随输入数据规模呈指数级增长,通常这类算法在处理大规模数据时效率极低。
  • 多项式时间复杂度 O(n^k):其中k为常数,表示算法的运行时间与输入数据规模的k次方成正比。
  • 线性对数时间复杂度 O(n log n):结合了线性时间和对数时间的特点,例如快速排序算法。

六. 复杂度算法题 

旋转数组:. - 力扣(LeetCode)

思路1:

时间复杂度 O(n^2)

循环K次将数组所有元素向后移动一位(代码不通过)

void rotate(int* nums, int numsSize, int k) 
{
   while(k--)
   {
      int end = nums[numsSize-1];
      for(int i = numsSize - 1;i > 0 ;i--)
      {
          nums[i] = nums[i-1];
      }
      nums[0] = end;
   }
}

思路2:

空间复杂度 O(n)

申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中

void rotate(int* nums, int numsSize, int k)
{
   int newArr[numsSize];
   for (int i = 0; i < numsSize; ++i)
   {
      newArr[(i + k) % numsSize] = nums[i];
   }
   for (int i = 0; i < numsSize; ++i)
   {
      nums[i] = newArr[i];
   }
}

思路3:

空间复杂度 O(1)

• 前n-k个逆置: 4 3 2 1 5 6 7

• 后k个逆置 :4 3 2 1 7 6 5

• 整体逆置 : 5 6 7 1 2 3 4 

void reverse(int* nums,int begin,int end)
{
   while(begin<end)
   {
      int tmp = nums[begin];
      nums[begin] = nums[end];
      nums[end] = tmp;
      begin++;
      end--;
   }
}
void rotate(int* nums, int numsSize, int k)
{
   k = k%numsSize;
   reverse(nums,0,numsSize-k-1);
   reverse(nums,numsSize-k,numsSize-1);
   reverse(nums,0,numsSize-1);
}

后记 

又是许久未更的摆烂日常,不过既然休假结束了,就要卷起来了

共勉!!!

码字不易,求个三连

抱拳了兄弟们! 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值