2.3 单链表
单链表
定义:链式存储的线性表。
链式:列表元素分散在内存各处,每个元素都包含数据域和指针域,并用指针来指向下一个元素。
分类
根据链表第一个结点是否存储数据,将链表分为带头结点的链表和不带头结点的链表。
带头结点链表
链表的第一个结点(又称头结点)不存储数据,数据从第二个结点开始存储。列表的第一个元素是链表的第二个结点。
不带头结点链表
链表头结点存储数据。列表的第一个元素也是链表的第一个结点。
定义
定义链表结点,包含数据和指针的结构体。
#ifndef Elemtype
#define Elemtype int
#endif
typedef struct LNode{
Elemtype data; // 数据域
struct LNode *next; // 指针域
} LNode, *LinkList; // LNode 用于定义结点, LinkList 用于定义链表指针。
基本操作
带头结点的链表,头结点不存储数据,故而无需对头结点指针,即链表指针进行操作。所有数据操作只需对结点上的指针进行操作。
不带头结点的链表,头结点存储数据。需要修改头结点时,如删除头结点,就需要将链表指针指向第二个结点。而链表指针的赋值和结点指针的赋值是不一样的:
#include <malloc.h>
LinkList L;
LNode *p1 = (LNode*)malloc(size(LNode)),
*p2 = (LNode*)malloc(size(LNode));
L = p1; // 链表指针赋值
p->next = p2;// 结点指针赋值
故而需要单独考虑对链表指针的操作。
总结:不带头结点的链表需要额外添加对链表指针(头结点)的操作。
1、初始化
对链表指针进行初始化。
时间复杂度 O(1);
// 初始化一个带头结点的链表,修改链表指针,传引用。
bool LinkListInit(LinkList &L){
// 创建一个头结点
L = (LinkList)malloc(sizeof(LNode));
if (L == NULL) // 内存不足,创建失败。
return false;
L->next = NULL; // 置空头结点
return true;
}
// 初始化不带头结点链表
bool LinkListInit(LinkList &L, bool withHeaderNode){
// 带头结点的链表
if (withHeaderNode) return LinkListInit(L);
// 不带头结点链表
L = NULL;
return true;
}
2、插入
1)后插
在指定结点后面插入新元素。
办法:先让新结点指向下一个结点,再让当前结点指向新结点即可。
考虑:
1、均是修改结点指针,没有涉及到修改链表指针,两种链表操作一致。不同的是,带头结点的链表可以在第一个元素前面插入新元素,即头结点后插,而不带头结点的链表不行。
2、如果是尾结点,尾结点指针指向NULL,交换指针后,新结点指针为NULL,无需单独考虑。
时间复杂度 O(1);
// 后插
bool LNodeBackInsert(LNode *p, Elemtype e){
// 创建新节点。
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) return false;
// 交换指针、赋值
s->next = p->next;
p->next = s;
s->data = e;
return true;
}
// 兼容
bool LNodeBackInsert(LNode *p, Elemtype e, bool withHeaderNode){
// 节点指针为空,无法访问链表,毫无意义。
if (p == NULL) return false;
// 两种链表后插操作相同。
return LNodeBackInsert(p, e);
}
2)前插
在指定结点前面插入新元素。
问题:要将新结点放在当前结点的前面,就必须让前一结点的指针域指向新结点才行,但是前一节点无法访问(没有指向前一节点的指针,也没有引用),故而此路不通。
办法:偷天换日,先将新结点插入当前结点后面,然后交换两者数据域,新元素就位于指定元素前面了。虽然偷天换日成功,但也留下了蛛丝马迹:当前结点里装的是新元素,原来的元素被放到下一个结点了。
考虑:只修改结点指针域,而非链表指针,两种链表操作相同。注意不能给头结点前插(带头结点链表)。
时间复杂度 O(1);
// 前插
// 详细流程
bool _LNodePreInsert(LNode *p, Elemtype e){
// 创建新节点。
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s == NULL || p == NULL ) return false;
// 交换指针
s->next = p->next;
p->next = s;
// 交换值
s->data = p->data;
p->data = e;
return true;
}
// 简写:前插与后插操作只有最后的值交换不同,故而可以调用后插函数简化前插代码
bool LNodePreInsert(LNode *p, Elemtype e){
// 在新节点中插入当前结点中的元素,可能插入失败
if (!LNodeBackInsert(p, p->data)) return false;
// 在当前节点插入新元素
p->data = e;
return true;
}
// 兼容
bool LNodePreInsert(LNode *p, Elemtype e, bool withHeaderNode){
// 节点指针为空,无法访问链表,毫无意义。
if (p == NULL) return false;
// 两种链表前插操作相同。
return LNodePreInsert(p, e);
}
3)头插
在链表头部插入新元素。
办法:头插是前插的一个特例,先获取第一个元素的指针,然后直接引用前插代码。
考虑:1、空链表的情况:带头结的链表,新结点插入头结点后面,因此可以换成后插操作;不带头结点的链表,将链表指针指向新结点,此法即使不是空链表也适用,因此无需调用前插函数了。
时间复杂度 O(1);
// 头插,修改链表指针,L传引用
bool LNodeHeadInsert(LinkList &L, Elemtype e, bool withHeaderNode){
// 带头结点的链表
if (withHeaderNode) return LNodeBackInsert(L, e);
// 不带头结点链表
LNode *s = (LNode*)malloc(sizeof(LNode));
if (s==NULL)
return false;
s->next = L;
s->data = e;
L = s;
return true;
}
4)尾插
在链表尾部插入新结点。
办法:后插特例,可先寻到最后一个元素,然后调用后插函数将新结点插入链尾。
考虑:空链表情况:带头结点带头结的链表,新结点插入头结点后面,无需额外操作。不带头结点的链表,需将链表指针指向新结点,与前插操作相同。
时间复杂度 O(n);
// 尾插(带头结点)
bool LNodeRearInsert(LinkList L, Elemtype e){
LNode *p = L;
// 检查参数
if ( p == NULL ) return false;
// 不是最后一个结点的指针域不为NULL
while(p->next != NULL) p = p->next;
// 调用后插函数
return LNodeBackInsert(p, e);
}
// 尾插(兼容)。修改链表指针,L传引用
bool LNodeRearInsert(LinkList &L, Elemtype e, bool withHeaderNode){
// 带头结点的链表
if (withHeaderNode)
return LNodeRearInsert(L, e);
// 不带头结点链表
// 空链表,尾插和前插操作一样。
if (L==NULL)
return LNodeHeadInsert(L, e, false);
// 有结点的链表,后插操作都一样。
return LNodeRearInsert(L, e);
}
3、删除
1)删除指定结点
问题:删除当前结点,需修改前一结点的指针域,但没有能访问前一结点的指针。
办法:偷天换日。不删除当前结点,而删除后一结点,将后一结点元素,赋给当前结点。这样当前结点的元素就被删除了。
考虑:1、如果当前结点是尾结点,后面没有元素,便无法使用以上方法,只能通过链表指针遍历到前一结点。2、删除结点时,链表不能为空,但链表可以有一个结点。带头结点的链表情况同删尾结点,不带头结点链表,需修改链表指针为NULL。3、当前结点是头结点,偷天换日删除头结点后一个结点,头结点不变,链表指针不变。无需单独考虑。
时间复杂度 O(n);
// 带头结点情况,删除指定结点(结点只是容器,真正要删除的还是元素。)
bool LNodeDeleteByPoint(LinkList L, LNode *p){
LNode *t; // 临时指针,用于释放被删除的后继结点
// 检查参数
if(L == NULL || L->next == NULL || p == NULL) return false;
// 非尾结点,用后一元素覆盖当前结点的元素,再删除后一结点。就把当前元素删除了。
if (p->next){
p->data = p->next->data;
t = p->next;
p->next = p->next->next;
}else{
// 尾结点只能遍历到链表的倒数第二个结点
t = p; p = L;
while(p->next==t){
p = p->next;
if (p==NULL) return false; // 遍历完整条链表都没找到指定结点。
}
// 新的尾结点
p->next = NULL;
}
free(t);
return true;
}
// 兼容,删除唯一的结点,需修改链表指针,传引用。
bool LNodeDeleteByPoint(LinkList &L, LNode *p, bool withHeaderNode){
// 检查参数。
if (L == NULL || p == NULL ) return false;
// 带头结点的链表
if (withHeaderNode) return LNodeDeleteByPoint(L, p);
// 不带头结点的链表
// 只有一个结点的情况
if (L->next==NULL ){
if (L != p) return false; // 不是链表的节点
free(L); // 释放内存。
L == NULL;
return true;
}
// 多个结点,删除操作相同。
return LNodeDeleteByPoint(L, p);
}
2)按位序删除结点
指定删除第 n 个结点。
只能遍历链表到第 n-1 个元素,修改其后继。
考虑:1、最多遍历到倒数第二个结点,否则位序错误。2、删除头结点,只有不带头结点的链表才能删除头结点,此时需将链表指针指向头结点的下一项(元素或NULL)。
时间复杂度 O(n);
// 7、按位序删除结点
bool LNodeDeleteByRank(LinkList L, int i){
// 检查参数
if (L == NULL || i < 1 ) return false;
// p 指向要删除结点的前驱,s 指向要删除的结点,在结点脱链后释放结点。
LNode *p=L, *s;
// i 为 1 时,p 为头结点,正好是第一个元素的前驱,无需循环。
while (--i){
p = p->next;
if (p->next==NULL)return false; // 已经是尾结点了,i已经超限。
}
// p 为前驱,s为要删除的结点
s = p->next;
p->next = s->next;
free(s);
}
// 兼容,修改链表指针,传引用
bool LNodeDeleteByRank(LinkList &L, int i, bool withHeaderNode){
// 检查参数
if (L == NULL || i < 1 ) return false;
// 带头结点的链表
if (withHeaderNode) return LNodeDeleteByRank(L, i);
// 不带头结点的链表
// 删除的是头结点,或删除后为空表,也就是头结点后面为空。可见两者可归纳为同种情况。
if (i == 1){
L = L->next;
return true;
}
// 删除的不是头结点,操作相同,不过不带头结点的第二个元素是带头结点链表的第一个元素。
return LNodeDeleteByRank(L, i-1);
}
4、查找
1)按位序查找
查找并获取第 n 个元素。
只能遍历链表。
考虑:由于需要知道查找是否成功以及查找到的元素值。宜将是否查找成功作为函数返回值,而元素值则通过传址或传引用的方式获取。
时间复杂度 O(n);
bool LNodeGetByRank(LinkList L, int i, Elemtype &e){
// 检查参数
if (i<1 || L == NULL || L->next == NULL) return false; // i 不合法
LNode *p=L;
// i 为 1 时,循环一次,p 变为第一个结点。
while (i--){
p = p->next;
if (p==NULL) return false; // 已过尾结点,i 已经超限。
}
e = p->data;
return true;
}
// 兼容
bool LNodeGetByRank(LinkList L, int i, Elemtype &e, bool withHeaderNode){
// 检查参数
if (L == NULL || i < 1 ) return false;
// 带头结点的链表
if (withHeaderNode) return LNodeGetByRank(L, i, e);
// 查找头结点
if (i == 1) {
e = L->data;
return true;
}
// 不是头结点
return LNodeGetByRank(L, i-1, e);
}
2)按值查找
查找链表中是否有指定值,如果有获取其位序。
只能遍历链表,不带头结点的链表,还需要比较头结点的值。
时间复杂度 O(n);
// 按值查找
int LNodeGetByValue(LinkList L, Elemtype value){
// 检查参数
if (L == NULL || L->next == NULL) return false;
LNode *p = L->next; // 指向第一个元素
int i = 1;
// 第一个元素为 value 时,循环不执行,i 为 1;
while (p->data != value){
if (p->next==NULL) return 0; // p 的后继为 NULL,链表遍历结束,终止循环。
p = p->next;
i++;
}
return i;
}
// 9、按值查找
int LNodeGetByValue(LinkList L, Elemtype value, bool withHeaderNode){
// 检查参数
if (L == NULL) return false;
// 带头结点的链表
if (withHeaderNode) return LNodeGetByValue(L, value);
// 不带头结点的链表,比较头结点
if (value == L->data) return 1;
// 不是头结点,返回结果需加1,才是不带头结点的位序。
return LNodeGetByValue(L, value)+1;
}
5、批量插入表头
使用头插法插入表头O(1),插入n个数据需要O(n)的时间。
bool LNodeHeadInsertValues(LinkList &L, Elemtype *arry, int len, bool withHeaderNode){
for (int i=0; i<len; i++){
if (!LNodeHeadInsert(L, *(arry+i), withHeaderNode))
return false; // 插入失败
}
return true;
}
6、批量插入表尾
如果每插入一个数据就调用尾插函数O(n), 插入n个数据就需要O(n2)的时间。但如果有尾指针,就能一步插入到表尾了。要获取表尾指针,就需要先遍历链表O(n),再插入元素,后移尾指针即可O(n), 两者相加仍为O(n)。
考虑:插入元素到无结点的链表,和头插法一样。有结点的链表插入操作相同。不要忘了给尾结点指针域置NULL。否则链表就无限长了。
bool LNodeRearInsertValues(LinkList &L, Elemtype *arry, int len, bool withHeaderNode){
int i=0;
if (L == NULL) // 空链表
if(!LNodeHeadInsert(L, *(arry+i++), withHeaderNode))
return false; // 插入第一个元素失败
// 遍历到表尾
LNode *p = L;
while(p->next) p = p->next;
for (LNode *s; i<len; i++){
s = (LNode*)malloc(sizeof(LNode)); // 创建新结点
if (s == NULL) return false;
s->data = arry[i];
s->next = NULL;
p->next = s; // 尾结点指向新结点
p = s; // 新结点变成尾结点
}
return true;
}