本文中有笔者的愚见,欢迎大家指正
提示:若是有时间 这边建议先把单链表学会再来搞这一篇双向链表基础篇,毕竟双向是单向的延申,拿这一篇与上一篇作为对比,下一篇博文计划写双向链表的拔高篇
附上一篇写的很好的单链表的博文!!!
有的书上使用DLinkList强调是一个链表,有的使用DNode*是强调是一个结点,虽然运算是一样的结果但是意义有所区别,下面的代码就没有注意这个问题,但是我也不改了,大家注意一下就好
文章目录
前言
提示:本文写的是双向链表,更加注重代码的实现,目的是与上一篇中单链表做对比
提示:以下是本篇文章正文内容,下面案例可供参考
一、首先还是使用一张思维导图来开始我们今天的闹剧吧!!!
二、对插入以及删除的解析
对插入删除 因为有头节点的缘故 ,所以插入或者删除的时候 ,主题思想你只需要考虑末尾的时候就行了
(ps) 有些人可能会说 为什么你不讨论是不是第一个结点,是不是最后一个结点上面的 我这里就不详细的用文字来表述了 ,代码中有讨论 ,但这里可以给你一个另外一个思路,既然存在头结点 为什么你不可以设置一个尾部设置一个哨兵结点 其中也不需要存储信息呀,当然这一篇中没有涉及。
1、插入
很多人被这四步搞不明白 亦或者是被则四步吓到了,我们就来解析一下这四步,
我们设 要插入的结点为(NewNode) (NewNode->next=NULL) ( NewNode->prev=NULL;)
第一步,我们要知道我们已有的条件,知道了要插入结点的位置信息,通过这个我们可以找到要插入结点的前驱以及后继 也就是知道了 ai(PreNode ) 和a(i+1)(PreNode->next;) 通过这两个数据我们就可以实现插入。
第二步,同过以上的五个数据 我们发现什么是没有用的,也就是这两个NULL似乎是没有用的,所以我们就先让他们两个有用,NewNode->next=PreNode->next; 和 NewNode->Pre=PreNode; 也就对应上面的第一步第二步,所以你发现这两步是可以交换顺序的,因为没有发生数据的覆盖,有效数据没有丢失。
第三步,NewNode结点的两个指针已经处理好了,但是前驱结点的PreNode->next后继的指向与后继结点NewNode->next (或者写成PreNode->next)的前驱的指向依然没有改变(后继结点的前驱可以写成PreNode->next->prev) 。 现在我们来改变它两个的指向,PreNode->next->prev=NewNode; PreNode->next=NewNode;这样写不能改变,但是若是这样写NewNode->next->prev=NewNode; PreNode->next=NewNode 这样写 两个就是可以改变的 (介于篇幅问题 这里原因就不详述了,实在不行留言一下)
2、删除
第一步相较于插入就简单多了, 直接找到要删除结点的前驱PreNode结点后继指向 跳过要删除结点 PreNode->next=PreNode->next->next;(或者写成PreNode->next=DelNode->next)
第二步要删除结点后继结点的前驱的指向跳过要删除的结点DelNode->next->prev=PreNode;
同样的问题这两步能不能交换次序呢? 这样写是可以交换顺序的,重要一点要用一个DelNode来保存要删除的结点 以及free(DelNode) 就是别忘了 要释放,
对插入删除的小结:尽管网上有许多关于能不能交换顺序的判断 , 个人愚见:主要看你写的语句,关键是否会值覆盖,值覆盖会不会导致你进行下一步的时候,是不是之前的那个值了
三、代码改动(我给改动的部分拿出来了)
注 :单前驱 或者单后继都是很好判断的 ,但是若是前驱的后继 或者后继的前驱的时候 也别忘了是否会取到非法地址;使用尾插法的时候别忘了 因为咱们使用了一个tail来标记尾部,所以这个尾部在一次填入数据也是需要移动的,T=T->>next;,
1、定义结构体的时候是要多一个指针的
typedef struct LNode{
ElemType data;
struct LNode *prev;
struct LNode *next;
}LNode,*LinkList;//这两个是等价的 但是意思上可能有区别,本篇开篇中的那个黄色的部分。
2、较上一篇添加了逆序打印
status AbnormPrintList(LNode* L){//添加一个逆序的功能
LNode* P=L->next;
while(P->next!=NULL){//指向最后一个结点
P=P->next;
}
cout<<"逆向打印出来的值是"<<endl;
while(P->prev!=NULL){//头结点的前驱我们设置的也是NULL 现在我们要在首结点处停
cout<<P->data<<" ";
P=P->prev;
}
cout<<endl;
}
3、初始化的时候两个指针赋值
status InitList(LinkList &L){
/*所谓初始化就是给结构体中变量赋值,分为带头结点与不带头节点
数据域不需要赋值 ,这里用的是引用符号,因为要对其中的L进行修改 */
//带头节点,就需要申请一个头结点L此时就是头结点
L=(LNode*)malloc(sizeof(LNode));
L->next=NULL; L->data=-1;
L->prev=NULL;
/*头指针中的数据域可以用来存放链表的长度;
因为使用了malloc所以就要加一个判断是否申请成功*/
if(NULL==L){
cout<<"空间申请失败"<<endl;
exit(1);
}
else{
cout<<"空间申请成功"<<endl;;
}
//若是不带头节点
//L=NULL;
return OK;
}
4、头插法建立双链表(考虑原来是否是空表)
//头插法建立单链表
status ListHeadInsert(LNode* &L){
cout<<"进入Head中"<<endl;
ElemType e;
LNode* P=NULL;//作为新插入的结点
cout<<"请输入一系列值"<<endl;
while(scanf("%d",&e)!=EOF){
//要插入数据就要重新搞一个结点,结点里面放数据
P=(LNode*)malloc(sizeof(LNode));
if(P==NULL){
printf("空间申请失败");
exit(1);
}
P->data=e;
/*将P这结点插入的链表中,头结点存储的信息交给P中存储
再将P的地址信息交给头结点*/
P->next=L->next;
P->prev=L;
if(L->next==NULL){//这里需要加一个判断是否为空表 会少一个后继的前驱
L->next=P;
}
else{
L->next->prev=P;
L->next=P;
}
}
NormPrintList(L);
return OK;
}
在使用某一个指针的时候要考虑这个指针是否可能为NULL
5、尾插法(少了一个后继的前驱的赋值)
status ListTailInsert(LNode* &L){
cout<<"进入Tail中"<<endl;
ElemType e;
LNode* P=NULL;//P作为新插入的结点
LNode* T=L; //T的作用找到yi巴并标记;
cout<<"开始输入"<<endl;
while(T->next!=NULL) T=T->next;
cout<<"请输入一系列的值"<<endl;
while(scanf("%d",&e)!=EOF){
//要插入数据就要重新搞一个结点,结点里面放数据
P=(LNode*)malloc(sizeof(LNode));
P->data=e;
P->next=NULL;
P->prev=T;
T->next=P;
T=T->next;//尾插法需要注意最后yi巴的位置也是需要移动的。
//注意这里少了一个后继的前驱的赋值 尾插法没有后继的前驱
}
NormPrintList(L);
return OK;
}
6、删除(考虑删除的是否是尾结点 因为没有后继的前驱这一项)
6.1 提供某值删除结点
status DeleteListElemType(LNode* &L,ElemType e){
LNode* Pre=L->next;//用来遍历链表 找到要删除的元素
LNode* DelNode=NULL;
while(Pre!=NULL&&Pre->data!=e){//没有到末尾且没有找到e
Pre=Pre->next;//遍历指针向后移动
}
if(Pre==NULL){
printf("没有找到元素\n");
cout<<e;
return ERROR;
}
else{//否则就是找到了 ,此时我们要考虑是不是尾结点,不需要考虑是不是首结点,因为有头结点还在罩着,所以首结点不需要特殊处理。
if(Pre->next==NULL){//尾结点
DelNode=Pre;//保存便于释放
DelNode->prev->next=NULL;//也是只需要进行一步操作就可以了。
free(DelNode);
}
else{//除了尾结点之外的任何结点
DelNode=Pre;
DelNode->prev->next=DelNode->next;
DelNode->next->prev=DelNode->prev;
free(DelNode);
}
}
NormPrintList(L);
return OK;
}
6.2给某个结点删结点
DeleteListLNode(LNode *L,LNode* Ptr){//这里我们使用第二种方式
LNode* P=L;
if(Ptr->next==NULL){
cout<<"删除的是尾结点"<<endl;
Ptr->prev->next=NULL;
free(Ptr);
}
else{//这里就不需要值覆盖了 直接删
cout<<"不是尾结点"<<endl;
Ptr->prev->next=Ptr->next;
Ptr->next->prev=Ptr->prev;
}
NormPrintList(L);
}
6.3给某个位置删结点
status DeleteListLocation(LNode* L,int i){
LNode* P;
LNode* Pre=L->next;
while((--i)&&Pre!=NULL){//定位到要删除的元素
Pre=Pre->next;
}
if(Pre==NULL){
cout<<"你输入的数字不对"<<endl;
}
else{
if(Pre->next==NULL){//是末尾结点
Pre->prev->next=NULL;
}
else{//若不是末尾结点
Pre->prev->next=Pre->next;
Pre->next->prev=Pre->prev;
}
}
NormPrintList(L);
return OK
}
四、可执行代码汇总
//InitList(&L):初始化一个空的线性表
//Length(L):求表长,返回线性表L的长度,即即L中数据元素的个数
//LocateElem(L,e):按值查找操作,即获取表L中具有给定关键字值的元素
//GetElem(L,i):按位查找操作,获取L中第i个位置上的元素的值
//ListHeadInsert(&L,e):插入操作,使用头插法建立单链表
//PrintList(L):输出操作,按照前后 顺序输出线性表L的所有元素的值
//Empty(L):判空操作:若是L为空表,返回true,否则返回false
//DeleteLite(&L,i);删除结点,其中有两种,一种是删除某个位置
//DeleteLite(&L,*ptr) 删除某个给定的结点
//DestoryList(&l):销毁操作,销毁线性表,并释放线性表L占用的存储空间
//考试的时候最好也是使用这些名称
#include<bits/stdc++.h>
#define OK 1;
#define ERROR 0;
#define status int
#define ElemType int
using namespace std;
typedef struct LNode{
ElemType data;
struct LNode *prev;
struct LNode *next;
}LNode,*LinkList;//这两个是等价的
//打印链表
status NormPrintList(LNode* L){
cout<<"此时的链表中的数据是"<<endl;
LNode *P=L->next;
while(P!=NULL){
cout<<P->data<<" ";
P=P->next;
}
cout<<endl;
return OK;
}
status AbnormPrintList(LNode* L){//添加一个逆序的功能
LNode* P=L->next;
while(P->next!=NULL){//指向最后一个结点
P=P->next;
}
cout<<"逆向打印出来的值是"<<endl;
while(P->prev!=NULL){//头结点的前驱我们设置的也是NULL 现在我们要在首结点处停
cout<<P->data<<" ";
P=P->prev;
}
cout<<endl;
}
status InitList(LinkList &L){
/*所谓初始化就是给结构体中变量赋值,分为带头结点与不带头节点
数据域不需要赋值 ,这里用的是引用符号,因为要对其中的L进行修改 */
//带头节点,就需要申请一个头结点L此时就是头结点
L=(LNode*)malloc(sizeof(LNode));
L->next=NULL; L->data=-1;
L->prev=NULL;
/*头指针中的数据域可以用来存放链表的长度;
因为使用了malloc所以就要加一个判断是否申请成功*/
if(NULL==L){
cout<<"空间申请失败"<<endl;
exit(1);
}
else{
cout<<"空间申请成功"<<endl;;
}
//若是不带头节点
//L=NULL;
return OK;
}
int Length(LNode* L){//判断长度要设置一个计数器
int count=0;
LNode* P=L;//P初始化为头指针
while(P->next!=NULL){
P=P->next;//这一句话相当于是将P的下一个位置的信息给了P 也就实现了P向后移动的操作
++count;
}
NormPrintList(L);
cout<<"此时的链表长度是"<<count<<endl;
/*加上这样一句话方便判断代码的问题*/
return count;
}
LNode* LocateElem(LNode* L,ElemType e){//这个时候返回位置就没有意义了,当然是返回指针,
LNode* P=L;
int count=0;//从头开始数的所以这里初始化就为0;
//尾结点的P也不是NULL ,tail->next==NULL
while(P&&P->data!=e){
P=P->next;//向后移动
++count;
}
if(P==NULL){
cout<<"未发现元素"<<endl;
return NULL;
}
else printf("在第%d个位置发现了元素\n",count);
NormPrintList(L);
return P;
}
//获取链表某一个位置的值
ElemType GetElem(LNode* L,int i){
LNode* P=L->next;
while((--i)&&P){
P=P->next;
}
NormPrintList(L);
if(P==NULL) {
cout<<"你输入的位置不正确"<<endl;
}
else {
return P->data;
}
}
//头插法建立单链表
status ListHeadInsert(LNode* &L){
cout<<"进入Head中"<<endl;
ElemType e;
LNode* P=NULL;//作为新插入的结点
cout<<"请输入一系列值"<<endl;
while(scanf("%d",&e)!=EOF){
//若是其中使用cin>>e;第一遍走程序还没有问题,但是后面走就有问题了 ,不知道原因
//要插入数据就要重新搞一个结点,结点里面放数据
P=(LNode*)malloc(sizeof(LNode));
if(P==NULL){
printf("空间申请失败");
exit(1);
}
P->data=e;
/*将P这结点插入的链表中,头结点存储的信息交给P中存储
再将P的地址信息交给头结点给*/
P->next=L->next;
P->prev=L;
if(L->next==NULL){//这里需要加一个判断是否为空表 会少一个后继的前驱
L->next=P;
}
else{
L->next->prev=P;
L->next=P;
}
/*可能你会问我,这两句话,能不能反过来,当然不能,因为要是翻过来
L原来首结点的地址没有被保存,就被断开了 这里的第一步既是连接新生成的
结点 同样的也是保存了首结点的地址*/
}
NormPrintList(L);
return OK;
}
//尾插法建立单链表
status ListTailInsert(LNode* &L){
cout<<"进入Tail中"<<endl;
ElemType e;
LNode* P=NULL;//P作为新插入的结点
LNode* T=L; //T的作用找到 yi巴并标记;
cout<<"开始输入"<<endl;
while(T->next!=NULL) T=T->next;
cout<<"请输入一系列的值"<<endl;
while(scanf("%d",&e)!=EOF){
//要插入数据就要重新搞一个结点,结点里面放数据
P=(LNode*)malloc(sizeof(LNode));
P->data=e;
P->next=NULL;
P->prev=T;
T->next=P;
T=T->next;
//注意这里少了一个后继的前驱的赋值 尾插法没有后继的前驱
}
NormPrintList(L);
return OK;
}
//判断链表是否为空
bool Empty(LNode *L){
if(L->next==NULL) {
return true;
}
else {
return false;
}
}
//删除某一个元素(给元素) 不需要快慢指针了
status DeleteListElemType(LNode* &L,ElemType e){
LNode* Pre=L->next;//用来遍历链表 找到要删除的元素
LNode* DelNode=NULL;
while(Pre!=NULL&&Pre->data!=e){//没有到末尾且没有找到e
Pre=Pre->next;//遍历指针向后移动
}
if(Pre==NULL){
printf("没有找到元素\n");
cout<<e;
return ERROR;
}
else{//否则就是找到了 ,此时我们要考虑是不是尾结点,不需要考虑是不是首结点,因为有头结点还在罩着
if(Pre->next==NULL){//尾结点
DelNode=Pre;//保存便于释放
DelNode->prev->next=NULL; //也是只需要进行一步操作就可以了
free(DelNode);
}
else{//除了尾结点之外的任何结点
DelNode=Pre;
DelNode->prev->next=DelNode->next;
DelNode->next->prev=DelNode->prev;
free(DelNode);
}
}
NormPrintList(L);
return OK;
}
//删除某一个给定的结点
DeleteListLNode(LNode *L,LNode* Ptr){//这里我们使用第二种方式
LNode* P=L;
if(Ptr->next==NULL){
cout<<"删除的是尾结点"<<endl;
Ptr->prev->next=NULL;
free(Ptr);
}
else{//这里就不需要玩值覆盖了 直接删
cout<<"不是尾结点"<<endl;
Ptr->prev->next=Ptr->next;
Ptr->next->prev=Ptr->prev;
}
NormPrintList(L);
}
status DeleteListLocation(LNode* L,int i){
LNode* P;
LNode* Pre=L->next;
while((--i)&&Pre!=NULL){//定位到要删除的元素
Pre=Pre->next;
}
if(Pre==NULL){
cout<<"你输入的数字不对"<<endl;
}
else{
if(Pre->next==NULL){//是末尾结点
Pre->prev->next=NULL;
}
else{//若不是末尾结点
Pre->prev->next=Pre->next;
Pre->next->prev=Pre->prev;
}
}
NormPrintList(L);
return OK
}
//销毁的话 ,还是需要两个指针,基本思想不变,一动一静
status DestoryListL(LNode* &L){//销毁一个链表
if(L->next==NULL){
cout<<"为空链表"<<endl;
}
LNode* Pre=L->next;LNode* Next=L;
while(Pre){
Next=Pre;
Pre=Pre->next;
free(Next);
}
L->next=NULL;
cout<<"成功销毁"<<endl;
return OK;
}
status ModifyLocation(LNode* &L,int i,ElemType e){
if(Length(L)<i||i<0){
cout<<"你输入的位置信息不对"<<endl;
return ERROR;
}
LNode *P=L->next;
while(--i){
P=P->next;
}
P->data=e;
cout<<"修改之后表是";
NormPrintList(L);
return OK
}
//修改所有值为e的改成value
status ModifyElemType(LNode* &L,ElemType e,ElemType value){
if(LocateElem(L,e)==NULL){
printf("你输入的值表中不存在\n");
return ERROR;
}
LNode *P=L->next;
while(P){
if(P->data==e) P->data=value;
P=P->next;
}
cout<<"修改之后的表是";
NormPrintList(L);
return OK;
}
/*******************操作函数***************************/
//上述中使用两种方式,一种是头插入,一种是未插入
void Add(LNode* &L){
int flag;
cout<<"请输入你的选择"<<endl;
cout<<"1、头插入 2、yi巴插入"<<endl;
cin>>flag;
if(1==flag) ListHeadInsert(L);
else ListTailInsert(L);
}
void Delete(LNode* &L){
int flag;
int Location;
ElemType value;
LNode* P;
cout<<"1、删除某值,2、删除某一个具体结点 3、删除某一个位置 4、删除全表"<<endl;;
cin>>flag;
switch(flag){
case 1:{
cout<<"请输入你要删除的值"<<endl;
cin>>value;
DeleteListElemType(L,value);
break;
}
case 2:{//我们不好找一个具体结点,但是我们可以通过LocateElem(L,e)来返回一个指针来确定一个具体的结点
cout<<"请输入你要删除的结点的值" <<endl;
cin>>value;
P=LocateElem(L,value);
DeleteListLNode(L,P);
break;
}
case 3:{
cout<<"请输入一个具体的位置"<<endl;
cin>>Location;
DeleteListLocation(L,Location);
break;
}
case 4:{
DestoryListL(L);
break;
}
default:break;
}
}
void Modify(LNode* &L){
int choice;int location;ElemType value,value2;
cout<<"1、修改某个位置 2、修改某个值"<<endl;
cin>>choice;
switch(choice){
case 1:{
cout<<"请输入你要修改的位置"<<endl;
cin>>location;
cout<<"请输入你现在要放入的值"<<endl;
cin>>value;
ModifyLocation(L,location,value);
break;
}
case 2:{
cout<<"请输入你修改的值"<<endl;
cin>>value;
cout<<"你现在可以输入要替换的值"<<endl;
cin>>value2;
ModifyElemType(L,value,value2);
break;
}
default:break;
}
}
void Seek(LNode* &L){
int choice;int location;
cout<<"1、表中元素的个数 2、表是否为空 3、某一个位置的值 4、逆向打印表"<<endl;
cin>>choice;
switch(choice){
case 1:{
cout<<"表中的元素个数是"<<Length(L);
break;
}
case 2:{
if(!Empty(L)){
cout<<"表不为空"<<endl;
}
else cout<<"表为空"<<endl;
break;
}
case 3:{
cout<<"请输入你想获取的位置"<<endl;
cin>>location;
cout<<"你想获取的位置上的数是"<<GetElem(L,location);
break;
}
case 4:{
AbnormPrintList(L);
break;
}
default:break;
}
}
void menu(){
cout<<"请输入你要进行的操作"<<endl;
cout<<"1、增加 2、删除 3、修改 4、查看 5、退出"<<endl;
}
int main()
{
LinkList L;int choice;
InitList(L);
while(1)
{
menu();
printf("请输入菜单序号:\n");
scanf("%d",&choice);
if(choice==5) break;
switch(choice)
{
case 1:Add(L);break;
case 2:Delete(L);break;
case 3:Modify(L);break;
case 4:Seek(L);break;
default:printf("输入错误!!!\n");
}
}
return 0;
}
2.读入数据
代码如下(示例):
data = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())
该处使用的url网络请求的数据。
总结
提示:这里对文章进行总结:在这里必须要吐槽一下自己了,逆序输出的代码想写不等于,写成等于号 结果调试好久 都服了,其实你上一个掌握了,在知道我开篇提出的那些知识 可以很快基于上一篇文章修改处这个代码的
若是文章对你的提升由哪怕一点帮助的话 请答应我 不要吝啬你的点赞评论