双向链表的总结

 

目录

一.基础知识

二.代码实现

1.头文件的设计

2.双链表的初始化

3.头插,尾插,按位置插

4.头删尾删按位置删

 5.查找元素,按值删

6.获取有效值个数,判空,清空,销毁,打印

三.测试


一.基础知识

        上一篇文章,讨论的链式存储结构中的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针往后寻查其他结点。若要查询结点的直接前驱,则需要从链表头指针出发。换句话说,在链表中,NextElem的执行时间O(1),而PriorElem的执行时间为O(n).为克服单链表这种单向性的缺点,可利用双向链表(double Linked list)

         双向链表和单链表作对比,每一个结点不仅仅只保存一个结点的地址,还需要一个指针域保存上一个结点的地址。双向链表存在的意义:每一个结点即可一找到直接后继,也可以找到直接前驱。每一个结点既可以向前走,也可以向后走。

二.代码实现

1.头文件的设计

#pragma once

//双向链表的结构体设计

typedef int ELEM_TYPE;

typedef struct DNode {
	ELEM_TYPE data;//数据域
	struct DNode* next;//直接后继指针
	struct DNode* prior;//直接前驱指针
}DNode,*PDNode;


//初始化
void Init_dlist(PDNode pdlist);

//头插
bool Insert_head(PDNode pdlist,ELEM_TYPE val);

//尾插
bool Insert_tail(PDNode pdlist, ELEM_TYPE val);

//按位置插
bool Insert_pos(PDNode pdlist,int pos, ELEM_TYPE val);

//头删
bool Del_head(PDNode pdlist);

//尾删
bool Del_tail(PDNode pdlist);

//按位置删
bool Del_pos(PDNode pdlist,int pos);

//按值删
bool Del_val(PDNode pdlist, ELEM_TYPE val);

//查找
PDNode Search(PDNode pdlist,ELEM_TYPE val);

//判空
bool IsEmpty(PDNode pdlist);

//清空
void Clear(PDNode pdlidt);

//销毁
void Destroy(PDNode pdlist);

//打印
void Show(PDNode pdlist);
//获取有效值长度
int Getlength(PDNode pdlist);

2.双链表的初始化

如果没有有效结点,则双向链表的头结点的指针域均指向NULL。即:头结点的数据域浪费掉不使用;头结点的next域,赋值为NULL;头结点的prior域,赋值为NULL。(初始化状态下,头结点后面和前面都不存在有效结点,所以next域和prior域都默认赋值为NULL)

//初始化
void Init_dlist(PDNode pdlist) {

	//pdlist->data  头结点的数据域不使用
	pdlist->next = NULL;
	pdlist->prior = NULL;
}

3.头插,尾插,按位置插

1)头插

 实现头插的步骤:

1.安全性处理,进行断言防止链表为NULL;

2.购买新结点,购买一个新的待插入结点pnewnode;

3.找到合适插入位置,头插位置可以不用找,为头结点的直接后继;

4.将待插入结点插入即可。

难点:第四步将待插入结点插入

 

分析:

       插入一个结点一共会有四个指针域收到影响需要改值。分别为:待插入结点pnewnode的两个指针域,以及插入位置的上一个结点的next域和插入位置的下一个结点的prior域。

      现在给这四个指针域标号,带插入结点pnewnode的next域记为1,待插入结点pnewnode的prior域记为2,插入位置的上一个结点的next域记为3,插入位置的下一个结点的prior域记为4。

问题:应该先修改先修改哪一个指针域?可不可以先修改3号指针域,为什么?

不可以先修改3号指针域,如果先修改了3号指针域,则修改结点之后的结点全部丢失。    

可以选择按照1,2,4,3的顺序进行修改,即先修改待插入结点pnewnode的两个指针域,再修改插入位置的下一个结点的prior域,最后修改待插入位置的上一个结点的next域。(当然这个方法不是唯一的,掌握方法之后可以融会贯通,灵活调整)

