上一章我们对单链表的简单功能做了代码实现,但对于单链表不适合存储的一些缺点,这一章我们来实现另一种常见的链式结构---带头双向循环链表。
文章目录
- 前言
- 双向循环链表的初始化
- 双向循环链表的打印
- 双向循环链表新的结点
- 双向循环链表尾插
- 双向循环链表头插
- 双向循环链表尾删
- 双向循环链表头删
- 双向循环链表查找
- 双向循环链表的指定位置前插入
- 双向循环链表的指定位置删除
- 双向循环链表销毁
- 头文件代码
- 源文件代码
- 链表与顺序表区别总结
前言
上一章我们介绍了单链表的增删查改等简单功能,链表的划分大致有是否带哨兵位的头结点,是否双向,是否循环,这章我们介绍带头双向循环链表(如下图所示),看起来虽然很复杂,但是实现起来其实比单链表还要简单,因为它有着结构上的优势,在存储或者增删查改中都有着自己的优势,在以下代码实现中大家就能感受到。
(本篇参考比特科技学习)
typedef struct listNode {
dataType val;
struct listNode* next;
struct listNode* prev;
}listNode;
双向循环链表带头且每个结点可以指向下一个结点也可以指向上一个结点,最后一个结点指向头结点,头结点也指向最后一个结点,如果只有头结点自己,它自己指向自己。
双向循环链表的初始化
listNode* listInit() {
listNode* phead = (listNode*)malloc(sizeof(listNode));
phead->prev = phead;
phead->next = phead;
return phead;
}
初始化就是直接开辟一个新结点自己指向自己,这里面的值不重要,因为它就起到一个头结点的作用。
双向循环链表的打印
void listPrint(listNode* phead) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
打印直接传链表头结点,让cur等于头结点的next每次往后走,不是头结点就打印,到了就结束。
双向循环链表新的结点
listNode* buyListNode(dataType val) {
listNode* newnode = (listNode*)malloc(sizeof(listNode));
newnode->val = val;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
因为头插尾插随即插入都需要开辟新结点,所以封装一下。
双向循环链表尾插
void listNodePushBack(listNode* phead, dataType val) {
assert(phead);
/*listNode* newnode = buyListNode(val);
listNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;*/
listNodeInsert(phead->prev,val);
}
注释掉的是我们不用insert优化自己写的,下面介绍完insert我们可以用insert来优化头插尾插。不用insert尾插思路也很简单,创建一个tail记录phead的上一个结点也就是现在的尾结点,让这个新结点的prev指向原来尾结点,再让原来尾结点指向这个新结点,再让新结点跟头结点相互存一下地址即可,即使是空链表只有一个头结点这样依然可行。
双向循环链表头插
void listNodePushFront(listNode* phead, dataType val) {
assert(phead);
/*listNode* newnode = buyListNode(val);
listNode* headNext = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = headNext;
headNext->prev = newnode;*/
listNodeInsert(phead->next, val);
}
头插思路也一样,为了保持代码可读性,我们先把头结点存的next用一个临时表量存起来,然后新链表跟头结点互存地址,跟headNext互存地址,如果原本是空链表,那么这个headNext还是头结点,也不会出现问题。
双向循环链表尾删
void listNodePopBack(listNode* phead) {
assert(phead && (phead->next != phead));
/*listNode* newTail = phead->prev->prev;
free(phead->prev);
newTail->next = phead;
phead->prev = newTail;*/
listNodeDelete(phead->prev);
}
尾删就需要判断一下是不是空链表了,我们总不能把哨兵位的头结点删了。只需要断言一下头结点的next是不是它自己就行。这个时候就凸显出来这种结构的优势了,既然是尾删,那么新的尾就是头结点的prev的prev,如果就一个,那么新的尾就是头结点也不会出问题,这个时候我们只需要把原来的尾free掉,把头结点指向新的尾,再把新的尾指向头结点即可。
双向循环链表头删
void listNodePopFront(listNode* phead) {
assert(phead && (phead->next != phead));
/*listNode* newHead = phead->next->next;
free(phead->next);
newHead->prev = phead;
phead->next = newHead;*/
listNodeDelete(phead->next);
}
头删的思路跟尾删几乎一样,不再赘述。
双向循环链表查找
listNode* listNodeFind(listNode* phead, dataType val) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
if (cur->val == val) return cur;
cur = cur->next;
}
return NULL;
}
定义一个cur存头结点下一个结点,循环走直到遇到头结点说明找了一遍这时还没有找到返回空。如果找到了直接返回cur,没有找到往后挪。
双向循环链表的指定位置前插入
void listNodeInsert(listNode* pos, dataType val) {
assert(pos);
listNode* newnode = buyListNode(val);
listNode* posPrev = pos->prev;
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
在指定位置前插入其实脑子里想一想就很简单,只需要搞出一个新结点,让本来前面那个跟新结点互相存址,再让这个跟新结点互相存址即可。为了保持可读性,我们先定义好前面那个结点posPre这样代码看起来直接就能明白啥意思。就算是链表中只有哨兵位的头,那么插入进来的结点也是和头结点互相存址的。
双向循环链表的指定位置删除
void listNodeDelete(listNode* pos) {
assert(pos);
listNode* posPrev = (pos)->prev;
listNode* posNext = (pos)->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
删除的思路也很简单,我们直接定义这个位置前后的两个结点,让前后两个结点互指,再把中间这个要删的free就行。 同样就算链表中除了头只有一个结点也可以成功删除,最后还是头自己指向自己。
双向循环链表销毁
void listDestroy(listNode* phead) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
listNode* curNext = cur->next;
free(cur);
cur = curNext;
}
free(phead);
}
销毁这里我们没有传二级指针,因为整个代码传的几乎都是一级指针,为了保持一致性我们可以这里传一级然后在外面将phead搞为空指针。
头文件代码
#pragma once
#define _CRT_NO_SECRUE_WARNINGS
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int dataType;
typedef struct listNode {
dataType val;
struct listNode* next;
struct listNode* prev;
}listNode;
//双向循环链表的初始化
listNode* listInit();
//双向循环链表的打印
void listPrint(listNode* phead);
//双向循环链表新的结点
listNode* buyListNode(dataType val);
//双向循环链表的尾插
void listNodePushBack(listNode* phead, dataType val);
//双向循环链表的头插
void listNodePushFront(listNode* phead, dataType val);
//双向循环链表的尾删
void listNodePopBack(listNode* phead);
//双向循环链表的头删
void listNodePopFront(listNode* phead);
//双向循环链表的查找
listNode* listNodeFind(listNode* phead, dataType val);
//双向循环链表的指定位置前插入
void listNodeInsert(listNode* pos, dataType val);
//双向循环链表的指定位置删除
void listNodeDelete(listNode* pos);
//双向循环链表的销毁
void listDestroy(listNode* phead);
源文件代码
#include "list.h"
listNode* listInit() {
listNode* phead = (listNode*)malloc(sizeof(listNode));
phead->prev = phead;
phead->next = phead;
return phead;
}
listNode* buyListNode(dataType val) {
listNode* newnode = (listNode*)malloc(sizeof(listNode));
newnode->val = val;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
void listPrint(listNode* phead) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
printf("%d ", cur->val);
cur = cur->next;
}
printf("\n");
}
void listNodePushBack(listNode* phead, dataType val) {
assert(phead);
/*listNode* newnode = buyListNode(val);
listNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
phead->prev = newnode;
newnode->next = phead;*/
listNodeInsert(phead->prev,val);
}
void listNodePushFront(listNode* phead, dataType val) {
assert(phead);
/*listNode* newnode = buyListNode(val);
listNode* headNext = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = headNext;
headNext->prev = newnode;*/
listNodeInsert(phead->next, val);
}
void listNodePopBack(listNode* phead) {
assert(phead && (phead->next != phead));
/*listNode* newTail = phead->prev->prev;
free(phead->prev);
newTail->next = phead;
phead->prev = newTail;*/
listNodeDelete(phead->prev);
}
void listNodePopFront(listNode* phead) {
assert(phead && (phead->next != phead));
/*listNode* newHead = phead->next->next;
free(phead->next);
newHead->prev = phead;
phead->next = newHead;*/
listNodeDelete(phead->next);
}
listNode* listNodeFind(listNode* phead, dataType val) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
if (cur->val == val) return cur;
cur = cur->next;
}
return NULL;
}
void listNodeInsert(listNode* pos, dataType val) {
assert(pos);
listNode* newnode = buyListNode(val);
listNode* posPrev = pos->prev;
posPrev->next = newnode;
newnode->prev = posPrev;
newnode->next = pos;
pos->prev = newnode;
}
void listNodeDelete(listNode* pos) {
assert(pos);
listNode* posPrev = (pos)->prev;
listNode* posNext = (pos)->next;
posPrev->next = posNext;
posNext->prev = posPrev;
free(pos);
pos = NULL;
}
void listDestroy(listNode* phead) {
assert(phead);
listNode* cur = phead->next;
while (cur != phead) {
listNode* curNext = cur->next;
free(cur);
cur = curNext;
}
free(phead);
}
链表与顺序表区别总结
顺序表:
优点:
- 支持随机访问,可以用二分,排序等算法
- CPU高速缓存的利用率更高
缺点:
- 插入删除数据的效率较低,时间复杂度为O(N)
- 在物理结构上是连续存储的,空间不够了需要增容,增容会有一定消耗,增容空间大了还需要找更大空间并把已有数据拷贝,为避免频繁扩容一般都按两倍增容,但用不完也会导致一定的空间浪费
链表(双向带头循环链表):
优点:
- 可以在任意位置插入删除,时间复杂度为O(N)
- 可以按需申请释放空间,不会导致空间浪费
缺点:
- 不支持随机访问,二分,排序等算法不适用
- 每存一个值的同时还需要存其他结点的地址,会有一定消耗
- CPU高速缓存命中率更低