21、线性表链式存储结构

链式存储逻辑结构:基于链式存储结构的线性表中,每个结点都包含数据域和指针域:数据域存储数据元素本身,指针域存储相邻结点的地址。(物理上没关系,逻辑上有关系)

顺序表:基于顺序存储结构的线性表。

链表:基于链式存储机构的线性表。  

链表中的基本概念:

头结点:链表中的辅助结点,包含指向第一个数据元素的指针(简化代码的编写)。意义:辅助数据元素的定位,方便插入和删除操作;因此,头结点不存储实际的数据类型。

数据节点:链表中代表数据元素的节点,表现形式为:(数据元素,地址)

尾节点:链表中的最后一个数据结点,包含的地址信息为空(单链表)(如果地址信息是第零个数据元素的地址信息,那么是循环链表)(如果是随机值,则是非法链表)。

单向链表:每个节点只有下一个节点的指针,只能从一个方向对其进行遍历,而且不能循环进行。表头指针指向表头节点,表尾指针指向最后一个节点,这个节点不在指向其他节点,它称为表尾,指针值为NULL。

在目标位置处插入数据元素:

1、从头结点开始,通过crrent指针定位到目标位置(i为几就移动几次,移动到当前序号结点的前一个结点,如插入位置1,移动到0)。

2、从堆空间申请新的Node结点。

扫描二维码关注公众号,回复: 946938 查看本文章

3、执行操作:

node->value=e;(插入的数据)

node->next=current->next;(新插入的结点中的地址是current结点(要插入之前)中的地址也就是下一个结点的地址)

current->next=node;(current结点的中的地址为要插入结点的地址(而不是它里边的地址))

在目标位置处删除元素:

1、从头结点开始,通过current指针定位到目标位置(注意:目标位置的地址为前一个结点中的内容,所以是前一个结点的地址赋值给current)(i为几就移动几次,移动到当前序号结点的前一个结点,如插入位置1,移动到0中的地址赋值给current)。所以要删除的是current->next。

2、使用toDel指针指向需要删除的结点。

3、指向操作:

toDel=current->next;  (要删除的)

current->next=toDel->next;  (跳过要删除的结点)

delete toDel;

小结:链表中的数据元素在物理内存中无相邻关系,链表中的结点都包含数据域和指针域(新的数据类型:结点),头节点用于辅助数据元素的定位,方便插入和删除操作。插入和删除操作需要保证链表的完整性。

