二、线性表
线性结构:
除第一个和最后一个之外,集合中的每个元素均只有一个直接前驱和一个直接后继
表的抽象数据类型:
数据对象:ai:代表一个元素,属于数据元素的集合
数据关系:一个学生信息跟着另一个学生的信息,是有序的。
线性表:逻辑结构
顺序表&链表:实现的存储结构
2.1 线性表的定义和特点
同一线性表中的元素必定有相同特性,数据元素之间是线性关系。
开始结点:没有直接前驱,仅有一个直接后继。
终端结点:没有直接后继,仅有一个直接前驱。
其余内部结点都有且仅有一个直接前驱和直接后继。
2.2 线性表的线性表示和实现
线性表顺序存储结构的图示:
用一组连续的存储单元依次存储线性表的数据元素。
以物理位置相邻表示逻辑关系相邻,且任意一个元素均可随机存取。
LOC(ai) = LOC(a1)+(i-1)* l
LOC(a1):称为基地址
l: 每个dataelement存储的长度
好处:只要知道起始位置在哪,可以随机访问
elemType:代表数组中元素的类型。
若数据元素为复杂类型,可以定义一个结构类型。
类C语言的补充
数组的定义:
第二个:用内存动态分配的函数来分配内存
c语言中动态存储分配:
SqList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
maxSize:元素个数。 elemType:类型所占的字节数。
(Elemtype*):通过此类型划分开辟的空间,强制类型转换运算。转换为指针,获得地址。
需要加载头文件:<stdlib.h>
c++的动态存储分配
int *p1 = new int;
delete p1;
new出来的是一块空间的地址,所以要复制给指针变量。
释放空间:
delete 指针P//p必须是new操作的返回值
c++中参数的传递
用指针进行交换
#include<iostream>
using namespace std;
void swap (float *m, float *n){
float t;
t = *m;
*m = *n;
*n = t;
}
int main(){
float a, b, *p1, *p2;
cin >> a >> b;
p1 = &a;
p2 = &b;
swap(p1, p2);
cout << a << " "<< b << endl;
}
引用类型做参数:
引用:给一个对象提供一个替代的名字。是同一个东西,共用一块空间。
void swap (int &m, int &n);
int main(){
int a, b;
cin >> a >> b;
swap(a, b);
cout << a << " " << b;
}
void swap (int &m, int &n){
int t;
t = m;
m = n;
n = t;
}
几点说明:引用类型做形参是直接对实参操作,形参改变实参也发生变化,是同一个东西。因此当参数传递的数据量较大时,用引用比用一般变量传递参数的时间和空间效率都好。
2.2.1 线性表的初始化及一些基本操作(参数用引用)
typedef struct{
int *elem;
int length;
}SqList;
//线性表L的初始化
int InitList_Sq(SqList &L){ //构造一个空的顺序表,
L.elem = (int*)malloc(MAXSIZE*sizeof(int); //动态分配,为顺序表分配空间返回基地址
if(!L.elem) // 异常处理,存储分配失败(内存太小可能分配不成功),返回-2
exit(OVERFLOW);
L.length = 0; //空表长度为0
return OK;
}
//销毁线性表L
void DestroyList(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;
}
2.2.2 顺序表的取值
int GetElem(SqList L, int i, int &e){
if(i<1 || i>L.length)
return ERROR;
e = L.elem[i-1];
return OK;
}
所有代码都只执行一次,时间复杂度为常量阶O(1)
2.2.3 顺序表的查找
按值查找
时间复杂度:
先找循环次数最多的语句:循环体里的。
平均查找长度
第二个公式:
1,2……7 查找第i个元素的比较次数,1/7:每个元素被查找的概率
数量级就是O(n)
2.2.4 顺序表的插入
Status ListInsert_Sq(SqList &L, int i, ElemType e){
int j;
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;
}
算法时间:
概率都是1/n+1,因为有(n+1)个可以插入的位置,时间复杂度为O(n)
2.2.5 顺序表的删除
Status ListDelete_Sq(SqList &L, int i){
int j;
if((i<1) || (i>L.length))
return ERROR;
for(j=i; j<L.length-1; j++)
L.elem[j-1] = L.elem[j];
L.length--;
return OK;
}
时间复杂度,有n个元素可以删除。
O(n)
2.3 链式存储结构
2.3.1基础
记录第一个元素地址的是头指针H,单链表可以用头指针唯一确定,因此单链表可以用头指针的名字来命名。
带头结点的单链表
2.3.2 单链表,双链表,循环链表
结点只有一个指针域的链表,称为单链表或线性链表。用来存储后继元素
结点有两个指针域的链表,称为双链表。一个存储前驱,一个存储后继。
首尾相接的链表称为循环列表。
头指针,头节点,首元结点
头指针:是指向链表中第一个结点的指针
首元结点:是指量表中存储第一个数据元素a1的结点
头结点:为了处理方便,在第一个结点之间附加一个结点
链表的两种形式:
带头结点和不带头结点
表示空表:
设置头结点的好处:
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表的长度值。
2.3.3 链表的特点
节点在存储器中位置是任意的,访问的时候只能透过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
顺序表:随机存取。
2.3.4 单链表的存储
next是下一个的地址,是指针变量。
struct Lnode 是指向这种类型的指针,这种类型又包括指向这两个成员。用自己定义自己,嵌套定义。
Lnode 指向一个结点,Lnode a; a.data。Lnode *L;
*LinkList:指向结点的指针,LinkList L;
例;
存储学生的学号,姓名,成绩的单链表结点类型定义如下:
typedef Struct student{
char num[8];
char name[8];
int score;
}D;
typedef struct Lnode{
D data;
struct Lnode *next;
}Lnode, *LinkList;
LinkList L;
2.3.5 单链表的基本操作
1、单链表的初始化
构造一个空的单链表
算法步骤:
1、生成的新结点作头结点,用头指针L指向头结点。
2、将头结点的指针域置空
Status InitList L(LinkList &L){
L = new Lnode;
L->next = NULL;
return OK;
}
new一个结点,从内存中找的这么一块空间,得到指向结点一个指针。将这块空间的地址赋值给L。
补充算法1:判断链表是否为空
思路:判断头结点的指针域是否为空
int ListEmpty (LinkList L){
if(L->next) //非空
return 0;
else
return 1;
}
补充算法2:单链表的销毁
链表销毁后不存在了,头结点,头链表都不存在了。
算法思路:
从头指针开始,一次释放所有结点
一个用来操作当前想要操作的结点:p,让他指向头结点,再把它指向的结点删除掉。
Status DestroyList_L(LinkList &L){
Lnode *p;
while(L){
p = L;
L = L->next;
delete p;
}
return OK;
}
结束条件:L==NULL
补充算法3:清空链表
链表仍存在,单链表中无元素,成为空链表(头指针和头结点仍然存在)
算法思路:
依次释放所有结点,并将头节点的指针域设置为空
p指向当前要删除的结点,q来指向释放掉结点的下一个结点
//清空链表
Status ClearList(LinkList &L){
Lnode *p, *q;
p = L->next;
while(p){
q = p->next;
delete p;
p = q;
}
L->next = NULL; //头结点的指针域为空
return OK;
}
补充算法4:单链表的表长
算法思路:从首元结点开始,依次计数所有结点
int ListLength_L(LinkList L){
Lnode *p;
int i = 0;
p = L->next;
while(p){
i++;
p = p->next;
}
return i;
}
几个重要操作:
p=L; //p指向头结点
s=L->next; //s指向首元结点
p=p->next; //p指向下一结点
2. 取值——取单链表中第i个元素的内容
算法思路:从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存储结构。
算法步骤:
3. 按值查找——根据指定的数据获取该数据所在的位置
算法步骤:
1、从第一个结点起,依次和e相比较。
2、如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或地址。
3、如果查遍整个链表都没有找到其值与e相等的元素,则返回0或NULL
Lnode *LocateElem_L(LinkList L, Elemtype e){//加*表示函数返回的类型是Lnode的指针类型的
p = L->next;
while(p && p->data != e){
p = p->next;
}
return p;//找到后返回地址
}
// 返回值为e的位置序号
int LocateElem_L(LinkList L, Elemtype e){
p = L->next;
int j = 1;
while(p && p->data != e){
p = p->next;
j++;
}
if(p){
return j;
}
else
return 0;
}
如果找到了,即p不为空,返回位置。如果没找到,p为空,返回0,查找失败
4. 插入——在第i个结点前插入值为e的新结点
算法步骤:
循环:当查找到第i-1的时候停止,即 j=i-1。或者第i-1个元素已经超出了表长,p已经为空了。也不用找了。
异常情况:返回error;i 大于表长+1或者小于1,插入位置非法
5.删除——删除第i个结点
算法步骤:
可能会存在删除的位置不合理:i > 表长或 i < 1;
Status ListDelete_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;
q = new Lnode;
q = p->next; //临时保存被删结点的地址以备释放
e = q->data; //保存删除节点的数据域
p->next = q->next; //删除
delete q; //释放删除结点的空间
return OK;
}
6. 单链表的查找,插入,删除算法时间效率分析
查找:
循环体中的那条语句循环次数最多
7.单链表的建立
头插法
void CreateList_H(LinkList &L, int n){
L = new LNode;
L->next = NULL; //建立一个带头结点的单链表
for(i=n; i>0; i--){
p = new Lnode;
cin >> p->data;
p->next = L->next;
L->next = p;
}
}
时间复杂度O(n)
尾插法:
void CreateList_R(LinkList &L, int n){//最后通过L可以带回整个链表
L = new Lnode;
L->next = NULL;
r = new Lnode;
r = L; //尾指针r指向头结点
for(i=0; i<n; i++){
p = new Lnode; //生成新结点
cin >> p->data; //输入元素值
p->next = NULL;
r->next = p; //插入到表尾
r = p; //r指向新的尾结点
}
}
2.3.6 循环链表
循环链表通常使用尾指针,更方便。
将带尾指针的循环链表合并。
LinkList Connect(LinkList Ta, LinkList Tb){
p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next;
Tb->next = p;
return Tb;
}
2.3.7 双向链表
在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。为了在O(1)的时间内找到一个元素的前驱和后继。
双向链表:
双向循环链表:
1. 双向链表的插入
void ListInsert_Dul(DuLinkList &L, int i, ElemType e){
if(!(p = GerElemP_DuL(L,i))) //找到第i个元素的函数
return ERROR
s->prior = p->prior;
p->prior->next = s;
p = s->next;
p->prior = s;
return OK;
}
2.双向链表的删除
//删除循环链表的第i个元素,并用e返回
void 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;
}
2.3.8 时间复杂度的比较
2.4 顺序表与链表的比较
链式存储优缺点
存储密度
比较一下
2.5 线性表的应用
线性表合并的算法步骤:
依次取出Lb中的每个元素:在La中查找该元素,如果找不到,则将其插入La的最后
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)) //如果找到了返回位置,如果没找到返回0,进行插入
ListInsert(&La, ++La_len, e)
}
}
有序表的合并(顺序表):
算法步骤:
这里++优先级更大,所以*p++ = *(p++)加的是地址
void MergeList_Sq(SqList LA, SqList LB, SqList &LC){
pa = LA.elem;
pb = LB.elem; //指针分别指向两个表的第一个元素
LC.length = LA.length + LB.length; //新表长度
Lc.elem = new ElemType[LC.length]; //为新表分配一个数组空间
pc = LC.elem; //指针指向第一个元素
pa_last = LA.elem + LA.length-1;
pb_last = LB.elem + LB.length-1; //指针指向 最后一个元素
while(pa<=pa_last && pb<=pb_last){
if(*pa<=*pb)
*pc++ = *pa++; //移动的是地址
else
*pc++ = *pb++;
}
while(pa<=pa_last)
*pc++ = *pa++; //将LA剩余元素加入LC
while(pb<=pb_last)
*pc++ = *pb++;
}
}
有序表合并——链表实现
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的头结点
}
时间复杂度:
空间复杂度:不需要额外的空间,因此为O(1)
2.6 案例
案例一 —— 一元多项式的运算:
案例二 —— 系数多项式的运算