双向链表----“双轨联动,高效运行” (第九讲)

一. 链表的分类

链表的结构非常多样,按照以下的组合起来就有8种:

(1)带头或者不带头

带头链表中的"头结点",不存储任何有效的数据,只是用来占位的,我们称之为"哨兵位"。

在前面的文章中,有时候表述"头结点",但实际上单链表中把第一个节点称为"头结点"这种说法是错误的。

(2)单向或者双向

(3)循环或者不循环

虽然这么多的链表结构,但是我们最常用的有两种:单链表(不带头单向不循环链表)和双链表(带头双向循环链表)。


二. 双向链表

2.1概念与结构

双向链表由一个一个的节点组成,这里的节点包括3个部分。

cpp 复制代码
struct ListNode{
   int data;
   struct  ListNode* prev;
   struct  ListNode* next;
};

注:当双向链表为空 时,这里表示双链表中只有一个哨兵位,且它的前驱指针和后继指针都指向自身。图示如下:

2.2 双向链表的初始化

cpp 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	LTNode* prev;
	LTNode* next;
}LTNode;

//1.初始化
void LTInit(LTNode** phead);
cpp 复制代码
#include "List.h"
void LTInit(LTNode** pphead)
{
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if (*pphead == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	(*pphead)->data = -1;//哨兵位节点不存储任何有效数据,-1为无效
	(*pphead)->next = (*pphead)->prev = NULL;
}

现在,我们已经有了一个空的双向链表,接下来,我们就要对其"增删改查"了。注意:在双向链表中,增删改查都不会改变哨兵位节点

2.3 尾插

cpp 复制代码
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	while (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->prev = newnode->next = newnode;
	newnode->data = x;
	return newnode;
}
cpp 复制代码
//2.尾插
LTNode* pushback(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;
	phead->prev = newnode;
} 

注:在尾插时,我们要特别注意几个指针的顺序,应该先处理newnode的前驱指针和后继指针,再处理原来双链表中尾节点的后继指针,使其指向newnode,最后处理头节点的前驱指针,使其指向最后一个节点(此时也就是newnode)。(此处一定要注意顺序,如果错乱,就有可能找不到原来的头节点)。

2.4 头插

头插时,节点是插在哨兵位的前面,还是插在哨兵位和下一个节点的中间?显然,答案是后者。因为哨兵位不存储有效数据,并不可以算作是双链表的第一个节点。

cpp 复制代码
//3.头插
LTNode* pushfront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;
	phead->next->prev = newnode;
	phead->next = newnode;
}

2.5 尾删

cpp 复制代码
//4.判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
		return phead->next == phead;
}
//5.尾删
void popback(LTNode* phead)
{
	if (!LTEmpty(phead))
	{
		LTNode* del = phead->prev;
		del->prev->next = phead;
		phead->prev = del->prev;
		free(del);
		del = NULL;
	}
}

2.6 打印双链表

cpp 复制代码
//6.打印双链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead;
	while (pcur != phead)
	{
		printf("%d-> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

2.7 头删

cpp 复制代码
//7.头删
LTNode* popfront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

2.8 查找

cpp 复制代码
//8.查找
LTNode* find(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

2.9 在指定位置之后插入数据

cpp 复制代码
//9.在pos位置之后插入数据
void LTInit(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;
	pos->next = newnode;
}

2.10 删除指定位置的节点

cpp 复制代码
//10.删除pos位置的节点
void erase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

2.11 销毁双链表

cpp 复制代码
//11.销毁双链表
void LTDestory(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(*pphead);
	*pphead = NULL;
}

三. 代码改进

由上述的代码可以看出,除了初始化和释放形参是二级指针,其余功能实现形参都是一级指针,那我们为了保持接口一致性,能不能初始化和释放这两个功能也弄成一级指针呢?具体方法如下:

3.1 初始化

cpp 复制代码
//1.初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

3.2 销毁

cpp 复制代码
void destory(LTNode* phead)
{
	LTNode* pcur = (phead)->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(phead);
	phead = NULL;
}

但是,这种写法需要自己手动将实参置为空。


四. 顺序表与链表分析

|--------------|----------------------|------------------------|
| 不同点 | 顺序表 | 链表(单链表) |
| 存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
| 随机访问 | 支持O(1) | 不支持: |
| 任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
| 插入 | 动态顺序表,空间不够时需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
| 应用场景 | 元素高效存储+频繁访问 | 任意位置高效插入和删除 |


以上就是今天的内容,到目前为止,顺序表和链表就告一段落啦~喜欢的朋友们可以一键三连哦~

相关推荐
_dindong28 分钟前
牛客101:二叉树
数据结构·c++·笔记·学习·算法
潼心1412o4 小时前
数据结构(长期更新)第5讲:单链表算法题
数据结构
小十一再加一8 小时前
【数据结构初阶】单链表
数据结构
ZIM学编程10 小时前
「学长有话说」作为一个大三学长,我想对大一计算机专业学生说这些!
java·c语言·数据结构·c++·python·学习·php
子枫秋月11 小时前
单链表实现全解析
c语言·数据结构·c++
刀法自然11 小时前
栈实现表达式求值
数据结构·算法·图论
Yupureki13 小时前
从零开始的C++学习生活 19:C++复习课(5.4w字全解析)
c语言·数据结构·c++·学习·1024程序员节
ゞ 正在缓冲99%…13 小时前
leetcode1312.让字符串成为回文串的最少插入次数
数据结构·算法·leetcode·动态规划·记忆化搜索
laocooon52385788614 小时前
寻找使a×b=c成立的最小进制数(2-16进制)
数据结构·算法
一匹电信狗18 小时前
【牛客CM11】链表分割
c语言·开发语言·数据结构·c++·算法·leetcode·stl