1. 链表的分类
链表可分为八类。
单向链表 | 双向链表 |
---|---|
单向带头循环 | 双向带头循环 |
单向带头不循环 | 双向带头不循环 |
单向不带头循环 | 双向不带头循环 |
单向不带头不循环 | 双向不带头不循环 |
上一篇 单链表 的博文中,主要实现的是 单向不带头非循环链表 的链表结构。这种链表结构,结构简单,一般不会单独用来存储数据。实际中更多地是作为其他数据结构的子结构,如哈希桶、图的邻接表等。
本文主要实现 双向带头循环链表 的链表结构。这种链表结构,结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是双向带头循环链表。 另外,这种结构虽然结构复杂,但是代码实现以后会发现结构会带来很多优势,实现反而简单了。
2.什么是双向带头循环链表
双向带头循环链表是由一个数据域和两个指针域组成,其中指针包含前驱指针 prev 和后继指针 next。其中 prev 指向上一个节点,next 指向下一个节点。
3. 双向带头循环链表的实现
代码的实现将放在三个文件中,分别是 List.h(用于声明)、List.c(用于定义)、Test.c(用于测试)。
List.h 文件
#pragma once #include<stdio.h> #include<stdlib.h> #include <string.h> #include <assert.h> typedef int LTDataType; // 双向带头循环链表的结构 typedef struct ListNode{ LTDataType data; // 数据域,存储数据 struct ListNode* next; // 后继指针,存储下一个节点的地址 struct ListNode* prev; // 前驱指针,存储上一个节点的地址 }ListNode; // 双向带头循环链表的初始化 ListNode* ListInit(); // 创建一个新节点 ListNode* BuyListNode(LTDataType x); // 打印 void ListPrint(ListNode* phead); // 尾插 void ListPushBack(ListNode* phead, LTDataType x); // 尾删 void ListPopBack(ListNode* phead); // 头插 void ListPushFront(ListNode* phead, LTDataType x); // 头删 void ListPopFront(ListNode* phead); // 查找 ListNode* ListFind(ListNode* phead, LTDataType x); // 在 pos 位置之前插入数据 void ListInsert(ListNode* pos, LTDataType x); // 删除 pos 位置的节点 void ListErase(ListNode* pos); // 销毁 void ListDestroy(ListNode* phead);
3.1 创建一个新节点
List.c 文件
#include "List.h" // 创建一个新节点 ListNode* BuyListNode(LTDataType x){ ListNode* newnode = (ListNode*)malloc(sizeof(ListNode)); // 检查内存是否开辟成功 if (newnode == NULL){ printf("malloc fail\n"); exit(-1); } newnode->data = x; newnode->next = NULL; newnode->prev = NULL; return newnode; }
3.2 双向带头循环链表的初始化
思路: 创建的链表为带头链表,所以在初始化链表时,需要将作为哨兵位的头节点创建好。作为哨兵位的头节点不存储数据,但却有着至关重要的作用。当链表为空链表时,头节点仍然存在。同时因为是双向链表,所以头节点的 prev 和 next 都指向自己。
List.c 文件
#include "List.h" // 双向带头循环链表的初始化 ListNode* ListInit(){ // 创建作为哨兵位的头节点,不存储有效数据 ListNode* phead = BuyListNode(0); phead->next = phead; // 后继指针指向自己 phead->prev = phead; // 前驱指针指向自己 return phead; }
3.3 打印
思路: 首先需要明确的是,作为哨兵位的头节点不存储数据,所以需要创建一个指针变量 cur 从 phead->next 开始依次遍历打印链表。因为链表是循环的,所以当 cur 回到 phead 时停止。
List.c 文件
#include "List.h" // 打印 void ListPrint(ListNode* phead){ assert(phead); ListNode* cur = phead->next; while (cur != phead){ printf("%d ", cur->data); cur = cur->next; } printf("\n"); }
3.4 尾插
思路: 尾插体现了双向循环链表的结构优势,因为头节点的 phead->prev 指向了链表尾节点的地址,所以就找到了尾节点,此时需要再创建一个节点存储插入的数据。最后将头节点 phead、原尾节点 tail、新节点 newnode 链接起来即可。
List.c 文件
#include "List.h" // 尾插 void ListPushBack(ListNode* phead, LTDataType x){ assert(phead); ListNode* tail = phead->prev; // 保存尾节点地址 ListNode* newnode = BuyListNode(x); // 创建一个新的节点 newnode->data = x; // 将头节点 phead、原尾节点 tail、新节点 newnode 链接起来 tail->next = newnode; newnode->prev = tail; newnode->next = phead; phead->prev = newnode; // 方法二、调用 ListInsert 函数 // ListInsert(phead, x); }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 ListPushBack(plist, 1); ListPushBack(plist, 2); ListPushBack(plist, 3); ListPushBack(plist, 4); // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.5 尾删
思路: 尾删同样体现了双向循环链表的结构优势。因为链表是循环的,所以链表尾节点的地址保存在头节点的 prev 中。我们可以创建指针变量 tail 保存尾节点地址。在删除尾节点时,也要保证尾节点的上一个节点的地址被保存,所以创建指针变量 tailPrev 指向 tail->prev,然后把 tailPrev 的 next 指向头节点 phead,将 phead 的 prev 置成 tailPrev。
List.c 文件
#include "List.h" // 尾删 void ListPopBack(ListNode* phead){ assert(phead); // 哨兵位不能为空 assert(phead->next != phead); // 链表不能为空,防止删除作为哨兵位的头节点 ListNode* tail = phead->prev; // 尾节点地址放入 tail ListNode* tailPrev = tail->prev; // 新的尾节点 tailPrev free(tail); tail = NULL; // 将头节点 phead、原尾节点的上一个节点 tailPrev 链接起来 tailPrev->next = phead; phead->prev = tailPrev; // 方法二、调用 ListErase 函数 //ListErase(phead->prev); }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 6; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 尾删 ListPopBack(plist); ListPopBack(plist); ListPopBack(plist); // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.6 头插
思路: 首先创建一个新节点 newnode,创建指针变量 next 保存 phead->next(即头节点的下一个节点的地址)。然后将头节点 phead、新节点 newnode、原头节点的下一个节点链接起来。
List.c 文件
#include "List.h" // 头插 void ListPushFront(ListNode* phead, LTDataType x){ assert(phead); ListNode* newnode = BuyListNode(x); ListNode* next = phead->next; // 先找到头 // 将头节点 phead、新节点 newnode、原头节点的下一个节点链接起来 phead->next = newnode; newnode->prev = phead; newnode->next = next; next->prev = newnode; // 方法二、调用 ListInsert 函数 //ListInsert(phead->next, x); }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 头插 ListPushFront(plist, 10); ListPushFront(plist, 20); // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.7 头删
思路: 创建指针变量 next 保存 phead->next(即哨兵位的下一个节点的地址),再创建指针变量 nextNext 保存 next->next(即哨兵位后的第二个节点的地址)。然后将哨兵位 phead、哨兵位后的第二个节点链接起来。
List.c 文件
#include "List.h" // 头删 void ListPopFront(ListNode* phead){ assert(phead); // 如果哨兵位的后继指针指向头,即链表为空,则不能进行头删 assert(phead->next != phead); ListNode* next = phead->next; // 先找到哨兵位的下一个节点 ListNode* nextNext = next->next; // 再找到哨兵位后的第二个节点 // 将哨兵位 phead、哨兵位后的第二个节点链接起来,并释放原哨兵位的下一个节点 phead->next = nextNext; nextNext->prev = phead; free(next); // 方法二、调用 ListErase 函数 //ListErase(phead->next); }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 头删 ListPopFront(plist); ListPopFront(plist); // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.8 查找
思路: 创建一个指针变量 cur 指向哨兵位 phead 的 next,然后遍历链表。判断 cur->data 是否为查找的数据,如果是则返回 cur,不是则继续遍历,终止条件是 cur 指向哨兵位节点(即遍历完成,找不到所查找的数据)。
List.c 文件
#include "List.h" // 查找 ListNode* ListFind(ListNode* phead, LTDataType x){ assert(phead); ListNode* cur = phead->next; while (cur != phead){ // 找到返回对应的地址 if (cur->data == x){ return cur; } // 找不到继续向后找 cur = cur->next; } // 彻底找不到 return NULL; }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 查找 ListNode* pos = ListFind(plist, 2); if (pos){ printf("找到了,pos 节点:%p->%d\n", pos, pos->data); } } int main(){ TestList1(); return 0; }
运行结果:
3.9 在 pos 位置之前插入数据
思路: 创建一个新节点 newnode,创建一个指针变量 posPrev 保存 pos->prev(即储存 pos 位置的上一个节点的地址),将 posPrev->next 指向新节点 newnode,同时将 newnode->prev 指向 posPrev,newnode->next 指向 pos,pos->prev 指向 newnode。
List.c 文件
#include "List.h" // 在 pos 位置之前插入数据 void ListInsert(ListNode* pos, LTDataType x){ assert(pos); // 创建插入节点 ListNode* newnode = BuyListNode(x); // 储存 pos 位置的上一个节点地址 ListNode* posPrev = pos->prev; // 将 pos 的上一个节点 posPrev、新节点 newnode、pos 节点链接起来 posPrev->next = newnode; newnode->prev = posPrev; newnode->next = pos; pos->prev = newnode; }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 查找 ListNode* pos = ListFind(plist, 2); if (pos){ // 在 pos 位置之前插入数据 ListInsert(pos, 10); } // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.10 删除 pos 位置的节点
思路: 创建两个指针变量 posPrev 和 posNext 分别保存 pos 位置的上一个节点和下一个节点的地址,然后将 pos 节点 free 掉,连接 posPrev 和 posNext。
List.c 文件
#include "List.h" // 删除 pos 位置的节点 void ListErase(ListNode* pos){ assert(pos); // 保存 pos 位置的前一个节点 ListNode* posPrev = pos->prev; // 保存 pos 位置的下一个节点 ListNode* posNext = pos->next; // 将 pos 的上一个节点 posPrev、下一个节点 posNext链接起来 posPrev->next = posNext; posNext->prev = posPrev; free(pos); pos = NULL; }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 查找 ListNode* pos = ListFind(plist, 2); if (pos){ // 删除 pos 位置的节点 ListErase(pos); } // 打印 ListPrint(plist); } int main(){ TestList1(); return 0; }
运行结果:
3.11 销毁
思路: 创建指针变量 cur 遍历链表,先将有效数据的节点 free 掉,最后再将哨兵位节点 free 掉即可。
List.c 文件
#include "List.h" // 销毁 void ListDestroy(ListNode* phead){ assert(phead); // 在销毁链表时,需要将节点逐个销毁, // 销毁前一个节点,必须要保存下一个节点的地址 ListNode* cur = phead->next; while (cur != phead){ ListNode* next = cur->next; free(cur); cur = next; } free(phead); phead = NULL; }
Test.c 文件
#include "List.h" void TestList1(){ // 初始化链表 ListNode* plist = ListInit(); // 尾插 for (int i = 0; i < 4; i++){ ListPushBack(plist, i); } // 打印 ListPrint(plist); // 销毁 ListDestroy(plist); } int main(){ TestList1(); return 0; }
4. 顺序表与链表的对比总结
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间 | 物理上连续 | 逻辑上连续 |
随机访问 | 支持 O(1) 的随机访问 | 不支持随机访问,访问元素需要 O(N) |
任意位置插入删除 | 需要移动元素,O(N) | 只需改变指针指向 |
插入数据 | 要考虑扩容,会带来一定的空间消耗 | 没有容量的概念,可以按需申请和释放 |
缓存利用率 | 高 | 低 |
应用场景 | 元素高效存储以及频繁访问的场景 | 任意位置插入和删除频繁的场景 |