文章目录
一、数据结构绪论
1、基本概念和术语
说到数据结构是什么,我们得先来谈谈什么叫数据。
正所谓,“巧妇难为无米之炊”,再强大得计算机,也是要有“米”下锅才可以干活得,否则就是一堆破铜烂铁。这个“米”就是数据。
1.1、数据
定义:描述客观事务的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。
这里说的数据,其实就是符号,而且这些符号必须具备两个前提:
- 可以输入到计算机中
- 能被计算机程序处理
1.2、数据元素
定义:是组成数据的、具有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。
比如,在人类中,人就是数据类型。在禽类中,牛、马、羊、鸡、猪、狗等动物就是禽类的数据元素。
1.3、数据项
定义:一个数据元素可以由若干个数据项组成。
比如,人这样的数据元素,可以有眼、耳、鼻、嘴、手这些数据项,也可以拥有姓名、年龄、性别、出生地址、联系电话等数据项。、
数据项是数据不可分割的最小单位。但真正讨论问题时,数据元素才是数据结构中建立数据模型的着眼点。
1.4、数据对象
定义:性质相同的数据元素的集合,是数据的子集。
相同性质是指数据元素具有相同数量和类型的数据项,比如,还是刚刚的例子,人都有姓名、生日、性别等相同的数据项。
有了这些概念的铺垫,我们的主角登场了。
1.5、数据结构
结构,简单的理解就是关系。比如分子结构,就是说组成分子的原子之间的排列方式。严格地说,结构是指各个组成部分相互搭配和排列的方式。在现实世界中,**不同数据元素之间不是独立的,而是存在特定的关系,我们将这些称为结构。**那么数据结构是什么?
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
其中有如下重要关系:数据项 ∈ 数据元素 ∈ 数据对象 ∈ 数据
2、逻辑结构与物理结构
按照视点的不同,我们把数据结构分为逻辑结构和物理结构。
2.1、逻辑结构
定义:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种:
-
集合结构
定义: 集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。即各个元素是”平等”的,它们共同的特点就是“同属于一个集合”。
-
线性结构
定义:线性结构中的数据元素之间是一对一的关系。
-
树形结构
定义:树形结构中的数据元素之间存在一种一对多的层次关系。
-
图形结构
定义:图形结构的数据元素是多对多的关系。
我们在使用示意图表示数据的逻辑结构时,要注意两点:
- 将每一个数据看作一个结点,用圆圈表示。
- 元素之间的逻辑关系用结点之间的连线表示,如果这个关系是有方向的,那么用带箭头的连线表示。
2.2、物理结构
定义:是指数据的逻辑结构在计算机中的存储形式。
数据是数据元素的集合,根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘、软盘、光盘等外部存储的数据组织通常用文件结构来描述。
数据元素的存储结构形式有两种:顺序存储和链式存储。
2.2.1、顺序存储结构
定义:把数据元素存放在地址来连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
说白了,就是排队占位。大家都按顺序排好,每个人占一小段空间,大家谁也别插谁的队。
2.2.2、链式存储结构
定义:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关数据元素的位置。
显然,链式存储就灵活多了,数据存在哪里不重要,只要有一个指针存放了相应的地址就能找到它了。
3、抽象数据类型
3.1、数据类型
定义:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
在C语言中,按照取值的不同,数据类型可以分为两类:
- 原子类型:是不可以在分解的基本类型,包括整型、实型、字符型等。
- 结构类型:由若干个类型组合而成,是可以在分解的。例如,整型数组是由若干个整型数据组成的。
抽象是指抽取出事物具有的普遍性的本质。
3.2、抽象数据类型( Abstract Date Type, ADT )
定义:是指一个数学模型及定义在该模型上的一组操作。
“抽象”的意义在于数据类型的数学抽象特性。事实上,抽象数据类型体现了程序设计中问题分解、抽象和信息隐藏的特性。
二、算法的定义
定义:算法是解决特定问题求解步骤的描述,在计算机中变现为指令的有限序列,并且每条指令表示一个或多个操作。
对于给定的问题,是可以有多种算法来解决的。但是如果我问你,有没有通用的算法呀?这个问题其实很弱智,就像问有没有包治百病的药呀!
现实世界中的问题千奇百怪,算法当然也就千变万化,没有通用的算法可以解决所有的问题。甚至解决一个小问题,很优秀的算法却不一定适合它。
1、算法的特性
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。
-
输入输出
算法具有零个或多个输入,尽管对于绝大多数算法来说,输入参数都是必要的,但对于个别情况,如打印“hello world ! ”这样的代码就不需要输入任何参数,因此算法的输入可以是零个。
算法至少有一个或多个输出,算法是一定需要输出的,不需要输出,你用这个算法干什么呢?输出的形式可以是打印输出,也可以是返回一个或多个值等。
-
有穷性
指算法在执行有限的步骤之后,自动结束而不会出现循环,并且每一个步骤在可接受的时间内完成。
-
确定性
算法的每一步骤都具有确定含义,不会出现二义性。
-
可行性
算法的每一步都必须是可行的,也就是说,每一步都能通过执行有限次数完成。
2、算法设计的要求
-
正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。大体分为以下四个层次:
- 算法程序没有语法错误
- 算法程序对于合法的输入数据能够产生满足要求的输出结果
- 算法程序对于非法的输入数据能够得出满足规则说明的结果
- 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果
对于这四层,层次1要求最低,层次4最难,几乎不可能逐一验证所有的输入都得到正确的结果。
-
可读性
算法设计的另一个目的是为了便于阅读、理解和交流。
-
健壮性
当输入数据不合法是,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
-
时间效率高和存储量低
**算法设计应该尽量满足时间效率高和存储量低的要求。**在生活中,人们都希望花最少的钱,用最短的时间,半最大的事,算法也是一样的思想,最好用最少的存储空间,花最少的时间,办成同样的事就是好的算法。
3、算法效率的度量方法
刚刚我们提到设计算法要提高效率,这里的效率多数情况下指的是算法的执行时间。那我们该如何度量一个算法的执行的时间呢?
正所谓“是骡子是马,拉出来溜溜”。比较容易想到的方法就是通过对算法的数据测试,利用计算机的计时功能,来计算不同算法的效率是高还是低。
3.1、事后统计方法
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
但是这种方法显然有很大缺陷的:
- 必须依据算法事先编制好程序。如果编制出来发现它根本是很糟糕的算法,不是竹篮打水一场空吗?
- 时间的比较依赖计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣。
- 算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据钱买你往往得不到体现。
3.2、事前分析估计方法
在计算机程序编制前,依据统计方法对算法进行估算。
一个高级程序语言编写的程序在计算机上运行所消耗的时间取决于下列因素:
- 算法采用的策略、方法
- 编译产生的代码质量
- 问题的输入规模
- 机器执行指令的速度
第一条当然是算法好坏的根本,第二条由软件来支持,第四条要看硬件性能。也就是说,抛开这些于计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
举例:两种求和的算法“
-
第一种
int i,sum = 0,n = 100; //执行1次 for(i = 1; i <= n; i++){ //执行了n + 1次 sum = sum + i; //执行n次 } printf("%d",sum); //执行1次
-
第二种
int sum = 0,n = 100; //执行1次 sum = (1 + n) * n/2; //执行1次 printf("%d",sum); //执行1次
显然,第一种算法执行了2n + 1
次;而第二种算法,就是3
次。算法好坏显而易见。
4、函数的渐近增长
我们现在来判断以下,两个算法 A 和 B 哪一个更好。假设两个算法的输入规模都是 n ,算法 A 要做 2n + 3 次操作,算法 B 要做 3n + 1 次操作。你觉得它们谁更快呢?
准确来说,答案是不一定的。
次数 | 算法A(2n + 1) | 算法A’(2n) | 算法B(3n +1) | 算法B‘(3n) |
---|---|---|---|---|
n = 1 | 5 | 2 | 4 | 3 |
n = 2 | 7 | 4 | 7 | 6 |
n = 3 | 9 | 6 | 10 | 9 |
n = 10 | 23 | 20 | 31 | 30 |
n = 100 | 203 | 200 | 301 | 300 |
当 n = 1时,算法 A 的效率不如算法 B(次数比算法 B 要多一次)。而当 n = 2时,两者效率相同;当 n > 2时,算法 A 就开始优于算法 B 了,随着n的增加,算法A就比算法B越来越好了。于是我们得出结论,算法A总体上要好过算法B。
函数的渐近增长:给定两个函数 f(n) 和 g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比 g(n) 大,那么,我们说 f(n) 的增长渐近快于 g(n) 。
从中发现,随着n的增大,后面的 +3 还是 +1 其实是不影响最终的算法变化的,例如算法A’与算法B‘,所以,我们可以忽略这些加法常数。另外,与最高次项相乘的常数并不重要。判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更因该关注主项(最高阶项)的阶数。