【C语言数据结构与算法(基础)】引言部分(数据结构的概念及内容、时间复杂度和空间复杂度的计算等)

前言

       最近半个月,我将对数据结构与算法进行基础学习,算是开学考试的备考波,会进行一些基础知识点的简单总结,放在这里算是对自己学习的一个激励,如果对大家有用那就更开心啦!本篇主要为数据结构与算法引言部分的内容,快快一起来学习叭!

一、一些概念

首先介绍几个与数据结构相关的重要概念,简单复习一下波。

1.数据:数据是描述客观事物的数值、字符以及能够输入计算机且能被处理的各种符号集合。

2.数据元素:数据元素是组成数据的基本单位,是数据集合的个体。(也称元素、结点、顶点、记录等)

3.数据项:数据项是数据不可分割的最小单位。

注:数据>数据元素>数据项

例:通讯类为数据,通讯录里每个人的信息为数据元素,而通讯录里的个人的姓名为数据项。

4.数据对象:数据对象是性质相同的数据元素的集合,是数据的一个子集。

5.数据结构:数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。

6.数据类型:数据结构是一组性质相同的值集合,以及定义在这个值集合上的一组操作的总称。

二、数据结构的内容

       数据结构的内容可归纳为三部分:逻辑结构、存储结构和运算集合。这里我们通过结构图更加清晰地理解和学习数据结构的内容。

 三、时间复杂度和空间复杂度

1.时间复杂度

(1)定义

        在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

(2)计算方法:大O的渐进表示法

  我们首先来看一个例子:

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

       上面我们已经了解到时间复杂度为算法中的基本操作的执行次数,那么我们来分析一下这个例子的准确执行次数。上面嵌套循环为N的平方次,加上下面for循环为2*N次、while循环为10次,即总共执行N²+2*N+10次。观察准确执行次数结果的这个表达式,我们发现,随着N的增大,表达式中N²对整个表达式结果的影响最大。实际中我们计算时间复杂度,并不一定要计算准确执行次数,而只需要一个大概的执行次数,也就是说在计算时间复杂度时是计算一个估算值,只去看表达式中影响最大的那一项即可,这里我们使用大O的渐进表示法

大O符号:用于描述函数渐进行为的数学符号。

推导大O阶方法:

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

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

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

而上面那个例子,我们使用大O的渐进法表示以后,时间复杂度为O(N²)。

注:时间复杂度一般情况关注的是算法的最坏运行情况。

(3)常见时间复杂度计算举例

例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的准确执行次数为2*N+10次,通过推导大O阶方法可知,时间复杂度为O(N)

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

       直接观察代码基本操作执行了M+N次,通过推导大O阶方法可知,时间复杂度为O(M+N)。此题目中并未给出M、N的大小条件,若M远大于N最终时间复杂度为O(M),若N远大于M最终时间复杂度为O(N),若M约等于N最终时间复杂度为O(M)或O(N)。

例3:

//计算Func4的时间复杂度?
void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++k;
	}

	printf("%d\n", count);
}

       直接观察代码Func4的基本操作准确执行次数为100次,通过推导大O阶方法可知,时间复杂度为O(1)

例4:

//计算strchr的时间复杂度?
const char *strchr(const char * str, char character)
{
	while (*str != '\0')
	{
		if (*str == character)
			return str;

		++str;
	}

	return NULL;
}

         此题我们假设字符串长度为N,基本操作最好的情况是执行一次直接查找到,最坏的情况是N次,时间复杂度一般看最坏的情况,所以时间复杂度为O(N)

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

        观察代码为冒泡排序算法,以最坏情况考虑,冒泡排序第一趟执行次数为N次,第二趟为N-2次,......,第N趟为1次,总共执行次数为(1+N)*N/2次,通过推导大O阶方法可知,时间复杂度为O(N²)

例6:

//计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
	assert(a);

	int begin = 0;
	int end = n;
	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;
}

      观察代码为二分查找算法,也称折半查找。以最坏情况考虑,二分查找每次排除掉一半值,所以对于一组N个元素的情况,我们假设找了x次,一次二分剩下:N/2,两次二分剩下N/2/2 = N/4...x次二分剩下:N/(2的x次方),在最坏情况下是在排除到只剩下最后一个值之后得到结果N/(2的x次方),即2的x次方=N,算法的复杂度计算喜欢省略底数写为logN,所以时间复杂度为O(logN)

例7:

//计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
	return N < 2 ? N : Facterial(N - 1) * N;
}

        观察代码我们可以发现,递归调用了N次 ,每次递归运算为常数次即时间复杂度为O(1),所以总的时间复杂度为O(N)

 

 总结:

          ①写时间复杂度一定要记得写O,表示估算的意思

          ②计算时间复杂度最好通过算法过程分析,不是说一层循环就是N,两层循环就是N²。

          ③有些算法存在最好、平均、最坏情况,时间复杂度一般关注的是最坏的情况

          ④常见的时间复杂度:O(N²)  O(N)  O(logN)  O(1)

 2.空间复杂度

(1)定义

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

(2)计算方法

     大O的渐进表示法(与时间复杂度计算规则类似)

(3)常见空间复杂度计算举例

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

         观察代码发现,一共创建了常数个变量,使用了常数个额外空间,所以空间复杂度为O(1)。在这里我们要注意,时间是累计的而空间是不累计的,所以即使循环走了N次,但是重复了利用一个空间。

例2:

//计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{
	return N < 2 ? N : Facterial(N - 1) * N;
}

        观察代码发现,递归调用了N次,每次调用建立了一个栈帧,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度为O(1),所以总的空间复杂度为O(N)。注意递归调用时建立栈帧,返回时销毁栈帧,同样计算最坏情况即建立栈帧时的空间复杂度。

例3:

// 计算Fibonacci的空间复杂度?
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个变量的数组,再加上其他5个变量,一共开辟了n+6个空间,所以空间复杂度为O(N)

写在最后

       本篇重点应放在时间复杂度和空间复杂度计算的学习与理解,我们在写一些算法题时,可能会对时间复杂度与空间复杂度有要求。我们下次见咯~~~

猜你喜欢

转载自blog.csdn.net/weixin_62604754/article/details/128778098