2018年第11周-大话数据结构的笔记(线性表)

在我看来,编程语言其实就是跟操做系统借用计算及空间资源去实现本身的业务。

数据结构绪论

数据结构历史

计算机的诞生之初,是用于数值运算的工具。
但现实生活中,咱们不少状况下不是处理数值计算的问题,因此须要一些更科学有效的手段(好比表、树和图等数据结构)的来处理问题。因此数据结构是一门研究非数值计算的程序设计问题中的操做对象,以及它们之间的关系和操做等相关问题的学科。
1968年,美国的高德纳(Donald E. Knuth)教授在其写的《计算机程序设计艺术》第一卷《基本算法》中,较系统的阐述了数据的逻辑结构和存储结构及操做,开创了数据结构的课程体系。算法

程序设计 = 数据结构 +算法
而程序我认为就是,编程语言(程序)其实就是跟操做系统借用计算及空间资源去实现本身的业务。

基本概念和术语

数据:是描述客观事物的符号,是计算机中能够操做的对象,是能被计算机识别,并输入给计算机处理的符号集合。shell

数据不只包括整型,浮点等数值类型,还包括字符及声音、图像、视频等非数值类型。
整型、浮点,咱们能够进行数值计算。
对应字符数据类型,咱们能够进行非数值的处理,而声音、图像、视频等,咱们能够经过编码的手段变成字符数据来处理。

数据元素:是组成数据的、有必定意义的基本单位,在计算机中一般做为总体处理。也被称为记录编程

数据项:一个数据元素能够由若干个数据项组成。数据项是数据不可分割的最小单位。设计模式

数据对象:是性质相同的数据元素的集合,是数据的子集。 数组

数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。 数据结构

逻辑结构:是指数据对象中数据元素之间的相互关系。编程语言

包括:
集合结构:集合结构中的数据元素除了同输一个集合外,它们之间没有其余关系。
线性结构:线性结构中的数据元素之间是一对一的关系。
树形结构:树形结构中的数据元素之间是一对多的层次关系。
图形结构:图形结构的数据元素是多对多的关系。

物理结构:是指数据在逻辑结构中在计算机中的存储形式。存储结构应正确翻译数据元素之间的逻辑关系,这才是最为关键的。如何存储数据元素之间的逻辑关系,是实现物理结构的重点和难点。模块化

顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元能够是联系的,也能够不是连续的。该存储并不能反应其逻辑关系,所以须要用一个指针存放数据元素的地址,这样经过地址就能够找到相关数据元素的位置。

数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操做的总称。函数

简单的说: 数据类型 = 集合 + 操做
这个有点相似于Java的类的定义。

数据类型,这个概念的来源思路是:在计算机中,内存不是无限大的,你若是要计算一个如1+1=二、 3+5=8 这样的整型数字的加减乘除运行,显然不须要开辟很大的适合小数甚至字符运算的内存空间。因而计算机的研究者们就考虑,要对数据进行分类,分出多种数据类型。 工具

在C语言中,按照取值的不一样,数据类型能够分为两类:

原子类型:是不能够再分解的基本类型,包括整型、实型、字符等。在Java里,也叫基本类型。
结构类型:由若干个类型(能够是基本类型,也能够不是基本类型)组合而成,是能够再分解的。例如,整型数组是由若干个整型数据组成的。

举个栗子,在C语言中,变量声明的int a,b,这就意味着,在给变量a和b赋值时不能超过int的取值范围,变量a和b之间的运算只能是int类型所容许的运算。

抽象是指取出事务具备的广泛性的本质。它是抽出问题的特征而忽略本质的细节,是对具体事务的一个归纳。抽象是一种思考问题的方式,它隐藏了繁杂的细节,只保留实现目标所必须的信息。

抽象数据类型(Abstract Data Type, ADT):咱们对与已有的数据类型进行抽象,就有了抽象数据类型,正规的定义:是指一个数学模型及定义在该模型上的一组操做。

抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表现和实现无关(这是说这概念与计算机细节无关,不是说是实现该抽象类型时无关),在计算机编程者的角度来看,“抽象”的意义在于数据类型的数学抽象特征。

算法现广泛承认的定义是:算法是解决特定问题求解步骤的描述,在计算机表现为指令的有限序列,而且每条指令表示一个或多个操做。
既然刚刚说抽象这个概念,那就是试着抽象“算法”,它有五个基本特征:输入、输出、有穷性,肯定性和可行性。

算法设计的要求:正确性、可读性、健壮性、时间效率高和存储量低

根据上面说的游戏规则,科学家们在翻译现实世界的需求和计算机虚拟过程时,就提炼出一些高效的、不断被验证过的标准流程,这些流程就是咱们所说的计算机算法。另外模块化是计算机思惟中很重要的思想,而在软件中,那些模块就是一个个算法,所以算法构成了计算机科学的基础

上面说算法都是抽象的,那咱们怎么对比算法的好坏呢?
比较容易想到的方法就是对算法进行数据测试,利用计算机的计时功能,来计算不一样算的效率是高仍是低,这种方法叫:过后统计方法。但缺点有,咱们不妨看这里两个场景下,A、B两种算法的速度:

场景一:使用1万个数据进行测试,算法A的运行时间是1毫秒,算法B则须要运行10毫秒。
场景二:使用100万个数据测试,算法A的运行时间是10000毫秒,算法B运行6000毫秒。
这时我问你,哪个算法好?若是单纯从第一个场景做判断,显然是算法A好,可是若是单纯看场景二,彷佛算法B更好一点。按照人的思惟,可能会说,数量小的时候算法A好,数量大的时候算法B好,而后还津津乐道本身懂得辩证法。计算机则不一样,它比较笨,比较直接,不会辩证法,它要求你最好制定一个明确的标准(也就是上面说的五个基本特征的肯定性),不要一下子这样,一下子那样。因此总结上述缺点有:
1.编写代码和准备数据很耗时间。2.依赖具体的计算环境。3。依赖具体的数据量。

对于方法一来判断算法的好坏好像不太客观,那么咱们应该用什么做为标准来评判呢?
在计算机科学发展的早起,其实科学家们对这件事情也不很清楚。1965年哈特马尼斯(Juris Hartmanis)和斯坦恩斯(Richard Stearns)提出了算法复杂度的概念(二人后来所以所以得到了图灵奖),计算机科学家们开始考虑一个公平的、一致的评判算法好坏的方法。不过最先将复杂度严格量化衡量的是著名计算机科学家、算法分析之父高德纳(Don Knuth)。今天,全世界计算机领域都以高德纳的思想为准。

另外咱们先看一下函数的渐近增加:给定两个函数f(n)和g(n),若是存在一个整数N,使得对于全部的n>N,f(n)老是比g(n)大,那么,咱们说f(n)的增加渐近快于g(n)。

这个定义用来比较是时间复杂度O(n)。时间复杂度,咱们先看这个例子
第一种算法

int i,sum=0,n=100;                                //执行1次
for(i=1;i<=n;i++){                                //执行n+1次
    sum = sum + i;                                //执行了n次
}
prinf("%d",sum);                                //执行1从

则这种算法执行了1+(n+1)+n+1次=2n+3次;
再来看第二种算法

int sum = 0, n = 100;                            //执行1次
sum = (1+n)*n/2;                                //执行1次
printf("%d",sum);                                //执行1次

第二种算法执行了1+1+1=3次。
这两种算法,都是计算1到100的和,去掉头尾和循环判断的开销,那么这两个算法其实就是n次和1次的差距。

测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操做的执行次数。运行时间与这个计数成正比。

咱们在分析一个算法的运行时间时,重要的是把(对运行时间有消耗)基本操做的数量与输入规模关联起来,即基本操做的数量表示为输入规模的函数。

