链表是线性表的一种存储方式,每个节点存储两个信息:
- 节点数据
- 指向下一个节点的指针
对于链表,只要求逻辑上元素与元素之间相邻,物理上可以将它们离散的存放在存储空间中,通过每个节点的指针来遍历整个链表。由于链表在存储空间中无序,且每个节点只能指向下一个节点,故对链表的访问只能采用顺序访问,不能随机访问。由于链表不要求物理上相邻,对其进行增删等操作不需要像顺序表一样移动大量元素,只需要修改指针。离散存放的链表不需要提前分配连续空间, 长度不受限。
接下来使用代码来理解链表的定义与操作。为方便理解,以下代码中链表皆为单链表的操作。
定义
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
结点中包括数据域和指向下一个节点的指针域。此处的数据域的类型也可以是其他类型。
通常用一个头指针标识一个单链表,头指针指向NULL表示这是一个空表。实际使用时,为了方便操作,会令头指针指向头结点,头结点内部不存储任何信息,也可以存储与链表相关的信息。头结点的指针指向链表的第一个节点。
带头结点和不带头结点的链表的区分:
- 不带头结点的单链表:头指针指向第一个节点。当链表为空时,头指针指向NULL;
- 带头结点的单链表:头指针指向头结点,头结点指向的下一个节点为第一个数据节点。当链表为空时,头结点的指针域为NULL;
初始化
//不带头结点的单链表,初始化
bool Init_List(LinkList &L){
L = NULL;
return true;
}
//带头结点的单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //空表,暂时还没有任何节点(防止脏数据)
if (L == NULL)
return false; //若内存不足,分配失败
L->next = NULL; //头结点之后暂时还没有节点,指针域为空
return true;
}
按位插入
对按位插入我们首先来分析有头结点的链表的插入,传入的参数有头指针、插入位置以及数据。首先我们要分析插入位置是否合法。链表中可插入范围为第一个结点前到最后一个节点后。插入位置不合法有两种情况:1. 小于1;2.大于链表长度+1(此时链表中无节点与其逻辑上相邻)。由于链表无法随机访问,要找到插入的位置就需要从头遍历整个链表,遍历时需要设置一个记录值记录当前节点的位置。带头结点的链表头指针指向头结点,因此头结点是第0个节点。将一个节点插入链表需要将其与插入位置的前后结点连接,插入时先将其指针域指向后一个节点,再令前一个节点的指针域指向此节点。由于单链表的每一个节点指向下一个节点,故插入时需要找到前一个节点,再进行插入操作。将以上分析进行分步概述:
- 判断插入位置是否合法;
- 从头遍历整个链表,找到插入位置的前一个节点;
- 进行插入操作;
//向链表指定位置插入元素
bool InsertList(LinkList &L,int i,int e){
if(i < 1) //位序不合法,插入失败
return false;
LNode *p; //指针p指向当前扫描到的头结点
int j = 0; //当前p指向的是第几个节点
p = L; //L指向头结点,头结点是第0个节点
while(p != NULL && j < i-1){
p = p->next;
j++;
}
if(p == NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; //将新节点插入链表指定位置
p->next = s;
return true;
}
带有头结点的链表在插入时不需要考虑空表插入的情况,因为自身带有头结点,只需要在头结点后插入,和其他位置的插入操作无区别。
当向没有头结点的第一个位置插入结点时,由于没有前驱节点,需要对其进行特殊处理。向其他位置插入时需要遍历链表,此时头指针指向第一个节点,此时的初始记录值应为1.
//向链表指定位置插入元素
bool InsertList(LinkList &L,int i,int e){
if(i < 1)
return false;
//向头指针后插入结点
if(i == 1){
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s; //头指针指向新节点
return true;
}
LNode *p; //指针p指向当前扫描到的头结点
int j = 1; //当前p指向的是第几个节点
p = L; //L指向头结点,头结点是第0个节点
while(p != NULL && j < i-1){
p = p->next;
j++;
}
if(p == NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next; //将新节点插入链表指定位置
p->next = s;
return true;
}
指定节点的后插
- 创建一个新结点,并将传入的数据值赋给新结点的数据域;
- 将新结点连接到指定结点后;
//指定节点的后插操作
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;
}
指定结点的前插
对指定结点p进行索引只能找到其后面的结点,实现结点后插,无法向前插入,因此要实现指定结点的前插需要另寻他法。对于一个链表,我们知道,每个节点包括自身数据以及下一个结点的指针,前插操作实际上是令插入的数据在指定结点的数据之前并且不改变其他结点的顺序,故我们可以直接交换指定结点与插入结点的值,插入后链表中数据的顺序仍满足我们的要求。
//前插操作:在p节点之前插入元素e
bool InsertPriorNode(LNode *p,int e){
if(p == NULL)
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s == NULL) //内存分配失败
return false;
s->data = p->data;
p->data = e;
s->next = p->next;
p->next = s;
return true;
}
按位查找
从链表的第一个节点开始,遍历整个链表找到对应位置的结点。注意查找位置是否合法。带头结点和不带头结点的链表按位查找代码有一点区别。
//按位查找(不带头结点)
LNode *GetElem(LinkList &L,int i){
if(i < 1)
return NULL;
LNode *p;
int j = 1;
p = L;
while(p != NULL && j < i){ //当
p = p->next;
j++;
}
return p;
}
//按位查找(带头结点)
LNode *GetElem(LinkList &L,int i){
if(i < 1)
return NULL;
LNode *p;
int j = 0;
p = L;
while(p != NULL && j < i){
p = p->next;
j++;
}
return p;
}
按值查找
- 从头遍历整个链表;
- 对每个节点值与参数进行对比;
- 若未找到,传回结点指向null;
//按值查找
LNode *LocateElem(LinkList &L,int e){
LNode *p;
p = L;
while(p != NULL && p->data != e){
p = p->next;
}
return p;
}
按位序删除
- 验证位序是否合法;
- 遍历链表找到要删除的结点;
- 将结点与链表分开并释放结点;
//按位序删除(带头结点)
bool ListDelete(LinkList &L, int i,int &e){
if(i < 1)
return false;
LNode *p;
int j = 0;
p = L;
while(p != NULL && j < i-1){
p = p->next;
j++;
}
if(p == NULL)
return false;
if(p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
//按位序删除(不带头结点)
bool ListDelete(LinkList &L, int i,int &e){
if(i < 1)
return false;
//(当链表无头结点时)
if(i == 1){
LNode *s = (LNode *)malloc(sizeof(LNode));
e = L->data; //获取删除节点的数据值
s = L; //第一个节点为头指针指向的结点
L = s->next; //令头指针指向第二个节点
free(s); //释放第一个节点
return true;
}
LNode *p;
int j = 1;
p = L;
while(p != NULL && j < i-1){
p = p->next;
j++;
}
if(p == NULL)
return false;
if(p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
指定结点删除
对单链表,通过结点本身只能找到它的后继而无法找到它的前驱,故要想在保持链表的连接基础上删除节点只能删除结点的后继。我们无法直接删除指定结点,但可以采取交换值的方法,在删除后继结点前将结点自身与其后继的值交换,再删除后继节点。此方法有一处bug,就是当要删除的结点是链表的最后一个结点时无后继节点,无法交换值。这是单链表的局限性,没有前驱指针无法得到前驱结点。
//删除指定节点
bool DeleteNode(LNode *p){ //有bug.对单链表,当p是最后一个节点时,无后继节点,无法与其后节点交换值
if(p == NULL)
return false;
LNode *s = p->next;
p->data = p->next->data;
p->next = s->next;
free(s);
return true;
}
获取链表长度
带头结点的链表头结点不计入链表长度。
//求链表长度(带头结点的)
int Length(LinkList L){
LNode *p = L;
int len = 0;
while(p->next!=NULL){
p = p->next;
len++;
}
return len;
}
//求链表长度(不带头结点的链表)
int Length(LinkList L){
LNode *p = L;
int len = 1;
while(p->next!=NULL){
p = p->next;
len++;
}
return len;
}
打印整个链表
//顺序打印链表的值(带头结点)
void PrintList(LinkList L){
LNode *p = L;
while(p->next != NULL){
p = p->next;
cout<<p->data<<" ";
}
cout<<endl;
}
//顺序打印链表的值(不带头结点)
void PrintList(LinkList L){
LNode *p = L;
while(p != NULL){
cout<<p->data<<" ";
p = p->next;
}
cout<<endl;
}
单链表的建立
头插法
对于带头结点的单链表,每次插入的结点都在头结点之后,对于不带头结点的单链表,每次插入的结点在头指针之后,因此在插入时操作有些不同。
//头插法建立链表(不带头结点)
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int num;
L = (LinkList)malloc(sizeof(LNode));
L = NULL;
cout<<"请输入建立的链表节点数:"<<endl;
cin>>num;
while(num!=0){
int node;
s = (LNode*)malloc(sizeof(LNode));
cout<<"输入节点的值:";
cin>>node;
s->data = node;
s->next = L; //将s结点插入到链表首位
L = s; //让头指针指向第一个节点
num--;
}
return L;
}
//头插法建立链表(带头结点的链表)
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int num;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
cout<<"请输入建立的链表节点数:"<<endl;
cin>>num;
while(num!=0){
int node;
s = (LNode*)malloc(sizeof(LNode));
cout<<"输入节点的值:";
cin>>node;
s->data = node;
s->next = L->next; //令插入结点的指针指向链表的第一个结点
L->next = s; //令头结点指向插入节点
num--;
}
return L;
}
尾插法
尾插法即每次在链表末尾插入结点。对单链表可进行遍历找到最后一个结点,在其后插入。如果每次采用尾插都要遍历链表效率较低,故此处设一个辅助指针,令指针每次都指向链表末尾结点,每次插入结点只需在辅助指针指向的结点后插入即可。由于没有头结点的空链表最开始没有节点,故需要对插入的第一个结点做单独处理。
//尾插法建立链表(带头结点的单链表)
LinkList List_TaliInsert(LinkList &L){
LNode *s,*r;
int num;
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;
r = L;
cout<<"请输入建立的链表节点数:"<<endl;
cin>>num;
while(num!=0){
int node;
s = (LNode*)malloc(sizeof(LNode));
cout<<"输入节点的值:";
cin>>node;
s->data = node;
s->next = NULL;
r->next = s;
r = s;
num--;
}
return L;
}
//头插法建立链表(不带头结点)
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int num;
L = (LinkList)malloc(sizeof(LNode));
L = NULL;
cout<<"请输入建立的链表节点数:"<<endl;
cin>>num;
while(num!=0){
int node;
s = (LNode*)malloc(sizeof(LNode));
cout<<"输入节点的值:";
cin>>node;
s->data = node;
s->next = L; //将s结点插入到链表首位
L = s; //让头指针指向第一个节点
num--;
}
return L;
}
以上即为单链表的所有基本操作。本篇内容适合初学数据结构的学习者,链表对初学者可能有些难以理解但一定要搞懂,建议在其他平台听一听老师讲的课,了解链表的基本逻辑和概念再来结合代码理解,代码一定要自己敲一敲,理解每一行的逻辑,以下为测试代码:
int main(){
LinkList l;
List_TaliInsert(l);
//List_HeadInsert(l);
PrintList(l);
cout<<Length(l);
/*
Init_List(l);
int n,e;
cout<<"请输入添加的结点数:"<<endl;
cin>>n;
for(int i = 0;i < n;i++){
cout<<"请输入第"<<i+1<<"个节点的值:"<<endl;
cin>>e;
InsertList(l,i+1,e);
}
PrintList(l);
//删除
ListDelete(l,1,e);
//打印删除后的链表
PrintList(l);
LNode *p = GetElem(l,3);
cout<<p->data<<endl;
LNode *q = LocateElem(l,4);
cout<<q->next->data<<endl;
cout<<Length(l);
*/
return 0;
}
如果有错误或问题请评论指出,如果有帮助就留下一个赞吧!