线性表
线性表的定义和特点
线性表:由n(n≥0)个数据元素(结点)a1,a2,…,an组成的有限序列
- 其中数据元素的个数n定义为表的长度
- 当n=0时称为空表
- 将非空的线性表(n>0)记作:(a1,a2,…,an)
- 这里的数据元素ai(1≤i≤n)只是一个抽象的符号,其具体含义在不同的情况下可以不同
线性表的逻辑特征:
- 在非空的线性表,有且仅有一个开始结点a1,它没有直接前驱,而仅有一个直接后继a2;
- 有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前驱a_n-1;
- 其余的内部结点ai(2≤i≤n-1)都有且仅有一个直接前驱a_i-1和一个直接后继a_i+1
线性表是一种典型的线性结构
线性表的类型定义
抽象数据类型线性表的定义如下:
ADT List{
数据对象:D={ai|ai属于Elemset,(i=1,2,...,n,n≥0)}
数据关系:R={<a_i-1,a_i>|a_i-1,a_i属于D,(i=1,2,...,n)}
基本操作:
InitList(&L); DestroyList(&L);
ListInsert(&L,i,e); ListDelete(&L,i,&e);
......等等
}ADT List
基本操作
-
InitList(&L)
-
操作结果:构造一个空的线性表L。
-
Status InitList_Sq(SqList &L){ L.elem = new ElemType[MAXSIZE]; if(!L.elem) exit(OVERFLOW); L.length = 0; return OK; }
-
-
DestroyList(&L)
-
初始条件:线性表L已经存在。
-
操作结果:销毁线性表L。
-
void DestoryList(SqList &L){ if(L.elem) delet L.elem; //释放存储空间 }
-
-
ClearList(&L)
-
初始条件:线性表L已经存在。
-
操作结果:将线性表L重置为空表。
-
void ClearList(SqList &L){ L.length = 0; //将线性表的长度置为0 }
-
-
ListEmpty(L)
-
初始条件:线性表L已经存在。
-
操作结果:若线性表L为空表,则返回TURE;否则返回FALSE。
-
int IsEmpty(SqList L){ if(L.length==0) return 1; else return 0; }
-
-
ListLength(L)
-
初始条件:线性表L已经存在。
-
操作结果:返回线性表L中的数据元素个数。
-
int GetLength(SqList L){ return(L.length); }
-
-
GetElem(L,i,&e)
-
初始条件:线性表L已经存在,1≤i≤ListLength(L)。
-
操作结果:用e返回线性表L中第i个数据元素的值。
-
int GetElem(SqList L,int i,ElemType &e){ if(i<1||i>L.length)return ERROR; //判断i值是否合理,若不合理,返回ERROR e = L.elem[i-1];//第i-1的单元存储着第i个数据 return OK; }
-
-
LocateElem(L,e,compare())
-
初始条件:线性表L已经存在,compare()是数据元素判定函数。
-
操作结果:返回L中第1个与e满足compare()的数据元素的位序。若这样的数据元素不存在则返回0。
-
int LocateElem(SqList L,ElemType e){ //在线性表L中查找值为e的数据元素,返回其序号(是第几个元素) for(i=0;i<L.length;i++) if(L.elem[i]==e) return i+1;//查找成功,返回序号 return 0;//查找失败,返回0 }
-
-
PriorElem(L,cur_e,&pre_e)
- 初始条件:线性表L已经存在。
- 操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无意义。
-
NextElem(L,cur_e,&next_e)
- 初始条件:线性表L已经存在。
- 操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无意义。
-
ListInsert(&L,i,e)
-
初始条件:线性表L已经存在,1≤i≤ListLength(L)+1。
-
操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加一。
-
Status ListInsert_Sq(SqList &L,int i,ElemType e){ if(i<1||i>L.length+1) return ERROR;//i值不合法 if(L.length==MAXSIZE) return ERROR;//当前存储空间已满 for(j=L.length-1;j>=i-1;j--) L.elem[j+1]=L.elem[j]; //插入位置及之后的元素后移 L.elem[i-1]=e; //将新元素e放入第i个位置 L.length++; //表长增1 return OK; }
-
-
ListDelete(&L,i,&e)
-
初始条件:线性表L已经存在,1≤i≤ListLength(L)。
-
操作结果:删除L的第i个元素,并用e返回其值,L的长度减一。
-
Status ListDelete_Sq(SqList &L,int i){ if(i<1||i>L.length+1) return ERROR;//i值不合法 for(j=i;j>=i-L.length-1;j++) L.elem[j-1]=L.elem[j]; //被删除元素之后的元素前移 L.length--; //表长减1 return OK; }
-
-
ListTraverse(&L,visited())
- 初始条件:线性表L已经存在。
- 操作结果:依次对线性表中每个元素调用visited()
线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
线性表的第一个数据元素a1的存储位置,称作线性表的起始位置或基地址。
线性表顺序存储结构占用一片连续的存储空间。知道某个元素的存储位置就可以计算其它元素的存储位置
顺序表的特点:以物理位置相邻表示逻辑关系。任一元素均可随机存取。 (优点)
顺序表(元素)地址连续、依次存放、随机存取、类型相同,可用一维数组表示
线性表长可变(删除),而数组长度不可动态定义,故用一变量表示顺序表的长度属性
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量
typedef struct{
ElemType elem[LIST_INIT_SIZE];
int length; //当前长度
}SqList
线性表的链式表示和实现
-
链式存储结构
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
-
线性表的链式表示又称为非顺序映像或链式映像。
-
用一组物理位置任意的存储单元来存放线性表的数据元素。
-
这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
-
链表中元素的逻辑次序和物理次序不一定相同。
-
与链式存储有关的术语
-
结点:数据元素的存储映像。由数据域和指针域两部分组成
-
链表:n个结点由指针链组成一个链表
它是线性表的链式存储映像,称为线性表的链式存储结构
-
单链表、双链表、循环链表:
- 结点只有一个指针域的链表,称为单链表或线性链表
- 结点有两个指针域的链表,称为双链表
- 首尾相接的链表称为循环链表
-
头指针、头结点和首元结点:
头指针:是指向链表中第一个结点的指针
首元结点:是指链表中存储第一个数据元素a1的结点
头结点:是在链表的首元结点之前附设的一个结点
-
-
链表(链式存储结构)的特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等(这种存取元素的方法被称为顺序存取法)
单链表的定义和表示
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。若头指针名是L,则把链表称为表L
typedef Struct{
//例
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
}ElemType;
typedef struct Lnode{ //声明结点的类型和指向结点的指针类型
Elemtype data; //结点的数据域
struct Lnode *next; //结点的指针域
}Lnode, *LinkList;
单链表基本操作的实现
-
单链表的初始化(带头结点的单链表)
Status InitList L(LinkList &L){ L=(LinkList)malloc(sizeof(LNode)); L->next=NULL; return OK; }
-
补充算法
-
判断链表是否为空
int ListEmpty(LinkList L){ if(L->next) //非空 return 0; else return 1; }
-
单链表的销毁
Status DestroyList_L(LinkList &L){ Lnode *p; while(L){ p=L; L=L->next; delete p; } return OK; }
-
清空链表
Status ClearList(LinkList &L){ Lnode *p,*q; p=L->next; while(p){ q=p->next; delete p; p=q; } L->next=NULL; return OK; }
-
求单链表的表长
int ListLength_L(LinkList L){ LinkList p; p=L->next; i=0; while(p){ i++; p=p->next; } return i; }
-
-
-
取值——取单链表中第i个元素的内容
Status GetElem_L(LinkList L,int i,ElemType &e){ p=L->next;j=1; while(p&&&j<i){ p=p->next;++j; } if(!p||j>i) return ERROR; e=p->data; return OK; }
-
按值查找——根据指定数据获取该数据所在的位置(地址)
Lnode *LocateElem_L(LinkList L,Elemtype e){ //找到,则返回L中值为e的数据元素的地址,查找失败返回NULL p=L->next; while(p&&p->data!=e) p=p->next; return p; } int LocateElem_L(LinkList L,Elemtype e){ //返回L中值为e的数据元素的位置序号,查找失败返回0 p=L->next;j=1; while(p&&p->data!=e) {p=p->next;j++;} if(p) return j; else return 0; }
-
插入——在第i个结点前插入值为e的新结点
Status ListInsert_L(LinkList &L,int i,ElemType e){ p=L;j=0; while(p&&j<i-1){p=p->next;++j;}//寻找第i-1个结点,p指向i-1结点 if(!p||j>i-1) return ERROR;//大于表长+1或者小于1,插入位置非法 s=new LNode; s->data=e;//生成新结点s,将结点s的数据域置为e s->next=p->next; p->next=s; return OK; }
-
删除——删除第i个结点
Status ListDelete_L(LinkList &L,int i,ElemType &e){ p=L;j=0; while(p->next&&j<i-1){p=p->next;++j;}//寻找第i个结点,并令p指向其前驱 if(!(p->next)||j>i-1)return ERROR;//删除位置不合理 q=p->next;//临时保存被删结点的地址以备释放 p->next=q->next;//改变删除结点前驱结点的指针域 e=q->data;//保存删除结点的数据域 delete q;//释放删除结点的空间 return OK; }
-
建立单链表:头插法——元素插入在链表头部,也叫前插法
void CreateList_H(LinkList &L,int n){ L=new LNode; L->next=NULL;//先建立一个带头结点的单链表 for(i=n;i>0;--i){ p=new LNode;//生成新结点p=(LNode*)malloc(sizeof(LNode)); cin>>p->data;//输入元素值scanf(&p->data); p->next=L->next;//插入到表头 L->next=p; } }
-
建立单链表:尾插法——元素插入在链表尾部,也叫后插法
void CreateList_R(LinkList &L,int n){ L=new LNode; L->next=NULL; r=L;//尾指针r指向头结点 for(i=0;i<n;++i){ p=new LNode;cin>>p->data;//生成新结点,输入元素值 p->next=NULL; r->next=p;//插入到表尾 r=p;//r指向新的尾结点 } }
循环链表
循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
**优点:**从表中任一结点出发均可找到表中其它结点。
注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
-
带尾指针循环链表的合并
LinkList Connect(LinkList Ta,LinkList Tb){ //假设Ta、Tb都是非空的单循环链表 p=Ta->next;//1.p存表头结点 Ta->next=Tb->next->next;//2.Tb表头连结Ta表尾 delete Tb->next;//释放Tb表头结点(或free(Tb->next);) Tb->next=p;//修改指针 return Tb; }
双向链表
双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
定义:
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior,*next;
}DuLNode,*DuLinkList;
双向循环链表
和单链的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
算法:
-
双向链表的插入
void ListInsert_DuL(DuLinkList &L,int i,ElemType e){ if(!(p=GetElemP_DuL(L,i))) return ERROR; s=new DuLNode; s->data=e; s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; return OK; }
-
双向链表的删除
void ListDelete_DuL(DuLink &L,int i,ElemType &e){ if(!(p=GetElemP_DuL(L,i))) return ERROR; e=p->data; p->prior->next=p->next; p->next->prior=p->prior; free(p); return OK; }
单链表、循环链表和双向链表的时间效率比较
查找表头结点(首元结点) | 查找表尾结点 | 查找结点*p的前驱结点 | |
---|---|---|---|
带头结点的单链表L | L->next时间复杂度O(1) | 从L->next依次向后遍历时间复杂度O(n) | 通过p->next无法找到其前驱 |
带头结点仅设头指针L的循环单链表 | L->next时间复杂度O(1) | 从L->next依次向后遍历时间复杂度O(n) | 通过p->next可以找到其前驱时间复杂度O(n) |
带头结点仅设尾指针R的循环单链表 | R->next时间复杂度O(1) | R时间复杂度O(1) | 通过p->next可以找到其前驱时间复杂度O(n) |
带头结点的双向循环链表L | L->next时间复杂度O(1) | L->prior时间复杂度O(1) | p->prior时间复杂度O(1) |
顺序表和链表的比较
-
链式存储结构的优点:
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
-
链式存储结构的缺点:
-
存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即存储密度=结点数据本身所占用的空间/结点占用的空间总量
一般地,存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为1(100%),而链表的存储密度小于1。
-
链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
-
比较项目 | 存储结构 | 顺序表 | 链表 |
---|---|---|---|
空间 | 存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配,不会出现存储空间闲置或溢出现象 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 | |
时间 | 存取元素 | 随机存取,按位置访问元素的时间复杂度为O(1) | 顺序存取,按位置访问元素时间复杂度为O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为O(n) | 不需要移动元素,确定插入、删除位置后,时间复杂度为O(1) | |
适用 | 情况 | ①表长变化不大,且能事先确定变化的范围②很少进行插入或删除操作,经常按元素位置序号访问数据元素 | ①长度变化较大②频繁进行插入或删除操作 |
线性表的应用
-
线性表的合并
void union(List &La,List Lb){ La_len=ListLength(La); Lb_len=ListLength(Lb); for(i=1;i<=Lb_len;i++){ GetElem(Lb,i,e); if(!LocateElem(La,e)) ListInsert(&La,++La_len,e); } }
-
有序表的合并
-
顺序表实现
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){ pa=LA.elem; pb=LB.elem; //指针pa和pb的初值分别指向两个表的第一个元素 LC。length=LA.length+LB.length; //新表长度为待合并两表的长度之和 LC。elem=new ElemType[LC.length]; //为合并后的新表分配一个数组空间 pc=LC.elem; //指针pc指向新表的第一个元素 pa_last=LA.elem+LA.length-1; //指针pa_last指向LA表的最后一个元素 pb_last=LB.elem+LB.length-1; //指针pb_last指向LB表的最后一个元素 while(pa<=pa_last&&pb<=pb_last){ //两个表都非空 if(*pa<=*pb) *pc++=*pa++; //依次“摘取”两表中值较小的结点 else *pc++=*pb++; } while(pa<=pa_last) *pc++=*pa++; //LB表已到达表尾,将LA中剩余元素加入LC while(pb<=pb_last) *pc++=*pb++; //LA表已到达表尾,将LB中剩余元素加入LC }
-
链表实现
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){ pa=La->next; pb=Lb->next; pc=Lc=La; //用La的头结点作为Lc的头结点 while(pa && pb){ if(pa->data<=pb->data){pc->next=pa; pc=pa; pa=pa->next;} else{pc->next=pb; pc=pb; pb=pb->next;} } pc->next=pa?pa:pb; //插入剩余段 delete Lb; //释放Lb的头结点 }
-