第1章 数据结构绪论
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
1.1 开场白
1.2 你数据结构怎么学的?
1.3 数据结构起源
- 数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问
题的学科。 - 程序设计=数据结构+算法
1.4 基本概念和术语
1.4.1 数据
- 定义:数据是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并
输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符
及声音、图像、视频等非数值类型 - 数据符号必须具备的两个条件
- 可以输入到计算机
- 能被计算机程序处理
1.4.2 数据元素
- 定义:数据元素是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。
也被称为记录。 - 例子:牛、马、羊、鸡、猪、狗等动物当然就是禽类的数据元素
1.4.3 数据项
- 定义:一个数据元素可以由若干个数据项组成。(即数据项构成了数据元素)
- 例子:比如人这样的数据元素,可以有眼、耳、鼻、嘴、手、脚这些数据项
- 特点:数据项是数据不可分割的最小单位,但真正讨论问题时,数据元素才是数据结构中建立数据模型的着眼点。
1.4.4 数据对象
- 定义:是性质相同的数据元素的集合,是数据的子集。
- 例子:人都有姓名、生日、性别等相同的数据项。
- 注意:默认将数据对象简称为数据
1.4.5 数据结构
- 定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合
1.5 逻辑结构与物理结构
1.5.1 逻辑结构
-
定义:逻辑结构是指数据对象中数据元素之间的相互关系。
-
分类(逻辑结构分为以下四种)
-
集合结构:数据元素除了同属于一个集合外,它们之间没有其他关系。
-
线性结构:数据元素之间是一对一的关系
-
树形结构:数据元素之间存在一种一对多的层次关系
-
图形结构:元素是多对多的关系
-
-
注意
- 将每一个数据元素看做一个结点,用圆圈表示。
- 元素之间的逻辑关系用结点之间的连线表示,如果这个关系是有方向的,那么用
带箭头的连线表示。
1.5.2 物理结构
-
定义:是指数据的逻辑结构在计算机中的存储形式
-
分类:
-
顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和
物理关系是一致的 -
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续
的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一
个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置
-
总结:逻辑结构是面向问题的,而物理结构就是面向计算机的,其基本的目标就是将数据及其逻辑关系存储到计算机的内存中。
1.6 抽象数据类型
1.6.1 数据类型
- 定义:是指一组性质相同的值的集合及定义在此集合上的一些操的总称
- 在C语言中,按照取值的不同,数据类型可以分为两类
- 原子类型:是不可以再分解的基本类型,包括整型、实型、字符型等。
- 结构类型:由若干个类型组合而成,是可以再分解的。例如,整型数组是由若干
整型数据组成的。
- 抽象是指抽取出事物具有的普遍性的本质。它是抽出问题的特征而忽略非本质的细
节,是对具体事物的一个概括。
1.6.2 抽象数据类型
- 抽象数据类型:是指一个数学模型及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
1.7 总结回顾
-
概念
-
数据结构
第2章 算法
算法是解决特定问题求解步骤的描述,在计算机中表现为指令有限序列,并且每条指令表示一个或多个操作
2.1 开场白
2.2 数据结构与算法关系
2.3 两种算法的比较
2.4 算法定义
- 算法定义:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
- 指令定义:指令能被人或机器等计算装置执行。它可以是计算机指令,也可以是我们平时的语言文字。
2.5 算法的特性
- 五个基本特性:输入、输出、有穷性、确定性和可行性。
2.5.1 输入输出
- 特点:算法具有零个或多个输入,至少有一个或多个输出。
2.5.2 有穷性
- 定义:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成
2.5.3 确定性
- 算法的每一步骤都具有确定的含义,不会出现歧义性
2.5.4 可行性
- 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
2.6 算法设计的要求
2.6.1 正确性
- 算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案
- 算法层次(要求从低到高,一般把层次3作为一个算法是否正确的标准。):
- 算法程序没有语法错误。
- 算法程序对于合法的输入数据能够产生满足要求的输出结果。
- 算法程序对于非法的输入数据能够得出满足规格说明的结果。
- 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
2.6.2 可读性
- 算法设计的另一目的是为了便于阅读、理解和交流。
- 写代码的另外一个重要的目的是为了便于他人阅读,让人理解和交流,自己将来也可能阅读,可读性是算法(也包括实现它的代码)好坏很重要的标志。
2.6.3 健壮性
- 当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果
2.6.4 时间效率高和存储量低
- 时间效率指的是算法的执行时间
- 存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间
- 设计算法应该尽量满足时间效率高和存储量低的需求
综上,好的算法,应该具有正确性、可读性、健壮性、高效率和低存储量的特征。
2.7 算法效率的度量方法
2.7.1 事后统计方法
- 定义:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低
- 缺陷:
- 必须依据算法事先编制好程序,这通常需要花费大量的时间和精力
- 时间的比较依赖计算机硬件和软件等环境因素
- 算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据面前往往得不到体现。
基于事后统计方法有这样那样的缺陷,我们考虑不予采纳
2.7.2 事前分析估算方法
-
定义:在计算机程序编制前,依据统计方法对算法进行估算。
-
程序运行耗时的因素:
- 算法采用的策略、方法
- 编译产生的代码质量(软件支持)
- 问题的输入规模
- 机器执行指令的速度(硬件支持)
抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。
-
例子
显然,第一种算法,执行了1+(n+1)+n+1次=2n+3次;而第二种算法,是1+1+1=3次。事实上两个算法的第一条和最后一条语句是一样的,所以我们关注的代码其实是中间的那部分,我们把循环看作一个整体,忽略头尾循环判断的开销,那么这两个算法其实就是n次与1次的差距。算法好坏显而易见。
-
测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操作的执行次数。运行时间与这个计数成正比。
-
我们不关心编写程序所用的程序设计语言是什么,也不关心这些程序将跑在什么样的计算机中,我们只关心它所实现的算法。这样,不计那些循环索引的递增和循环终止条件、变量声明、打印结果等操作,最终,在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。
2.8 函数的渐近增长
- 定义:输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另一个函数,我们称函数是渐近增长的。例如给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。
- 判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
2.9 算法时间复杂度
2.9.1 算法时间复杂度定义
- 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
- 这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法
- 一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。
- O(1)叫常数阶、O(n)叫线性阶、O(n2)叫平方阶
2.9.2 推导大O阶方法
步骤:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
2.9.3 常数阶
- 不管这个常数是多少,我们都记作O(1),而不能是O(3)、O(12)等其他任何数字,这是初学者常常犯的错误。
- 单纯的分支结构(不包含在循环结构中),其时间复杂度也是O(1)。
2.9.4 线性阶
2.9.5 对数阶
2.9.6 平方阶
- 循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
2.10 常见的时间复杂度
- 我们前面已经谈到了O(1)常数阶、O(logn)对数阶、O(n)线性阶、O(n2)平方阶等,至于O(nlogn)我们将会在今后的课程中介绍,其他的一般不讨论
- 复杂度排序:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)
2.11 最坏情况与平均情况
- 通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
- 而平均运行时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。
2.12 算法空间复杂度
- 算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数
- 一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。
2.13 总结回顾
第3章 线性表
3.1 开场白
3.2 线性表的定义
- 定义:零个或多个数据元素的有限序列
- 前驱元素于后继元素的定义(注意唯一性)
3.3 线性表的抽象数据类型
- 线性表的抽象数据类型定义如下
3.4 线性表的顺序存储结构
3.4.1 顺序存储定义
- 定义:是用一段地址连续的存储单元依次存储线性表的数据元素。
3.4.2 顺序存储方式
-
数组:可以用一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
-
结构代码
-
顺序存储结构的三个属性
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度MaxSize。
- 线性表的当前长度:length。
3.4.3 数组长度与线性表长度区别
- 数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。
- 线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的
- 在任意时刻,线性表的长度应该小于等于数组的长度
3.4.4 地址计算方法
-
下标从0开始
-
假设存储单元大小是c,所以对于第i个数据元素ai的存储位置可以由a1推算得出:LOC(ai)=LOC(a1)+(i-1)*c
-
线性表存取的时间性能为O(1),通常把具有这一特点的存储结构称为随机存取结构。
3.5 顺序存储结构的插入与删除
3.5.1 获得元素操作
- C代码(核心是中括号获取)
3.5.2 插入操作
- 插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处; 线性表长加1。
- 代码
3.5.3 删除操作
- 删除算法的思路:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减1。
- 代码
线性表的顺序存储结构在存读数据时,时间复杂度为O(1);而插入或删除时,时间复杂度都是O(n)。
3.5.4 线性表顺序存储结构的优缺点
3.6 线性表的链式存储结构
3.6.1 顺序存储结构不足的解决办法
- 线性表的顺序存储结构最大的缺点就是插入和删除时需要移动大量元素,需要耗费时间。
- 解决方法:所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里
3.6.2 线性表链式存储结构定义
-
特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。在顺序结构中,每个数据元素只需要存数据元素信息。但在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址
-
组成:把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
-
头尾:把链表中第一个结点的存储位置叫做头指针(注意:是指向第一个结点的指针称为头指针,而非第一个结点的指针是头指针),那么整个链表的存取就必须是从头指针开始进行了;最后一个结点的指针指向NULL
-
头结点(不是链表的必要元素,头结点的数据域可以不存储数据,头结点的指针指向第一个结点的地址)
3.6.3 头指针与头结点的异同
3.6.4 线性表链式存储结构代码描述
- 不带头结点的单链表
-
带头结点的单链表
-
空链表
-
代码
若p是指向线性表第i个元素的指针,则:
3.7 单链表的读取
- 算法思路(获得链表第i个数据):
- 声明一个指针p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,返回结点p的数据。
- 代码(C语言,p = L->next指向的是L的第1个结点,默认L是头结点)
时间复杂度是O(n)
3.8 单链表的插入与删除
3.8.1 单链表的插入
-
单链表第i个数据插入结点的算法思路:
- 声明一指针p指向链表头结点(存在头节点),初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句s->next=p-next;p->next=s;
- 返回成功。
-
代码
3.8.2 单链表的删除
-
单链表第i个数据删除结点的算法思路:
- 声明一指针p指向链表头结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功
-
代码
时间复杂度:单链表插入和删除算法的时间复杂度都是O(n),如果在我们不知道第i个结点的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。
但如果,我们希望从第i个位置,插入10个结点,对于顺序存储结构意味着,每一次插入都需要移动n-i个结点,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
3.9 单链表的整表创建
-
特点:创建单链表的过程就是一个动态生成链表的过程。它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
-
算法思路:
- 声明一指针p和计数器变量i;
- 初始化一空链表L;
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
- 循环:
- 生成一新结点赋值给p;
- 随机生成一数字赋值给p的数据域p->data;
- 将p插入到头结点与前一新结点之间。
-
代码
- 头插法
- 尾插法
3.10 单链表的整表删除
-
算法思路
- 声明一指针p和q;
- 将第一个结点赋值给p;
- 循环:
- 将下一结点赋值给q;
- 释放p;
- 将q赋值给p。
-
代码
3.11 单链表结构与顺序存储结构优缺点
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构。而如果事先知道线性表的大致长度,则使用顺序存储结构
3.12 静态链表
-
对于一些没有指针的语言使用数组来代替指针,来描述单链表。
-
让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个data和一个cur。这种用数组描述的链表叫做静态链表。
- 数据域data,用来存放数据元素,也就是通常我们要处理的数据;
- 而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫做游标。
-
另外我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作
-
若已经将数据存入静态链表
3.12.1 静态链表的插入操作
-
要解决的问题:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。
-
在动态链表中,结点的申请和释放分别使用**malloc()和free()**两个函数来实现。在静态链表中,操作的是数组,所以需要自己实现这两个函数
-
结点申请:将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
-
例如,在乙和丁之间插入丙
-
3.12.2 静态链表的删除操作
-
若删除甲
3.12.3 静态链表优缺点
3.13 循环链表
-
定义:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)
-
特点:循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。
-
两链表合并
3.14 双向链表
-
定义:双向链表(double linkedlist)是在单链表的每个结点中,再设置一个指向其前驱结点的指针
域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。 -
代码
-
插入
-
删除
双向链表是用空间来换时间