1.67k likes | 1.76k Views
第二章 线性表. 2.1 线性表的基本概念 2.2 顺序表 2.3 链表 2.4 顺序表和链表比较 2.5 链表的应用. 2.1 线性表的基本概念 2.1.1 定义. 线性表是一种线性结构,是某类东西的集合,每个线性表的数据元素都具有以下共性: 1 、集合中的所有元素都是同一种数据类型的; 2 、这些集合具有有限的大小; 3 、集合中的元素都是线性排列的,即存在一个首元素和一个尾元素;除了尾元素外,每个元素都有一个后继;除了首元素外,每个元素都有一个前驱。. 2.1.1 定义.
E N D
第二章 线性表 2.1 线性表的基本概念 2.2 顺序表 2.3 链表 2.4 顺序表和链表比较 2.5 链表的应用
2.1 线性表的基本概念2.1.1 定义 线性表是一种线性结构,是某类东西的集合,每个线性表的数据元素都具有以下共性: 1、集合中的所有元素都是同一种数据类型的; 2、这些集合具有有限的大小; 3、集合中的元素都是线性排列的,即存在一个首元素和一个尾元素;除了尾元素外,每个元素都有一个后继;除了首元素外,每个元素都有一个前驱。
2.1.1 定义 数据元素“一个接一个的排列”的关系叫做线性关系,线性关系的特点是“一对一”,在计算机领域用“线性表”来描述这种关系。另外,在一个线性表中数据元素的类型是相同的,或者说线性表是由同一类型的数据元素构成的,如学生情况信息表是一个线性表,表中数据元素的类型为学生类型;一个字符串也是一个线性表:表中数据元素的类型为字符型等等。
2.1.1 定义 综上所述,线性表定义如下: 线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,通常记为: (a1,a2,… ai-1,ai,ai+1,…an) 其中n为表长,n=0 时称为空表。 表中相邻元素之间存在着顺序关系。将 ai-1称为 ai的直接前驱,ai+1称为 ai 的直接后继。就是说:对于ai,当 i=2,...,n 时,有且仅有一个直接前驱ai-1,当i=1,2,...,n-1 时,有且仅有一个直接后继 ai+1,而 a1是表中第
2.1.1 定义 个元素,没有前驱;an 是最后一个元素,无后继。 需要说明的是:ai是序号为 i 的数据元素(i=1,2,…,n),通常我们将它的数据类型抽象为DataType,DataType根据具体问题而定,如在学生情况信息表中,它是用户自定义的学生类型; 在字符串中,它是字符型等等。
2.1.2 线性表的存储结构 数据的存储结构是依赖于计算机的,常见的存储结构有顺序存储结构、链式存储结构、索引存储结构和散列存储结构四种,参考1.1.2节的第1小节。线性表通常使用这其中的两种存储结构:顺序存储和链式存储结构。 1.顺序存储结构 该存储结构是把逻辑上相邻的结点存储在物理位置上相邻的存储单元里,结点之间的逻辑关系由存储单元的邻接关系来体现。
2.1.2 线性表的存储结构 使用顺序存储结构存储数据的线性表叫顺序表(Sequence List)。这种结构的主要优点是节省存储空间,因为分配给数据的存储单元全用于存放结点的数据,结点之间的逻辑关系没有占用额外的存储空间。 采用这种方法存储,可实现对结点的随机访问,即每个结点对应有一个序号,由该序号可直接计算出该结点的存储地址。但不便于修改,对结点的插入、删除运算,可能要移动大量的结点。
2.1.2 线性表的存储结构 2.链式存储结构 该结构不要求逻辑上相邻的结点在物理位置上也相邻,结点之间的逻辑关系是由额外的指针域来表示的。其结构如图2.10。 链式结构的主要优点是便于修改。在进行插入、删除运算时,仅需修改结点的指针域值,不必修改结点。其主要缺点是存储空间的利用率低,因为分配给数据的存储单元有一部分被用来存储节点之间的逻辑关系了。另外,由
2.1.2 线性表的存储结构 于逻辑上相邻的结点在存储器中不一定相邻,在访问线性表中指定的元素时必须从首元素开始查找,所以不能对结点进行随机访问,因为查找链表中的元素必须沿着链表的指针逐个进行,因而所有的运算都必须从链表的头开始。
2.1.3 线性表的运算 数据结构的运算是定义在逻辑结构层次上的,而运算的具体实现是建立在存储结构上的,因此下面定义的线性表的基本运算作为逻辑结构的一部分,每一个操作的具体实现只有在确定了线性表的存储结构之后才能完成。 假设存在一个线性表L,长度为n,用x表示一个元素,用i表示元素序号。下面具体给出对线性表的一些具体操作: ⑴线性表初始化:InitList(L)。
2.1.3 线性表的运算 如果线性表L不存在,构造一个空的线性表L(没有任何数据元素)。 ⑵求线性表的长度:Length(L)。 当线性表L存在时,返回L中的所含元素的个数。 ⑶取表元:Get(L,i) 当线性表L存在且1≤i≤n时,返回线性表L中的第i个元素的值。 ⑷查找:Locate(L,x)
2.1.3 线性表的运算 当线性表L存在时,在表L中查找值为x的数据元素,其结果返回在L中首次出现的值为x的那个元素的序号或地址,称为查找成功;否则,在L中未找到值为x的数据元素,返回一特殊值表示查找失败。 ⑸插入操作:Insert(L,i,x) 当线性表L存在且插入位置适当 (1≤i≤n+1)时,可以在L的第 i 个位置上插入一个值为 x 的新元素,这样使原序号为 i , i+1, ... , n 的数据元素的序号变为
2.1.3 线性表的运算 i+1,i+2, ... , n+1,插入后表长=原表长+1。 如果i=1,则在表头插入x;如果i=n+1,则在表尾插入x。 ⑹删除操作:Delete(L,i) 当线性表L存在且1≤i≤n,在L中删除序号为i的数据元素,删除后使序号为 i+1, i+2,…, n 的元素变为序号依次变为 i, i+1,...,n-1,新表长=原表长-1。如果i=1,则删除表头元素;如果i=n,则删除表尾元素。 ⑺线性表判空:Empty(L)
2.1.3 线性表的运算 如果线性表L存在,判断L是否为空。若为空返回真,否则返回假。这是一个逻辑函数。 ⑻显示表元素:DispList(L) 如果线性表L存在,显示线性表的所有元素。 注意: 1. 某数据结构上的基本运算,不是它的全部运算,而是一些常用的基本运算,而每一个基本运算在实现时也可能根据不同的存储结构派生出一系列相关的运算来,没
2.1.3 线性表的运算 有必要,也不可以把所有的基于不同结构和不同想法的运算都罗列出来。 2. 在上面各操作中定义的线性表L仅仅是一个抽象在逻辑结构层次的线性表,尚未涉及到它的存储结构,因此只能写出运算的功能,还不能写出具体算法,而算法的实现要依赖于下面讲述的数据存储结构。
2.2 顺序表2.2.1 顺序存储结构 线性表采用在内存中用地址连续的一块存储空间顺序存放各元素值,各个元素之间的相邻关系用内存地址的相邻关系表示,这种存储形式存储的线性表叫顺序表。因为内存中的地址空间是线性的,因此,用物理地址上的相邻实现数据元素之间的逻辑相邻关系既简单又自然。这种结构最适合于用高级语言中的数组来描述。 为了统一起见,今后的描述中都使用DataType这样的抽象数据类型来表示特定的数据类型,使读者跳出各种具体的
2.2.1 顺序存储结构 数据类型的限制,便于理解数据结构的本质。 定义2.1 #define MAX 100 /*表空间大小设为100*/ typedef int DataType; /*DataType可以是任何相应的数据类型如int, float或char*/ typedef struct { DataType data[MAX]; /*一维数组data用于存放表结点元素*/ int length; /*用于描述线性表的实际元素个数*/ }SeqList; 此定义通过typedef定义了一个结构体变量SeqList,这个变量可以用来定义其它的同结构的结构体变量。同时约定,
2.2.1 顺序存储结构 表为空时,length=-1。 上述定义可以形象地表示成图2.2。请注意元素的下标与存储单元地址正好差1。 根据定义2.1,可以用两种不同方式定义顺序表。 SeqList L ;
2.2.1 顺序存储结构 这样表示的线性表如图2.3(a) 所示。表长=L.length,线性表中的数据元素a1至an分别存放在L.data[0]、L.data[1]、……、L.data[L.length-1]中。
2.2.1 顺序存储结构 另一种方法是定义一个指向SeqList 类型的指针: SeqList *L ; L是一个指针变量,线性表的存储空间通过C语言中在内存创建块的函数malloc()创建,具体操作语句为: L=malloc(sizeof(SeqList))。 L中存放的是顺序表的地址,这样表示的线性表如图2.3(b)所示。表长表示为L->length,线性表的存储区域为 L->data ,线性表中数据元素的存储空间为:L->data[0]、L->data[1]、……、L->data[L->length-1]。
2.2.2 顺序表的运算 1. 顺序表的初始化 顺序表的初始化即构造一个空表,将L设为指针参数,动态分配存储空间,然后,将表中length置为-1,表示表为空。算法如下: 算法2.1 顺序表初始化 SeqList *InitList(SeqList *L) { L->length=-1; return L; }
2.2.2 顺序表的运算 此算法相当简单,执行此算法所需的时间为O(1)。 2. 顺序表的建立 顺序表的建立是在一个空线性表的基础上,逐一向线性表中输入元素的操作。 算法2.2 顺序表创建 void CreateList(SeqList *L,int n) /*建立含n个元素的线性表*/ { for (i=0;i<n;i++) /*逐一输入n个元素保存在内存中*/ scanf("%d",&L->data[i]);
2.2.2 顺序表的运算 L->length=n; /*修改线性表的长度length */ } 此算法需要输入n个数据元素,因此执行此算法所需的时间为O(n)。 3. 求线性表长度 算法2.3 求线性表长度 int Length(SeqList *L) { return L->length; /*返回链表长度length*/ }
2.2.2 顺序表的运算 4. 读表元素 算法2.4 读线性表元素 DataType Get(SeqList *L,int i) { if(i<0 || i> L->length) return -1; else return L->data[i-1]; } 当位置i不正确时,返回“-1”,正确时返回第i个元素值。执行此算法所需的时间为O(1)。
2.2.2 顺序表的运算 5. 查找 线性表中的查找是指在线性表中查找与给定值x相等的数据元素。在顺序表中完成该运算最简单的方法是:从第一个元素 a1 起依次和x比较,直到找到一个与x相等的数据元素,则返回它在顺序表中的存储下标或序号;或者查遍整个表都没有找到与 x 相等的元素,返回-1。 算法2.5 在线性表中查找元素 算法如下:
2.2.2 顺序表的运算 int Locate(SeqList *L, DataType x) { while(i<=L->length && L->data[i]!= x) i++; if (i>L->length) /*i超过最大下标,查找失败*/ { printf(“没有找到”); return -1; } else /*找到i,表明查找成功*/ { printf(“查找成功,位于第%d个位置”, i+1); return i; /*返回元素存储位置*/ } }
2.2.2 顺序表的运算 本算法的主要运算是比较。显然比较的次数与x在表中的位置有关,也与表长有关。当 a1=x 时,比较一次成功。当 an=x 时比较 n 次成功。平均比较次数为(n+1)/2,所需的时间为O(n)。 6. 插入 线性表的插入算法比较前几种算法要复杂一些,可以分为一般线性表和有序线性表两种操作。
2.2.2 顺序表的运算 (1)一般线性表的插入运算 向线性表L中的第i个位置插入一个元素x,插入成功返回1,否则返回0。 线性表的插入是指在表的第i个位置上插入一个值为 x 的新元素,插入后使原表长为 n的表(a1,a2,... ,ai-1,ai,ai+1,... ,an)成为表长为 n+1 的表(a1,a2,...,ai-1,x,ai,ai+1,...,an),i 的取值范围为1≤i≤n+1。
2.2.2 顺序表的运算 插入操作时需要考虑算法的功能性和健壮性。 插入操作的功能性是指该算法能否把元素插入到线性表的正确位置。首先,在第i个位置插入元素x后,第1个元素(下标为0)到第i-1个元素(下标为i-2)的位置不变,而第i个元素(下标为i-1)到最后一个元素(第n个元素,下标为n-1)都后移了一位,腾出一个空位(第i个元素的位置),然后将待插元素填入空位即可。其次,第i到第n个元素往后移动一个位置时,怎样移动比较安全,以保证后移元素的正
2.2.2 顺序表的运算 确性?为了保证第i个元素到第n个元素安全往后移,同时也不被覆盖,采用从线性表的尾部逐个移动元素的方法,即把第n个元素移动到第n+1的位置,第n-1个元素移动到第n个位置,……,直到将第i个元素移动到第i+1的位置为止。具体的插入操作参考图2.4。 算法的健壮性是指当算法遇到不合理的数据输入时,能够作出基本处理而不至于使系统崩溃或得到意想不到的结果。一般的做法是检测错误并给出一个出错信息。
2.2.2 顺序表的运算 综上所述,可以得到插入算法的思想: ①行插入位置的合法性检查,是否有1≤i≤n+1(n+1为表尾后的位置),若i<1或i>n+1,则表明i值超界,无法插入,给出提示信息。 ②检查线性表的存储空间是否已被占满。 ③从表尾元素向前至第i个元素(下标为i-1)止,逐一后移一个存储位置,空出下标为i-1的位置,以便插入新元素。
2.2.2 顺序表的运算 ④将新元素插入到第i个元素位置,即下标为i-1的位置。 ⑤将表的长度加1。 ⑥返回1表示插入成功。 从而可得出顺序表上插入操作的算法如下: 算法2.6a 在一般线性表中插入元素 int Insert(SeqList *L,int i,DataType x) { if (i<1 || i>L->length+1)/*检查插入位置的正确性,表尾后也可插入*/ { printf("位置错");return 0; } if (L->length==MAX) /*表空间已满,不能插入*/
2.2.2 顺序表的运算 { printf("表满"); return -1; } for(j=L->length-1;j>=i-1;j--) L->data[j+1]=L->data[j]; /* 结点移动,腾空第i-1位置 */ L->data[i-1]=x; /*插入x*/ L->length++; /*修改表长*/ return 1 ; /*插入成功,返回*/ } 本算法的第一个条件语句检验插入位置的有效性。第二个条件语句检查表空间是否满(MAX个存储单元),在表满的情况下不能再做插入,否则产生溢出错误。 此算法的两个关键操作:移动元素(for循环),插入新
2.2.2 顺序表的运算 元素L->data[i-1]=x。 插入算法的时间性能分析。顺序表上的插入运算,时间主要消耗在了数据的移动上,在第i个位置上插入 x ,从 ai到 an都要向下移动一个位置,共需要移动 n-(i-1)个元素,而 i 的取值范围为 :1≤i≤n+1,即有 n+1个位置可以插入。设在第i个位置上作插入的概率为Pi,则平均移动数据元素的次数:
2.2.2 顺序表的运算 在等概率的情况下:Pi=1/ (n+1) ,则: 这说明:在顺序表上做插入操作需移动表中一半的数据元素。显然时间复杂度为O(n)。 当然,算法2.6a是在表中进行的,具有一般性,那么,作为特例,如何在表头或表尾插入元素x呢?这留着习题让大家去思考。算法2.6a是在线性表元素未经过排序的情况下进行的插入操作。下面讨论对于一个有序的线性表,在插入元
2.2.2 顺序表的运算 素x后,仍然保持有序,应该如何操作? (2)有序线性表的插入操作 线性表中的元素按元素值从小到大顺序排列,或按元素值从大到小顺序排列,这种线性表称为有序表,即有序的线性表。这里,我们讨论从小到大排序的线性表。 该操作应该考虑如下的因素: 首先,健壮性,只需考虑表长是否超界;因为必需保证插入元素后线性表仍然有序,可以不考虑插入位置是否有效。
2.2.2 顺序表的运算 其次,寻找插入位置,以便留出空位将待插元素和线性表中的元素从头开始逐一比较,找到当待插元素刚好小于或等于某个元素时为止,该位置即为待插元素的位置。 第三,进行移动元素,插入元素,修改表长度的操作。 这样,可以得到有序表的插入算法如下: 算法2.6b 在有序表中插入元素 /*在有序线性表中插入元素,插入后使该表仍然有序*/ int Insert2(SeqList *L,DataType x)
2.2.2 顺序表的运算 { if (L->length==MAX) {printf("表满"); return -);} /*表空间已满,不能插入*/ for(i=0; i<L->length; i++) /* 寻找插入位置i */ if(x<=L->data[i]) break; for(j=L->length-1;j>=i-1;j--) /* 结点移动 */ L->data[j+1]=L->data[j]; L->data[i]=x;/*新元素插入*/ L->length++; /*修改表长*/ return 1; }
2.2.2 顺序表的运算 在算法2.6b中,运行时间主要花费在寻找插入位置的比较元素次数和腾出空位需移动的次数上,其和为n的一次函数,所以时间复杂度为T(n)=O(n)。 图2.5中给出了在有序表L=(3,9,15,20,22,30,45,47)插入元素26的操作示意。
2.2.2 顺序表的运算 据此,读者可以考虑尝试利用有序表插入算法的思想,来建立一个线性表? 7. 删除 从线性表L中删除第i个元素并返回该元素的值,若删除失败,则返回出错信息并停止程序运行。 线性表的删除运算是指将表中第 i 个元素从线性表中去掉,删除后使原表长为 n 的线性表:(a1,a2,... ,ai-1,ai,ai+1,...,an)成为表长为 n-1 的线性表:(a1,a2
2.2.2 顺序表的运算 ,... ,ai-1,ai,ai+1,...,an)成为表长为 n-1 的线性表:(a1,a2,... ,ai-1, ai+1,... ,an),i 的取值范围为:1≤i≤n 。 从图2.6可以看出,线性表的删除操作,是将表长为n的线性表变为表长为n-1的线性表。若删除的是第i个元素(下标为i-1),则从第i+1个元素到第n个元素(最后一个元素)依次往前移动了一个位置,表长减少了1。在移动元素时,从安全性考虑,采用将第i+1个元素移动到第i个元素,将第i+2
2.2.2 顺序表的运算 个元素移动到第i+1个元素,……,直到第n个元素移动到第n-1个元素为止。
2.2.2 顺序表的运算 要实现删除元素的算法,我们先考虑其健壮性,后考虑其功能性,经过上述分析,顺序表上的删除操作算法可以设计如下: 算法2.7 在线性表中删除元素 DataType Delete(SeqList *L,int i) { if(i<1 || i>L->length) /*检查删除位置的合法性*/ { printf ("不存在第i个元素\n"); return 0; } temp= L->data[i-1]; /*暂时保存待删元素,以便返回*/ for(j=i;j<=L->length;j++) /*n-i个元素向前移动*/ L->data[j-1]=L->data[j]; L->length--; /*修改表长*/
2.2.2 顺序表的运算 printf(“删除了第i个元素,其值为:%d\n”, temp); /*返回被删元素*/ return 1; /*删除成功,返回1*/ } 本算法中条件(i<1 || i>L->length)包括了对空表的检查和对删除位置的有效性检查。删除 ai之后,该数据已不存在,如果需要,先取出 ai,再做删除。 删除算法的时间性能分析。与插入运算相同,其时间主要消耗在了移动表中元素上,删除第i个元素时,其后面的元素 ai+1~an都要向上移动一个位置,共移动了 n-i 个元
2.2.2 顺序表的运算 素,所以平均移动数据元素的次数: 在等概率情况下,pi =1/ n,则: 这说明顺序表上作删除运算时大约需要移动表中一半的元素,显然该算法的时间复杂度为O(n)。 图2.7是在线性表L=(3,9,15,20,22,30,45,47)中删除元素22的操作示意。
2.2.3 遍历 遍历是数据结构中最重要的运算之一,所有关于数据结构的运算都建立在遍历的基础上,所以我们把这个内容独立成节。 遍历一个线性表就是从线性表的第一个元素起,依次访问表中的每一个元素。每个元素只访问一次,直到访问完所有元素为止,如图2.8。 要实现遍历算法,先看是否需要考虑健壮性?因为不存在插入、删除,没有元素的移动,从而就没有存储空间的增
2.2.3 遍历 删。因此,本算法的健壮性可不予考虑,只考虑功能性即可。访问a1以后,指针往后移到a2,…,直到访问an止,只需将下标增1即可。算法如下: 算法2.8 线性表的遍历算法 void TraverseList(SeqList *L ) { for(i=0;i<L->length;i++) visit(L->data[i]); } 算法2.8中的visit()操作,含义十分广泛,稍作修改,