考研数据结构之双向链表

本文中有笔者的愚见,欢迎大家指正

提示:若是有时间 这边建议先把单链表学会再来搞这一篇双向链表基础篇,毕竟双向是单向的延申,拿这一篇与上一篇作为对比,下一篇博文计划写双向链表的拔高篇

附上一篇写的很好的单链表的博文!!!
有的书上使用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网络请求的数据。


总结

提示:这里对文章进行总结:在这里必须要吐槽一下自己了,逆序输出的代码想写不等于,写成等于号 结果调试好久 都服了,其实你上一个掌握了,在知道我开篇提出的那些知识 可以很快基于上一篇文章修改处这个代码的

若是文章对你的提升由哪怕一点帮助的话 请答应我 不要吝啬你的点赞评论

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值