目录
认识链表
链表的分类
链表的实现
单链表的增删查改
增操作
删操作
查操作
改操作
带头双向循环链表
认识链表
链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构。链表由一系列节点(结点)组成,节点在运行时动态生成(malloc)
每个节点包括两个部分:存储数据元素的数据域和存储下一个节点地址的指针域。一般定义为如下结构:
struct List
{
DataType data; //这里的DataType在不同的应用场景下是不同的类型
struct List* next;
};
例如下放就是一个带头(哨兵位)单链表的抽象图。
链表的分类
实际中链表的的使用非常灵活,总体来看共有8种,大致可以从三个角度来区分
角度1、单向或者双向
单向链表指针域只有一个next,而双向链表的指针域还有一个pre用于指向前驱节点。
角度2、不带头或带头
一般带头链表的头节点不是用来存储数据的,而是用来索引链表内容的,类似于一个哨兵,所以也叫哨兵位。
角度3、循环或不循环
常规的链表最后的尾节点的next指向空,而循环链表的尾节点的next指向链表的头节点
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。但这种结构在笔试面试中经常出现
2. 带头双向循环链表:结构复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
链表的实现
单链表的增删查改
首先我们来看不带头的单链表,其抽象图如下。
学习一个数据结构首先要掌握其增删查改,接下来我们就来看上述单链表怎么实现增删查改。
增操作
- 增操作 - 尾插
由于链表不能随机访问元素,所在尾插时,需要先找到最后一个结点的位置。那么就将分成两种情况讨论:如果链表为空,则直接插入即可;如果链表不为空,则需要找到尾结点再插入。
void SListPushBack(SListNode** pplist, const SLTDateType x)
{
assert(pplist);
SListNode* node = BuySListNode(x);
assert(node != NULL);
if (*pplist == NULL)
*pplist = node;
else
{
SListNode* plist = *pplist;
while (plist->next)
plist = plist->next;
plist->next = node;
}
}
- 增操作 - 头插
头插相对来说比较简单,不需要考虑链表是否为空,直接将新结点的next指向为原来的头结点,然后将头结点改成新结点即可。
void SListPushFront(SListNode** pplist, const SLTDateType x)
{
assert(pplist);
SListNode* node = BuySListNode(x);
assert(node != NULL);
SListNode* next = *pplist;
node->next = next;
*pplist = node;
}
- 增操作 - 在指定位置后面插入
void SListInsertAfter(SListNode* pos, const SLTDateType x)
{
SListNode* node = BuySListNode(x);
assert(node != NULL);
SListNode* next = pos->next;
node->next = next;
pos->next = node;
}
思考 - 为什么不在pos位置之前插入呢?
因为单链表的节点很难直接访问到其前驱节点,但访问其后继节点却很方便
删操作
- 删操作 - 尾删
尾删同尾插一样,也需要先找到尾结点。所以这里需要考虑三种情况:1.链表为空的情况。2.链表中只有一个结点的情况。3.链表中有多个结点的情况。
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist);
assert((*pplist) != NULL);
if ((*pplist)->next == NULL)
{
free(*pplist);
*pplist = NULL;
}
else
{
SListNode* plist = *pplist;
while (plist->next->next)
plist = plist->next;
free(plist->next);
plist->next = NULL;
}
}
- 删操作 - 头删
头删相对比较简单,相当于将头指针移动到第二个结点上。也是分为链表为空和链表不为空两种情况的。
// 单链表头删
void SListPopFront(SListNode** pplist)
{
assert(pplist);
assert((*pplist) != NULL);
SListNode* head = *pplist;
(*pplist) = head->next;
free(head);
}
- 删操作 - 删除指定位置的节点
查操作
查操作就比较简单了,只需要逐个对比就可以了。这里直接放code了
// 单链表查找
SListNode* SListFind(SListNode* plist, const SLTDateType x)
{
while (plist)
{
if (plist->data == x)
return plist;
plist = plist->next;
}
return NULL;
}
改操作
改操作也很简单,就是在查找的基础上然后对找到的内容进行修改即可。所以这里就不再放code了,有兴趣可以自己尝试。
带头双向循环链表
学会了单链表的增删查改操作之后再来看带头双向循环链表的操作就简单了,只需要考虑额外pre的指针域就可以了。而且由于是带头链表,所以很多操作也变得简单了起来,这里直接放代码,就不再一一分析了。
// 带头+双向+循环链表的增删查改
typedef int LTDataType;
typedef struct ListNode
{
LTDataType _data;
struct ListNode* _next;
struct ListNode* _prev;
}ListNode;
// 创建返回链表的头结点
ListNode* ListCreate()
{
//成功返回指针,失败返回NULL
ListNode* head = (ListNode*)malloc(sizeof(ListNode));
if (head != NULL)
{
head->_data = 0; //头节点的data赋值为0单纯是为了好看,没什么用
head->_next = head;
head->_prev = head;
return head;
}
return NULL;
}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
//暴力检查
assert(pHead != NULL);
//从头开始free,最后free头节点
ListNode* tmp = pHead->_next;
while (tmp != pHead)
{
ListNode* next = tmp->_next;
free(tmp);
tmp = next;
}
free(tmp);
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
//暴力检查
assert(pHead != NULL);
//从头开始打印,不打印头节点
ListNode* tmp = pHead->_next;
while (tmp != pHead)
{
ListNode* next = tmp->_next;
printf("%d ", tmp->_data);
tmp = next;
}
puts("");
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead != NULL); //头指针不能为空
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
assert(newNode != NULL); //申请新节点失败
newNode->_data = x;
newNode->_next = pHead;
newNode->_prev = pHead->_prev;
pHead->_prev->_next = newNode;
pHead->_prev = newNode;
}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead != NULL); //头指针不能为空
assert(pHead->_next != pHead); //链表不能为空
ListNode* tail = pHead->_prev;
pHead->_prev = tail->_prev;
tail->_prev->_next = pHead;
free(tail);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead != NULL);
assert(pHead != NULL); //头指针不能为空
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
assert(newNode != NULL); //申请新节点失败
newNode->_data = x;
newNode->_next = pHead->_next;
newNode->_prev = pHead;
pHead->_next->_prev = newNode;
pHead->_next = newNode;
}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead != NULL); //phead不为空,防止传入错误
assert(pHead->_next != pHead); //链表为空的情况
ListNode* head = pHead->_next;
pHead->_next = head->_next;
head->_prev = pHead;
free(head);
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead != NULL);
ListNode* tmp = pHead->_next;
while (tmp != pHead)
{
if (tmp->_data == x)
return tmp;
tmp = tmp->_next;
}
return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
assert(pos != NULL);
ListNode* pre = pos->_prev;
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
assert(newNode != NULL); //申请新节点失败
//如果是在phead的前面插入就相当于尾插
newNode->_data = x;
newNode->_next = pos;
newNode->_prev = pre;
pre->_next = newNode;
pos->_prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
assert(pos != NULL);
//要避免删除头节点的情况
ListNode* next = pos->_next;
ListNode* pre = pos->_prev;
pre->_next = next;
next->_prev = pre;
free(pos);
}