【数据结构】— 「时间复杂度」与「空间复杂度」

   ꧁   各位大佬们好!很荣幸能够得到您的访问,让我们一起在编程道路上任重道远!꧂

☙ 博客专栏:【数据结构初阶】

⛅ 本篇内容简介:数据结构初阶中的时间复杂度与空间复杂度的详解!

⭐ 了解作者:励志成为一名编程大牛的学子,目前正在升大二的编程小白。

励志术语:编程道路的乏味,让我们一起学习变得有趣!

✂   正文开始


文章目录

☞ 什么是数据结构?

☞ 什么是算法?

☞ 数据结构与数据库的区别

☞ 如何学好数据结构和算法

▶ 算法效率

☞ 如何衡量一个算法的好坏

☞ 算法的复杂度

☞ 时间复杂度的概念

☞ 大O的渐进表示法

⭑ 推到大O阶的方法

☞ 常见时间复杂度计算举例

⭑ 实例 1

⭑ 实例 2

⭑ 实例 3

⭑ 实例 4

⭑ 实例 5

⭑ 实例 6

⭑ 实例 7

⭑ 实例 8

☞ 空间复杂度

☞ 常见空间复杂度计算举例

⭑ 实例 1

⭑ 实例 2

⭑ 实例 3

☞ 常见复杂度对比

 ☞ 结束语


☞ 什么是数据结构?

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

    通俗说法:就是数据在内存中的存储方式,以及如何在内存中管理数据。

☞ 什么是算法?

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

☞ 数据结构与数据库的区别

相同点:都是在管理数据的内容,都进行的是 增删查改 。

不同点:数据结构是在内存中进行数据的管理,而数据库是在磁盘中对数据进行管理。

☞ 如何学好数据结构和算法

① 死敲代码,把自己的代码量提升上去,对C语言中的 动态内存管理、指针、结构体等知识了如指掌就差不多了。那敲代码.....敲成什么样才算好呢,我想应该是这样(哈哈哈,玩笑话!):

【数据结构】— 「时间复杂度」与「空间复杂度」_第1张图片

 ② 多注意画图与思考,比如我们画一个链表的图:

【数据结构】— 「时间复杂度」与「空间复杂度」_第2张图片

 ③ 注意多刷题 比如:LeetCode和 牛客网 的题型都是不错的选择。

LeetCode链接:https://leetcode-cn.com/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china

牛客网链接:https://hr.nowcoder.com/?qhclickid=2abfa3675dda5302

▶ 算法效率

☞ 如何衡量一个算法的好坏

如何衡量一个算法的好坏呢?比如我们之前学习过的斐波那契数列:

long long Fib(int n)
{
	if (n < 3)
	{
		return 1;
	}
	else
		return Fib(n - 1) + Fib(n - 2);
}

如果我们用递归的方式实现求斐波那契数列,它的方式非常简洁,但是我们还可以用循环相加的方式去实现斐波那契数列,比如:

int a = 0;
int b = 1;
int c = 0;
while (1)
{
	c = a + b;
	a = b;
	b = c;
}

看,我们这样的方法去实现也是可以求出斐波那契数列的,但是怎么样去衡量哪个算法的好坏呢?我相信看完这篇博客,你就会知晓答案了!

☞ 算法的复杂度

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

时间复杂度主要衡量一个算法运行快慢,而空间复杂度主要衡量一个算法运行时所需要的而额外空间。在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度很是在乎。大家都知道摩尔定律吧,集成电路上可以容纳晶体管数目在大约每经过18个月便会翻一倍。经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度,所以我们如今已经不需要再特别关注一个算法的空间复杂度。

但是在校招的试题中,时间复杂度与空间复杂度依旧是作为限制的条件。

☞ 时间复杂度的概念

在计算机科学中,算法的时间复杂度是一个函数(这里的函数指的是在数学计算中的函数,不是代码过程中的函数),它定量描述该算法的运行时间。一个算法执行所消耗的时间,从理论上说,是算不出来的,只有你把程序放在机器上跑起来,才能知道。但是如果是这样,那每次的算法都要上机测试吗?太麻烦了,所以规定算法所花费的时间与其中的语句的执行次数成正比,算法中的基本操作的执行次数,为算法的时间复杂度。

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

我们来看一个例子:

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

问:在Fun1中count++语句共执行了多少次?

Fun1执行的基本操作次数: F(n)=n^2+2*n+10 

●  n=10             F(n)=130

●  n=100           F(n)=10210

●  n=1000         F(n)=1002010

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

☞ 大O的渐进表示法

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

⭑ 推到大O阶的方法