这里“函数”是指数学上的函数概念,如y=f(x)
假设算法A的函数是4n+8, 和算法B的函数是2n^2+1,和它们对应的阶函数
次数 算法A(4n+8) 算法A'(n) 算法B(2n^2+1) 算法B'(n^2)
n=1 12 1 3 1
n=2 16 2 9 4
n=3 20 3 19 9
n=10 48 10 201 100
n=100 408 100 20001 10000
n=1000 4008 1000 2000001 1000000

根据上面的表格,咱们拿算法A与算法B的比较,发现其比较结果跟 算法A'和算法B'的比较结果是同样。再根据函数的渐近增加的定义,咱们发现,与最高次项相乘的常数并不重要。

再来看一个例子
次数 算法C(2n^2+3n+1) 算法C'(n^2) 算法D(2n^3+3n+1) 算法D'(n^3)
n=1 6 1 6 1
n=2 15 4 23 8
n=3 28 9 64 27
n=10 231 100 2031 1000
n=100 20231 10000 2000301 1000000

根据上面的表格,咱们拿算法C与算法D的比较,发现其比较结果跟 算法C'和算法D'的比较结果是同样。再根据函数的渐近增加的定义,咱们发现,最高次项的指数大的,函数随着n的增加,结果也会变得增加特别快。

最后看一个例子
次数 算法E(2n^2) 算法F(3n+1) 算法G(2n^2+3n+1)
--- ---- ----- -----
n=1 2 4 6
n=2 8 7 15
n=5 50 16 66
n=10 200 31 231
n=100 20 000 301 20 301
n=1,000 2 000 000 3001 2003001
n=10,000 200 000 000 30 001 200030001
n=100,000 20 000 000 000 300 001 20000300001
n=1,000,000 2 000 000 000 000 3 000 001 200 000 3000 001

根据上面的表格,咱们发现当n值愈来愈大,3n+1已经无法和2n^2的结果比较,最终几乎能够忽略不计。并且算法E也越来月接近算法G。因此咱们能够得出一个结论,判断一个算法的效率时,函数中的常数和其余次要项经常能够忽略,而更应该关注主项(最高阶项)的阶数

综上关于算法的函数描述,咱们能够得出:某个算法,随着n的增大,它会愈来愈优于另外一个算法,或者愈来愈差于另外一算法。

时间复杂度 在进行算法分析,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化状况并肯定T(n)的数量级。算法的时间复杂度,也就是算法的时间亮度,记做:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增加率和f(n)的增加率相同,称做算法的渐近时间复杂度,简称时间复杂度。其中f(n)是问题规模n的某个函数。

分官方的名称,O(1)叫常数阶,O(n)叫线性阶,O(n^2)叫平方阶。

推导大O阶方法:

  1. 用常数1取代运行花四溅中的全部加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 若是最高阶项存在且不是1,则去除与这个项相乘、相加的常数。

常见的时间复杂度
图:

clipboard.png

最坏状况与平均状况

最坏状况是一种保证,不可能比它更坏了,咱们能够理解为数学上的边界值或极限。在应用中,这是一种最重要的需求,一般,除非特别指定,提到的时间复杂度都是最坏状况的运行时间。
平均状况是全部状况中最有意义的,由于它是指望的运行时间。

算法的空间复杂度经过计算算法所需的存储空间实现,算法空间复杂度的计算公式记做: S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
如:算法执行时所需的辅助空间对于输入数据量而言是个常数,则称为算法为原地工做,空间复杂度为O(1)。

线性表

线性表(List):零个或多个数据元素的有限序列。
线性表的抽象数据类型定义:

ADT 线性表(List)
Data
    线性表的数据对象集合为{a1,a2,...,an},每一个元素的类型均为DataType.其中出第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素.数据元素之间的关系是一对一的关系.
