C++线性表的链式存储结构

时间:2021-09-17 14:54:03

C++实现线性表的链式存储结构:

为了解决顺序存储不足:用线性表另外一种结构-链式存储。在顺序存储结构(数组描述)中,元素的地址是由数学公式决定的,而在链式储存结构中,元素的地址是随机分布的,每个元素都有一个明确的指针指向线性表的下一个元素的位置(即地址)。

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。在顺序结构中,每个数据元素只需要存数据元素信息就行了,而在链式结构中,除了存储数据元素信息外,还要存储它的后继元素的存储地址。所以一般结点包括两个信息:数据和指针。链表就是n个节点组成的,如果每个结点只包含一个指针,那么就是单链表。

有头有尾:我们把链表中第一个结点的存储位置叫作头指针,那么整个链表的存取就必须是从头指针开始进行的。而线性链表的最后一个结点指针为空(NULL)。从下图中可以看到,结点都是由两部分组成,数据域和指针域。

C++线性表的链式存储结构

有时,为了更方便对链表进行操作,会在单链表的第一个结点前加一个头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。

C++线性表的链式存储结构

头指针和头结点的异同

  1. 指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。

  2. 头指针具有标识作用,所以常用头指针冠以链表的名字。

  3. 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。

(这句话真的歧义,若没有头结点,头指针head指向第一个节点,当空表时,head=NULL。应该是必须要有头指针。)

头结点

(1)头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)

(2)有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。

(3)头结点不一定是链表必须的要素。

链式存储结构的线性表进行的基本操作。主要包括:

  • 插入:操作方式为在指定元素前插入、在指定元素之后插入、在指定位置完成插入
  • 删除:操作方式可分为删除指定元素、删除指定位置的元素,删除第一个元素,删除最后一个元素
  • 显示数据
  • 查找:查询指定的元素(可根据某个数据成员完成查询操作)
  • 定位操作:定位指定元素的序号
  • 更新:修改指定元素的数据
  • 数据文件的读写操作
  • 计算链表的长度

C++线性表的链式存储结构

定义一个结点类

/*Define a friend class to facilitate direct manipulation of data*/
template<class T>
class LinkList;
template <class T>
class LinkNode
{
friend class LinkList<T>;
private:
T _data;
LinkNode<T> *_next;
public:
LinkNode(T x )
{
_data = x;
_next = NULL;
}
};

链表的主要函数构成

template <class T>
class LinkList
{ private:
LinkNode<T> * _head;
public:
LinkList()
{_head = NULL;}
LinkNode<T>* _CreateNode(const T& x);//Create a new node
void clear(LinkNode<T>* &cur);//Delete a new node
void PushBack(const T& x);//tail insertion to create a linked list
void PushFront(const T& x);//Head insertion to create a linked list
void PopBack(); //Remove an element from the tail
void PopFront();//Remove an element from the head
int Length();//Find the length of the linear table
LinkNode<T>* Find(T x);//Find a number
void Insert_right(int pos, const T& x);//Insert after the nth
void Insert_cur(int pos, const T& x);//Insert at the specified location
void Insert_left(int pos, const T& x);//Insert in front of the nth
void Delete_pos(int pos);//Delete the nth element
void Delete_val(const T& x);//Delete specified element
void reset(const T &x,const T &y);//Update an element
int located(const T &x);//Locate the serial number of the specified element
void Print();// Print linear table
bool writeToFile();//Write file
T* readFromFile();//Read in data file
int readlen();
};

各个函数的实现

Print()函数
template<class T>
void LinkList<T>::Print()
{
LinkNode<T> *tmp = _head;
while (tmp != NULL)
{
cout << tmp->_data << "-->";
tmp = tmp->_next;
}
cout << "NULL" << endl;
}

创建一个新的结点,并为其分配空间

template <class T>
LinkNode<T>* LinkList<T>:: _CreateNode(const T& x)
{
LinkNode<T>* tmp = new LinkNode<T>(x);
return tmp;
}

清除某一个节点,释放空间

template<class T>
void LinkList<T>::clear(LinkNode<T> *&cur)
{
cur->_next = NULL;
delete cur;
cur = NULL;
}

获得链表的长度

template<class T>
int LinkList<T> ::Length()
{ int len = readlen();/*调用一个读文件的函数,来判别链表状态,Call a function that reads the file to determine the state of the linked list*/
if(len>0){
return len;
}
else {
len=0;
if (_head == NULL)
{
return 0;
}
else
{
LinkNode<T> * begin = _head;
while (begin != NULL)
{
begin = begin->_next;
len++;
}
}
}
return len;
}

前插法建立链表

从一个空表开始,重复读入数据,执行以下两步

(1)生成新的结点,将读入数据存放在新节点的的_data域中

