一:前言
本篇只列举比较常见和基础的四种链表,另外的静态链表(即用数组进行链表的模拟实现)以及跳表在这里不进行实现。有兴趣或者需要的同学可以自行检索,代码使用了C和C++语法风格相结合的代码实现(个人认为较为方便的一种写法)。代码相关的解释均放入代码注释中。关于头结点的介绍,只在单向链表中进行区别,相信大家在独立思考后会对有无头结点有什么不同的影响会有一个深入地认识。后续三种链表均将设置头结点。
二:链表的定义
链表是物理存储单元上非连续、非顺序的存储结构,链表的结点间通过指针相连,每个结点只对应有一个前驱结点和一个后继结点,其中,首结点没有前驱结点,尾节点没有后继结点。
三:链表的结构
链表的结点由数据域和指针域组成。
链表则是由n个结点通过指针相互链接所形成的,当链表结点个数为0时,一般称该链表为空链表。
有效结点:存储真实有效的数据的结点。
首(元)结点:链表中第一个有效结点。
尾结点:链表中最后一个结点。
*头结点:头结点属于可有可无的存在,头结点的存在只是为了简化链表的一些操作(比如对于删除和插入操作的边界检查),后续会讲。如果头结点存在的话,该头结点需要自己分配一个结点的空间,该结点拥有数据域与指针域,数据域可以存储链表的长度或其他一些数据,即有没有该头结点都不能影响链表中的数据,也可以不存储数据。以下操作我均不在头结点的数据域设置数据,头结点的指针域指向首(元)结点。
头指针:指向链表第一个结点的指针,有头结点指向头结点,无头结点指向首(元)节点。
尾指针:指向链表最后一个结点的指针。
四:单向链表
(1):有头结点的情况
单向链表的指针域next始终指向下一结点,尾结点的next指向nullptr(NULL),头结点的数据域为空。
在这里的插入操作是在选定结点前进行插入,可以实现除了尾插的其他所有情况,在看懂了代码逻辑后,其他操作可以自己进行实现。
头结点的设置是为了在删除和插入操作时不需要再检查链表的边界情况,即不需要在对链表进行插入和删除时进行额外的方法进行所插入或删除的结点是否为首元结点的检查。
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node *next;
} *ListNode;
// 创建新结点
ListNode creatNewNode()
{
ListNode newNode = new Node;
if (newNode == nullptr) // 判断内存分配是否成功
{
cout << "New Failed!!!" << endl;
return nullptr;
}
cout << "请输入新结点中的数据: ";
cin >> newNode->data;
newNode->next = nullptr;
return newNode;
}
// 创建链表
ListNode creatLinkList(int n)
{
ListNode head = nullptr; // 将头指针与尾指针置为空,便于检查后续循环中新创建节点是否为第一个结点的情况,也方便检查链表是否为空的操作
ListNode tail = nullptr;
for (int i = 0; i != n; ++i)
{
ListNode newNode = creatNewNode();
if (newNode == nullptr)
{
cout << "New Failed!!!" << endl;
return nullptr;
}
if (head == nullptr) // 如果头指针head为空,说明是链表的第一个结点,将头指针head与尾指针指向该节点
{
head = newNode;
tail = newNode;
}
else // 如果头指针不为空,即不是第一个结点,此时只需要将尾指针进行跟进就好
{
tail->next = newNode;
tail = tail->next;
}
}
return head; // 头指针在指向链表中创建的第一个结点后,便没有发生改变,正常返回即可
}
// 判断链表是否为空
bool emptyList(ListNode head)
{ // 此处我添加了打印的操作,便于检查该操作的正确性!
if (head->next == nullptr)
{
cout << "该链表为空!!!" << endl;
return true;
}
else
{
cout << "该链表不为空!!!" << endl;
return false;
}
}
// 查找结点(返回上一结点)
ListNode findPreNode(ListNode head, int data)
{
ListNode current = head->next;
ListNode prev = head;
while (current != nullptr)
{
if (current->data == data)
return prev;
prev = current;
current = current->next;
}
cout << "未找到数据!!!" << endl;
return nullptr;
}
// 删除结点
void deleteNode(ListNode head)
{
int data = 0;
cout << "请输入待删除的节点数据: ";
cin >> data;
ListNode position = findPreNode(head, data); // 如果找到该数据,返回该数据的前一个结点
ListNode temp = position->next->next; // 将temp表示为待删除结点的后一个结点
delete position->next; // 删除节点时要释放空间,结点是手动进行的内存分配,删除时应及时销毁
position->next = temp; // 将所删除结点的前后指针相连
}
// 插入结点
void insertNode(ListNode head)
{
int data = 0;
cout << "请输入待被插入的节点数据: ";
cin >> data;
ListNode position = findPreNode(head, data);
ListNode newNode = creatNewNode(); // 创建一个待插入的新结点
ListNode temp = position->next; // temp记录待被插入节点的后一节点的位置
position->next = newNode; // 将前一结点指向新结点
newNode->next = temp; // 再将新结点指向之前保存的后一结点temp
// 注意插入顺序,先保存后一结点位置,在更改前一结点的指针域,否则后一结点的地址将会丢失
}
// 销毁链表
void distroyList(ListNode head)
{
ListNode current = head; // 将current指向头结点,头结点也要在循环内进行删除
while (head)
{
current = head->next; // 保存后一结点位置
delete head; // 删除当前结点
head = current; // 更新结点
}
}
// 打印链表
void printList(ListNode head)
{
ListNode current = head->next; // 跳过头结点,从首元节点开始打印
while (current)
{
cout << current->data << " ";
current = current->next;
}
}
int main()
{
ListNode head = new Node;
cout << "请输入链表中元素的个数: ";
int num = 0;
cin >> num;
head->next = creatLinkList(num);
printList(head); // 注意下面操作传入的是头结点head本身
cout << endl;
emptyList(head);
deleteNode(head);
printList(head);
insertNode(head);
printList(head);
distroyList(head);
head = nullptr; //注意我只在销毁函数中销毁了已分配的堆空间,然而并没有改变head指针的指向,如果此时不将head置为空,那么此时head指针将指向一个已经销毁了的堆空间,此时会造成悬挂指针的问题
return 0;
}
(2):无头结点的情况
无头结点的情况链表直接从首元节点开始。
之所以分这两种情况,是为了对比有无头结点对代码本身的影响,大家应着重看删除和插入操作,其余操作并无两样。观察无头结点的代码比有头结点的代码多了什么操作。
#include <iostream>
#include <string>
using namespace std;
typedef struct LinkNode
{
int data;
LinkNode *next;
} Node, *ListNode;
// 创建新节点
ListNode CreatNode()
{
ListNode newNode = new Node;
if (newNode == nullptr)
{
cout << "New failed!!!" << endl;
return nullptr;
}
cout << "请输入新节点的值:";
cin >> newNode->data;
newNode->next = nullptr; // 最后一个设置为空
return newNode;
}
// 创建链表
ListNode CreatList(int listSize)
{
ListNode head = nullptr;
ListNode tail = nullptr;
for (int i = 0; i != listSize; ++i)
{
ListNode newNode = CreatNode();
if (newNode == nullptr)
cout << "New Failed!!!" << endl;
if (head == nullptr)
{
head = newNode;
tail = newNode;
}
else
{
tail->next = newNode;
tail = newNode;
}
}
return head;
}
// 打印链表
void printList(ListNode &head)
{
ListNode current = head;
while (current)
{
cout << "链表中的值为:";
cout << current->data << endl;
current = current->next;
}
}
// 销毁链表
void destroyList(ListNode &head)
{
ListNode current = head;
while (current)
{
ListNode temp = current->next;
delete current;
current = temp;
}
head = nullptr;
}
// 查找节点(返回前一节点)
ListNode findPreNode(ListNode &head, int data)
{
ListNode current = head;
while (current->next->data != data)
{
current = current->next;
}
return current;
}
// 删除节点
void deleteNode(ListNode &head)
{
cout << "请输入待删除的值:";
int data = 0;
cin >> data;
// 检查所删除结点是否为第一个结点,如果为第一个结点则需要传递头指针的引用,对外部头指针的连接进行修改,将其指向所删除结点的下一个结点,若不进行引用进行参数传递,则该函数中只是对副本链表的副本进行操作,无法作用于外部头指针head,即无法正确的删除链表中的结点
if (head->data == data)
{
ListNode temp = head;
head = head->next;
delete temp;
}
else //若不为首元节点,则进行正常删除
{
ListNode position = findPreNode(head, data);
ListNode temp = position->next->next;
delete position->next;
position->next = temp;
}
}
// 插入节点
void insertNode(ListNode &head)
{
int data = 0;
cout << "请输入待插入位置后的值:";
cin >> data;
//与删除操作一致,需要检查是否为首元节点。
if (head->data == data)
{
ListNode newNode = CreatNode();
newNode->next = head;
head = newNode;
}
else
{
ListNode position = findPreNode(head, data);
// cout << "请输入待插入的值:";
ListNode newNode = CreatNode();
ListNode temp = position->next;
position->next = newNode;
newNode->next = temp;
}
}
// 判断链表是否为空
bool emptyList(ListNode head)
{
//因为传递的是头指针,并不是头结点,所以只需对头指针head进行检查即可
return head == nullptr;
}
int main()
{
ListNode head = CreatList(3);
if (!emptyList(head))
cout << "该链表不为空!" << endl;
printList(head);
deleteNode(head);
printList(head);
insertNode(head);
printList(head);
destroyList(head);
//head = nullptr; //注意我在销毁链表中的操作,因为传递的是指针类型的引用,所以我可以在内部对指针本体进行更改,在那里我将指针只为了空,这个操作是可以作用到main()中的head指针的,因为对head指针的引用,实际上就相当于操作head指针自己。所以此时可以不再对head进行置空操作,因为已经在销毁函数中完成了。
if (emptyList(head))
{
cout << "该链表为空!" << endl;
}
return 0;
}
五:双向链表
双向链表的指针域有两个指针,一个指向前驱结点,一个指向后继结点,头指针的prior指向nullptr(NULL),尾结点的nex指向nullptr(NULL)。
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node *prior; //指向前驱节点的指针
Node *next; //指向后继结点的指针
} *ListNode;
// 创建节点
ListNode creatNode()
{
ListNode newNode = new Node;
int data = 0;
cout << "请输入新节点数据: ";
cin >> data;
newNode->data = data;
newNode->next = nullptr;
newNode->prior = nullptr;
return newNode;
}
// 创建链表
void creatLinkList(ListNode head, int n)
{
head->next = nullptr; //将指向前驱结点和指向后继结点的指针均置为空
head->prior = nullptr;
ListNode tail = nullptr;
for (int i = 0; i != n; ++i)
{
ListNode newNode = creatNode();
if (head->next == nullptr) //判断是否为第一个结点,如果是,将新结点与头结点相互建立联系
{
head->next = newNode; //头指针的next域指向新结点
newNode->prior = head; //新结点的prior域指向头结点
tail = newNode; //尾指针tail进行跟进
}
else //如果不是第一个结点,利用跟进的尾指针tail与新结点建立指针域的互相联系即可
{
newNode->prior = tail;
tail->next = newNode;
tail = tail->next; //更新尾结点
}
}
}
// 查找节点(返回当前节点) //由于结点带有prior指针,所以可以轻松获取前驱结点的地址,所以返回所查找节点即可。
ListNode findNode(ListNode head, int data)
{
ListNode current = head->next; // 跳过头结点
while (current)
{
if (current->data == data)
return current;
else
current = current->next;
}
return nullptr;
}
// 删除节点
void deleteNode(ListNode &head)
{
cout << "请输入待删除的节点数据: ";
int data = 0;
cin >> data;
ListNode current = findNode(head, data);
ListNode prev = current->prior; //prev为待删除结点的前驱结点
ListNode next = current->next; //next为待删除结点的后继结点
if (next == nullptr) //如果后继结点为空,则待删除结点为尾结点
{
prev->next = next; //让前驱结点指向后继结点(此时next为nullptr);
delete current; //删除当前节点即可
}
else //若不为空,则为正常情况,由于设置了头结点,所以直接删除即可
{
prev->next = next; //让前驱结点的next域指向后继结点
next->prior = prev; //让后继结点的prior域指向前驱结点
delete current; //删除被架空的当前结点
}
}
// 插入节点
void insertNode(ListNode &head)
{
cout << "请输入待插入的位置(前) : ";
int data = 0;
cin >> data;
ListNode current = findNode(head, data);
ListNode newNode = creatNode();
newNode->prior = current->prior; //让新结点的prior指向当前结点的前驱结点,开始逐步建立联系
newNode->next = current; //将新结点的next指向所查找到的结点前
current->prior->next = newNode; //让当前结点的前驱结点的next指向新结点
current->prior = newNode; // 让当前结点的prior指向新结点
}
// 销毁链表
void destroyList(ListNode head)
{
ListNode current = head;
while (head != nullptr)
{
current = head->next;
delete head; //从头结点进行销毁,直到道尾节点为空,到最后head被置为空
head = current;
}
}
// 遍历链表
void printList(ListNode head)
{
ListNode current = head->next; //跳过头结点
while (current != nullptr)
{
cout << current->data << " ";
current = current->next;
}
}
int main()
{
ListNode head = new Node; //头结点
creatLinkList(head, 3);
printList(head);
insertNode(head);
printList(head);
deleteNode(head);
printList(head);
destroyList(head);
head = nullptr; //由于原始指针依旧指向的为被销毁后的头结点,为避免悬挂指针,将head置空,由于没有在销毁函数中在销毁结点后进行置空操作,所以需要在此进行置空操作
return 0;
}
六:单向循环链表
循环链表,顾名思义,该链表可以抽象性的想象成绕成一个圆圈。与单/双向链表不同的是,单向循环链表的尾结点的next域不再为空,而是指向链表的头结点,形成一个闭环。因为单向循环链表的指针域是没有为空的情况的,所以如果链表为空时,头结点的next域是指向本身的,即head->next = head;所以在判断链表是否为空时,只需要判断头结点next域是否指向自己即可。
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node *next;
} *ListNode;
// 创建节点
ListNode creatNode()
{
ListNode newNode = new Node;
cout << "请输入数据 : ";
int data = 0;
cin >> data;
newNode->data = data;
newNode->next = nullptr;
return newNode;
}
// 创建链表
void creatLinkList(ListNode head, int n)
{
head->next = head; // 初始化头结点的next域指向自身
ListNode tail = nullptr; // 将尾结点初始化为空,准备进行跟进
for (int i = 0; i != n; ++i)
{
ListNode newNode = creatNode();
if (head->next == head) // 如果指向自身,说明为newNode为第一个结点
{
head->next = newNode; // 将头结点与新结点相连
newNode->next = head; // 尾结点的next域始终指向头结点,所以每当创建新结点是要进行更新
tail = newNode;
}
else // 不是第一个结点,利用尾结点的跟进依次在后面进行链接
{
tail->next = newNode;
newNode->next = head;
tail = tail->next;
}
}
}
// 查找
ListNode findPreNode(ListNode head, int data)
{
ListNode current = head->next; // 跳过头结点
ListNode prev = head; // 因为是单向循环链表,虽为循环,但还是单项,即要增设一个指针记录前驱结点的地址
while (current != head) // 若current指向头结点,即说明已经把链表遍历了一遍
{
if (current->data == data)
return prev;
current = current->next; // prev指针始终比current指针满一步,即current找到时,返回prev就行
prev = prev->next;
}
cout << "未找到数据!!!" << endl;
return nullptr; // 遍历一遍后,若没有找到数据,即返回nullptr;
}
// 删除节点
void deleteNode(ListNode head)
{
cout << "请输入待被删除的数据: ";
int data = 0;
cin >> data;
ListNode prev = findPreNode(head, data); // 查找操作返回的是前驱结点,用prev保存
ListNode toDelete = prev->next; // 确定待删除结点位置
ListNode next = toDelete->next; // 用next记录待删除节点的后继结点
prev->next = next; // 架空待删除结点
delete toDelete;
}
// 插入节点 与单链表的插入类似,不做赘述
void insertNode(ListNode head)
{
cout << "请输入插入在哪个数据前面: ";
int data = 0;
cin >> data;
ListNode prev = findPreNode(head, data);
ListNode newNode = creatNode();
ListNode next = prev->next; // 这里next为待插入结点的后继结点
prev->next = newNode;
newNode->next = next;
}
// 销毁链表
void destroyList(ListNode &head)
{
// 如果链表为空,直接返回
if (head == nullptr)
return;
// 若链表只有一个头结点,删除后将head置空就行
if (head->next == head)
{
delete head;
head = nullptr;
return;
}
ListNode current = head; // 利用current对链表进行遍历
ListNode nextNode = nullptr; // 初始化下一结点nextNode为空
while (current->next != head)
{
nextNode = current->next; // 更新newNode,始终在删除当前节点前获取下一结点位置,这样才能找到
delete current;
current = nextNode; // 更新current
}
head = nullptr;
}
// 判断链表是否为空
bool emptyList(ListNode head)
{
if (head->next == head)
{
cout << "该链表为空!!!" << endl;
return true;
}
else
{
cout << "该链表不为空!!!" << endl;
return false;
}
}
// 打印链表
void printList(ListNode head)
{
ListNode current = head->next; // 跳过头结点
while (current != head) // 遍历一圈
{
cout << current->data << " ";
current = current->next;
}
cout << endl;
}
int main()
{
ListNode Head = new Node;
creatLinkList(Head, 3);
printList(Head);
deleteNode(Head);
printList(Head);
insertNode(Head);
printList(Head);
emptyList(Head);
deleteNode(Head);
deleteNode(Head);
deleteNode(Head);
emptyList(Head);
destroyList(Head);
// Head = nullptr; //由于传递的是引用,所以在destroyList()函数中对头节点删除后进行了置空操作,此时外部的Head也会受到同样的改变,即为空,不需要在次置空
return 0;
}
七:双向循环链表
双向循环链表,结点之间关系是双向的,整个链表也是可循环的,该链表的头结点的prior指针指向尾结点,而尾结点的next指针指向头结点。
#include <iostream>
using namespace std;
typedef struct Node
{
int data;
Node *prior;
Node *next;
} *ListNode;
// 创建节点
ListNode creatNode()
{
cout << "请输入新节点数据: ";
int data = 0;
cin >> data;
ListNode newNode = new Node;
newNode->data = data;
newNode->next = nullptr;
newNode->prior = nullptr;
return newNode;
}
// 创建链表
void creatLinkList(ListNode head, int n)
{
head->next = head; // 老样子,初始化链表的头结点的next域指向自己
head->prior = nullptr; // 将头结点的prior置为空,等待新结点加入进行更新
ListNode tail = nullptr;
for (int i = 0; i != n; ++i)
{
ListNode newNode = creatNode();
if (head->next == head) // 判断链表是否为空,如果为空,即为创建的第一个结点
{
head->next = newNode; // 头结点的next指针指向新结点
head->prior = newNode; // 头结点的prior指针指向新结点
newNode->prior = head; // 新结点的prior指针指向头结点
newNode->next = head; // 新结点的next指针指向头结点
// 此时该两个结点既是双向的,也是循环的
tail = newNode;
}
else
{
// 利用尾指针进行两个结点相互的链接
tail->next = newNode;
newNode->prior = tail;
newNode->next = head; // 将尾结点的next域指向头结点
tail = tail->next;
head->prior = newNode; // 不要忘了最后要把头结点的prior域指向新结点
}
}
}
// 查找节点
ListNode findNode(ListNode head, int data)
{
// 由于是双向,可以不用刻意返回前驱节点
ListNode current = head->next; // 跳过头结点
while (current != head)
{
if (current->data == data)
return current;
else
current = current->next;
}
return nullptr; // 没找到则返回空
}
// 删除节点
void deleteNode(ListNode head)
{
cout << "请输入待删除的节点数据:";
int data = 0;
cin >> data;
ListNode current = findNode(head, data);
ListNode prev = current->prior; // 待删除结点的前驱结点
ListNode next = current->next; // 待删除结点的后继结点
prev->next = next;
next->prior = prev;
// 架空当前结点(即待被删除结点)
delete current;
}
// 插入节点
void insertNode(ListNode head)
{
cout << "请输入在哪个数据之前插入:";
int data = 0;
cin >> data;
ListNode current = findNode(head, data);
ListNode newNode = creatNode();
ListNode prev = current->prior;
newNode->next = current;
current->prior = newNode;
newNode->prior = prev;
prev->next = newNode;
}
// 销毁链表
void destroyList(ListNode head)
{
// 如果链表为空,直接返回
if (head == nullptr)
return;
// 若链表只有一个头结点,删除后将head置空
if (head->next == head)
{
delete head;
head = nullptr;
return;
}
ListNode current = head; // 利用current对链表进行遍历
ListNode nextNode = nullptr; // 初始化下一结点nextNode为空
while (current->next != head)
{
nextNode = current->next; // 更新newNode,始终在删除当前节点前获取下一结点位置,这样才能找到
delete current;
current = nextNode; // 更新current
}
}
// 打印节点数据
void printNode(ListNode head)
{
ListNode current = head->next; // 跳过头结点
do
{
// 也是一种写法,先打印一遍,在更新结点进行循环判断
cout << current->data << " ";
current = current->next;
} while (current != head);
cout << endl;
}
// 判断链表是否为空
bool emptyList(ListNode head)
{
if (head->next == head)
{
cout << "该链表为空!!!" << endl;
return true;
}
else
{
cout << "该链表不为空!!!" << endl;
return false;
}
}
int main()
{
ListNode Head = new Node;
cout << "请输入链表数据个数: ";
int n = 0;
cin >> n;
creatLinkList(Head, n);
printNode(Head);
deleteNode(Head);
printNode(Head);
insertNode(Head);
printNode(Head);
deleteNode(Head);
deleteNode(Head);
deleteNode(Head);
emptyList(Head);
destroyList(Head);
Head = nullptr; //注意我在销毁函数中虽然将分配的堆空间进行了删除,但是并没有改变Head指针,所以我需要将Head置空
return 0;
}
八:各链表的优缺点及选择优先级
(1)单向链表
优点:
- 操作简便:只需要对各结点的next指针进行操作即可。
- 高效的插入和删除:在链表中插入和删除节点时,只需要改变相关节点的指针域即可,不需要移动大量的数据,因此效率较高。尤其是在链表中间或头部插入、删除节点时,比数组等数据结构更为高效。
- 内存利用率高:链表可以充分利用计算机的内存空间,不要求物理上的连续存储,只要逻辑上连续即可。
缺点:
- 访问效率低:访问链表中的节点需要从头节点开始遍历,直到找到所需的节点,因此访问效率较低。特别是在链表较长且需要频繁访问节点时,这一缺点更为明显。
(2)双向链表
优点:
- 双向访问:双向链表中的节点既包含了指向下一个节点的指针,也包含了指向前一个节点的指针,因此可以从任意一个节点开始向前或向后遍历链表。
- 高效的插入和删除:与单向链表类似,双向链表在插入和删除节点时也非常高效,只需要改变相关节点的指针域即可。而且由于存在前驱指针,使得在删除节点时可以更快地找到前驱节点,从而进一步提高了效率。
缺点:
- 空间开销大:相比于单向链表,双向链表中的每个节点需要额外存储一个指向前一个节点的指针,因此空间开销更大。
- 实现复杂:由于双向链表需要同时维护两个方向的指针,因此在实现时需要考虑更多的边界条件和错误处理情况,使得实现相对复杂。
(3)单向循环链表
优点:
- 循环访问:单向循环链表的最后一个节点指向头节点,形成了一个环,因此可以从任意一个节点开始遍历整个链表,并最终回到起始节点。这种特性在某些场景下非常有用,比如实现循环队列等。
- 节省空间:在某些情况下,单向循环链表可以节省一个指针的空间。例如,在实现循环队列时,可以通过判断尾节点的下一个节点是否为头节点来判断队列是否已满,而不需要额外设置一个表示队列状态的变量。
缺点:
- 访问效率低:与单向链表类似,单向循环链表在访问节点时也需要从头节点开始遍历,因此访问效率较低。
- 实现复杂:由于需要处理循环的情况,因此在实现时需要特别注意指针的指向和遍历的结束条件等问题。
(4)双向循环链表
优点:
- 双向访问和循环访问:双向循环链表结合了双向链表和单向循环链表的优点,既可以从任意一个节点开始向前或向后遍历链表,也可以形成一个环进行循环访问。这种特性使得双向循环链表在需要频繁访问和修改链表节点的场景下非常有用。
- 高效的操作:双向循环链表在插入、删除和访问节点时都非常高效。特别是在需要同时访问前驱节点和后继节点的场景下,双向循环链表的优势更加明显。
缺点:
- 空间开销大:与双向链表类似,双向循环链表中的每个节点需要额外存储两个指针(一个指向前一个节点,一个指向后一个节点),因此空间开销相对较大。
- 实现复杂:由于需要同时维护两个方向的指针和循环的特性,因此在实现双向循环链表时需要特别注意指针的指向和遍历的结束条件等问题,使得实现相对复杂。
优先级
对于这四种链表来说并没有绝对的优先级,我们需要根据上述各个链表的优缺点进行有效的排除与选择,所以在这里只说明适用场景。
适用场景
①单向链表:
- 当只需进行单向遍历,且不需要频繁访问前驱节点时。
- 链表长度较短,或者对时间复杂度要求不是特别高时。
②双向链表:
- 需要频繁访问前驱节点或进行双向遍历的场合。
- 链表操作频繁,且对时间复杂度有较高要求的场景。
③单向循环链表:
- 需要从链表的任意位置开始遍历整个链表的场景。
- 链表操作相对简单,但希望提高遍历灵活性的场合。
④双向循环链表:
- 需要频繁进行双向遍历,且对时间复杂度有较高要求的场景。
- 链表操作复杂,且需要快速访问前驱和后继节点的场合。
九:总结(必看!!!)
①关于链表的实现,一百个人大概有一百种写法,我写了很多遍链表,但是没有一遍是代码相同的。虽然代码不同,但是内核不会变,所以一定要理解,并不是对着别人的代码一顿乱记,等理解了之后自己也可以写出来,不理解的话很快就会忘掉的。
②对于链表的操作实际上就是处理链表中结点与结点之间的关系,改变结点的next指向,复杂一点也就是改变prior和next的指向。处理好这些细节,链表就没有问题了。在实现链表的途中,想象力不好的一定要画图辅助,跟着图走逻辑不会乱,等熟练了之后就可以用脑子想象代替画图了。
③关于函数参数传递的问题,一级指针可以对普通变量的值进行修改,如果想修改一级指针的值或间接性的需要修改一级指针所指向的内容时,则需要二级指针,一般最多只会用到二级指针,三级指针很罕见。在c++中,可以选择对指针类型的变量进行引用,可以达到同样的效果,注意我在这四种链表的销毁操作和在main()函数中对于头指针的处理以及在main()中在已经调用了销毁函数下是否选择再次将head置空的注释。
④链表是很多复杂数据结构的基础,如树,图等,学不好链表,学习其他复杂的数据结构时将更为困惑。因此,链表与数组一样,是重中之重。
⑤大家加油!