链表
数据结构之链表
什么是链表
链表(Linked List)是一种常用的数据结构,用于存储一系列元素。与数组不同,链表中的元素不必在内存中连续存储,每个元素包含一个指向下一个元素的指针,通过这些指针,就可以将一组零散的内存块串联起来,形成一个链式结构。
链表通常包括头指针和节点。头指针指向链表的第一个节点,而节点包含两部分信息:数据和指针。其中数据部分用于存储实际的数据,指针部分则指向下一个节点。最后一个节点的指针指向 NULL,表示链表的末尾。
链表的类别
- 单向链表
- 双向链表
- 循环链表
实现链表
单向链表
首先定义基础结构:
template<typename T>
struct LinkNode{
T data;
LinkNode* next;
}
再定义一个链表类,用于实现线性表:
template <class _Tp>
class Link{
public:
Link();
~Link();
void pop_back();
void push_back(const _Tp& val);
bool insert(size_t npos, const _Tp& val);
private:
LinkNode<_Tp>* head_;
size_t size_;
};
接下来,看看我们如何插入一个数据到链表中:
template <class DataType> void Link<DataType>::push_back(const DataType& val)
{
LinkNode<DataType>* node = new LinkNode<DataType>();
node->next = nullptr;
node->data = val;
if (this->head_ == nullptr){
this->head_ = node;
}
else{
for (auto curnode = this->head_;
curnode->next != null;
curnode = curnode->next);
curnode->next = node;
}
++this->size_;
}
由以上代码可以看出, push_back
时间复杂度是O(n)。 因其需要遍历到一个后继节点为空的指针。但如果我们在Link
类中添加一个尾指针tail_
,那么push_back
的效率将提升至O(1)。
那在指定位置插入元素呢?
template <class DataType>
bool Link<DataType>::insert(size_t npos, const DataType& val)
{
if (npos > size_){
return false;
}
LinkNode<DataType>* node = new LinkNode<DataType>();
node->next = nullptr;
node->data = val;
if (this->head_ == nullptr){
this->head_ = node;
}else{
LinkNode<DataType>* curnode = this->head_;
for (size_t i = 1; i < npos; ++i){
curnode = curnode->next;
}
if (npos == 0){
node->next = this->head_;
this->head_ = node;
}else{
node->next = curnode->next;
curnode->next = node;
}
}
++this->size_;
return true;
}
可知, 插入到头部只需要 O(1) 的时间复杂度, 而其他位置为 O(n)。 同理, 如果我们在Link
类中添加一个尾指针tail_
,那么push_back
的效率将提升至 O(1)。
由插入的例子可知, 查找和删除首尾节点的时间复杂度要优于访问中间节点。此处将不再代码展示了。
双向链表
双向链表的节点定义如下:
template<typename T>
struct LinkNode{
T data;
LinkNode* previous;
LinkNode* next;
}
由上可知, 节点中多了一个previous
的指针, 便于查找上一个元素。 在单向链表中, 如果要访问上一个元素, 需要从头部开始遍历。而双向链表, 只要获取到任意节点的指针, 即可获取到上下节点的值。
双向链表在删除一个元素时无需遍历, 只需要修改当前节点, 让上一个元素指向下一个元素, 让下一个元素指向上一个元素, 如下所示:
template <class DataType>
bool Link<DataType>::remove(LinkNode<DataType>* node)
{
if(node == nullptr){
return false;
}
if (node->next != nullptr){
node->next->previous = node->previous;
}
if (node == this->tail_){
this->tail_ = node->previous;
if(this->tail_!= nullptr)
this->tail_->next = nullptr;
}
else
node->previous = node->next;
if(node == this->head_){
this->head_ = node->next;
if(this->head_ != nullptr)
this->head_->previous = nullptr;
}
--this->size_;
delete node;
return true;
}
由以上代码可知, 仅仅只需O(1) 的时间即可做删除操作。
循环链表
循环链表即链表尾指针指向了链表头部,形成一个环状。多用于循环队列。 本篇幅仅仅针对以循环单链表实现队列作为用例展开。
首先定义一个节点结构:
template <typename DataType>
struct LinkNode{
DataType data;
LinkNode* next;
};
再定义一个循环链表结构:
template <typename DataType>
class CircledList
{
public:
explicit CircledList(size_t capacity=0);
~CircledList();
size_t size() const;
bool empty() const;
bool isfull() const;
LinkNode<DataType>* top();
void pop();
void push(const DataType& val);
private:
LinkNode<DataType>* tail_;
size_t size_;
size_t capacity_;
};
由上可知, 该循环单链表尾指针表示法, 而非头指针。其优势在于访问尾指针与头指针都只需O(1) 的时间复杂度。头指针表示法访问队首时间复杂度为O(1) ,而访问链表尾部则需要O(n)。
尾指针表示法为什么可以以O(1)的时间复杂度访问头部, 主要在于其next指向了头部。
下面代码示例如何入队:
template <class DataType>
void CircledList<DataType>::push(const DataType& val)
{
LinkNode<DataType>* node = new LinkNode<DataType>();
node->next = nullptr;
node->data = val;
if (this->tail_ == nullptr){
this->tail_ = node;
this->tail_->next = node;
}
else{
node->next = this->tail_->next;
this->tail_->next = node;
this->tail_ = node;
}
++this->size_;
}
不用过多解释, 其特殊在于当tail_为nullptr时, 其next
指向本身。
那如何出队呢?
template <class DataType>
void CircledList<DataType>::pop()
{
if (this->tail_ == nullptr){
return;
}
auto head = this->tail_->next;
this->tail_->next = head->next;
if(head == this->tail_){
this->tail_ = nullptr;
}
--size_;
delete head;
}
此处需要注意的是, 当this->tail_next==this->tail_
时表示当前出队的为队尾元素, 需要置空操作。
最后展示如何访问队首:
template <class DataType>
LinkNode<DataType>* CircledList<DataType>::top()
{
if (this->tail_ == nullptr){
return nullptr;
}
return this->tail_->next;
}
总结
至此, 链表就研究完了。 链表用途广泛, 可用以实现线性表、栈、队列等诸多数据结构。
在链表中插入和删除节点时,需要更新节点的指针域,保证链表的连续性和正确性。
链表的插入和删除操作比较高效,但是访问链表中的特定节点需要从头节点开始遍历,效率较低。因此,在实现链表时需要考虑数据的访问模式和性能需求,选择合适的数据结构。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?