(2)将该节点插入到链表的前端,直到读入到结束符为止。

template <class T>
void LinkList<T> :: PushFront(const T& x)
{
if (_head == NULL)
{
_head = _CreateNode(x);
}
else
{
LinkNode<T> * prev = _CreateNode(x);
prev->_next = _head;
_head = prev;
}
}

用后插法建立链表

需要设置一个尾部指针end,总是指向新链表的最后一个节点,新节点链接到它所指链尾节点的后面。end最初要置于附加头节点位置

template <class T>
void LinkList<T> ::PushBack(const T& x)
{
if (_head == NULL)
{
_head = _CreateNode(x);
}
else
{
LinkNode<T> * end = _head;
while (end->_next != NULL)
{
end = end->_next;
}
end->_next = _CreateNode(x);
}
}

从尾部删除一个数据

考虑只有一个节点情况,多个结点的情况。

多个结点,首先找到尾部元素,然后调用clear()函数,清理掉尾部第一个元素

template <class T>
void LinkList<T> :: PopBack()
{
if (_head == NULL)
{
cout << "The List is empty!!!" << endl;
return;
}
else if (_head->_next == NULL)
{
clear(_head);
}
else
{
LinkNode<T> * temp = _head;
LinkNode<T> * end = _head;
while (end->_next != NULL)
{
temp = end;
end = end->_next;
}
temp->_next = NULL;
clear(end);
}
}

从头部删除一个元素

实现方法和从尾部删除一个元素基本相似,不多加以解释

template <class T>
void LinkList<T> ::PopFront()
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return;
}
else if (_head ->_next == NULL)
{
clear(_head);
}
else
{
LinkNode<T> * tmp = _head;
_head = _head->_next;
clear(tmp);
tmp = NULL;
}
}

查找某一个元素

遍历整个链表,并将其数据_data与x进行比对,如果是其他类型就需要重载运算符==

template <class T>
LinkNode<T>* LinkList<T> ::Find (T x)
{
if (_head == NULL)
{
cout << "List is empty,not found!!!" << endl;
return NULL;
}
else if(_head->_data==x)
{
return _head;
}
else
{
LinkNode<T> * n = _head ;
while (n->_next != NULL && n->_data != x )
{
n = n->_next;
if (n->_data == x)
{
return n;
}
}
}
return NULL;
}

在第pos个元素后插入一个新元素

创建一个新的结点,通过移动begin指针,pos控制指针最终位置,将新元素插入到之后

template <class T>
void LinkList<T> ::Insert_right(int pos,const T& x)
{
int len = Length();
if (pos <= len)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return;
}
else
{
LinkNode<T> * begin = _head;
LinkNode<T> * tmp = _CreateNode(x);
while (--pos)
{
if (begin->_next != NULL)
{
begin = begin->_next;
}
}
tmp->_next = begin->_next;
begin->_next = tmp;
}
}
else
{
cout << "Input Error!!!" << endl;
}
}

在第pos个元素插入一个新元素

因为写了一个在后面插入的函数,为了偷懒,就直接调用了Insert_right()这个函数

template <class T>
void LinkList<T> ::Insert_cur(int pos,const T& x)
{
int len = Length();
if (pos <= len)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return;
}
else
{
Insert_right(pos-1, x);
}
}
else
{
cout << "Input Error!!!" << endl;
}
}

在第pos个元素前插入一个新元素

创建一个新的结点,通过移动begin指针,pos控制指针最终位置,将新元素插入到之前,与插入之后实现方法基本类似,只是需要注意pos的控制

template <class T>
void LinkList<T> ::Insert_left(int pos,const T& x)
{
int len = Length();
int temp=pos-1;
if (pos <= len)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return;
}
else
{
LinkNode<T> * begin = _head;
LinkNode<T> * tmp = _CreateNode(x);
while (--temp)
{
if (begin->_next != NULL)
{
begin = begin->_next;
}
}
tmp->_next = begin->_next;
begin->_next = tmp;
}
}
else
{
cout << "Input Error!!!" << endl;
}
}

删除第pos个元素

依旧利用pos控制指针位置,然后删除就ok

template <class T>
void LinkList<T> ::Delete_pos(int pos)
{
int len = Length();
if (pos <= len)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
}
else if (_head->_next == NULL)
{
clear(_head);
}
else
{
LinkNode<T> * begin = _head->_next;
LinkNode<T> * temp = _head;
pos = pos - 1;
while (--pos)
{
if (begin->_next != NULL)
{
begin = begin->_next;
temp = temp->_next;
}
}
temp->_next = begin->_next;
begin->_next = temp;
}
}
else
{
cout << "Input Error!!!" << endl;
}
}

查找某一个元素位置并返回其位置

