线性表是具有相同特性的数据元素的一个有限序列。
其中数据元素的个数n定义为表的长度。
当n=0时称为空表。
将非空的线性表(n>0)记作: (a1, a2,…an)
这里的数据元素ai(1<=i<=n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。
(A, B, C, D, …, Z)
26个字母是数据元素; 元素(字母)间关系是线性。
某单位历年拥有计算机的数量(6, 14, 18, 50, 92, 198)
12星座(白羊座、金牛座、双子座、巨蟹座、狮子座、处女座、天秤座、天竭座、射手座、摩羯座、水瓶座、双鱼座)
同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系。
在非空的线性表,有且仅有一个开始结点a,它没有直接前趋,而仅有一个直接后继a2;
有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋an-1;
其余的内部结点ai(2<=i<=n-1)都有且仅有一个直接前趋ai-1和一个直接后继ai+1。
线性表是一种典型的线性结构。
ADT List{
数据对象:D = {ai | ai属于Elemset, (i = 1,2,……,n,n≥0)} //数据对象是一些元素,元素个数≥0
数据关系:R = {<ai-1,ai> | ai-1,ai属于D, (i = 2,3,……,n)} //数据关系序偶关系,ai-1是ai的前驱,ai是ai-1的后继
基本操作:
InitList(&L); DestroyList(&L);
ListInsert(&L,i,e); ListDelete(&L,i,&e);
……等等
}ADT List
InitList(&L) //(或Initialization List)
操作结果:构造一个空的线性表L
DestroyList(&L)
初始条件:线性表L已经存在
操作结果:销毁线性表L
ClearList(&L)
初始条件:线性表L已经存在
操作结果:将线性表L重置为空表
ListEmpty(L)
初始条件:线性表L已经存在
操作结果:若线性表L为空表(n=0),则返回TRUE;否则返回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
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的长度加一
ListDelete(&L, i, &e)
初始条件:线性表L已经存在,1 <= i <= ListLength(L)
操作结果:删除线性表L的第i个数据元素,并用e返回其值,L的长度减一
ListTraverse(&L, visited())
初始条件:线性表L已经存在
操作结果:依次对线性表中每个元素调用visited() 【遍历】
以上所提及的运算是逻辑结构上定义的运算,属于“做什么”,至于“如何做”等实现细节,只有待确定了存储结构之后才考虑。下面开始实现。
线性表的顺序表示又称为顺序存储结构或顺序映像。
顺序存储定义:逻辑上相邻,物理上也相邻。
线性表的第一个数据元素a1的存储位置,称作线性表的起始位置或基地址。
线性表(1,2,3,4,5,6)的存储结构:
依次存储,地址连续,中间没有空出存储单元——是一个典型的线性表顺序存储结构。
占用一片连续的存储空间。
地址不连续,中间存在空的存储单元——不是一个线性表顺序存储结构。
所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
LOC(ai) = LOC(a1) + (i - 1) × L //a1为基地址
顺序表(元素):地址连续、依次存放、随机存取、类型相同
(这些特点与数组一致)
用一维数组表示顺序表
线性表长度可变(删除)
C语言中,数组长度不可动态定义
解决方法:另用一变量表示顺序表的长度属性
#define LIST_INIT_SIZE 100 //线性表存储空间的初始分配量
typedef struct{
ElemType elem[LIST_INIT_SIZE]; //定义数组
int length; //当前长度
}SqList;
typedef struct{
ElemType data[]; //定义数组
int length; //当前长度
}SqList; //总体来看,是一个【多项式】
ElemType是可变类型,根据后面data[]的实际类型变化,可以是int/char/float等
也可以先定义:
typedef char ElemType;
typedef int ElemType;
typedef struct{
ElemType data[MaxSize]; //data[MaxSize]放的是第一个元素的地址
int length;
}SqList; //顺序表类型
typedef struct{
ElemType *data; //*data也可以表示数组,多大呢?
int length;
}SqList; //顺序表类型
数组有多大?用动态内存分配的函数来分配内存:
SqList L; //L有两个成员
L.data = (ElemType*)malloc(sizeof(ElemType)*MaxSize);
//(ElemType*)强制类型转换成指向对应类型的指针
//sizeof(ElemType)中,ElemType的大小是根据数据类型变化的。
开辟m字节长度的地址空间,并返回这段空间的首地址。
计算变量x的长度
释放指针p所指变量的存储空间,即彻底删除一个变量
(需要加载头文件
int *p1 = new int; 或 int *p1 = new int(10);
功能:申请用于存放T类型对象的内存空间,并依初值列表赋以初值
结果值:
成功:T类型的指针,指向新分配的内存
失败:0(NULL)
功能:释放指针P所指向的内存。P必须是new操作的返回值
函数调用时传送给形参表的实参必须与形参三个一致。
类型、个数、顺序
①传值(参数为整形、实型、字符型等)(只改变形参,实参值不发生改变)
②传地址(参数为指针变量、引用类型、数组名)(共用一块空间,形参、实参都会变化)
传地址方式——指针变量作参数
传地址方式——引用类型作参数
引用:用来给一个对象提供一个替代的名字
①通过引用变量直接操作实参,形参变化实参也变化。
②引用变量,形参与实参共用一块存储空间。
#include
using namespace std;
void swap(float& m, float& n)
{
float temp;
temp = m;
m = n;
n = temp;
}
int main()
{
float a, b;
cin >> a >> b;
swap(a, b);
cout << a << endl << b << endl;
}
逻辑位序和物理位序相差1。
Sequence List 顺序表(SqList)
描述:
#define MAXSIZE 100
typedef struct{
ElemTpye elem [MAXSIZE];
int length;
} SqList; //静态分配
typedef struct{
ElemType *elem;
int length;
}SqList L; //定义变量L,L是SqList这种类型的,是个顺序表
L.elem = (ElemTpye*)malloc(sizeof(ElemTpye)*MAXSIZE); //动态分配
普通变量用xxx.xxx来引用,指针变量用xxx->xxx来引用
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
#define int Status;
#define char ElemType;
Status InitList_Sq(SqList &L){ //构造一个空的顺序表L
L.elem = new ElemType[MAXSIZE]; //首元素地址赋值给L.elem
if(!L.elem) exit(OVERFLOW); //存储分配失败,返回-2
L.length = 0; //空表长度为0
return OK;
}
void DestroyList(SqList &L){
if(L.elem) delete L.elem;
}
void ClearList(SqList %L){
L.length = 0; //将线性表长度置为0,内存仍存在
}
int GetLength(SqList L){
return (L.length);
}
void IsEmpty(SqList L){
if(L.length == 0) return 1;
else return 0;
}
(根据位置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;
}
随机存取
时间复杂度:O(1)——所有语句都只执行一次,常量阶。
int LOcateElem(SqList L, ElemType e){
//在线性表L中查找值为e的数据元素,返回其序号(第几个元素)
for(i = 0; i < L.length; i++) //亦可用while循环
if(L.elem[i] == e) return i+1; //查找成功,返回序号
return 0; //查找失败,返回0
}
时间复杂度:找到执行次数最多的一条语句。不能确定执行多少次,所以用平均查找长度。
为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。
时间复杂度:O(n)——最高次方是1次
插入位置在最后
插入位置在中间
插入位置在首位
①判断插入的位置i是否合法。
②判断顺序表的存储空间是否已满,若已满返回ERROR。
③将第n至第i位的元素依次向后移动一个位置,空出第i个位置。
④将要插入的新元素e放入第i个位置。
⑤表长+1,插入成功返回OK。
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; //将新元素he放入第i个位置
L.length++; //表长增1
return OK;
}
找到规律:插入位置 + 移动次数 = n+1
等差数列求和方式,得平均移动次数:n/2,级数为n
所以时间复杂度:O(n)
删除元素在最后
删除元素在中间
删除位置在首位
①判断删除的位置i是否合法(合法值为1 <= i <= n)。
(②将欲删除的元素保留在e中。)
③将第i+1至第n位的元素依次向前移动一个位置。
④表长-1,删除成功返回OK。
Status ListDelete_Sq(SqList &L, int i){
if(i<1 || i>L.length+1) return ERROR; //①i值不合法
for(j = i; j<=L.length-1; j++)
L.elem[j-1] = L.elem[j]; //被删除位置之后的元素前移
L.length--; //表长减1
return OK;
}
所以时间复杂度:O(n)
数据元素的存储映像,由数据域和指针域两部分组成。
n个结点由指针链组成一个链表
是线性表的链式存储映像,称为线性表的链式存储结构。
结点只有一个指针域的链表,称为单链表或线性链表。
结点由两个指针域的链表,称为双链表。
首尾相接的链表称为循环链表。
头指针:指向链表第一个结点的指针
首元结点:指链表中存储第一个数据元素a1的结点
头结点:在链表的首元结点之间附设的一个结点
头指针为空时表示空表
当头结点的指针域为空时表示空表
使在链表的第一个位置上的操作和其他位置一致,无需进行特殊处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理就一致了
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此节点不能计入链表长度值。
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
顺序表——随机存储
链表——顺序存取
单链表由表头唯一确定,因此可以用头指针的名字来命名。(头指针名是L,则把链表称为表L )
【嵌套定义】自己定义自己
typedef struct Lnode{
ElemType data;
struct Lnode *next;
}Lnode, *LinkList;
例:存储学生学号、姓名、成绩的单链表结点类型定义如下:
typedef Struct student{
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
struct student *next; //指针域
}Lnode, *LinkList;
LinkList L; //定义指向类型LinkList的指针L
先将要存储的多个数据项定义成一个数据类型
typedef Struct{
char num[8];
char name[8];
int score;
}ElemType;
typedef struct Lnode{
ElemType data;
struct Lnode *next;
}Lnode, *LinkList;
typedef struct LNode{
ElemType data;
struct LNode *next;
}Lnode, *LinkList; //定义新的类型名:①类型的结点LNode; ②指向这种结点类型的指针类型*LinkList
LinkList L; //定义头指针L 【LinkList类型本身是一个指针,所以L不用加*】
LNode *p, *s; //指向结点的指针 【需要加*】
p = L; //p指向头结点
s = L -> next; //s指向首元结点
p = p -> next; //p指向下一结点
构造一个空表(带头结点的单链表)
步骤:
(1)生成新结点作头结点,用头指针L指向头结点
(2)将头结点的指针域置空
算法描述:
Status InitList L(LinkList &L){
L = new LNode; //或C语言中的L = (LinkList)malloc(sizeof(LNode)); //将得到的地址复制个L
L -> next = NULL;
return OK;
}
空表:头指针和头结点存在,但链表中无元素,称为空链表。
【算法思路】判断头结点指针域是否为空
int ListEmpty(LinkList L){
if(L -> next) //非空
return 0;
else
return 1;
}
链表销毁后,头指针头结点都不存在了
【算法思路】从头指针开始,依次释放所有结点
Status DestroyList_L(LinkList &L){ //销毁单链表L
Lnode *p; //或LinkList p;
while(L){
p = L;
L = L -> next;
delete p;
}
return OK;
}
C: malloc(); free();
C++: new(); delete();
链表、头指针、头结点仍存在,但链表中无元素,成为空链表
【算法思路】依次释放所有结点,并将头结点指针域设置为空。
Status ClearList(LinkList &L){ //将L重置为空表
Lnode *p, *q; //或LinkList p,q;
p = L -> next;
while(p) //没到表尾
{
q = p -> next;
delete p;
p = q;
}
L -> next = NULL; //头结点指针域置位空
return OK;
}
算法思路:从首元结点开始,依次计数所有结点
int ListLength_L(LinkList L){
LNode *p; //或LinkList p;
p = L -> next;
i = 0;
while(p)
{
i++;
p = p -> next;
}
return i;
}
与顺序表不同,链表中取值只能从链表的首元结点出发,顺着链域next逐个结点向下访问,链表不是随机存取结构。
步骤:
(1)用指针p指向首元结点,用j做计数器初值赋为1
(2)从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p不为空(NULL),并且没达到序号为i的结点,则循环执行:
p指向下一个结点
计数器j+1
(3)退出循环时,若指针p为空,或者计数器j大于1,说明指定的序号i值不合法(i大于表长n或i<=0),取值失败返回ERROR;
否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。
Status GetElem_L(LinkList L, int i, ElemType &e){ //在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个元素的值
p = L->next; j = 1; //初始化,p指向首元结点,计数器j初值赋为1
while(p && j < i) //顺链域向后扫描,直到p为空或p指向第i个元素
{
p = p->next;
++j;
}
if(!p || j>1) return ERROR;
e = p->data;
return OK;
}
按值查找——给一个数据,找到后返回该数据所在的位置(地址)
Lnode *LocateElem_L(LinkListL, Elemtype e){
p = L->next;
while(p && p->data != e) //查找值e,找到则返回值为e的数据元素的地址
p = p->next;
return p;
}
按值查找——给一个数据,找到后返回该数据所在的位置序号
int 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;
}
在第i个结点前插入值为e的新节点
步骤:
①首先找到ai-1的存储位置p
②生成一个数据域为e的新结点s
③插入新结点:新结点的指针域指向结点ai、结点ai-1的指针域指向新结点
s->next = p->next; p->next = s;
【算法描述】
//在L中第i个元素之前插入数据元素e
Status ListInsert_L(LinkList &L, int i, ElemType e){
p = L; j = 0;
while(p && jnext;
++j;
}
if(!p || j>i-1) return ERROR; //如果i大于表长j+1,或者小于1,插入位置非法
s = new Lnode; //生成新节点s
s->data = e; //将结点s的数据域置为e
s->next = p -> next; //将结点s插入L中,接在i的前驱
p->next = s //将结点s接在i-1的后继
return OK;
}
删除第i个结点
【p->next = p->next->next】
//将线性表L中第i个数据元素删除
Status ListDelete_L(LinkList &L, int i, ElemType &e){
p = L; j = 0; q = i;
while(p->next && j < i - 1) //寻找第i个结点,并【令p指向其前驱i-1】
{
p = p->next;
++j;
}
if(!(p->next) || j > i - 1) return ERROR; //删除位置不合理
q = p->next; //q指针指向要删除的结点i,临时保存以备释放
p->next = q->next; //改变删除结点前驱结点的指针域 //或p->next = p->next->next
e = q->data; //保存删除结点的数据域
delete q; //释放删除结点的空间
return OK;
}
线性链表只能顺序存取,从头开始查找,时间复杂度O(n)
线性链表不需要移动元素,只需要修改指针,一把情况下时间复杂度O(1)。
但是如果需要查找插入位置,时间复杂度为O(n)
元素插入在链表头部(头结点后面),从最后一个结点开始操作插入
【思路】
L = new LNode; L->next = NULL; //或用C语言语法:L = (LinkList)malloc(sizeof(LNode));
p = new LNode; p->data = an; //new一个新的空间,将值an写入新空间的数据域
p->next = L->next; //将老结点的指针域 拿到 新节点的指针域
L->next = p; //将新结点接在头结点L后面
【算法描述】
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)
元素插在链表尾部
【算法描述】
//正位序输入n个元素的值,建立带头结点的单链表L
void CreateList_R(LinkList &L, int n){
L = new LNode; //建立头结点
L->next = NULL;
r = L; //尾指针r指向头结点
for(i = 0; i < n; ++i) //执行n次,插入n个结点
{
p = new LNode; //生成新节点
cin >> p->data; //输入元素值
p->next = NULL;
r->next = p; //插入到表尾
r = p; //指针r指向新的尾结点
}
}
时间复杂度:O(n)
循环链表是一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)。
优点:从表中任一结点出发均可以找到表中其他结点。
指针域指向本身。
循环链表中没有NULL指针,故执行遍历操作时,其终止条件应该:判断是否等于头指针。
表的操作常常是在表的首尾位置上进行。
操作:p存表头结点,Tb表头连接到Ta表尾,释放Tb表头结点,删除指针
p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next;
LinkList Connect(LinkList Ta, LinkList Tb){
p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next; //或free(Tb->nexr);
Tb->next = p;
return Tb;
}
时间复杂度是O(1)。
在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,这样链表中就形成两个方向不同的链。
双向链表的结构定义:
typedef struct DuLNode{
Elemtype data;
struct DuLNode *prior, *next;
}DuLNode, *DuLinkList;
p->prioir->next = p = p->next->prior
【算法2.13】
//在带头结点的双向循环链表L中第i个位置之前插入元素e
void ListInsert_Dul(DuLinkList &L, int i, ElemType e){
if(!(p = GetElemP_DuL(L,i))) return ERROR; //调用在链表L中找第i个位置的操作,将i地址赋值给p
s = new DuLNode; //指针变量s指向一个新节点
s->data = e; //s的data域赋值。以下修改双向四个指针:
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
【算法2.14】
//删除带头结点的双向循环链表L的第i个元素,并用e返回。
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;
}
O(n) 变成 O(1),牺牲空间效率,换成时间效率。
void union(List &La,List Lb){ //结果通过La返回,所以La要加&
La_len = ListLength(La); //求出表La的长度
Lb_len = ListLength(Lb); //求出表Lb的长度
for(i = 1; i <= Lb_len; i++)
{
GetElem(Lb, i, e); //将线性表Lb中第i个位置元素返回给e
if(!LocateElem(La, e)) ListInsert(&La, ++La_len, e); //如果没找到元素,则在线性表La中第++La_len个位置插入新元素e
}
}
时间复杂度:执行次数最多的是:ListInsert(&La, ++La_len, e);语句。属于for循环与if循环的内层
则时间复杂度:O(ListLength(La))*O(ListLength(Lb))
注:基本操作不用再写,直接用(见前文2.4.2)
有两种方法(顺序表、链表):
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
}
时间复杂度:取决于while(pa <= pa_last && pb <= pb_last)循环,取决于其中较短的表。与while(pa <= pa_last)或while(pb <= pb_last),将剩余的内容加入表LC。
则时间复杂度为:O(ListLength(LA)+ListLength(LB))
空间复杂度:表LC的表长,取决于表LA和表LB的长度和。
则空间复杂度为:O(ListLength(LA)+ListLength(LB))
用带头结点的单链表
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc){
pa = La->next; pb = Lb->next; //指针pa、pb指向两个首元结点
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的头结点
}
}
时间复杂度:while(pa && pb)较短的,最坏情况:pa、pb所有结点都要访问一次。
算法的时间复杂度:O(ListLength(La) + ListLength(Lb))
空间复杂度:只在原来的链表上操作,不需要额外的空间。
算法的空间复杂度:O(1)
【因笔者所使用编辑器限制,笔者无法将图片插入笔记中,大家在阅读笔记时可能有所不便。如有需要,请联系笔者发送完整笔记。给您带来不便深表歉意,谢谢!】