750 likes | 1k Views
第 2 章 线性表. 2.1 线性表的基本概念 2.2 线性表的顺序存储 2.3 线性表的链式存储结构 2.4 一元多项式的表示及相加. 2.1 线性表的类型定义 . 从本章开始到第四章讨论的线性表、栈、队列和串的逻辑结构都属于线性结构。在线性结构中,元素之间存在一个对一个的相互关系,其逻辑特征为:在数据元素的 非空有限 集中: ( 1 )存在唯一的一个被称为“起始”的数据元素。 ( 2 )存在唯一的一个被称为“终端”的元素。. ( 3 )除起始元素外,其它每个元素有且仅有一个直接前趋。
E N D
第2章 线性表 • 2.1 线性表的基本概念 • 2.2 线性表的顺序存储 • 2.3 线性表的链式存储结构 • 2.4 一元多项式的表示及相加
2.1 线性表的类型定义 从本章开始到第四章讨论的线性表、栈、队列和串的逻辑结构都属于线性结构。在线性结构中,元素之间存在一个对一个的相互关系,其逻辑特征为:在数据元素的非空有限集中: (1)存在唯一的一个被称为“起始”的数据元素。 (2)存在唯一的一个被称为“终端”的元素。
(3)除起始元素外,其它每个元素有且仅有一个直接前趋。 (4)除终端元素之外,其它每个元素有且仅有一个直接后继。 本章的基本内容:线性表的逻辑结构定义和各种存储结构、描述方法及其建立在这两种存储结构上的运算实现。
2.1.1 线性表的逻辑结构 在实际应用中,线性表是最常用而且最简单的一种数据结构。 线性表定义:线性表是由n个数据元素组成的有限序列,其中,n即数据元素的个数,定义为线性表的长度,当n为零时称为空表,一般将n>0时的线性表记为: 其中, 是第一个数据元素,又称为起始结点, 是最后一个数据元素,又称为终端结点。
当i=1,2,…,n-1时, 有且仅有一个直接后继 ;当i =2,3,…n 时, 有且仅有一个直接前趋 。注意这里 (1≤ i≤ n)仅仅是一个抽象的符号,其具体含义,在不同的情况下各不相同,它可以是一个数,一条记录或一个符号,甚至是更复杂的信息。例如,英文字母表(A,B,……Z)是一个线性表,职工工资表等。 线性表的结点之间的逻辑关系符合线性结构的逻辑特征,是一种线性结构。
线性表的特点: • 同一线性表中的元素必定具有相同特性; • 相邻数据元素之间存在着序偶关系; • 线性表中元素个数n(n>=0)为线性表的长度,当n=0时称为空表; • 在非空线性表中,每个数据元素都有一个确定的位置; • 线性表的长度可以根据需要增长或缩短。
抽象数据类型线性表的定义格式 ElemSet 是某个确定的、将由用户自行定义的、含某个关系运算的数据对象 ADT list { 数据对象: 数据关系: 基本操作: 以下是一些常用操作,以函数方式出现 … … } ADT list
2.1.2 线性表的运算 常见的线性表的基本运算: (1)置空表ClearList(L):将线性表L置成空表。 (2)求长度ListLenght(L):求给定线性表L中数据元素的个数。 (3)取元素GetElem(L,i,&e):用e返回线性表L中的第i个数据元素。 (4)插入ListInsert(&L,i, e):在线性表L中第i个位置之前插入新的元素e,L的长度加1。
(5)删除ListDelete (&L,i,&e):删除L中第i个元素,并用e返回其值,L的长度减1。 (6)判定ListEmpty(L):若L为空表,则返回true,否则返回false。 利用基本运算可以实现线性表的其它复杂运算。 需要指出的是,这里给出的只是定义在逻辑结构上的抽象运算,即只关心这些运算是“做什么”的,至于“怎样实现”则依赖于所选定的存储结构。
2.2 线性表的顺序表示和实现 顺序表是线性表的顺序存储结构,即按顺序存储方式构成的线性表的存储结构。其存储方式是:线性表的第一个元素存放在个一片连续的存储空间开始位置处,第二个元素紧跟着存放在第一元素之后…,以此类推。 利用顺序表来存储线性表,可借助数据元素在计算机内的物理位置相邻特性来表示线性表中数据元素之间的线性逻辑关系。 线性表中第 i 个数据元素 的存储位置计算公式为: L 是每个元素占用的存储单元
这样,一旦起始地址LOC(a1)(图2.2中设为b)和一个数据元素占用的存储单元的大小(L值)确定下来,就可求出任一个数据元素的存储地址,显然,这种表便于进行随机访问,因此,顺序表是一种随机存取的结构。这样,一旦起始地址LOC(a1)(图2.2中设为b)和一个数据元素占用的存储单元的大小(L值)确定下来,就可求出任一个数据元素的存储地址,显然,这种表便于进行随机访问,因此,顺序表是一种随机存取的结构。
在具体实现时,可利用高级程序设计语言中的一维数组即向量来为线性表的顺序存储开辟存储空间,存储空间大小应大于等于线性表长度,用一个整型变量来表示线性表的长度,从而可将顺序表描述为:在具体实现时,可利用高级程序设计语言中的一维数组即向量来为线性表的顺序存储开辟存储空间,存储空间大小应大于等于线性表长度,用一个整型变量来表示线性表的长度,从而可将顺序表描述为: # define List_INIT 100 typedef int elemtype ; /* elemtype表示元素类型,此处设为 int */ typedef struct {elemtype vec[List_INIT]; int lenght; }sequenlist ;
在上面描述中,顺序表是一个结构体类型。其中,vec成员(又称为数据域)指线性表数据元素所占用的存储空间,而lenght成员(又称为长度域)则用于存储线性表长度,它表示线性表最后一个元素在向量中的位置,elemtype 则为线性表中数据元素类型。
2.2.2 顺序表上的基本运算 本节讨论在定义线性表顺序存储结构之后,如何在这种结构上实现有关数据运算。下面重点讨论线性表的顺序表的建立、数据元素的插入和删除运算。
3. 顺序表的常用算法 算法1 顺序表的建立 (P23 算法2.3) 输入n个整数,产生一个存储这些整数的顺序表A的函数如下: typedef int vector [ m ] 定义了一个新的类型,顺序表中n小于或等于m main() { vector B; int j,n; cin>>n; create(B,n); for (j=1;j<=n;j++) cout<<B[j]; /*Printf(“%d”,B[j] ); */ } void create(A,n) vector A; int n; { int i; for (i=1;i<=n;i++) cin>>A[i]; /* scanf(“%d”,A[i]); */ }
2.插入运算 线性表的插入运算是指在表的第i个元素之前插入一个新的数据元素,插入的结果使得线性表的长度由n变为n+1,线性表的数据元素间的逻辑关系发生了变化,使得物理存储顺序也发生相应的变化。插入过程如图2.3所示。
插入25 插入后 (b) (a)
插入25 算法2 顺序表的插入 (如图2.3) 在一个有n个元素的顺序表A中的第i个元素之前插入一个元素x的函数如下: void insert(A,n,x) vector A; int n,x; { int i,j; scanf(“%d”,&i); /*确定插入位置*/ if(i<1 || i>n) print(“i值错误!\n”); else { for (j=n;j>=i;j--) A[j+1]=A[j]; A[i]=x; n++; } }
P24【算法2.4 顺序表的插入】 int Insert(Elemtype List[ ],int *num,int i,Elemtype x) {/*在顺序表List[ ]中,*num为表尾元素下标位置,在第i个元素前插入数据元素x,若成功,返回TRUE,否则返回FALSE。*/ int j; if (i<0||i>*num+1) {printf(“Error!”); /*插入位置出错*/ return FALSE;} if (*num>=MAXNUM-1) {printf(“overflow!”); return FALSE;} /*表已满*/ for (j=*num;j>=i;j--) List[j+1]=List[j]; /*数据元素后移*/ List[i]=x; /*插入x*/ (*num)++; /*长度加1*/ return TRUE;}
在本算法中是从最后一个元素开始后移,直到第i个元素为止。该算法时间主要花费在for循环语句上,执行的次数为n-i+1。当i=1时,全部元素均参加移动,共需要移动n次;当i = n+1时,不需移动元素。 算法在最坏情况下,时间复杂度为O(n),最好情况下时间复杂度为O(1)。显然,元素移动的次数直接影响了算法执行时间,平均移动次数。 假设pi为在第i个元素之前插入一个元素的概率,且为等概率,则平均移动次数的期望值为: 其中,当1≤i≤n+1时,p1=p2=… …=pn+1 = 可见,在顺序存储的线性表中插入一个元素,约平均移动线性表中一半的元素,显然,当n较大时,算法效率较低,算法的平均时间复杂度为O(n)。
在本算法中是从最后一个元素开始后移,直到第i个元素为止。该算法时间主要花费在for循环语句上,执行的次数为n-i+1。当i=1时,全部元素均参加移动,共需要移动n次;当i = n+1时,不需移动元素。 算法在最坏情况下,时间复杂度为O(n),最好情况下时间复杂度为O(1)。显然,元素移动的次数直接影响了算法执行时间,平均移动次数。 假设pi为在第i个元素之前插入一个元素的概率,且为等概率,则平均移动次数的期望值为: 其中,当1≤i≤n+1时,p1=p2=… …=pn+1 =
可见,在顺序存储的线性表中插入一个元素,约平均移动线性表中一半的元素,显然,当n较大时,算法效率较低,算法的平均时间复杂度为O(n)。可见,在顺序存储的线性表中插入一个元素,约平均移动线性表中一半的元素,显然,当n较大时,算法效率较低,算法的平均时间复杂度为O(n)。
3.删除运算 删除运算是指将线性表中的第i个元素删除,使线性表的长度由n变成n-1,由:(a1,a2,…,ai-1,ai,ai+1,…,an)变成为: (a1,a2,…,ai-1,ai+1,…,an)其中, 1≤i≤n。 线性表进行删除元素后,仍是一个线性表。删除过程如图2.4所示。
算法3 顺序表的删除 (如图2.4) 具体算法如下: void delete(L,i) sequenlist *L; int i; { int j; if ((i<1) ¦¦ (i>(*L).len+1)) printf(“delete fail!\n”); /*删除位置不正确*/ else { *y =(*L).vec[i-1];/*y为一外部变量,用于接收被删除的元素*/ for(j= i;j<=(*L).len;j ++) (*L).vec[j-1]=(*L).vec[j]; /*元素前移*/ (*L).Len--; /*修改表长*/ } } /*delete*/
与插入算法相似,要删除一个元素需向前移动元素,删除第i个元素时,将第i+1,i+2,…,n个元素依次前移,其移动次数为n-i,假设删除线性表中的任一位置上元素的概率相等(等于1/n),则删除运算所需的移动元素的平均移动次数如下所示。与插入算法相似,要删除一个元素需向前移动元素,删除第i个元素时,将第i+1,i+2,…,n个元素依次前移,其移动次数为n-i,假设删除线性表中的任一位置上元素的概率相等(等于1/n),则删除运算所需的移动元素的平均移动次数如下所示。 由此可见,对线性表删除一个元素时,约需有一半的元素参加移动。
显然,该算法的时间复杂度为O(n)。 通过以上讨论可以发现,当表长较大时,对顺序存储结构进行插入和删除运算,由于要移动元素,运算效率低,但这种存储结构对于随机存取元素却十分方便。
例如,一个线性表中可能含有重复的元素(值相同),现要求删除重复元素中的多余元素,只保留其中位序最小的一个。如线性表(5,2,2,3,5,2),经过清除重复元素后变为( 5,2,3 )。 假定线性表以顺序存储方式存储,则其算法可描述如下:void purge(sequenlist *L ) { int i=0,j ,k;while ( i<=(*L).Len-1) { j = i+1;while ( j<=(*L).Len-1) if ((*L).vec[i]= =(*L).vec[j]) { for ( k =j;k<=(*L). Len-1;k ++) (*L) .vec [k –1] = (*L) .vec [k ];/ *元素前移*/ (*L ) .Len- -;/*修改表长度*/ } else j + +; i + +; } } /* purge */
算法4 顺序表的查找(P26 算法2.6) 在一个有n个元素线性表A中查找值为x的元素的函数如下: void find(A[ ],n,x) int n.x; { int j; i=1; while (i<=n && A[i] !=x ) i++; if (i<=n) printf(“找到了! \n”); else printf (“没找到\n”); }
算法5:顺序表的合并(P26 算法2.7) 有两个顺序表A(有m个元素)和B(有n个元素),其元素均以从小到大的升序排列,编写一个函数将它们合并成一个顺序表C,要求C的元素也是从小到大的升序排列. 解:本题的解法思想是:依次扫描A和B的元素,比较当前的元素的值,将较小值的元素赋给C,如此直到一个表扫描完毕,然后将未完的一个表余下所有元素赋给C即可.
算法2.7C语言代码为: Void link(int A[ ],int B[ ];int m,n;int C[ ]) { int i=1,j=1,k=1,s; while (i<=m && j<=n) /*扫描A和B,将当前的较小元素赋给C */ if A[i]<B[j]) { C[k]=A[I]; i++; k++; } else { C[k]=B[ j]; j++; k++; } if(j==n) /*当A未完时,将A余下的元素赋给C */ for(s=i+1;s<=m;s++) { C[k]=A[s];k++; } if (i==m) /*当B未完时,将B余下元素赋给C*/ for (s=j+1;s<=n; s++) { C[k]=B[s]; k++ ; } }
作 业 1. 基础知识:题集2.1; 2.算法实现:写出教材算法2.4、2.5(线性表中元素的插入和删除算法)的 C/C++语言程序,并上机调试; 3. 算法设计题: 题集2.10、2.11。
2.3 线性表的链式表示和实现 线性表顺序存储结构的特点: (1)逻辑关系上相邻的两个元素在物理位置上也是相邻的,因此可以随机存取表中任意元素(可用地址公式). (2) 无需为表示数据元素之间的关系而增加额外存储空间; (3)可以方便地随机存取表中任一元素。 (4)必须预先为线性表分配空间,表容量难以扩充,必须按线性表最大可能的长度分配空间,有可能造成存储空间浪费。
(5)插入和删除运算时必须移动大量元素,效率较低;(5)插入和删除运算时必须移动大量元素,效率较低; 为避免大量元素的移动,本节讨论线性表的另一种存储结构,即链式存储结构,是最常用的存储方法之一,它不仅可以表示线性表,而且可以表示各种复杂的非线性数据结构。这种结构按不同的分类方法可分为单链表、循环链表和双向链表等。
2.3.1 线性链表 线性表的链式存储结构特点是: 逻辑关系上相邻的两个元素,在物理位置上不一定相邻,不能随机存取链中数据. a. 一个结点——数据元素ai的存储映象。 指向其后继 存入的数据 如:链表中存入数据 6,9, 3, 7 Head(头指针)
这样,利用每个结点的指针域就可以将n个结点按其逻辑次序连成一个链表,这种链表中每个结点只含一个指针域,故称为线性链表或单链表。例如,线性表(red,orange,white,yellow,green,blue),其单链表的存储方式如图2.5所示。这样,利用每个结点的指针域就可以将n个结点按其逻辑次序连成一个链表,这种链表中每个结点只含一个指针域,故称为线性链表或单链表。例如,线性表(red,orange,white,yellow,green,blue),其单链表的存储方式如图2.5所示。 由于单链表中第一个结点无直接前趋,可另设一个头指针head存储第一个结点的地址。同样,最后一个结点无直接后继,其指针域为空值即为NULL,有时用^ 表示。整个单链表可由头指针唯一地确定。单链表是一种非随机存取的存储结构。
也可以将图2.5中的单链表直观地画成如图2.6所示,其中箭头用来表示链域中的指针。也可以将图2.5中的单链表直观地画成如图2.6所示,其中箭头用来表示链域中的指针。
b. 结构指针定义 单链表可以用C语言中的指针数据类型实现,描述如下: typedef int elemtype ;typedef struct node /*结点类型定义*/ { elemtype data ; /* 数据域 */ struct node *next ; /* 定义指针域 */ } linklist ;linklist * head ,*p; /* head,p为单链表指针类型 */ 另外,利用C语言中的指针数据类型要注意指针变量和结点变量,其关系如图2.7所示。
单链表上的基本运算 为了便于实现各种运算,常在单链表的第一个结点之前增设一个附加的结点,称为头结点,其它的结点称为表结点。本章若无特殊说明,均采用带头结点的单链表,如图2.8的(a),(b)所示。
1.建立一个单链表 (1) 算法2.8 C语言程序 输入一系列整数,以0标志结束,将这些整数作为data域建立一个单链表的函数: void crea() { node *head,*p,*s; int x,cycle=1; /*cycle是循环变量*/ head=(node *) malloc(sizeof(node)); /*建立头结点,由dead所指向 */ p=head; while (cycle) { scanf(“%d”,&x); if (x!=0) { s=(node *) malloc(sizef(node)); /*建立下一个结点,由s所指向*/
s->data=x; p->next=s; p=s; } /*把s结点链接到前面建立的单链表中*/ else cycle=0; } head=head->next; /*删除头结点*/ p->next=NULL; } 如果输入的整数序列是: 6 10 3 6 7 5 0
(2)建立带头结点的单链表 linklist * creatlist ( ) /* 函数返回一个指向单链表表头的指针 */ { char ch;int x ;linklist *head , *r , *p ;p =(linklist *)malloc(sizeof(linklist));head = p ; p->next =NULL; /* 生成头结点*/ r = p ; /*建立单链表表尾指针 */ ch = getchar ( ) ; /*ch为建立与否标志 */ while ( ch ! = ‘ ? ’ ) /*‘? ’为输入结束标志符 */ { scanf ( “ % d ” , &x ) ; /*读入新数据元素*/ p =(linklist *)malloc (sizeof (linklist ));p->data = x ; p ->next = NULL; /*生成新结点*/ r->next = p ; /*连接新结点 */ r = r->next ; /*修改尾指针 */ ch = getchar ( ) ; /*读入建立与否的标志*/ } return ( head ) ; /*返回表头指针head */ } /* creatlist */
(3)建立不带头结点的单链表linklist * creatlist ( ) /*函数返回一个指向链表表头的指针*/ {char ch;int x ;linklist *head , *p, *r head =NULL ;r =NULL ; /*设立尾指针r,其初值为空 */ ch = getchar ( ); /*读入建立与否标志 */ while ( ch != ‘ ? ’ ) /*‘ ? ’为输入结束标志符 */ { p=(linklist *)malloc(sizeof(linklist)); /*申请新结点空间 */ scanf ( “ % d ”,&x ) ; p->data = x ; if (head = = NULL) head = p; else r->next = p; /*非空表,则新结点*p插入到*r之后 */ r = p ; /*尾指针移动,指向新的表尾 */ ch = getchar ( ); /* 读入建立与否的标志 */ } if ( r! = NULL ) r->next = NULL; return head ; /*返回表头指针head */ } /* creatlist */在算法中引入头结点可以不必区分空表与非空表,可以统一空链表与非空链表的处理。 上述两个算法的时间复杂度都为O(n)。
2.单链表上元素的插入和删除运算 插入结点的指针变化图2.9所示。指针的修改操作为:① s->next=p->next; ② p->next=s; 上述指针进行相互赋值的语句顺序不能颠倒,若次序变化为:① p->next=s; ② s->next=p->next;则会导致错误。 同样,要删除单链表结点*p的后继结点也很简单,只要用一个指针指向被删除结点,修改*p的指针域,最后释放结点*p即可,如图2.10所示。删除一个结点* p的后继结点的指针操作为:p->next = p->next ->next ; 不难发现,在单链表存储结构中进行元素的插入和删除时,只需要修改有关的指针内容,而不需要移动元素。
(1) 插入算法 (a)insert (linklist *p ,elemtype x) /*将值为x的新结点插入*p之后 */ { linklist *s;s =(linklist *)malloc ( sizeof( linklist)) ; /* 生成x的新结点*s */ s->data=x;s->next=p->next ; /*新结点链入单链表 */ p->next=s;} /*insert*/
(b) void insert (linklist *head,elemtype k,elemtype x) /*在单链表中值为k的结点之前插入一个值为x的新结点*/ { linklist *q, *p, *r;r =(linklist *)malloc( sizeof( linklist)); / *生成新结点 */ r->data = x;if ( head->next = =NULL) { head->next = r ;r->next = NULL ; } else { q = head->next ;p = head->next->next ;while ( p ! = NULL ) { if ( p->data ! = k ) */ { q = p;p = p->next ; } else break ;
if ( p ! = NULL ) { q ->next = r ;r->next = p; } else /*在链表表尾处插入新结点 */ { q ->next = r;r->next =NULL;} } } /* insert */该算法的时间主要耗费在查找值为k的结点位置上,算法时间复杂度为O(n) 。
算法2.9 单链表的插入( C语言函数) 在单链表中第i个结点(i>=0)之后插入一个元素为x的结点。 else { p=head; j=i; /*在单链表中查找第i 个结点,由p指向*/ while (p!=NULL && j<i) { j++; p=p->next;} if(p!=NULL) /*若找查成功,则把s插入到其后*/ { s->next=p->next; p->next=s;} else printf(“未找到!\n”); } } void inser(head,i) node *head; int i,x; { node *s,*p; int j; s=(node *) malloc(sizeof(node)); /*建立一个待插入的结点s*/ scanf(“%s”,&x); s->data=x; if (i==0) /*如果i=0,则将s所指结点插入到表头后返回*/ { s->next=head; head=s;}