数据结构
数据结构的基本概念
数据结构和算法是计算机科学中两个非常重要的概念 ,它们相辅相成缺一不可,数据结构是指数据的组织和存储方式,它决定了数据的逻辑结构和物理结构,良好的数据结构可以提高数据的存储效率、查询效率和修改效率,同时,还可以降低程序的复杂度和错误率。算法则是解决问题的步骤和方法,它决定了程序的执行效率和正确性,一个好的算法应该具有高效性、正确性、简洁性和可维护性等等的特点。在设计算法时需要考虑数据结构的选择和使用,以便能更好地利用数据结构的优势来提高算法的效率。数据结构和算法是相互依赖的关系,在设计程序时,需要根据具体问题选择合适的数据结构和算法。
简单来说,数据结构就像是一个容器,然后对容器里的元素执行增删改查的过程我们称之为算法。所以在学习数据结构的过程中,同样可以学到算法。
数据
数据是描述客观事物的符号,是计算机中可以操作的对象。数据不仅仅包括整数、小数、字符;还包括声音、图像、视频等等非数值类型。它满足两个特点:1.可以输入到计算机中2.能被计算机程序处理。对于数值类型可以进行数值计算;对于字符类型就需要进行非数值的处理,而声音、图像、视频等可以通过编码的手段变成字符数据来进行处理。
结构
结构的简单理解就是关系,比如分子结构,就是说组成分子的原子之间的排列方式。而数据结构就是不同数据相互之间存在一种或多种特定关系的元素的集合。在计算机中,数据元素并不是孤立的、杂乱无序的,而是具有内在联系的数据集合,数据元素之间存在一种或多种特定的关系,也就是数据的组织形式。它可以分为逻辑结构和物理结构。
1.数据结构的逻辑结构
指的是数据元素之间的逻辑关系,即数据在用户面前呈现的方式,而与数据在计算机内部的存储关系无关。它包括以下四种基本类型
集合结构:数据元素之间没有固定的顺序只是属于同一个集合
线性结构:数据元素之间存在一对一的线性关系
树状结构:数据元素之间存在一对多的层次关系
图形结构:数据元素之间存在多对多的任意关系
2.数据结构的存储结构(又叫物理结构)
指的是数据在计算机中的存储形式,总共有两种:顺序存储和链式存储
顺序存储:指的是数据元素按照一定的顺序存储在连续的内存空间中,c/c++中的数组就是这样的存储结构
链式存储:是把数据元素存储在任意的内存中,无所谓连不连续,然后通过链接的关系,把两个元素联系在一起。就像链表就是一种基础的链式结构。
时间复杂度
很多人觉得算法难,极大部分原因是被困在时间和空间这两个维度上。如果我们不需要考虑时间和空间的因素,其实可以把所有的问题都通过穷举法来解决。
一、穷举法
说到穷举法,我们可以将其与BF算法(暴力算法)联系起来。暴力,可以将其理解成一种力大砖飞的意思,即在不考虑时间复杂度的情况下将所有可能性都穷举出来。下面我们来看一些例子
1、单层循环
void solve(){ int n; cin >> n; for(int i = 1; i <= n; i++){ } return; }
这个算法的时间复杂度是O(n),我们来看一些更复杂的
2、双层循环
void solve(){ int n, m; cin >> n >> m; for(int i = 1; i <= n; i++){ for(int j = 1; j <= m; j++){ } } return; }
因为有两层循环,这个算法的时间复杂度是O(n^2)
3、三层循环
void solve(){ int n; for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ for(int k = 1; k <= n; k++){ } } } return; }
通过上面的例子你应该不难猜到这个算法的时间复杂度是O(n^3)
二、时间复杂度
1、时间复杂度的表示方法
算法的时间复杂度,就是算法时间的度量。记作:O(f(n)) 用大写的O来体现算法时间复杂度的记法,我们称之为大O记法。
2、时间复杂度
一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
T(n) = O(f(n))
(1)当f(n) = n,我们称这个算法拥有线性时间复杂度,记作O(n);
(2)当f(n) = n(n - 1) / 2,我们称这个算法拥有平方级时间复杂度,记作O(n^2);
(3)当f(n) = n(n - 1)(n - 2) / 6,我们称这个算法拥有立方级的时间复杂度,记作O(n^3);
这时候我们发现,f的函数可能很复杂,但是O表示的函数往往比较简单,它舍弃了一些”细节“,这是为什么呢?
下面我们看这样一个例子
int a[100010]; void solve(){ int n; for(int i = 1 ;i <= n; i++){ cin >> a[i]; } int k = 10, cnt = 0; for(int i = i; i <= n; i++){ for(int j = i + 1; j <= n; j++){ if(a[i] + a[j] >= k) cnt++; } } cout << cnt << endl; return; }
我们可以计算出该例子的f(n) = 1/2n^2 + 3/2n,根据高阶无穷小,我们可以把3/2n与n^2的系数1/2忽略不计,因此得到O(n^2)的时间复杂度。
三、常见的时间复杂度
1、常数阶
这个比较好理解,就是没有循环,是常数时间,表示为O(1)。
2、对数阶
时间复杂度为O(logn),例如二分查找。
3、根号阶
时间复杂度为O(根号n),例如判断一个正整数是不是素数。
4、线性阶
单层循环就是最经典的线性阶,时间复杂度为O(n)。
5、线性对数阶
即O(nlogn),例如希尔排序,堆排序,快速排序,归并排序。
6、多项式阶
可以表示成如下形式:
f(n) = a * n^k + b * n^(k - 1) + ...... + C
所以O(n),O(n^2),O(n^3)……都是多项式时间
7、指数阶
例如:给出n个点,以及每两个点之间的关系(连通还是不连通),求一个最大的集合,使得在这个集合中都连通。
这个算法的时间复杂度是O(n^2 * 2^n)。
8、阶乘阶
暴力枚举的时间复杂度为O(n!)。
当然,一般这类问题,暴力搜索没有实际意义,我们可以通过动态规划来进行优化。
四、如何判断时间复杂度
1、标准
根据一些大佬的做题经验我们将标准定为10^6,也就是说,高于这个总执行次数的时间复杂度就偏高了,有了标准以后,我们还需要知道问题规模,也就是O(n)中的n。
2、套公式
然后就是凭感觉套公式了
当n < 12时,可能是需要用到阶乘级别的算法,即O(n!);
当n < 16时,可能是需要状态压缩的算法,比如O(2^n)、O(n * 2^n)、O(n^2 * 2^n);
当n < 30时,可能是需要O(n^4)的算法,因为30^4差不多接近10^6;
当n < 100时,可能是需要O(n^3)的算法,因为100^3 = 10^6;
当n < 1000时,可能是需要O(n^2)的算法,因为1000^2 = 10^6;
当n < 100000时,可能是需要O(nlog2n)、O(n(log2n)^2)的算法;
当n < 1000000时,可能是需要O(根号n)、O(n)的算法;
但是以上数据量都是通过各位大佬通过做题总结出来的,有时候还需要结合题目本身的时间限制、出题人的阴险程度来决定,所以不能一概而论。
算法的空间复杂度
一、空间复杂度的概念
空间复杂度是指算法在执行的过程中所需的额外存储空间。这包括算法在运行时使用的变量、数组、链表等数据结构所占用的内存空间。它和算法的时间复杂度一起,是衡量算法性能的重要指标之一。
在算法设计中,我们通常希望尽可能地降低空间复杂度,以减少内存的使用,提高算法的效率。然而,在某些情况下,为了实现算法的功能,可能需要使用更多的存储空间。
二、常见数据结构的空间复杂度
1.顺序表:O(n),其中n是顺序表的长度。
2.链表:O(n),其中n是链表的长度。
3.栈:O(n),其中n是栈的最大深度。
4.队列:O(n),其中n是队列的最大长度。
5.哈希表:O(n),其中n是哈希表中元素的数量。
6.树:O(n),其中n是树的结点数量。
7.图:O(n + m),其中n是图中顶点的数量,其中m是图中边的数量。
三、空间换时间
通常使用额外空间的目的,就是为了换取时间上的效率,也就是我们常说的空间换时间。最经典的空间换时间就是动态规划,例如求一个斐波那契数列第n项的值,如果不做任何优化,就是利用循环进行计算,时间复杂度O(n),但是如果引入了数组,将计算结果预先存储在数组中,那么每次询问只需要O(1)的时间复杂度就可以得到第n项的值,而这时,由于引入了数组,所以空间复杂度就变成了O(n)。
四、总结
对于空间复杂度,不需要太过纠结于概念,学习动态规划后,会对它有非常深刻的理解。