介绍线性表之前我们先了解一下线性结构,上篇说到数据结构从逻辑上分为线性结构和非线性结构两种。
线性结构由一个B=(K,R)的二元组组成,其中K={a0,a1,...,an-1},R={r},K中存储的是线性结构集合中的元素,R维护节点之间的关系。
对于线性结构中的非空集合K一定有一个唯一的开始结点,它没有前驱结点,只能有一个唯一的直接后继结点。还会存在一个唯一的终止结点,它有一个唯一的直接前驱,而没有后继结点。其它的结点被称为内部结点,每一个内部结点都有且仅有一个唯一的直接前驱,也有唯一的一个直接后继。对于
维护前驱后继关系的r,具有反对称性和传递性。
均匀性:虽然不同的线性表的数据元素可以是各种各样的,但对于同一线性表的各数据元素必定具有相同的数据类型和长度。
有序性:各数据元素在线性表中都有自己的位置,且各数据元素之间的相对位置是线性的。
1.按复杂程度划分
简单的:线性表、栈、队列、散列表
高级的:广义表、多维数组、文件...
2.按访问方式划分
直接访问性、顺序访问型、目录索引型
线性表简称表,是零个或多个元素组成的有穷序列,通常可以表示成k0,k1,...,kn-1(n>=1)
表目:线性表中的元素
索引(下标):i称为表目ki的索引或下标
表的长度:线性表中所含元素的个数n
空表:长度=0的线性表(n=0)
操作灵活、长度可以增长缩短
所有表目都是同一类型结点
不限制操作形式
线性表的主要属性包括线性表的长度、表头(head)、表尾(tail)、当前位置(current position)
按存储结构分为顺序表和链表
顺序表的存储结构是按索引值从小到大存放在一片相邻的连续区域,结构紧凑,存储密度为1
链表的存储位置不需要相邻,链表由分为单链表、双链表、循环链表
按操作分类为线性表、栈、队列
栈的特点是插入和删除操作都限制在表的同一端进行,表中的元素先进后出
队列的特点是插入操作在表的一端,删除操作在另一端,表中的元素先进先出
template class List{
void clear(); //清空线性表的元素
bool isEmpty(); //判断线性表是否是空表
bool append(const T value); //在尾部追加元素,表长度+1
bool insert(const int p,const T value); //在位置p插入一个元素,表长度+1
bool delete(const int p); //删除位置p的元素,表长度-1
bool getPos(int& p,const T value); //查找值为value的元素,并返回位置p
bool getValue(const it p,T& value); //获取位置p上元素的值到value
bool setValue(const int p,const T value); //修改位置p元素的值为value
};
线性表也称为向量,采用定长的一维数组存储结构。
主要特性有:元素的类型相同、元素顺序的存储在连续存储空间中,每个元素有唯一的索引值、向量的长度为常数
特点:读写其数据很方便,通过下标即可指定位置。只要确定了首地址,线性表中任意数据元素都可以随机存取。
因为存储在连续的存储空间内所以元素地址的计算为:
Loc(ki)=Loc(k0)+c*i 其中c=sizeof(ELEM)即为顺序表中每个元素的size
class arrayList:public List{
private:
T * aList; //私有变量,存储顺序表的实例
int maxSize; //私有变量,存储表的最大长度
int curLen; //私有变量,表的当前长度
int position; //私有变量,当前处理位置
public:
arrayList(const int size){ //创建表时,设置表实例的最大长度
maxSize=size;
aList=new T[maxSize];
curLen=position=0;
}
~arrayList(){ //析构函数,用于消除该表的实例
delete [] alist;
}
void clear(){ //将顺序表的内容清除,成为空表
delete[] aList;
curLen=position=0;
aList=new T[maxSize];
}
int length(); //获取当前长度
bool append(const T value); //尾部插入结点
bool insert(const int p,const T value); //位置p插入结点
bool delete(const int p); //删除位置p结点
bool setValue(const int p,const T value); //设元素值
bool getValue(const int p,T& value); //返回元素
bool getPos(int &p,const T value); //查找元素位置
};
顺序表在插入时需要将待插入位置以后的结点都依次下移一位再执行插入操作
//设元素类型为T,aList是存储顺序表元素的数组,maxSize是其最大长度
//p为新元素value的插入位置,插入成功返回ture,否则返回false
template bool arrayList :: insert(const int p,const T value){
int i;
if(p<0 || p > curLen) {//检查位置p是否合法
cout << "Insertion position is illegal"<p;i--){ //从表尾到curLen-1起往右移动直到p
aList[i]=aList[i-1];
}
aList[p]=value; //位置p插入新元素
curLen++; //表长度+1
return true;
}
同样在删除时需要将待删除位置直到表尾所有元素上移一位
//设元素类型为T,aList是存储顺序表元素的数组,p为将删除元素的位置
//删除成功返回ture,否则返回false
template bool arrayList :: delete(const int p){
int i;
if(p<0||p>curLen-1){ //检查输入的合法性
cout<<"提示信息"<
表中元素的移动:
插入时:移动n-i个,时间复杂度O(n)
删除时:移动n-i-1个,时间复杂度O(n)
链表的特点是通过指针把它的一串存储结点链接成一个链,不同于顺序表,链表的存储结点由两部分组成:数据域(data)+指针域(next)(记录下个结点的存储地址)。
根据链接方式和指针多寡可以分为单链、双链、循环链表
单链表的结点由数据域+指针域组成
template class Link{
public:
T data;
Link * next;
Link(const T info,const Link * nextValue =NULL){
data=info;
next=nextValue;
}
Link(const Link * nextValue){
next=nextValue;
}
};
template class lnkList:public List {
private:
Link *head,*tail; //头、尾指针
Link *setPos(const int p); //第p个元素指针
public:
lnkList(int s); //构造函数
~lnkList(); //析构函数
bool isEmpty();
bool clear();
int length();
bool append(const T value);
bool insert(const int p,const T value);
bool delete(const int p);
bool getValue(const int p,T& value);
bool getPos(int &p;const T value);
};
单链表中查找只能从头节点开始,一个一个结点往下去查找
template Link * lnkList ::getPos(int i){
int count=0;
if(i==-1){
return head;
}
//循环定位,若i为0则定位到第一个结点
Link *p=head->next;
while(p!=NULL&&countnext;
count++;
};
return p;
}
假设需要在结点p后插入新结点q,那么将q->next=p->next,p->next=q即可完成
//插入数据内容为value的新结点作为第i个结点
template bool lnkList :: insert(const int i,const T value){
Link *p,*q;
if((p=getPos(i-1))==NULL){ //先查找第i-1个结点赋值给p,如果不存在,则插入非法
cout<<"非法插入点"<(value,p->next);
p->next=q;
if(p==tail){ //如果之前的第i-1个结点是表尾,那么p设置成新的表尾
tail=q;
}
return true;
}
假设需要从链表中删除结点p,步骤是:
1.更改p的上一个结点的next指向p的下一个结点
2.删除结点p,释放占据的空间
template bool lnkList :: delete(const int i){
Link *p,*q;
//查找待删结点的上一个结点并赋值给p,如果节点不存在或者p是尾结点都不合法
if((p=getPos(i-1))==NULL||p==tail){
cout<<"非法删除点"<next; //q是p的下一个结点也就是待删除节点
if(q==tail){ //如果待删除的结点是尾结点,那么将它的前一个结点p变成尾结点
tail=p;
p->next=NULL;
}
else{
p->next=q->next;
}
delete q;
return true;
}
对一个结点操作前,必须要先找到它,找单链表中任意一个结点,都必须从第一个结点开始
p=head; where(没有找到) p=p->next;继续寻找
因此单链表操作的时间复杂度均为O(n)
查找:O(n) 插入:O(n)+O(1) 删除:O(n)+O(1)
为了弥补单链表的不足而产生了双向链表,单链表的next字段仅仅指向后继结点,不能有效找到前驱结点,反之亦然。所以给单链表增加一个指针域prev让它指向上一个结点就成为了双向链表。双链表结构如下图所示:
双向链表在查询过程中可以按照prev指针域反向查找,插入时需要变更插入位置后继结点的prev指针域的值为新插入的结点p,删除时需要将待删除结点后继结点的prev指向待删除结点的前驱结点。代码就不再写上了,只是多了一步操作。
至于双向循环链表的特点是tail->next=head;也就是说链表首尾相连了,从链表中任意节点出发都能访问到其它所有结点,而且没有增加存储上的额外开销。
1.没有使用指针,不用付出额外的存储开销
2.线性表中元素的读访问非常简洁便利
1.无需了解线性表的长度
2.允许线性表的长度动态变化
3.能够适应经常插入删除内部元素的情况
总结一下就是:顺序表是存储静态数据的不二选择,而链表是存储动态变化数据的良方。
顺序表:
1.插入、删除运算时间复杂度O(n),查找为O(1)
2.需要预先申请固定长度的连续空间
3.如果数组元素很满,则没有结构性存储开销
链表:
1.插入、删除运算时间复杂度O(1),但查找第i个元素的运算时间复杂度为O(n),插入删除的综合时间复杂度为O(n)
2.存储利用了指针,动态的按照需要为表中元素分配存储空间
3.每个元素都有指针域的结构性存储开销
顺序表不适用的场合:
1.经常插入删除时,不适合使用顺序表
2.集合最大长度不能确定时,不适合使用顺序表
链表不适用的场合:
1.当读操作频率比插入删除操作频率大时,不适合使用
2.当指针的存储开销,和整个结点内容所占空间相比其比例较大时,应该慎重选择