双向链表
1.带头双向循环链表
实际中要实现的链表结构非常多样,以下情况结合起来就有8中链表结构:
1.链接方向:单向、双向
2.带不带头节点(哨兵卫)
3..循环、非循环
1.无头单向非循环链表:结构最简单,一般不会单独用来存放数据。实际中跟多的是作为其他数据结构的子结构,比如哈希桶、图
2.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
2.带头双向循环链表的实现
2.1定义双向链表
//对数据类型进行重命名,这样可以对多种数据类型进行存储,有利于控制链表
typedef int LTDataType;
//结点(结构体)
typedef struct LTNode
{
DateType val;
struct LTNode* next;//结点的后继
struct LTNode* prev;//结点的前驱
}LTNode;
2.2函数接口(双向链表的操作)
//初始化
LTNode* ListInit();
//打印
void ListPrint(LTNode* phead);
//尾插
void ListPushBack(LTNode* phead, LTDataType x);
//头插
void ListPushFront(LTNode* phead, LTDataType x);
//尾删
void ListPopBack(LTNode* phead);
//头删
void ListPopFront(LTNode* phead);
//删除链表结点时,用于判断链表是否为空,为空返回真,不为空返回假
bool ListEmpty(LTNode* phead);
//计算链表大小
size_t ListSize(LTNode* phead);
//链表查找
LTNode* ListFind(LTNode* phead, LTDataType x);
//链表插入
void ListInsert(LTNode* pos, LTDataType x);
//链表删除
void ListErase(LTNode* pos);
//链表销毁
void ListDestroy(LTNode* phead);
//NOTE:不适用二级指针解决方案
//1.c++的引用
//2.带返回值类型
//3.建立头结点(哨兵卫)
2.2.1初始化双向链表
//初始化 首先Buy一个 然后头节点的前驱后继都指向自己
LTNode* LTInit()
{
//创建一个哨兵位
LTNode* phead = BuyLTNode(-1);
//带头双向循环链表的初始化
//就是先建立一个头结点
//头结点的两个指针域都指向头结点,形成自环
phead->next = phead;
phead->prev = phead;
return phead;
}
2.2.2创建新结点
LTNode* BuyLTNode(DateType x)
{
//以下过程就是创建哨兵卫的操作
//动态开辟一个新空间
LTNode* newnode = (LTNode*)malloc(sizeof(struct LTNode));
//开辟失败操作
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
//前驱后继都指向空
newnode->next = NULL;
newnode->prev = NULL;
newnode->val = x;
return newnode;
}
2.2.3打印双向链表操作
void LTPrint(LTNode* pHead)
{
//断言判断pHead是否为空
assert(pHead);
//当cur!=pHead时遍历整个链表
LTNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d", cur->val);
printf("<-->");
cur = cur->next;
if (cur == pHead)
{
printf("phead");
}
}
printf("\n");
}
2.2.4头插法
void LTPushFront(LTNode* pHead,DateType x)
{
//创建一个节点
//LTNode* newnode = BuyLTNode(x);
//LTNode* first = pHead->next;
//pHead->next = newnode;
//newnode->prev = pHead;
//newnode->next = first;
//first->prev = newnode;
//复用
LTInsert(pHead->next, x);
}
2.2.5头删法
//头删法 free掉frist 然后phead->next 指向second second->prev指向phead
void LTPopFront(LTNode* pHead)
{
//assert(pHead);
//assert(pHead->next != pHead);
//LTNode* cur = pHead->next;
//LTNode* next = cur->next;
//pHead->next = next;
//next->prev = pHead;
//free(cur);
//cur = NULL;
LTErase(pHead->next);
}
2.2.6尾插法
void LTPushBack(LTNode* pHead,DateType x)
{
//assert(pHead);
//LTNode* newnode = BuyLTNode(x);
记录尾节点地址
//LTNode* LTtail = pHead->prev;
//LTtail->next = newnode;
//newnode->prev = LTtail;
//newnode->next = pHead;
//pHead->prev = newnode;
//复用
LTInsert(pHead,x);
}
2.2.7尾删法
void LTPopBack(LTNode* pHead)
{
//assert(pHead);
//assert(pHead->next!= pHead);
//LTNode* pTail = pHead->prev;
//LTNode* prev = pTail->prev;
//pHead->prev = prev;
//prev->next = pHead;
//free(pTail);
//pTail = NULL;
LTErase(pHead->prev);
}
2.2.8查找
LTNode* LTFind(LTNode* pHead, DateType x)
{
assert(pHead);
//遍历寻找
LTNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->val == x)
{
return cur;
}
//迭代
cur = cur->next;
}
return NULL;
}
2.2.9判断链表是否为空
//判断双向链表是否为空 return phead->next==phead
bool IsLTEmpty(LTNode* pHead)
{
assert(pHead);
//if (pHead->next == pHead)
//{
// return true;
//}
//return false;
return pHead->next == pHead;
}
2.2.10链表大小
//LTSize的大小
size_t LTSize(LTNode* pHead)
{
assert(pHead);
assert(!IsLTEmpty(pHead));
LTNode* cur = pHead->next;
int count = 0;
while (cur != pHead)
{
count++;
cur = cur->next;
}
return count;
}
2.2.11销毁链表
//销毁链表
void LTDestory(LTNode* pHead)
{
assert(pHead);
LTNode* cur = pHead->next;
while (cur != pHead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
//释放哨兵卫
free(pHead);
pHead = NULL;
}
3.顺序表与链表的区别
1、存取方式
顺序表支持随机存取,而链表不支持,它只能从表头按顺序存取元素。
2、逻辑结构与存储结构
在顺序表中,逻辑上相邻的元素,其对应的物理位置也相邻,而链表中,逻辑上相邻的元素,其物理位置不一定相邻。
3、空间分配
在顺序表中,对于静态存储,一旦存储空间装满就不能扩充,再加入新元素将导致内存溢出;对于动态存储,存储空间装满时可以扩充,但需要移动大量元素导致效率降低,而且当内存中没有更大块的连续存储空间时导致内存分配失败。在链表中,对于静态存储,与顺序表的静态存储类似;对于动态存储,结点的空间只在需要的时候申请分配,只要内存有空间就可以分配成功。
4、存储密度
顺序表中,无论静态存储还是动态存储,其存储密度均为1,因为数组空间只用来存数据元素。而在链表中,对于静态存储和动态存储,每个结点除了存储数据元素自身外,还会至少存储直接后继的存储位置信息。相对于顺序表,链表的存储密度要低得多。
顺序表的优点:
1.尾插尾删效率很高
2.随机访问(利用下标访问)
3.相比链表结构,顺序表CPU高速缓存命中率更高
顺序表的缺点:
1.头部和中部插入删除效率低。(O(N))
2.扩容:性能消耗,空间浪费
链表的优点:
1.任意位置插入删除效率很高。O(1)
2.按需申请释放
链表的缺点:
1.不支持随机访问