1.简介
前面3.1的单链表在操作过程中有一个缺点,就是后面的节点无法直接找到前面的节点,这使很多操作都得从头到尾去搜寻节点,算法效率变得非常低,解决这个问题的方法就是重新定义链表的节点使每个节点有两个指针,一个指向前驱一个指向后驱,这就是双向链表。
节点定义
template<class T>
class DLLNode {
public:
DLLNode() {
next = prev = 0;
}
DLLNode(const T& el, DLLNode<T> *n = 0, DLLNode<T> *p = 0) {
info = el; next = n; prev = p;
}
T info;
DLLNode<T> *next, *prev;
};
3.1节中所定义方法的局限性是链表只能存储整数. 如果希望链表存储浮点数点数或者数组.就必须重新写一套类似的代码。然而,更好的做法是只对这样的类声明一次,并且不提前确定其中存储什么数据类型,在 C++中,用模板就能很方便地做到这一点 。 为了说明模板在链表处理中的用法, 从本节开始使用模版来定义链表,但链表操作的示例仍然采用存储整数的链表。
2.双向链表的操作
DoublyLinkedList类定义了双向链表的操作
template<class T>
class DoublyLinkedList {
public:
DoublyLinkedList() {
head = tail = 0;
}
void addToDLLTail(const T&);//
插入节点到双向链表结尾T deleteFromDLLTail();//
删除末尾的节点并返回其值
~DoublyLinkedList() {
clear();
}
bool isEmpty() const {
return head == 0;
}
void clear();
void setToNull() {
head = tail = 0;
}
void addToDLLHead(const T&);
T deleteFromDLLHead();
T& firstEl();
T* find(const T&) const;
protected:
DLLNode<T> *head, *tail;
friend ostream& operator<<(ostream& out, const DoublyLinkedList<T>& dll) {
for (DLLNode<T> *tmp = dll.head; tmp != 0; tmp = tmp->next)
out << tmp->info << ' ';
return out;
}
};
这里只讨论两种操作方法:插入节点到双向链表结尾和删除末尾的节点,其余操作方法读者可以自行参考代码。
(1)插入节点到双向链表结尾
具体分为下面几个步骤:
- 创建一个新的节点并初始化三个数据成员(info初始化为el,next初始化为null)
- 将prev的值设置为tail
- 将tail指向新加入的节点
- 前驱节点的next指向新加入的节点
template<class T>
void DoublyLinkedList<T>::addToDLLTail(const T& el) {
if (tail != 0) {
tail = new DLLNode<T>(el,0,tail);
tail->prev->next = tail;
}
else head = tail = new DLLNode<T>(el);
}
(2)删除末尾的节点
双向链表删除一个末尾节点比较简单,因为不需要通过循环来查找待删除节点的前驱节点,将tail指向待删除节点的前驱,
然后将待删除节点的next设置为null。但是如果待操作的链表是空链表可能会引起程序崩溃,因此使用者在删除末尾节点之前要先检查链表是否为空,检查链表是否为空,代码中也提供了。
if (!list.isEmpty())
n = list.deleteFromDLLTail();
else do not delete;
另一种特殊情况是链表只有一个节点,要将head和tail设置为空。
因为可以直接访问最后一个节点,因此这两个方法执行的时间复杂度为O(1)。