提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、链表的分类
- 1、 单向或者双向链表
- 2、带头结点或者不带头结点的链表
- 3、循环或者非循环链表
- 二、带头双向循环链表
- 1、链表结点的结构体定义
- 2、链表的初始化
- 3、链表的头插和尾插
- 3、链表的头删和尾删
- 4、链表指定位置结点的插入和删除
- 5、链表的头插尾插头删尾删使用ListInsert和ListErase实现
- 总结
前言
我们知道当要删除一个单链表中目标结点的上一个结点时,需要先遍历单链表,然后找到该目标结点,并且标记目标结点的上一个结点,这样才能实现删除目标结点的上一个结点的操作。而还有一种双向链表,该链表的结点不仅有next指针域存储下一个结点的地址,还有一个prev指针域用来存储上一个结点的地址,这样双向链表删除目标结点的上一个结点就不要再遍历链表了,这只是双向链表的一个特点,双向链表还有很多实用的地方。
一、链表的分类
1、 单向或者双向链表
2、带头结点或者不带头结点的链表
3、循环或者非循环链表
上述这些链表经过组合共有8中链表结构
虽然有很多种链表结构,但是我们最常用的还是两种结构:
不带头单向非循环链表 即为前面我们介绍的单链表。
不带头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多,在刷题中关于链表的题,都是使用单链表来考核。
带头双向循环链表即为我们这次介绍的链表结构
带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现增删操作时反而简单了。
二、带头双向循环链表
1、链表结点的结构体定义
下面代码就为带头双向循环链表的结点的结构体定义,可以看到相比于单链表,双向链表不仅定义了next指针域用来存储下一个结点的地址,还定义了prev指针域用来存储上一个结点的地址。
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
2、链表的初始化
带头双向循环链表是带头结点的,所以链表在初始化时就要创建一个头结点。单链表我们使用了二级指针,因为当单链表为空时,此时指向结构体的指针指向NULL,要改变指向结构体的指针,我们就需要得到指向结构体的指针的地址,所以使用了二级指针。但是双向链表的操作我们只改变结构体里面的next指针域和prev指针域,并不会改变指向结构体的指针,所以只需用一级指针即可。
带头双向循环链表的初始化就是创建一个头结点。即代码如下。
LTNode* CreateNode(LTDataType x)
{
//创建一个新结点
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL)
{
perror("CreateNode_malloc_fail");
exit(-1);
}
//将结点的数据域改为指定值x,
newNode->data = x;
newNode->next = NULL;
newNode->prev = NULL;
//将该结点返回
return newNode;
}
LTNode* ListInit()
{
//带头双向循环链表的初始化就是创建一个头结点
//该结点的数据域不需要存有用数据
LTNode* phead = CreateNode(-1);
//当链表只有一个头结点时,此时phead的next还是phead
//phead的prev是phead,这样设置为了使后面的插入删除的代码也适用于只有一个头结点的链表。
phead->next = phead;
phead->prev = phead;
//将头结点返回
return phead;
}
3、链表的头插和尾插
链表的头插和尾插。
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//创建一个结点
LTNode* newNode = CreateNode(x);
//记录原来链表的尾结点
LTNode* tailPrev = phead->prev;
//将新结点插入到原来的尾结点之后。
tailPrev->next = newNode;
newNode->prev = tailPrev;
phead->prev = newNode;
newNode->next = phead;
/*
//还可以不记录原来的尾结点,不过这种操作需要注意前后顺序,不然会发生地址丢失的现象
//创建一个结点
LTNode* newNode = CreateNode(x);
//先将尾结点的指针改变
phead->prev->next = newNode;
newNode->prev = phead->prev;
//上面两语句和下面两语句的顺序不能变
//再将newNode的指针改变
phead->prev = newNode;
newNode->next = phead;
*/
}
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//创建一个新结点
LTNode* newNode = CreateNode(x);
//记录头结点后面的结点
LTNode* headNext = phead->next;
//将新结点插入在头结点之后。
newNode->next = headNext;
headNext->prev = newNode;
phead->next = newNode;
newNode->prev = phead;
/*
//还可以使用这种不创建结点的方法,不过要注意顺序
//创建一个新结点
LTNode* newNode = CreateNode(x);
phead->next->prev = newNode;
newNode->next = phead->next;
//语句执行顺序不能变
phead->next = newNode;
newNode->prev = phead;
*/
}
当链表中插入结点后,我们可以打印结点的值。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* curr = phead->next;
while (curr != phead)
{
printf("%d ", curr->data);
curr = curr->next;
}
printf("\n");
}
3、链表的头删和尾删
在删除链表中的结点时,要先判定链表是否为空,如果链表为空,则不可以删除。
判断链表是否为空。
bool ListEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
头删和尾删。
void ListPopBack(LTNode* phead)
{
assert(phead);
//如果链表为空,则会断言
//assert(!ListEmpty(phead));
//也可以这样判断
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
}
void ListPopFront(LTNode* phead)
{
assert(phead);
//如果链表为空,则会断言
//assert(!ListEmpty(phead));
//也可以这样判断
assert(phead->next != phead);
LTNode* headNext = phead->next;
LTNode* headNextNext = headNext->next;
phead->next = headNextNext;
headNextNext->prev = phead;
free(headNext);
}
4、链表指定位置结点的插入和删除
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newNode = CreateNode(x);
LTNode* posPrev = pos->prev;
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
}
5、链表的头插尾插头删尾删使用ListInsert和ListErase实现
当链表的ListInsert和ListErase函数实现后,则上述实现的头插尾插头删尾删就可以使用这两个函数来实现。
尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//在phead结点之前插入新结点就相当于尾插。
ListInsert(phead, x);
}
头插
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
//在phead的next结点之前插入一个结点,就相当于头插法
ListInsert(phead->next, x);
}
尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
//如果链表为空,则会断言
//assert(!ListEmpty(phead));
//也可以这样判断
assert(phead->next != phead);
//删除phead的上一个结点,就相当于删除尾结点
ListErase(phead->prev);
}
头删
void ListPopFront(LTNode* phead)
{
assert(phead);
//如果链表为空,则会断言
//assert(!ListEmpty(phead));
//也可以这样判断
assert(phead->next != phead);
//删除phead的下一个结点就相当于头删
ListErase(phead->next);
}
总结
当我们写出来指定位置插入结点和删除结点的ListInsert和ListErase方法后,头插尾插头删尾删都可以用这两个方法实现,所以当我们需要快速写出来一个双向链表的操作时,可以直接写出来这两个函数,这两个函数实现了,其他的操作都可以调用这两个函数来实现。