基础数据结构之数组和链表(三)

         前面两篇主要介绍基础数据结构中的数组及其变种,本篇开始学习另一个重要的基础数据结构——链表(Linked List)。链表有很很多种,主要可以分为单链表(Singly-Linked List)和双链表(Doubly-Linked List)以及循环链表(Circle Linked List)。本篇主要介绍单链表及其变种。

一、结构设计

    A singly-linked list is simply a sequence of dynamically allocated storage elements, each containing a pointer to its successor. 

        单链表是一个动态分配存储空间的存储容器,其最大的特点就是:每一个元素都包含一个指向下一个元素的指针。单链表在逻辑上是线性的,在存储空间上(物理上)是非连续的;与之对应的数组,在逻辑上是线性的,在存储空间上是连续的,即顺序表。

        单链表的变种很多,以下是几种最常见的变种。

基础数据结构之数组和链表(三)_第1张图片基础数据结构之数组和链表(三)_第2张图片

Figure: Single-linked List Variations (sentinel)                                               Figure: Empty Single-linked List


1,图a展示的是最基本的一种单链表,它只有一个field,即head指针,用于标记链表的开始节点。它的尾节点的next指针指向NULL,表示链表的尾端。这种链表的缺点是:在链表尾部插入新节点的操作,需要先遍历(traverse)整个链表,找到尾节点,然后再执行add操作,因此效率比较低。

2,图b是对图a的改进,增加了一个tail指针,指向链表的尾节点,提升了在链表尾部插入新节点的效率。

3,图c所示的单链表,设计了一个sentinel节点。sentinel是一个空节点,始终占据链表的首位,并一直存在链表中(即使是空链表),它本身不存储数据。sentinel节点的设计,实际上是一种编程技巧,它可以简化链表的某些操作。比如:由于sentinel是链表的首节点,所以在操作链表的时候,无需修改head指针。此外,该链表的尾节点的next指针不再指向NULL,而是指向sentinel,它是一个循环链表。

PS:理解sentinel与head的区别,sentinel是一个节点(元素),可以作为链表的第一个节点,head是一个指针,指向链表的第一个节点。

4,图d将head指针与sentinel合并,它将head指针作为sentinel的next指针,节省了额外的空间。它以sentinel作为链表的入口(句柄),而不是head指针。

5,图e展示的是一个没有sentinel的循环指针,它使用一个tail指针标记链表的入口。循环链表的优点是:无论是在头部插入元素还是尾部插入元素,或者其他地方插入元素,编程上进行的操作都是相同的。


二、简单单链表的实现

本文选取链表b进行实现。如下图:

基础数据结构之数组和链表(三)_第3张图片


Figure: Memory Representation of Linked List Object

上图用两个结构来表述链表对象,链表和链表元素(节点)。其中,节点包括两个字段(field),数据域和指针域;链表也包括两个字段,头指针和尾指针。

代码如下:

#ifndef LINKED_LIST_H
#define LINKED_LIST_H

#include <stdexcept>

namespace FoundationalDataStructure
{
    // forward declaration
    template <typename T>
    class LinkedList;

    template <typename T>
    class Node
    {
    public:
        T const & Datum() const;
        Node const * Next() const;

        friend LinkedList<T>;

    private:
        T datum;
        Node * next;

        Node(T const &, Node *);
    };

    template <typename T>
    class LinkedList
    {
    public:
        LinkedList();
        ~LinkedList();

        LinkedList(LinkedList const &);
        LinkedList & operator=(LinkedList const &);

        Node<T> const * Head() const;
        Node<T> const * Tail() const;

        bool IsEmpty() const;
        T const & First() const;
        T const & Last() const;

        void Prepend(T const &);
        void Append(T const &);
        void Extract(T const &);
        void Purge();
        void InsertAfter(Node<T> const *, T const &);
        void InsertBefore(Node<T> const *, T const &);
    private:
        Node<T> * head;
        Node<T> * tail;
    };


    //////////////////////////////////////////////////Implementation/////////////////////////////////////////////////////////////////////
    template <typename T>
    Node<T>::Node(T const & _datum, Node * _next)
        : datum(_datum)
        , next(_next)
    {}

    template <typename T>
    T const & Node<T>::Datum() const
    {
        return datum;
    }

    template <typename T>
    Node<T> const * Node<T>::Next() const
    {
        return next;
    }

    template <typename T>
    LinkedList<T>::LinkedList()
        : head(NULL)
        , tail(NULL)
    {}

    template <typename T>
    void LinkedList<T>::Purge()
    {
        // The main loop of the Purge function simply traverses all the elements of linked list, deleting each of them one-by-one.
        while (NULL != head)
        {
            auto temp = head;
            head = head->next;    // move head pointer
            delete temp;
        }
        tail = NULL;
    }
    template <typename T>
    LinkedList<T>::~LinkedList()
    {
        Purge();
    }
    
    template <typename T>
    Node<T> const * LinkedList<T>::Head() const
    {
        return head;
    }

    template <typename T>
    Node<T> const * LinkedList<T>::Tail() const
    {
        return tail;
    }

    template <typename T>
    bool LinkedList<T>::IsEmpty() const
    {
        return NULL == head;
    }

    template <typename T>
    T const & LinkedList<T>::First() const
    {
        if (NULL == head)
            throw std::domain_error("List is empty");

        return head->datum;
    }

