第一章 绪论
1.数据结构概述
首先介绍数据结构(Data Structure)。数据(Data)是指数字化后的能被计算机处理的符号,它可以是比较容易理解的数值型数据,如某位学生的年龄为 20 岁、在某门功课上取得成绩为 90 分等,也可以是其他相对难处理的非数值型数据,如文本、图片、音频、视频等。结构(Structure)即为事物之间的内在关系,如一所大学包含若干学院,每个学院又包含若干研究所,这是一种层次结构。其数据结构示意图如图所示。
为了编写一个好的程序,开发者必须分析待处理对象的特性以及各处理对象之间的内在关系。构建数据结构的过程有下面几个步骤:
用计算机解决具体问题时的主要步骤如下:
- 从具体问题抽象出一个适当的数学模型。
- 设计一个解决此数学模型的算法。
- 编写程序,并进行测试、调试,直至问题得到最终解答。
其中建立数学模型即是分析具体问题的过程,它包括以下两个步骤:
- 分析具体问题中的操作对象。
- 找出这些对象间的关系,并用数学语言描述。数学模型主要分为以下两大类。第一类是数值计算类,数据类型基本都是整型、实型、浮点型等原子类型,可用已有的数学理论进行建模,第二类是非数值计算类。
2.数据结构的基本概念
-
数据。数据即是所有能输入计算机中并被计算机程序加工、处理符号的总称,如整数、实数、字符、音频、图片、视频等。
-
数据元素。数据元素是数据的基本单位。在不同的数据结构中数据元素有不同的称呼,如记录、结点或顶点。它在计算机程序中通常被作为一个整体进行考虑和处理。
-
数据项。数据项是数据不可分割的最小单位。一个数据元素可由一个或多个数据项组成,如数据元素(姓名、年龄)由两个数据项“姓名”和“年龄”组成。
-
数据对象。数据对象是由性质相同(或者说类型相同)的数据元素组成的集合,数据对象是数据的一个子集。实例说明如下。
由 4 个整数组成的数据对象:
D1={20,- 30,88,45}
由正整数组成的数据对象:D2={1,2,3,…}
-
数据结构。数据结构是相互之间存在一种或多种特定关系的数据元素的集合。数据元素之间的关系称为结构,主要有 4 类基本结构,如下图所示。
① 集合:数据元素之间的关系比较松散。
② 线性结构:数据元素之间有严格的先后次序关系。
③ 树状结构:数据元素之间是一对多的层次关系。
④ 图结构:数据元素之间是多对多的关系。
- 存储结构。所有的数据输入计算机后都必须存储在计算机中才能进行相关操作。数据结构在计算机存储器中的映像(mapping)称为数据的存储结构,也称为存储表示、物理结构、物理表示。数据的存储结构分为以下两大类。
顺序存储结构
数据元素顺序存放在内存储器的连续存储单元中。例如线性表
L=('A','B','C','D')
存放在内存储器中,首地址是 a,接下来是a+1、a+2、a+3
,如下图所示。
非顺序存储结构(也称为链接存储结构)
数据元素存放在非连续的存储空间中。例如单链表中的数据元素
'A'、'B'、'C'、'D'
分别存放在地址 a、地址 b、地址 c、地址 d 中,而这 4 个地址是分散的。但为了表示这 4 个元素有前驱和后继的线性关系,我们用指针来构建元素之间的联系。例如,一个头指针Head
指向数据'A'
的结点地址,数据'A'
的指针指向数据'B'
的结点地址,数据'B'
的指针指向数据'C'
的结点地址,数据'C'
的指针指向数据'D'
的结点地址,具体如图所示。
上图数据元素存储的地址在整体上具有前后次序,但实际对单链表数据元素所分配的存储空间是随机的。
如下图所示,数据元素'A'
在物理存储地址上可能位于数据元素'B'
和'D'
存储地址之后。
-
数据类型。数据类型(Data Type)是一个所有可能取值构成的集合和定义在这些值上的一组操作的总称。数据类型主要分为两类:一类是原子类型,如
int、char、float
等;另一类是结构类型,如基于线性、树状、图等结构的数据结构定义。 -
抽象数据类型。抽象数据类型(Abstract Data Type,ADT)是指一个数学模型以及定义在该模型上的一组操作。它是对数据结构逻辑上的定义,与计算机的实现无关。
一个抽象数据类型可以用一个三元组来表示,如(D,S,P)
,其中 D 表示数据对象,S 是 D 中数据元素之间的关系集,P 是对 D 中数据元素的基本操作。一般形式如下。
3.算法设计的一般步骤
3.1算法定义与性质
好的程序需要有好的数据结构和算法。选择好的数据结构可以为算法设计打下良好基础,数据结构与算法设计密不可分。算法是对特定问题求解步骤的一种描述。换言之,算法给出了求解一个问题的思路和策略。
一个算法应该具有以下 5 个特征。
(1)有穷性,即算法的最基本特征,要求算法必须在有限步(或有限时间)之后执行完成。
(2)确定性,即每条指令或步骤都无二义性,具有明确的含义。
(3)可行性,即算法中的操作都可以通过已经实现的基本运算执行有限次来实现。
(4)有 0 或多个输入。
(5)至少有一个输出。
针对算法的 5 个特征,现给出算法的设计要求如下。
(1)正确性:算法有 4 个不同层次的正确性,即无语法错误。对 n 组输入能产生正确结果, 对特殊输入能产生正确结果,对所有输入能产生正确结果(理想状态)。
(2)可读性:算法的变量命名、格式符合行业规范,并在关键处给出注释,以提升算法的可理解性。
(3)健壮性:算法能对不合理的输入给出相应的提示信息,并做出相应处理。
(4)高执行效率与低存储量开销:涉及算法的时间复杂度和空间复杂度评判。
算法设计出来后有多种表述方法,一般有如下几种描述工具:
第一种是自然语言;第二种是程序设计语言;第三种是程序流程图;第四种是伪码语言,它是一种包括高级程序设计语言 3 种基本结构(顺序、选择、循环)和自然语言成分的“面向读者”的语言;第五种是类 C 语言,其是介于伪码语言和程序设计语言之间的一种表示形式,保留了 C 语言的精华,不拘泥于 C 语言的语法细节,同时添加一些 C++的成分,特点是便于理解、阅读且能方便地转换成 C 语言。
3.2算法设计步骤
算法设计的一般过程可以归纳为以下几个步骤。
- 分析问题:通过对问题进行详细的分析,确定算法主要策略。
- 确定数据结构与算法:确定使用的数据结构,并在此基础上设计对此数据结构实施各种操作的算法。
- 选用语言:选用某种高级程序设计语言将算法转换成程序。
- 调试并运行:测试修正语法错误和逻辑错误,保证程序可运行。
4.算法复杂度分析
4.1算法时间复杂度分析
同一个算法用不同的语言实现、用不同的编译程序进行编译或在不同的计算机上运行时,执行时间可能不相同。因此,我们很难以算法的实际执行时间来评判算法的效率,而是往往比较关注算法的时间开销相对于问题规模变化的趋势,也就是时间复杂度。
设 n
为求解问题的规模,即为数据量的大小。首先,不区分算法中基本操作或语句执行时间开销上的差异,计算算法(或程序)中基本操作或语句重复执行次数总和,记作f(n)
,称为语句频度。
在此基础上,需要执行下面步骤来计算算法时间复杂度。
(1)只保留问题规模 n
的最高阶项。
(2)去掉 f(n)
中的所有常量系数。所得算法的时间复杂度,记作 T(n)
,用 O
表示。对于一个函数 f(n)
,当 n
趋于无穷大时,若T (n)/f(n)
的极限值为不等于 0
的常数,则称 f(n)
是 T(n)
的同数量级函数,记作 T(n)= O(f(n))
, 称 O(f(n))
为算法的渐进时间复杂度(简称时间复杂度)。也就是说,只求出 T (n)的最高阶(数量级),忽略其低阶项和常系数,这样既可以简化 T (n)的计算,又能客观反映出针对问题规模 n 的算法时间性能。
下面用实例来说明如何计算算法的时间复杂度。
s
=
∑
i
=
1
n
▒
i
s=∑_i=1^n▒i
s=i∑=1n▒i
并输出,算法代码如下。
#include <stdio.h>
int getSum(){
int s, n;
scanf("%d", &n);
s = n*(n + 1) / 2;
printf("%d", s);
return s;
}
int main () {
getSum();
return 0;
}
在该算法中,问题规模为 n
,即算法中涉及的数据量大小,如 n=10
表示计算 10
个数的和,n=100
表示计算 100
个数的和。无论 n
取什么值,都是通过以下 3 条语句完成其计算:一条输入 n
, 一条计算 s
,最后一条输出 s
。每条语句执行 1 次,这样,语句频度为 f(n)=3
,时间复杂度为T(n)=O(f(n))=O(3)=O(1)
,O(1)
称为常量阶或常量数量级。
-
常见的时间复杂度有
O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(n3)
和O(2n)
,满足关系O(1)<O(logn) <O(n) <O(nlogn) <O(n2) <O(n3) <O(2n)
。常见时间复杂度的曲线图如图所示。
4.2算法空间复杂度分析
一个算法在计算机存储器上所占用的存储空间由存储算法本身所占用的存储空间、算法输入及输出数据所占用的存储空间和为求解问题所需要的辅 助空间组成。其中,空间复杂度是对算法运行过程中所开辟辅助空间大小的 度量。与时间复杂度类似,空间复杂度通常用
O
表示法来描述,如O(n)、O(nlogn)、O(nα)、O(2n)
等。其中,n
是问题的规模。
- 计算斐波那契数列的第
n
项。
斐波那契数列中第一项为1,第二项为1,从第三项开始,每一项的值是它前两项数值之和。用变量 f
表示第 n
项,f1
和f2
分别表示第 n
项的前 2
项, 算法如下。
#include <stdio.h>
int fib1(int n)
{
int f1=1,f2=1,f=1;
while (n-->=3)
{
f=f1+f2;
f1=f2;
f2=f;
}
return f;
}
int main () {
int n=10;
int res=fib1(n);
printf("结果是:%d",res);
return 0;
}
>>>
结果是:55
sandbox> exited with status 0
由于此例函数中变量个数固定,因此空间大小不随 n
的改变而发生变化。当一个算法的辅助空间为一个常量,即不随着处理数据量 n
的大小变化而改变,算法的空间复杂度是 O(1)
。
- 同前例问题,采用递归的方式计算斐波那契数列。
其递归算法如下:
#include <stdio.h>
int fib3(int n){
if (n<=2){
return 1;
}
return fib3(n-1) + fib3(n-2);
}
int main () {
int n=10;
int res=fib3(n);
printf("结果是: %d",res);
return 0;
}
>>>
结果是: 55
sandbox> exited with status 0
在递归算法中,空间复杂度=递归的深度×每次递归空间的大小。其中递归过程可抽象为右图所示的二叉树,因此本例中递归的深度就是该“递归树”的高度。
如图所示,
当输入变量为 4 时,该递归树的高度为 4,此时空间复杂度为 4×
每次递归空间的大小。依此类推,当输入变量为 n 时,该递归树的高度为 n,空间复杂度为 n×
每次递归空间的大小。每次递归所需辅助空间大小为 O(1)
。该算法的空间复杂度随变量 n 的改变而改变,因此空间复杂度是 O(n)
。
记得关注我哦!