王道考研——数据结构学习笔记(第一章)

数据结构在学什么?

  • 如何用程序代码把现实世界的问题信息化
  • 如何用计算机高效地处理这些信息从而创造价值
    王道考研——数据结构学习笔记(第一章)_第1张图片

绪论

王道考研——数据结构学习笔记(第一章)_第2张图片

什么是数据?

数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。数据是计算机程序加工的原料。

数据元素、数据项

  • 数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。
  • 一个数据元素可由若干数据项组成,数据线是构成数据元素的不可分割的最小单位。
    在实际问题中,要根据具体的任务需求来确定什么是数据元素,什么是数据项。

举个例子,我们去餐厅排队吃饭,需要在排号系统中取号等待。在这个场景中,排队的队列中每一列顾客就是一个数据元素,其具有的信息,比如号数,取号时间,就餐人数就分别为一个数据项。

数据结构、数据对象

  • 结构:各个元素之间的关系
  • 数据结构:是相互之间存在一种或多种特定关系的数据元素的集合
  • 数据对象:是具有相同性质的数据元素的集合,是数据的一个子集

还是拿餐厅排队举例子,我们可以把每个顾客都看成一个数据元素,而相邻的顾客之间,数据项显然有着一定的逻辑联系(比如号数是一个接着一个的);我们就把这些有着特定关系的数据元素称为一个数据结构。
将目光放到其它门店,其它门店的排队场景跟此处的性质是相同的,但是又有一些区别(号数不可能一模一样);我们把就所有门店里的排队顾客信息看成同一数据对象里的数据元素。

数据结构的三要素

不难理解,数据结构强调元素之间存在某种关系,而数据对象要求在其集合内的所有数据元素具有相同的性质。
当我们在设计,讨论一个数据结构的时候,应该注意其三要素:逻辑结构,物理结构(存储结构),数据的运算。
王道考研——数据结构学习笔记(第一章)_第3张图片

逻辑结构

所谓逻辑结构,就是探索数据元素之间的逻辑关系。
王道考研——数据结构学习笔记(第一章)_第4张图片

集合

王道考研——数据结构学习笔记(第一章)_第5张图片

线性结构

王道考研——数据结构学习笔记(第一章)_第6张图片

树形结构

王道考研——数据结构学习笔记(第一章)_第7张图片
王道考研——数据结构学习笔记(第一章)_第8张图片

物理结构(存储结构)

如何用计算机表示数据元素的逻辑关系?
王道考研——数据结构学习笔记(第一章)_第9张图片

顺序存储

王道考研——数据结构学习笔记(第一章)_第10张图片

链式存储

王道考研——数据结构学习笔记(第一章)_第11张图片

索引存储

王道考研——数据结构学习笔记(第一章)_第12张图片

要点:

  • 若采用顺序存储,则各个数据元素在物理上必须是连续的;若采用非顺序存储,则各个数据元素在物理上可以是离散的
  • 数据的存储结构会影响存储空间分配的方便程度。
  • 数据的存储结构会影响对数据运算的速度。

通俗的理解,对于排队问题,对于号数的存储,我们可以选择顺序或者非顺序存储两种方式;显然两者占用的内存空间是不一样的,但在遇到特殊情况时,比如有VIP客户优先用餐,顺序存储的运算会比非顺序存储更加耗时。

数据的运算

施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

数据类型、抽象数据类型

数据类型:一个值的集合和定义在此集合上的一组操作的总称。

  • 原子类型:其值不可再分的数据类型
  • 结构类型:其值可以再分解为若干成分(分量)的数据类型。
    王道考研——数据结构学习笔记(第一章)_第13张图片
    抽象数据类型(Abstract Data Type,ADT):是抽象数据组织及与之相关的操作,ADT用数学化的语言定义数据的逻辑结构,定义运算,于具体的实现无关。(不考虑其物理结构,除非要用计算机实现)。
    注意:
  • 定义了一个ADT,就是定义了数据的逻辑结构、数据的运算,也就是定义了一个数据结构;
  • 确定了一种存储结构,就意味着再计算机中表示处数据的逻辑结构。存储结构不同,也会导致运算的具体实现不同。确定了存储结构,才能实现数据结构。

