零、前言
程序 = 数据结构 + 算法程序=数据结构+算法
一. 相关概念和术语
1. 数据结构的定义
数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象,以及它们之间的关系和操作等的学科。
2. 数据结构的相关历史
1)电子计算机的主要用途
早期:主要用于数值计算。
后来:处理逐渐扩大到非数值计算领域(能处理多种复杂的具有一定结构关系的数据)。
2)数据结构理论上的一些关系
N.沃思(Niklaus Wirth)教授提出:程序=算法+数据结构
简单地解释,
程序设计:为计算机处理问题编制一组指令集
算法:处理问题的策略
数据结构:问题的数学模型
而,软件=程序+文档
3)数据结构的形成与发展
形成阶段
60年代初期,“数据结构”有关的内容散见于操作系统、编译原理和表处理语言等课程。1968年,“数据结构”被列入美国一些大学计算机科学系的教学计划。
发展阶段
数据结构的概念不断扩充,包括了网络、集合代数论、关系等“离散数学结构”的内容。
70年代后期,我国高校陆续开设该课程。
数据结构重要性如下
二、 基本概念
2.1 基本术语
数据:所有能输入到计算机中去的描述客观事物的符号的总称。包括数字、字符、声音、图形、图像、网页、视频等不同形式都是数据,只不过它们在计算机中的格式不同罢了。简单地说,数据是计算机可以操作的对象,能够被计算机识别并且处理的符号。
数据元素 :数据的 基本单位
,也称结点 、记录、顶点,在计算机处理和程序设计中通常被作为一个整体进行考虑和处理。一般用于完整地描述一个对象。例如,在学生群体中,每一名学生都是一个数据元素。
数据项 :有独立含义的数据 最小单位
,也称域 。一个数据元素可由多个数据向组成。(eg,一个学生的姓名/专业/年龄)
数据对象:相同特性数据元素的集合,是数据的一个子集。
关系理解:
数据 > 数据对象 > 数据元素 > 数据项
某某大学学生表>某某学院学生表 > 某个学生 > 学号、姓名
数据结构 :是相互之间存在一种或多种特定关系的数据元素的集合。元素之间的关系称之为 结构。简单地来说,数据结构就是在计算机内存中管理数据,对数据进行增删查改的操作。通常是研究数据的 存储结构和逻辑结构
以及它们之间的关系。
而数据结构主要分为逻辑结构和物理结构,两种结构。
2.2 逻辑结构
2.2.1 概念
数据的逻辑结构 :数据元素间抽象化的相互关系,与数据的存储无关,独立于计算机,它是从具体问题抽象出来的数学模型。数据的逻辑结构与数据元素本身的内容和形式无关。
2.2.2 集合
集合 : 数据元素之间 仅存在 “同属于一个集合”的关系,指数据元素之间属于同一个集合,且在集合中是无序的。各个数据元素之间是平等的,它们的共同属性就是 “属于同一个集合”。
其图解如下:
①②③④⑤⑥ 这几个元素 都是集合关系,属于同一个集合,但是元素之间没有任何的其他关系。
2.2.3 线性结构
线性结构 : 数据元素之间存在 “一对一”的关系。对于中间节点而言,有且只有一个直接前驱,有且只有一个直接后继。
相关概念:
1)前驱
线性结构的数据元素,除了头部元素以外,每个元素的前一个元素被称为前驱。
2)后继
线性结构的数据元素,除了尾部元素以外,每个元素的后一个元素被称为后继。
图解如下:
其中,1号节点为头部元素,没有前驱节点;4号节点为尾部元素,没有后继节点。
1 是 2 的 前驱节点,2 是 1 的后继节点。
2.2.4 树形结构
树形结构 :数据元素之间存在 “一对多”的关系。
如图所示,①②③④⑤⑥ 这 n 个元素通过 (n-1) 条 线段连接起来,并且它们依靠这些线互相连通,那么这个图就一定是一棵树。
2.2.5 图形结构
图状结构 / 网状结构 :数据元素之间存在“多对多”的关系。图分为有向图和无向图。两者区别在于,线段有无方向。
图解:
2.3 存储结构
2.3.1 概念
数据的存储结构:数据的逻辑结构在计算机内存中的存储方式,又称为 物理结构。这里的存储形式主要针对内存而言,对于硬盘来说,主要通过文件结构来描述。
物理存储结构大致分为,顺序储存结构、链式存储结构、散列存储、索引储存。 其中,主要的存储结构为 顺序存储结构和链式存储结构。
2.3.2 顺序存储结构
顺序存储结构:利用数据元素在存储器中的相对位置来表示数据元素之间的逻辑顺序,多用 数组
来实现。其地址必定相邻,它的数据间的逻辑关系和物理关系是完全一致的。
2.3.3 链式存储结构
链式存储结构:利用结点中 指针
来表示数据元素之间的逻辑关系,数据元素存放在地址 任意
的存储单元中,其地址不一定相邻。而链式存储中,物理结构和逻辑结构完全没有关系,所以需要一个指针来存放数据元素的地址。
因此,链式链式存储不单需要 数据域 还需要 指针域 。以二叉树为例,其每个数据元素除了本身数据以外,还需要两个指针来分别指向它的两个左右孩子节点。
2.3.4 散列存储
散列(哈希)存储方式 : 专门用于 集合 的数据 存储 / 查找 方式。用一个哈希函数将数据元素按关键字和一个唯一的存储位置关联起来。( eg,Hash(x) = x%15; )
2.3.5 索引存储方式
索引存储方式 :为数据元素创建索引表来进行储存,类似于课本的目录。
2.4 数据的运算
数据的运算主要包括:数据的插入、删除、修改、查找、排序等操作。
3.抽象数据结构(ADT)
抽象数据结构(ADT):为了直观地描述数据结构,只定义其逻辑结构和操作说明。特征 :使用与现实相分离,实现封装和隐藏。写论文时的多用。其中,抽象是指抽取出事物具有的普遍性的本质,是一种思考问题的方式,它隐藏了繁杂的细节,只保留实现目标所必需的信息。
注意,数据结构类型与计算机内部表示和实现无关。
三、 算法和算法分析
1. 算法的定义
通俗地讲,算法是求解问题的方法。
- 算法的几个特性:
- 有穷性 即程序必须要在有穷的时间内完成,而不是陷入死循环。其中的 “有穷性”指的是在可接受的时间内完成,不然你写一个算法,计算机要算上个十几二十年,那么即便是在数学上是有穷,现实中朝朝暮暮早已成为回忆了,这样的算法意义不大!
- 确定性 即程序能够对相同的数据存在相同的答案,无二义性。
- 可行性 能够完成运算。如,对于 除0 的操作显然是不可行的。
- 输入 至少0个输入
- 输出 至少1个输出
2.设计一个好算法需要考虑的目标:
a.正确性:算法能够正确实现预定目标;
正确性分为四种层次
1、算法程序没有语法错误
2、算法程序对于合法的输入数据能够产生满足要求的输出结果
3、算法程序对于非法的输入数据能够得出满足规格说明的结果
4、算法程序对于精心选择的、甚至故意刁难的测试数据都有满足要求的输出结果
其中,
1 的要求是最低的,这就像你找对象一样,你能够和对方拍拖,还不能算是一场良好姻缘。
而 4 的层次明显是最难的,我们几乎不可能去逐一验证所有的输入都能达到正确的结果。
如同在一个夕阳即将落幕的黄昏,你坐在秋千上,他轻轻地推着绳子,然后两个人相视一笑,想起了相遇的场景。
b.易读性:算法易于阅读和理解,以便调试、修改、扩充;
c.健壮性:遇到非法输入时能够做出处理;
d.高效率:具有较高的时间和空间性能;
但需要注意的是,算法 必须有穷,而程序不一定(如,操作系统程序是在无限循环中执行的程序);而程序必须通过特定的程序设计语言去书写,算法则不受限制!
2. 算法分析
衡量一个算法的好坏,一般是从 时间和空间 两个维度来衡量,即时间的复杂度和空间的复杂度。
时间复杂度
概念
问题规模 n 与原操作中重复执行的次数关系的数量级称之为 该算法的(渐近)时间复杂度。
对于解决一个问题,我们当然希望,算法在能够得到正确性答案的前提下,重复执行的时间越少越少。你总不希望当你在抢课的时候,别人一下子抢到了,而你还在外面望着那个小锤锤吧!
【例1】
图源 高数叔
分析:
- 主要语句是 while循环
- sum += ++i 表示 sum = 1+2+3+4+…+x 直到sum>n;
- x就是该循环的执行次数 ,当 (1+x)x /2 >= n 时 退出循环 ,则 x与 根号n 相关 所以 时间复杂度为 B;
度量算法执行时间的方法主要是两大类:事后统计法 和 事前分析估算法。
顾名思义,两种方法分别是在程序运行前后进行度量,但因为事后统计法费时费力,且较受程序环境影响。故而,多用 事前分析估算法。
大 O 记法
在进行算法分析时,语句总的执行次数T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随着 nn 的变化情况而确定 T(n)的数量级。
算法的时间复杂度,就是算法的时间度量,记作:
T(n) = O(f(n))
用大写的 O 来体现算法时间复杂度的记法,我们称之为 大 O 记法。
推导 大O 的方法:
1)用常数1取代运行时间中的所有加法常数。
2)在修改后的运行次数函数中,只保留最高阶项。
3)如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
一个良好的算法它的基本操作数量一定不能太大,这样才能有效的运行。
就好像打游戏上分一样,有的人一直卡在钻石,有的人早已是王者,而有的人还在真金白银苦苦挣扎。
所以,一个足够高效的算法,才能支持程序的高效的运行。
一些关于问题规模的算法规律
图源 天涯哥
空间复杂度
解决问题的效率也是空间利用的效率有关
【例2】
定义一个输入参数 n 的函数 ,要求输入n后,打印从 1到n 的数字
一般来说有两种方式
/// func1 循环
void func1(int n)
{
for(int i =1;i<=n;i++)
{
printf("%d\n,i);
}
return ;
}
/// func2 递归
void func2(int n)
{
if(n)
{
printN( n-1);
printf("%d\n",n);
}
return;
}
将它们分别放在编译器中运行,你会发现,当 n = 100000 的时候,
func1 虽然运行时间较长,但能够运行出正确答案;
func2 的程序却直接奔溃;
原因在于,
func1 是用 循环 去实现的,而func2 是用 递归 实现的。
func1所占用的空间远不如 func2 的大,而func2 也因为占用空间过大导致程序奔溃。
然而, 随着计算机的发展,不再那么关注空间复杂度 ,因为现在设备存储空间都比较大。甚至在一些情况下会牺牲空间来换取时间效率。空间可以重复利用,不累加,而时间是一次性的,是累加的。因此,理解便好,duck不必深究。
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
后记
算法与数据结构是相辅相成的。
数据结构相当于数据的缓存
而算法就是对数据结构实现 增删改查
图源 钟sir