目录
2.1 线性表的定义和特点
线性表的定义:用数据元素的有限序列表示
线性表的特点:只有一个线性起点和一个线性终点,对于数据ai,只有一个直接前驱ai-1和一个直接后继ai+1
同一线性表中的元素必定具有相同特性
总结:
线性表中数据元素的类型可以为简单类型,也可以为复杂类型。
许多实际应用问题所涉的基本操作有很大相似性,不应为每个具体应用单独编写一个程序。
从具体应用中抽象出共性的逻辑结构和基本操作(抽象数据类型),然后实现其存储结构和基本操作。
2.2 线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
简言之,逻辑上相邻,物理上也相邻
顺序存储方法:用一组地址连续的存储单元依次存储线性表的元素,可通过数组V[n]来实现。
代码实现(转载)
#include<bits/stdc++.h>
using namespace std;
#define LIST_INIT_SIZE 100
typedef int ElemType;
typedef struct{
ElemType* data;
int length;
int MaxSize;
}SeqList;
//初始化
bool InitList(SeqList &L)
{
L.data = (ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
L.length = 0;
L.MaxSize = LIST_INIT_SIZE;
return true;
}
//求表长
int Length(SeqList L)
{
return L.length;
}
//按值查找
int LocateElem(SeqList L, ElemType e)
{
for(int i = 0; i<L.length; i++)
{
if(L.data[i] == e)
return i+1;
}
return 0;
}
//按下标查找
ElemType GetElem(SeqList L, int i)
{
if(i<1 || i>L.length)
{
cout<<"i不在合法范围内"<<endl;
return -1;
}
return L.data[i-1];
}
//插入
bool ListInsert(SeqList &L, int i, ElemType e)
{
if(i<1 || i>L.length+1)
return false;
if(L.length > L.MaxSize)
return false;
for(int j = L.length-1; j>=i; j--)
L.data[j] = L.data[j-1];
L.data[i-1] = e;
L.length++;
return true;
}
//删除
bool ListDelete(SeqList &L, int i, ElemType &e)
{
if(i<1 || i>L.length+1)
return false;
e = L.data[i-1];
for(int j = i; j<L.length; j++)
L.data[j-1] = L.data[j];
L.length--;
return true;
}
//输出
void PrintList(SeqList L)
{
for(int i = 0; i<L.length; i++)
{
cout<<L.data[i]<<" ";
}
cout<<endl;
}
//判空
bool Empty(SeqList L)
{
if(L.length == 0)
return true;
return false;
}
//销毁
bool DestroyList(SeqList &L)
{
free(L.data);
L.length = 0;
L.MaxSize = 0;
return true;
}
int main()
{
SeqList L;
InitList(L);
for(int i = 0; i<10; i++)
ListInsert(L,i+1,i+20);
cout<<"初始化后:"<<endl;
PrintList(L);
cout<<"长度为:"<<Length(L)<<"为空?"<<Empty(L)<<endl;
cout<<"表中值为22的是第"<<LocateElem(L,22)<<"位"<<endl;
cout<<"表中第5位是"<<GetElem(L,5)<<endl;
ElemType e;
ListDelete(L,7,e);
cout<<"删除第7位后"<<endl;
PrintList(L);
cout<<"长度为:"<<Length(L)<<"为空?"<<Empty(L)<<endl;
DestroyList(L);
cout<<"长度为:"<<Length(L)<<"为空?"<<Empty(L)<<endl;
return 0;
}
查找时间复杂度:O(1);插在第 i 个结点之前,移动 n-i+1 次;删除第 i 个结点,移动 n-i 次
查找、插入、删除算法的平均时间复杂度为O(n);显然,顺序表的空间复杂度S(n)=O(1)(没有占用辅助空间)
顺序表(顺序存储结构)的特点
(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等
这种存取元素的方法被称为随机存取法
顺序表的优缺点
优点:存储密度大(结点本身所占存储量/结点结构所占存储量);可以随机存取表中任一元素
缺点:在插入、删除某一元素时,需要移动大量元素;浪费存储空间;属于静态存储形式,数据元素的个数不能自由扩充
2.3 线性表的链式表示和实现
链式存储结构
结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
线性表的链式表示又称为非顺序映像或链式映像。
与链式存储有关的术语
1、结点:数据元素的存储映像。由数据域和指针域两部分组成
2、链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构
3、单链表、双链表、循环链表:
•结点只有一个指针域的链表,称为单链表或线性链表
•有两个指针域的链表,称为双链表
•首尾相接的链表称为循环链表
4、头指针、头结点和首元结点
头指针是指向链表中第一个结点的指针
首元结点是指链表中存储第一个数据元素a1的结点
头结点是在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息
讨论1. 如何表示空表?有头结点时,当头结点的指针域为空时表示空表
讨论2. 在链表中设置头结点有什么好处?
⒈.便于首元结点的处理首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理;
⒉.便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
讨论3. 头结点的数据域内装的是什么? 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
链表(链式存储结构)的特点
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
这种存取元素的方法被称为顺序存取法
链表的优缺点
优点
–数据元素的个数可以自由扩充
–插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
缺点
•存储密度小
•存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问(顺藤摸瓜)
单链表代码实现:
#include<bits/stdc++.h>
using namespace std;
#define LIST_INIT_SIZE 100
typedef int ElemType;
typedef struct LNode{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为Lnode类型的指针
//初始化
bool InitList(LinkList &L){
L = new LNode;
L->next=NULL;
return true;
}
//求表长
int Length(LinkList L)
{
LinkList p=L->next;
int l = 0;
while(p)
{
l++;
p=p->next;
}
return l;
}
//按值查找
LNode* LocateElem(LinkList L, ElemType e)
{
//返回L中值为e的数据元素的地址,查找失败返回NULL
LinkList p=L->next;
while(p && p->data != e)
p=p->next;
return p;
}
//按下标查找
ElemType GetElem(LinkList L, int i)
{
LinkList p = L->next;
int pos = 1; //初始化
while(p && pos<i)
{
//向后扫描,直到p指向第i个元素或p为空
p=p->next;
pos++;
}
if(!p || pos>i)return -1; //第i个元素不存在
return p->data;
}
//插入
bool ListInsert(LinkList &L, int i, ElemType e)
{
LinkList p=L;
int pos = 0;
while(p&&pos<i-1)
{
p=p->next;
pos++;
} //寻找第i−1个结点
if(!p || pos>i-1)
return false; //i大于表长 + 1或者小于1
LNode *s = new LNode; //生成新结点s
s->data = e; //将结点s的数据域置为e
s->next = p->next; //将结点s插入L中
p->next = s;
return true;
}
//删除
bool ListDelete(LinkList &L, int i, ElemType &e)
{
LinkList p=L;
int pos = 0;
while(p && pos<i-1)
{
p=p->next;
pos++;
} //寻找第i−1个结点
if(!p || pos>i-1)
return false;
LNode *q = p->next; //临时保存被删结点的地址以备释放
p->next = q->next; //改变删除结点前驱结点的指针域
e = q->data; //保存删除结点的数据域
delete q; //释放删除结点的空间
return true;
}
//清空
bool ClearList(LinkList & L)
{
// 将L重置为空表
LinkList p,q;
p=L->next; //p指向第一个结点
while(p) //没到表尾
{
q=p->next;
delete p;
p=q;
}
L->next=NULL; //头结点指针域为空
return true;
}
//输出
void PrintList(LinkList L)
{
LNode *p = L->next;
while(p)
{
cout<<p->data<<" ";
p = p->next;
}
cout<<endl;
}
//判空
bool Empty(LinkList L)
{
//若L为空表,则返回1,否则返回0
if(L->next) //非空
return false;
return true;
}
//销毁
bool DestroyList(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
delete p;
}
return true;
}
//前插法
void CreateList_F(LinkList &L,int n)
{
L=new LNode;
L->next=NULL; //先建立一个带头结点的单链表
for(int i=n; i>0; i--)
{
LNode *p=new LNode; //生成新结点
cin>>p->data; //输入元素值
p->next=L->next;
L->next=p; //插入到表头
}
}//CreateList_F
//尾插法
void CreateList_L(LinkList &L,int n)
{
//正位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL;
LNode *r=L; //尾指针r指向头结点
for(int i=0; i<n; i++)
{
LNode *p=new LNode; //生成新结点
cin>>p->data; //输入元素值
p->next=NULL;
r->next=p; //插入到表尾
r=p; //r指向新的尾结点
}
}//CreateList_L
void InvertL(LinkList &L)
{
LNode *p = L->next;
L->next = NULL;
while(p)
{
//cout<<p->data<<" ";
LNode *s = new LNode;
s->data = p->data;
s->next = L->next;
L->next = s;
p = p->next;
}
}
int main()
{
LinkList L;
CreateList_L(L,5);
PrintList(L);
return 0;
}
循环链表
空表中,L->next = L;
从循环链表中的任何一个结点的位置都可以找到其他所有结点,而单链表做不到;
循环链表避免死循环:
循环条件:p->next!=NULL&&p->next!=L
对循环链表,有时不给出头指针,而给出尾指针,可以更方便的找到第一个和最后一个结点
循环链表的合并
LinkList Connect(LinkList Ta,LinkList Tb)
{
//假设Ta、Tb都是非空的单循环链表
p=Ta->next; //①p存表头结点
Ta->next=Tb->next->next; //②Tb表头连结Ta表尾
delete Tb->next; //③释放Tb表头结点
Tb->next=p; //④修改指针
return Tb;
}
双向链表
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;
status 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;
}
status ListDelete_DuL(DuLinkList &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;
delete p;
return OK;
}
顺序表和链表的比较
| 顺 序 表 | 链 表 | |
空间 | 存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配,不会出现闲置或溢出现象 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 | |
时间 | 存取元素 | 随机存取,时间复杂度为O(1) | 顺序存取,时间复杂度为O(n) |
插入、删除 | 平均移动约表中一半元素,时间复杂度为O(n) | 不需移动元素,确定插入、删除位置后,时间复杂度为O(1) | |
适用情况 | ① 表长变化不大,且能事先确定变化的范围 ② 很少进行插入或删除操作,经常按元素序号访问数据元素 | ① 长度变化较大 ② 频繁进行插入或删除操作 |
2.4 线性表的应用
2.4.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);
}
}
2.4.2 有序表的合并
LinkList MergeLinkList(LinkList L1, LinkList L2)
{
LinkList p1 = L1->next;
LinkList p2 = L2->next;
LinkList L = L1;
LinkList p = L;
while(p1 && p2)
{
if(p1->data < p2->data)
{
p->next = p1;
p = p1;
p1 = p1->next;
}
else
{
p->next = p2;
p = p2;
p2 = p2->next;
}
}
if(p1)
p->next = p1;
else
p->next = p2;
return L;
}