数据结构篇————线性表
线性表的抽象数据类型的定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,....,an},每个元素的类型均为DataType。其中,除了第一个元素a1外,每一个元素有且只有一个直接前驱元素,除最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表。
ListEmpty(L):若线性表为空,返回true,否则返回false。
ClearList(*L):线性表清空。
GetElem(L,i,*e):将线性表L中第i个位置元素返回给e。
LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序列号;否则,返回0表示失败。
ListInsert(*L,i,e):在线性表的第i个位置插入元素e。
ListDelete(*L,i,*e):删除线性表L中的第i个元素,并用e返回其值
ListLength(L):返回线性表L的元素个数。
PrintList(L):打印线性表
对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的。
对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。
线性表顺序存储
顺序表,一般使用数组实现,事实上就是在内存中找个初始地址,然后通过占位的形式,把一定连续的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中,数组大小有两种方式指定,一是静态分配,二是动态扩展。
线性表我用类模板来实现,提高实用性
const int maxx=100;
template <class datatype>
class seqlist
{
private:
datatype data[maxx];
int lenght;
public:
seqlist(){lenght=0;};//无参数的构造函数
seqlist(datatype a[],int n);//有参数的构造函数
~seqlist(){}//析构函数,直接析构。
int _lenght(){return lenght;}//返回链表的当前长度。
datatype _getvalue(int i);//得到第i个元素的值
int _location(datatype data1);//返回元素value的位置
void _insert(int i,datatype value);//插入一个参数
datatype _delete(int i);//删除一个元素
void _printf();//遍历线性表的每一个元素。
};
顺序表的封装需要三个属性:
- 存储空间的起始位置。数组data的存储位置就是线性表存储空间的存储位置
- 线性表的最大存储容量。数组长度maxx
- 线性表的当前长度。length
注意:数组的长度与线性表的当前长度是不一样的。数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会改变的。
通过数组初始化有参构造函数
template <class datatype>
seqlist<datatype>::seqlist(datatype a[],int n)
{
if (n>maxx) cout<<"错误"<<endl;
else
{
for(int i=0;i<n;i++)
{
data[i]=a[i];
//p++;
}
lenght=n;
}
}
元素的查找
- 按位查找的时间复杂度为O(1) 。
template <class datatype>
datatype seqlist<datatype>::_getvalue(int n)
{
if(n<1 && n>lenght) {cout<<"出错"<<endl; return -1;}
else return data[n-1];
}
- 按值查找,需要对顺序表中的元素依次进行比较。
template <class datatype>
int seqlist<datatype>::_location(datatype data1)
{
for(int i=0;i<lenght;i++)
{
if(data[i]==data1) return i+1;
//else return -1;
}
return -1;
}
数据的插入操作
void seqlist<datatype>::_insert(int i,datatype value)
{
if(lenght>=maxx) cout<<"出错";
int j;
if(i<1||i>lenght+1) cout<<"出错";
for(j=lenght;j>i-1;j--)
data[j]=data[j-1];
data[j]=value;
lenght++;
}
数据的删除操作
template <class datatype>
datatype seqlist<datatype>::_delete(int i)
{
int x;
if(lenght==0) cout<<"出错";
if(i<1 || i>lenght) cout<<"出错";
x = data[i-1];
for(int j=i;j<lenght;j++)
data[j-1] = data[j];
lenght--;
return x;
}
数据的遍历
template <class datatype>
void seqlist<datatype>::_printf()
{
for(int i=0;i<lenght;i++)
cout<<data[i]<<endl;
}
主函数的实现操作
int main()
{
//本节点参考了如下文章:https://blog.csdn.net/qq_30611601/article/details/79516986#%E4%BA%94%E5%85%B6%E4%BB%96%E7%BA%BF%E6%80%A7%E8%A1%A8
int a[]={1,2,3,4,5,6,7,8,9,10},m=10;
seqlist<int> p(a,m);
p._insert(5,13);
p._insert(12,20);
cout<<p._getvalue(3)<<endl;
cout<<p._lenght()<<endl;
cout<<p._location(20)<<endl;
p._delete(7);
cout<<p._lenght()<<endl;
p._printf();
return 0;
}
优点:
随机访问特性,查找O(1)时间,存储密度高;
逻辑上相邻的元素,物理上也相邻;
无须为表中元素之间的逻辑关系而增加额外的存储空间;
缺点:
插入和删除需移动大量元素;
当线性表长度变化较大时,难以确定存储空间的容量;
造成存储空间的“碎片”
线性表的链式存储
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些元素可以存在内存未被占用的任意位置,链表的定义是递归的,它或者为空null,或者指向另一个节点node的引用,这个节点含有下一个节点或链表的引用,线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)
存储的实现
结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
struct node //结点结构
{ int data ;
node * next;
//Node<DataType>这个和node没有区别,唯一的区别是node结构体中的数据类型都为DataType,学会了这种的同学可以试着把int数据类型变为类模板实现。
};
*单链表的类的实现 *
class linklist
{
private:
node *first;
public:
linklist();//无参构造,生成一个只有头结点的空链表
linklist(int a[],int n);//有参构造。
~linklist();//析构函数
int _length();//返回链表的长度
int _get(int i);//返回第i个元素的值
int _locate(int num);//返回值num的序号,从1开始。
void _insret(int i,int num);//中间插入元素:i表示第i个元素,元素的值为num.
int _Delete(int i);//删除第i个元素的值。
void _print ();//遍历整个链表。
};
无参数构造
只生成一个一个空的头结点。代码实现:
linklist::linklist()//构建空链表
{
first = new node;
first->next = NULL;
}
有参数的构造———头插法构造
头插法是每次将新申请的结点插在头结点后面 ,代码实现:
linklist::linklist(int a[],int n)//头插法插入链表
{
first =new node ;//
first->next=NULL;//这两行是头结点,next指向空。
for(int i=0;i<n;i++)
{
node *s=new node;
s->data=a[i];
s->next=first->next;
first->next=s;
}
}
有参数的构造———尾插法构造
尾插法就是每次将新申请的结点插在终端节点的后面 ,代码实现:
linklist::linklist(int a[],int n)//尾插法就是每次将新申请的结点插在终端节点的后面
{
first=new node ;
node *p=first;
for(int i=0;i<n;i++)
{
node *s=new node;
s->data=a[i];
p->next=s;
p=s;//把s节点赋值给p节点。
}
p->next=NULL;//链表的最后一个节点的指针为空。
}
析构函数
单链表类中的结点是用new申请的,在释放的时候无法自动释放,所以,析构函数要将单链表中的结点空间释放。
linklist::~linklist()
{
if(first!=NULL)
{
node *p=first;
first=first->next;
delete p;
}
}
计算链表的长度
单链表中不能直接求出长度,所以我们只能将单链表扫描一遍,所以时间复杂度为O(n)。
int linklist::_length()
{
node *p=first->next;
int count=0;
while(p!=NULL)
{
p=p->next;
count++;
}
return count;
}
查找第i个元素的值
单链表中即使知道节点位置也不能直接访问,需要从头指针开始逐个节点向下搜索,平均时间性能为O(n)
int linklist::_get(int i)
{
node *p=first->next;
int ocunt =1;
while(p!=NULL&&ocunt<i)
{
p=p->next;
ocunt++;
}
if(p==NULL) {return 0;cout<<"不存在"<<endl;}
else return p->data;
}
查找元素a,返回元素的位置
单链表中按值查找与顺序表中的实现方法类似,对链表中的元素依次进行比较,平均时间性能为O(n)
int linklist::_locate(int num)
{
node *p=first->next;
int count=1;
while(p!=NULL)
{
if(p->data==num) return count;
p=p->next;
count ++;
}
return 0;
}
插入元素
单链表在插入过程中需要注意分析在表头、表中间、表尾的三种情况,由于单链表带头结点,这三种情况的操作语句一致,不用特殊处理,时间复杂度为O(n)。
void linklist::_insret(int i,int num)//i表示第i个元素,元素的值为num.
//单链表在插入过程中需要注意分析在表头、表中间、表尾的三种情况,由于单链表带头结点,
//这三种情况的操作语句一致,不用特殊处理,时间复杂度为O(n)
{
node *p=first;
int count=0;
while(p!=NULL&&count<i-1)
{
p=p->next;
count++;
}
if(p==NULL) cout<<"错误";
else
{
node *s= new node ;
s->data=num;
s->next=p->next;
p->next=s;
}
}
删除操作
删除操作时需要注意表尾的特殊情况,此时虽然被删结点不存在,但其前驱结点却存在。因此仅当被删结点的前驱结点存在且不是终端节点时,才能确定被删节点存在,时间复杂度为O(n) .
int linklist::_Delete(int i)
{
node *p=first;
int count =0;
while(p!=0&&count<i-1)
{
p=p->next;
count++;
}
if(p==NULL){cout<<"不存在";
return 0;}
else
{
node *s=p->next;
int x=s->data;
p->next=s->next;
return x;
}
}
遍历元素
遍历单链表时间复杂度为O(n) .
void linklist::_print ()
{
node *p=first->next;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}