链表

数据结构之链表

什么是链表

链表(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;
}

总结

至此, 链表就研究完了。 链表用途广泛, 可用以实现线性表、栈、队列等诸多数据结构。

在链表中插入和删除节点时,需要更新节点的指针域,保证链表的连续性和正确性。

链表的插入和删除操作比较高效,但是访问链表中的特定节点需要从头节点开始遍历,效率较低。因此,在实现链表时需要考虑数据的访问模式和性能需求,选择合适的数据结构。

posted @ 2023-07-13 10:31  汗牛充栋  阅读(28)  评论(0编辑  收藏  举报