速通数据结构与算法第一站 复杂度

本文介绍了时间复杂度和空间复杂度的基本概念,通过实例演示如何计算常见算法如冒泡排序、二分查找、递归等的复杂度,强调了在算法设计中考虑最坏情况的重要性。同时,对比了时间复杂度与空间复杂度的区别,指出空间可以反复利用的特点。
摘要由CSDN通过智能技术生成

系列文章目录

速通数据结构与算法系列

速通数据结构与算法第一站 复杂度        

感谢佬们支持!


目录

系列文章目录

  • 前言
  • 一、时间复杂度
  •          例1
  •          例2
  •          例3
  •          例4
  •          例5
  •          例6
  •          例7
  •          例8
  •          例9
  • 二、空间复杂度
  •          例1
  •          例2
  •          例3
  • 总结

前言

         终于,我们在上一篇博客结束了C语言的学习(当然后续还会再深入学习),我们可以进行数据结构的学习。数据结构是一名很重要的课,不仅是对于我们算法的提升,作为408四大件之一

学好数据结构对于理解操作系统的底层等很重要。这一节我们先来看最简单的复杂度问题


 衡量一个算法的好坏,一般会从花了多少时间和用了多少额外空间来看,以此诞生了两大标准:时间复杂度和空间复杂度

一、时间复杂度

我们先来给出时间复杂度的定义

在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。

直接计算运行时间非常的麻烦,不仅需要多次的上机测试,而且不同配置的电脑和网络的波动都可能影响时间的计算。由此引入了时间复杂度这种分析方式,一个算法花费的时间与其执行语句的次数成正比,所以我们称算法中基本语句的执行次数为一个算法的时间复杂度

在实际运算中,我们并不需要算出精确的次数,而是大概的次数就行,这称为大O渐进法

实际计算中,有如下规则

  1.  保留精确次数中影响最大的
  2.  以常数1取代时间中的所有常数
  3. 如果最高阶存在且不为1,则去除这个项目中相乘的常数,得到的结果即为大O阶

多的不说,我们先给一波例子


下面的代码中,test1函数中,++count执行了多少次呢

例1

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

 由题易得

F(N)=N^2 + 2*N + 10

套用上面的规则,我们知道,最后时间复杂度是O(n^2)


再来看下一个例子

例2

void test2(int N)
{
		int count = 0;
		for (int k = 0; k < 2 * N; ++k)
		{
			++count;
		}

		int M = 10;
		while (M--)
		{
			++count;
		}
	
}

此时就是O(n)了


再例 有的时候复杂度的计算涉及不止一个变量

例3

void test3(int N,int M)
{
	int count = 0;
	for (int k = 0; k <  N; ++k)
	{
		++count;
	}

	for (int k = 0; k < M; ++k)
	{
		++count;
	}
}

此时为O(M+N)


再例

例4

void test4()
{
	int count = 0;
	int M = 100;
		while (M--)
		{
			count++;
		}
}

此时由于只做了常数次运算,所有是O(1)

所以之后如果让你讲算法优化至O(1),不是让你做1次运算,而是做常数次运算


大多数的时候算法的计算并非如此简单,而是根据不同的情况而不同

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

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

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

复杂度是个悲观的算法,我们一般更关注算法的最坏情况


例5

介绍一个函数strchr

定位字符串中要查字符的首个出现位置,并返回一个指向它的指针

它的实现大致如下

const char* strchr(const char* str, int character)
{
	while (*str)
	{
		if (*str == character)
			return str;
		else
			++str;
	}
	return str;
}

我们可以简单计算一下他的时间复杂度

比如我们给到一个字符串hello world,分3种情况:

1 查h:显然我们第一次就能查到,是最好的情况,也就是O(1)

2 查w:由于w在字符串的中间,所以遍历至字符串的中间时能查到,是平均的情况O(N/2)也就是O(N)

3 查d :我们最后一次才能查到,是最坏的情况,O(N)。

由于我们关注最坏的情况,所以最终是O(N)


例6

计算冒泡排序的复杂度?

void bubbleSort(int* arr, int size)
{
	assert(arr);
	for (int i = 0; i < size; ++i)
	{
		for (int j = i + 1; j < size; ++j)
		{
			if (arr[i] > arr[j])
			{
				int temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
}

数一下循环,第一次是size次,第二次是size-1,……最后是1,这是一个等差数列,最后循环

n*(n-1)/2次,也就是O(N方)

熟练以后我们看到双for循环,一般是典型的O(n方)

虽然但是,我们的计算结果来自于对冒泡排序思想的理解,所以复杂度的计算不能浮于表面,要关注到代码的思想


例7

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

int BinarySearch(int* a, int n, int x)
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mid = begin + ((end - begin) >> 1);
		if (a[mid] < x)
		{
			begin = mid + 1;
		}
		else if (a[mid] > x)
		{
			end = mid;
		}
		else
		{
			return mid;
		}
	}
	return -1;
}

由于二分查找的逻辑是找一次减一半,即除2,设a的size为N的话,N=2^x,所以x=logN(以二为底但是可以忽略不记)


例8

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

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

众所周知递归是一个不断建立栈帧的过程,上面的例子中是这样

每层建立一个栈帧,每个栈帧中比较为一次,计算为一次,仍然为常数次运算

所以时间复杂度给到的是O(n)


例9

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

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

	return Fib(N - 1) + Fib(N - 2);
}

还是和上面一样,我们先画递归展开图

显然,右边有几个分支结束的早,不过我们仍然可以将上面看作一颗完全二叉树,所以他的时间复杂度是O(2^n)

由此可见,用递归的方式求斐波那契是非常慢的,还有可能出现栈溢出的问题。


二、空间复杂度

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

,同理于时间复杂度,空间复杂度的计算也用大O渐近法。
另外需要注意的是,函数运行时所需的栈空间(存取参数,局部变量,一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数运行时显示申请的额外空间来确定


例1

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

void bubbleSort(int* arr, int size)
{
	assert(arr);
	for (int i = 0; i < size; ++i)
	{
		for (int j = i + 1; j < size; ++j)
		{
			if (arr[i] > arr[j])
			{
				int temp = arr[i];
				arr[i] = arr[j];
				arr[j] = temp;
			}
		}
	}
}

显然,在这段代码中我们只定义了i,j两个变量,

循环结束时,i,j销毁,然后再重新建立,用的还是那俩块空间

所以空间复杂度为O(1)


例2

计算返回斐波那契数列的前n项算法的空间复杂度?

long long* Fibnacci(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 (0 == N)
	{
		return 1;
	}
	return Fac(N - 1) * N;
}

我们还是看一下刚才画的递归展开图

每层建立一个栈帧而栈帧内部不定义临时变量,所以最终的空间复杂度是O(n)


例4

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

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

	return Fib(N - 1) + Fib(N - 2);
}

我们在计算之前首先要明确他的递归顺序:从Fib(n)这个节点由左一路向下递归直到Fib(n),此时递归结束,开辟了n个空间,空间销毁;再递归右边的时,依然会用刚才的空间

所以最终还是O(n)


至此我们可以做个总结,就是时间是一去不复返的,但是空间是可以反复利用的。

在上面的例子中无论是递归还是循环,变量,函数调用完后销毁,其占的空间也会又还回来,

下次接着用,而时间则不行。


总结

 做总结,作为速通数据结构与算法的第一篇,主要先给大家铺垫一下复杂度的相关知识,下一篇

将给大家带来第一个数据结构---顺序表。

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值