从来没有想到过自己会写这方面的技术文,可以说是对自己的一个挑战的吧,有兴趣的朋友可以一起学习的,应该会坚持写一个系列的,话不多说,我们开始吧。
线性表:它是最基础、最简单、最常用的一种基本数据结构,线性表总存储的每个数据称为一个元素,各个元素及其索引是一一对应的关系。线性表有两种存储方式:顺序存储方式和链式存储方式。
链表常用的有 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 头结点。
}
我个人感觉,想要学好数据结构首先就要自己多画图,多画画图自然也就理解其中的具体操作过程了。如果文章中存在错误,欢迎大家前来指正,如果有不明白的地方,也欢迎给我留言,我们一起学习的。完整代码,微信公众号后台回复 [ 单链表 ],[ 双链表 ] 即可获得。
如果觉得文章还不错的话,还请大家点赞分享下。算是对「正经的码农」最大的支持!