数据结构--链表

本文深入讲解链表数据结构,涵盖单链表、双向链表及循环链表的创建、查找、插入与删除操作,附带代码示例,适合初学者入门。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

从来没有想到过自己会写这方面的技术文,可以说是对自己的一个挑战的吧,有兴趣的朋友可以一起学习的,应该会坚持写一个系列的,话不多说,我们开始吧。

线性表:它是最基础、最简单、最常用的一种基本数据结构,线性表总存储的每个数据称为一个元素,各个元素及其索引是一一对应的关系。线性表有两种存储方式:顺序存储方式和链式存储方式。

链表常用的有 3 类: 单链表、双向链表、循环链表。具体将介绍:链表的创建,查找,插入,删除。

 

来源(https://upload-images.jianshu.io/upload_images/1411747-cb235ad128545a4e.png?imageMogr2/auto-orient/)

 

单链表:

单链表是最简单的链表形式。简单的来说几个特点有,1、由 data 数据 + next 指针,组成一个单链表的内存结构 ;2、第一个内存结构称为链头,最后一个内存结构称为链尾,链尾的 next 指针设置为 NULL [指向空];3、单链表的遍历方向单一,只能从链头一直遍历到链尾。

 

单链表的定义:

 

这里定义的是单链表中每个结点的存储结构,它包括两个部分:存储结点的数据域 data ,存储后继结点位置的指针域 next ,其类型为指向结点的指针类型 Lnode * 。

 

// --------- 单链表的存储结构---------

typedef struct Lnode{
   int data;           // 结点的数据域
   struct Lnode *next; // 结点的指针域
}Lnode;

 

创建单链表:

 

单链表的创建有两种方式:前插法创建,后插法创建,我这里为大家介绍常见的后插法创建,前一种方法留给大家自己思考,代码内带有详细的注释,其中的 *r 可以相当于是 *head 的尾结点,一直是把新结点 *p 插在结点 *r 之后,最后返回头结点 。

 

// 后插法建立链表
Lnode *Createlist(Lnode *head,int n){
// 正位序输入 n 个元素的值,建立带表头结点的单链表 head;
   head = new Lnode ;
   head->next = NULL;// 先建立一个带头结点的空链表
   Lnode *r = head; // 头结点赋给 r;
   for(int i=0;i<n;++i)
   {
       Lnode *p = new Lnode ;// 生成新结点 *p
       cin>>p->data;   // 输入元素值赋值给新结点*p 的数据域
       p->next =NULL; r->next = p; // 将新结点*p 插入结点*r 之后
       r = p;// r 指向新的尾结点 *p;
   }
   return head;
}

 

什么是头结点? 首元结点是指链表中存储第一个数据元素的结点;头结点是在首元结点之前附设的一个结点,首元结点的地址保存在头节点的指针域中,增设头结点是为了方便对单链表的操作。

 

查找:

 

链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将结点值和给定值 key 进行比较,返回查找结果。

 

// 查找
Lnode *Findkey(Lnode *head,int key)
{   // 从带头结点的单链表 head 中查找值为 key 的元素
   Lnode *p = head->next;  // 初始化,p 指向首元结点
   while(p!=NULL&&p->data != key) // p 为空或 p 所指结点的数据域等于 key 跳出
       p = p->next;    // p 指向下一个结点
   return p; // 查找成功返回值为 key 的结点地址 p,查找失败返回 NULL;
}

 

插入:

 

将值为 key 的新结点插入到表的第 i 个结点的位置上,首先找到位置,然后模拟就好,上代码。

 

// 插入
void Insertkey(Lnode *head,int key,int i)// 在 i 位置,插入 key
{
   Lnode *p = head;int j =0;
   while(p&&j<i-1){// 查找第 i-1 个结点,p 指向该结点
       p = p->next;++j;// 看不懂的话,可以用 i 等于 1,进行检查程序。
   }
   if(!p||j>i-1) cout<<"ERROR"<<endl;// 没有找到指定的位置
   Lnode *s = new Lnode;       // 生成新结点 *s
   s->data = key;  // 将结点 *s 的数据域值置为 key;
   s->next = p->next; // 将结点 *s 的指针域指向结点位置 i+1 
   p->next = s;    // 将结点 *p 的指针域指向结点 *s
   cout<< "success" <<endl; // 成功
}

 

删除:

 

由于单向链表只存储了头指针,所以删除单向链表中的元素时,需要找到目标节点的前驱节点。

 

// 删除
void Deletekey(Lnode *head,int key)
{
   if(head->next == NULL) // 空表
   {
       printf("empty list!\n");
       return;
   }
   Lnode *p = head,*r; // r 用来表示目标位置前一个地址,前驱
   while(p&&p->data!=key)
   {
       r = p;
       p = p->next;
   }
   if(p==NULL) {cout<<"Not Have This key"<<endl;return;}
   r->next = p->next; // 相当于跳过 p ,指向 p 的下一个结点。
   delete p; // 释放内存。
   n--; cout<< "Delete success" <<endl; // n 为链表长度减少一 ,成功
}

 

双向链表:

 

单链表的存储结构的结点中只有一个指示直接后继的指针域,若要寻查结点的直接前驱,则必须从表头指针出发,换句话说,在单链表中,查找直接后继结点的执行时间为 O(1),而查找直接前驱的执行时间为 O(n),为了克服这种单向性的缺点,我们可利用双向链表。

 

双向链表的定义:

 

顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱,上代码。

 

// --------- 双向链表的存储结构---------
typedef struct Dulnode{
   int data; // 数据域
   struct Dulnode *prior; // 指向直接前驱
   struct Dulnode *next; // 指向直接后继
}Dulnode;

 

创建双向链表:

 

跟单链表创建的方式很接近,这里就不再给出详细的注释了,如果理解有困难,可以看看后面介绍的双向链表的插入,两个操作非常的相似。

 

// 后插法建立双向链表,不再写有详细注释了
Dulnode *Createdullist(Dulnode *head,int n){
   head = new Dulnode ;
   head->next = NULL;
   head->prior = NULL;
   Dulnode *r = head;
   for(int i=0;i<n;i++)
   {
       Dulnode *p = new Dulnode ;
       cin>>p->data;
       p->next = NULL;
       p->prior = r;
       r->next = p; r=p;
   }
   return head;
}

 

双向链表的插入:

 

双向链表在插入的操作中,需要同时修改两个指针,为了方便理解,我特地找了一张图过来的,结合代码注释,相信能够很好理解的。

 

 

// 插入
void Insert_dul(Dulnode *head,int i,int x)
{ // 在 i 位置,插入值为 x 的结点
   Dulnode *p = head->next;int j =0; // 这个过程跟单链表一样,注意 p 为首元结点
   while(p&&j<i-1){// 查找第 i-1 个结点,p 指向该结点
       p = p->next;++j;// 看不懂的话,可以用 i 等于 1,进行检查程序。
   }
   if(!p||j>i-1) cout<<"ERROR"<<endl;// 没有找到指定的位置
   Dulnode *s = new Dulnode ; // 生成新结点 *s ;
   s->data = x;    // 将结点 *s 的数据域值置为 x;
   s->prior = p->prior; // 图操作 1
   p->prior->next = s;// 图操作 2
   s->next = p;// 图操作 3
   p->prior = s;// 图操作 4
   cout<< "success" <<endl; // 成功
}

 

双向链表的删除:

 

由于双向链表中每个节点记录了它的前驱结点,所以不需要像单向链表中一样索引目的节点的前驱节点,而是可以通过目标节点直接获得。

 

// 删除
void Delete_dul(Dulnode *head,int i)
{
   if(head->next == NULL) // 空表
   {
       printf("empty list!\n");
       return;
   }
   Dulnode*p = head->next; int j =0;
   while(p&&j<i-1){// 查找第 i-1 个结点,p 指向该结点
       p = p->next;++j;
   }
   if(!p||j>i-1) cout<<"ERROR"<<endl;// 没有找到指定的位置
   p->prior->next = p->next;// 图操作 1
   p->next->prior = p->prior;// 图操作 2
   delete p;// 释放内存。
}

 

循环链表:

 

循环链表的主要特点是表中最后一个结点的指针域指向头节点,整个链表形成一个环,由此,从表中任一结点出发均可找到表中其他结点。我这里主要再介绍下循环单链表。

 

循环单链表的操作基本上和单链表的一致,主要差别在于:当链表遍历时,判别当前指针 p 是否指向表尾结点的终止条件不同。在单链表中的判别条件为 p->next != NULL ,而循环单链表的判别条件为 p->next != head 。

 

合并循环单链表:

 

将两个循环单链表合并成一个表时,仅需要将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点即可。

 

void *Connect(Lnode *tail1,Lnode *tail2){
   Lnode *p = tail1->next; //  p 存表头结点
   tail1->next = tail2->next->next; //表1尾 连结表 2 首元结点;
   delete tail2->next; // 释放表 2 的头结点
   tail2->next = p; // 表 2 尾指向 表 1 头结点。
   
}

 

我个人感觉,想要学好数据结构首先就要自己多画图,多画画图自然也就理解其中的具体操作过程了。如果文章中存在错误,欢迎大家前来指正,如果有不明白的地方,也欢迎给我留言,我们一起学习的。完整代码,微信公众号后台回复 [ 单链表 ],[ 双链表 ] 即可获得。

 

如果觉得文章还不错的话,还请大家点赞分享下。算是对「正经的码农」最大的支持!

同时欢迎大家关注微信公众号「正经的码农」更多好文章等着你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值