在学习完循环链表之后,我们是不是觉得链表就完美了嘛?既有单链表,还有循环链表供我们使用,看起来貌似链表已经够我们所使用了,其实不然。试想下:如果我们在某个结点处需要查看它之前的结点,那么我们只能利用循环链表来遍历一次才能获取到这个结点,这种做法是很低效的。因此,我们需要另一种新的链表来供我们调用,那便是双向链表了。
单链表天生的缺陷,那便是它的高效性是建立在从头结点开始访问链表中的数据元素的;如果需要逆向访问的话,那么它的效率极其低下。我们来看看下面的代码
我们在插入的时候,其时间复杂度为 O(n),而在其获取的时候,其时间复杂度为 O(n²)。是不是觉得很奇怪?那么这时双向链表便产生了。它的设计思路是:在“单链表”的结点中增加一个指针 pre,用于指向当前结点的前驱结点。结构如下所示
下来我们来看看双向链表的继承结构层次,如下
它是基于 List 类的,和 LinkList 归于同一层次的。下来我们来看看它的具体实现,和 LinkList 很是相似
DualLinkList.h 源码
#ifndef DUALLINKLIST_H #define DUALLINKLIST_H #include "List.h" #include "Exception.h" namespace DTLib { template < typename T > class DualLinkList : public List<T> { protected: struct Node : public Object { T value; Node* next; Node* pre; }; mutable struct : public Object { char reserved[sizeof(T)]; Node* next; Node* pre; } m_header; int m_length; int m_step; Node* m_current; Node* position(int i) const // O(n) { Node* ret = reinterpret_cast<Node*>(&m_header); for(int p=0; p<i; p++) { ret = ret->next; } return ret; } virtual Node* create() { return new Node(); } virtual void destroy(Node* pn) { delete pn; } public: DualLinkList() // O(1) { m_header.next = NULL; m_header.pre = NULL; m_length = 0; m_step = 1; m_current = NULL; } bool insert(const T& e) // O(n) { return insert(m_length, e); } bool insert(int i, const T& e) // O(n) { bool ret = ((0 <= i) && (i <= m_length)); if( ret ) { Node* node = create(); if( node ) { Node* current = position(i); Node* next = current->next; node->value = e; node->next = next; current->next = node; if( current != reinterpret_cast<Node*>(&m_header) ) { node->pre = current; } else { node->pre = NULL; } if( next != NULL ) { next->pre = node; } m_length++; } else { THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element ..."); } } return ret; } bool remove(int i) // O(n) { bool ret = ((0 <= i) && (i < m_length)); if( ret ) { Node* current = position(i); Node* toDel = current->next; Node* next = toDel->next; if( m_current == toDel ) { m_current = toDel->next; } current->next = toDel->next; if( next != NULL ) { next->pre = toDel->pre; } m_length--; destroy(toDel); } return ret; } bool set(int i, const T& e) // O(n) { bool ret = ((0 <= i) && (i < m_length)); if( ret ) { position(i)->next->value = e; } return ret; } virtual T get(int i) const // O(n) { T ret; if( get(i, ret) ) { return ret; } else { THROW_EXCEPTION(IndexOutOfBoundsException, "Invaild parameter i to get element ..."); } } bool get(int i, T& e) const // O(n) { bool ret = ((0 <= i) && (i < m_length)); if( ret ) { e = position(i)->next->value; } return ret; } int find(const T& e) const // O(n) { int ret = -1; int i = 0; Node* node = m_header.next; while( node ) { if( node->value == e ) { ret = i; break; } else { node = node->next; i++; } } return ret; } int length() const // O(1) { return m_length; } void clear() // O(n) { while( m_length > 0 ) { remove(0); } } virtual bool move(int i, int step = 1) // O(n) { bool ret = (0 <= i) && (i < m_length) && (step > 0); if( ret ) { m_current = position(i)->next; m_step = step; } return ret; } virtual bool end() // O(n) { return (m_current == NULL); } virtual T current() { if( !end() ) { return m_current->value; } else { THROW_EXCEPTION(INvalidOPerationException, "No value at current position ..."); } } virtual bool next() { int i = 0; while( (i < m_step) && !end() ) { m_current = m_current->next; i++; } return (i == m_step); } virtual bool pre() { int i = 0; while( (i < m_step) && !end() ) { m_current = m_current->pre; i++; } return (i == m_step); } ~DualLinkList() { clear(); } }; } #endif // DUALLINKLIST_H
我们下来做个测试,看看是否正确
#include <iostream> #include "DualLinkList.h" using namespace std; using namespace DTLib; int main() { DualLinkList<int> d1; for(int i=0; i<5; i++) { d1.insert(0, i); } for(d1.move(0); !d1.end(); d1.next()) { cout << d1.current() << endl; } cout << "begin" << endl; for(d1.move(d1.length()-1); !d1.end(); d1.pre()) { cout << d1.current() << endl; } d1.move(d1.length()-1); cout << "end" << endl; return 0; }
我们先是正向打印,再是反向打印。结果应该是4-0, 0-4。我们来看看结果
结果正确,实现上基本没啥问题。再来测试下,测试代码如下
#include <iostream> #include "DualLinkList.h" using namespace std; using namespace DTLib; int main() { DualLinkList<int> d1; for(int i=0; i<5; i++) { d1.insert(0, i); d1.insert(0, 5); } for(d1.move(0); !d1.end(); d1.next()) { cout << d1.current() << endl; } cout << "begin" << endl; d1.move(d1.length()-1); while( !d1.end() ) { if( d1.current() == 5 ) { cout << d1.current() << endl; d1.remove(d1.find(d1.current())); } else { d1.pre(); } } cout << "end" << endl; for(d1.move(d1.length()-1); !d1.end(); d1.pre()) { cout << d1.current() << endl; } return 0; }
我们插入 i 的同时也插入5 ,也就是说,每插入一个 i ,便会插入一个 5。再次查找并删除 5 ,最后打印的是 0-4(因为是反向遍历打印)。看看结果
我们看到结果是正确的,如我们所期望的那样。通过今天对双向链表的学习,总结如下:1、双向链表是为了弥补单链表的缺陷而设计的;2、在概念上,双向链表不是单链表,没有继承关系;3、双向链表的游标能够直接访问当前结点的前驱和后继;4、双向链表是线性表概念的最终实现(更贴近理论上的线性表)。后面我们会继续学习双向链表的子类:DualStaticLinkList 和 DualCircleList。