【数据结构学习】数据结构和算法

一、数据结构

1.1 什么是数据结构

数据结构由数据和结构两个词组合而来,它是计算机储存、组织数据的方式。

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

什么是数据?

常见的数值1、2、3、4...,教务系统里保存的用户信息,网页里肉眼可以看到的信息(文字、图片、视频等)都是数据。它是所有能输入到计算机中并被计算机程序处理的符号的总称。

什么是结构?

组织数据的方式。
数据元素:数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。
数据项:组成数据元素的、有独立含义的、不可分割的最小单位。一个数据元素由多个数据项构成。

1.2、数据结构三要素

1.2.1 逻辑结构

独立于存储结构 ------ 定义一个车,并不需要考虑如何进行造车。

1.2.2 存储结构(物理结构)

基于逻辑结构+数据的运算

1.2.3 数据的运算

逻辑结构 ------ 由模型去想象这个模型能拥有什么功能。

存储结构 ------ 去实现这些运算。

1.3、抽象数据类型

通过逻辑结构和数据的运算可定义一个数据结构,即抽象数据类型abstract data type(ADT)。

1.4、数据的逻辑结构

可以简单理解为数据元素之间的相互关系,分为线性结构非线性结构

线性结构:受限制的线性表(栈和队列、串)、未受限制的线性表(一般线性表)、线性表推广(数组);

非线性结构:集合、树(二叉树、一般树、森林)、图(有向图、无向图)

1.4.1 线性结构:线性表、栈、队列

(1)数据元素之间存在一对一的关系。

(2)除了第一个元素以外,其余元素均有唯一"前驱"。

(3)除了最后一个元素以外,其余元素均有唯一"后驱"。

1.4.2 非线性结构:图、树、集合

(1)集合:结构中的数据元素之间除了同属一个集合外,没有其他关系。

(2)树:树结构中的数据元素之间存在一对多的关系。

(3)图:结构中元素存在多对多的关系。

1.5、数据的物理结构(存储结构)

数据对象在计算机中的存储表示(存储结构)。

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

1.5.1 顺序存储

(1)占用一大片连续的内存空间,逻辑相邻的数据元素物理也相邻,通过位置直接反映逻辑关系。

(2)不需要额外空间存储逻辑关系,空间利用率高。

(3)可以顺序访问,支持随机访问。

(4)往往通过数组实现。

(5)数据元素的插入和删除操作通过移动元素完成。

1.5.2 链式存储

(1)不要求占用连续的内存空间,逻辑上相邻,物理上可不相邻,通过指针反映逻辑关系。

(2)不仅要存储数据,还要存储数据之间的关系(指针)。

(3)只可以顺序访问,不支持随机访问,必须沿着指针。

(4)数据元素的插入和删除通过修改指针完成。

1.6 索引存储和哈希存储

1.6.1 索引存储

(1)不要求占用连续内存空间,逻辑上连续,物理结构上可不连续。

(2)不仅要存储数据,还需要额外存储空间,通过索引表存储逻辑关系,总空间需求较大,存储密度低。

(3)可顺序访问,支持随机访问,数据元素的插入和删除操作通过修改索引表相关数据元素的存储。

(4)需要额外操作时间对索引进行维护。

1.6.2 哈希存储

(1)物理位置通过哈希函数计算得到

(2)逻辑不连续,物理可不连续

(3)可能产生冲突(多个元素放入同一个地址),解决冲突需要时间。

二、算法

2.1 什么是算法

算法是解决某类特定问题的求解步骤。

2.2 算法的特性

(1)有穷性:不能出现无限循环和无限递归,必须要在有限的步骤内结束。

(2)可行性:能够在计算机上解决。

(3)确定性:算法中每一条指令必须有确切的含义,对于相同的输入只能得到相同的输出。

(4)输入:一个算法有零个或多个的输入。

(5)输出:一个算法有零个或多个输出。

2.3 好的算法

(1)正确性:能够正确的解决问题。

(2)可读性:具有良好的可读性,帮助人们理解。

(3)健壮性:能够对非法数据做出反应或处理,不会产生莫名其妙的输出。

