概述
在线性表的链式描述中,线性表的元素在内存中的位置是随机的。每个元素都有一个明确的指针或链(指针和链是一个意思)指向线性表的下一个元素的位置。
单向链表
描述
在链式描述中,数据对象实例的每一个元素都用一个单元或节点来描述。节点不必是数据成员,因此不是用公式来确定元素的位置。取而代之的是,每一个节都明确包含另一个节点相关的位置信息,这个信息成为链或者指针。
每一个节点都有一个链域,它的值是线性表下一个元素的位置,即地址。这样一来,ei的节点链接着元素ei+1的节点。最后一个元素的节点没有其他的节点可以连接,因此链域的值为NULL。变量firstNode用来指向链式描述的第一个节点。所以,在线性表的链式描述中,想要找到ei的位置,就必须从e1开始逐个寻找,不能像数组描述中一样,用下标索引进行寻找。
在链式描述中,每一个节点只有一个链,这种结构成为单向链表。链表从左到右(最后一个节点除外)都连接着下一个节点,最后一个节点的链域值为NULL。这样额结构也称为链条。
结构chainNode
为了用链表描述线性表,我们要定义一个结构chainNode和一个类chain。
#include<iostream>
using namespace std;
template<class T>
struct chainNode
{
T element;
chainNode<T>* next;
chainNode(){ }
chainNode(const T& element)
{this->element = element;}
chainNode(const T& element,chainNode<T>* next)
{this->element = element;this->next = next;}
};
类chain
类chain用单向链表实现了线性表,其最后一个指针域的节点为NULL ,即它用单向连接的一组节点实现线性表。
template<class T>
class chain
{
public:
chain(int initailCapacity = 10);
chain(const chain<T>& );
~chain();
bool empty(){return listSize = 0;}
int size() const {return listSize;}
T& get(int theIndex) const ;
int indexOf(T& theElement ) const;
void erase(int theIndex);
void Insert(int theIndex,const T& theElement);
void output(ostream& out) const;
protected:
void checkIndex(int theIndex) const;
chainNode<T>* firstNode;
int listSize;
};
数据成员firstNode和listSize。firstNode是线性表指向首元素(即线性表第零个元素的节点)的指针。当链表为空的时候firstNode的值为NULL,listSize表示线性表的元素个数,它等于链表的节点数。
构造函数和赋值构造函数
为了创建一个空链表,只需令第一个节点的指针firstNode 的值为NULL,与数组描述的线性表不同,链表在创建的时候不需要估计元素的最大个数以分配初始空间。不过构造函数还是具有一个表示初始容量的形参initialCapacity,目的是为了和arrayList相容。
下面展示一些 内联代码片
。
template<class T>
chain<T>::chain(int initialCapacity)
{
if (initialCapacity<1 )
{
cerr<<"wrong!wrong!wrong!"<<endl;
}
firstNode = NULL;
listSize = 0;
}
template<class T>
chain<T>::chain(const chain<T>& theList)
{
listSize = theList.listSize;
if(listSize == 0)
{
firstNode = NULL;
return ;
}
else
{
chainNode<T>* sourceNode = theList.firstNode;
firstNode = new chain<T>(sourceNode->element);
sourceNode = sourceNode->next;
chainNode<T>* targetNode = firstNode;
while(sourceNode!=NULL)
{
targetNode->next = new chainNode<T>*(sourceNode->element);
targetNode = targetNode->next;
sourceNode = sourceNode->next;
}
targetNode->next = NULL;
}
}
其中构造函数相比于复制构造函数好理解,下面我将会详细的解释一波复制构造函数的运行原理:
析构函数
析构函数是要逐个清除链表的节点。实现的策略是重复清除链表的首元素节点,直到链表为空。注意,我们在清除首节点的时候必须要用nextNode保存第二个元素节点的指针。
下面展示一些 内联代码片
。
template <class T>
chain<T>::~chain()
{
while(firstNode!= NULL)
{
chainNode<T>* nextNode = firstNode->next;
delete firstNode;
firstNode = nextNode;
}
}
析构函数相对更好理解撒。
方法get
在数组描述的线性表中,我们根据公式来计算一个表元素的位置。然而在链表中,要寻找索引为theIndex的元素,跟踪链域next直到找到所需要的的元素节点指针,也就是说必须跟踪theIndex个指针。
下面展示一些 内联代码片
。
template<class T>
T& chain<T>::get(int theIndex) const
{
chainNode<T>* currentNode = firstNode;
for(int i = 0;i < theIndex ;i++)
{
currentNode = currentNode->next;
}
return currentNode->element;
}
方法indexOf
返回元素首次出现时的索引
template<class T>
int chain<T>::indexOf(T& theElement) const
{
chainNode<T>* currentNode = firstNode;
int index = 0;
while(currentNode != NULL && currentNode->element != theElement)
{
currentNode = currentNode->next;
index++;
}
if(currentNode == NULL)
{
return -1;
}
else
{
return index;
}
}
这个算法也是很好理解的。
方法erase
从链表中删除索引为theIndex 的元素,本质上就是让被删除元素的前一个节点的next指针直接指向被删除元素后一个节点,然后再把要删除的节点空间释放。需要注意的是,和析构链表一样,我们需要把被删除节点中的next指针留存下来,因为被删除节点中的next指针指向的是被删除的节点后一个节点,我们要将这个指针赋给被删除链表的前一个节点的next值。
template<class T>
void chain<T>::erase(int theIndex)
{
chainNode<T>* deleteNode ;
if(theIndex == 0)
{
deleteNode = firstNode;
firstNode = firstNode->next;
}
else
{
chainNode<T>* p = firstNode;
for(int i = 0;i<theIndex-1;i++)
{
p=p->next;
}
deleteNode = p->next;
p->next = p->next->next;
}
listSize--;
delete deleteNode;
}
上面展示的代码主要的思路是这样的:要删除的那个节点的地址在要删除节点的前一个节点的next指里,所以我们析构前一个节点的next指针。然后再把要删除节点的next指针赋给前一个指针就可以完成节点的删除操作。所以,我们应该定义一个deleteNode用来暂时储存前一个节点的next指针,这个时候前一个节点的地址就会有两个备份,一份在deleteNode里,另一份在next里。然后再用要被删除的节点的next指针给前一个next指针赋值(这个时候deleteNode中的地址没变,前一个next指针的地址被改成了指向被删除元素后面一个节点的地址),然后再析构deleteNode,就是释放了地址deleteNode指向的那一片要被删除的空间。
方法insert
删除和插入的操作过程很相似,为了在链表中索引为theIndex的位置上插入一个新元素,需要首先找到索引为theIndex-1的元素节点,然后在该节点之后插入新元素节点。
这个算法相对理解起来相对较困难,我会比较详细的介绍:
下面展示一些 内联代码片
。
template<class T>
void chain<T>::Insert(int theIndex,const T& theElement )
{
if(theIndex< 0||theIndex > listSize)
{
cerr<<"wrong!wrong!wrong!"<<endl;
}
else
{
if(theIndex == 0)
{
firstNode = new chainNode<T>*(theElement,firstNode);
}
else
{
chainNode<T>* p = firstNode;
for(int i=0;i<theIndex-1;i++)
{
p = p->next;
}
p->next = new chainNode<T>*(theElement,p->next);
}
}
listSize++;
}
解析:在链表中的某个索引位置上插入一个节点,例如在索引3的位置上插入一个新节点取代原来在索引3的节点,并把原来在索引3 的节点挤到索引为4的位置。那么一旦操作完成,节点2的next指针就会储存新加入的节点的地址,而新加入节点的next指针会储存插入前节点3的地址。相对应的操作是:首先我们要调用产Node的构造函数生成一个新节点。在新节点中我们把next指针赋成原来节点2中的next指针,再把新元素放到新节点的element域中。然后将这个新生成的节点的地址赋给节点2的next指针上(这个操作我们用new来完成。)
相对应的代码就是
p->next = new chainNode*(theElement,p->next);
这样我们就完成了向链表中插入一个新节点的操作。
输出链表
输出链表就是将链表插入输出流(有的时候会用到流插入运算符的重载)
下面展示一些 内联代码片
。
template<class T>
void chain<T>::output(ostream& out) const
{
for(chainNode<T>* currentNode = firstNode;
currentNode != NULL;
currentNode = currentNode->next)
out<<currentNode->element<<" ";
}
template<class T>
ostream& operator<<(ostream& out,const chain<T>& x)
{
x.output(out);
return out;
}
以上就是线性表的链式描述的一些内容,我们再来回顾一下:
如何定义出一个链表;
链表的构造函数
链表的析构函数
链表的复制构造函数
查找索引为index的元素
查找元素第一次出现时的索引
如何在链表中删除一个元素
如何在链表中插入一个元素
将链表链表插入输出流
循环链表和头结点
将链表描述成一个单项循环链表,要在链表的前面增加一个节点(用到了单链表的插入操作,上文有介绍),成为头结点,将链表的尾节点和头结点连接起来(用尾节点的null指针赋上headerNode),单向链表就会变成一个循环链表。(代码就不展示了,操作都是已经展示过的)
双向链表
双向链表的每一个节点都有三部分构成:
1.指向前驱的指针
2.指向后继的指针
3.元素域
可以双向的访问历遍。
后记
以上就是线性表链式描述的的一些已基本算法,希望能对你有所帮助。