数据结构和算法基本概念

什么是数据结构?

数据

数据即信息的载体,是能够输入到计算机中并且能被计算机识别、存储和处理的符号总称。文字、视频、音频等等,都是数据,数据并不仅仅指数字。

数据元素

数据元素是数据的基本单位,又称之为记录(Record)。一般数据元素由若干基本数据项组成。

数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。换言之就是,数据结构指的是数据元素及数据元素之间的相互关系,或组织数据的形式。

我们生活中遇到的一些问题,计算机不懂,我们需要把问题抽象成一种数据模型(逻辑结构),这种模型是计算机可以理解的,然后存储到计算机中(存储结构),让计算机去帮助我们解决。

数据之间的结构关系

数据元素与数据元素之间的关系分为两种:逻辑结构、存储结构。

1.逻辑结构

逻辑结构表示数据之间的抽象关系(如邻接关系、从属关系等),按每个元素可能具有的直接前趋个数和直接后继个数,将逻辑结构分为“线性结构”和“非线性结构”两大类(与数学模型相关)。

2.存储结构

存储结构是指在计算机中的具体实现方法,分为顺序存储方法、链接存储方法、索引存储方法、散列存储方法。

逻辑结构

1.特点

描述数据结构中数据元素之间的关系,是从具体问题中抽象出来的数学模型,是独立于计算机存储器的。

2.分类

主要分为两大类:线性结构、非线性结构。

线性结构

简单地说,线性结构是n个数据元素的有序(次序)集合。

线性结构的特点:

  • 集合中必存在唯一的一个"第一个元素";
  • 集合中必存在唯一的一个"最后的元素";
  • 除最后元素之外,其它数据元素均有唯一的"后继";
  • 除第一元素之外,其它数据元素均有唯一的"前驱"。

在这里插入图片描述

树形结构

又叫层次结构,树形结构指的是数据元素之间存在着“一对多”的树形关系的数据结构。

在树形结构中,树根结点没有前驱结点,其余每个结点有且只有一个前驱结点。叶子结点没有后续结点,其余每个结点的后续节点数可以是一个也可以是多个。

在这里插入图片描述

图形结构

又叫网状结构,在图结构中任意两个元素之间都可能有关系,也就是说这是一种多对多的关系。

在这里插入图片描述

其他结构

除了以上几种常见的逻辑结构外,数据结构中还包含其他的结构,比如集合等。

存储结构

计算机的存储设备有:寄存器(辅助cpu运算,临时存储的)、高速缓存、主存储器(内存)、外存储器(硬盘、磁盘)等。

1.特点

数据的逻辑结构在计算机存储器中的映象,存储结构是通过计算机程序来实现的,因而是依赖于具体的计算机语言的,也就是指数据是如何在计算机中存储的。

2.分类

顺序存储

顺序存储(Sequential Storage):将数据结构中各元素按照其逻辑顺序存放于存储器一片连续的存储空间中

优点:结构紧凑,查找速度快,支持随机访问(下标访问)。

缺点:头部、中间插入删除数据,速度慢。

链式存储

链式存储(Linked Storage):将数据结构中各元素分布到存储器的不同点,用记录下一个结点位置的方式建立它们之间的联系,由此得到的存储结构为链式存储结构。

优点:分散,不会占用整块空间,对空间利用更加灵活和随意(有空就插入存储),减少了已有数据的移动;

缺点:遍历查找速度慢 ,不支持随机访问(下标访问)。

什么是算法?

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

算法和数据结构密切相关,算法设计取决于选定的逻辑结构,算法实现依赖于采用的存储结构。

算法的特性

  • 有穷性 :算法执行的步骤(或规则)是有限的;
  • 确定性 :每个计算步骤无二义性;
  • 可行性 :每个计算步骤能够在有限的时间内完成;

评价算法好坏的方法

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

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

时间复杂度

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

一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。相同的代码在不同的机器上运行的时间可能不同,但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。

没办法算时间,那我们算什么呢?一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度

// 请计算一下Func1中++count语句总共执行了多少次?
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);
}

Func1执行的基本次数为:
   F(N) = N^2 + 2*N + 10

当N的值不同时,执行的次数不同,N越大后两项对结果的影响越小。所以,实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法

大O的渐进表示法

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

推导大O阶方法:

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

这样得到的结果就是大O阶。

使用大O的渐进表示法以后,Func1的时间复杂度为:

   O( N^2)

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