(4)高效性:包括时间和空间两个方面,分别用时间复杂度空间复杂度来衡量。

三、复杂度

如何衡量一个算法的好坏?

一般通过时间和空间两个维度来衡量。
频度:一个语句的频度是指该语句在该算法中被重复执行的次数,一般用函数F(n)表示所有语句的频度之和。

3.1 时间复杂度

时间复杂度不是指程序运行的具体时间,因为程序所运行的硬件平台不同,运行的时间也会不同。
时间复杂度是所有语句的频度之和F(n)的数量级,在实际计算中,我们通常关注的是算法中执行频度最高的基本运算。

3.1.1 时间复杂度计算步骤

(1)找到算法中最耗时的操作(基本操作)。
(2)计算基本操作的次数。
(3)忽略常数和低阶:只保留最高阶项,忽略常数系数,得到O表示法(数学中的渐近表示法)

3.1.2 常见的时间复杂度

实例1:

c 复制代码
//计算Func2的时间复杂度
void Func(int N)
{
	int count = 0;
	for(int k = 0; k < 2 * N; ++k)
	{
		++count;
	}

	int M = 10;
	while(M--)
	{
		++count;
	}
	printf("%d\n", count);
}

(1)找到基本操作:++count、++count(2个)
(2)计算操作次数:

第一个for循环:

轮次 k ++count次数
1 0 1
2 1 1
3 2 1
... ... ...
2N 2n-1 1
2N+1 2n 不再执行

第二个while循环:

轮次 M ++count次数
1 10 1
2 9 1
3 8 1
... ... ...
10 1 1
11 0 不再执行

第一个for循环,++count执行次数为2N;

第二个while循环,++count执行次数为10;

F(n) = 2N + 10
(3)忽略常数和低阶项

F(n) = N,所以时间复fot杂度为O(N)。

实例2:

c 复制代码
//计算Func3的时间复杂度
void Func(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);
}

(1)找基本操作:++count、++count(2个)
(2)计算基本操作次数:

第一个for循环:

轮次 k ++count次数
1 0 1
2 1 1
3 2 1
... ... ...
M M-1 1
M+1 M 不再执行

第二个for循环:

轮次 k ++count次数
1 0 1
2 1 1
3 2 1
... ... ...
N N-1 1
N+1 N 不再执行

第一个for循环里面,++count执行M次;

第二个for循环里面,++count执行N次;

F(n) = M + N;
(3)忽略常数系数和低阶

①M>>N,则F(n) = M;时间复杂度为O(M)

②M<<N,则F(n) = N;时间复杂度为O(N)

③M≈N,则F(n) = M/N;时间复杂度为O(M)/O(N)

综上,时间复杂度为O(M+N)

实例3:

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

(1)确定基本操作:++count
(2)计算基本操作次数:

for循环:

轮次 k ++count次数
1 0 1
2 1 1
3 2 1
... ... ...
100 99 1
101 100 不再执行

++count执行100次,所以时间复杂度为O(100)?
(3)忽略常数系数和低阶项

时间复杂度为O(1)

可以认为,CPU计算速度非常快,执行100次++count和执行1次是差不多的。

实例4:

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

这段代码其实是想在一个str字符串里面查找一个字符,character是字符的ASCII码值。
(1)基本操作:str++
(2)计算基本操作次数:

最坏的情况是假设str里面有n个字符,要查n次才能找到字符。O(n)
**(3)忽略常数系数和低阶项:**时间复杂度为O(n)。

实例5:

c 复制代码
//计算BubbleSort的时间复杂度
void BulleSort(int* a, int n)
{
	assert(a);
	for(size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		if(a[i - 1] > a[i])
		{
			Swap(&a[i - 1], &a[i]);
			exchange = 1;
		}
	}
	if(exchange == 0)
		break;
}

这是一个冒泡排序
(1)基本操作:Swap(&a[i-1], &a[i])
(2)计算基本操作次数:

轮次 end i Swap(&a[i-1], &a[i])次数
1 n 1~n-1 n-1
2 n-1 1~n-2 n-2
3 n-2 1~n-3 n-3
... ... ... ...
n-2 2 1 1
n-1 1 error 不再执行