用一个temp进行计数,遍历整个链表,一一进行比对其——data数据(若是其它类型就需要重载运算符“=”,后面的函数提到比对的都需要实现,才能进行比对)比对成功就返回,其它类的实现依然需要重载

template <class T>
int LinkList<T> ::located(const T &x)
{
if (_head == NULL)
{
cout << "List is empty,not found!!!" << endl;
return -1;
}
else if(_head->_data==x)
{
return 1;
}
else
{
LinkNode<T> * n = _head ;
int temp=0;
while (n->_next != NULL && n->_data != x )
{
n = n->_next;
temp++;
if (n->_data == x)
{
return temp+1;
}
}
}
return -1;
}

删除指定的元素遍历链表

一一比对,找到就调用chear()函数删除并清理空间

template <class T>
void LinkList<T> ::Delete_val(const T &x)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return;
}
else if (_head->_next == NULL && _head->_data == x)
{
clear(_head);
return;
}
else
{
if(_head->_data==x){
while(1){
if(_head->_data!=x)break;
PopFront();
}
}
else
{
while(1)
{
LinkNode<T> * n = Find(x);
if(n==NULL)break;
else{
LinkNode<T> * begin = _head;
while (begin->_next != n && begin->_next != NULL)
{
begin = begin->_next;
}
begin->_next = n->_next;
clear(n);
} }
return ;
}
}
return;
}

更新指定元素的值

遍历链表,一一比对(前面提到了重载的),找到要更新的元素后就将新的值赋值给它

template <class T>
inline void LinkList<T>::reset(const T &x,const T &y)
{
if (_head == NULL)
{
cout << "List is empty!!!" << endl;
return ;
}
else
{
while(1)
{
LinkNode<T> * n = Find(x);
if(n==NULL)break;
else{
n->_data=y;
}
}
}
}

链表的数据写入文件

能实现所有数据的储存,但是如果是其他类,写入没有问题,如果想从同一个文件中读入之前写的数据,遇到一些困难没有实现,但是基本数据类型是ok的,测试函数就表现出来了的(其它类的读入需要重载输入流,输出流)

template <class T>
inline bool LinkList<T>::writeToFile()
{
ofstream writefile("data.txt");
int len;
len=Length();
writefile<<len<<endl;
LinkNode<T> * begin=_head;
while(begin!=NULL)
{
writefile<<begin->_data<<endl;
begin=begin->_next;
}
return true;
}

从文件读入先前写入的数据,上面有提到只能满足基本数据类型

template <class T>
inline T* LinkList<T>::readFromFile()
{
ifstream readfile("data.txt");
int length;
readfile>>length;
T *temp=new T[length];
for(int i=0;i<length;i++)
{
readfile>>temp[i];
}
return temp;
}
为了更好的操作,测试相关函数的功能,写了这样一个函数来从文件得到链表长度的函数
template <class T>
inline int LinkList<T>::readlen()
{
ifstream readfile("data.txt");
int length;
readfile>>length;
return length;
}

写到这基本上函数的功能都实现了,这个链表结构肯定有很多不完善的地方,第一次写博客,还望大家见谅。上面提到了其它类的重载问题,我就把写的一个point类分享给大家。

class Point
{
private:
double x;
double y;
public:
Point(double x=0.0,double y=0.0)
{this->x=x,this->y=y;}
void setdata(double a,double b)
{this->x=a;this->y=b;}
Point operator=(Point a);//重载运算符“=”
int operator==(Point a);//重载运算符“==”
int operator!=(Point a);
double getx()
{return x;}
double gety()
{return y;}
//重载输入流
friend istream &operator>>(istream &is,Point &p)
{
is>>p.x;
is>>p.y;
return is;
}//重载输出流
friend ostream &operator<<(ostream &os,Point p)
{
os<<"("<<p.x<<","<<p.y<<")";
return os;
}
}; inline Point Point::operator=(Point a)
{
this->x=a.x;
this->y=a.y;
return *this;
}
inline int Point::operator==(Point a)
{
if(this->x==a.getx()&&this->y==a.gety())
{
return 1;
}
else {
return 0;
}
}
inline int Point::operator!=(Point a)
{
if(this->x!=a.getx()&&this->y!=a.gety())
{
return 1;
}
else if(this->x!=a.getx()||this->y!=a.gety()){
return 1;
}
else {
return 0;
}
}
一个输入之后的展示

C++线性表的链式存储结构

C++线性表的链式存储结构

以上是C++顺序表使用过程中最常用的主要操作,相关完整代码已经push到GitHub,需要的小伙伴自行clone,如果觉得还不错的话,欢迎Star,这里是传送门C++链式顺序表,除此之外,想要了解更多的C,C++,Java,Python等相关知识的童鞋,欢迎来我的博客相逢的博客,我们一起讨论!