0
点赞
收藏
分享

微信扫一扫

C++关于树的基础知识

慎壹 2024-10-15 阅读 26

目录

1.认识数据结构

什么是数据结构

逻辑结构

物理结构

常见的数据结构

2.认识算法

什么是算法

如何衡量算法效率

时间复杂度

什么是时间复杂度

如何计算时间复杂度

大O渐进表示法

常见时间复杂度计算例子

空间复杂度

什么是空间复杂度

如何计算空间复杂度

常见空间复杂度计算例子

1.认识数据结构

什么是数据结构

计算机在被发明之初,其目的就是用来处理数据的,而且要处理的数据通常不是一个一个的,而是多个,有多个数据要被处理,计算机就需要将这多个数据组织并存储起来,以便高效地使用。那么,计算机组织并存储数据的方式就显得尤为重要了。于是,数据结构这门学科便诞生了。所以,数据结构是计算机组织、存储数据的方式。

而计算机组织、存储数据的方式通常涉及到两个方面 —— 逻辑结构、物理结构。

逻辑结构

逻辑结构描述的是数据元素之间的逻辑关系,即数据元素之间的关联方式。比如:一对一、一对多、多对多。

常见的逻辑结构:

物理结构

物理结构描述数据元素在计算机中的存储方式,即数据在计算机内存中的表示和布局。物理结构也称为存储结构

常见的物理结构:

常见的数据结构

2.认识算法

什么是算法

所谓算法,其实就是定义良好的计算过程,取一个或一组的值为输入,并产生出一个或一组值作为输出。

如何衡量算法效率

我们已经知道了算法其实就是一系列的计算步骤,也就是解决问题的方法,既然是方法,就有好的方法和坏的方法,那么如何衡量算法的好坏呢?

当算法被实现出来之后,其实就是程序,程序在运行的时候需要消耗时间资源空间资源,因此,衡量一个算法的好坏是从时间和空间两个方面来衡量的,也就是时间复杂度空间复杂度。

时间复杂度

什么是时间复杂度

一个算法所花费的时间与其中语句的执行次数成正比,但是算法中的语句往往比较多,所以,我们选择算法中的基本操作的执行次数,为算法的时间复杂度。

如何计算时间复杂度

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

我们可以计算一下给func函数传递不同的值时,++count语句共执行了多少次?

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

在实际计算时间复杂度时,我们并不一定要计算精确的执行次数,只需要计算大概的执行次数,也就是抓大头取决定性结果的哪一项;这时,就需要使用大O渐进表示法了。

大O渐进表示法

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

推导大O阶方法:

通过上述过程我们发现大O渐进表示法其实就是去掉了对结果影响不大的项,简洁明了的表示出了执行次数。

常见时间复杂度计算例子

例一:时间复杂度为O(n)

void func2(int n)
{
	int count = 0;
	
	for (int k = 0; k < 2 * n ; ++k)
	{
		++count;
	}
	
	int m = 10;
	while (m--)
	{
		++count;
	}
}

该函数中的基本语句是 ++count,如果我们精确计算基本语句的执行次数,则执行次数为2n+10;使用大O渐进表示法表示出的时间复杂度为O(n)。 

例二:时间复杂度为O(m+n)

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

在该函数中,由于我们并不清楚m和n的具体情况,所以我们并不能确定m和n谁才是大头,所以,使用大O渐进表示法表示的时候,需要将二者相加。

如果:

  • m 等于 n,时间复杂度为O(m) 或 O(n)。
  • m 远大于 n,时间复杂度为O(m)。
  • m 远小于 n,时间复杂度为O(n)。

例三:时间复杂度为O(1)

void func4()
{
	int count = 0;
	
	for (int k = 0; k < 100; ++ k)
	{
		++count;
	}
}

基本语句++count 共执行100次,根据大O渐进表示法,用常数1取代运行时间中的所有加法常数,表示出来的时间复杂度为O(1)。

例四:时间复杂度为O(N);

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

对于这种具有最好情况、平均情况、最坏情况的算法,在实际中,一般关注的是最坏情况,所以该算法的时间复杂度为O(N)。可以看出,时间复杂度的计算是一种保守的估计

例五:时间复杂度为O(N^2)

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

冒泡排序的思想是进行多趟两两比较, 当两个数的位置不符合预期就会进行交换,每趟都能将最后一个不正确的值放在正确的位置,比较的次数依次为:n-1、n-2 …… 3、2、1、0。

根据大O渐进表示法表示出来之后就是O(N^2)

例六:时间复杂度为O(logN)

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

二分查找的思想是在有序空间上查找,每次和中间值作比较,每次都能排除一半的值,查找的效率非常高。同样,二分查找也具有最好、平均、最坏情况,我们只考虑最坏情况。

当查找的区间缩放只剩一个值的时候,此时就是最坏情况。最坏情况下,除了多少次2,就查找了多少次,假设查找了x次,2^x = N,x = logN(以2为底)。因此,二分查找的时间复杂度是O(logN)。

例七:时间复杂度为O(N)

long long fac(size_t n)
{
	if(0 == n)
		return 1;

	return fac(n-1)*n;
}

变形:时间复杂度为O(N^2)

long long fac(size_t n)
{
	if(0 == n)
		return 1;
	
	for(int i = 0; i < n; ++i)
	{
		printf("%d ",i);
	}
	
	return fac(n-1)*n;
}

递归算法是自己调用自己,这意味着当前函数会被执行多次,所以,递归函数的时间复杂度是函数被调用的次数与单次函数的时间复杂度之积。

例八:时间复杂度为O(2^N)

long long fib(size_t n)
{
	if(n < 3)
		return 1;
		
	return fib(n-1) + fib(n-2);
}

与上面的递归不同,该递归函数为双路递归,我们可以简单地画画递归展开图

我们发现,下一层调用的次数是上一层的2倍,符合等比数列的性质,我们可以根据等比数列的求和公式求出大概的时间复杂度,使用大O渐进表示法表示之后,时间复杂度为O(2^N)。

空间复杂度

谈完时间复杂度,下面我们谈谈空间复杂度。

什么是空间复杂度

我们可以这样理解,解决同一个问题,可能有不同的算法, 但是不同的算法都不能避免解决该问题本身所需要的存储空间。比如排序问题,数据元素本身就要占用一定的内存空间,这是使用任何算法都不能避免的,而不同的算法在解决该问题时,所需要的额外的、临时的空间就是我们要计算的空间复杂度。

如何计算空间复杂度

空间复杂度的计算和时间复杂度是差不多的,我们并不需要计算精确的空间大小,只需要计算大概额外使用的变量的个数即可。同样使用大O渐进表示法来表示。

复习一下大O渐进表示法:

常见空间复杂度计算例子

例一:空间复杂度为O(1)

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

在冒泡排序算法中,数组arr的空间是固定的,使用任何算法都不能避免的。在算法运行过程中,使用了end、flag变量,额外临时使用的空间是常数个,所以空间复杂度是O(1)。

例二:空间复杂度为O(N)

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

该算法在运行过程中动态开辟了n+1个空间,使用大O渐进表示法表示出来的空间复杂度为O(N)。

例三:空间复杂度为O(N)

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

    return Fac(N-1)*N;
}

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

举报

相关推荐

0 条评论