还需注意,如果链表中只存在头结点时,4号指针域不存在。即需要分情况处理,链表中存在至少两个结点时需要修改4个指针域;链表中只存在头结点时,只需要修改3个指针域

代码如下:

//头插
bool Insert_head(struct DNode *pdlist, ELEM_TYPE val)
{
	//0.安全性处理
	assert(pdlist != NULL);

	//1.购买新节点
	struct DNode *pnewnode = (struct DNode *)malloc(1 * sizeof(struct DNode));
	assert(pnewnode != NULL);
	pnewnode->data = val;

	//2.找到合适的插入位置(其实就是找到插入在哪一个节点后边,用指针p指向)
	//因为是头插,所以直接使用pdlist即可

	//3.插入   我们的规则是:1,2,4,3
	//         先处理自身的两个指针域(1,2)
	//         再处理插入位置的下一个节点的prior域(4),但是4有特例(空链表进行头插),不存在
	//         最后处理插入位置的上一个节点的next域(3)

	pnewnode->next = pdlist->next;//1
	pnewnode->prior = pdlist;//2
	if(pdlist->next != NULL)//说明不是空链表,则不是特例,4存在
	{
		//此时,插入位置的下一个节点可以通过pdlist->next访问到,还可以通过pnewnode->next访问到
		pdlist->next->prior = pnewnode;//4       
		//pnewnode->next->prior = pnewnode;//4
	}
	pdlist->next = pnewnode;

	return NULL;
}

2)尾插

 需要注意的点是:每一种情况都是只需要修改三个指针域。插入位置的下一个结点的prior域不存在。其次,需要将链表遍历一遍找到尾插的位置。

 

//尾插
bool Insert_tail(struct DNode *pdlist, ELEM_TYPE val)
{
	//0:
	assert(pdlist != NULL);
	
	//1.购买新节点
	struct DNode *pnewnode = (struct DNode *)malloc(1 * sizeof(struct DNode));
	assert(pnewnode != NULL);
	pnewnode->data = val;

	//2.找到合适的插入位置,用指针p指向插入位置的前一个节点
	//判断是否使用带前驱的for循环
	struct DNode *p = pdlist;
	for(; p->next!=NULL; p=p->next);

	//3.插入(不存在特殊情况,每一种情况都需要修改三个指针域)
	//按照之前编号顺序,修改的这三个指针域分别是1,2,(4不存在),3
	pnewnode->next = p->next;//pnewnode->next = NULL;//1
	pnewnode->prior = p;//2
	//4不存在
	p->next = pnewnode;//3
	
	return true;
}

3)按位置插

       与实现头插不同的点是:寻找插入位置。默认pos=0为头删,当pos==length时,非法。定义两个指针p和q。当pos==“几”时,先指针q,让指针q从头结点开始向后走“几”步,然后再通过指针q找指针p。

 

      还可以把头插和尾插分情况写出来,其余的插入均需要改变4个指针域。因为头插和尾插前面代码已经实现,在此处可以直接调用函数。

代码如下:

//按位置插
bool Insert_pos(struct DNode *pdlist, int pos, ELEM_TYPE val)
{
	//因为,在写这个函数之前,头插和尾插已经实现,所以这里可以直接调用
	//0.安全性处理
	assert(pdlist != NULL);
	assert(pos>=0 && pos<=Get_length(pdlist));

	//1.分类处理,将头插和尾插的情况,分别调用对应的函数处理
	if(pos == 0)//头插
	{
		return Insert_head(pdlist, val);
	}
	if(pos == Get_length(pdlist))//尾插
	{
		return Insert_tail(pdlist, val);
	}
	//如果既不是头插,也不是尾插,则只有可能是中间插入,1,2,4,3,都存在

	//2.剩下来的都是中间位置插入,都正常情况,修改4个指针域
	//2.1 购买新节点
	struct DNode *pnewnode = (struct DNode *)malloc(1 * sizeof(struct DNode));
	assert(pnewnode != NULL);
	pnewnode->data = val;

	//2.2 找到合适的插入位置,指针p(pos=="几",指针p从头结点出发向后走pos步)
	struct DNode *p = pdlist;
	for(int i=0; i<pos; i++)
	{
		p = p->next;
	}

	//2,3 正常插入,1,2,4,3 都存在,4不需要去判断
	pnewnode->next = p->next;//1
	pnewnode->prior = p;//2
	p->next->prior = pnewnode;//4 pnewnode->next->prior = pnownode; 
	p->next = pnewnode;//3

	return true;
}

