数据结构之时间复杂度与空间复杂度

一、数据结构与算法

一、什么是数据结构

数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。简单来说,数据结构就是对数据进行管理(增删查改)的一系列操作。数据结构分为线性数据结构和非线性数据结构。

二、什么是算法

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

数据结构和算法是相辅相成的,二者是我中有你、你中有我的关系:在一个数据结构中可能会用到算法来优化,一个算法中也可能用到数据结构来组织数据。

二、算法的复杂度

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,通常由循环来决定的,而空间复杂度主要衡量一个算法所需要的额外空间,通常由空间的开辟大小决定的

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

三、时间复杂度

一、时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。(这里的函数指的是数学中的函数,而不是我们C语言中的函数)

一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

通俗来说,算法中基本操作的执行次数(循环部分)就是代表了该算法的时间复杂度。

 二、时间复杂度的表示方法

我们是用大O渐进表示法的,什么是大O渐进表示法呢:

算法的时间复杂度通常用大O符号表述,定义为T[n] = O(f(n))。称函数T(n)以f(n)为界或者称T(n)受限于f(n)。 如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n)。T(n)称为这一算法的“时间复杂度”。当输入量n逐渐加大时,时间复杂度的极限情形称为算法的“渐近时间复杂度”。

大O符号:是用来描述函数渐进行为的数学符号,这个符号有点像数学中取极限

我们计算时间复杂度时不是计算算法运行的具体次数,而是用大O的渐进表示法来计算,其具体计算方法如下:

  • 用常数1取代运行时间中的所有加法常数。
  • 在修改后的运行次数函数中,只保留最高阶项。
  • 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。

三 、算法复杂度的三种情况

算法复杂度分为三种情况 

  • 最坏情况:任意输入规模的最大运行次数(上界)
  • 平均情况:任意输入规模的期望运行次数
  • 最好情况:任意输入规模的最小运行次数(下界)

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

最好情况:1次找到
平均情况:N/2次找到
最坏情况:N次找到
平均情况:N/2次找到

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

四、时间复杂度的例题示范

题目一:

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

答案:O(1)

上面程序具体执行的次数:100
用大O的渐进表示法得出时间复杂度:O(1) (用常数1取代运行时间中的所有加法常数。)

题目二:


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

 答案:O(N + M)
因为这里有两次循环,并且 N 和 M 都是未知数,无法区分出谁是最高阶项,因此两个都取出,都没有带常数项,不做去除操作。综上 Func1 的时间复杂度就是 O(N + M)

题目三:

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

答案:O(N^2)

上面程序具体执行的次数:N * N + 2*N + 10
用大O的渐进表示法得出时间复杂度:O(N^2) (只保留最高阶项)

题目四:

//计算strchr的时间复杂度?
const char* strchr(const char* str, int character);

答案: O(N)
说明:strchr 是一个字符串寻找函数,作用是在字符串str中查找目标字符character
有三种情况:

最好的情况,只找一次,此时的时间复杂度为 O(1)
最坏的情况,没有目标字符,需要把整个字符串找一遍,时间复杂度为 O(N)
平均的情况,在中间就找到了,时间复杂度是 O(N / 2)
面对这种多分支情况,我们要做预期管理,用最悲观的态度来判断程序,这样做的好处是预期值低,结果出来时不会有很大落差,生活中也可以像这样,做好准备。言归正传,这里选择最坏的情况,即 O(N),当然这种情况比较特殊,值得注意一下

题目五:

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

答案:O(N)

上面程序具体执行的次数:2*N + 10
用大O的渐进表示法得出时间复杂度:O(N) (如果最高阶项存在且不是1,则去除与这个项目相乘的常数

题目六:冒泡排序的时间复杂度 

void  BubbleSort(int arr[], int n)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < n - 1; i++)
	{
		for (j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

 答案: O(N ^ 2)
冒泡排序是一个神奇的算法,每次冒泡比较的趟数都不同,可以这样推导

第一趟:比较 N - 1 次
第二趟:比较 N - 2 次
第三趟:比较 N - 3 次
…………
最后一趟: 比较 1 次
总共比较的次数,就是时间复杂度,即 (N - 1) + (N - 2) + …… + 1,显然这是一个首项为 N - 1,尾项为 1 的等差数列,并且共有 N - 1 项,把高中学的知识用起来,N - 1 项和为 (N - 1) * N / 2 ,通过 大O渐进表示法 进行计算,最终结果为 O(N ^ 2)

冒泡排序的时间复杂度需要重点记忆:O(N^2)

题目七:二分查找的时间复杂度

int BinarySearch(int arr[], int n, int x) //n元素个数  x:要查找的数
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > x)
		{
			right = mid - 1;     //中间元素比x大就挪动right下标
		}
		else if (arr[mid] < x)
		{
			left = mid + 1;    //中间元素比x小就挪动left下标
		}
		else
			return mid;     //找到就返回该元素所在的下标
	}
	return 0;               //找不到就返回0
}

 答案:O(log2)

