第1章第2节 线性表的链式表示(1)

最后更新于:2022-04-01 19:50:31

由于顺序表的插入,删除操作需要移动大量的元素,影响了运算效率,由此线性表的链式存储便应运而生。链式存储线性表时,逻辑上连续的元素物理结构上不需要连续,它们彼此可以通过“链”建立起数据元素之间的逻辑关系,因此对于线性表的插入,删除操作并不需要移动元素,只需修改指针即可。 ## 一.单链表的定义 线性表的链式存储又称为单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。为了建立起数据元素之间的线性关系,对每个链表结点,除了存放元素自身的信息外,还需要存放一个指向其后继的指针。 单链表结点的结构如下图所示,其中,data为数据域,存放数据元素;next为指针域,存放其后继结点的地址。 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e66946e1a31.jpg "") 对于每一个结点的描述如下: ~~~ typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; ~~~ 利用单链表在解决顺序表需要大量的连续存储空间的缺点的同时,也引入了一些不可避免的缺点。比如因为需要额外的指针域,因此需要额外的存储空间;由于单链表是离散的分布在存储空间中,所以单链表不能完成随机存取的操作。 为了方便标识一个单链表,我们一般需要引入头指针来操作整个单链表。此外,为了统一增加和删除元素的操作,我们一般会在单链表的第一个结点之前附加一个结点,称为头结点。头结点的指针域指向单链表的第一个元素结点。 注:这里应该注意区分头指针和头结点。而不管单链表有没有头结点,头指针总是指向单链表的第一个结点。简单说就是如果单链表有头结点,那么头指针将指向头结点;如果单链表没有头结点,头指针将指向单链表的第一个结点。此处我们应该注意到一般情况下头结点内不存储任何信息。这里说明下,如果后面的例题中没有具体说明,一般都是建立有头结点的单链表。 ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e66946f1334.jpg "") 引入头节点后,可以带来两个优点: - 由于开始节点的位置被存放在头节点的指针域中,所以在链表的第一个位置上操作与表其他位置上的操作一致,无需进行特殊处理。 - 无论链表是否为空,其头指针都是指向头节点的非空指针(在空表中,头节点的指针域为空),因此也使得空表和非空表的处理方式变得统一。 ## 二.单链表基本操作的实现 ### 2.1建立单链表 ### 2.1.1头插法建立单链表 该方法中,首先建立一个具有头结点的空单链表,然后每生成一个读取到数据的新节点,就将其放置到头结点之后。如下图所示: ![头插法建立单链表](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e669470b5c6.jpg "") 算法描述如下: ~~~ typedef int ElemType; LinkList CreateLink(LNode *head) { LNode *s; ElemType x; scanf("%d",&x); while(x!=999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=head->next; head->next=s; scanf("%d",&x); } return head; } ~~~ 注:采用头插法建立单链表,读入数据的顺序与生成的链表中的元素的顺序刚好是相反的。 每个结点插入的时间复杂度为O(1),设单链表长为n,则总的时间复杂度为O(n)。 ### 2.1.2尾插法建立单链表 该方法中同样首先建立一个具有头结点的空单链表,然后每生成一个读取到数据的新节点,就将它插入到表尾;为了达到这样的目的,必须增加一个尾指针r,使其始终指向当前链表的尾结点。如下图所示 ![尾插法建立单链表](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e669472736f.jpg "") 算法描述如下 ~~~ LinkList CreatLink(LNode *head) { LNode *s; LNode *r=head; ElemType x; scanf("%d",&x); while(x!=999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; scanf("%d",&x); } s->next=NULL; return head; } ~~~ 注:因为附加设置了一个指向表尾的尾指针r,因此每个结点插入的时间复杂度同样为O(1),设单链表长为n,则总的时间复杂度为O(n)。 ### 2.2按序号查找结点值 从单链表的第一个结点出发,顺着指针next域逐个从上往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。 算法描述如下: ~~~ LNode* GetElem(LNode *head, ElemType i) { LNode *p=head->next; int j=1; if(i==0){ printf("The Link is empty!\n"); return head; } if(i<=0){ printf("The postion is illegal!\n"); return NULL; } while(p){ if(j==i){ printf("Find success!\n"); break; } p=p->next; j++; } return p; } ~~~ 注:按序号查找的时间复杂度为O(n)。 ### 2.3按值查找结点值 从单链表的第一个结点开始,由前往后依次比较各结点数据域的值,若某结点数据域的值等于给定值x,则返回该结点的指针。若整个单链表中没有这样的结点,则返回NULL。 算法描述如下: ~~~ LNode* GetElem(LNode *head, ElemType x) { LNode *p=head->next; if(head==NULL){ printf("The LinkList is empty!\n"); return head; } while(p){ if(p->data==x){ printf("Find the number!\n"); return p; } p=p->next; } printf("Not Find the number!\n"); return NULL; } ~~~ 注:按值查找的时间复杂度为O(n)。 ### 2.4插入结点 ### 2.4.1插入后继结点 插入操作是将值为x的新结点插入倒单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i−1个结点,再在其后插入新的结点。 算法首先需要调用`GetElem(L,i-1)`查找第i−1个结点。假设返回的第i−1个结点为*p,然后令新结点*s的指针域指向*p的后继结点,再令结点*p的指针域插入新的结点*s。其操作过程如下图所示: ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e6694737731.jpg "") 算法描述如下: ~~~ LNode* GetElem(LNode *head, int i) { if(i==0){ printf("The LinkList is empty!\n"); return head; } if(i<=0){ printf("The LinkList is illegal!\n"); return NULL; } LNode *p=head->next; int j=1; while(p){ if(j==i){ printf("Find it!The postion is %dth\n", j); break; } j++; p=p->next; } return p; } void InertElem(LNode *p, ElemType x){ LNode *s; s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=p->next; p->next=s; } ~~~ 算法中,时间的主要开销是查找第i−1个元素,时间复杂度为O(n)。若是在给定的结点后面插入新结点,则时间复杂度仅为O(1)。 ### 2.4.1插入前驱结点 在方法一中,我们可以在指针p指向的结点后面插入新的结点s,但是有时如果我们需要在指针p指向的结点前面插入新的结点s时,上述算法明显是办不到的。 但是如果我们换个思路,将指针p指向的结点和s结点它们之间的数据域做一次交换,依旧将s结点插入到指针p指向的结点后面,如此我们便将前插操作变为了向指针p指向的结点的后插操作,并且在逻辑上仍旧满足条件。 算法描述如下: ~~~ void InertElem(LNode *p, ElemType x){ LNode *s; s=(LNode*)malloc(sizeof(LNode)); s->data=x; ElemType temp; temp=p->data; p->data=s->data; s->data=temp; s->next=p->next; p->next=s; } ~~~ 注:同方法一相同,时间的主要开销是查找第i−1个元素,时间复杂度为O(n)。若是在给定的结点后面插入新结点,则时间复杂度仅为O(1)。 ### 2.5删除结点 #### 2.5.1删除后继结点 删除结点操作即将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i−1个结点(即将要被删除结点的前驱结点),然后在删除第i个结点。其操作过程如下图所示: ![删除结点](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-14_56e6694748c05.jpg "") 假设我们要删除指针q指向的结点,那么我们首先通过遍历所有结点找到指针q指向的结点的前驱结点p,为了实现算法,我们只需修改指针p的指针域,将指针p的指针域next直接指向指针q指向的结点的指针域next所指的下一个结点便可。 算法描述如下: ~~~ void DelLNode(LNode *head, int i) { LNode *p=head; LNode *q=head->next; while(i){ p=q; q=q->next; i--; } p->next=q->next; free(q); } ~~~ 注:删除操作中,时间的主要开销是查找第i−1个元素,因此时间复杂度为O(n)。若是删除给定结点的后继结点,则时间复杂度为O(1)。 #### 2.5.2删除自身结点 有时我们需要删除指针所指向的自身结点(比如指针p指向的结点),此时如果继续使用上述方法明显是不可能的。我们采用与`2.4.1插入前驱结点`相似的方法,将指针p所指向的结点的数据域与指针q所指向的结点的数据域进行一次交换(因为是一次删除操作,我们只需要将指针q指向的结点的数据域直接赋值为指针p指向的结点的数据域便可),这样,我们就又变成了删除指针q指向的结点的操作。 算法描述如下: ~~~ void DelList(LNode *head, int i) { LNode *p=head; LNode *q=p->next; while(i){ p=q; q=q->next; i--; } p->data=q->data; p->next=q->next; free(q); } ~~~ 注:与上述算法一样,删除操作中,时间的主要开销是查找第i个元素,因此时间复杂度为O(n)。若是删除给定的结点,则时间复杂度为O(1)。 ### 2.5求链表长度 求表长实际上就是计算单链表中数据结点(不含头节点)的个数。为了达到这个目的,我们只需对链表进行一次遍历,同时设置计数器对每次访问到的结点计数便可。 算法描述如下: ~~~ int LenLink(LNode *head) { int cnt=0; LNode *p=head->next; while(p){ p=p->next; cnt++; } return cnt; } ~~~ 注:遍历操作中需要访问所有结点,因此时间复杂度为O(n)。
';