3.3 双向链表
单链表最大的缺点就在于它只能按照一个方向进行循环:从开始到结束。编写插入和删除操作的
代码也较为困难,因为只有指向下一个节点的指针,而没有指向前一个节点的指针。
双向链表中的每一个节点包括了前向指针和后向指针。链表管理器既指向头节点也指向尾节点,这
样的节点可以从两个方向加以遍历:从头到尾或者从尾到头。例如,可以按照年月日顺序保存一个
账户交易程序。新的事务被插入到链表的头部。“first/next”“第一个/下一个)形式的遍历可
以首先取得最近的一笔交易,而“last/prior”(最后一个/先前一个)形式可以首先取得最老的
一笔交易。
注意:从理论上说,对于能够在链表中添加的指针数量和方向没有任何限制。比如,没有什么限制
能够阻止节点拥有一个指向链表管理器对象的指针。但是,添加的指针越多,在修改链表成员关系
时需要修改的指针也越就越多。
3.3.1 双向链表的节点
单链表和双向链表的第一个不同之处体现在Node类上。正如程序3-15和程序3-16所看到的,现
在这个类包括了一个指向前驱(prior)节点的指针,以及getPrior和setPrior函数。
[程序3-15]双向链接类的声明
#ifndef NODE#define NODE
#include “thing.h”
class Node{ Private: Thing*theThing;//pointer to object being linked Node*next;//pointer to next node in list Node*prior;//pointer to prior node in list Public: Node(Thing*); Node*getNext(); Node*getPrior(); Thing*getThing(); Void setNext(Node*); Void setPrior(Node*);};
#endif
[程序3-16]双向链表节点类的具体实现#include “node.h”
Node::Node(Thing*theObject){ theThing=theObject; next=0; prior=0; }
Node*Node::getNext(){ return next;}
Node*Node::getPrior(){ return prior;}
Thing*Node::getThing(){ return theThing;}
void Node::setNext(Node*nextNode){ Next=nextNode;}
void Node::setPrior(Node*priorNode){ prior=priorNode;}
3.3.2 双向链表的链表管理器 双向链表的链表管理器(声明见程序3-17)看上去和单链表的非常相似。它对单链表的链表管
理器的两处主要增强在于:增加了一个指向链表结尾节点的指针和一个getLast函数。此外,用来
添加和删除元素的函数也有略微的不同。在对链表进行遍历以查找被删除函数的时候,这些函数无
需保存先前节点,但是修改链表时它们必须处理多个指针。 【程序3-17】双向链表管理器类的声明 #ifndef LISTMGR #define LISTMGR//这里是在干什么? #include “thing.h” #include “node.h” class ListMgr { private: Node*first,*last; public: ListMgr(); void insert (Thing*); Thing*fine(int);//traverse list to locate by ID int remove(int);//use to number to locate tor removal Node*getFirst(); Node*getLast(); }; #endif 1. 向双向链表中添加元素假设一个双向链表按照某个关键字进行排序,插入一个新元素的操作首先要找到新元素应该被插入
的位置。然后,必须调整前向和后向指针(见图3-6)。
在双向链表中插入元素的程序代码可以在程序3-18中找到。具体插入过程如下:(1) 为将要插入到链表中的对象创建一个Node对象(2) 取得待插入对象的关键字(3) 确定链表是否为空(链表管理器的first变量是否包含0)。如果是,则说明链表为空。插入
的节点将成为链表的头节点和尾节点。退出函数。否则,继续步骤(4)。(4) 假定待插入的节点是一个新的头节点(5) 让链表的头节点成为当前节点(6) 进入一个循环,只要存在当前节点就继续循环(7) 取得当前节点链接的对象(8) 将待插入节点的关键字和当前节点所链接对象的关键字进行比较。如果待插入对象的关键字
小于当前节点对象的关键字。则说明找到了插入位置。跳到步骤(11)(9) 利用当前节点指向后继节点的指针(“next“指针)代替当前节点。(10) 回到步骤(6)(11) 如果没有当前节点,则将新节点插入到链表末尾
1) 将新节点的prior指针指向链表当前的尾节2) 将尾点节点的next指针指向新节点3) 将链表管理器的last变量指向新节点4) 退出函数 2. 从双向链表中删除元素正如您所预计的,从双向链表中删除元素的操作是插入元素操作的完全反向操作首先必须查找待删
除元素的位置,然后让待删除元素前后的节点互相指向对方,从而绕过了(也就是有效删除了)中
间的节点(图略)
程序3-19提供了节点删除操作的源代码。函数使用被搜素的关键字作为输入参数,并且返回一个
boolean值,指出操作是否成功完成。
【程序3-19】从双向链表中删除元素。int ListMgr ::remove (int searchNumb){ Thing*currentThing; Node*current,*previous,*next; if(first==0) return false;//List is empty
int firstNode=true;currrent=first;
while(current!=0){ currentThing=current->getThing(); if(searchNumb==currentThing->getKey()) break;//jump out of loopcurrent=current->getnext();firstNode=false;//not the first node}
if(current==0)return false;//node not found
if(current->getNext()=0)//if last node{previous=current->getPrior90;previous->setNext(0);last=previous;}
else if(!firstNode)//if in the middle of the list{//get node after node being removednext=current->getNext();//get node preceding node being removedpreviosu =current->getPrior(0;previous_.setNext(next);next->setPrior(previous);
else//must be first in list //set first to node after node being removed first=current->getNext90;
delete current->getThing();//remove thing object from memorydelete current;//remove node object from memoryreturn true;//remove was successful}
删除操作以如下方式进行:
(1)确定链表是否为空,如果链表为空,返回false,否则,返回,继续执行步骤(2) (2)假定待删除的节点是头节点 (3)将当前节点变为链表的头节点(4)进入一个循环,只要存在当前节点就继续循环(5)取得当前节点对象】(6)将正在搜索的关键字和当前的关键字进行比较,如果两个关键字相等,说明找到了应该删除
的节点。退出循环,跳到步骤(9)。(7)将当前节点的下一个节点变为当前节点(8)继续执行步骤(4)(9)如果不存在任何当前节点,说明拥有正在查找的关键字的对象不在链表中。返回false,指出
删除操作失败并退出函数。否则,继续执行步骤(10)(10)如果待删除的节点是链表的尾节点,按照如下步骤删除该节点: 1)取得指向尾节点的前驱节点的指针 2)将前驱节点的next指针设为0 3)设置链表管理器的last变量,使其指向前驱节点 4)继续执行步骤(13)
(11)如果待删除的节点位于两个节点之间,则按照如下步骤删除该节点 1)取得指向当前节点的后继节点的指针 2)取得指向当前节点的前驱节点的指针 3)设置前驱节点的next指针,使其指向当前节点的后继结点 4)设置后继节点的prior指针,使其指向当前节点的前驱节点 5)继续执行步骤(13)
(12)待删除节点一定是链表的头节点。设置链表管理器的last变量,使其指向前节点的后继结点
,从而实现对节点的删除。(13)删除节点所链接的对象(可选)(14)删除待删除节点(15)返回true,指出删除操作成功完成。
3。双向链表的查找和单链表的查找完全相同
3。3.3 实现多个迭代类
迭代类能够以某种方式精确地遍历整个数据结构。如果根据同一个迭代类创建了多个对象,那
么就可以同时对数据结构进行多种形式的遍历,但是每一种遍历都以同样的顺序从数据结构中取得
对象。所以,如果您希望在某个数据结构上实现多种顺序的遍历。则需要采用多个不同的迭代类。
双向链表就是这样的一种情况。迭代类(程序3-20和程序3-21)以升序(从头到尾)遍历双向链
表。这和单链表的遍历逻辑完全相同。
【程序3-20】双向链表升序(从头到尾)迭代类的声明
#ifndef LISTITR_ASC #define LISTITR_ASC
#include"node.h" #include"listmgr.h"
class ListItAsc { private: Node*currrent; ListMgr*theList; public: ListItrAsc(ListMgr*); Thing*getNext(); };
#endif
[程序3-21】双向链表升序(从头到尾)迭代类的具体实现
#include "listitrasc.h"
ListItrAsc::ListItrAsc(ListMgr*whichList){ current=0; theList=whichList;}
Thing*ListTtrAsc::getNext(){ if (current==0) current=theList->getFirst90; else current=current->getNext90;
if(current!=0) return current->getThing90; else return 0; }
降序(从头到尾)迭代类(程序3-22和程序3-23)使用了一种非常类似的逻辑。但是它使用的
是链表管理器的last变量,从链表的尾节点开始遍历。然后,它使用prior指针完成遍历,而不是
next指针。
两个迭代类的程序代码基本上是一类的。例如,程序3-24中的应用程序类函数使用了升序迭代
。该函数与程序3-25中使用了降序迭代的函数之间的唯一不同在于用来生成迭代对象的迭代类不一
样。使用迭代类的最准备原因就是:无论迭代类以何种顺序为函数提供数值,针对同一个数据结构
所创建的所有迭代类都以完全相同的方式展开工作。
【程序3-22】双向链表降序(由后向前)迭代类的声明
#ifndef LISTITR_DESC#define LISTITR_DESC
#include "node.h"#include"listmgr.h"
class ListTtrDesc{
private: Node*current; ListMgr*theList; public: ListITrDesc(ListMgr*) Thing*getNext();};
#endif
[程序3-23]双向链表(由后向前)迭代类的具体实现#include “listitradesc.h"
ListItrDesc::ListItrDesc(ListMgr*whichList)
{ current=0; theList=whichList;}
Thing*ListItrDesc::getNext(){ if (current==0) current=theList->getLast90; else current=current->getPrior90;
if(current!=0) return current->getThing(); else return 0;
【程序3-24】使用升序的双向链表迭代类 void AppClass::viewThingAsc90 { Thing*current; ListTtrAsc*theIterator;
theIterator=new ListItrAsc(theList); current=theIterator->getNext(); cout<<endl;//just a blank line for spacing while(current!=0) { cout<<"ID="<<current->getID()<<";Name=" <<current->getName()<<"."<<endl; current=theIterator->getNext(); }}
[程序3-25]使用降序的双向列表跌代类void AppClass::viewThingDesc(){
Thing*current; ListItrDesc*theItrator;
theItrator=new ListItrDesc(theList); current=theItrator->getNext(); cout<<endl;//just a blank line for spacing
while(current!=0) { cout<<"ID="<<current->getID()<<";Name=" <<current->getName()<<"."<<endl; current=theIterator->getNext(); }}
3.4 小结
链表是一种线性的数据结构,它通过一条指针链实现了对链表元素的顺序访问。在最简单的链
表形式中,链表仅仅包含一个指向”next“(下一个)节点的指针:为了按照逆顺序访问链表,链
表还必须包括一个”prior“(前一个)指针 链表由链表管理器对象和一系列的节点对象组成,每一个节点都指向一个对象,该对象也属于
链表的一部分。节点的使用避免了在节点所链接的对象中放置指针,从而使对象能够独立于它所参
与的数据结构。
//摘抄自《面向对象c++数据结构》
【美】Jan Harrington 著
陈博译