前面两篇主要介绍基础数据结构中的数组及其变种,本篇开始学习另一个重要的基础数据结构——链表(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.
单链表是一个动态分配存储空间的存储容器,其最大的特点就是:每一个元素都包含一个指向下一个元素的指针。单链表在逻辑上是线性的,在存储空间上(物理上)是非连续的;与之对应的数组,在逻辑上是线性的,在存储空间上是连续的,即顺序表。
单链表的变种很多,以下是几种最常见的变种。
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进行实现。如下图:
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
此外,关于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,参考:点击打开链接
此外还可以参考维基百科。