在探讨一种数据结构时,我们需要:
1.定义逻辑结构(数据元素之间的关系)
2.定义数据的运算(针对具体任务,应该定义什么样的数据结构,以及什么样的运算)
3.确定某种春初结构,实现数据结构,并实现一些对数据结构的基本运算

算法

什么是算法?

王道考研——数据结构学习笔记(第一章)_第14张图片
简单来说,算法就是解决实际问题的具体步骤。
拿排队问题来说,如果我们想让带小孩的顾客优先用餐,我们可以设计这么一个算法:
1.新顾客取号
2.将信息与前一桌对比,如果前一桌没有小孩则交换其号数;
3.重复第二步

算法的特性:

  • 有穷性:
    一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
    注:算法是有穷的,而程序可以是无穷的。

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

  • 可行性:
    算法描述的操作都可以通过已经实现的基本运算执行有限次来实现。

  • 输入。一个算法有零个或多个输入,这些输入取自某个特定的对象的集合

  • 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。

  • 一个“好”算法的特质:
    当我们在设计一个算法的时候,我们应当尽量接近以下目标:

1.正确性。算法应正确地解决求解问题。
2.可读性。算法应具有良好的可读性,以帮助人们理解。注:算法可以用“伪代码”表示,甚至用文字描述;重要的是要“无歧义”地描述出解决问题的步骤
3。健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会输出一些莫名其妙的结果。
4.高效率和低存储量需求。高效率指的就是执行速度快,时间复杂度低;而低存储量需求则是不费内存,空间复杂度低。

算法效率的度量

时间复杂度

在时间角度上,我们如何评价一个算法的时间开销?
也许我们会想到,先执行算法,事后记录运行时间来评价,但这种方法存在一些缺点:
 1.算法执行时间与机器性能有关,比如一台超级计算机与单片机比较,显然不合理。
 2.和编程语言有关,越高级的编程语言,封装程度越高,执行效率越低。
 3.和编译程序产生的机器指令质量有关
 4.有些算法不能事后统计时间。
综合以上问题,我们提出以下假设:衡量方法是否排除与算法本身无关的外界因素?是否能事先估计?
我们规定,算法时间复杂度,就是事前预估算法时间开销(Tn)与问题规模n的关系(T表示Time)

对于这个规定,我们提出一些思考:、
1,当时间复杂度表达式构成比较复杂时,我们将难以判别算法的好坏,那么是否可以忽略表达式某些部分?
2.如果代码的行数达到的成百上千行,用此办法显然太耗费时间,是否还有其他办法来评判?

针对第一个问题,我们规定,当问题规模n足够大时,我们选择只考虑阶数较高的部分,忽略低阶的部分。
我们用以下写法来表示忽略后的表达式:
T 1 ( n ) = 3 n + 3 T 2 ( n ) = n 2 + 3 n + 1000 T 3 ( n ) = n 3 + n 2 + 999999 , 有 : T 1 ( n ) = O ( n ) T 2 ( n ) = O ( n 2 ) T 3 ( n ) = O ( n 3 ) O 表 示 “ 同 阶 ” , 同 等 数 量 级 。 即 , 当 n − > ∞ 时 , 二 者 之 比 为 常 数 T_1(n)=3n+3 \\ T_2(n)= n^2+3n+1000 \\ T_3(n)=n^3+n^2+999999 , 有:\\ T_1(n)=O(n) \\ T_2(n)=O(n^2) \\ T_3(n)=O(n^3) \\ O表示“同阶”,同等数量级。即,当n->∞时,二者之比为常数 T1(n)=3n+3T2(n)=n2+3n+1000T3(n)=n3+n2+999999T1(n)=O(n)T2(n)=O(n2)T3(n)=O(n3)On>

