线性表
- 线性表是由n(n>=0)个相同类型的数据元素组成的有限序列,它是最基本、最常用的一种线性结构。顾名思义,线性表就像是一条线,不会分叉。线性表有唯一的开始和结束,除了第一个元素外,每个元素都有唯一的直接前驱:除了最后一个元素外,每个元素都有唯一的直接后继,如图所示。
- 线性表有两种存储方式:顺序存储和链式存储。采用顺序存储的线性表称为顺序表,采用链式存储的线性表称为链表。链表又分为单链表、双向链表和循环链表。
顺序表
- 顺序表采用顺序存储方式,即逻辑上相邻的数据在计算机内的存储位置也是相邻的。
- 顺序存储方式,元素存储是连续的,中间不允许有空,可以快速定位第几个元素,但是插入和删除时需要移动大量元素。
- 根据分配空间方法不同,顺序表可以分为静态分配和动态分配两种方法。
静态分配
顺序表最简单的方法是使用一个定长数组data[]存储数据,最大空间为Maxsize,用length记录实际的元素个数,即顺序表的长度。这种用定长数组存储的方法称为静态分配。静态顺序表如图所示。
顺序表的静态分配结构体定义,如图所示。采用静态分配方法,定长数组需要预先分配一段固定大小的连续空间,但是在运算的过程中,如合并、插入等操作,容易超过预分配的空间长度,出现溢出。解决静态分配的溢出问题,可以采用动态分配的方法。
动态分配
在程序运行过程中,根据需要动态分配一段连续的空间(大小为Maxsize),用elem记录该空间的基地址(首地址),用length记录实际的元素个数,即顺序表的长度。采用动态存储方法,在运算过程中,如果发生溢出,可以另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储空间的目的
问题1,使用typedef有什么好处?
typedef是C/C++语言的关键字,用于给原有数据类型起一个别名,在程序中可以等价使用,语法规则如下。
typedef 类型名称 类型标识符
- “类型名称”为已知数据类型,包括基本数据类型(如int、float等)和用户自定义数据类型(如用struct自定义的结构体)。
- “类型标识符”是为原有数据类型起的别名,需要满足标识符命名规则。就像给某个人起一个小名或绰号一样
问题2:为什么使用ElemType作为数据类型?
使用ElemType是为了让算法的通用性更好,因为使用线性表的结构体定义后,并不清楚具体问题处理的数据是什么类型,不能简单地写成某一种类型。结合typedef使用,可以提高算法的通用性和可移植性。
顺序表的基本操作
初始化是指为顺序表分配一段预定义大小的连续空间,用elem记录这段空间的基地址,当前空间内没有任何数据元素,因此元素的实际个数为0。假设我们已经预定义了一个最大空间数Maxsize,那么就用new分配大小为Maxsize的空间,分配成功会返回空间的首地址,分配失败会返回空指针。
#include <iostream>
using namespace std;
#define Maxsize 100//最大空间
typedef struct {
int *elem;//基地址,前面加*表示取地址中的内容
int length;//顺序表的长度
}SqlList;
bool InitList(SqlList &L){//构造一个空的顺序表L
//L前加&表示引用参数,函数内部的改变跳出函数后仍然有效
//如果不加&,函数内部改变跳出函数后无效
L.elem=new int[Maxsize];//为顺序表动态分配Maxsize个空间
if(!L.elem)return false;//分配空间失败
L.length=0;//顺序表长度为0
return true;
}
bool CreateList(SqlList &L){//创建一个顺序表
int x,i=0;
cin >>x;
while(x!=-1){//当输入-1时结束,也可以设置其他的条件结束
if(L.length==Maxsize){
cout <<"顺序表已满";
return false;
}
L.elem[i++]=x; //将数据存入第i个位置,然后i++
L.length++;//顺序表长度加1
cin >>x;//继续输入
}
return true;
}
bool GetElem(SqlList L,int i,int &e){
if(i<1||i>L.length)return false;
e=L.elem[i-1];
return true;
}
int LocateElem(SqlList L,int e){
for(int i=0;i<L.length;i++){
if(L.elem[i]==e)return i+1;//下标为i,实际为i+1个元素
}
return -1;
}
int InsertElem(SqlList &L,int i,int e){
if(i<1||i>L.length+1)return false;
if(L.length==Maxsize)return false;
for(int j=L.length-1;j>=i-1;j--){
L.elem[j+1]=L.elem[j];
}
L.elem[i-1]=e;
L.length++;
return true;
}
bool DeleteElem(SqlList &L,int i,int &e){
if(i<1||i>L.length)return false;
e = L.elem[i-1];//将欲删除的元素保存在e中
for(int j=i;j<=L.length-1;j++){
L.elem[j-1]=L.elem[j];
}
L.length--;
return true;
}
void print(SqlList L)
{
cout<<"输出顺序表"<<endl;
for(int j=0;j<=L.length-1;j++)
cout<<L.elem[j]<<" ";
cout<<endl;
}
void DestroyList(SqlList &L)
{
//使用new申请的内存,释放时用delete,使用new [ ]申请的内存释放时要用delete [ ]
if (L.elem) delete []L.elem; //释放存储空间
}
int main()
{
SqlList myL;
int i,e,x;
cout << "1. 初始化\n";
cout << "2. 创建\n";
cout << "3. 取值\n";
cout << "4. 查找\n";
cout << "5. 插入\n";
cout << "6. 删除\n";
cout << "7. 输出\n";
cout << "8. 销毁\n";
cout << "0. 退出\n";
int choose=-1;
while(choose!= 0)
{
cout<<"请选择:";
cin>>choose;
switch(choose)
{
case 1://初始化顺序表
cout<<"顺序表初始化..."<<endl;
if(InitList(myL))
cout<<"顺序表初始化成功!"<<endl;
else
cout<<"顺序表初始化失败!"<<endl;
break;
case 2://创建顺序表
cout<<"顺序表创建..."<<endl;
cout<<"输入整型数,输入-1结束"<<endl;
if(CreateList(myL))
cout<<"顺序表创建成功!"<<endl;
else
cout<<"顺序表创建失败!"<<endl;
break;
case 3://取值
cout <<"输入整型数i,取第i个元素输出"<<endl;
cin>>i;
if(GetElem(myL,i,e))
cout<<"第i个元素是: "<<e<<endl;
else
cout<<"顺序表取值失败!"<<endl;;
cout<<"第i个元素是: "<<e<<endl;
break;
case 4://查找
cout << "请输入要查找的数x:";
cin>>x;
if(LocateElem(myL,x)==-1)
cout<<"查找失败!"<<endl;
else
cout<<"查找成功!"<<endl;
break;
case 5://插入
cout<<"请输入要插入的位置和要插入的数据元素e:";
cin>>i>>e;
if(InsertElem(myL,i,e))
cout<<"插入成功!"<< endl;
else
cout<<"插入失败!"<<endl;
break;
case 6://删除
cout<<"请输入要删除的位置i:";
cin>>i;
if(DeleteElem(myL,i,e))
cout<<" 删除成功!"<<endl;
else
cout<<"删除失败!"<<endl;
break;
case 7://输出
print(myL);
break;
case 8://销毁
cout<<"顺序表销毁..."<<endl;
DestroyList(myL);
break;
}
}
return 0;
}
单链表
链表是线性表的链式存储方式。逻辑上相邻的数据在计算机内的存储位置不一定相邻。那么怎么表示逻辑上的相邻关系呢?
存储方式
可以给每个元素附加一个指针域,指向下一个元素的存储位置,如图
从图中可以看出,每个节点包含两个域:数据域和指针域。数据域存储数据元素,指针域存储下一个节点的地址,因此指针指向的类型也是节点类型。每个指针都指向下一个节点,都是朝一个方向的,这样的链表称为单链表
结构定义
顺序存取
在顺序表中,想找第i个元素,可以立即通过L.elem[i−1]找到,想找哪个就找哪个,称为随机存取。但是在单链表中,想找第i个元素就没那么容易,必须从头开始,按顺序一个一个找,一直数到第i个元素,称为顺序存取
单链表的基本操作
初始化
单链表的初始化是指构建一个空表。先创建一个头节点,不存储数据,然后令其指针域为空,如图
插入(头插)
插入头节点的后面
头插法每次把新节点插入到头节点之后,创建的单链表和数据输入顺序相反
等号右侧是节点的地址,左侧是节点的指针域
① s->next=L->next:L->next存储的是下一个节点地址“9630”,将该地址赋值给s->next指针域,即s节点的指针指向1节点
② L->next=s:将s节点的地址“2046”赋值给L->next指针域,即节点的指针指向s节点
修改指针顺序?
为什么要先修改后面那个指针呢?
因为一旦修改了L节点的指针域指向s,那么原来L节点后面的节点就找不到了,因此修改指针是有顺序的。
原则:先修改没有指针标记的那一端
插入(尾插)
其创建的单链表和数据输入顺序一致
尾插法每次把新节点链接到链表的尾部,因此需要一个尾指针永远指向链表的尾节点
取值
查找
在一个单链表中查找是否存在元素e,可以定义一个p指针,指向第一个元素节点,比较p指向节点的数据域是否等于e。如果相等,查找成功,返回true;如果不等,则p指向的下一个节点,继续比较,如果为空,查找失败,返回false,如图
删除
删除一个节点,实际上是把这个节点跳过去。根据单向链表向后操作的特性,要想跳过第i个节点,就必须先找到第−1个节点,否则是无法跳过去的。
#include <iostream>
using namespace std;
//typedef 将结构体等价为类型名Lnode,指针Linklist
typedef struct Lnode{
int data;
struct Lnode *next;//指向下一个节点的指针
}Lnode,*LinkList;
bool InitList(LinkList &L){//构建一个空的单链表L
L = new Lnode;//生成新节点作为头节点
if(!L)return false;//生成失败
L->next=NULL;//头节点的指针域置空
return true;
}
void CreateListT(LinkList &L){//头插法创建单链表
int n;//输入n个元素的值,建立到头节点的单链表L
LinkList s;//定义一个指针变量
L = new Lnode;
L->next=NULL;
cout<<"输入n个元素"<<endl;
cin >>n;
cout<<"请依次输入n个元素"<<endl;
cout<<"头插法创建单链表"<<endl;
while(n--){
s=new Lnode;//创建新节点
cin >>s->data;
s->next=L->next;
L->next=s;//将节点s插入头节点后面
}
}
void CreateListW(LinkList &L){//尾插法创建单链表
int n;//输入n个元素的值,建立到头节点的单链表L
LinkList s,r;
L = new Lnode;
L->next=NULL;//先建立一个带头节点的空链表
r=L;//尾指针r指向头节点
cout<<"输入n个元素"<<endl;
cin >>n;
cout<<"请依次输入n个元素"<<endl;
cout<<"尾插法创建单链表"<<endl;
while(n--){
s=new Lnode;//创建新节点
cin >>s->data;
s->next=NULL;
r->next=s;//将节点s插入尾节点后面
r=s;//r指向新的尾节点 s
}
}
bool GetElem(LinkList L,int i,int &e){//单链表取值
//在带头节点的单链表L中查找第i个元素,e记录第i个元素的值
int j;
LinkList p;
p=L->next;//定义p指针,指向第一个数据节点
j=1;//j为计数器
while(j<i&&p){//顺着链表向后扫描,直到p指向第i个元素或p为空
p=p->next;//p指向下一个节点
j++;
}
if(!p||j<i)return false;//p为空,或j值<i
e=p->data;//取第i个节点的数据域
return true;
}
bool LocateElem(LinkList L,int e){//在带头节点的单链表L中查找值为e的元素
LinkList p;
p=L->next;//定义p指针,指向第一个数据节点
while(p&&p->data!=e){//p不为空且未找到则继续
p=p->next;//指向下一节点
}
if(!p)return false;//查找失败,p为NULL
return true;
}
bool InsertElem(LinkList &L,int i,int e){
//在带头节点的单链表L中第i个位置之前插入值为e的新节点
int j;
LinkList p,s;
p=L->next;//定义p指针,指向第一个数据节点
j=1;
while(p&&j<i-1){//p不为空且j<i-1,则继续
p=p->next;
j++;
}
if(!p||j>i-1)return false;//p为空,或j不满足
s = new Lnode;
s->data=e;
s->next=p->next;//类似于头插
p->next=s;
return true;
}
bool DeleteElem(LinkList &L,int i){
//在带头节点的单链表L中,删除第i个位置
int j;
LinkList p,q;
p=L->next;
j=1;
while(p&&j<i-1){
p=p->next;
j++;
}
if(!p||j>i-1)return false;
q=p->next;//临时保存被删除节点的地址已备释放空间(第i个节点)
p->next=q->next;
delete q;//释放第i个节点的空间
return true;
}
void PrintElem(LinkList L){
LinkList p;
p=L->next;
while(p){
cout <<p->data<<" ";
p=p->next;
}
cout <<endl;
}
int main()
{
int i,x,e,choose;
LinkList L;
cout<<"1. 初始化\n";
cout<<"2. 创建单链表(前插法)\n";
cout<<"3. 创建单链表(尾插法)\n";
cout<<"4. 取值\n";
cout<<"5. 查找\n";
cout<<"6. 插入\n";
cout<<"7. 删除\n";
cout<<"8. 输出\n";
cout<<"0. 退出\n";
choose=-1;
while(choose!=0)
{
cout<<"请输入数字选择:";
cin>>choose;
switch(choose)
{
case 1: //初始化一个空的单链表
if(InitList(L))
cout<<"初始化一个空的单链表!\n";
break;
case 2: //创建单链表(前插法)
CreateListT(L);
cout<<"头插法创建单链表输出结果:\n";
PrintElem(L);
break;
case 3: //创建单链表(尾插法)
CreateListW(L);
cout<<"尾插法创建单链表输出结果:\n";
PrintElem(L);
break;
case 4: //单链表的按序号取值
cout<<"请输入一个位置i用来取值:";
cin>>i;
if(GetElem(L,i,e))
{
cout<<"查找成功\n";
cout<<"第"<<i<<"个元素是:"<<e<<endl;
}
else
cout<<"查找失败\n\n";
break;
case 5: //单链表的按值查找
cout<<"请输入所要查找元素x:";
cin>>x;
if(LocateElem(L,x))
cout<<"查找成功\n";
else
cout<<"查找失败! "<<endl;
break;
case 6: //单链表的插入
cout<<"请输入插入的位置和元素(用空格隔开):";
cin>>i;
cin>>x;
if(InsertElem(L,i,x))
cout<<"插入成功.\n\n";
else
cout<<"插入失败!\n\n";
break;
case 7: //单链表的删除
cout<<"请输入所要删除的元素位置i:";
cin>>i;
if(DeleteElem(L, i))
cout<<"删除成功!\n";
else
cout<<"删除失败!\n";
break;
case 8: //单链表的输出
cout<<"当前单链表的数据元素分别为:\n";
PrintElem(L);
cout<<endl;
break;
}
}
return 0;
}
双向链表
定义
单链表只能向后操作,不可以向前操作。为了向前、向后操作方便,可以给每个元素附加两个指针域,一个存储前一个元素的地址,另一个存储下一个元素的地址。这种链表称为双向链表
结构体定义
链表优缺点
优点:链表是动态存储,不需要预先分配最大空间;插入删除不需要移动元素。
缺点:每次动态分配一个节点,每个节点的地址是不连续的,需要有指针域记录下一个节点的地址,指针域需要占用一个int的空间,因此存储密度低(数据所占空间/节点所占总空间)。存取元素必须从头到尾按顺序查找,属于顺序存取。
双向链表的基本操作
创建(头插)
插入
单链表只有一个指针域,是向后操作的,不可以向前处理,因此单链表如果在第i个节点之前插入一个元素,就必须先找到第i−1个节点。在第i个节点之前插入一个元素相当于把新节点放在第i−1个节点之后。而双向链表不需要,因为有两个指针,可以向前后两个方向操作,直接找到第i个节点,就可以把新节点插入第i个节点之前。这里假设第i个节点是存在的,如果第i个节点不存在,而第i−1个节点存在,还是需要找到第i−1个节点,将新节点插入第−1个节点之后,如图
#include <iostream>
using namespace std;
typedef struct DuLnode{
int data;
struct DuLnode *prior,*next;
}DuLnode,*DuLinklist;
bool InitList(DuLinklist &L){//构建一个空的双向链表
L = new DuLnode;//生成新节点作为头节点,用头指针L指向头节点
if(!L)return false;
L->prior=L->next=NULL;//头节点的两个指针域置空
return true;
}
void CreateListT(DuLinklist &L){//头插法创建双向链表
//输入n个元素的值,建立到头结点的单链表L
int n;
DuLinklist s;//定义一个指针变量
L=new DuLnode;
L->prior=L->next=NULL;
cout <<"请输入元素个数n: "<<endl;
cin >>n;
cout <<"请依次输入n个元素: "<<endl;
cout <<" 头插法创建单链表"<<endl;
while(n--){
s=new DuLnode;//生成新节点
cin >>s->data;
if(L->next){
//如果L后面有节点,则修改其后面节点的prior指针,否则只修改后面3个指针即可
L->next->prior=s;
}
s->next=L->next;
s->prior=L;
L->next=s;
}
}
bool InsertElem(DuLinklist &L,int i,int &e){
int j=1;
DuLinklist p,s;
p=L->next;
while(p&&j<i){
p=p->next;
j++;
}
if(!p||j>i)return false;
s = new DuLnode;
s->data=e;
p->prior->next=s;
s->prior=p->prior;
s->next=p;
p->prior=s;
return true;
}
bool DeleteElem(DuLinklist &L,int i){
int j=1;
DuLinklist p;
p=L->next;
while(p&&j<i){
p=p->next;
j++;
}
if(!p||j>i)return false;
if(p->next){
p->next->prior=p->prior;
}
p->prior->next=p->next;
delete p;//释放被删除节点的空间
return true;
}