700 likes | 870 Views
第二章 线性表 第一节 线性表的逻辑结构 第二节 线性表的顺序存储及运算实现 第三节 线性表的链式存储及运算实现 第四节 顺序表与链表的比较 本 章 小 结 实 训 思 考 与 习 题. 第二章 线性表. 学习要求: 通过本章的教学,读者应理解线性表的逻辑结构,掌握顺序与链式两种存储结构的基本操作的算法实现,了解两种存储结构的不同特点和应用场合。 主要内容: 本章介绍了线性表的逻辑结构、定义和基本操作,线性表的顺序存储及链式存储的运算实现,最后比较了两种存储结构的不同特点。.
E N D
第二章 线性表 第一节 线性表的逻辑结构 第二节 线性表的顺序存储及运算实现 第三节 线性表的链式存储及运算实现 第四节 顺序表与链表的比较 本 章 小 结 实 训 思 考 与 习 题
第二章 线性表 学习要求: 通过本章的教学,读者应理解线性表的逻辑结构,掌握顺序与链式两种存储结构的基本操作的算法实现,了解两种存储结构的不同特点和应用场合。 主要内容: 本章介绍了线性表的逻辑结构、定义和基本操作,线性表的顺序存储及链式存储的运算实现,最后比较了两种存储结构的不同特点。
数据结构分线性结构和非线性结构。第2章、第3章、第4章分别介绍线性的数据结构,包括线性表、栈、队列、数组和矩阵。线性结构的特点是:在数据元素的非空有限集合中:数据结构分线性结构和非线性结构。第2章、第3章、第4章分别介绍线性的数据结构,包括线性表、栈、队列、数组和矩阵。线性结构的特点是:在数据元素的非空有限集合中: ·存在唯一的“第一个”数据元素; ·存在唯一的“最后一个”数据元素; ·除第一个数据元素之外,集合中的每一个数据元素都只有一个直接前驱; ·除最后一个数据元素之外,集合中的每一个数据元素都只有一个直接后继。
第一节 线性表的逻辑结构 一、线性表的定义 线性表(linear list)是最常用而又最简单的一种数据结构。简单地说,线性表是由n个数据元素组成的有限序列。 线性表中元素的个数n定义为线性表的长度,当n为零时,线性表为空,称为空表。而在非空表中的每个数据元素都有一个确定的位置,即当n>0时,可以将线性表抽象表示为: L=(a1,a2,…,ai-1,ai,ai+1,…,an) 其中ai(1≤i≤n)代表了线性表中的数据元素。至于每个数据元素的具体含义在不同的情况下是不相同的,它可以是一个数、一个符号、一幅图,或者是一页书,甚至是其他更复杂的信息。
例如,由每个英文字母组成的字母表:(A,B,…,X,Y,Z)是一个线性表,表中的数据元素是单个字符。例如,由每个英文字母组成的字母表:(A,B,…,X,Y,Z)是一个线性表,表中的数据元素是单个字符。 又如,某公司从2000年至2006年各种型号商用计算机的拥有量的变化情况,可以用向量的形式给出表达式(16,27,38,60,102,198,366),这也是一个典型的线性表结构,表中的数据元素是自然数。 再如,表2.1中的学生成绩表也是线性表的具体实例,也满足线性表的所有特性,表中的数据元素是包含了学生学号、姓名和各课成绩的一条条记录。现实生活中还存在许多线性表实例,在此不再一一枚举。 表2.1 学生成绩表
由以上几例可以看出,数据元素可以是单个的元素,也可由若干个数据项组成,这时常把数据元素称为记录,含有大量记录的线性表又称为文件。由以上几例可以看出,数据元素可以是单个的元素,也可由若干个数据项组成,这时常把数据元素称为记录,含有大量记录的线性表又称为文件。 线性表中的数据元素类型可以是多种多样的,但同一线性表中的数据元素必定具有相同特性,即属于同一数据对象,相邻数据元素之间存在着序偶关系。 从上面的实例和线性表的抽象表示可以看到,线性表的逻辑结构是通过数据元素之间的相邻 关系来体现的:a1是第一个数据元素,可以把它简称为“起点”,an是最后一个数据元素,可以把它简称为“终点”;ai是第i个元素,把i称为数据元素ai在线性表中的位序;ai是ai+1的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,3,4,…,n-1时,ai有且仅有一个直接后继ai+1;当i=2,3,4,…,n时,ai有且仅有一个直接前驱ai-1。
根据以上线性表的定义,归纳线性表的逻辑结构特性 (条件)为: 1存在唯一的一个称为“起点”的数据元素。 2存在唯一的一个称为“终点”的数据元素。 3除“起点”外,每个数据元素均只有一个直接前驱。 4除“终点”外,每个数据元素均只有一个直接后继。 5对同一线性表而言,其数据元素必须具有同一特性,只能属于同一数据对象。 线性表是一种简单的线性结构,对它的操作相当灵活,它的长度可根据需要随时变化(增长或缩短),即对线性表的数据元素不仅可以进行访问,还可以进行插入和删除等操作,但变化后的结构必须保持线性表自身的逻辑结构特性。
二、线性表的基本操作 在实际应用中,由于不同的运算种类将构成不同的数据类型,运算实现及其优劣性一般依赖于所采用的存储结构,因此,在这里只给出线性表基本操作的抽象定义,实际实现时,保持总体架构,实现具体算法。 线性表的基本运算主要包括:表的初始化、求表的长度、取表中的结点、查找结点、插入结点和删除结点等。以上还不是全部操作,不同的问题需要的操作不同。下面列出了几种常见的线性表的基本操作类型。 (一)线性表的结构化操作民主 线性表的结构化操作是关系到线性表生死命运的两个基本操作,即初始化操作,也叫构造操作,以及销毁操作。这两个操作也是线性表中最基本的操作,是一个相互对立的操作,初始化操作用是来构建一个空的线性表结构的,而销毁操作是将已经存在的一个线性表从内存中销毁,释放内存空间。
1. Create_List(L) 操作的功能:创建一个空的线性表L。 2. Free_List(L) 操作的条件:已经存在的线性表L。 操作的功能:销毁已存在的线性表L,回收内存空间。 (二)线性表的加工型操作 线性表的加工型操作,顾名思义就是对已存在的线性表的内容进行加工或修改。它主要包括三种基本操作,即插入操作、删除操作和清空操作,这也是线性表中最常用的基本操作。线性表的加工型操作执行后原有线性表的内容发生了变化,比如线性表的长度发生了增加或缩短等相应的改变。 1. Insert (L,i,new_data) 操作的条件:已经存在的线性表L,一个指定的数据元素new_data,在线性表中的位序i,且1≤i≤Length(L),其中,Length(L)表示线性表的长度。 操作的功能:在线性表L中第i个位置之前插入新的数据元素new_data,线性表L的长度加1。
2. Delete (L,i,return_data) 操作的条件:已经存在的线性表L,且该线性表L非空,在线性表中的位序i满足条件:1≤i≤Length(L),并指定返回值宿主return_data。 操作的功能:删除线性表L中的第i个位置的数据元素,并用return_data返回其值,线性表L的长度减1。 3. SetEmpty (L) 操作的条件:已经存在的线性表L,且该线性表L非空。 操作的功能:将线性表L重置成空表。 (三)线性表的引用型操作 线性表的引用型操作实际上是对线性表进行的检索性的基本操作,也可叫取值型操作。这种类型的操作执行后,原线性表的结构和内容都会保持不变。线性表的引用型操作主要包括获取线性表长度的操作,检索
线性表中指定数据元素的位序的操作,获取线性表中指定数据元素的直接前驱的操作,获取线性表中指定数据元素的直接后继的操作,以及获取线性表指定位置数据元素的操作等。线性表中指定数据元素的位序的操作,获取线性表中指定数据元素的直接前驱的操作,获取线性表中指定数据元素的直接后继的操作,以及获取线性表指定位置数据元素的操作等。 1. Length(L) 操作的条件:已经存在的线性表L。 操作的功能:返回给定线性表L的长度,即该线性表L中所包含的数据元素的个数。 2. Locate(L,data) 操作的条件:已经存在的线性表L和指定的需要检索的数据元素data。操作的功能:返回线性表L中第1个与data相等的数据元素的位序,否则返回一个特殊值表示不存在。 3. Prior(L,L_data,P_data) 操作的条件:已经存在的线性表L和指定的需要检索的数据元素L_data,以及指定返回值宿主P_data。
操作的功能:若L_data为L中的数据元素,且不是第一个,则P_data将返回它的直接前驱,否则操作失败。操作的功能:若L_data为L中的数据元素,且不是第一个,则P_data将返回它的直接前驱,否则操作失败。 4. Next(L,L_data,N_data) 操作的条件:已经存在的线性表L和指定的需要检索的数据元素L_data,以及指定返回值宿主N_data。 操作的功能:若L_data为L中的数据元素,且不是最后一个,则N_data将返回它的直接后继,否则操作失败。 5. Get(L,i) 操作的条件:已经存在的线性表L,指定在线性表L中要获取的位序i,且位序i满足条件:1≤i≤Length(L)。 操作的功能:返回线性表L中第i个数据元素。 由上述的基本操作可以实现一些复杂的操作。例如,将两个及两个以上的线性表合并成一个线性表,或者把一个线性表拆分成两个或多个线性表等。
第二节 线性表的顺序存储及运算实现 一、顺序表 线性表的顺序存储指的是用内存空间中一组地址连续的存储单元,依次存放线性表中的数据元素。由于线性表中的所有数据元素属于同一类型的数据,所以每个数据元素在存储器中所占的存储空间大小都是相等的。我们把顺序存储结构的线性表称为顺序表。 线性表顺序存储的特点是: (1)使用之前,预先分配存储空间; (2)逻辑上连续同时在物理表现时也同样是连续的,即线性表中相邻的数据元素在存储器中的存储位置也是相邻的,且方向保持一致性; (3)随机存取,只要确定了存储线性表的起始位置,线性表中的任一数据元素可随机存取。
在顺序存储方式中很容易确定每个数据元素在存储单元中与“起点”的相对位置,为此可假设线性表每个元素占用了内存空间中l个存储单元。若以“起点”所占用的存储单元的存储地址作为线性表中数据元素存储的起始地址,则线性表中第i+1个数据元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间必然满足下面所表示的关系:在顺序存储方式中很容易确定每个数据元素在存储单元中与“起点”的相对位置,为此可假设线性表每个元素占用了内存空间中l个存储单元。若以“起点”所占用的存储单元的存储地址作为线性表中数据元素存储的起始地址,则线性表中第i+1个数据元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间必然满足下面所表示的关系: LOC(ai+1)= LOC(ai)+l 因此,线性表的第i个数据元素的存储位置就应该为: LOC(ai)=LOC(a1)+(i-1)*l (1≤i≤n) 式中LOC(a1)是线性表的第一个数据元素a1的存储位置。当起始地址和一个数据元素占有的存储单元大小确定时可求出任一数据元素的存储地址。由此可见,对顺序表的存取是随机的,所以顺序存储结构属于一种随机的存取结构。在图2.1中,假设第一个元素存放的位置为b,每个元素占用的空间大小为l,则元素在内存中的存储位置、存储地址及内存状态如图所示。
由于高级语言中一维数组的表现形式如同顺序表,为此用C语言的数组将顺序表描述如下:由于高级语言中一维数组的表现形式如同顺序表,为此用C语言的数组将顺序表描述如下: /*线性表的顺序存储结构*/ #define MAXSIZE typedef struct{ elemType elem[MAXSIZE];//存储空 间基地址 int len;//顺序表当前长度 } Sqlist; 上述结构定义中,指针变量elem表示在系统分配顺序表的内存空间时分配的连续区域的首地址。由此可知,通过指针变量elem可实现顺序表的随机存取。 图2.1 线性表的顺序存储结构示意图
二、顺序表上基本运算的实现 在线性表的顺序存储结构下,一些基本操作Get(L,i)和Length(L)是非常容易实现的。下面重点讨论线性表的插入和删除操作的实现。为了讨论方便,不使用数组中下标为“0”的单元。 (一)插入 线性表的插入表示在位序为i (1≤i≤n)的数据元素之前添加一个新的数据元素。插入成功,线性表当前长度增加1,而且线性表的存储结构也相应地发生了改变。在此可分为两种情况进行讨论(n为当前线性表的最大长度):(1)插入位置在1~n之间,操作时必须将表中位序为i,i+1,…,n的数据元素向后移动,将插入的数据元素放置到i位序。(2)插入位置在n+1时,无须移动数据元素。操作过程如图2.2所示。
【算法2.1】 线性表的插入操作算法。 int Insert_sq(Sqlist*L, int i, ELEMTP x) /*在直线性表的第i-1和第i之间插入一个新元素x*/ {if (i<1‖i>L→len+1) return 0;/*不合理的插入位置i*/ if (L→Len==MAXSIZE-1) return-1;/*表已满*/ for(j=L→Len;j>=i;--j) L→elem[j+1]=L→elem[j]; /*插入位置及之后的元素右移*/ L→elem[i]=x; ++L→len;/*表长加1*/ return 1; } /*Insert_sq*/
(二)删除 线性表上的删除表示删除位序为i的数据元素。删除成功,线性表当前长度减1,线性表存储结构发生改变。同样,删除也分为两种情况讨论(n为当前线性表的最大长度):(1)删除位置在1~(n-1)之间,操作时必须将(i+1)~n的数据元素向前移动。(2)删除位置为n时,无须移动。操作过程如图2.3所示。 图2.3 顺序表删除一个元素的过程示意图
【算法2.2】 线性表的删除操作算法。 int Delete_sq(sqlist *L,int i) /*删除线性表中第i个元素*/ { if (i<1‖i>L→Len) return 0; /*不合理的删除位置i*/ if (L→Len==0) return -1 /*表已空*/ for(j=i;j<=L→Len-1;j+1) L→elem[j]=L→elem[j+1]; /*被删除元素之后的元素左移*/ --L→len; /*表长减1*/ return1; } /*Delete_sq */ 从插入和删除算法可见,当在顺序存储结构的线性表中某个位置上插入或删除一个元素时,其时间主要耗费在移动元素上,而移动元素的个数取决于插入或删除元素的位置。 假设pi是在第i个元素之前插入一个元素的概率,则在长度为n的线性表中插入一个元素时所需移动元素次数的平均次数为:
假设qi是删除第i个元素的概率,则在长度为n的域性表中删除一个元素时所需移动元素次数的平均次数为:假设qi是删除第i个元素的概率,则在长度为n的域性表中删除一个元素时所需移动元素次数的平均次数为: 如果在表的任何位置上插入或删除元素的概率相等,即 则 可见,在顺序存储结构的线性表中插入或删除一个元素时,平均约移动表中的一半元素,若表长为n,则上述算法的时间复杂度均为O(n)。
三、顺序表的应用举例 【例2.1】 表2.2所示为一学生基本信息表,现在需要把表中的信息录入到计算机的内存中,并显示出其信息表的内容。 算法思路:数据录入的过程就是不断地进行插入操作,通过顺序表的插入操作来模拟这一过程。 表2.2 学生基本信息表
【算法2.3】 建立一学生信息表的算法。 /*构造顺序表,向顺序表插入元素(学生记录)*/ #define LIST_MAX_SIZE 60 void main() { struct studentIn //学生基本信息结构声明 { int s_id;//学号 string s_name;//姓名 int s_age;//年龄 string s_sex;//性别 string s_sp;//专业(speciality) }; struct studentIn *student; int i; int num;//学生人数 pritnf("请输入学生人数:\n");
scanf("%d",&num); if ( num>LIST_MAX_SIZE )//检查区域的合法性 { printf("超过限定人数!\n"); return 0; } student=(struct studentIn *)malloc(num * sizeof(stuctstudentIn) ); if (!student ) { printf("内存分配失败!\n"); return 0; } for ( i=0;i<num;i++ ) //学生信息的录入 { printf("此时输入的是第%d个学生信息",i+1); student[i].s_id=i+1;
printf("学生姓名:"); scanf("%s",student[i]→s_name); printf("学生年龄:") scanf("%d",student[i]→s_age); printf("学生性别:"); scanf("%s",student[i]→s_sex); printf("专业:"); scanf("%s",student[i]→s_sp); clear();} for ( i=0;i<num;i++)//显示输入的学生记录 {printf("%d",student[i]→s_id); printf("%s",student[i]→s_name); printf("%d",student[i]→s_age); printf("%s",student[i]→s_sex); printf("%s", student[i]→s_sp); printf("\n");} }
【例2.2】 有顺序表A和B,其元素均按从小到大的升序排列,编写一个算法将它们合并成一顺序表C,要求表C也是从小到大的升序排列。算法思路:依次扫描A和B的元素的值,将较小值的元素赋给C,如此直到一个线性表扫描完毕,然后将未扫描完的那个顺序表中余下部分赋给C即可。C的容量要能容纳A、B两个线性表相加的长度。 【算法2.4】 有序表的合并算法。 void merge (Sqlist A, Sqlist B, Sqlist * C) { int i,j,k; i=0; j=0; k=0; while (i<=A.len && j<=B.len) if (Aelem[i]<Belem[j]) C→elem[k++]=Aelem[i++]; else C→elem[k++]=Belem[i++];
while(i<=Alen) C→elem[k++]=Aelem[i++]; while (j<=Blen) C→elem[k++]=Belem[j++]; C→len=k-1; } 读者可自己分析上述算法的时间复杂度。其时间复杂度为O(m+n),其中m是A的表长,n是B的表长。
第三节 线性表的链式存储及运算实现 从上一节的讨论可知,线性表顺序存储的特点是:物理位置上相邻的元素在逻辑关系上也是相邻的,这就是物理关系和逻辑关系的一致性。这一特点使顺序表有以下的优点: 1. 可以方便地随机读取表中任一元素,读取任一元素所花的时间相同。 2. 存储空间连续,不必增加额外的存储空间。 但顺序存储的缺点也很明显,其缺点是: 1. 插入和删除运算时,除特殊位置外一般要移动大量的元素,所花时间较多,效率较低。 2. 由于顺序表要求连续的存储空间,存储分配只能预先进行,因此当表长经常变化时,难以确定合适的存储空间量。 为了克服顺序的缺点,本节讨论线性表的另一种存储结构——链式存储结构。线性表的链式存储结构是用内存
中一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。通常我们将链式存储的线性表称为链表。但是在下面的讨论中也可以发现,链式存储结构相对于顺序存储结构最大的不同是:数据的逻辑结构和物理存储相互独立,物理位置上相邻的元素在逻辑关系上不一定相邻。中一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。通常我们将链式存储的线性表称为链表。但是在下面的讨论中也可以发现,链式存储结构相对于顺序存储结构最大的不同是:数据的逻辑结构和物理存储相互独立,物理位置上相邻的元素在逻辑关系上不一定相邻。 一、单向链表 (一)单向链表的概念 为了表示每个数据元素之间的逻辑关系,在存储数据元素时除了存储数据元素值的本身外,还需存储指向其后继数据元素的信息,即指出后继元素的存储地址。由这两部分组成一个结点,每个结点包括两个域:一个域存储数据元素信息,称为数据域(data);另一个存储直接后继结点的地址,称为指针域(next),指针域中存储的信息称为指针。 链表中结点的结构如下:
链表正是通过每个结点的指针域将线性表中n个结点按其逻辑顺序链接在一起的。由于上述链表中的每一个结点只有一个指针域,这种链表又称为单向链表,简称单链表。链表正是通过每个结点的指针域将线性表中n个结点按其逻辑顺序链接在一起的。由于上述链表中的每一个结点只有一个指针域,这种链表又称为单向链表,简称单链表。 单链表中除第一个结点外的每个结点的存储地址都存放在其直接前驱结点的next域中。设头指针head指向开始结点,即存放第一个结点的起始地址。终端结点无后继结点,因此终端结点的next域为空,即NULL(也可以用∧表示)。图2.4是与线性表L对应的单链表存储结构示意图。 图2.4 单链表存储结构示意图
L=(A,B,C,D,E,F),设链表头指针head=20,L单链表的示意图如图2.5所示。 用C语言描述单链表的结点结构如下: typedef struct LNode {ELEMTP data; /* 数据域*/ struct LNode *next; /*指针域*/ }LNode,*LinkList; 每个单链表必须有一个头指针指向(存放)表中第一个结点(地址)。已知一单链表,就是已知了链表的起始地址,即头指针。因此单链表可以用头指针的名字来命名。例如,头指针的名字是head,则把链表称为表head。 图2.5 L单链表示意图
(二) 单向链表上基本运算的实现 1. 建立单链表 设线性表中结点的数据类型为字符,依次输入这些字符,并以“$”作为输入结束标志符。动态地建立单链表的常用方法有下面两种: (1) 头插入法建表 该方法的思路是从一个空表开始,重复读入数据,生成新结点,将读入的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直至读到结束标志符为止。图2.6给出了在空链表head中依次插入a,b,c之后,将d插入到当前链表表头上的情况。头插入法建立单链表的算法如下: 图2.6 头插入法建表示意图
【算法2.5】 头插入法建单链表的算法。 main( ) { LinkList *head, *t; char ch; while ((ch=getchar( ))'$') {t=malloc(sizeof(LinkList)); /*对应图2.6中的①*/ t→data=ch;/*对应图2.6中的②*/ t→next=head;/*对应图2.6中的③*/ head=t;}/*对应图2.6中的④*/ } (2)尾插入法建表 头插入法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插入法链表,即每次将新结点插在链表的表尾。为此必须增加一个指针Last,使其始终指向当前链表的尾结点。图2.7给出了在空链表head中插入a,b,c之后,将d插到当前链表表尾上的情况。
分析尾插入法建立单链表的过程:在一般情况下,插入一个元素对应的指针操作是:分析尾插入法建立单链表的过程:在一般情况下,插入一个元素对应的指针操作是: last→next=t;/*对应图2.7中的③*/ last=t;/*对应图2.7中的④*/ 而当单链表为空链表(head=NULL, last=NULL)又是插入第一个元素时,对应的操作是: head=t; last=t; 图2.7 尾插入法建表示意图
可见,当单链表为空链表而又是插入第一个元素时的情况比较特殊。为了使链表上有些操作实现起来简单、清晰,通常在链表的第一个结点之前增设一个类型相同的结点,称为头结点。带头结点的链表通常有两个优点。首先,线性表中的第一个元素结点的地址被存放在头结点的指针域中,这样表中所有元素结点的地址均放在其直接前驱结点中,算法中对所有元素结点的处理可一致化;其次,无论链表是否为空,头指针均指向头结点,给算法的处理带来方便。带头结点的单链表如图2.8所示,图中阴影部分表示头结点的数据域不存储信息。但在有的应用中,可利用该域来存放表的长度等附加信息。可见,当单链表为空链表而又是插入第一个元素时的情况比较特殊。为了使链表上有些操作实现起来简单、清晰,通常在链表的第一个结点之前增设一个类型相同的结点,称为头结点。带头结点的链表通常有两个优点。首先,线性表中的第一个元素结点的地址被存放在头结点的指针域中,这样表中所有元素结点的地址均放在其直接前驱结点中,算法中对所有元素结点的处理可一致化;其次,无论链表是否为空,头指针均指向头结点,给算法的处理带来方便。带头结点的单链表如图2.8所示,图中阴影部分表示头结点的数据域不存储信息。但在有的应用中,可利用该域来存放表的长度等附加信息。 图2.8 带头结点的单链表head
引入头结点后,尾插入法建表的算法如下: 【算法2.6】 尾插入法建单链表的算法。 main() { LinkList*head, *last, *t; char ch; t=malloc (sizeof(LinkList)); /*建立表头结点*/ head=t; last=t; t→next=NULL; while ((ch=getchar())'$') { t=malloc(sizeof(LinkList)); /*对应图2.7中的①*/ t→data=ch; /*对应图2.7中的②*/ last→next=t; /*对应图2.7中的③*/ last=t; /*对应图2.7中的④*/ t→next=NULL;} } 显然,在建立了头结点的单链表上做插入操作算法比较简捷,因此,链表中一般都附加一个头结点。 以上两个算法的时间复杂度均为O(n)。
2. 查找运算 (1) 按序号查找 在链表结构中,如果要访问线性表中序号为i的结点,只能从链表的头指针出发,顺着next域往下搜索,直至搜索到第i个结点为止。 设链表的长度为n,将头结点看成是第0个结点。可以从头结点开始顺着链扫描,用指针p指向当前扫描到的结点,用j作计数器,累计当前扫描过的结点数。p的初值指向头结点,j的初值为0,当p扫描下一个结点时,计数器j相应地加1。因此当j=i时,p所指的结点就是要找的第i个结点。 下面就是在带头结点的单链表head中查找第i个结点的算法。若找到第i个结点(0≤i≤n),则返回该结点的存储位置。否则返回NULL。 【算法2.7】 按序号查找结点的算法。 LinkList *get(int i,LinkList *head) { int j; LinkList *p;
j=0; p=head; while ((j<i)&&(p→nextNULL)) {p=p→next; j++;} if (j==i) return p; else return NULL; } (2) 按值查找 下面的算法是在带头结点的单链表中查找是否存在元素值等于给定值x的结点。若有,则返回第一个找到的结点的存储位置。否则返回NULL。查找过程从头指针出发。 【算法2.8】 按值查找结点的算法。 LinkList *locate(ELEMTP x, LinkList *head) { LinkList *p; p=head→next; while (pNULL)
if (p→data==x) return p; else p=p→next; return NULL; } 3. 插入运算 (1) 在已知结点的后面插入一新结点 设指针p指向某一结点,t指向待插入的值为x的新结点。若将*t结点插在*p之后,则操作比较简单,插入过程如图2.9所示。算法如下: 图2.9 在已知结点*p之后插入*t结点
【算法2.9】 在已知结点的后面插入一新结点算法。 void insertafter (ELEMTP x, LinkList *p) { LinkList*t; t=malloc (sizeof (LinkList)); /*对应图2.9中的①*/ t→data=x; /*对应图2.9中的②*/ t→next=p→next; /*对应图2.9中的③*/ p→next=t; /*对应图2.9中的④*/ } (2) 在第i个结点之前插入一个新结点 首先生成一个值为x的新结点,然后插入到链表L中第i个结点之前,也就是插入到第i-1个结点之后。因此我们可以调用前面的函数get得到第i-1个结点的存储位置,再调用上面的insertafter函数,就完成了这一插入操作。i的合法取值范围是1≤i≤n+1。算法正常,返回1。否则返回0。算法如下: 【算法2.10】 在第i个结点之前插入新结点算法。 int insertbefore (ELEMTP x, int i, LinkList *head
{ LinkList *p int r=1; p=get(i-1,head); if (pNULL) insertafter (x,p); else r=0; return r; } 4. 删除运算 (1) 删除已知结点的后继结点 删除链表中结点*p的后继结点很简单,用一个指针t指向被删除结点,然后修改结点*p的next指针域并释放结点*t,其过程如图2.10所示。算法正常,返回1。否则返回0。算法如下:
【算法2.11】 删除已知结点的后继结点算法。 int deleteafter(LinkList *p) { LinkList *t; int r=1; if (p→nextNULL) {t=p→next; /*对应图2.10中的①*/ p→next=t→next; /*对应图2.10中的②*/ free(t); /*对应图2.10中的③*/ } else r=0 return r; } 图2.10 删除*p结点的后继结点
(2)删除已知结点本身 若被删除结点就是指针p所指结点本身,则必须要知道*p结点的直接前驱结点*q的地址,才能修改*q的指针域,并将*p结点删除。删除过程如图2.11所示。 实现操作的语句组如下: {while (q→nextp) q=q→next; /*寻找p结点的直接 驱,对应图2.11中的①*/ q→next=p→next; /*修改*q的指针域,对应图2.11中的②*/ free(p); /*对应图2.11中的③*/ 图2.11 删除已知结点*p
(3)在单链表中删除第i个结点 实现该操作的算法的思路是必须找到被删除结点i的直接前驱,即第i-1个结点*p,然后删除结点*p的后继结点。因此我们可以调用前面的函数get得到i-1个结点的存储位置,再调用上面的deleteafter函数,就完成了删除第i个结点的操作。算法正常,返回1。否则返回0。算法中须注意的是if((pNULL)&&(p→nextNULL))语句,只有当第i-1个结点存在(pNULL)而又不是终端结点时(p→nextNULL),才能确定被删除结点存在。算法如下: 【算法2.12】在单链表中删除第i个结点的算法。 int deleteorder (int i, LinkList *head) { LinkList *p; int r=1; p=get(i-1;head) if ((pNULL)&&(p→nextNULL))
deleteafter(p); else r=0; return r; } 从上面的讨论中可以看出,在单链表中插入或删除一个元素时,都要从链表的头结点开始,先顺链向后寻找插入或删除的位置,无需移动结点,仅需修改指针,然后进行插入或删除。所以,若表长为n,则上述算法的时间复杂度均为O(n)。 二、循环链表 循环链表(circular linked list)是一种首尾相接的链表。只需将单链表中终端结点的指针域NULL改为指向单链表的第一个结点,就得到了单链形式的循环链表。在有些应用问题中,用循环链表可使操作更加方便灵活。循环链表中也可设一个头结点。
空循环链表仅由一个头结点组成,并自成循环。带头结点的循环链表如图2.12所示。空循环链表仅由一个头结点组成,并自成循环。带头结点的循环链表如图2.12所示。 图2.12 带头结点的循环链表示意图 循环链表的基本操作类似于普通链表(单向链表),其差别在于算法中循环条件不再是p或p→next是否为空,而是它们是否等于头指针。
循环链表的特点是从表中任一结点出发均可找到表中其他所有结点。在很多实际问题中,链表的操作常常是在表的首尾位置上进行的,此时用图2.12表示的循环链表就显得不够方便。如果改用尾指针rear来表示(见图2.13),则查找开始结点a1和终端结点an都很方便,它们的存储地址分别由rear→next→next和rear指出。显然,查找时间都是O(1)。因此,经常采用尾指针表示循环链表。循环链表的特点是从表中任一结点出发均可找到表中其他所有结点。在很多实际问题中,链表的操作常常是在表的首尾位置上进行的,此时用图2.12表示的循环链表就显得不够方便。如果改用尾指针rear来表示(见图2.13),则查找开始结点a1和终端结点an都很方便,它们的存储地址分别由rear→next→next和rear指出。显然,查找时间都是O(1)。因此,经常采用尾指针表示循环链表。 图2.13 用rear指针表示的循环单链表
三、双向链表 在单链表中,从任何一个结点都能通过指针域找到它的后继结点,但要寻找它的前驱结点,则需从表头出发顺链查找。 双向链表克服了这个缺点。双向链表的每一个结点除了数据域外,还包含两个指针域,一个指针指向该结点的直接后继结点,另一个指针指向它的直接前驱结点。其结点结构如图2.14(a)所示。这样形成的链表中有两条不同方向的链,称之为双向链表(double linked list)。双向链表也可以是循环链表,对头结点和尾结点链接起来就构成了循环链表,并称之为双向循环链表,如图2.14(b),(c)所示。 图2.14 带头结点的双向循环链表示意图
用C语言描述双向链表的结点结构如下: typedef struct dnode { ELEMTP data; struct dnode *prior, *next; } DLinkList; 双向链表是一种对称结构,它克服了单链表上指针单向性的缺点,既有前向链又有后向链,可以从两个方向搜索某个结点,这就使得双向链表上数据元素的插入操作和删除操作都很方便。设指针p指向表中的某一结点,则表结构的对称性体现在下列式子中: p=(p→prior)→next=(p→next)→prior (一)在双链表中p指针指向的结点前插入一新结点 插入过程如图2.15所示。其算法如下: 图2.15 在双向链表中插入一个结点
【算法2.13】 在双向链表中插入一个结点的算法。 void dinsertbefore (ELEMTP x, DLinkList *p) { DLinkList*t; t=malloc (sizeof (DLinkList)); t→data=x; t→prior=p→prior; /*对应图2.15中的①*/ t→next=p; /*对应图2.15中的②*/ (p→prior)→next=t;/*对应图2.15中的③*/ p→prior=t; /*对应图2.15中的④*/ } (二)在双链表中删除p指针指向的结点 删除过程如图2.16所示。其算法如下: 图2.16 在双向链表中删除一个结点