链表
链表是由一系列称为表的结点的对象组成的。它可以动态分配存储空间,解决了数组的静态分配存储空间的一些弊端。在一些应用中,常采用动态分配静态化的思想为链表分配存储空间,这种技术称为“存储池”。常见的链表类型有:单链表、双链表、循环链表,本文将对三者做详细介绍。
单链表
实现一个链表首先需要定义链表的“零件”——Link类
template <typename E> class Link
{
public:
E element;
Link *next;
Link(const E& elemval, Link* nextval = NULL)
{ element = elemval; next = nextval; }
Link(Link* nextval = NULL)
{ next = nextval; }
}
此为单链表的Link类:包含一个存储元素值的element域和一个存储表中下一结点指针的next域。它的构造函数有两种形式,一个函数有初始化元素的值,另一个则没有。
注:看上去Link类的数据成员声明为公有违反了封装原则,实际上Link类作为链表(或栈、队列)实现的一个私有类来实现,因此对程序其他部分是不可见的。
下面给出单链表类LList的实现
template <typename E> class LList : public List<E>
{
private:
Link<E>* head;
Link<E>* tail;
Link<E>* curr;
int cnt;
void init()
{
curr = tail = head = new Link<E>;
cnt = 0;
}
void removeall()
{
while (head != NULL)
{
curr = head;
head = head->next;
delete curr; //一切操作都是对当前位置的操作
}
}
public:
LList(int size = defaultSize) { init(); }
~LList() { removeall(); }
void print() const;
void clear() { removeall(); init(); }
// 关键函数实现
void insert(const E& it)
{
curr->next = new Link<E>(it, curr->next);
if (tail == curr) tail = curr->next; // New tail
cnt++;
}
void append(const E& it)
{
tail = tail->next = new Link<E>(it, NULL);
cnt++;
}
E remove()
{
Assert(curr->next != NULL, "No element")
E it = curr->next->element;
Link<E>* ltemp = curr->next;
if(tail == curr->next) tail = curr;
curr->next = curr->next->next;
delete ltemp;
cnt--;
return it;
}
void moveToStart()
{ curr = head; }
void moveToEnd()
{ curr = tail; }
void prev() //前驱
{
if(curr == head) return;
Link<E>* temp = head;
while(temp->next != curr) temp = temp->next;
curr->temp;
}
void moveToPos(int pos)
{
Assert((pos >= 0) && (pos <= cnt), "Position out of range");
curr = head;
for(int i = 0; i < pos; i++) curr = curr->next;
}
void next() //后继
{
if(curr != tail) curr = curr->next;
}
// 链表当前相关参数
int length() const { return cnt; }
int currPost() const
{
Link<E>* temp = head;
int i;
for (int i = 0; curr != temp; i++)
temp = temp->next;
return i;
}
const E& getValue() const //Return current element
{
Assert(curr->next != NULL; "No value");
return curr->next->element;
}
};
注:
- 为了加速对链表尾端的访问,特别为了允许append函数的执行时间为一常数值,名为tail的指针保存了链表的最后一链。
- 为了
操作的便利性
以及考虑对特殊情况的一致性处理
,本例的设计考虑了如下两个策略:
i) 让curr指针指向当前元素
的前一个元素
ii) 增加特殊的表头结点
- cnt存放线性表的长度,对链表的每个修改操作都要更新它们的值。
- 类LList包含两个私有成员函数:init和removeall。LList的构造函数、析构函数和clear函数都用到了这两个函数。
- 请额外注意体会
insert
和remove
的实现。
i) 在链表的当前位置插入一个新元素包括三个步骤。首先,要创建一个新的结点;其次,新结点的next域要指向当前结点;最后,重定向curr的next域至新结点。
ii) 从链表删除一个元素必须小心。不要“丢失”被删除结点的内存。此内存应该返回给存储器。因此,首先将要删除的指针指向临时指针ltemp,然后调用delete函数释放被删除结点占用的内存。
下面给出一个小结论帮助记忆:修改curr,使用temp指针作为辅助量;修改链表元素,使用curr指针作为辅助量。(分别参考prev和clear函数)
双链表
单链表只允许从一个表结点直接访问它的后继结点,而双链表存储了两个指针,使得双链表可以从一个表结点出发,在线性表中随意访问它的前驱结点和后继结点。双链表使用起来比单链表更加便捷,也更容易实现和调试,因此广受喜爱。它和单链表相比唯一的缺点就是使用更多的空间1。
类似于单链表,为了去除一些特殊情况以及简化操作,双链表的实现使用了不包含任何数据信息的头结点和尾结点。它们在双链表初始化时被创建,并用head和tail指针分别指向。
双链表Link类:
template <typename E> class Link
{
public:
E element;
Link* prev;
Link *next;
Link(const E& it, Link* prevp, Link* nextp)
{
element = it;
prev = prevp;
next = nextp;
}
Link( Link* prevp = NULL, Link* nextp = NULL)
{
element = it;
prev = prevp;
next = nextp;
}
}
双链表关键操作:
void insert(const E& it)
{
curr->next = curr->next->prev = new Link<E>(it, curr, curr->next);
//先长后短:先修改curr->next->prev,再修改curr->next
cnt++;
}
template<typename E>
void LList<E>:: append(const E& it)
{
tail->prev = tail->prev->next = new Link<E>(it, tail->prev, tail);
cnt++;
}
template<typename E>
E LList<E>:: remove()
{
if(curr->next==tail) return NULL;
E it = curr->next->element;
Link<E>* ltemp = curr->next;
/*先长后短:先修改curr->next->next->prev,再修改curr->next*/
curr->next->next->prev = curr;
curr->next = curr->next->next;
delete ltemp;
cnt--;
return it;
}
template<typename E>
void LList<E>:: prev()
{
if(curr != head)
curr = curr->prev;
}
这里再给出一个小结论: 双链表增删操作中,总要重定向原表中两个指针,且总是先修改"易丢失"的结点的指针(即没被curr指向的结点),在本示例代码中可以直观的体现为修改的先长后短原则(如下图)
注意,此结论同样适用于单链表的增删操作。
思考
考虑一种基于异或
的节省空间的方法,用来消除双链表额外的空间需求,尽管它会使实现变得复杂,运行速度稍微减慢。(这种方法是一个时空权衡的例子)
分析:
由 (L^R)^R = L, (L^R)^L = R 可知,给出两个值,并将它们异或,则其中任何一个值都可以由另外一个值与它们异或后的结果再异或而得到。因此,双链表可以通过在一个指针域中存储两个指针值的异或结果来实现。这样一来,一个结点的指针值可以由它的前驱结点指针值与该前驱的next域异或得到。因此,只要分解链接域就能顺着链表走下去,像拉开拉链一样。
注:这种方法的原理值得注意,计算机图形学中广泛利用了这一原理。(把屏幕上一个区域与一个矩形图像异或,使该区域的图像高亮,再次与矩形图像异或使之复原)
循环链表
循环链表是指链表中最后那个链结点的指针域存放指向链表最前面那个结点的指针,整个链表形成一个环。
只要给出任何一个结点的位置,则由此出发就可以访问表中其他所有结点。
template<class E>
void LList<E>:: LList(const int sz)
{
head = tail = curr = new link();
head->next = head;
cnt = 0;
}
template<class E>
void LList<E>::clear()
{
while(head->next != NULL)
{
curr = head->next;
head->next = curr->next;
delete curr;
}
tail = curr = head->next = head;
}
template<class E>
void LList<E>::append(const E& it)
{tail = tail->next = new link(it, head);}
template<class E>
void LList<E>::insert(const E& it)
{
assert(curr != NULL);
curr->next = new link(it, curr->next);
if(tail->next != head) tail = tail->next;
cnt++;
}
template<class E>
void LList<E>::next()
{curr = curr->next;}
template<class E>
void LList<E>::prev()
{
link* temp = curr;
while(temp->next != curr) temp = temp->next;
curr = temp;
}
总结
通过三种链表的学习,我们可以发现,链表的组成就是一个个Link对象,链表的操作无非为增删查改,代码编写过程中的需注意的特殊情况主要有是否为空、是否为首尾,删除时注意内存管理防止内存泄露。