我们需要设计一种新的表数据结构,这种数据结构允许元素分散于内存的各个位置,并不强求他们都在一个连续的内存块里。为了建立表元素之间的链接,每个元素结构都要包含指向临近元素在内存中位置的指针。这种结构叫做链表。
9.1 链表节点
要在计算机上实现链表,第一步就是建立节点结构。在节点结构中,包括数据(称作nodeValue)和一个指针(next),她指向序列中的下一个节点。
一般来说,类的数据成员应该声明为private的,通过public的成员函数来访问和更新他们。但是,node类只是为实现单向链表而设计的,node对象不会有其他多方面的用途。在实现单向链表时,直接访问节点的数据和指针比通过成员函数访问有更高的效率。
template<typename T>
class node
{
public:
T nodeValue ;
node<T> *next ;
node():next(NULL)
{
}
node(const T& item , node<T> *nextNode = NULL):nodeValue(item),next(nextNode)
{
}
};
注意::成员变量next是指向node<T>对象的指针。node类是自引用的结构,她包含一个指向自己类型对象的指针。
节点是链表中的单个元素。为了能使整个链表的长度在添加删除数据时增加和缩短,节点是动态分配和回收的。这意味着节点本身驻留在堆里,需要有一个指针来引用它的地址。要创建节点,首先要声明node<T>类型的指针,然后用new运算符来分配节点所需要的内存。
node<int> *p ;
p = new node<int>(10);
通过指针变量p访问 节点的数据和指针时,要用运算符-> 。
添加节点:newNode –>next = curr ;
prev - >next = newNode ;
删除节点: prev –> next curr –> next ;
delete curr ;
9.2 建立链表
9.2.1 定义单向链表
单向链表是若干节点的集合,她包含一个指向第一个节点的指针。通常情况下,我们称这个为front 。除了最后一个节点,其他节点都有唯一的后继节点,他们的next指针就指向各自的后继节点。最后一个节点的next指针为null 。
为什么要有front指针呢?因为front指针提供了访问第一个节点的方法,所以在链表的表头添加和删除数据只需要简单的修改front指针。
我们说front定义了一个链表。如果有front,就可以访问链表中的每个节点。没有front链表就没有入口,所有的节点也将无法访问。
表头的声明:node<T> *front ;
node<T> *front = NULL ; //模板
9.2.2 在链表表头插入节点
node<T> *front , *newNode ;
newNode = new node<T>(item, front) ;//当分配新节点时,要把front指针作为构造函数的指针参数传递过去,这样通过new运算符产生的新节点就自然连接到原来front指向的节点的前面了。
front= newNode ;
注意空表的情况,这时传递给构造函数的front指针为NULL,其效果是使newNode的next指针也是NULL,对于非空链表,上述操作使newNode的next指针指向原来的front节点。
如果堆里的内存不够,则分配节点失败。产生异常:把分配过程和检测过程绑定为一个函数,函数返回新节点的地址。
template<typename T>
node<T> *getNode(const T& item , node<T> *nextNode = NULL)
{
node<T> *newNode ;
newNode = new node<T>(item,nextNode);
if (newNode == NULL)
{
throw memoryAllocationError("node allocation error");
}
return newNode ;
}
9.2.3 在链表表头删除节点
只有当链表不为空时,才能从其表头删除节点。删除操作只需要把front指针更新为链表中的第二个节点(front->next)即可。删除算法首先把front指针赋给一个临时指针p,在front指针更新为指向链表中的第二个节点后,再用p作为delete运算符的参数,释放原来表头节点占用的内存。
template<typename T>
void deleteNode(node<T> *front)
{
node<T> *p ;
if (front != NULL)
{
p = front ;
front = front->next ;
delete p ;
}
}
当程序不在需要链表时,应删除链表
template<typename T>
void deleteList(node<T> *front)
{
node<T> *p ;
while(front != NULL)
{
p = front ;
front = front->next ;
delete p ;
}
}
9.2.4 删除特定的节点
必须先遍历链表,确定要删除的节点位置。然而,光有这个位置还不够,还必须要有指向该节点的前驱节点的指针;因此,在遍历过程中,要使用一对指针。这两个指针一前一后,一个指向遍历的当前节点,另一个指向当前节点的前驱节点。
分两种情况:链表中的第一个节点,中间位置的某个节点。
template<typename T> //node<T>* & front 理解为对node型指针的引用
void eraseValue(node<T>* & front , const T& target)
{
node<T> *curr = front , *prev = NULL ;
bool foundItem = false ;
while(curr != NULL && !foundItem)
{
if (curr->nodeValue == target)
{
if (prev == NULL) //the head of list
{
front = front->next ;
}
else
prev->next = curr->next ;
delete curr ;
foundItem = true ;
}
else
{
prev = curr ;
curr = curr->next ;
}
}
}
9.3 处理链表表尾
我们介绍了在链表的表头添加和删除节点的算法。相关的函数只需简单的更新front指针,时间复杂度为O(1)。栈数据结构只在数据序列的一端进行操作。所以单向链表可以有效地实现栈类。
队列是提供在数据序列的两端进行操作的容器。需要定义新的链表结构,用front 和back指针分别指向表首和表尾。当链表为空时,(front == null,back == null),在分配了新节点后,要把front指针和back指针都指向他。
添加新节点代码::
node<T> *front ,*back , *newNode ;
newNode = new node<T>(item) ;
if(front != null)
back->next = newNode ;
else
{
front = newNode ; back = newNode ;}
删除节点代码::
node<T> *front ,*back , *p ;
if(front != null)
p = front ;
front = front->next ;
if(front = null)
back = null ;
delete p ;
9.4 用链表实现队列
使用单向链表来存储数据元素,这是队列的又一种实现方法。
9.5 双向链表
dnode,他有两个指针,分别指向当前节点的前驱和后继。由一系列dnode对象连接构成的序列叫做双向链表。
在某个位置上插入节点:
首先更新newNOde节点的两个指针。然后更新前驱和后继节点的指针。共四个步骤:
dnode<T> *newNode , * prevNode ;
newNode = new dnode<T>(item) ;
prevNode = curr –>prev ;
//更新newNode的指针
newNode –>prev = prevNode ;
newNode –>next =curr ;
//更新前驱和后继节点指针
prevNode->next = newNode ;
curr –>prev = newNode ;
删除某个位置上的节点:
dnode<T> *prevNode = curr –>prev , *succNode = curr –>next ;
prevNode –>next = succNode ;
succNode –>prev = preNode ;
delete curr ;
9.5.2 双向循环链表
在双向链表内,包含了一个标记节点,我们通常称她为头节点(header)。头节点是一个dnode对象,他的值为类型T的默认值,双向链表从来不会用到头结点的值。头结点的next指针指向双向链表中第一个包含数据元素的节点,最后一个节点的next指针又指回头结点。这样一来,整个链表就成了一个循环结构。
双向链表中的每个节点,包括头结点,都有唯一的前驱节点和后继节点。他们分别由指针成员prev和next所引用。头结点扮演的角色是建立双向链表的基石,他的next指针指向链表中第一个实际数据节点,prev指针指向最后一个数据节点。
声明双向链表:头结点定义了一个双向链表,所以要声明双向链表,首先要声明头结点。默认构造函数的主要功能就是分配头结点。dnode<T> *header = new dnode<T> ;
新建立的双向链表至少包含一个节点(头节点)。要检测单向链表是否为空,就看front指针是否为null。要检测双向链表是否为空,就得看header –>prev 和 header –>next 是不是等于header 。
头结点的指针成员分别指向链表中第一个节点(header->next)和最后一个节点(header->prev),头结点本身就是越过链表末端遇到的第一个节点,这是因为双向链表是循环结构。遍历过程从第一个节点开始,一直向后进行,最终回到头结点。如果用给迭代器使用的区间符号[first , last ) ,那么,指向双向链表的节点序列是所有指针应该在[header –>next , header )之内。
在链表的表头添加和删除节点,需要通过传递指针参数header->next ,调用insert()或者erase()函数。头结点同样定义了链表的末端。在链表末端加入节点的算法把header作为参数调用insert(),把header->prev作为参数调用erase()会删除链表的最后一个节点。
9.7 约瑟夫问题
函数Josephus()假定,有n个人,每隔m个人淘汰一个。函数用n-1次循环,为了避免产生无效指针,每次都把指针多移一步,由于节点是在双向链表中,每次移动指针后都必须检测头结点,确保计数过程没有计算头结点,头结点也不会被删除。
void josephus(int n, int m)
{
dnode<int> *dList = new dnode<int> , *curr ;
int i,j ;
for (i = 1 ;i<=n;i++)
{
insert(dList , i);
}
curr = dList->next;
for (i = 1 ;i<n ;i++)
{
for (j=1;j<=m-1;j++)
{
curr = curr->next ;
if (curr == dList)
{
curr = curr->next ;
}
}
cout<<"delete node"<<curr->nodeValue<<endl;
curr = curr->next ;
erase(curr->prev);
if (curr == dList)
{
curr = curr->next ;
}
}
cout <<endl <<curr->nodeValue <<"win the cruise" <<endl ;
delete curr ;
delete dList;
}
9.8 miniList 类
class miniList
{
public:
// include the iterator nested classes
#include "d_liter.h"
miniList();
// constructor. create an empty list
miniList(int n, const T& item = T());
………………………….
………………………………..
把迭代器定义成内嵌类,完全是考虑到封装性。内嵌类没有特殊方式访问miniList类的成员;而且,miniList类也没有特殊访问迭代器成员的特权。在其包含类作用域之外引用内嵌类时,必须包含类作用域运算符:: 。iterator & operator ++()
重载前缀运算符时,重载函数没有任何参数。重载后缀运算符的原型包含一个int类型的参数。当编译器发现了一条使用后缀加1运算符的语句时,他会提供一个值为0的运行参数。在运算符重载函数的声明中,指明存在一个int参数是必须的,但是却没有必要给出这个参数的名字。iterator & operator ++(int)
9.9 选择顺序容器
我们在前面章节中介绍的顺序容器包括:数组,向量,表和双端队列。
提供几条选择顺序容器时的基本原则:
1)只有在能够预知数据元素个数的情况下,才使用C++d数组。
2)如果应用程序需要根据下标访问数据元素,并且所有添加和删除元素的工作都在序列末端进行,那么选用向量。
3)如果应用程序需要根据下标访问数据元素,并且所有添加和删除元素的工作都在序列两端进行,那么选用双端队列。
4)当程序要在序列的不定位置上频繁的进行添加和删除操作,而且不用下标来访问数据元素时,就用表。