一、链表的基本概念
链表(Linked List)是一种物理存储单元上非连续的、非顺序的线性数据结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点(链表中每一个元素称为节点)组成,节点可以在运行时动态生成。每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的指针域。相比于线性数组,链表的好处在于不需要事先分配固定大小的存储空间,并且在插入和删除数据时不需要移动其他元素。
最适合比喻链表的就是火车了,如下图所示。
二、链表的类型
C++中常见的链表类型包括单向链表、双向链表和循环链表。
1. 单向链表(Singly Linked List)
单向链表是最简单的链表结构,每个节点只包含一个指向下一个节点的指针。单向链表只能从头节点开始顺序访问到尾节点。
示例:在链表头部插入节点,遍历链表并打印节点的数据,代码如下。
#include <iostream>
// 定义单向链表的节点结构体
struct Node {
int data; // 数据域
Node* next; // 指针域,指向下一个节点
// 构造函数,用于初始化节点
Node(int data) : data(data), next(nullptr) {}
};
// 单向链表类
class SinglyLinkedList {
private:
Node* head; // 头节点指针
public:
SinglyLinkedList() : head(nullptr) {}
// 在链表头部插入节点
void insertAtBeginning(int data) {
Node* newNode = new Node(data);
newNode->next = head;
head = newNode;
}
// 遍历链表并打印节点的数据
void printList() {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}
// 销毁链表
~SinglyLinkedList() {
Node* temp;
while (head != nullptr) {
temp = head;
head = head->next;
delete temp;
}
}
};
int main() {
SinglyLinkedList list;
list.insertAtBeginning(5);
list.insertAtBeginning(3);
list.insertAtBeginning(1);
list.printList();
return 0;
}
结果如下所示:
2. 双向链表(Doubly Linked List)
双向链表包含两个指针,一个指向前一个节点,另一个指向下一个节点。这使得双向链表可以从任何一个节点向前或向后遍历。
示例:在链表头部插入节点,遍历链表并打印节点的数据,代码如下。
#include <iostream>
// 定义双向链表的节点结构体
struct Node {
int data; // 数据域
Node* prev; // 指针域,指向前一个节点
Node* next; // 指针域,指向下一个节点
// 构造函数,用于初始化节点
Node(int data) : data(data), prev(nullptr), next(nullptr) {}
};
// 双向链表类
class DoublyLinkedList {
private:
Node* head; // 头节点指针
public:
DoublyLinkedList() : head(nullptr) {}
// 在链表头部插入节点
void insertAtBeginning(int data) {
Node* newNode = new Node(data);
if (head != nullptr) {
head->prev = newNode;
}
newNode->next = head;
head = newNode;
}
// 遍历链表并打印节点的数据
void printList() {
Node* temp = head;
while (temp != nullptr) {
std::cout << temp->data << " ";
temp = temp->next;
}
std::cout << std::endl;
}
// 销毁链表
~DoublyLinkedList() {
Node* temp;
while (head != nullptr) {
temp = head;
head = head->next;
if (temp->next != nullptr) {
temp->next->prev = temp->prev;
}
delete temp;
}
}
};
int main() {
DoublyLinkedList list;
list.insertAtBeginning(5);
list.insertAtBeginning(3);
list.insertAtBeginning(1);
list.printList();
return 0;
}
在上述2个示例中,我们定义了节点结构体,并为单向链表和双向链表创建了类。这些类包含了插入节点到链表头部、遍历链表并打印节点数据以及销毁链表的方法。对于双向链表,我们还需要处理`prev`指针以确保链表结构的正确性。
请注意,以上代码只是一个简单的示例,并没有处理错误情况和异常。在实际应用中,你可能需要添加额外的错误检查和边界情况处理,例如当插入节点失败时返回错误码或抛出异常。此外,对于大型数据集,你可能还需要考虑性能优化和内存管理。
3. 循环链表(Circular Linked List)
循环链表与单向链表和双向链表的主要区别在于,循环链表的尾节点指向头节点,形成一个环状结构。这使得循环链表可以从任何节点开始遍历整个链表。循环链表是链表结构的一个变种,它的特点是链表的最后一个节点指向链表的第一个节点,形成一个环状结构。在实际开发中,循环链表常用于实现某些特殊的数据结构,如循环队列、循环双端队列等。
我们举一个实例:假设我们正在开发一个音乐播放器应用,需要实现一个音乐播放列表,这个播放列表支持添加、删除、循环播放音乐的功能。循环链表非常适合用来实现这样的播放列表。
首先,我们定义一个循环链表的节点类,每个节点包含一个音乐信息和一个指向下一个节点的指针。代码如下。
#include <iostream>
#include <string>
class MusicNode {
public:
std::string title; // 音乐标题
MusicNode* next; // 指向下一个节点的指针
MusicNode(const std::string& title) : title(title), next(nullptr) {}
};
接着,我们定义一个循环链表类,包含插入、删除、遍历等基本操作。代码如下。
class CircularMusicPlaylist {
private:
MusicNode* head; // 头节点
int size; // 链表大小
public:
CircularMusicPlaylist() : head(nullptr), size(0) {}
~CircularMusicPlaylist() {
clear();
}
// 插入音乐到播放列表
void addMusic(const std::string& title) {
MusicNode* newNode = new MusicNode(title);
if (isEmpty()) {
head = newNode;
newNode->next = head; // 构成循环
} else {
MusicNode* current = head;
while (current->next != head) {
current = current->next;
}
current->next = newNode;
newNode->next = head; // 新节点指向头节点,形成闭环
}
size++;
}
// 从播放列表中删除音乐
void removeMusic(const std::string& title) {
if (isEmpty()) {
return;
}
MusicNode* prev = nullptr;
MusicNode* current = head;
do {
if (current->title == title) {
if (prev == nullptr) { // 删除的是头节点
while (current->next != head) {
current = current->next;
}
head = head->next;
delete current;
head->next = head; // 更新头节点的指针
} else {
prev->next = current->next;
if (current == head) {
head = prev->next;
}
delete current;
}
size--;
return;
}
prev = current;
current = current->next;
} while (current != head); // 遍历整个链表
std::cout << "Music not found in the playlist." << std::endl;
}
// 遍历播放列表
void play() {
if (isEmpty()) {
std::cout << "Playlist is empty." << std::endl;
return;
}
MusicNode* current = head;
do {
std::cout << "Playing: " << current->title << std::endl;
current = current->next;
} while (current != head); // 循环播放
}
// 判断播放列表是否为空
bool isEmpty() const {
return head == nullptr;
}
// 清除播放列表
void clear() {
MusicNode* current = head;
while (current != nullptr) {
MusicNode* toDelete = current;
current = current->next;
delete toDelete;
}
head = nullptr;
size = 0;
}
// 获取播放列表大小
int getSize() const {
return size;
}
};
现在,我们可以使用这个循环链表播放列表了。代码如下。
int main() {
CircularMusicPlaylist playlist;
// 添加音乐到播放列表
playlist.addMusic("Song 1");
playlist.addMusic("Song 2");
playlist.addMusic("Song 3");
// 打印播放列表大小
std::cout << "Playlist size: " << playlist.getSize() << std::endl;
// 播放整个播放列表
playlist.play();
// 删除一个歌曲
playlist.removeMusic("Song 2");
// 再次打印播放列表大小
std::cout << "Playlist size after removal: " << playlist.getSize() << std::endl;
// 再次播放整个播放列表
playlist.play();
// 清除播放列表
playlist.clear();
// 检查播放列表是否为空
if (playlist.isEmpty()) {
std::cout << "Playlist is now empty." << std::endl;
}
return 0;
}
上述代码中,我们首先创建了一个`CircularMusicPlaylist`对象,然后向其中添加了几首歌曲。我们打印了播放列表的大小,并播放了整个播放列表。之后,我们删除了一个歌曲,并再次打印了播放列表的大小。最后,我们清除了播放列表,并检查它是否为空。
通过实现一个音乐播放列表,我们展示了循环链表的基本操作,包括添加元素、删除元素、遍历元素以及检查链表是否为空。在实际应用中,循环链表还可以用于实现各种需要循环访问数据结构的场景,如循环队列、环形缓冲区等。