4.头删尾删按位置删

1)头删

实现头删的步骤:

1.安全性处理,判空(防止链表为空链表)

2.申请两个指针p和q,用指针p指向待删除结点

3.用指针q指向待删除结点的上一个结点

4.跨越指向+释放p指针

 

       正常情况下,需要修改两个指针域:待删除结点的上一个结点的next域以及待删除结点的下一个结点的prior域。

 

      存在特例:头删时,删除的是仅剩下的唯一一个结点。这时只需要修改一个指针域:待删除结点的上一个结点的next域。

代码实现:

//头删   //这里写的也要注意,也存在特例
bool Del_head(struct DNode *pdlist)
{
	//0.安全性处理
	assert(pdlist != NULL);
	if(IsEmpty(pdlist))
	{
		return false;
	}

	//1.用指针p指向待删除节点
	struct DNode *p = pdlist->next;

	//2.用指针q指向待删除节点的上一个节点
	//因为是头删,所以这里指针q用指针pdlist代替

	//3.跨越指向(存在特例,正常情况下需要修改两个指针域,而特例时,只需要修改一个指针域)
	pdlist->next = p->next;
	if(p->next != NULL)//先得判断待删除节点的下一个节点是否存在
	{
		p->next->prior = pdlist;
	}

	//4.释放
	free(p);

	return true;
}

2)尾删

 

尾删不存在特例,因为待删除结点时尾结点,所以只有待删除结点的前一个结点存在,而待删除结点的下一个结点永远不存在。

总结:永远只需要处理一个指针域(待删除结点前的一个结点的next域)

代码实现:

//尾删
bool Del_tail(struct DNode *pdlist)
{
	//0.
	assert(pdlist != NULL);
	if(IsEmpty(pdlist))
	{
		return false;
	}

	//1.用指针p指向待删除节点
	struct DNode *p = pdlist;
	for(; p->next!=NULL; p=p->next);

	//2.用指针q指向待删除节点的上一个节点
	struct DNode *q = pdlist;
	for(; q->next!=p; q=q->next);


	//3.跨越指向(不存在特例,永远只需要去修改待删除节点的前一个节点的next域)
	q->next = p->next;//q->next = NULL;

	//4.释放
	free(p);

	return true;
}

3)按位置删

       与头删不同的是,需要寻找到待删除结点的位置,for循环遍历寻找即可。因为删除元素位置不确定,所以需要更改几个指针域也不确定。可以选择将头删和尾删情况单独分类出来写。前面已经写过头删和尾删函数,直接调用即可。中间位置删除的均需要改变两个指针域,不需要单独分析。

代码实现:

//按位置删
bool Del_pos(struct DNode *pdlist, int pos)
{
	//0.安全性处理
	assert(pdlist != NULL);
	assert(pos >=0 && pos<Get_length(pdlist));
	if(IsEmpty(pdlist))
	{
		return false;
	}

	//1.分类处理,将头删和尾删的情况,分别调用对应的函数处理
	if(pos == 0)//头删
	{
		return Del_head(pdlist);
	}
	if(pos == Get_length(pdlist)-1)//尾删
	{
		return Del_tail(pdlist);
	}
	//如果既不是头删,也不是尾删,则只有可能是中间删除,则统一需要修改两个指针域


	//2.剩下来的都是中间位置删除,统一需要修改两个指针域
	//2.1 找到q,让q从头结点开始向后走pos步
	struct DNode *q = pdlist;
	for(int i=0; i<pos; i++)
	{
		q = q->next;
	}

	//2.2 找到p,p=q->next
	struct DNode *p = q->next;

	//2.3 跨越指向+释放
	q->next = p->next;
	p->next->prior = q;

	free(p);

	return true;

}

 5.查找元素,按值删

