目录
前言
链表的概念与理解
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
此图为单向链表的物理图(箭头便于理解,实际上并不存在直接链接)
- 链式结构在逻辑上是连续的,但是在物理上不一定连续
- 现实中的结点一般都是从堆上申请出来的
- 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
链表的分类
- 单向或者双向
- 带头或不带头
- 循环或非循环
最常用的链表有两种:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。
我们要实现的就是带头双向循环链表
功能实现
在实现了无头非循环单向链表后,现在实现的带头双向循环链表实际在实现上更加简单
大多功能实现都有很强的相似性,关键在于能否画图理解
头文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>//清空链表功能使用
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
//初始化链表
LTNode* ListInit();
//创建节点
LTNode* BuyListNode(LTDataType x);
//打印链表
void ListPrint(LTNode *phead);
//链表尾插
void ListPushBack(LTNode* phead, LTDataType x);
//链表尾删
void ListPopBack(LTNode* phead);
//链表头插
void ListPushFront(LTNode* phead, LTDataType x);
//链表头删
void ListPopFront(LTNode* phead);
//查找链表元素
LTNode* FindListNode(LTNode* phead, LTDataType x);
//任意位置插入
void ListInsert(LTNode* pos, LTDataType x);
//任意位置删除
void ListErase(LTNode* pos);
//销毁链表
void ListDestory(LTNode* phead);
//计算链表大小
size_t LTSize(LTNode* phead);
//清空链表
bool LTEmpty(LTNode* phead);
该双向链表有
next
和prev
两个指针,分别指向节点的前后,而头节点的prev
是尾节点,尾节点的next
是头节点,这就是循环的体现
初始化链表
- 在单链表中我们使用二级指针的方式改变链表,这里我们选择返回指针的方式(二级指针仍可)
- 初始化及将节点的next和prev都先指向自己
LTNode* ListInit()
{
//哨兵位头节点创建
//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;//返回指针
}
创建节点
- 所有插入功能都需要创建节点,所以单独写一个功能
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)//判断是否malloc成功
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
尾插
- 尾插定义一个
tail
在phead->prev
后创建一个newnode节点,之后就是把各个指向位置设置好即可
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = BuyListNode(x);
//(位置关系)phead ....................... tail newnode
phead->prev = newnode;
newnode->next = phead;
newnode->prev = tail;
tail->next = newnode;
}
尾删
尾删我们介绍两个方法
- 一种创建两个指针,一种创建一个指针
- 不论哪种实际上都是改变指针的指向再释放尾部的节点
(法一)两个指针:
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tailPrev = phead->prev->prev;//倒数第二个节点
phead->prev = tailPrev;//头节点的prev指向tailPrev
LTNode* tail = tailPrev->next;//尾节点
tailPrev->next = phead;//tailPrev的next指向头节点(循环)
//释放尾节点
free(tail);
//tail = NULL; //非必须
}
(法二)单个指针:
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;//尾节点
phead->prev = tail->prev;//头节点的prev指向倒数第二个节点
tail->prev->next = phead;//倒数第二个节点的next指向头节点
//释放尾节点
free(tail);
}
头插
- 总体思路不变,创建
newnode
节点后更改哨兵位和第二个节点(哨兵位后一位节点)的指针指向
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
头删
- 把哨兵位(设哨兵位为第一位节点)的
next
指向第三位节点,第三位节点的prev
指向哨兵位再释放第二位节点(需提前记录第二位节点位置)
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* next = phead->next;//哨兵位后一位节点(第二位节点)
LTNode* nextNext = next->next;//第三位节点
phead->next = nextNext;
nextNext->prev = phead;
free(next);
}
元素数据查找
- 查找进行的遍历和打印一致,只需要改变
while
循环内部语句
void FindListNode(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
//遍历
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;//向后找
}
//找不到返回空
return NULL;
}
任意位置插入
我们这里实现的任意位置插入是向前插入,可以对头插尾插进行改写
- 创建
posPrev
指向pos
的前一位节点和newnode
节点 posPrev
和newnode
间建立链接关系(互相next/prev
指向)newnode
和pos
间建立链接关系
void ListInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* newnode = BuyListNode(x);
//(位置关系)posPrev newnode pos
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
由此可以对尾插和头插进行更改:
头插:
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead->next, x);
}
尾插:
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
ListInsert(phead, x);//phead的prev是最后一个节点
}
任意位置删除
- 先定义
pos
前和后的两个指针posPrev,posNext
- 再让
posPrev
和posNex
t互相指向 - 释放
pos
(并置空)
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* posPrev = pos->prev;
LTNode* posNext = pos->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
此时可以进行头删尾删的更改:
头删:
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
ListNodeErase(phead->next);
}
尾删:
void ListPopBack(LTNode* phead)
{
assert(phead);
//链表为空
assert(phead->next != phead);
ListErase(phead->prev);
}
销毁链表
- 遍历每一位并释放
void ListDestory(LTNode* phead)
{
LTNode* cur = phead->next;
while (phead != cur)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;//可以省去但要在使用ListDestory后手动置空
}
求链表大小
- 即求链表元素个数,因为大小肯定不为负,所以使用
size_t
- 定义遍历size,遍历每次
size++
即可
size_t LTSize(LTNode* phead)
{
assert(phead);
size_t size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
判断链表是否为空
- 用
bool
类型,直接用返回phead->next == phead
,即判断
bool LTEmpty(LTNode* phead)
{
//用if语句也可
assert(phead);
return phead->next == phead;
}