C数据结构线性表:最全链表实战剖析—单 双 循环链表&增删改查

前言

说明1

文章首先是从单链表引入,由浅入深拓展到循环链表,但是循环链表我没有给代码,原因很简单,基本操作基本和单链表一样,就是判断条件从之前的是否为NULL变成了是否为头指针。比较着重性的给出单链表的具体如何操作与实现,循环链表的部分会提一下在单链表中如何修改成为循环链表。

说明2

双向链表具体实现其实也和单链表差不多,但是双向链表在连接节点的时候步骤比较多,我在文章和代码里面也总结了一下双向链表连接节点需要的“四步法。” 另一个循环双向链表我就不提了,因为看到这的小伙伴想必都对链表的功能实现已经烂熟于心。

再啰嗦一下:
我是用DEV_C++来编译运行的。
我在每一个部分完结的时候放出该部分的源代码,大家可以自取,如果是因为编译器的不同出现问题大家不必慌张,可以私信我或者你找出原因将一些小细节修改一下就好

接下来就让我们正式开始吧~

A:关于为什么传链表要用二级指针

只要想对链表本身进行一些不可描述的操作的时候,都要用到二级指针。
这是为什么呢?
因为如果你想传进来的外部的这个变量本身就是一个一级指针的话,把他传进来后,你的参数也只是把传进的这个指针地址复制一遍,进入函数的时候也只是把形参的指针进行修改,而实参的指针不会发生改变,因为你那个是复制品,真正能把链表修改的一级指针在实参那边,而函数这边并没有修改到。
所以这时候需要二级指针,进行一层解引用之后,也就是用*号解一层指针,解出来的必然是真正的链表所能修改的指针。
当你用二级指针拿到了链表真正的地址之后就能对链表为所欲为的实施一些不可描述的操作了。

B:单链表

1:定义结构体

这里用的是typedef, 所以在后面你可以看到我直接用了*Link_p来定义结构体指针,所以在后面看到这个的时候要反应过来来是一个结构体指针。

typedef struct _Link{
	int num;
	struct _Link *next;
}Link, *Link_p;

2:初始化链表

初始化链表的时候头插法和尾插法所插入链表的顺序是不一样的,可以根据你数据的具体情况所定,我这里就直接给出两种方法来。
还有需要注意的是,当你每次想对链表进行一些操作的时候,一定要单独考虑链表的头尾特殊情况,因为这两部分和中间不一样,中间是前趋后继都有,而头部只有后继,尾部只有前趋,尾部后继是NULL.

a)头插法

头插法顾名思义从链表的头部开始插入,新的元素不断从链表的头部进去,所以在你打印单链表的时候就会打印出逆序的信息。
如:输入123,打印:321
①函数声明

void InitLink_toHead(Link_p*, int);
//初始化链表——头插法 ,传入链表和要初始化链表的长度 

②函数功能实现

void InitLink_toHead(Link_p*L, int len)
//传入二级指针才能对链表L进行修改 
{
 	int i;
 	for(i = 0; i < len; i++)
	 {
	 	if((*L) == NULL)
	 	{
	 		Link_p newElem = (Link_p)malloc(sizeof(Link));
	 		if(newElem == NULL)
			{
				PF(分配空间失败!); 
				return;
			}
		 	newElem->num = i+1;
		 	newElem->next = NULL;
		 	(*L) = newElem;
		 }
		 else
		 {
		 	Link_p newElem = (Link_p)malloc(sizeof(Link));
		 	if(newElem == NULL)
			{
				PF(分配空间失败!); 
				return;
			}
		 	newElem->num = i+1;
		 	newElem->next = (*L);
		 	(*L) = newElem; 
		 }
	  }
 }

b)尾插法

尾插法顾名思义从链表的尾部插入,当你输入数据的时候,会按顺序在链表中排好序。
如:输入123 输出:123
这个尾插法比较适合用于一些有顺序的数据

具体实现过程中想比头插法多了一个变量,就是用一个指针变量存放尾部指针,当你每次放进一个元素都能直接取到链表的尾部,能快速方便的直接存进尾部,这时候的尾部指针也要改变,变成新插入在尾部的元素,方便下一个元素插进来也能直接取到尾部的位置。
①函数声明

void InitLink_toTail(Link_p*, int);//初始化链表——尾插法

②函数功能实现

void InitLink_toTail(Link_p*L, int len)
{
 	int i;
 	Link_p tail, current, newElem;
 	
 	for(i = 0; i < len; i++)
 	{
 		if((*L) == NULL)
 		{
 			newElem = (Link_p)malloc(sizeof(Link));
 			newElem->num = i + 1;
 			(*L) = newElem;
 			newElem->next = NULL;
 			tail = newElem;//记录尾部指针 
		 }
		 else
		 {
		 	newElem = (Link_p)malloc(sizeof(Link));
		 	newElem->num = i + 1;
		 	tail->next = newElem;//把尾部指向新空间 
			newElem->next = NULL;//把尾部继续指向NULL
			tail = newElem;//继续记录尾部指针 
		 }
	 }
 } 

3:销毁链表内容 (释放整个链表空间,把L指针赋值为NULL )

这个函数很简单就不赘述了,直接上代码。

①函数声明

void DestoryLink(Link_p*);
//释放整个链表空间 ,把L指针赋值为NULL

②函数功能实现

void DestoryLink(Link_p*L)
{
	Link_p temp;
	while((*L) != NULL)
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
} 

4:增加某一个位置上的元素

这里是链表中比较重要的一个函数功能,在实现过程中也是要时时刻刻记得要把头尾两部分单独拿出来想想。代码里面都有注释,放心食用。
①函数声明

void AddElem(Link_p*, int Locate, int AddElem);
//增加某一个位置上的元素 ,第二个参数是插入元素的位置,第三个参数是插入的元素 