常见时间复杂度计算

计算如下几个例题的时间复杂度

// 计算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);
}

   F(N) = 2*N+10

1取代常数加法,只保留最高阶,去掉常数系数,那么时间复杂度为:

   O(N)

// 计算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);
}

F(N) = M+N

一般情况下时间复杂度计算时未知数都是使用N,但是也可以是M,K等。

如果M远大于N,是O(M)
如果N远大于M,是O(N)
M和N差不多大,可以是O(M)或者O(N)

// 计算Func4的时间复杂度?
void Func4(int N)
{
	int count = 0;
	for (int k = 0; k < 100; ++ k)
	{
		++count;
	}
	printf("%d\n", count);
}

F(N) = 100
用常数1代替所有加法的常数

O(1)

注意:O(1)并不是代表算法运行1次,而是常数次。

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

strchr是库函数,在字符串中查找某个字符

while(*str)
{
   if(*str == charcter)
   {
       return str;
   }
   else
   {
       ++str;
   }
}

这个算法分情况:
最好:第一个字符就是要找的字符O(1)
最坏:最后一个字符是要找的字符O(N)
平均:中间查找到要找的字符O(N/2)

有些算法的时间复杂度存在最好、平均和最坏情况:

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

例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到

当一个算法随着输入的不同,时间复杂度不同,那么时间复杂度做悲观预期,看最坏的情况。

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

n个数
第一趟:n-1对
第二趟:n-2对
第三趟:n-3对

第n-1趟:1对

等差数列

F(N) = n*(n-1)/2
O(N^2)

算法的时间复杂度的计算不能只看有几层循环,而要看思想。

// 计算BinarySearch的时间复杂度?
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;
}

对于二分查找,
最好的情况::第一次就找到O(1)

最坏的情况是:最后才找到,最后剩下一个数就是我们要查找的数,每次查找排除掉一半数据,n是数组元素个数,
n/2/2/2…/2 = 1

在这里插入图片描述

n/(2^m) = 1

m是查找的次数
所以查找的次数是: log ⁡ 2 n \log_2^n log2n
时间复杂度是:O( log ⁡ 2 n \log_2^n log2n)

二分查找是很牛逼的算法

N个数中查找   大概查找次数
1000          10
100w          20
10亿           30

但是,二分查找要求必须有序,排序的代价很大。

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
	if(0 == N)
		return 1;
	return Fac(N-1)*N;
}

递归:不停的调用栈帧
递归算法时间复杂度:递归次数 * 每次递归调用的次数

所以阶乘递归的时间复杂度是O(N)

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
	if(N < 3)
		return 1;
	return Fib(N-1) + Fib(N-2);
}

在这里插入图片描述
右边的一些递归会提前结束,需要减去X项

F(N) = 2 0 2^0 20 + 2 1 2^1 21 + 2 2 2^2 22 + … + 2 N − 1 − X 2^{N-1} - X 2N1X
等比数列求和,时间复杂度为: O ( 2 N ) O(2^N) O(2N)

斐波那契数列的递归写法完全是一个实际中没有用的算法,太慢了。实际中,斐波那契数列用循环来写。

空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时额外占用存储空间大小的量度(算法本身需要的变量,不需要计算,比如形参不需要计算) 。

空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

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

O(1)

额外的变量是exchange,end,i,空间是可以重复利用不累计的;时间一去不复返累计的.

内存循环每次结束,i就销毁了,下次循环重新创建i,所以一共使用了3个额外的变量,所以空间复杂度是O(1)。

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前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;
}

空间复杂度是O(N)
时间复杂度是O(N)。

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
	if(N == 0)
		return 1;
	return Fac(N-1)*N;
}

每次调用都会建立栈帧,空间复杂度是O(N).

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
	if(N < 3)
		return 1;
	return Fib(N-1) + Fib(N-2);
}

在这里插入图片描述
Fib(N) = Fib(N-1) + Fib(N-2)
先算Fib(N-1),再算Fib(N-2),Fib(N-1)又层层递归,当Fib(N-1)计算完毕后,所有空间释放掉,Fib(N-2)又重新开辟空间。
空间可以重复利用,不累计;时间一去不复返,是累计的。上图中左边的递归结束后,空间释放,右边的递归再重复利用之前的空间。

空间复杂度是O(N)。

常见复杂度对比

一般算法常见的复杂度如下:
在这里插入图片描述
在这里插入图片描述
本章完。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值