资源简介 (共88张PPT)第2章 线性表线性表的概念线性表的顺序存储线性表的链式存储单向循环链表双向循环链表线性表的概念线性表的顺序存储线性表的链式存储2.22.32.1 点击查看本小节知识架构 点击查看本小节知识架构 点击查看本小节知识架构单向循环链表2.4 点击查看本小节知识架构双向循环链表2.5 点击查看本小节知识架构学习目标了解掌握掌握掌握了解线性表的基本概念1掌握单向循环链表的代码编写方法42掌握线性表顺序存储结构的代码编写方法3掌握线性表链式存储结构的代码编写方法本章将主要介绍数据结构中的核心概念——线性表,线性表作为一种最基本的数据结构类型,表中的数据元素之间满足线性结构。线性表可以通过不同的物理结构实现,如顺序存储与链式存储。使用顺序存储实现的线性表称为顺序表,使用链式存储实现的线性表称为单链表。单链表作为数据结构中常用的数据存储方式,还可以实现单向循环链表以及双向循环链表。本章将主要讨论这些数据结构的代码实现以及结构中数据元素的操作。2.1 线性表的概念2.1.1线性表的定义返回目录2.1.2线性表的运算2.1.1 线性表的定义线性表中的数据元素之间满足线性结构(线性结构的概念详见1.2.1节)。线性表是n个数据元素的有限序列,记为(a 1 ,a 2 ,a 3 ,…,a n ),其含义如下。(1)n为数据元素的个数,也可以称为线性表的长度,当n为0时,线性表称为空表。(2)a i 为线性表中的第i个数据元素(也可以称为数据结点),i称为数据元素在线性表中的位序(等同于数组元素的下标)。从线性表的定义可以看出,线性表中存在唯一的首个数据元素和唯一的末尾数据元素。除了首个数据元素,其他每个数据元素都有一个直接前驱(ai 的直接前驱为 a i-1 ));除了末尾数据元素,其他每个数据元素都有一个直接后继(ai的直接后继为 a i+1),如图所示。2.1.1 线性表的定义例如,公司的员工信息表就是一个典型的线性表。在员工信息表中,一名员工的记录就是一个数据元素,如表所示。从表中可以看出,员工小千的记录为首个数据元素,员工小研的记录为末尾数据元素。其他员工都有一个直接前驱的员工以及直接后继的员工。2.1.2 线性表的运算线性表的运算指的是对线性表中的数据进行操作,其具体的实现与线性表的物理结构有关。线性表常见的几种运算如下。(1)置空:将线性表变成空表。(2)求长度:获取线性表数据结点的个数。(3)取结点:取出线性表中的某个数据结点。(4)定位:获取某一个指定的数据结点。(5)插入:将数据结点插入到线性表的指定位置。(6)删除:删除线性表中的指定数据结点。上述运算并非线性表的所有运算,也不一定要同时使用,用户可根据不同的需求合理选择。2.2 线性表的顺序存储2.2.1顺序表的定义返回目录2.2.2顺序表的创建2.2.3插入数据结点2.2.4删除数据结点2.2 线性表的顺序存储2.2.5其他操作返回目录2.2.6顺序表总结2.2 线性表的顺序存储采用顺序存储的线性表称为顺序表。其数据元素之间的逻辑结构为线性结构,并且数据元素按照逻辑顺序依次存放在地址连续的存储单元中。2.2.1 顺序表的定义一般情况下,线性表中的所有数据结点的类型是相同的。在C语言中,通常使用一维数组和结构体来表示顺序表,代码实现如下所示。2.2.1 顺序表的定义由以上述代码可知,结构体中的第1个成员为一维数组,使用该数组表示顺序表(因为数组中的元素在计算内存中为连续存储),数组中保存的元素为顺序表的数据结点;结构体中的第2个成员last表示数组的下标,其初始值为-1,表示数组中没有数据结点,每插入一个数据结点,last的值加1。如图所示。2.2.2 顺序表的创建在对顺序表中的数据结点进行操作之前,需要先创建一个空的顺序表。假设一个结点所占的空间大小为L,顺序表中的结点有n个,则线性表所占的空间为n*L。但实际的情况是顺序表中的结点数是不确定的,其占有的空间也是不确定的,因此需要先分配max*L个连续的存储单元,使其能存储max个结点。通过代码实现创建空的顺序表,如例所示。2.2.2 顺序表的创建由例可知,创建空的顺序表只需为结构体在内存上申请一块连续的空间,并将表示数组下标的last置为-1,表示顺序表(数组)中没有任何结点。2.2.3 插入数据结点2.2.2节完成了创建空顺序表的操作,接下来将通过代码展示如何在顺序表中插入数据结点。在插入数据结点之前,需要判断顺序表是否为满,如果为满则不允许插入数据结点,否则会造成数据在内存上越界。1.不指定位置插入数据结点插入数据结点需要先判断顺序表是否为满,代码如下所示。(变量定义与例2-1一致)由上述代码可知;当last的值等于数组元素的最大下标值时条件为真,返回值为1,表示顺序表已满;否则返回0,表示顺序表还有空间。2.2.3 插入数据结点数据表未满即可插入数据结点,代码如下所示。(变量定义与例2-1一致)2.2.3 插入数据结点2.指定位置插入数据结点指定位置插入数据结点,需要先遍历整个顺序表,然后找到指定的位置,并将该位置后的结点依次向后移动,最后插入数据,如图所示。2.2.3 插入数据结点代码如下所示。(变量定义与例2-1一致)2.2.3 插入数据结点由以上代码可知,指定位置插入数据结点首先需要确定插入的位置,然后将该位置后的结点依次向后移动一个存储单元(先移动末尾结点),再将新结点存入指定位置,顺序表末尾结点的下标last加1。3.显示数据结点插入数据结点完成后,可通过打印顺序表中结点的数据判断是否插入成功,代码实现如下所示。(变量定义与例2-1一致)2.2.3 插入数据结点4.整体测试将以上功能代码与例2-1结合,测试数据操作是否成功,代码参考教材2.2.3节。(变量定义与例2-1一致)例中,主函数main()主要用于测试子函数是否正确,先创建空的顺序表,然后依次插入数据结点,最后通过指定位置的方式再次插入数据结点。输出结果如下所示。由输出结果可以看出,顺序表中成功插入数据结点。第一轮插入的结点数据为10、20、30、40,第二轮通过指定位置的方式插入数据结点,结点数据为5、15、25、35。2.2.4 删除数据结点在删除数据结点之前,需要判断顺序表是否为空,如果为空,不允许删除数据结点。1.不指定位置删除数据结点删除数据结点需要先判断顺序表是否为空,代码如下所示。(变量定义与例2-1一致)由上述代码可知,当last的值等于-1时,条件为真,返回值为1,表示顺序表为空;否则返回0,表示顺序表非空。2.2.4 删除数据结点顺序表非空即可删除数据结点,代码如下所示。(变量定义与例2-1一致)2.指定位置删除数据结点指定位置删除数据结点与插入数据结点类似,如图所示。2.2.4 删除数据结点代码如下所示。(变量定义与例2-1一致)2.2.4 删除数据结点由以上代码可知,删除结点首先需要确定删除的位置,然后将该位置后的结点依次向前移动一个存储单元(先移动指定位置的下一个结点),再将顺序表末尾结点的下标last减1。3.整体测试将以上功能代码与例2-2结合,测试数据操作是否成功,代码参考教材2.2.4节。(变量定义与例2-1一致)例中,主函数main()主要用于测试子函数是否正确,先创建空的顺序表,然后将数据结点依次插入到顺序表的末尾,最后通过两种方式删除顺序表中的结点。输出结果如下所示。由输出结果可以看出,数据结点插入空顺序表成功,总共插入5个结点,数据分别为10、20、30、40、50;执行删除结点操作,成功删除顺序表首个结点和末尾结点。2.2.5 其他操作对顺序表中的数据结点除了可以进行插入与删除,还可以进行其他类型的操作,例如,修改结点数据,查找数据结点位置,删除重复数据结点,合并顺序表。1.修改结点数据修改结点数据指的是对顺序表中某一结点的数据进行修改,如图所示。2.2.5 其他操作代码如下所示。(变量定义与例2-1一致)2.2.5 其他操作2.查找数据结点位置查找数据结点位置指的是获取结点的下标值,如图所示。2.2.5 其他操作代码如下所示。(变量定义与例2-1一致)3.删除重复数据结点删除重复数据结点指的是删除顺序表中数据相同的结点,如图所示。2.2.5 其他操作代码如下所示。(变量定义与例2-1一致)2.2.5 其他操作4.合并顺序表合并顺序表指的是将两个顺序表合并为一个顺序表,同时去除数据重复的结点,如图所示。2.2.5 其他操作代码如下所示。(变量定义与例2-1一致)5.整体测试将以上功能代码与例2-2、例2-3使用的函数接口结合,查看数据操作是否成功,代码参考教材2.2.5节。2.2.5 其他操作例中,主函数调用子函数实现的功能包括创建空顺序表并插入数据结点;修改顺序表中结点的数据;删除数据重复的结点;创建第2个空顺序表并插入数据结点;合并两个顺序表。输出结果如下所示。由输出结果可以看出,第1个顺序表创建并插入数据成功,修改结点数据后顺序表中出现数据同为20的两个结点,然后成功删除数据相同的结点;创建第2个顺序表并插入数据成功,然后第1个顺序表与第2个顺序表合并。2.2.6 顺序表总结通过前面的介绍可知,顺序表是将数据结点放到一块连续的内存空间上(使用数组表示顺序表,数组在内存上占有连续的空间),因此顺序表结构较为简单,根据数据结点的下标即可完成数据的存取。如图所示。2.2.6 顺序表总结虽然通过结点下标的方式访问数据十分高效,但是用户每次存取数据时,都需要重新遍历表,批量移动数据结点。如图所示。2.2.6 顺序表总结如图所示,移动数据是将前一个结点的数据赋值给后一个结点。因此,无论是删除还是插入操作,都会导致批量的赋值操作。(赋值操作的本质是重写内存)综上所述,顺序表并不适合频繁存取数据,而使用线性表的另一种存储形式——链式存储(单链表),则可以很好地解决这一问题。2.3 线性表的链式存储2.3.1单链表的定义返回目录2.3.2单链表的创建2.3.3插入数据结点2.3 线性表的链式存储2.3.5删除数据结点返回目录2.3.6其他操作2.3 线性表的链式存储采用链式存储的线性表称为单链表。其数据元素之间的逻辑结构为线性结构,但是数据元素所在的存储单元内存地址是不连续的。2.3.1 单链表的定义单链表不同于顺序表,其结点存储在内存地址非连续的存储单元上,并通过指针建立它们之间的关系。需要注意的是,单链表中的结点形式不同于顺序表,如图所示。由图可知,单链表中的结点都是由两部分组成,一部分为数据域(data),另一部分为指针域(next)。简单地说,数据域用来存放结点的数据,而指针域存放的是一个指针,该指针保存的是下一个结点的内存地址,或者说该指针指向下一个结点,如图所示。2.3.1 单链表的定义根据图所示的单链表结构可知,表中每一个结点的结构都是相同的。因此,通过代码对单链表进行定义时,默认的做法是定义一个结构体。根据图所示的结点结构可知,结构体中需要定义的成员有2个,即存储的数据与指向下一个结点的指针。代码如下所示。由以上代码可知,结构体的第1个成员为结点数据(结点数据的类型是不固定的,代码中的data为整型数据,仅作为参考);第2个成员为指针变量,保存的是该结点的下一个结点的内存地址。2.3.2 单链表的创建在对单链表中的数据进行操作之前,需要先创建一个空的单链表。通过代码实现创建一个空的单链表,如例所示。2.3.2 单链表的创建创建空单链表与创建顺序表完全不同。例2-5的第13~21行代码,其功能并非是在内存上申请一块空间存放单链表中所有的结点,而是在内存上申请一个结点(一个结构体)所需的空间,该结点的结构与其他结点一致,只是不保存任何数据,仅作为单链表的头结点使用,如图所示。以生活中常见的火车来打比方,创建空单链表操作只是申请了一个火车头,车头并不会载客,即不保存任何数据。初始时车头并没有牵引车厢,即未指向下一个结点。2.3.3 插入数据结点2.3.2节完成了创建空单链表的操作,接下来将通过代码展示如何在单链表中插入数据结点。向单链表插入数据结点的方法有很多,包括头插法、尾插法、顺序插入法、指定位置插入法。1.头插法单链表不同于顺序表,单链表使用的存储空间是不固定的。因此,插入数据结点无须判断单链表是否为满。使用头插法插入数据结点有两种不同的情况,即插入第一个数据结点与插入除第一个结点以外的其他数据结点,如图所示。2.3.3 插入数据结点2.3.3 插入数据结点由图可以看出,插入数据结点的本质为修改数据结点的指针域,使指针指向的地址改变,代码如下所示。(变量定义与例2-5一致)2.3.3 插入数据结点2.尾插法尾插法即从单链表的末尾插入数据结点,如图所示。2.3.3 插入数据结点代码如下所示。(变量定义与例2-5一致)由以上代码可知,尾插法需要先找到单链表末尾的结点,然后将末尾结点的指针指向新插入的结点。2.3.3 插入数据结点3.顺序插入法顺序插入法是头插法的改良版,在插入数据结点前需要进行判断,保证单链表中的数据有序排列,如图所示。2.3.3 插入数据结点代码如下所示。(变量定义与例2-5一致)由上述代码可知,顺序插入法在插入前会将插入的结点数据与单链表中的结点数据进行对比,如果插入的结点数据大于链表中结点数据,则移动指针,继续对比下一结点。2.3.3 插入数据结点4.指定位置插入法指定位置插入法类似于头插法,不同的是该方法在插入数据结点前进行了判断,选择位置后再进行插入操作,如图所示。2.3.3 插入数据结点代码如下所示。(变量定义与例2-5一致)由以上代码可知,指针移动的次数(while循环的次数)等于插入位置的下标值。2.3.3 插入数据结点5.显示数据结点插入数据结点完成后,即可通过打印结点数据判断结点是否插入成功。遍历整个单链表的方法很简单,只需要使用指针依次访问结点中的数据即可,如图所示。2.3.3 插入数据结点根据图设计代码完成显示功能,代码如下所示。(变量定义与例2-5一致)6.整体测试将以上功能代码与例2-5结合,查看数据操作是否成功,代码参考教材2.3.3节。2.3.3 插入数据结点例中,主函数主要用于测试子函数是否正确。测试分为4个部分:先创建一个空单链表,使用顺序插入法将数据80、60、40插入链表,通过显示结点数据,判断结点是否插入成功并自动排序;使用尾插法将数据100、200、300插入链表,通过显示结点数据,判断结点是否插入成功;使用头插法将数据30、10插入链表,通过显示结点数据,判断结点是否插入成功,查看数据显示顺序是否与插入顺序相反;使用指定位置插入法将数据5、5插入链表的第0位和第1位,通过显示结点数据,判断结点是否插入成功,查看数据是否在指定的位置上。输出结果如下所示。由输出结果可以看出,4种插入方法都可以成功将数据结点插入到链表中。其中,使用顺序插入法插入的数据自动完成排序(从小到大);使用头插法插入的数据显示顺序与插入顺序相反;使用指定位置插入法插入的数据出现在正确的位置上。2.3.4 删除数据结点在删除数据结点之前,需要判断单链表是否为空(如果为空,则不允许删除数据结点)。接下来将通过代码展示如何在单链表中删除数据结点。从单链表删除数据结点的方法包括头删法、指定数据删除法。1.头删法删除数据结点需要先判断单链表是否为空,代码实现如下所示。(变量定义与例2-5一致)如果单链表不为空,则可使用头删法删除数据结点。其实现的原理是:定义一个指针,指向要删除的数据结点,然后改变指针指向,最后释放指针指向的空间。如图所示。2.3.4 删除数据结点代码如下所示。(变量定义与例2-5一致)2.3.4 删除数据结点2.指定数据删除法指定数据删除法是根据指定的具体数据,删除单链表中与数据对应的结点,如图所示。2.3.4 删除数据结点代码如下所示。(变量定义与例2-5一致)2.3.4 删除数据结点3.整体测试将以上功能代码与例2-6结合测试,查看数据操作是否成功,代码参考教材2.3.4节。例中,主函数主要用于测试子函数是否正确。测试分为3个部分:先创建一个空单链表,使用顺序插入法将数据80、60、40插入链表,通过显示结点数据,判断结点是否插入成功并自动排序;使用头删法将数据从链表中删除,通过显示结点数据,判断第一个结点是否删除;使用指定数据删除法将数据从链表中删除,通过显示结点数据,判断数据为80的结点是否删除。运行结果如下所示。由输出结果可以看出,两种删除方法都可以成功将数据结点从链表中删除。其中,使用顺序插入法插入的数据自动完成排序(从小到大);使用头删法成功将链表首个数据结点删除;使用指定数据删除法成功将数据为80的结点删除。2.3.5 其他操作单链表中的数据结点除了可以完成插入与删除的需求,还可以进行其他类型的操作,例如,修改结点数据,查找数据结点位置,链表数据翻转,合并单链表。1.修改结点数据修改结点数据指的是通过指定的数据在链表中寻找匹配的结点,并将该结点的数据修改。代码如下所示。2.3.5 其他操作2.查找数据结点位置查找数据结点位置指的是通过指定的数据在链表中寻找匹配的结点,并获取该结点在单链表中的下标。2.3.5 其他操作3.链表数据翻转链表数据翻转指的是将单链表中的数据倒序排列。例如,单链表中结点数据的顺序为1、2、3、4、5,经过翻转后结点数据的顺序为5、4、3、2、1。如图所示。2.3.5 其他操作如图所示,断开原有的连接并使用头插法重新插入结点,需要通过指针操作来完成。代码如下所示。以上示例中,定义了两个指针p、q,将q指向需要重新插入的结点,p指向q的下一个结点。通过操作指针完成重新插入,插入采用头插法,每插入一个结点,p、q向后移动。2.3.5 其他操作4.合并单链表合并单链表指的是将两个单链表合并为一个单链表,如图所示。如图所示,将data1与data2对比,选择值较小的结点插入新的单链表,如果data3较小则插入data3所在的结点,然后将data1与data4进行对比,如果data1较小则插入data1所在的结点,以此类推。2.3.5 其他操作上述合并使用的方式称为归并排序,即将已有序的子序列合并,得到完全有序的序列。例如,单链表1 的结点数据为1、3、5、7,单链表2的结点数据为2、4、6、8,那么经过合并后链表中的结点数据为1、2、3、4、5、6、7、8。需要注意的是,归并排序的前提是子序列原本就是有序序列,否则不满足归并条件。例如,单链表1的结点数据为1、2、1,单链表2的结点数据为2、3、2,以图所示的方式合并后,单链表中的结点数据为1、2、1、2、3、2, 无法得到有序排列。2.3.5 其他操作代码实现如下所示。2.3.5 其他操作5.整体测试将以上功能代码与例2-7结合,查看数据操作是否成功,代码参考教材2.3.5节。例中,主函数主要用于测试子函数是否正确。测试分为4个部分:先创建一个空单链表1,使用尾插法将数据70、50、20、10插入链表,通过显示结点数据,判断结点是否插入成功;将单链表1中的结点数据修改为30,通过显示结点数据,判断数据是否修改成功;将单链表1中的数据结点翻转,通过显示结点数据,判断链表是否翻转成功;创建空单链表2,使用尾插法将数据20、40、60插入链表,将单链表1和单链表2进行合并,通过显示结点数据,判断单链表合并是否成功。运行结果如下所示。由输出结果可以看出,操作全部成功。2.4 单向循环链表2.4.1单向循环链表的定义返回目录2.4.2单向循环链表的创建2.4.3插入数据与显示数据2.4.1 单向循环链表的定义在单链表中,头结点指针是相当重要的,因为单链表的操作都需要头结点指针。如果头指针丢失或者损坏,那么整个链表都会遗失,并且浪费链表内存空间。为了避免这种情况的产生,可以将单链表设计成循环链表。循环链表可以是单向循环也可以是双向循环,下面将介绍单向循环链表的操作。单向循环链表指的是在单链表的基础上,将尾结点的指针指向链表中的头结点,而非NULL,如图所示。由图所示,单向循环链表中的结点通过指针指向形成一个闭合的连接,结点的结构与单链表相同,代码如下所示。2.4.2 单向循环链表的创建在对单向循环链表中的数据进行操作之前,需要先创建一个空表。通过代码实现创建空的单向循环链表,如例所示。2.4.3 插入数据与显示数据1.头插法插入数据可选用头插法,其原理与单链表一致,代码如下所示。2.4.3 插入数据与显示数据2.显示数据显示数据即获取单向循环链表中结点的数据,代码如下所示。3.整体测试将以上功能代码与例2-9结合,测试数据操作是否成功,代码参考教材2.4.3节。2.4.3 插入数据与显示数据例中,主函数主要用于测试子函数是否正确。主要测试的功能为创建单向循环链表,使用头插法插入结点,最后显示结点数据。输出结果如下所示。由输出结果可以看出,数据结点插入成功。采用头插法插入数据结点,结点排列顺序与插入顺序刚好相反。2.5 线性表的链式存储2.5.1双向循环链表的定义返回目录2.5.2双向循环链表的创建2.5.3插入与删除数据结点2.5.1 双向循环链表的定义在单链表中,如果需要查找某一个结点的后继时,直接通过指针移动到下一个结点即可。但是要查找某一结点的前驱时,则需要从链表头开始。为了提高数据操作的效率,引入双向循环链表。双向循环链表中结点的结构与单链表不同,每一个结点都有一个指向前一个结点的指针和一个指向后一个结点的指针,如图所示。2.5.1 双向循环链表的定义结点结构的代码实现如下所示。双向循环链表中,可以通过指针快速访问某个结点的前驱和后继,如图所示。由图可知,每个结点的prior指针指向自身的前驱,每个结点的next指针指向自身的后继,并且形成闭合的连接。2.5.2 双向循环链表的创建在对双向循环链表中的数据进行操作之前,需要先创建一个空表。通过代码实现创建空的双向循环链表,如例所示。2.5.2 双向循环链表的创建例中创建空双向循环链表,头结点的两个指针prior与next的初始状态为指向自己,如图所示。2.5.3 插入与删除数据结点双向循环链表插入数据与删除数据比单链表要复杂,删除数据时需要判断链表是否为空,如果为空不允许执行删除操作。1.插入数据结点双向循环链表第一次插入数据结点与后续插入的数据结点的情况不同,如图所示。2.5.3 插入与删除数据结点2.5.3 插入与删除数据结点由图可以看出,采用头插法插入新结点,只需要保证新结点与头结点(前驱)、头结点的下一个结点(后继)建立相互指向的关系即可。为了便于理解,在第一次插入结点时,可以假设双向循环链表中已经存在其他结点,其操作代码与其他情况一致。代码如下所示。2.5.3 插入与删除数据结点2.删除数据结点删除数据结点之前,必须判断双向循环链表是否为空,代码如下所示。删除数据结点采用头删法的思想,如图所示。2.5.3 插入与删除数据结点由图可知,删除数据结点,只需将该结点与其前驱(头结点)、后继的指针指向关系断开,并重新连接,然后将被删除结点的空间释放。代码如下所示。2.5.3 插入与删除数据结点3.显示结点数据插入或删除结点完成后,可通过打印结点数据,判断双向循环链表中的结点操作是否正确。代码如下所示。4.整体测试将以上功能代码与例2-11结合,测试数据操作是否成功,代码参考教材2.5.3节。2.5.3 插入与删除数据结点例中,主函数主要用于测试子函数是否正确,首先创建一个空的双向循环链表,然后插入数据结点,最后删除数据结点。输出结果如下所示。由输出结果可以看出,插入数据结点成功,删除数据结点成功。本章小结本章主要介绍了线性表的两种物理结构的实现——顺序表与单链表,通过代码设计详细展示了二者的数据操作。单链表可以实现循环链表,循环链表可以是单向循环也可以是双向循环。读者需要理解顺序表与单链表的结点操作原理,并熟练代码设计,从中掌握各个功能模块的算法设计思想。 展开更多...... 收起↑ 资源预览