②函数实现功能

void AddElem(Link_p*L, int Locate, int AddElem)
{
	Link_p newElem, pre, cur;
	int count = 0;
	 cur = (*L); //把首空间地址给cur 
	if(Locate<1)
	{
		return;//如果给的位置是非法的下标位置就直接返回 
	} 
	while(cur != NULL && count < Locate-1)
	//第二个判断条件不能取等号,假设是加入的位置为第一个,那么还是会通过判断条件,就无法解决插入位置为首元素的情况了 
	{//找到该位置了就退出循环 ,减一是为了不让指针跑过头了,还能判断在插入位置为1的时候直接退出进行下面的赋值
		pre = cur;
		cur = cur->next;
		count++;
	}
	if(count == 0)//优先判断是否插入的是第一个位置 
	{
		newElem  = (Link_p)malloc(sizeof(Link));
		newElem->num = AddElem;
		newElem->next = (*L);//新空间的next指向首元素空间 ,取代他,自己变成首元素 
		(*L) = newElem;//把头指针指向新元素空间 
	 }
	 else//包括越界,如果越界那就直接把他加在链表的尾部、、、、如果不想传入的下标为越界的,那就加上if(cur!= NULL)就修改完成了 
	 {
	 	newElem  = (Link_p)malloc(sizeof(Link));
		newElem->num = AddElem;
		pre->next = newElem;//2位置的前继指向新空间 
		newElem->next = cur;//新空间的next指向2原本的空间 ,取代2位置,使原本的元素变为3位置 
	  }
} 

5:删除元素

a)删除某一个位置上的元素 ,并释放该元素

这个函数开始难度开始上升了,因为在删除元素的时候需要用两个指针变量,在链表开始搜索之前,pre为前一个指向结构体数据的指针,cur为当前的,结合两个指针就能完成删除的工作。就是把pre的下一个指向cur的下一个,那么现在cur就是无主之魂了,直接把他free掉,完成删除操作。

①函数声明

void DeleteElem_1(Link_p*, int Locate);
//删除某一个位置上的元素

②函数实现功能

void DeleteElem_1(Link_p*L, int Locate)
{
	int count = 0;
	Link_p pre, cur, temp;
	cur = (*L); 
	while(cur != NULL && count < Locate-1)
	{//第二个判断条件不能取等号,假设是加入的位置为第一个,那么还是会通过判断条件,就无法解决插入位置为首元素的情况了
		pre = cur;
		cur = cur->next;
		count++;
	}
	if(count == 0)//若为0则表示删除的是第一个位置的元素
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
	else
	{
		temp = cur;
		pre->next = cur->next;
		free(temp);
	}
}

b)删除某一个特定元素,链表中与之重复的也全部删除

这个删除功能相比第一个比价难度稍稍加大了些。在找到与之相同的数据之后,进行与上面那个函数一样的删除操作,但是这个函数还没结束,因为他还要继续扫描后面的数据是否还有要删除的,所以在删除完出来之后要把cur这个指针重新指向当前位置,继续往下扫描。
代码中这条语句就是(cur = pre->next;//把前继的地址给cur继续搜索 )

①函数声明

void DeleteElem_2(Link_p*, int D_Elem);
//传入要删除的元素,把所有有重复的都删除掉 

②函数功能实现

void DeleteElem_2(Link_p*L, int D_Elem)
{
	Link_p cur, pre = NULL, temp; 
	cur = (*L);
	while(cur->next != NULL)
	{
		
		if(cur->num == D_Elem && pre != NULL)
		{
			temp = cur;
			pre->next = cur->next;
			free(temp);
			cur = pre->next;//把前继的地址给cur继续搜索 
		 } 
		 //下面这两条语句一定要放在最后面,因为这一个循环只能处理中间部分,头尾部分不能处理 
		pre = cur;
		cur = cur->next;
	}
	/*下面是处理头尾特殊部分的*/ 
	if((*L)->num == D_Elem)//还要判断一下第一个元素是否是要删除的 
	{
		temp = (*L);
		(*L) = (*L)->next;//直接把头改掉
		free(temp); 
	}
	if(cur->num == D_Elem)//判断最后一个是否也为删除的元素 
	{
		temp = cur; 
		pre->next = NULL;
		free(temp);
	 } 
}

6:查找元素,返回最先找到的元素下标

在这函数中需要注意的是返回下标这个功能,我们传了一个int型的指针变量index进来,目的就是通过直接修改这个index的值,使得我们在主函数里面可以直接使用index,用来获取某一个元素的下标。
所以我们写函数的时候记得一定是要传进一个指针,进行赋值的时候也是要记得解引用,对改该变量进行修改。
①函数声明

void SearchElem(Link_p, int Elem, int *index);
/*第二个参数为要查找的元素 ,
第三个元素为返回该元素的下标 
,没找到就把index的值改为-1*/

②函数功能实现

void SearchElem(Link_p L, int Elem, int *index)
{
	int i = 1; 
	while(L != NULL)
	{
		if(L->num == Elem)
		{
			*index = i;
			break;
		}
		L = L->next;
		i++;
	}
	if(L == NULL) *index = -1;//没有找到就把下标修改为-1 
}

7:修改元素内容

修改这个功能也是比较重要,因为在实际生活中,我们应用比较多的就是修改功能了,所以对应在这块内容中,我也做了两个函数来实现。

a)修改某一个位置上的元素

这个函数需要注意的点是对locate的应用比较严格,下面用来判断是否是头尾也用到了Locate,在寻找中间部分的时候,判断条件也是Locate,就是用的时候要尽量细心一点,稍加不注意就会酿成大错,整个程序就会崩溃或者造成内存越界。
还有一个可能不懂的点是为什么要用count<Locate这个其实和count==Locate一样都是为了判断遇到了下标为Locate的时候退出循环。
①函数声明