假设需要查找的次数为 k 次,那么可以这样写

N / 2 -> N / 2 ^ 1
N / 4 -> N / 2 ^ 2
N / 8 -> N / 2 ^ 3
…………
其中,左边的序号就是查找的次数 k ,可得出式子 N = 2 ^ k ,稍微变换下,得到 k = logN,其中第二个式子就是二分查找的时间复杂度,

注:因为在键盘上无法表示出log的底数,所有在时间复杂度中把log的底数2省略掉了,直接用logN表示log2N的时间复杂度。

题目八:斐波那契数列时间复杂度(递归)

long long Fibonacci(size_t N)
{
	return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}

答案为:O(2^N)

我们可以将递归斐波那契数列水平展开,即 1+2+4+8+16+32+……+2^N
去除影响小的常数项,最终结果为 O(2 ^ N) 

五、空间复杂度

一、空间复杂度的概念 

 算法的空间复杂度是指临时占用储存空间大小的量度,空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟时间复杂度类似,也使用大O的渐进表示法。

二、空间复杂度的计算方法 

跟时间复杂度计算基本相似

三、常见空间复杂度的计算

题目一:冒泡排序的空间复杂度 

void  BubbleSort(int arr[], int n)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < n - 1; i++)
	{
		for (j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

答案: O(1)

这里我们在循环外部定义了两个变量,然后在循环内部又定义了一个变量;可能有的同学会认为temp变量因为在循环内部,每次进入循环都会被重新定义,所以空间复杂度为N^2,其实不是的;

我们知道虽然时间是累积的,一去不复返,但是空间是不累积的,我们可以重复使用;对于我们的temp变量来说,每次进入if这个局部范围时开辟空间,离开这个局部范围时空间销毁,下一次在进入时又重新开辟空间,出去又再次销毁;所以其实从始至终temp都只占用了一个空间;

所以上面一共一共定义了三个变量,用大O的渐进表示法得到空间复杂度为O(1)。

题目二:二分查找的空间复杂度

int BinarySearch(int arr[], int n, int x) //n元素个数  x:要查找的数
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] > x)
		{
			right = mid - 1;     //中间元素比x大就挪动right下标
		}
		else if (arr[mid] < x)
		{
			left = mid + 1;    //中间元素比x小就挪动left下标
		}
		else
			return mid;     //找到就返回该元素所在的下标
	}
	return 0;               //找不到就返回0
}

答案:O(1)

和冒泡排序的空间复杂度一样,这里只定义了三个(常数个)变量,所以空间复杂度是O(1)。 

题目三: 斐波那契数列的空间复杂度(递归)

long long Fibonacci(size_t N)
{
	return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}

 答案:O(N)

递归的规则是先递出,再回归,如果中途遇到递归,继续递出,因此在计算递归的空间复杂度时,计算的是每次递归调用的变量个数相加(所开辟的空间),也可以看作递归的深度显然这里的递归深度是 N,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度自然就是 O(N) 了

六、总结 

时间复杂度和空间复杂度都是用大O的渐进表示法来表示。

时间复杂度看运算执行的次数,空间复杂度看变量定义的个数。

在递归中,时间复杂度看调用的次数,空间复杂度看调用的深度。

时间是累积的,一去不复返;空间是不累积的,可以重复利用。

 

冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1)。

二分查找的时间复杂度为O(logN),空间复杂度为O(1)。

斐波那契递归的时间复杂度为O(2^N),空间复杂度为O(N)。

 

 

 

 

 

 

 

 

 

 

 

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