数据结构修炼第一篇:时间复杂度和空间复杂度

系列文章目录

第一章 时间复杂度和空间复杂度

第二章 顺序表,列表

第三章 栈和队列

第四章 二叉树

第五章 排序


目录

系列文章目录

文章目录

前言

一、算法的复杂度

二、时间复杂度的概念

大0渐进


作者:乐言

简介:大一学生,目前在致力于c/c++/python,高数的学习,有问题尽管问我,关注后私聊!

持续更新专栏:《c进阶》,《数据结构修炼》(优质好文持续更新中)

文章目录


前言

数据结构 (Data Structure) 是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的
数据元素的集合。而数据结构和算法在校招,考研,企业面试中,一般都占据大部分内容,占比极大,因此我们十分有必要学好数据结构
有人问,如何学好数据结构,我想说的是,学成这样就行了:

一、算法的复杂度

算法在编写成可执行程序之后,需要花费时间资源和空间资源,而这正式衡量一个算法好坏的标准之一。因此衡量一个算法的复杂度,通常是由这两方面决定的,即是时间复杂度和空间复杂度

时间复杂度主要指的是代码运行的快慢

空间复杂度主要指的是代码运行所耗费的内存

我们需要注意的是,在经过计算机的迭代发展后,计算机的储存容量已经达到了很高的程度,所以我们应该更加注意时间复杂度,空间复杂度已经并没有那么重要了

二、时间复杂度的概念

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

例如以下的代码:

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

当此时,我们明显可以发现count++执行了n^n+2*n+10次

n=10    N=130

n=100  N=10210

n=1000N=1002010

实际上我们计算时间复杂度的时候,并不需要如此精确,而是我们需要计算大概执行次数

我们通常使用大0渐进法

大0渐进

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

推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
  4. 使用大O的渐进表示法以后,Func1的时间复杂度为:

                                                                O(N^2)

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

此时,我们去掉了那些无关紧要的项,简洁明了的给出了执行次数

举例1:

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

当此时,时间复杂度就显得扑朔迷离了,我们无法知道M和N的大小关系,所以我们只能写成

O(M+N)

举例2:

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

当此时,我们明显的可以看出这显然是常数次,我们规定所有常数次都写成

O(1)

举例3:

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

该代码我们则可以明显看出执行了2n+10次,而根据刚刚所学,10对于2n来说显得微不足道,直接去掉即可,而当此时n趋向于无穷大时(可以这么理解),n的倍数也显得微不足道了,因此也可以不写,所以该代码的时间复杂度为:

        O(N)

举例4:

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

当此时,冒泡排序的次数为n!,因此时间复杂度,不计算/2,不计算+-1,我们可以得出时间复杂度为:

O(N^2)

举例5

int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号
while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}

该代码为二分查找法,我们可以知道二分查找每次都/2

那么次数是n/2/2/2.........每次都缩小一半

二分查找的最坏情况:只剩下一个值一共查找了N次

则2^x=N

则x=log2N

当我们暴力查找的时候,需要查找N次,差别巨大,二分查找是一个十分牛犇的算法

则该代码的时间复杂度为:

O(log2N)

举例6:

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

该函数明显是递归实现

fac的函数被调用了n+1次,每次都是常数次,所以时间复杂度仍然是

O(N)

举例7:

long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);

这是一个斐波那契函数的递归,我们可以画图如下

数据结构修炼第一篇:时间复杂度和空间复杂度_第1张图片

二叉树的层数一直是乘以2,每个都是O(1)

所以该时间复杂度为等比数列 

为:2^0 +2^1 +2^2 +2^3+..............+2^(N-2)=2(1-2^(N-1))/(1-2)

所以该函数的时间复杂度为:

O(2^N)

三:空间复杂度的概念

空间复杂度也是一个数学表达式,是对一个算法在运行过程中 临时占用存储空间大小的量度 。 空间复杂度不是程序占用了多少bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是 变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用 O 渐进表示法
注意: 函数运行时所需要的栈空间 ( 存储参数、局部变量、一些寄存器信息等 ) 在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

举例1:
 

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

此处end,exchange都是常数个,所以的空间复杂度都是

O(1)

举例2

long long* Fibonacci(size_t n)
{
 if(n==0)
 return NULL;
 
 long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
 fibArray[0] = 0;
 fibArray[1] = 1;
 for (int i = 2; i <= n ; ++i)
 {
 fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
 }
 return fibArray;
}

此处malloc开辟了一个数组,由N+1个空间,所以空间复杂度为

O(N)

举例3:

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

此处涉及到函数栈帧的创建,如图所示:

数据结构修炼第一篇:时间复杂度和空间复杂度_第2张图片

 每个栈帧为常数个,且有n个栈帧,则有空间复杂度为;

O(N)

举例4:

long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

 他的空间复杂度为

O(N)

数据结构修炼第一篇:时间复杂度和空间复杂度_第3张图片

 函数递归的栈帧创建是往下创建的,再回来的时候会自动销毁,与原来的栈帧位于同一位置,所以该递归函数创建的栈帧仍然是N个,所以他的空间复杂度为:

O(N)

注意:     时间是不可以重复计算的

                空间是可以重复使用的

数据结构修炼第一篇:时间复杂度和空间复杂度_第4张图片

我们在调试窗口的调用堆栈,逐语句可以看出函数的调用堆栈的进程 

函数每次调用空间后销毁,并不是说这块空间不存在了,而是归还这块空间的使用权,内存是属于操作系统的,每次函数调用结束之后,会将空间的使用权归还,这块空间是永远存在的

类比:

进程           ------->垃圾桶

申请空间---------->扔垃圾

销毁内存----------->倒垃圾

越界/野指针----------->访问了不是属于自己的空间

四:常见复杂度的对比

数据结构修炼第一篇:时间复杂度和空间复杂度_第5张图片

而这些复杂度的复杂程度也相差很大:;

数据结构修炼第一篇:时间复杂度和空间复杂度_第6张图片


总结

本文主要讲解了时间复杂度和空间复杂度的内容,并且举例了例题讲解,需要老铁们继续加油,多做点题目积累,同时也希望大家多多支持,更多好文,敬请期待.........

你可能感兴趣的:(数据结构,数据结构,c++,c语言,算法)