void ChangeElem_1(Link_p*L, int Locate, int newElem);
//第二个参数为要修改元素的位置 ,第三个参数为新元素

②函数功能实现

void ChangeElem_1(Link_p*L, int Locate, int newElem)
{
	int count = 1;
	Link_p cur, temp;
	cur = (*L);
	if(Locate < 1) 
	{
		PF(非法位置不能修改!);
		return;//如果是非法位置也不修改直接返回
	}
	 
	while(cur != NULL && count < Locate)
	{
		cur = cur->next;
		count++;
	}
	if(cur != NULL) cur->num = newElem;//不为空则表示找到了,直接修改信息 
	else
	{
		PF(非法位置不能修改!);
		return;
	 } 
} 

b)修改某一个元素,与之重复的全部都替换掉

这个函数与上一个删除元素有异曲同工之处,都是把与要删除/修改的元素进行操作,包括与传进参数的元素重复的。
但是神奇的是反倒在这里的时候所用到的代码变少了,因为这里只需要替换数据内容,不需要修改指针的指向。应该也是比较容易理解的,就直接放代码了。
①函数声明

void ChangeElem_2(Link_p*L, int ChangeElem, int newElem);
//把要修改的元素全部改掉,若有重复全部都要修改掉 

②函数功能实现

void ChangeElem_2(Link_p*L, int ChangeElem, int newElem)
{
	Link_p cur, temp;
	cur = (*L);
	while(cur != NULL)
	{
		if(cur->num == ChangeElem) cur->num = newElem;
		cur = cur->next;
	 } 
} 

8:打印链表内容

比较简单直接放代码了。
①函数声明

void PLink(Link_p);//打印链表信息

②函数功能实现

void PLink(Link_p L)
{
 	int i;
 	Link_p current;
 	current = L;
 	while(current != NULL)
 	{
 		PF(%d\t, current->num);
 		current = current->next;
	 }
	 putchar('\n');
 } 

9:判断是否为空表

bool类型需要定义stdbool.h头文件
①函数声明

bool IsEmpty(Link_p);//判断是否为空表 

②函数功能实现

bool IsEmpty(Link_p L)
{
	if(L == NULL) return false;
	else return true;
}

源代码

main函数里面你可以全部删掉,那些只是我的测试代码而已。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdbool.h>
#define PF(format, ...) printf(#format, ##__VA_ARGS__)
/*A:单链表:
 1:初始化创建链表(a头插,b尾插)
 2:销毁链表内容 (释放整个链表空间,把L指针赋值为NULL ) 
 3:增加某一个位置上的元素
 4:a)删除某一个位置上的元素 ,并释放该元素 
 	b)删除某一个元素,与之重复的也全部删除 
 5:查找元素 ,返回最先找到的元素下标 
 6:a)修改某一个位置上的元素
 	b)修改某一个元素,与之重复的全部都替换掉 
 7:打印链表内容
 8: 判断是否为空表 
*/ 

typedef struct _Link{
	int num;
	struct _Link *next;
}Link, *Link_p;

void InitLink_toHead(Link_p*, int);//初始化链表——头插法 ,传入链表和要初始化链表的长度 

void InitLink_toTail(Link_p*, int);//初始化链表——尾插法, 

void DestoryLink(Link_p*);//释放整个链表空间 ,把L指针赋值为NULL 

void AddElem(Link_p*, int Locate, int AddElem);//增加某一个位置上的元素 ,第二个参数是插入元素的位置,第三个参数是插入的元素 

void DeleteElem_1(Link_p*, int Locate);//删除某一个位置上的元素 
void DeleteElem_2(Link_p*, int D_Elem);//传入要删除的元素,把所有有重复的都删除掉 

void SearchElem(Link_p, int Elem, int *index);//第二个参数为要查找的元素 ,第三个元素为返回该元素的下标 ,没找到就把index的值改为-1 

void ChangeElem_1(Link_p*L, int Locate, int newElem);//第二个参数为要修改元素的位置 ,第三个参数为新元素 
void ChangeElem_2(Link_p*L, int ChangeElem, int newElem);//把要修改的元素全部改掉,若有重复全部都要修改掉 

void PLink(Link_p);//打印链表信息 

bool IsEmpty(Link_p);//判断是否为空表 

int main(void)
{
	int index = 0;
	Link_p L = NULL;
	//InitLink_toHead(&L, 10); 
	InitLink_toTail(&L, 10);
	PLink(L);
	//DestoryLink(&L);
	AddElem(&L, 12, 15);
	//DeleteElem_1(&L, 5);
	DeleteElem_2(&L, 4); 
	//ChangeElem_1(&L, 5, 89);
	ChangeElem_2(&L, 5, 66); 
	if(IsEmpty(L)) PLink(L);
	SearchElem(L,10,&index);
	PF(*********%d\n, index);
	DestoryLink(&L);
	//FreeLink(&L);
	if(IsEmpty(L)) PLink(L);
	return 0;
 } 
 
void InitLink_toHead(Link_p*L, int len)//传入二级指针才能对链表L进行修改 
{
 	int i;
 	for(i = 0; i < len; i++)
	 {
	 	if((*L) == NULL)
	 	{
	 		Link_p newElem = (Link_p)malloc(sizeof(Link));
	 		if(newElem == NULL)
			{
				PF(分配空间失败!); 
				return;
			}
		 	newElem->num = i+1;
		 	newElem->next = NULL;
		 	(*L) = newElem;
		 }
		 else
		 {
		 	Link_p newElem = (Link_p)malloc(sizeof(Link));
		 	if(newElem == NULL)
			{
				PF(分配空间失败!); 
				return;
			}
		 	newElem->num = i+1;
		 	newElem->next = (*L);
		 	(*L) = newElem; 
		 }
	  }
 }
 
