目录
顺序表的特点:以物理位置相邻表示逻辑关系,任意元素均可随机存取
2.1线性表的定义和特点
线性表是具有相同特性的数据元素的一个有限序列
- 线性表
- 由n(n>=0)个数据元素(结点)构成的有限序列
- 数据元素的个数n定义为表的长度
- N=0时称为空表
- 将非空的线性表(n>0)记作:(a1,a2,a3…an)
- 数据元素ai只是一个抽象的符号,在具体含义不同的情况下可以不同
同一个线性表中的元素必定具有相同特性,数据元素间的关系是线性关系
- 线性表的逻辑特征
- 在非空的线性表,有且仅有一个开始结点,它没有直接前驱,而仅有一个直接后继
- 有且仅有一个终端结点,它没有直接后继,而仅有一个直接前驱
- 其余的内部结点都有且仅有一个直接前驱和一个直接后继
2.2案例引入
- 一元多项式的计算:
- 用数组来表示
- 简单一元多项式可以这么做,但是稀疏多项式就不能这么做,太麻烦了
- 创建一个新数组C
- 从头遍历比较a和b的每一项
- 指数相同:对应系数相加
- 指数不相同:将指数较小的项复制到c中
- 但是顺序存储结构存在问题
- 存储空间分配不灵活
- 运算的空间复杂度高
- 我们可以考虑用链式存储结构
2.3线性的类型定义
- 抽象数据类型线性表定义
基本操作(一)
- InitList(&L)
- 操作结果:创造一个空的线性表L
- DestroyList(&L)
- 初始条件:线性表L已经存在
- 操作结果:销毁线性表L
- ClearList(&L)
- 初始条件:线性表L已经存在
- 操作结果:将线性表L重置为空表
基本操作(二)
- ListEmpty(L)
- 初始条件:线性表L已经存在
- 操作结果:若线性表L为空表,则返回TURE;否则返回FALSE
- ListLength(L)
- 初始条件:线性表L已经存在
- 操作结果:返回线性表L中的数据元素个数
基本操作(三)
- GetElem(L,i,&e)
- 初始条件:线性表L已经存在,1<=i<=ListLength(L)
- 操作结果:用e返回线性表L中第i个数据元素的值
- LocateElem(L,e,compare())
- 初始条件:线性表L已经存在,compare()是数据元素判定函数
- 操作结果:返回L中第1个与e满足compare()的数据元素的位序,若这样的数据元素不存在就返回值为0
基本操作(四)
- Prior(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的长度加一
- ListDelete(&L,I,&e)
- 初始条件:1<=i<=ListLength(L)
- 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
- ListTraverse(&L,visited())
- 初始条件:线性表L已经存在
- 操作结果:依次对线性表中每个元素调用visited()
2.4线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
线性表的顺序存储结构占用一片连续的存储空间。
顺序表中元素存储位置的计算
顺序表的特点:以物理位置相邻表示逻辑关系,任意元素均可随机存取
顺序表的顺序存储表示
补充:类C语言的有关操作
元素类型说明
typedef struct{
ElemType data[];
int length;
}SqList;//顺序表类型定义
typedef char ElemType;
//例子
typedef struct{
float p;
int e;
}Polynimial;
typedef struct{
Polynimial *elem;
int length;
}SqList;
数组定义
- 数组静态分配
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;//顺序表类型
- 数组动态分配
typedef struct{
ElemType *data;
int length;
}SqList;//顺序表类型
SqList L;
L.data=(ElemType*)malloc(sizeof(ElemType)*MaxSize)
C语言的内存动态分配
需要头文件:<stdlib.h>
C++里面用new和delete
C++中的参数传递
- 函数调用时传送给形参表的实参必须和形参三个一致
- 类型、个数、顺序
- 参数传递有两种方式
- 传值方式
- 传地址
- 参考C语言就行,不难
引用类型作形参的三点说明
- 传递引用给函数与传递指针的效果是一样的,形参变化实参也发生变化
- 引用类型作形参,在内存中没有产生实参的副本,它直接对实参操作,而一般变量作参数,形参与实参占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据量较大时,用引用比用一般变量传递参数的时间和空间效率都好。
- 指针参数虽然也能达到与使用引用的效果,单数在被调函数中需要重复使用"*指针变量名"的形式进行运算,很容易产生错误并且程序的阅读性较差;另一方面,在主调函数的调用点处,必须有变量的地址作为实参。
顺序表示意图
逻辑位序和物理位序相差1
静态分配和动态分配都可以用
顺序表基本操作的实现
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
typedef int Status;
typedef char ElemType;
- 线性表的初始化(参数用引用)
Status InitList_Sq(SqList &L){//构造一个空顺序表L
L.elem = new ElemType[MAXSIZE];//为顺序表分配空间
if(!L.elem)exit(OVERFLOW);//存储空间失败
L.length = 0;//空表长度为0
return OK;
}
- 销毁线性表L
void DestoryList(SqList &L){
if(L.elem) delete L.elem;//释放存储空间
}
- 清空线性表L
void ClearList(SqList &L){
L.length = 0;//将线性表的长度置为0
}
- 求线性表L的长度
int GetLength(SqList L){
return (L.length);
}
- 判断线性表L是否为空
int IsEmpty(SqList L){
if(L.length==0) return 1;
else return 0;
}
- 顺序表的取值(根据位置i获取相应位置数据元素的内容)
int GetElem(SqList L,intt i,ElemType &e){
if(i<1||i>L.length) return ERROR;//判断i值是否合理,若不合理,返回ERROR
e = L.elem[i-1];//第i-1的单位存储着第i个数据
return OK;
}
随机存取,时间复杂度为O(1)
- 顺序表的查找
int LocateElem(SqList L,ElemType e){
for(i = 0;i < L.length;i++){
if(L.elem[i]==e) return i+1;//查找成功,返回序号
}
return 0;//查找失败,返回0
}
平均查找长度:
时间复杂度为O(n)
- 顺序表的插入
- 可以插在最后、中间、最前
- 算法思想
- 判断插入位置i是否合法
- 判断顺序表的存储空间是否已满
- 将第n至第i位的元素依次向后移动一个位置
- 将要插入的新元素e放入到第i个位置
- 表长加1,插入成功
Status ListInsert(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1) return ERROR;
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;
L.length++;
return OK;
}
时间复杂度为O(n)
- 顺序表的删除
- 可以删除最后、中间、最前
- 算法思想
- 判断删除位置i是否合法
- 将要删除的元素保留在e中
- 将第i+1至第n位的元素依次向前移动一位
- 表长减1,删除成功
Status ListDelete_Sq(SqList &L,int i){
if(i<1||i>L.length) return ERROR;
for(j=i;j<=L.length-1;j++){
L.elem[j-1] = L.elem[j];
}
L.lenggth--;
return OK;
}
时间复杂度为O(n)
顺序表的优缺点
- 优点:
- 存储密度大
- 可以随机存取表中任意元素
- 缺点:
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
2.5线性表的链式表示和实现
链表
- 链式存储结构
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
- 线性表的链式表示又称为非顺序映像或链式映像
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元即可以是连续的,也可以是不连续的甚至是零散分布在内存中的任意位置上的
- 链表中元素的逻辑次序和物理次序不一定相同
- 单链表是由头指针唯一确定,因此单链表可以用头指针的名字来命名
- 各个结点由两个域组成
- 数据域:存储元素数值数据
- 指针域:存储直接后继结点的存储位置
- 链表是顺序存取的,顺序表是随机存取的
链式存储有关的术语
结点:
- 数据元素的存储映像。由数据域和指针域两部分组成
链表:
- n个结点由指针链组成一个链表
- 它是线性表的链式存储映像,称为线性表的链式存储结构
带头结点单链表示意图
单链表、双链表、循环列表
- 结点只有一个指针域的链表,称为单链表或线性链表
- 结点有两个指针域的链表,称为双链表
- 首尾相接的链表称为循环列表
头指针、头结点和首元结点
- 头指针:是指向链表中第一个结点的指针
- 首元结点:是指向链表中存储第一个数据元素a1的结点
- 头结点:是在链表的首元结点之前附设的一个结点
存储结构两种形式
不带头结点
带头结点
表示空表
- 无头结点时,头指针为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
设置头结点的好处
1.便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无须进行特殊处理
2.便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理就统一了
头结点的数据域装什么
可以为空、存放线性表长度,但此结点不能计入链表长度值
带头结点的单链表
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L
typedef struct Lnode{
ElemType data;
struct Lnode *next;
}Lnode,*LinkList;//LinkList为指向结构体Lnode的指针类型
单链表基本操作的实现
单链表的初始化
- 算法操作
- 生成新结点作头结点,用头指针L指向头结点
- 讲头结点的指针域置空
- 代码
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 DestoryList_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个元素的内容
- 从链表的头指针出发,顺着链域next逐个结点往下搜索,直到搜索到第i个结点为止。链表不是随机存储结构
- 算法步骤
- 从第一个结点(L->next)顺链扫描,用指针p指向当前扫描的结点,p初值为p=L->next
- j作为计数器,累计当前扫过的结点数,j初值为1
- 当p指向扫描到下一结点时,计数器j加1
- 当j==i时,p所指的结点就是要找的第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;
}
按值查找——根据指定数据获取该数据所在的位置(地址)
- 算法步骤
- 从第一个结点起,依次和e相比较
- 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址
- 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或NULL
- 代码
Lnode *LocateElem_L(LinkList L,ElemType e){
p = L->next;
while(p&&p->data!=e)
p = p->next;
return p;
}
按值查找——根据指定数据获取该数据位置序号
- 代码
Lnode *LocateElem_L(LinkList L,ElemType e){
p = L->next;j = 1;
while(p&&p->data!=e)
{p = p->next;j++}
if(p)
return j;
else
return 0;
}
插入
- 算法步骤
- 首先找到ai-1的存储位置p
- 生成一个数据域为e的新结点s
- 插入新结点:
- 新结点的指针域指向结点ai
- 结点ai-1的指针域指向新结点
- 代码
Status ListInsert_L(LinkList &L,int i,ElemType e){
p = L;j = 0;
while(p&&j<i-1){p = p->next;++j;}
if(!p||j>i-1)return ERROR;
s = new Lnode;
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
删除
- 算法步骤
- 首先找到ai-1的存储位置p,保存要删除的ai的值
- 令p->next指向ai+1
- 释放结点ai的空间
- 代码
Status ListDelete_L(LinkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next&&j<i-1){p=p->next;++j;}
if(!(p->next)||j>i-1) return ERROR;
q=p->next;
p->next = q->next;
e = q->data;
delete q;
return OK;
}
单链表的查找、插入、删除算法时间效率分析
1.查找:O(n)
2.插入和删除:不算查找O(1),算查找O(n)
头插法
- 代码
void CreateList_H(LinkList &L,int n){
L = new LNode;
L->next = NULL;
for(i=n;i>0;--i){
p = new LNode;
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;
for(i = 0;i < n;i++){
p = new LNode;
scanf(&p->data);
p->next = NULL;
r->next = p;
r = p;
}
}
循环链表
- 循环链表:是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个列表形成一个环)
- 优点:从表中任一结点出发均可找到表中其他结点
- 循环链表没有NULL指针,判断是否为空是判断它们是否等于头指针
- 表的操作常常是在表的首尾位置进行的
- 尾指针方便:时间复杂度都是O(1)
双向链表
- 在单链表的每个结点里面再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表(可以克服单链表的缺点)
- 结构定义代码
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior, *next;
}DuLNode,*DuLinkList;
双向循环列表
概念
- 头结点的前驱指针指向链表的最后一个结点
- 最后一个结点的后继指针指向头结点
- 双向链表的对称性:p->prior->next = p = p->next->prior
- 插入删除需要同时修改两个方向的指针,两者操作的时间复杂度均为O(n)
插入
- s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
删除
- P->prior->next = p->next;
P->next->prior = p->prior;
- 删除是O(1),查找删除位置O(n)
单链表、双向链表、循环链表的时间序列比较
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|