/*
LinkList设计要点
类模板,通过头结点访问后继节点。
定义内部结点类型Node,用于描述数据域和指针域。
实现线性表的关键操作(增,删,查等)
*/
#ifndef LINKLIST_H_
#define LINKLIST_H_
#include "Wobject.h"
#include "List.h"
#include "Exception.h"
namespace WSlib
{
template <typename T>
class LinkList:public List<T> 
{
protected:
struct Node:public Wobject  //继承这个顶层父类的意思是继承new/delete,在创建一个结点的时候,需要在堆空间申请内存
{
T value;
Node* next;
};
//mutable Node m_header;  //mutable:const成员函数中能取地址
mutable struct:public Wobject //匿名类也必须继承顶层父类,如果不继承,就与上边的类型内存布局的不同,导致错误
      {
    char reserved[sizeof(T)]; //定义单链表对象时对成员进行构造,构造头结点对象时发现这里实现中不可能调用任何构造函数了,数组没什么用只是为了占空间
Node* next;                //头结点在内存布局上和之前是没有差异的,差异仅仅是不管T是什么都不会调用构造函数了。
      }m_header;


int m_length;


Node* position(int i) const
{
Node* ret=reinterpret_cast<Node*>(&m_header);  //虽然匿名类型在内存布局上和Node是一样的,但是由于类型不同不能初始化,需要用reinterpret_cast
for(int p=0;p<i;p++)
{
ret=ret->next;
}
return ret;
}
public:
LinkList()
{
m_header.next=NULL;
m_length=0;
}
bool insert(const T& e)
{
return insert(m_length,e);
}
bool insert(int i,const T& e)
{
bool ret=((0<=i)&&(i<=m_length));  //插入操作的话是i<=m_length的
if(ret)
{
Node* node=new Node();
if(node !=NULL)
{
Node* current=position(i);
/*
Node* current=&m_header;
for(int p=0;p<i;p++)
{
current=current->next;
}*/
node->value=e;
node->next=current->next;
current->next=node;
m_length++;
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException,"no enough memory...");
}
}
return ret;
}
bool remove(int i)
{
bool ret=((i>=0)&&(i<m_length));
if(ret)
{
Node* current=position(i);
/*
Node* current=&m_header;
for(int p=0;p<i;p++)
{
current=current->next;
}*/
Node* toDel=current->next;
current->next=toDel->next;
delete toDel;
m_length--;
}
return ret;
}
bool set(int i,const T& e)
{
bool ret=((0<=i)&&(i<m_length));
if(ret)
{

/*Node* current=&m_header;
for(int p=0;p<i;p++)
{
current=current->next;
}*/


//Node* current=position(i);
//current->next->value=e;
position(i)->next->value=e;
}
return ret;
}
T get(int i) const
{
T ret;
if(get(i,ret))
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException,"Invalid parameter i to get element...");
}
return ret;
}
bool get(int i,T& e) const
{
bool ret=((0<=i)&&(i<m_length));
if(ret)
{
/*Node* current=&m_header;  //由于当前成员函数是const成员函数,不允许改变任何成员变量的值,&取地址符编译器认为是有可能修改成员变量的值,
for(int p=0;p<i;p++)      //解决:只需要在对应的成员变量前加mutable声明就可以了
{
current=current->next;
}*/


//Node* current=position(i);
//e=current->next->value;

e=position(i)->next->value;  //因为是const成员函数,不可以调用position,所以position函数得定义为const成员函数
}
return ret;
}
int length() const
{
return m_length;
}
void clear()
{
while(m_header.next)   //m_header是类,对象用 . 而不是指针,指针用->
{
Node* toDel=m_header.next;
m_header.next=toDel->next;
delete toDel;
}
m_length=0;
}
~LinkList()
{
clear(); //如果单链表需要销毁,先将单链表对象中的每一个结点删除了,然后销毁单链表对象,所以在析构函数中调用clear()函数
}


};
}
#endif
/*
问题:
头结点是否存在隐患?实现代码是否需要优化?
头结点中 Node m_header;只使用了头结点的指针域,却没有使用头结点的数据域,而在定义对象的时候
如果
class Test
{
public:
Test()
{
throw 0;
}
};
用户这样定义 LinkList<Test>list;
单看这个代码是没有问题的,因为代码没有创建Test类对象。原因是创建list对象时,会构造成员对象,
构建头结点对象时会构造value,此时value是Test对象,是一个有问题的对象,所以抛出异常
方法:构造头结点时,不去调用泛指类型的构造函数,定义另一个内部的匿名新类型,没有类型名
mutable struct
{
char reserved[sizeof(T)]; //定义单链表对象时对成员进行构造,构造头结点对象时发现这里实现中不可能调用任何构造函数了,数组没什么用只是为了占空间
Node* next;                //头结点在内存布局上和之前是没有差异的,差异仅仅是不管T是什么都不会调用构造函数了。
}m_header;
代码优化:定位函数position
*/
/***************************************************************************************************
#include <iostream>
#include "LinkList.h"
using namespace WSlib;
using namespace std;


int main()
{
LinkList<int> lis;
LinkList<int> list;
for(int i=0;i<5;i++)
{
list.insert(0,i);
list.set(0,i*i);
}
for(int i=0;i<list.length();i++)
{
cout<<list.get(i)<<endl;
}
//list.remove(2);
list.remove(3);
//list.clear();
    for(int i=0;i<list.length();i++)
{
    cout<<list.get(i)<<endl;
}

return 0;
}
************************************************************************************************/
//通过类模板实现链表,包含头结点成员和长度成员。
//定义节点类型,并通过堆中的结点对象构成链式存储
//为了避免构造错误的隐患,头结点类型需要重定义
//代码优化是编码完成后必不可少的环节,代码只要改动就要重新测试

抽象类:就是包含有未定义的虚函数的类,也就是说只在类中声明了一个抽象类,但没有具体定义,或者把虚函数定义为virtual void f()=0;这样的形式。注意函数后有一个=0。因此派生类必须实现这个函数, 如果派生类没有实现这个函数,则这个派生类也是抽象的。抽象类为什么不能实例化对象:  因为抽象类中包含有没有定义的函数,因此不能用抽象类来实例化对象。 但可以声明抽象类的指针指向派生类。


猜你喜欢

转载自blog.csdn.net/ws857707645/article/details/80362748