1)查找元素

遍历链表,查找元素。找到之后返回该结点所在地址

//查找 //查找到,返回的是查找到的这个节点的地址
struct DNode *Search(struct DNode *pdlist, ELEM_TYPE val)
{
	//0.安全性处理
assert(pdlist!=NULL);
	//1.判断使用哪种for循环
	//使用不需要前驱的for循环

	struct DNode *p = pdlist->next;
	for(; p!=NULL; p=p->next)
	{
		if(p->data == val)
		{
			return p;
		}
	}

	return NULL;
}

2)按值删

先进行元素查找,再进行按位置删

//按值删
bool Del_val(struct DNode *pdlist, ELEM_TYPE val)
{
	//0.安全性处理

	//1.用指针p指向待删除节点,用search函数
	struct DNode *p = Search(pdlist, val);
	if(p == NULL)
	{
		return false;
	}
	//此时,代码执行到这里,可以保证待删除节点存在,且现在用指针p指向

	//2.用指针q指向待删除节点的上一个节点
	struct DNode *q = pdlist;
	for( ; q->next!=p; q=q->next);

	//3.跨越指向(有可能存在特例,例如如果待删除节点是尾结点,则只需要处理一个指针域,反之都是两个)
	if(p->next == NULL)//判断待删除节点是否是尾结点
	{
		q->next = NULL;//q->next = p->next;
	}
	else//如果待删除节点不是尾结点
	{
		q->next = p->next;
		p->next->prior = q;
	}

	//4.释放
	free(p);

	return true;
}

6.获取有效值个数,判空,清空,销毁,打印

1)获取有效值个数

//获取有效值个数
int Get_length(struct DNode *pdlist)
{
	//assert
	//使用不需要前驱的for循环
	int count = 0;

	struct DNode *p = pdlist->next;
	for(; p!=NULL; p=p->next)
	{
		count++;
	}

	return count;
}

2)判空

//判空
bool IsEmpty(struct DNode *pdlist)
{
	return pdlist->next == NULL;
}

3)清空

调用销毁函数

//清空
void Clear(struct DNode *pdlist)
{
	Destroy1(pdlist);
}

4)销毁

销毁1:无限头删

//销毁1 无限头删
void Destroy1(struct DNode *pdlist)
{
	//assert
	while(!IsEmpty(pdlist))
	{
		Del_head(pdlist);
	}

}

销毁2:不借助头结点,有两个辅助指针

//销毁2 不借助头结点,有两个辅助指针
void Destroy2(struct DNode *pdlist)
{
	assert(pdlist != NULL);
	struct DNode *p = pdlist->next;
	struct DNode *q = NULL;

	pdlist->next = NULL;

	while(p != NULL)
	{
		q = p->next;
		free(p);
		p = q;
	}
}

5)打印

//打印
void Show(struct DNode *pdlist)
{
	//安全性处理
assert(pdlist!=NULL);
	//使用不需要前驱的for循环

	struct DNode *p = pdlist->next;
	for(; p!=NULL; p=p->next)
	{
		printf("%d ", p->data);
	}
	printf("\n");

}

三.测试

代码测试如下:

int main() {

	struct DNode head;
	Init_dlist(&head);
	for (int i = 0;i<10;i++) {
		Insert_pos(&head,i,i+1);
	}
	Show(&head);
	Insert_head(&head,100);
	Insert_tail(&head,200);
	Insert_pos(&head, 3, 300);
	Show(&head);
	Del_head(&head);
	Del_tail(&head);
	Del_pos(&head,4);
	Del_val(&head, 7);
	Show(&head);
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值