㊀  用常数1取代运行时间中的所有加法常数。

㊁  在修改后的运行次数函数中,只保留最高阶项。

㊂  如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。

如上面的例子,在使用大O的渐进表示法后,Fun1的时间复杂度为:O(N^2)

●  n=10             F(n)=100

●  n=100           F(n)=10000

●  n=1000         F(n)=1000000

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示了执行次数。

另外有些算法的时间复杂度存在最好、平均、最坏的情况:

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

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

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

例如:在一个长度为N的数组中搜索某一个数据x

最好情况:1次找到

最快情况:将数组遍历完了,从找到,即N次。

平均情况:N/2次找到。

在实际中一般情况关注的是算法最坏运行情况,所以数组中搜索数据的时间复杂度为O(N)。

☞ 常见时间复杂度计算举例

⭑ 实例 1

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

计算Fun2的时间复杂度?

基本操作执行了2*N+10次,通过推到大O阶方法可知道,时间复杂度为O(N)。

⭑ 实例 2

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

计算Fun3的时间复杂度?

基本操作执行了M+N次,有两个未知数M和N,所以时间复杂度为O(N+M)。

⭑ 实例 3

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

计算Fun4的时间复杂度?

基本操作执行了100次,通过推到大O阶方法,时间复杂度为O(1)。

⭑ 实例 4

const char* strchr(const char* str, int character);

计算strchr函数的时间复杂度?———— strchr功能是在字符串中查找某个字符

基本操作执行最好为1次,最坏为N次,取最坏的,时间复杂度为O(N)。

⭑ 实例 5

void BubbleSort(int* a, int n)
{
	assert(a);
	for (int end = n; end > 0; end--)
	{
		int flag = 0;
		for (int i = 1; i < end; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}

计算冒泡排序的时间复杂度?

基本操作执行最好为N次,最坏遍历 N,N-1,N-2......2,1。为等差数列求和为(N*(N+1))/2次,取最坏的,即时间复杂度为O(N^2)。

⭑ 实例 6

void BinarySearch(int* a, int n, int x)
{
	assert(a);
	int left = 0;
	int right = n - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (a[mid] < x)
		{
			left = mid + 1;
		}
		else if (a[mid] > x)
		{
			right = mid - 1;
		}
		else
			return mid;
	}
	//找不到
	return -1;
}

计算二分查找的时间复杂度?

我们来画个图:

【数据结构】— 「时间复杂度」与「空间复杂度」_第3张图片

 基本操作执行最好1次,最坏为O(logN)次,时间复杂度为O(logN)。即(lgN)。

⭑ 实例 7

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

计算阶乘递归Fac的时间复杂度?

基本操作递归了N次,时间复杂度为O(N)。

⭑ 实例 8

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

计算斐波那契递归Fib的时间复杂度?

画个递归的图:

【数据结构】— 「时间复杂度」与「空间复杂度」_第4张图片

 可以看出,基本操作递归了2^N次,时间复杂度为O(2^N)。

☞ 空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。

空间复杂度不是程序占用了多少bytes的空间,因为这个没有太大的意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则跟时间复杂度类型,也是用大O渐进表示法。

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

☞ 常见空间复杂度计算举例

⭑ 实例 1

void BubbleSort(int* a, int n)
{
	assert(a);
	for (int end = n; end > 0; end--)
	{
		int flag = 0;
		for (int i = 1; i < end; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		if (flag == 0)
			break;
	}
}

计算冒泡排序的空间复杂度?

使用了常数个的额外空间,空间复杂度为O(1)。

⭑ 实例 2

long long* Fib(int n)
{
	if (n == 0)
		return NULL;
	long long* fib = (long long*)malloc((n + 1) * sizeof(long long));
	fib[0] = 0;
	fib[1] = 1;
	for (int i = 2; i <= n; i++)
	{
		fib[i] = fib[i - 1] + fib[i - 2];
	}
	return fib;//返回fib的前n项
}

计算Fib的空间复杂度?

动态开辟了N个空间,空间复杂度为O(N)。

⭑ 实例 3

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

计算阶乘递归Fac的空间复杂度?

递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度为O(N)。

☞ 常见复杂度对比

一般算法常见的复杂度如下:

【数据结构】— 「时间复杂度」与「空间复杂度」_第5张图片

 【数据结构】— 「时间复杂度」与「空间复杂度」_第6张图片

 ☞ 结束语

希望这篇接近5000字的博客能让你深刻了解时间复杂度于空间复杂度,再一次感谢各位大佬的阅读,创作不易,给个三联吧!!!

你可能感兴趣的:(【数据结构初阶】,数据结构,c语言)