void InitLink_toTail(Link_p*L, int len)
{
 	int i;
 	Link_p tail, current, newElem;
 	
 	for(i = 0; i < len; i++)
 	{
 		if((*L) == NULL)
 		{
 			newElem = (Link_p)malloc(sizeof(Link));
 			newElem->num = i + 1;
 			(*L) = newElem;
 			newElem->next = NULL;
 			tail = newElem;//记录尾部指针 
		 }
		 else
		 {
		 	newElem = (Link_p)malloc(sizeof(Link));
		 	newElem->num = i + 1;
		 	tail->next = newElem;//把尾部指向新空间 
			newElem->next = NULL;//把尾部继续指向NULL
			tail = newElem;//继续记录尾部指针 
		 }
	 }
 	
 } 
 
void DestoryLink(Link_p*L)
{
	Link_p temp;
	while((*L) != NULL)
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
} 

void AddElem(Link_p*L, int Locate, int AddElem)
{
	Link_p newElem, pre, cur;
	int count = 0;
	 cur = (*L); //把首空间地址给cur 
	if(Locate<1)
	{
		return;//如果给的位置是非法的下标位置就直接返回 
	} 
	while(cur != NULL && count < Locate-1)
	//第二个判断条件不能取等号,假设是加入的位置为第一个,那么还是会通过判断条件,就无法解决插入位置为首元素的情况了 
	{//找到该位置了就退出循环 ,减一是为了不让指针跑过头了,还能判断在插入位置为1的时候直接退出进行下面的赋值
		pre = cur;
		cur = cur->next;
		count++;
	}
	if(count == 0)//优先判断是否插入的是第一个位置 
	{
		newElem  = (Link_p)malloc(sizeof(Link));
		newElem->num = AddElem;
		newElem->next = (*L);//新空间的next指向首元素空间 ,取代他,自己变成首元素 
		(*L) = newElem;//把头指针指向新元素空间 
	 }
	 else//包括越界,如果越界那就直接把他加在链表的尾部、、、、如果不想传入的下标为越界的,那就加上if(cur!= NULL)就修改完成了 
	 {
	 	newElem  = (Link_p)malloc(sizeof(Link));
		newElem->num = AddElem;
		pre->next = newElem;//2位置的前继指向新空间 
		newElem->next = cur;//新空间的next指向2原本的空间 ,取代2位置,使原本的元素变为3位置 
	  }
} 

void DeleteElem_1(Link_p*L, int Locate)
{
	int count = 0;
	Link_p pre, cur, temp;
	cur = (*L); 
	while(cur != NULL && count < Locate-1)
	{//第二个判断条件不能取等号,假设是加入的位置为第一个,那么还是会通过判断条件,就无法解决插入位置为首元素的情况了
		pre = cur;
		cur = cur->next;
		count++;
	}
	if(count == 0)//若为0则表示删除的是第一个位置的元素
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
	else
	{
		temp = cur;
		pre->next = cur->next;
		free(temp);
	}
}

void DeleteElem_2(Link_p*L, int D_Elem)
{
	Link_p cur, pre = NULL, temp; 
	cur = (*L);
	while(cur->next != NULL)
	{
		
		if(cur->num == D_Elem && pre != NULL)
		{
			temp = cur;
			pre->next = cur->next;
			free(temp);
			cur = pre->next;//把前继的地址给cur继续搜索 
		 } 
		 //下面这两条语句一定要放在最后面,因为这一个循环只能处理中间部分,头尾部分不能处理 
		pre = cur;
		cur = cur->next;
	}
	/*下面是处理头尾特殊部分的*/ 
	if((*L)->num == D_Elem)//还要判断一下第一个元素是否是要删除的 
	{
		temp = (*L);
		(*L) = (*L)->next;//直接把头改掉
		free(temp); 
	}
	if(cur->num == D_Elem)//判断最后一个是否也为删除的元素 
	{
		temp = cur; 
		pre->next = NULL;
		free(temp);
	 } 
}

void SearchElem(Link_p L, int Elem, int *index)
{
	int i = 1; 
	while(L != NULL)
	{
		if(L->num == Elem)
		{
			*index = i;
			break;
		}
		L = L->next;
		i++;
	}
	if(L == NULL) *index = -1;//没有找到就把下标修改为-1 
}

void ChangeElem_1(Link_p*L, int Locate, int newElem)
{
	int count = 1;
	Link_p cur, temp;
	cur = (*L);
	if(Locate < 1) 
	{
		PF(非法位置不能修改!);
		return;//如果是非法位置也不修改直接返回
	}
	 
	while(cur != NULL && count < Locate)
	{
		cur = cur->next;
		count++;
	}
	if(cur != NULL) cur->num = newElem;//不为空则表示找到了,直接修改信息 
	else
	{
		PF(非法位置不能修改!);
		return;
	 } 
} 

void ChangeElem_2(Link_p*L, int ChangeElem, int newElem)
{
	Link_p cur, temp;
	cur = (*L);
	while(cur != NULL)
	{
		if(cur->num == ChangeElem) cur->num = newElem;
		cur = cur->next;
	 } 
} 

void PLink(Link_p L)
{
 	int i;
 	Link_p current;
 	current = L;
 	while(current != NULL)
 	{
 		PF(%d\t, current->num);
 		current = current->next;
	 }
	 putchar('\n');
 } 

bool IsEmpty(Link_p L)
{
	if(L == NULL) return false;
	else return true;
}

//void Create_RoundLink(Link_p*L)
//{
//	Link_p tail;
//	tail = (*L);
//	while(tail->next != NULL) tail = tail->next;
//	tail->next = (*L);
//}
// 

C:循环链表

如何生成循环链表

我们只需要把生成好的单链表的头尾相接就好了,是不是听上去很简单,实际操作其实也是很简单。
这里我也写了一个函数方便理解

