第九章:链表

我们需要设计一种新的表数据结构,这种数据结构允许元素分散于内存的各个位置,并不强求他们都在一个连续的内存块里。为了建立表元素之间的链接,每个元素结构都要包含指向临近元素在内存中位置的指针。这种结构叫做链表。

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)当程序要在序列的不定位置上频繁的进行添加和删除操作,而且不用下标来访问数据元素时,就用表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值