    template <typename T>
    T const & LinkedList<T>::Last() const
    {
        if (NULL == tail)
            throw std::domain_error("List is empty");

        return tail->datum;
    }

    template <typename T>
    void LinkedList<T>::Prepend(T const & item)
    {
        Node<T> * const temp = new Node<T>(item, head);
        if (NULL == head)
            tail = temp;    // tail = NULL; before this operation
        head = temp;    // move head pointer
    }

    template <typename T>
    void LinkedList<T>::Append(T const & item)
    {
        Node<T> * const temp = new Node<T>(item, NULL);
        if (NULL == head)
            head = temp;    // the first node
        else
            tail->next = temp;    // insert temp at the end of the list
        tail = temp;    // move tail to the end of the list
    }

    template <typename T>
    void LinkedList<T>::Extract(T const & item)
    {
        if (NULL == head)
            throw std::domain_error("list is empty");


        Node<T> * ptr = head;
        Node<T> * prevPtr = NULL;
        while (ptr != NULL && ptr->datum != item)
        {
            prevPtr = ptr;
            ptr = ptr->next;
        }
        if (ptr == NULL)
            throw std::invalid_argument("item not found");
        if (ptr == head)
            head = ptr->next;
        else
            prevPtr->next = ptr->next;    // bridget over ptr
        if (ptr == tail)
            tail = prevPtr;


        delete ptr;
        ptr = NULL;
    }

    template <typename T>
    LinkedList<T>::LinkedList(LinkedList const & linkedList)
        : head(NULL)
        , tail(NULL)
    {
        for (auto ptr = linkedList.Head(); ptr != NULL; ptr = ptr->next)
            Append(ptr->datum);
    }

    template <typename T>
    LinkedList<T> & LinkedList<T>::operator=(LinkedList const & linkedList)
    {
        if (&linkedList != this)
        {
            Purge();
            for (auto ptr = linkedList.Head(); ptr != NULL; ptr = ptr->next)
                Append(ptr->datum);
        }
        return *this;
    }

    template <typename T>
    void LinkedList<T>::InsertAfter(Node<T> const * arg, T const & item)
    {
        Node<T> * ptr = const_cast<Node<T>*> (arg);
        if (NULL == ptr)
            throw std::invalid_argument("invalid position");

        Node<T> * const temp = new Node<T>(item, ptr->next);
        ptr->next = temp;

        if (tail == ptr)
            tail = temp;
    }

    template <typename T>
    void LinkedList<T>::InsertBefore(Node<T> const * arg, T const & item)
    {
        Node<T> * ptr = const_cast<Node<T>*> (arg);
        if (NULL == ptr)
            throw std::invalid_argument("invalid position");

        Node<T> * const temp = new Node<T>(item, ptr);

        if (head == ptr)
            head = temp;
        else
        {
            auto prevPtr = head;
            while (prevPtr != NULL && prevPtr->next != ptr)
                prevPtr = prevPtr->next;

            if (NULL == prevPtr)
                throw std::invalid_argument("invalid position");

            prevPtr->next = temp;
        }
    }

} // namespace FoundationalDataStructure
#endif // LINKED_LIST_H

注:嵌套类,推荐使用friend,友元类,减少函数调用,提升性能。

此外,关于Extract(T const & item)函数,如果链表中有几个相等值的元素,则只会删除第一个。(可以在链表头和链表尾各插入一个相同的值进行测试)

现改进该函数如下:

    template <typename T>
    void LinkedList<T>::Extract(T const & item)    // pass by reference or value ?
    {
        if (NULL == head)
            throw std::domain_error("list is empty");

        unsigned int count = 0;
        Node<T> * ptr = head;
        Node<T> * prevPtr = NULL;
        while (ptr != NULL)
        {
            if (ptr->datum == item)
            {
                ++count;
                auto temp = ptr;    // backup

                if (ptr == head)
                {
                    if (ptr == tail)
                        tail = NULL;

                    ptr = ptr->next;
                    head = ptr;
                }
                else
                {
                    if (ptr == tail)
                        tail = prevPtr;

                    prevPtr->next = ptr->next;    // bridget over ptr
                    ptr = ptr->next;                // move ptr
                }

                delete temp;
                temp = NULL;
                continue;    // break; for the first one
            }
            prevPtr = ptr;
            ptr = ptr->next;
        }

        if (0 == count)
            throw std::invalid_argument("item not found");
    }

注:该函数的实现还是比较复杂,后续将展示带sentinel的链表,无需区分head,能极大简化类似操作。

PS:

        在第一次实现该函数的时候,我尝试采用for循环来遍历,一直未成功,后来才采用while循环来遍历。此后某日,在学习C++11的基于范围的for循环时,获知“使用for循环遍历容器时,不能在迭代过程中改变容器(添加或删除元素)”,幡然大悟。故此,在选择循环结构时,需要注意for循环与while循环的能力范围。

三、扩展

单链表还有其他变种,如上面结构图中的c、d、e。

1,参考:A Singly linked list with a sentinel implementation source code

该文是用C语言实现的一个sentinel结构体,包含head和tail以及其他简化运算的信息。

2,参考:linked-list implementation sentinel

改为演示了头尾各一个sentinel elements的linked list结构图。

3,参考:点击打开链接

此外还可以参考维基百科。


你可能感兴趣的:(基础数据结构之数组和链表(三))