void Create_RoundLink(Link_p*L)
{
	Link_p tail;
	tail = (*L);
	while(tail->next != NULL) tail = tail->next;
	tail->next = (*L);
}

原理就是头尾相接没有什么好解释的。

循环链表的具体操作

循环链表的具体操作和单链表的一模一样!!哈哈!
但是不同的是判断条件不再是当遇到NULL的是时候退出了,而是当你遇到表头L的时候表示整个链表遍历完成,所以你只需要在单链表中的功能函数下修改一些判断条件就好了,其他的代码都几乎一模一样。

D:双向链表

前言

双向链表中,一个结构体包含连个指针,分别指前面、指后面
双向链表的操作其实也和单链表类似,但是他因为是双向的链表,就要求我们每次对链表进行操作的时候都需要把两个节点交互的地方进行连接,需要连接两个节点,就需要四步走。


当前元素的上一个元素的next需要指向当前元素
当前元素的prior需要指向当前元素的上一个元素
当前元素的下一个元素的prior需要指向当前元素
当前元素的next需要指向当前元素的下一个元素


看不懂的可以手动画图看看
四步法一步不能缺少才能形成一个双向链表

下面的函数我就不放函数声明了,你都看到这了,肯定能懂什么意思。

1:定义结构体

typedef struct _DULink{
	struct _DULink *prior; //前趋 
	int date;//元素数据 
	struct _DULink *next;//后继 
}DULink, *DULink_p;

2:初始化双向链表

!!重要说明!!

在双向链表中我们初始化的时候,不再是把头变为一个指针,而是把头变为一个空的结构体,因为这样方便于我们后面进行操作,比如对头部进行删除操作的时候,我们不用单独拿出来要把头直接改掉,而是正常的进行前趋后继的连接。
帅哥的叮嘱
用的时候一定要记得把指针率先指向首元素,因为你头部结构体是空的,在函数里面把( L)指向下一个空间再进行操作!!!!

a)头插法

void InitDULink_toH(DULink_p*L, int len)//使用的头部是一个空的结构体,方便于双向链表的前趋后继的操作 
{
	int i; 
 	(*L) = (DULink_p)malloc(sizeof(DULink));//分配出一个空的空间,便于后面他的next指向非空的新的元素空间 
 	(*L)->next = NULL;//先指向NULL,表示尾部
	(*L)->prior = NULL;//前趋也赋值为NULL方便后面使用头指针 
 	DULink_p temp, cur;
 	for(i = 0; i < len; i++)
	{
	 	if((*L)->next == NULL)
	 	{
	 		cur = (DULink_p)malloc(sizeof(DULink));
	 		cur->date = i+1; 
	 		
		 	(*L)->next = cur;
			cur->prior = (*L);
			cur->next = NULL; 
		}else
		{
			cur = (DULink_p)malloc(sizeof(DULink));
			cur->date = i+1;
			temp = (*L)->next;//先把首元素的空间存起来,因为后面要把他和新的空间连接起来 
			/*双向链表的交互四步法,
			两个元素的前趋后继连接的时候这四步一步都不能少*/
			(*L)->next = cur; 
			cur->prior = (*L);
			cur->next = temp;//刚刚存下来的temp元素需要和新的进行交接 
			temp->prior = cur;//temp的前继也要和新的空间进行连接 
		}
		
	} 
	 
}

b)尾插法

同理双向链表的尾插法和单链表一样,额外需要一个指针来记录尾部指针,以便于每次访问尾部都能直接访问到。

void InitDULink_toT(DULink_p*L,int len)//使用的头部是一个空的结构体,方便于双向链表的前趋后继的操作 
{
	int i;
	DULink_p temp, cur, tail;
	(*L) = (DULink_p)malloc(sizeof(DULink));
	(*L)->next = NULL;
	(*L)->prior = NULL;//前趋也赋值为NULL方便后面使用头指针 
	for(i = 0; i < len; i++)
	{
		if((*L)->next == NULL)
		{
			tail = (DULink_p)malloc(sizeof(DULink));
			tail->date = i+1;
			(*L)->next = tail;
			tail->prior = (*L);
			tail->next = NULL;
		}
		else
		{
			cur = (DULink_p)malloc(sizeof(DULink));
			cur->date = i+1;
			
			tail->next = cur;
			cur->prior = tail; 
			cur->next = NULL; 
			tail = cur;//为指针指向新的尾部 
		 } 
	}
	
} 

3:获取链表某一个元素的地址

为什么增加这个功能?

(双向链表有这个功能就方便很多,看到后面你就会爱上这个函数)

因为双向链表具有两个指针,只需要知道一个空间,就能知道他的前后是什么,这个函数是为后面的删除修改做辅助功能的比较多。
就是因为是为后面的删除修改做辅助作用,因此我在这也包装了两个函数,方便后面直接使用。

↓↓↓↓↓↓结构体指针函数说明↓↓↓↓↓↓

DULink_p GetElem是一个结构体指针类型的函数

因为我们要返回的是一个双向链表中某一个结构体的指针,所以在定义函数的时候也要用该结构体的指针,我们前面定义结构体的时候看过了,DULink_p就是链表的结构体指针,所以我们直接用就行,不需要再用DULink*。

a)传进一个数据元素,在链表寻找对应元素地址

用的时候一定要记得把指针率先指向首元素,因为你头部结构体是空的。
切记一定要记住,无论是什么函数,都要对链表的头尾进行单独的讨论和分析。
如果给出的下标不符合要求或者说不符合链表的长度,我们直接返回头部的空结构体,因为这个结构体是在我们初始化的时候定义的,不再是指针,而是一个空结构体,我们打印的时候是直接把他忽略掉的,所以请放心对头部进行你的为所欲为的一系列不可描述的操作。

