线性表的链式储存又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。
为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针。
定义一个基本的单链表代码如下:
typedef struct LNode {//typedef关键字:重命名关键字,使代码更简洁
//此处等价于typedef struct LNode LNode;
int data;
struct LNode* next;//指针指向下一个节点
}LNode, *LinkList;//此处等价于typedef struct LNode *LinkList;
对单链表,带头结点的操作代码书写较为方便,但是408考研初试中可能考察不带头结点的单链表相关知识,故先放两个不带头结点的单链表操作函数。
初始化:
bool NHInitList(LinkList& L) {//初始化单链表(不带头结点)
L = NULL;
return true;
}
void Nohead() {//不带头结点的单链表
//不带头结点,L.data中存放数据;带头结点,L.data中不存数据,即可视为第0个节点
LinkList L;//声明一个指向单链表的指针
NHInitList(L);//初始化一个空表
//……然后进行后续操作
return;
}
按位插入(带头结点的写法在后面):
bool NHListInsert(LinkList& L, int i, int e) {//不带头结点的按位插入
if (i < 1)return false;
if (i == 1) {//需要单独为第1位序的结点开辟一个逻辑,比较麻烦
LNode* s = (LNode*)malloc(sizeof(LNode));//申请新节点
s->data = e;//赋值
s->next = L;//新节点后接原表L
L = s;//头结点变为s
return true;
}
else return ListInsert(L, i-1, e);//其他位置的插入同带头结点,直接调用相关函数
}
总之,不带头结点的单链表对第1位序的结点操作时通常要单独编写一套逻辑,非常不便。之后的代码全是默认带头结点的单链表。
初始化:
bool InitList(LinkList& L) {//带头结点的初始化(绝大多数情况下)
L = (LNode*)malloc(sizeof(LNode));//分配一个头结点
if (L == NULL)return false;//内存不足,分配失败
L->next = NULL;//头节点后暂无节点
return true;//分配成功,返回值为true
}
按位插入:
bool ListInsert(LinkList& L, int i, int e) {//按位插入,在第i个位置插入元素e(带头结点)
if (i < 1)return false;//排除不合法的i
LNode* p;//临时指针
int j = 0;//表示p指向第j个结点
p = L;//p初始状态和头结点指向同一位置,可视作第0个结点(不存数据)
while (p != NULL && j < i - 1) {//循环找到第i-1个结点
p = p->next;
++j;
}
if (p == NULL)return false;//i值不合法
LNode* s = (LNode*)malloc(sizeof(LNode));//申请一个新结点
s->data = e;//插值
s->next = p->next;//将p结点的下一个指向传给s
p->next = s;//然后将p指向s,完成s的插入
return true;
}
当然,这一段中的while循环也可用for代替,具体如下,不过初学者建议还是用while循环,便于理解。
LNode* p = L;
for (int j = 0; p != NULL && j < i - 1; p = p->next, ++j);
//(当然使用时要先声明后插函数)
/return InsertNextNode(p,e);
后插操作:
bool InsertNextNode(LNode* p, int e) {//对给定结点进行后插操作(在后面插入一个数据)
if (p == NULL)return false;//判断节点是否合法
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)return false;//内存分配失败
s->data = e;//基本逻辑同按位插入
s->next = p->next;
p->next = s;
return true;
}
因为单链表的性质,对给定结点,其前驱总是未知的,但是可以通过一种“偷天换日”的方法实现前插操作,具体代码如下:
bool InsertPriorNode(LNode* p, int e) {//对给定结点进行前插操作(在前面插入一个数据)
if (p == NULL)return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)return false;
s->next = p->next;//将s的后继指向p的后继
p->next = s;//p指向s,完成将s插入链表
s->data = p->data;//将p的数据赋给s
p->data = e;//再把需要插入的值e赋给p,从而完成变相的“前插”
return true;
}
按位删除:
bool ListDelete(LinkList& L, int i, int& e) {//按位删除:删除第i位序的元素,并用e返回其值
if (i < 1)return false;
LNode* p = L;
for (int j = 0; p != NULL && j < i - 1; p = p->next, ++j);//循环查找第i-1个结点,逻辑同按位插入
if (p == NULL || p->next == NULL)return false;//若第i-1个结点后无结点也非法(即不存在i结点)
LNode* q = p->next;//令q指向被删除结点
e = q->data;//用e返回被删除的值
p->next = q->next;//用p的后继继承q的后继,从而将q从链表中断开
free(q);//释放内存空间,完成删除操作
return true;
}
对指定结点的删除操作,王道考研书上的代码无法实现对表尾结点的正确删除,我对其进行了补正,不一定正确,具体代码如下:
bool DeleteNode(LNode* p) {//删除指定结点p
if (p == NULL)return false;
if (p->next != NULL) {//如果p不是表尾结点
LNode* q = p->next;//令q指向p的后继结点
p->data = q->data;//将p结点的数据用后继结点的数据覆盖
p->next = q->next;//再将p的后继指向p的后继的后继,从而实现q结点的断开
free(q);//此时p的位置实际上已经是q,p已经被删,所以释放q即可(思想类似前插)
}else free(p);
//如果p是表尾结点则直接释放p
//但是单链表的相应信息也要变化,比如len--,这里就需要使用全局变量或者将参数传入函数
//具体实现可能较为复杂,视情况而定
return true;
}
按位查找操作,其实在之前的“按位插入”操作中就已经实现,但是王道考研网课的讲解顺序是先讲了“按位插入”,所以我也这么写了。这个函数可以写在“按位插入”函数之前,然后直接调用之,代码如下:
LNode* GetElem(LinkList L, int i) {//按位查找,返回第i个元素,之前按位插入已经有过实现
//可封装入按位插入,按位删除函数
if (i < 0)return NULL;
LNode* p = L;
int j = 0;
while (p != NULL && j < i) {
p = p->next;
++j;
}
return p;
}
按值查找:
LNode* LocateElem(LinkList L, int e) {//按值查找,找到数据域为e的结点
LNode* p = L->next;
for (; p != NULL && p->data != e; p = p->next);
return p;//若没找到会返回NULL
}
求表长:
int Length(LinkList L) {//求表长
int len = 0;
for (LNode* p = L; p->next != NULL; p = p->next)//遍历单链表
len++;
return len;
}
基本操作之后是单链表的建立,一共有两种方式,分别是尾插法和头插法,其中头插法建立的单链表数据元素顺序与输入顺序相反。
尾插法:
LinkList List_TailInsert(LinkList& L) {//尾插法建立单链表
int x;
L = (LinkList)malloc(sizeof(LNode));//建立头结点
LNode* s, * r = L;//s为临时指针,r为表尾指针
while (scanf("%d", &x) != EOF) {//循环输入数据,直到无输入
s = (LNode*)malloc(sizeof(LNode));
s->data = x;//数据入表
r->next = s;//链接元素
r = s;//r指向新的表尾
}
r->next = NULL;//尾结点指针置空(同时预防空表产生野指针)
return L;
}
头插法:
LinkList List_HeadInsert(LinkList& L) {//头插法建立单链表
int x;
LNode* s;//临时指针
L = (LinkList)malloc(sizeof(LNode));//分配头结点
L->next = NULL;//初始为空
while (scanf("%d", &x) != EOF) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;//传入数据
s->next = L->next;//将头结点的后继作为新节点的后继
L->next = s;//新节点作为头结点的后继
}
return L;
}
关于单链表,考研初试需要掌握的知识基本上就是这么多,当然,学有余力的话也要思考一下不带头结点的单链表,基本操作又要如何实现。