线性表一般分为顺序表和链表,本篇是对链表的回顾,以C++创建简单(带头结点)递增单链表,实现插入和逆置功能为例。
链表的形式有很多种,其中最简单、最基础的是单链表 。其他诸如循环链表、双向链表、双向循环链表等,应该算是基于单链表的变形,个人认为其本质也还是单链表。
单链表用来表示线性表时,其定义是:一个存储节点分为数据域(data)和指针域(link)两部分,数据域用于储存线性表的一个数据元素(其实也就是原本的数据),而指针域用于存放一个指针,该指针指向下一个存储节点(即指针的值为下一个存储节点的开始存储地址)。由此形成一个链表结构,最后一个节点的指针指向空(NULL)。
1.单链表的类定义:
单链表的定义方法大致分为以下几种:
(1)复合类方式
class List;
class LinkNode{
friend class List; //声明List为友元类,让List可以使用LinkNode的数据成员,友元类是非对称的
private:
int Node;
LinkNode * Next;
}
class List{
public:
....... //链表的公共操作
private:
LinkNode * First; //链表的头结点
}
(2)嵌套类方式
class List{
public:
........ //链表的公共操作
private:
class LinkNode{
public:
int Node;
LinkNode * Next;
}
LinkNode * First; //链表的头结点
}
将节点类放入List类中,并且设为private使得外部无法直接访问,而节点类的数据成员又设为public,使得List类的成员可以直接访问它们。
(3)基类和派生类的方法
此种方法即将LinkNode类作为基类,将List类声明为派生类,然后继承基类的数据成员(有时也包括成员函数)。
(4)利用结构体来定义LinkNode
struct LinkNode
{
int elen;
number* next;
};
class List
{
public:
........ //链表的公共操作
private:
LinkNode * First; //链表的头结点
}
定义为struct使得节点失去了封装性,但是可以简化操作。且这种方法和第一种复合类的方法都很灵活,可以让一个节点类用于多个(类型)链表。后面的示例代码也是用的这种方法。
示例程序的类定义为:
struct number
{
int elen;
number* next;
};
class List
{
public:
List() { seqlist = new number; }
~List() {}
void createlist();
void insertlist(int x);
void printlist();
void returnlist();
private:
number* seqlist;
};
2.各个函数的具体定义为:
void List::createlist()
{
int iTemp = 2;
int j;
number* npTempNode;
npTempNode = seqlist;
for (j = 0; j < elenum; j++)
{
npTempNode->next = new number;
npTempNode->next->elen = iTemp;
npTempNode = npTempNode->next;
iTemp += 5;
}
npTempNode->next = NULL;
}
void List::insertlist(int x)
{
int i;
bool bJudge=false;
number* npTempNode=seqlist;
while (npTempNode->next!= NULL&&x>npTempNode->next->elen)
//循环直至某个节点的下一个节点的值大于X 或 已到链尾
{
npTempNode = npTempNode->next;
}
number* npTempNodeInsert = new number;
npTempNodeInsert->elen = x;
if (npTempNode->next==NULL) //判断是否因为到了链尾跳出循环
{
npTempNode->next = npTempNodeInsert;
npTempNodeInsert->next = NULL;
}
else
{
npTempNodeInsert->next = npTempNode->next;
npTempNode->next = npTempNodeInsert;
}
}
void List::returnlist() //原理是头结点不保存数据,且头结点的下一个节点就是第一个数据节点
{
number* npTempNode = seqlist->next; //先保存头节点的下一个节点
seqlist->next = NULL; //先将头结点指向空
number* npTempNodeReturn; //临时使用的节点
while (npTempNode!= NULL) //到了原链表的链尾跳出循环,
//此时头结点的下个节点就是原表的链尾
{
npTempNodeReturn = npTempNode->next; //用临时节点保存当前节点的下一个节点
npTempNode->next = seqlist->next; //将当前节点的下一个节点改成头节点的
//下一个节点,即当前的第一个数据节点
seqlist->next = npTempNode; //将当前节点变成头节点的下一个节点
npTempNode = npTempNodeReturn; //取回保存的值,继续循环
}
}
void List::printlist()
{
number* npTempNode = seqlist->next;
while (npTempNode!=NULL)
{
cout << npTempNode->elen << " ";
npTempNode = npTempNode->next;
}
cout << endl;
}
3.对链表的分析:
(1)由于单链表的每一项都只知道它的下一项,所以要在某一位置插入节点或删除某一位置的节点数据,需要从头节点开始,逐步追朔,才能到达特定位置。而要获得某一项的值,也要从头节点开始。
(2)而插入或者删除本身是很方便的,且通过指针获得下一个节点,也比逐个移动节点数据要快很多。所以单链表是易于插入或者删除,而相对难于获得某个特定位置的节点数据的。
(3)虽然说更复杂的循环链表、双向链表等可以减少搜索的节点数,但本质上与单链表相差不大。而且更复杂的链表就需要用更多的指针,这会使程序中可能出现的BUG增加(可能会是成倍增加),同时降低程序的安全性。
总的来说,当线性表需要有较多的插入或者删除这样的操作时,用链表是比较好的。当线性表会有较少插入或者删除,而较多的获取数据操作时(尤其是某个特定节点),用顺序表可能更好一点。