时间复杂度:F(n) = 1+2+...+n-1 = n(n-2)/2
(3)忽略常数系数和低阶项:O(n2^22)

实例6:

c 复制代码
//计算BinarySearch的时间复杂度
int BinarySearch(int* a, int n, int x)
{
	assert(a);
	
	int begin = 0;
	int end = n - 1;
	//[begin, end]:begin和end是左闭右闭区间,因此有等号
	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;
}

这是一个二分查找(折半查找)
(1)基本操作:int mid = begin + ((end-begin)>>1);没有找到元素,区间分成一半。
(2)基本操作次数:

轮次 区间长度n int mid = begin + ((end-begin)>>1);次数
1 n 1
2 n/2 1
3 n/4 1
... ... ...
k 1 1
k+1 0 不再执行

时间复杂度:2^(k-1) = n,k = logn+1
(3)忽略常数系数和低阶项:O(logn)

实例七:

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

(1)基本操作:Fac(N)
(2)基本操作次数:

轮次 N Fac(N)次数
1 N 1
2 N/2 1
3 N/4 1
... ... ...
N-1 1 1
N 0 不再执行

时间复杂度:O(N)
(3)忽略常数系数和低阶项:O(N)

实例8:

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

(1)基本操作:Fib(N)
(2)基本操作次数:

轮次 N Fac(N)次数
1 N 1
2 N-1, N-2 2
3 N-2, N-3, N-3, N-4 4
... ... ...
N 2^(N-1)

时间复杂度:F(n) = (1-2N-1)/(1-2) = 2N-1-1
**(3)忽略常数系数和低阶项:**O(2^N)

3.2 空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的度量。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

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

实例1:

c 复制代码
//计算BulleSort的空间复杂度
void BulleSort(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;
	}
}

上面代码中,变量个数为5个(常数个),所以空间复杂度为O(1)。

实例2:

c 复制代码
//计算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 = 0; i <= n; i++)
	{
		fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
	}
	return fibArray;
}

上述代码中,用malloc函数动态开辟了n+1个空间,所以空间复杂度为O(n)。

实例3:

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

上面代码中递归调用了N次,开辟N个栈帧,每个栈帧使用了常数个空间,空间复杂度为O(N)。

3.3 时间复杂度和空间复杂度通用计算规则

3.3.1 加法规则

S(n) or T(n) = f(n) + g(n) = O{max(f(n), g(n))};

3.3.2 乘法规则

S(n) or T(n) = f(n) * g(n) = O(f(n) * g(n));

3.3.3 忽略常数倍数或常数增量

O(C * f(n)) = O(f(n)) or O(f(n) + C) = O(f(n));

四、常见复杂度对比

5201314 O(1) 常数阶
3n+4 O(n) 线性阶
3n^2+4n+5 O(n^2) 平方阶
3logn+4 O(logn) 对数阶
2n+3nlogn+14 O(nlogn) nlogn阶
n^3+2n^2+4n+6 O(n^3) 立方阶
2^n O(n^2) 指数阶

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n)

相关推荐
milan-xiao-tiejiang2 小时前
ROS2面试准备
c++·面试·自动驾驶
杨恒982 小时前
GESPC++三级编程题 知识点
数据结构·c++·算法
koping_wu2 小时前
【leetcode】排序数组:快速排序、堆排序、归并排序
java·算法·leetcode
小O的算法实验室2 小时前
2025年AEI SCI1区TOP,基于自适应进化算法的城市空中交通多目标枢纽选址,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
week_泽2 小时前
题目 3330: 蓝桥杯2025年第十六届省赛真题-01 串
c++·贪心算法·蓝桥杯
历程里程碑2 小时前
LeetCode 283:原地移动零的优雅解法
java·c语言·开发语言·数据结构·c++·算法·leetcode
Lynnxiaowen2 小时前
今天我们开始学习腾讯云产品介绍及功能概述与应用场景
学习·云计算·腾讯云
程序猿零零漆2 小时前
Spring之旅 - 记录学习 Spring 框架的过程和经验(五)Spring的后处理器BeanFactoryPostProcessor
java·学习·spring