Operation  
    InitList(*L): 初始化操做,创建一个空的线性表L, 若是变量定义为L *l, 则调用该方法为InitList(l);
    ListEmpty(L): 若线性表为空, 返回true, 不然返回false. 若是变量定义为L *l, 则调用该方法为ListEmpty(*l);
    ClearList(*L): 将线性表清空.
    GetElem(L,i,*e): 将线性表L中的第i个位置元素值返回给e.
    LocateELem(L,e): 在线性表L中查找与给定值e相等的元素,若是查找成功,返回该元素在表中序号表示成功; 不然,返回0表示失败.
    ListInsert(*L,i,e): 在线性表L中的第i个位置插入新元素e.
    ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值.
    LIstLength(L): 返回线性表L的元素个数

endADT

以上方法是线性表的基本操做, 是最基本的,若是想要线性表的更为复杂的操做,彻底能够用这些基本操做的组合来实现.

线性表的顺序存储结构, 指的是用一段地址连续的存储单元依次存储线性表的数据元素.

线性表的链式存储结构,为了表示每一个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来讲,除了存储其自己的信息以外,还需存储一个指示其后继的信息(即直接后继的存储位置).咱们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域. 指针域中存储的信息称作指针或链.这两部分信息组成数据元素ai的存储映像,称为结点(Node).

哇,这好多概念,数据域,指针域,指针,链,存储映像,结点
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1, a2,...,an)的链式存储结构,所以此链表的每一个结点中只包含一个指针域,因此叫作单链表.

链表中的第一个结点的存储位置叫作头指针.
为了方便地对链表操做,会在单链表的第一个结点前附设一个结点,称为头结点,其包含的指针就叫作"头指针",指向第一个结点的存储位置.

单链表结构与顺序存储结构优缺点:

  1. 存储分配方式:
    1.1. 顺序存储结构用一段连续的存储单元一次存储线性表的元素. 对空间可能会有点浪费,或者须要频繁扩展.
    1.2. 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素. 需额外空间存储指针信息.
  2. 时间性能:
    2.1. 查找,顺序存储结构是O(1),单链表O(n)
    2.1. 插入和删除: 顺序存储结构须要平均移动表长一半的元素,时间为O(n),单链表在找出某位置的指针后,插入和删除时间为O(1)
  3. 空间性能:
    3.1. 顺序存储结构预分配存储空间,分大了,浪费,分小了易发生上溢(扩展)
    3.2. 单链表不须要分配存储空间,只要有就能够分配,元素个数也不受限制.

循环链表(circular linked list):将单链表中终端结点的指针端由空指针改成指向头结点,就使整个单链表造成一个环,这种头尾相接的单链表称为单循环链表.

双向链表(double likned list):在单链表的每一个节点中,再设置一个指向其前驱结点的指针域.

静态链表:用数组描述的链表叫作静态链表,或者叫游标实现法.

总结

线性表的链表存储结构和顺序存储结构,是后续数据结构(如栈、队列、树、图)的基础。固然术语和概念很重要,要时刻回顾并理解。
这线性表对应于Java的ArraysList、Vector和LinkedList。据我了解,之因此有了ArrayList,还要Vector的存在,除了是历史库以外,缘由之一,那估计就是由于C++的vector对象,他们仅仅是数组存储结构的线性表,而list也仅仅是链表存储结构的。感受是为了适应C++过来搞Java的人吧。这也引伸出语言可以火的缘由,如不少语言不是凭空诞生的,因此都是参考前人的经验,而且稍微兼容已有的语言,方便这些人员过分过来,让这些人员不太抗拒。 另一个就是语言的专一的层面,如Java是面向对象,高级语言,那它所涉及的机器知识、操做系统就较少,如内存、CPU(线程)等,可能体会没那么深,相反设计模式、软件工程上,反而比较吃香。而对应的大数据、人工智能,这相对于机器知识要比较多,如何让几台机器配合工做,如何新增机器时,自动加入控制计算机资源等。这就涉及到操做系统、shell脚步比较多,从而与shell脚步比较接近的Python,和c比较接近的go语言就比较火了。这个我想能够专开一篇文章来讲说。

参考:《大话数据结构》

相关文章
相关标签/搜索