DULink_p GetElem_1(DULink_p*L, int index)
{
	DULink_p temp;
	int count = 1;
	if(index < 1)//如果小于1下标位置直接返回头指针地址 
	{
		return (*L);
	}  
	temp = (*L)->next;//时刻记住你的链表表头是一个空的结构体,所以需要以next来移动到下一个首元素空间开始
	while(temp != NULL)
	{
		if(count == index)
		{
			return temp; 
	    } 
		else 
		{
			temp = temp->next;
			count++;
		}
	}
	if(temp == NULL) 
	{
		return (*L);//因为没有找到或者下标错误的话直接返回头指针 
	} 
}

b)传进元素下标位置,在链表寻找下标对应元素的地址

基本上你看到这个也不用怎么解释了。直接上代码吧~

DULink_p GetElem_2(DULink_p*L, int Elem)
{
	DULink_p temp;
	temp = (*L)->next;//时刻记住你的链表表头是一个空的结构体,所以需要以next来移动到下一个首元素空间开始 
	while(temp != NULL)
	{
		if(temp->date == Elem)
		{
			return temp;//这个是找到那个元素的结构体地址,是真实的,返回后可以直接拿这个地址进行修改链表的内容 
		}
		temp = temp->next;
	 }
	 if(temp == NULL)
	 {
	 	return (*L);//因为没有找到或者下标错误的话直接返回头指针 
	 } 	 
} 

4:增加某一个位置上的元素

这里就用到了我们上面包装的获取某个元素地址的函数了,有了上面的铺垫接下来就非常容易的进行实现。
注意的是,我们是需要首先对传进来的位置先进行获取地址的操作,因为我们知道这个地址后就方便我们对头尾进行特殊处理了。
(应该还记得我说过的,无论在实现什么函数的时候都要记得对头尾进行特殊讨论)
接下来就是判断是否为头部了,如果为头部就代表我们获取到的地址是非法的,也就是不在链表的范围内的下标地址,所以我们判断出来后,直接不进行操作就好了。
如果不是头部,就进行我们的四步法操作,双向链表的核心可以说就是这个四步法了。

void AddElem(DULink_p*L, int Locate, int AddElem)
{
	DULink_p pre, cur, newElem;
	cur = GetElem_2(L, Locate);
	if(cur->prior != NULL)//如果没找到到会返回头指针,头指针的前趋为NULL 
	{
		pre = GetElem_2(L, Locate-1);
		newElem = (DULink_p)malloc(sizeof(DULink));
		newElem->date = AddElem; 
		/*双链表交互四步法(务必要记住)*/
		pre->next = newElem;
		newElem->prior =  pre;
		newElem->next = cur;
		cur->prior = newElem;
	 }
} 

5:删除元素

删除元素也和单链表一样分为两个函数进行包装了。

a)删除某一位置上的元素

这里还是继续用到了我们包装的获取元素地址的操作。
删除元素其实只需要知道一个元素的地址就行了,不需要再用一次获取该元素的前趋,但是自己包装的函数不用白不用哇哈哈哈,但是会增加一点点的时间复杂度,减少一点点的空间复杂度,正所谓有得有失乃人生常事。

void DeleteElem_1(DULink_p*L, int Locate)
{
	DULink_p cur, pre;
	pre = GetElem_1(L, Locate-1); 
	cur = GetElem_1(L, Locate);
	if(cur->prior == NULL) return;//如果返回的是头指针那么就是没有找到,直接返回,不作删除操作(这步操作不能少!!!) 
	if(cur->next == NULL)//假如要删除的是最后一个 
	{
		pre->next = NULL;
		free(cur);
		return;
	} 
	else
	{
		pre->next = cur->next;//pre的后继要接上下一个的下一个空间的空间,因为cur就是pre的下一个,下一个的下一个就是cur的next,
		(cur->next)->prior = pre;//后面的前趋要接上pre
		free(cur);
	}
} 

b)删除某一特定元素 ,与之重复的也一并删除掉

第二个删除元素函数就比较绕了,因为这次没有传进下标,所以只能得到当前元素的下标,他的前趋也只能用该元素的prior来表示,所以会比较绕,只要看懂这部分基本上已经通过了。

void DeleteElem_2(DULink_p*L, int Elem)
{
	DULink_p cur, pre, temp;
	cur = GetElem_2(L, Elem);
	if(cur->prior == NULL) return;//表示删除的元素没有找到
	if(cur->next == NULL)//假如删除的元素是最后一个
	{
		(cur->prior)->next = NULL;//最后一个的前趋的next指向NULL 
		free(cur);//然后释放cur 
	}
	else
	{
		pre = cur->prior;//把前趋先存下来,方便下面使用 
		pre->next = cur->next;//cur的前一个的next指向cur的next(有点绕,不懂就画图)
		(cur->next)->prior = pre;
		free(cur);
	}
} 

6:查找元素,返回最先找到的元素下标

做到这里其实不难发现,双向链表和单链表其实就是一个东西,无非就是多了一个前趋需要我们存储下来。
这边我们直接放代码了,不继续赘述了。

void SearchElem(DULink_p L, int Elem, int *index)
{
	int count = 1;
	 L = L->next;//因为L的头是一个空的结构体,所以要先移动到下一个空间才开始计数 
	while(L != NULL)
	{
		if(L->date == Elem)
		{
			*index = count;
			return;//直接返回,不返回的话还会继续下面的操作,及时止损。 
		}
		L = L->next;
		count++;
	}
	if(L == NULL)//没找到就赋值为-1 
	{
		*index = -1;
	}
} 

7:修改元素

a)修改某一个位置上的元素

这里其实你也可以用单链表的操作也行,是一模一样的,没有差别。

