数据结构与算法(初阶)——时间复杂度

一、数据结构

        结构,简单的理解就是关系。结构是指各个组成部分相互搭配和排列的方式。在现实世界中,不同数据元素之间不是独立的,而是存在特定的关系,将这些关系称为结构。

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

        在计算机中,数据元素并不是孤立、杂乱无序的,而是具有内在联系的数据集合。数据元素之间存在的一种或多种特定关系,也就是数据的组织形式。

        数据结构分为逻辑结构和物理结构。物理结构是在数据内存中如何存储,而逻辑结构是靠我们想象的。 

 

 (一)逻辑结构

        逻辑结构:是指数据对象中数据元素之间的相互关系。这也是最需要关注的问题。逻辑结构分为以下四种:

        1.集合结构

        集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是“同属于一个集合”。数据结构中的集合关系就类似于数学中的集合。

        2.线性结构

        线性结构:线性结构中的数据元素之间是一对一的关系。

        3.树形结构

        树形结构:树形结构中的数据元素之间存在一种一对多的层次关系。

        4.图形结构

        图形结构:图形结构的数据元素是多对多的关系。

(二) 物理结构

        数据的物理结构(也称为做存储结构):是指数据的逻辑结构在计算机中的存储形式 

        数据是数据元素的集合,根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。

        数据元素的存储结构形式有两种:顺序存储和链式存储。 

        1、顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。数组就是这样的顺序存储结构,数组在物理结构和逻辑结构上都是连续的。

        2、链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。

        链式存储比较灵活,数据存在哪里不重要,只要有指针能够找到它就行。

        逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。

二、算法

        算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。

        为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都完成特定的功能,这就是算法了。

        算法的特性:有穷性、确定性、可行性、输入、输出。

        算法的设计的要求:正确性、可读性、健壮性、高效率低存储量需求。

        效率一般指时间效率,执行时间短的算法效率高,执行时间长的效率低。

        存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。设计算法应该尽量满足时间效率高和存储量低的需求。 

        要衡量一个算法的效率,需要时间复杂度和空间复杂度来度量 。

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

(一)时间复杂度

        在进行算法分析时,语句总的执行次数  T(n)  是关于问题规模n的函数,进而分析  T(n)  随n 的变化情况并确定  T(n)  的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。其中  f(n)  是问题规模 n 的某个函数。

        这样用大写  O()  来体现算法时间复杂度的记法,称之为大О记法。一般情况下,随着 n的增大,T(n)  增长最慢的算法为最优算法

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

        在上面的代码中,调用Fun1总共会执行   f(n)=n^2+2*n+10  次。

        而在实际中计算时间复杂度,不一定要计算精确的执行次数,只需要大概执行次数,也就是简化函数,将一些对公式最后结果影响相对较小的式子和常数省略。

        如何简化函数?     

由于执行次数  T(n)= O(f(n)),要简化函数,使用大O的渐进表示法:

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

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

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

        对于公式  f(n)=n^2+2*n+10,可将10和2*n省略,只保留n^2,如果n^2还存在常数项系数,比如3,也可以省略,此时使用大O的渐进表示法以后,Func1的时间复杂度为:

        O(n^2)

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

         f(n)=2*n+10,使用大O的渐进表示法后,时间复杂度为:O(n)

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

        f(n)=m+n,使用大O的渐进表示法后,时间复杂度为:O(m+n);

        如果有明确提示m和n的大小关系:

        (1)m远大于n,则可以将n省略,时间复杂度为:O(m)。

        (2)m远小于n,则可以将m省略,时间复杂度为:O(n)。

        (3)m和n差不多大,时间复杂度可表示为:O(m)或者O(n)。

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

         f(n)=100;

        大O的渐进表示法 (1)用常数1取代运行时间中的所有加法常数。

        使用大O的渐进表示法后,时间复杂度为:O(1);

        也就是说时间复杂度O(1)并不意味着只运行一次,而是运行常数次。

 4、例4
const char* strchr(const char* str, int character);

        strchar是在字符串中查找一个字符。

        此时可分为多种情况:

        (1)最好的情况:只找一次,将找到目标字符,此时时间复杂度为O(1);

        (2)最坏的情况:遍历整个字符串才找到,此时时间复杂度为:O(n);

        (3)平均情况:在最好和最坏之间,时间复杂度:O(n/2);

        最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,平常提到的运行时间都是最坏情况的运行时间。

 5、例5:冒泡排序
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;
			}
		}
		if (exchange == 0)
			break;
	}
}

        最坏情况,遍历两个循环,此时 f(n)=n-1  +  n-2  +  ...  +   3 + 2 + 1 = (n*(n+1)/2;

       使用大O的渐进表示法后, 时间复杂度是O(n^2)

 6、例6:二分查找
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 - 1;
		else
			return mid;
	}
	return -1;
}

        二分查找每次查找,能够筛选一半的数据, 时间复杂度:O(log n)。

 7、例7:递归
long long Fac(size_t N)
{
	if (0 == N)
		return 1;

	return Fac(N - 1) * N;
}

         递归算法的时间复杂度: 递归次数*每次函数调用的次数。

         时间复杂度:O(n)

 8、例8:斐波那契递归
long long Fib(size_t N)
{
	if (N < 3)
		return 1;

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

         时间复杂度:O(2^n)

 (二)空间复杂度

        算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=o(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数,也使用大O渐进表示法。

        空间复杂度不是程序占用了多少bytes的空间,空间复杂度算的是变量的个数。 也就是说空间复杂度不计算空间,计算大概定义的变量个数。

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;
			}
		}
		if (exchange == 0)
			break;
	}
}

         对于冒泡排序,函数只创建了常数个空间,空间复杂度为:O(1)。

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个空间,空间复杂度为:O(n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值