请回答数据结构【双向链表】

请回答数据结构【双向链表】_第1张图片

请回答数据结构【双向链表】

    • 1. Intro
    • 2. 实现双向链表
      • 2.1 用结构体创建双向链表
      • 2.2 BuyLTNode
      • 2.3 ListInit
      • 2.4 ListPushBack
      • 2.5 ListPopBack
      • 2.6 ListPrint
      • 2.7 ListFind
      • 2.8 ListInsert
      • 2.9 ListPushFront
      • 2.10 ListErase
      • 2.11 ListDestroy
    • 3. 双向带头链表 V.S. 顺序表

1. Intro

回顾一下之前在单链表中提到的8种链表结构,其中可以互相结合

单向 双向
带头 不带头
循环 不循环

而我们这章主要展开的是双向链表

单链表 双向带头链表
不带头 带头
不循环 循环

请回答数据结构【双向链表】_第2张图片

图片来源于Crash Course Computer Science

双向带头链表,虽然结构复杂但是操作反而简单,具有结构优势

2. 实现双向链表

文件名 功能作用
DList.c 创建双向链表,单链表主要函数的实现
DList.h 声明用头文件,实现每个函数的声明,用来引用
test.c 测试运行文件,主函数在这里

2.1 用结构体创建双向链表

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

双向链表和单链表一个很大的区别就是有一个哨兵位节点,有人说哨兵位头节点可以用来存储数据,这个数据就是链表长度,但是实际上这是不好的,因为你要考虑数据的类型,因为所有的数据类型都是一样的,倘若你今天typedef了的是char类型、指针类型或者是其他自定义的类型,那么不是就存不了值了吗,所以当然不太好,是有局限性的,如果说了是存int类型或者是size_t类型等长整型那还好说

2.2 BuyLTNode

获得一个节点

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

2.3 ListInit

void ListInit(LTNode** pphead)
{
	assert(pphead);
	*pphead = BuyLTNode(0);
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;
}

或者是,为了统一一下,我们全部采取一级指针,甚至不传参,直接我们改成返回值的方法,就统一了不用二级指针

LTNode* ListInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

2.4 ListPushBack

双向链表的结构优势就是尾插的时候不再用考虑头节点了,而且效率极高

还有一个问题,为什么单链表要传二级指针,双向链表不用,这是因为双向链表中我们没有改变*phead,而是改变了哨兵位的结构体,那传结构体地址(指针)就可以了

而单链表是传结构体指针的指针 ,所以说倘若双向链表一开始不是头结点的话,那么也要传二级指针

void ListPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

	tail->next = newnode;
	newnode->prev = tail;

	newnode->next = phead;
	phead->prev = newnode;
}

当有了insert之后可以简化

	ListInsert(phead, x);

2.5 ListPopBack

尾删也很简单只要改变指向哨兵指向 就可以

void ListPopBack(LTNode* phead)
{
	assert(phead);//avoid vacancy
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;

	free(tail);
	tail = NULL;

	tailPrev->next = phead;
	phead->prev = tailPrev;

}

2.6 ListPrint

打印输出,还是用了cout修改了一下

void ListPrint(LTNode* phead)
{
	assert(phead);

	LTNode* cur = phead->next;//不应该是从头开始
	while (cur != phead)//现在遍历要走到phead结束,而不是空位置为止
	{
		cout<<cur->data<<"->"
	}
	printf("\n");
}

2.7 ListFind

实现查找功能

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

2.8 ListInsert

设计一个带头双向循环链表就非常好用,不用排除很多情况,加入要写的话,上来应该先写ListInsert和ListErase,然后头插尾插,头删尾删可以复用,直接就写好了

双向循环链表的插入

请回答数据结构【双向链表】_第3张图片

图片来源于Crash Course Computer Science

void ListInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	/*LTNode* newnode = BuyLTNode(x);
	pos->prev->next = newnode;
	newnode->prev = pos->prev;

	pos->prev = newnode;
	newnode->next = pos;*///代码前后顺序易出错

	LTNode* newnode = BuyLTNode(x);
	LTNode* posPrev = pos->prev;
	//代码前后顺序随便
	newnode->next = pos;
	pos->prev = newnode;
	posPrev->next = newnode;
	newnode->prev = posPrev;
}

注意这是在某位置前插

2.9 ListPushFront

直接复用

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	ListInsert(phead->next, x);
}

2.10 ListErase

void ListErase(LTNode* pos)
{
	assert(pos);
	LTNode* prev=pos->prev;
	LTNode* next = pos->next;

	delete pos;
	pos =nullptr;

	prev->next = next;
	next->prev = prev;
}

不要传入哨兵位,因为这相当于在自断手臂,相当于phead变成了野指针,后面就不能使用plist了

还有一点是,注意这里的pos置为nullptr实际上只是一份拷贝,没有对实参影响,这里我们如果想要对pos处理,其实可以在函数外去置空,不过即使pos不置空,其实也没问题。但是强行要置空pos只能传二级指针,这样反而显得冗余,只要不去访问就可以了

	LTNode* pos = ListFind(pList, 3);
	if (pos)
	{
		ListErase(pos);
		pos = NULL;
	}

2.11 ListDestroy

void ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		//ListErase(cur);
		free(cur);
		cur = next;
	}

	free(phead);
	//phead = NULL;
}

3. 双向带头链表 V.S. 顺序表

优缺点\表名 顺序表 链表(双向带头链表)
优点 1. 物理空间是连续的,方便按下标进行随机访问 1. 按需申请内存,需要存储一个数据,就申请一块内存,不会造成过多的空间浪费,按需释放空间
2. cpu高速缓存命中率比较高(局部性原理) 2.任意位置节点插入删除效率高O(1),
缺点 1. 空间不够要增容,扩容的本身有一定消耗,扩容机制也存在一定的资源浪费 1. 不支持下标的随机访问,有些算法不支持如排序,二分查找
2. 头部或者中间插入或者删除数据,需要挪动数据,效率低,O(N) 2. 由于本身来说空间是不连续存储的,所以访问数据时,在缓存,命中,不在缓存,不命中,造成低命中缓存污染
假设栗子 QQ发信息消息记录,每次信息记录时后发的显示在最下面,在数据存储中相当于实现一个头插,因此不可能用顺序表,太慢了,每次都要移动数据

请回答数据结构【双向链表】_第4张图片

那么cpu就会去找缓存,若是在缓存叫命中,直接访问,如果不在就叫不命中,明显连续的空间是会容易命中的,然而链表由于空间不连续,所以命中率低,甚至把不要的东西读入缓存,产生了缓存污染,所以还有这个点

两个数据结构,是相辅相成的,互弥补对方的缺点,需要用谁存数据,具体是看场景

双向链表的模拟实现代码可至我的gitee库中查看https://gitee.com/allen9012/c-language/tree/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A81)

注:部分图片来源于Crash Course Computer Science,如有侵权请联系我删除

你可能感兴趣的:(数据结构初阶,c语言,后端,经验分享,数据结构,链表)