数据结构之双链表
Posted 芒果再努力
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之双链表相关的知识,希望对你有一定的参考价值。
通过上几篇文章,我们已经了解了什么是顺序表和单链表,具体可以点进去我的主页看!包括静态顺序表和动态顺序表,单链表。下面我们来实现一种新的链表---带头双向循环链表!
目录
一.什么是带头双向链表
节点共有3个空间,一个位置存数据,一个位置存指向前一个节点的指针,一个存指向下一个节点的指针。
注意:头结点我们不存数据,如果数据类型是char或者double之类的类型,不能存链表的长度。
二.为什么要使用双向链表
我们在使用单链表时,经常要找到前一个节点。如果要找到尾节点,要遍历链表才能找到。
单链表的尾插尾删时间复杂度都是O(N) ,若我们使用的是双向链表,头结点中指向上一个节点的指针指向的就是尾结点,进行尾插尾删很简单。
三.双向链表的特点
双向循环链表的结构是在双向链表的基础上使头节点的前驱指针指向末尾的节点,而使末尾的节点的一个指针指向开始节点,形成一个循环结构。
四:创建项目
工程文件 | 存放的内容 |
---|---|
List.h | 函数声明,宏定义 |
List.c | 实现顺序表函数接口的定义 |
test.c | 测试函数接口是否能实现功能 |
五:接口实现
1.创建结构体类型
为了后续我们想更改数据类型时方便,我们一般对类型进行typedef操作。
typedef int ListType;
typedef struct List
{
ListType data;//存放数据
struct List* next;//指向下一个节点
struct List* prev;//指向上一个节点
}List;
2. 带头节点开辟空间
若我们的头结点选择的是结构体,就需要对结构体初始化。若我们选择的头结点是以指针方式,就要开辟一个结构体空间,该指针指向该空间,充当头结点。
//初始化哨兵位
//哨兵位为结构体:
void ListInit1(List* phead)
{
//最初prev和next都指向自己
phead->data = 0;
phead->next = phead;
phead->prev = phead;
}
//哨兵位为指针->malloc一个结构体
List* ListInit2()
{
List* newnode = (List*)malloc(sizeof(List));
if (newnode == NULL)
{
printf("malloc is fail\\n");
return NULL;
}
else
{
//最初prev和next都指向自己
newnode->data = 0;
newnode->prev = newnode;
newnode->next = newnode;
return newnode;
}
}
注意:我们最开始初始化头结点时前驱指针和后继指针都指向自己,(后续就能看出有什么好处)
3.打印数据
遍历链表打印数据即可。当指针指向的是头结点时跳出循环。
注意:phead是头结点(哨兵位),phead->next才是第一个结点。
//打印
void ListPrint(List* phead)
{
List* cur = phead->next;//第一个节点
//最终回到phead
while (cur != phead)
{
printf("%d ->", cur->data);
cur = cur->next;
}
printf("NULL\\n");
}
4.创建节点
使用malloc动态开辟一个空间,要判断空间是否开辟成功!新节点的前驱指针和后继指针置空
//创建新节点
List *BuyNewnode(ListType x)
{
List* newnode = (List*)malloc(sizeof(List));
if (newnode == NULL)
{
printf("malloc is fail\\n");
return NULL;
}
else
{
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
}
5.尾插数据
步骤:
1.动态开辟一个新节点newnode
2.原来的尾节点的next链接newnode
3.newnode的前驱指针指向原来的尾节点,newnode的后继指针指向头结点phead
4.头结点的前驱指针指向newnode
注:phead->prev就是尾节点
//尾插
void ListPushBack(List* phead, ListType x)
{
assert(phead);
List* newnode = BuyNewnode(x);
List* tail = phead->prev; //phead->prev即为尾节点
tail->next = newnode; //尾节点的next链接新节点
newnode->prev = tail; //新节点的前指针链接原来的尾节点
newnode->next = phead; //新节点的后指针链接哨兵位
phead->prev = newnode; // 哨兵位的前指针指向新节点
}
6.头插数据
步骤:
1.动态开辟新节点newnode
2.保存第一个节点first(phead->next就是第一个节点)
3. phead newnode first 三者之间进行链接
//头插
void ListPushFront(List* phead, ListType x)
{
assert(phead);
List* newnode = BuyNewnode(x);
List* first = phead->next; //第一个节点
//phead newnode first进行链接
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
7.尾删数据
步骤:
1.注意先判断链表是否为空,如果链表为空就不删除了
2.保存倒数第二个节点 ,释放尾节点空间。
3.倒数第二个节点和头节点进行链接
倒数第二个节点的next指向头节点,头节点的prev指向倒数第二个节点。
//尾删
void ListPopBack(List* phead)
{
//若链表为空,按下面写法,就把哨兵位free掉了,所以判断一下
if (phead->prev == phead)
{
printf("链表为空\\n");
return;
}
else
{
//释放尾节点 +保存倒数第二个节点
List* tail = phead->prev;//尾节点
List* prev = tail->prev;//倒数第二个节点
free(tail);
prev->next = phead;;
phead->prev = prev;
}
}
8.头删数据
步骤:
1.注意先判断链表是否为空,如果链表为空就不删除了
2.保存第二个节点,释放第一个节点
3.第二个节点和头节点进行链接
第二个节点的prev指向头节点,头节点的next指向第二个节点
//头删
void ListPopFront(List* phead)
{
//若链表为空,按下面写法,就把哨兵位free掉了,所以判断一下
if (phead->prev == phead)
{
printf("链表为空\\n");
return;
}
else
{
//找到第二个节点
List* first = phead->next;
List* second = first->next;
free(first);
phead->next = second;
second->prev = phead;
}
}
9.查找数据所在位置
遍历查找,找到了返回所在的位置。找不到返回NULL。
//查找元素,返回对应的地址
List* ListFind(List* phead, int x)
{
//遍历
List* cur = phead->next;
while (cur != phead)
//我们定义时 cur指向的是第一个节点
//不可写成cur->next !=phead,若只有一个节点,且数据在第一个节点就err了
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
//找不到
return NULL;
}
10.在pos位置之前插入数据
步骤:
1.动态开辟一个节点
2.找到pos位置前一个节点 prev
3.prev newnode pos进行链接
//在某位置前插入数据
void ListInsert(List* pos, ListType x)
{
List* newnode = BuyNewnode(x);
//pos前一个位置
List* prev = pos->prev;
//prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
11.删除pos位置节点
步骤:
1.保存pos前的节点 和 pos后的节点
2.释放pos指向的节点
3.pos前的节点和pos后的节点进行链接
//删除某个节点
void ListErase(List* pos)
{
List* prev = pos->prev;//pos前的节点
List* next = pos->next;//pos后的节点
free(pos);
prev->next = next;
next->prev = prev;
}
12.判断链表是否为空
我们最开始定义头节点的next是指向自己的,如果现在还是指向自己,说明链表为空
//链表是否为空
bool ListEmpty(List* phead)
{
return phead->next == phead;
}
13.计算链表长度
使用计数器遍历计数
//计算链表长度
int ListLong(List* phead)
{
//遍历+计数器
List* cur = phead->next;
int count = 0;
while (cur != phead)
{
count++;
cur = cur->next;
}
return count;
}
14.销毁链表
遍历释放空间,要先保存下一个节点再释放。不能反过来,否则就找不到下一个节点。
//销毁
//我们定义的哨兵位是结构体,,不是结构体指针 所以不用置空也不用free
//free:动态开辟的内容
void ListDestory(List* phead)
{
//遍历销毁,
List* cur = phead->next;
while (cur != phead)
{
List* next = cur->next;
free(cur);
cur = next;
}
}
六.精华总结:
1.phead是头节点并不算进链表内容,当插入元素后,phead->next指向的才是第一个节点。
2.双向链表结构复杂但是实现起来很简单。因为双向链表有两个指针,一个指向前一个结点,一个指向后一个节点。相比于单链表,很容易就找到它的前一个节点。
附:源码链接
如何快速写完双向链表:
只写上面的Erase和Insert代码
//在某位置前插入数据
void ListInsert(List* pos, ListType x)
{
List* newnode = BuyNewnode(x);
//pos前一个位置
List* prev = pos->prev;
//prev newnode pos
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
//删除某个节点
void ListErase(List* pos)
{
List* prev = pos->prev;//pos前的节点
List* next = pos->next;//pos后的节点
free(pos);
prev->next = next;
next->prev = prev;
}
//尾插
void ListPushBack(List* phead, ListType x)
{
assert(phead);
//可以认为在phead前面插入
ListInsert(phead, x);
}
//头插
void ListPushFront(List* phead, ListType x)
{
assert(phead);
//在第一个节点后面插入
ListInsert(phead->next, x);
}
//尾删
void ListPopBack(List* phead)
{
assert(phead);
//防止链表尾空
if (phead->next == phead)
{
return;
}
//删的是phead->prev
ListDestory(phead->prev);
}
//头删
void ListPopFront(List* phead)
{
assert(phead);
//防止链表尾空
if (phead->next == phead)
{
return;
}
ListDestory(phead->next);
}
以上是关于数据结构之双链表的主要内容,如果未能解决你的问题,请参考以下文章