void ChangeElem_1(DULink_p*L, int Locate, int newElem)
{
	DULink_p cur;
	int count = 1;
	cur = (*L)->next;
	if(Locate<1) return;//直接返回不进行修改 
	while(cur != NULL)
	{
		if(count == Locate)
		{
			cur->date = newElem; 
			return;//修改完成直接返回 
		}
		cur = cur->next;
		count++;
	}
	
} 

b)修改某一个元素,与之重复的也全部修改掉

void ChangeElem_2(DULink_p*L, int ChangeElem, int newElem)
{
	DULink_p cur;
	cur = (*L);
	while(cur != NULL)
	{
		if(cur->date == ChangeElem)
		{
			cur->date = newElem;//直接修改 
		}
		cur = cur->next; 
	}
}

8:打印链表内容

void PDULink(DULink_p L)
{
	L = L->next;//因为我们的头为空的空间,没有元素,相当于一个头,所以需要往后移动一步
	 
	while(L!=NULL)
	{
		PF(%d\t,L->date);
		L = L->next;
	}
	putchar('\n');
} 

9:销毁链表(释放链表申请的空间 )

void DestroyDULink(DULink_p*L)
{
	DULink_p temp;
	while((*L) != NULL)
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
} 

10:判断是否为空表

bool类型需要定义stdbool.h头文件

bool IsEmpty(DULink_p L)
{
	if(L == NULL) return false;
	else return true;
}