对于时间复杂度,有以下规则:
1. 加 法 规 则 : T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) 多 项 相 加 , 只 保 留 最 高 阶 的 项 , 且 系 数 变 为 1 2. 乘 法 规 则 T ( n ) = T 1 ( n ) × T 2 ( n ) = O ( f ( n ) ) × O ( g ( n ) ) = O ( f ( n ) × g ( n ) ) 多 项 相 乘 , 都 保 留 1.加法规则: \\ T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(max(f(n), g(n))) \\ 多项相加,只保留最高阶的项,且系数变为1 \\ 2.乘法规则 T(n)=T_1(n)×T_2(n)=O(f(n))×O(g(n))=O(f(n)×g(n)) \\ 多项相乘,都保留 1.:T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))12.T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
对于不同的表达形式,给出比较大小的结果:
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)O(1)<O(log2n)<O(n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

结论1:顺序执行的代码只会影响常数项,可以忽略
结论2:只需挑循环中的一个基本操作,分析它的执行次数与n的关系即可
结论3:如果有多层嵌套循环,只需关注最深层循环循环了几次

3种复杂度:
最坏时间复杂度:考虑输入数据“最坏”的情况,即执行最长时间的情况
平均时间复杂度:考虑所有输入数据都等概率出现的情况
最好时间复杂度:考虑输入数据“最好”的情况
注意:算法的性能问题只有在n很大时才会暴露出来

空间复杂度

所谓空间复杂度,就是描述空间开销(内存开销)与问题规模n之间的关系。
当一段程序运行之前,需要将相应的程序代码编译成机器指令,再放入内存当中;当代码编写完成时,占用的内存空间就确定了,与问题的复杂程度无关。
还有一部分内存空间,用于存放数据(局部变量,参数)

举个例子来加深理解。现在假设有这么一个函数:

void Function(int n) {
	int i = 0;
	while(i < n) {
		i++;
		cout << i << endl;
	}
}

那么在内存空间中,会有一部分内存用于存放此代码编译后的机器指令,以及涉及到的变量(上方的n和i,整型变量,均为4字节)
算法空间复杂度为:
S ( n ) = O ( 1 ) , S 表 示 S p a c e S(n)=O(1),S表示Space S(n)=O(1)SSpace
我们规定,一个算法的空间复杂度是常数阶的话,那么我们称这个算法可以”原地工作“。

再举一个例子:

void Function2(int n) {
	int flag[n];
	int i;
	//......
}

对于上面这个函数,存储变量的内存空间与局部变量n的取值有关。只需关注存储空间大小与问题规模相关的变量。用大O表示法,则空间复杂度为S(n)=O(n);将结论推广开,如果定义的是一个二维数组,那么其空间复杂度应该是n²

对于上部提到过的,多个不同阶层的时间复杂度相加时,加法规则仍然适用;即只保留最高阶。
上文阐述的情况中,导致算法空间复杂度变化的原因为所存储变量的变化,而另一种情况则是函数递归调用带来的内存开销

void Function3(int n) {
	int a, b;
	if (n > 1) {
		Function3(n-1);
		cout << n << endl;
	}
	cout << "finished" << endl; 
}

每次递归调用,都会重新定义变量,占用新的内存空间。
对于递归调用的函数,其空间复杂度为:S(n)=O(n)。若在递归函数中加入了对数组(长度与输入变量相关)的定义,则每次定义的数组都是不一样的,空间复杂度就等于每次定义的内存求和:
S ( n ) = 1 + 2 + 3 + . . . + n = n ( n + 1 ) 2 = 1 2 n 2 + 1 2 n S(n)=1+2+3+...+n=\frac{n(n+1)}{2}=\frac{1}{2}n^2+\frac{1}{2}n S(n)=1+2+3+...+n=2n(n+1)=21n2+21n
同样应用加法规则,得到:S(n)=O(n²)
王道考研——数据结构学习笔记(第一章)_第15张图片

你可能感兴趣的:(数据结构)