一.线性表的链式表示
(一).单链表(*)
1.单链表的定义
-
单链表:线性表的链式存储;使用一组任意的存储单元来存储线性表中的数据元素,使用指向后继的指针表示元素之间的链式关系。
-
相关术语:数据域——存放相关元素/指针域——存放后继节点的地址
-
节点结构与程序定义
data next typedef struct LNode{ ElemType data;//数据域 struct LNode *next;//指针域 }LNode, *LinkList;
2. 头指针与头结点
- 头结点:为了操作上的方便,在单链表的第一个节点之前附加一个节点,称为头结点。头结点的数据域可以不设任何信息,也可以只记录表长等相关信息;头结点的指针域指向线性表的第一个元素节点。
- 头指针:头指针始终指向链表的第一个节点;若链表含有头结点,则头指针指向头结点;若链表不含有头结点,则头指针指向第一个数据节点。
- 引入头结点的好处
- 链表第一个数据元素节点的处理一般化:相当于在一个数据元素节点之前也加了一个节点,它与其他节点一样均有唯一的前趋和后继,操作一致。
- 空表处理一般化:即使是空表,头指针也不会是空的(头指针指向头结点,头结点的指针域为空,但头结点是存在的一个实体),操作一致。
3.单链表上的基本操作的实现
-
建立单链表
-
头插法:从一个空表开始生成新节点,将新生成的节点插入到当前链表的表头,即头结点之后
LinkList CreateList(LinkList &L){ LNode *s;int x; L = (LinkList)malloc(sizeof(LNode));//创建头结点 L->next = NULL;//初始化为一个空链表 scanf("%d",&x); while(x!=9999){//设定输入值为9999时停止 s = (LNode*)malloc(sizeof(LNode)); s->data = x; s->next = L->next; L->next = s; scanf("%d",&x); } return L; }
-
尾插法:从一个空表开始生成新节点,将新生成的节点插入到当前链表的表尾;为了每次插入到表尾,还需要一个尾指针来记录表尾的位置。
LinkList Create(LinkList &L){ int x; L = (LinkList)malloc(sizeof(LNode)); LNode *s,*r = L; scacnf("%d",&x); while(x != 9999){ s = (LNode*)malloc(sizeof(LNode)); s->data = x; r->next = s; r = s; scanf("%d",&x); } r->next = NULL;//尾结点指针要置空,易忘记!!! return L; }
-
-
查找单链表中某个节点
-
按位序查找
LNode *GetElem(LinkList L,int i){ int j = 1; LNode *p = L->next; if(i==0) return L; if(i<1) return NULL; while(p&&j<i){ p = p->next; j++; } return p; }
-
按值查找
LNode *LocateElem(LinkList L,ElemType e){ LNode *p = L->next; while(p&&p->data != e){ p = p->next; } return p; }
-
-
插入节点(均只谈论给定位序的插入,给定节点的插入只需修改指针即可,修改方式参照位序插入的核心部分)
-
后插:在某个节点的后面插入一个新节点——要先判断插入位置的合法性,找到待插入位置对应的前驱元素,然后做基于给定节点的插入即可。在链表中通常都是做后插操作。
a.p = GetElem(L,i-1);//当需要在位序i处插入某一个元素(理解:对应在i-1处后插) b.s->next = p->next; c.p->next = s;
-
前插:前插的操作往往通过转化成后插操作来实现,有两种方法:
-
一种就是同上后插所描述的那样,若需要在位序i处前插,就说明需要插入到i的位置上,那么这个时候找到其前驱,进行后插即可
-
还有一种就是先按照后插操作进行,再调换两个节点的值信息
//将*s节点插入到*p之前的主要代码 s->next = p->next; p->next = s; tmp = p->data; p->data = s->data; s->data = tmp;
-
-
-
删除节点
-
删除链表中某位序对应的节点:同样需要找前驱
//删除位序为i处的元素 p = GetElem(L,i-1); q = p->next; p->next = q->next; free(q);
-
删除链表中的某个指定的节点
-
方法1:从链表的头结点开始遍历,找到待删除节点的前驱节点,再执行删除操作
-
方法2:将待删除节点的后继节点的信息移动到自身来,再删除其后继节点(构造除了前驱节点,关键还是在于得到待删除项的前驱节点)
//删除节点 *p q = p->next; p->data = q->data; p->next = q->next; free(q);
-
-
-
求表长:设置一个计数器,从表头依次遍历到表尾并同时进行计数即可。但是注意,头结点并不算在链表的长度中;对于不带头结点的单链表要单独处理其为空的情况。
(二).双链表
1.双链表的定义
-
相关概念:
-
前驱节点prior,后继节点next
-
节点类型描述
typedef struct DNode{ ElemType data; DNode *prior,*next; }DNode,*DLinklist;
-
基本操作的特点:双链表较于单链表只是增加了一个指向前驱的指针,故采取从前往后遍历的查询操作没有改变,只有插入和删除发生变化。
-
2.双链表的基本操作实现
-
插入操作
-
后插:在 p所指的节点之后插入节点s;步骤d晚于步骤a c进行即可(用到了p->next)
a.s->next = p->next; b.p->next->prior = s; c.s->prior = p; d.p->next = s;
-
前插:在p所指的节点之前插入节点s/思考模式与后插相似,因为有前驱指针,所以复杂度也没有不同
a.p->prior->next = s; b.s->prior = p->prior; c.s->next = p; p->prior = s;
-
-
删除操作
-
删除后继节点,删除p的后继q
p->next = q->next; q->next->prior = p; free(q);
-
删除前驱节点,删除p的前驱q
p->prior = q->prior; q->prior->next - p; free(q);
-
-
双链表的建立:也可以采用头插法和尾插法,其中头插的操作和前插一致,尾插的操作和后插一致,尾插同样需要保留一个尾指针。
(三).循环链表
1.循环单链表
-
结构说明:与一般的单链表大致相似,只不过循环单链表的表尾的指针不指向NULL,而是指向头结点,使得整个链表形成一个环。
-
特点:
- 循环单链表整体是一个环,所以在表中的任何一个位置上进行插入和删除操作都是等价的,无需判断是否是表尾。
- 在循环单链表中可以从表中任意一个节点开始遍历整个链表
- 若对一个循环单链表的常用操作集中在表头和表尾,则可以只设一个尾指针,这样对表头和表尾的操作都是基于O(1)的
-
基本操作的实现:同前
2. 循环双链表
- 结构说明:在一般双链表的基础上,使得头结点的前驱指向表尾节点;表尾节点的后继指向头节点。
- 特点:在任意一个节点通过前向和后向都可以遍历整个链表 / 若只对双向链表进行表头和表尾的操作,则不管是设头指针还是尾指针,均只有O(1)的复杂度。
- 基本操作
- 判空:头节点的前驱和后继均指向头节点自身
- 判断尾结点:(和循环单链表一致)某个节点的后继为头节点则该节点即为尾结点
(四).静态链表
-
结构:借助数组来描述线性表的链式存储,节点也分为数据域和指针域,但是静态链表中的指针实际上是游标(存储的是节点的相对地址);因为实质采取的是数组,所以静态链表也和顺序表一样需要预先分配一块连续的内存空间。
-
图示
id data cur 0 2 1 b 6 2 a 1 3 d -1 4 5 6 c 3 静态链表以next=-1作为结束的标志
-
程序定义结构体
#define Maxsize 50 typedef struct{ ElemType data; int next; }SLinkList[Maxsize];
-
特点:静态链表的插入、删除操作的思想和动态链表是一致的,只需要修改指针,不需要移动元素。
-
二.顺序表和链表的比较
比较方面 | 顺序表 | 链表 |
---|---|---|
存取方式 | 随机存取、顺序存取 | 顺序存取 |
逻辑、物理结构 | 顺序存储,逻辑相邻=物理相邻 | 链式存储,逻辑相邻≠物理相邻 |
增删查 | 按值查找:有序(O(logn));无序(O(n)) 按序查找:O(1) 增删:O(n) |
查找:O(n) 增删:O(1) |
空间分配 | 静态分配:一旦装满无法扩充,预先分配大小需要权衡 动态分配:可扩充,但需移动大量元素,且需大量连续存储空间 |
随用随分配,操作灵活且高效 |
三.链表中的算法题与解
1.查找+删除
-
【无序/无头结点/删除特定值/递归】
void Del_x_3(LinkLIst &L,ElemType x){ LNode *p; if(L == NULL){ return;//递归终止条件 } if(L->data == x){ p = L; L = L->next; free(p); Del_x_3(L,x);//继续递归调用 } else Del_x_3(L->next,x); }
-
【无序/有头结点/删除特定值】
法一:这里的题解模型适用于所有无序单链表的查找+删除模型,不仅可以删除特定值,也可以删除某个区间内的值,删除最值等等(修改if条件即可)
void Del_x(LinkList &L,ElemType x){ //两个指针pre和p,p指向当前的元素,pre指向当前元素的前驱,如果p对应的数据是x,那么删除当前元素,否则p和pre同步后移 LNode *p = L->next,*pre = L,*q; while(p!=NULL){ if(p->data == x){ q = p; p = p->next; pre->next = p; free(q); } else{ pre = p; p = p->next; } } }
法二:采用尾插法建立单链表,用p指针扫描L中的所有节点,当其值不为x的时候链接到L之后,否则释放该节点。
这个模型有点类似顺序表中的用一个变量k记录不满足删除条件的元素然后将这些元素存在按照k存在数组中。
在对链表进行遍历的过程中,头结点L后面所连的已经是新的链表了(采用对头结点的尾插法),而遍历之所以得以进行是因为原来链表节点之间的链接关系还是保留着的。
void Del_x(LinkList &L,ElemType x){
LNode *p = L->next,*r = L,*q;
while(p!=NULL){
if(p->data!=x){
r->next = p;
r = p;
p = p->next;
}
else{
q = p;
p = p->next;
free(q);
}
r->next = NULL;//易忘记,插入结束后尾结点指针一定要置为NULL
}
}
2.元素逆置
-
逆序输出元素
可以采用递归的方法,每次先优先处理链表中后继元素,再输出自己本身的值void R_print(LinkList L){ if(L->next != NULL){ R_print(L->next); } print(L->data); }
-
就地逆置链表中的元素
法一:在头结点处对链表中的元素依次进行头插法LinkList Reverse_L(LinkList L){ //L是带有头结点的单链表 LNode *p,*r;//p就是循环遍历的指针,r是对遍历位置的一个副本,防止在金婷头插的过程中断链 p = L->next; L->next = NULL; while(p!=NULL){ r = p->next; p->next = L->next;//若前一步不进行副本保存,这一步之后就断链了 L->next = p; p = r; } return L; }
法二:对原链表元素依次遍历的过程中,将指针逆序;故首先需要设置两个指针分别指向当前指针连接到的两个元素,其次还需要额外增设一个元素,指向后元素的后继,防止断链。
LinkList Reverse_2(LinkList L){ LNode *pre,*p = L->next,*r = p->next; p->next = NULL;//第一个元素节点就是逆序后的链表的尾结点 while(r!=NULL){ pre = p; p = r; r = r->next; p->next = pre;//如果不设r的话,此处指针反转之后就断链了 } L->next = p;//遍历到最后退出循环是r为空,也就说明p时原链表的尾结点 return L; }
四.参考文档与思维导图
-
参考:《王道2019年数据结构考研指导》,在复习专业课时整理,有问题欢迎讨论。
-
思维导图:
前篇指路:数据结构(1):线性表(上)之顺序表