源代码

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define PF(format, ...) printf(#format, ##__VA_ARGS__)
/*
	双向链表
	1:初始化链表 
	a)头插法 
	b)尾插法
	2:获取链表某一个元素的地址(双向链表有这个功能就方便很多)
	3:增加某一个位置上的元素 
	4:a)删除某一个位置上的元素 
		b)删除某一个元素,与之重复的也全部删掉
	5:查找元素,返回最先找到的元素下标
	6:a)修改某一个位置上的元素 
		b)修改某一个元素,与之重复的也全部修改掉
	7:打印链表内容 
	8:销毁链表(释放链表申请的空间 
	9: 判断是否为空表 
*/

typedef struct _DULink{
	struct _DULink *prior; //前趋 
	int date;//元素数据 
	struct _DULink *next;//后继 
}DULink, *DULink_p;

//初始化双向链表
void InitDULink_toH(DULink_p*,int);//头插法 
void InitDULink_toT(DULink_p*,int);//尾插法 

//通过传进一个元素,在链表寻找这个元素地址,并返回这个元素的地址,若下标错误则直接返回头指针 
DULink_p GetElem_1(DULink_p*, int Elem);
//通过传进一个元素的下标位置,在链表寻找这个下标对应元素的地址,并返回这个元素的地址,若下标错误则直接返回头指针 
DULink_p GetElem_2(DULink_p*, int index);

void AddElem(DULink_p*, int Locate, int AddElem);//在某个位置上增加新元素 

void DeleteElem_1(DULink_p*, int Locate);//删除某一位置上的元素 
void DeleteElem_2(DULink_p*, int Elem);//删除某一特定元素 ,重复的也会一并删除掉 

void SearchElem(DULink_p, int Elem, int *index);//链表中最先找到那个元素,把他的下标的值给index,没找到就把index的值改为-1 

void ChangeElem_1(DULink_p*, int Locate, int newElem);//修改Locate位置上的元素为newElem 
void ChangeElem_2(DULink_p*, int ChangeElem, int newElem);//修改所有ChangeElem元素改为newElem 

void PDULink(DULink_p);//打印双向链表 

void DestroyDULink(DULink_p*);//销毁链表,释放空间 

bool IsEmpty(DULink_p L);//判断是否为空表 

int main(void)
{
	int index;
	DULink_p L;
	//InitDULink_toH(&L, 10);
	InitDULink_toT(&L,10);
	AddElem(&L, 10, 9);  
	//DeleteElem_2(&L, 99); 
	//DeleteElem_1(&L, 1);
	ChangeElem_1(&L, 11,9); 
	ChangeElem_2(&L,9,0); 
	PDULink(L);
	SearchElem(L, 0, &index);
	PF(*********(下标为:%d)\n, index);
	DestroyDULink(&L);
	if(IsEmpty(L)) PDULink(L);
	return 0;
 }
 
void InitDULink_toH(DULink_p*L, int len)//使用的头部是一个空的结构体,方便于双向链表的前趋后继的操作 
{
	int i; 
 	(*L) = (DULink_p)malloc(sizeof(DULink));//分配出一个空的空间,便于后面他的next指向非空的新的元素空间 
 	(*L)->next = NULL;//先指向NULL,表示尾部
	(*L)->prior = NULL;//前趋也赋值为NULL方便后面使用头指针 
 	DULink_p temp, cur;
 	for(i = 0; i < len; i++)
	{
	 	if((*L)->next == NULL)
	 	{
	 		cur = (DULink_p)malloc(sizeof(DULink));
	 		cur->date = i+1; 
	 		
		 	(*L)->next = cur;
			cur->prior = (*L);
			cur->next = NULL; 
		}else
		{
			cur = (DULink_p)malloc(sizeof(DULink));
			cur->date = i+1;
			temp = (*L)->next;//先把首元素的空间存起来,因为后面要把他和新的空间连接起来 
			/*双向链表的交互四步法,
			两个元素的前趋后继连接的时候这四步一步都不能少*/
			(*L)->next = cur; 
			cur->prior = (*L);
			cur->next = temp;//刚刚存下来的temp元素需要和新的进行交接 
			temp->prior = cur;//temp的前继也要和新的空间进行连接 
		}
		
	} 
	 
}

void InitDULink_toT(DULink_p*L,int len)//使用的头部是一个空的结构体,方便于双向链表的前趋后继的操作 
{
	int i;
	DULink_p temp, cur, tail;
	(*L) = (DULink_p)malloc(sizeof(DULink));
	(*L)->next = NULL;
	(*L)->prior = NULL;//前趋也赋值为NULL方便后面使用头指针 
	for(i = 0; i < len; i++)
	{
		if((*L)->next == NULL)
		{
			tail = (DULink_p)malloc(sizeof(DULink));
			tail->date = i+1;
			(*L)->next = tail;
			tail->prior = (*L);
			tail->next = NULL;
		}
		else
		{
			cur = (DULink_p)malloc(sizeof(DULink));
			cur->date = i+1;
			
			tail->next = cur;
			cur->prior = tail; 
			cur->next = NULL; 
			tail = cur;//为指针指向新的尾部 
		 } 
	}
	
} 

DULink_p GetElem_1(DULink_p*L, int index)
{
	DULink_p temp;
	int count = 1;
	if(index < 1)//如果小于1下标位置直接返回头指针地址 
	{
		return (*L);
	}  
	temp = (*L)->next;//时刻记住你的链表表头是一个空的结构体,所以需要以next来移动到下一个首元素空间开始
	while(temp != NULL)
	{
		if(count == index)
		{
			return temp; 
	    } 
		else 
		{
			temp = temp->next;
			count++;
		}
	}
	if(temp == NULL) 
	{
		return (*L);//因为没有找到或者下标错误的话直接返回头指针 
	} 
}

DULink_p GetElem_2(DULink_p*L, int Elem)
{
	DULink_p temp;
	temp = (*L)->next;//时刻记住你的链表表头是一个空的结构体,所以需要以next来移动到下一个首元素空间开始 
	while(temp != NULL)
	{
		if(temp->date == Elem)
		{
			return temp;//这个是找到那个元素的结构体地址,是真实的,返回后可以直接拿这个地址进行修改链表的内容 
		}
		temp = temp->next;
	 }
	 if(temp == NULL)
	 {
	 	return (*L);//因为没有找到或者下标错误的话直接返回头指针 
	 } 	 
} 

void AddElem(DULink_p*L, int Locate, int AddElem)
{
	DULink_p pre, cur, newElem;
	cur = GetElem_2(L, Locate);
	if(cur->prior != NULL)//如果没找到到会返回头指针,头指针的前趋为NULL 
	{
		pre = GetElem_2(L, Locate-1);
		newElem = (DULink_p)malloc(sizeof(DULink));
		newElem->date = AddElem; 
		/*双链表交互四步法(务必要记住)*/
		pre->next = newElem;
		newElem->prior =  pre;
		newElem->next = cur;
		cur->prior = newElem;
	 }
} 

void DeleteElem_1(DULink_p*L, int Locate)
{
	DULink_p cur, pre;
	pre = GetElem_1(L, Locate-1); 
	cur = GetElem_1(L, Locate);
	if(cur->prior == NULL) return;//如果返回的是头指针那么就是没有找到,直接返回,不作删除操作(这步操作不能少!!!) 
	if(cur->next == NULL)//假如要删除的是最后一个 
	{
		pre->next = NULL;
		free(cur);
		return;
	} 
	else
	{
		pre->next = cur->next;//pre的后继要接上下一个的下一个空间的空间,因为cur就是pre的下一个,下一个的下一个就是cur的next,
		(cur->next)->prior = pre;//后面的前趋要接上pre
		free(cur);
	}
} 

void DeleteElem_2(DULink_p*L, int Elem)
{
	DULink_p cur, pre, temp;
	cur = GetElem_2(L, Elem);
	if(cur->prior == NULL) return;//表示删除的元素没有找到
	if(cur->next == NULL)//假如删除的元素是最后一个
	{
		(cur->prior)->next = NULL;//最后一个的前趋的next指向NULL 
		free(cur);//然后释放cur 
	}
	else
	{
		pre = cur->prior;//把前趋先存下来,方便下面使用 
		pre->next = cur->next;//cur的前一个的next指向cur的next(有点绕,不懂就画图)
		(cur->next)->prior = pre;
		free(cur);
	}
} 

void SearchElem(DULink_p L, int Elem, int *index)
{
	int count = 1;
	 L = L->next;//因为L的头是一个空的结构体,所以要先移动到下一个空间才开始计数 
	while(L != NULL)
	{
		if(L->date == Elem)
		{
			*index = count;
			return;//直接返回,不返回的话还会继续下面的操作,及时止损。 
		}
		L = L->next;
		count++;
	}
	if(L == NULL)//没找到就赋值为-1 
	{
		*index = -1;
	}
} 

void ChangeElem_1(DULink_p*L, int Locate, int newElem)
{
	DULink_p cur;
	int count = 1;
	cur = (*L)->next;
	if(Locate<1) return;//直接返回不进行修改 
	while(cur != NULL)
	{
		if(count == Locate)
		{
			cur->date = newElem; 
			return;//修改完成直接返回 
		}
		cur = cur->next;
		count++;
	}
	
} 

void ChangeElem_2(DULink_p*L, int ChangeElem, int newElem)
{
	DULink_p cur;
	cur = (*L);
	while(cur != NULL)
	{
		if(cur->date == ChangeElem)
		{
			cur->date = newElem;//直接修改 
		}
		cur = cur->next; 
	}
}

void PDULink(DULink_p L)
{
	L = L->next;//因为我们的头为空的空间,没有元素,相当于一个头,所以需要往后移动一步
	 
	while(L!=NULL)
	{
		PF(%d\t,L->date);
		L = L->next;
	}
	putchar('\n');
} 

void DestroyDULink(DULink_p*L)
{
	DULink_p temp;
	while((*L) != NULL)
	{
		temp = (*L);
		(*L) = (*L)->next;
		free(temp);
	}
} 

bool IsEmpty(DULink_p L)
{
	if(L == NULL) return false;
	else return true;
}

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

竹等寒

谢过道友支持,在下就却之不恭了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值