数据结构和算法中更是生活中最常用的结构:线性表
线性表的基本概念
线性表的定义
线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。
在稍复杂的线性表中,一个数据元素可由多个数据项(item)组成,此种情况下常把数据元素称为记录(record),含有大量记录的线性表又称文件(file)。
线性表中的个数n定义为线性表的长度,n=0时称为空表。在非空表中每个数据元素都有一个确定的位置,如用ai表示数据元素,则i称为数据元素ai在线性表中的位序。
线性表的相邻元素之间存在着序偶关系。如用(a1,…,ai-1,ai,ai+1,…,an)表示一个顺序表,则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。
线性表的特点
- 数据元素之间具有一种线性的或“一对一”的逻辑关系。
- 第一个数据元素没有前驱,这个数据元素被称为开始节点;
- 最后一个数据元素没有后继,这个数据元素被称为终端节点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继
线性表的抽象数据类型
线性表的抽象数类型定义
ADT 线性表
Data :
线性表的数据对象集合为{a1,a2,......,an},每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation:
InitList(*L)
操作结果:构造一个空的线性表L
ClearList(*l)
初始条件:线性表已存在
操作结果:置线性表L为空表
ListEmpty(L)
初始条件:线性表已存在
操作结果:若线性表L为空表,则返回TRUE,否则返回FALSE
ListLenght(L)
初始条件:线性表已存在
操作结果:返回线性表L数据元素个数
GetElem(L,i,*e)
初始条件:线性表已存在(1≤i≤ListLenght(L))
操作结果:用e返回线性表L中第i个数据元素的值
LocateElem(L,e)
初始条件:线性表已存在
操作结果:在线性表L种查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号;否则返回0
ListInsert(*L,i,e)
初始条件:线性表已存在(1≤i≤ListLenght(L)+1)
操作结果:在线性表L中第i个数据元素之前插入新元素e,L长度加1
ListDelete(*L,i,*e)
初始条件:线性表已存在(1≤i≤ListLenght(L))
操作结果:删除线性表L中第i个数据元素,用e返回其值,线性表L长度减1
endADT
实现两个线性表A和B的并集操作
void unionL(List *La,List *Lb)
{
int La_len,Lb_len,i;
ElemType e;
La_len = ListLength(*La);
Lb_len = ListLength(*Lb);
for (i=1;i<=Lb_len;i++)
{
GetElem(Lb,i,&e);/*取Lb中第i个数据元素赋给e* */
if (!LocateElem(*La,e))/*La中不存在和e相同数据元素*/
ListInsert(La,++La_len,e);/*插入*/
}
}
线性表的顺序存储结构
顺序存储结构相关定义
线性表的顺序存储结构指的是用一组连续的存储单元一次存储线性表中的各个元素,使得线性表中在逻辑上相连的元素存储在连续的物理存储单元上。通过数据元素物理存储的连续性来反映数据元素在逻辑上的相邻关系。采用顺序存储结构的线性表通常叫做顺序表
顺序存储方式
各种语言都一般采用一维数组实现
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];/*数组存储数据元素,最大值为MAXSIZE*/
int length;/*线性表当前长度*/
}SqList
数据长度和线性表长度区别
数组的长度是不变的
线性表的长度是可变的
地址计算
数组和线性表长度区别是,数组一般是0,1,2,线性表一般是a1,a2,a3…
简单说,数组从0开始,线性表从1开始
假设一个数据元素占用c个存储单元,LOC表示Location
则有公式 LOC(ai)=LOC(a)+(i-1)*c
通过这个公式可知,计算顺序表任意位置的地址的时间都相同,那么可知对于计算机,存取顺序表数据都是O(1),通常把具有这一特点的存储结构称为随机存取结构
顺序存储结构的基本操作
取元素操作
GetElem:将顺序表L中第i个位置元素返回,即返回数组的i-1下标的值
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/*Status是函数的类型,值是函数结果状态*/
/*初始条件:顺序表L存在,且不为空表*/
/*操作结果:e返回L中第i个数据元素的值*/
Status GetElem(SqList L,int i,ElemType *e)
{
if (L.length==0 || i < 1 || i >L.length)
return ERROR;
*e = L.data[i-a];
return OK;
}
插入元素操作
插入操作实现思路,假设插入位置为i
- 插入位置不合理,抛出异常
- 顺序表长度大于数组长度,抛出异常或动态增长数组
- 从最后一个元素向前遍历到第i个位置,分别将他们向后移动一个位置
- 将要插入元素填入位置i处
- 顺序表表长增加1
实现代码
/*始条件:顺序表L已存在,且i位置存在*/
/*操作结果:在L中i位置之前插入新的数据元素e,L长度+1*/
Status ListInsert(SqList )
{
int k;
if (L->length==MAXSIZE)/*顺序表已满*/
return ERROR;
if (i<1 || i > L->length+1)/*i不在范围内*/
return ERROR;
if (i<=L->length)/*插入位置不在表尾*/
{
for (k=L->length-1;k>=i-1;k--)/*将要插入位置后数据元素向后移动一位*/
L->data[k+1]=L->data[k];
}
L->data[i-1]=e;/*将新元素插入*/
L->length++;
return OK;
}
删除操作
删除操作思路
- 删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置
- 表长-1
实现代码
/*初始条件:顺序表L已存在,表不为空,小于表长*/
/*操作结果:删除L的第i个元素,并用e返回其值,L的长度-1*/
Status ListDelete(SqList *L,int,ElemType *e)
{
int k;
if (L->length == 0)/*空表*/
return ERROR;
if (i<1 || i>L->length)
return ERROR;
*e =L->data[i-1];
if (i<L->length)
{
for (k=i;k<L->length;k++)
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
插入和删除的平均复杂度都为O(n)
分析过程
- 最优情况为插入最后一个元素或删除最后一个元素O(1)
- 最差情况为插入第一个元素或删除第一个元素 O(n)
- 所以有(n-1)/2
- 根据时间复杂度定义得 O(n)
线性表顺序存储结构的优缺点
线性表顺序存储结构的优点
- 存取速度高效,通过下标来直接存储
- 可以快速取出任一位置元素O(1)
线性表顺序存储结构的缺点
- 插入和删除需要移动大量元素,比较慢O(n)
- 不可以增长长度,顺序表容量难以确定
- 易造成存储空间“碎片”
线性表的链式存储结构
链表的几个概念
线性表的链式存储表示的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素 与其直接后继数据元素 之间的逻辑关系,对数据元素 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信息组成一个==“结点”(Node)== 不是节点 ,表示线性表中一个数据元素。数据域+指针域=结点
链表的第一个结点的存储位置叫头指针
一般线性链表的最后一个结点指针为“空”,用NULL或^表示
单链表的第一个结点前可以设置一个结点,称为头结点
也可以不设置头结点,这时第一个结点一般称为首元结点
头指针、头结点、首元结点的异同
从设置目的来看头指针、头结点、首元结点
- 头结点:头结点为了方便操作链表而附设的,数据域一般无意义。
- 首元结点:首元结点作为链表的开始结点。
- 头指针:头指针为了指向链表的基地址。
从必要性来看头指针、头结点、首元结点
设置头结点情况
不设置头结点情况
所以头结点不是必须的
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
线性表链式存储结构代码描述
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList;/*定义LinkList*/
设p是指向线性表第i个元素的指针,则
- p->data:其值是一个数据元素,表示结点ai的数据域
- p->next:其值是一个指针,表示结点ai的指针域,指向第i+1个元素
- p->data=ai
- p->next->data=ai+1
- p和(p->next)都是指针
链式存储结构的基本操作
单链表
单链表的读取
读取单链表第i个元素的思路
- 声明一个结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回结点p的数据。
代码实现
/*初始条件:顺序线性表L已存在,且1<=i<=ListLength(L)*/
/*操作结果:用e、返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p;/*声明一指针p*/
p = L ->next;/*让p指向链表L的第一个结点*/
j = 1;/*j为计数器*/
while (p && j<i)/* *p不为空且计数器j值不等于i,循环继续 */
{
p = p->next;/* p指向下一结点 */
++j;
}
if (!p || j>i)
return ERROR;/*第i个结点越界或不存在*/
*e = p->data; /*取第i个结点的数据*/
return OK;
}
核心思想:工作指针后移
最好的情况:i=1时不需要遍历
最坏的情况:i=n时遍历n-1次
故 O(n)
因为不知道要循环的次数,因此不用for循环,得用while循环
单链表元素的插入
单链表第i个数据插入结点算法思路
- 声明一结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,在系统中生成一个空节点s;
- 将数据元素e赋值给s->data;
- 单链表的插入标准语句:s->next=p->next; p->next=s;
- 返回成功。
单链表第i个数据插入实现代码
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度+1*/
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while (p && j < i)/*寻找i-1个结点*/
{
p = p ->next;
++j;
}
if (!p || j>i)
return ERROR;/*第i个结点不存在*/
s = (LinkList)malloc(sizeof(Node));
s->data=e;
s->next=p->next;/*将p的后继结点给s的后继*/
s->next=s;/*将s赋值给p的后继*/
return OK;
}
单链表元素的删除
将结点q删除,其中存储元素ai的结点为q。
其实就是将它的前继结点的指针绕过,指向它的后继结点即可。
核心操作代码:q=p->next; p->next=q->next; (p->next=p->next->next,用q->next来取代p->next)
操作思路
- 声明一结点p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句:p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功。
/*初始条件:顺序表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个结点,并用e返回其值,L的长度-1*/
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while (p->next && j < i)
{
p = p -> next;
++j;
}
if (!(p->next)||j>i)
return ERROR;/*第i个结点不存在*/
q = p->next
p->next=q->next;/*将q的后继赋值给p的后继*/
*e = q->data;/*将q结点中的数据给e*/
free(q);/*系统回收结点,释放内存*/
return OK
}
- 单链表的插入和删除:首先是遍历查找第i个元素,其次是插入和删除操作
- 故时间复杂度是O(n)
- 当插入删除一个元素时,与顺序结构比,没有太大优势
- 但是当从第i个元素的位置插入10个元素时:
- 顺序结构:每次插入都需要移动元素,每次都是O(n)
- 单链表:只需要在第一次时,找到第i个位置的指针,此时为O(n),后面每次都是O(1)
- 综上所述,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
单链表的创建
创建思路
- 声明一结点p和计数器变量i;
- 初始化一空链表L;
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
- 循环:
- 生成一新结点赋值给p;
- 随机生成一数字赋值给p的数据域p->data;
- 将p插入到头结点与前一新结点之间。
头插法创建单链表
/*随机产生n个元素的值,建立待头结点的单链表L(头插法)*/
void CreateListHead(LinkList *L,int n)
{
LinkList p;
int i;
srand(time(0));/*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;/*先建立一个带头结点的单链表*/
for (i=0;i<n;i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand()%100+1;/*随机生成100以内的数字*/
p->next=(*L)->next;
(*L)->next=p;/*插入到表头*/
}
}
先把 (* L) -> next 赋值给 p 结点的指针域,再把 p 结点赋值给头结点的指针域
否则会覆盖 ( * L ) -> next
尾插法创建单链表
实现代码
/*随机产生n个元素的值,建立带头结点的单链表(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
LinkList p,r;
int i;
srand(time(0));/*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
r=*L;/*r为指向尾部的结点*/
for (i=0;i<n;i++)
{
p=(Node *)malloc(sizeof(Node));/*生成新结点*/
p->data = rand()%100+1
r->next=p;/*将表尾终端结点的指针指向新结点*/
r=p;/*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL;/*表示当前链表结束*/
}
单链表的删除
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环;
- 将下一结点赋值给q;
- 释放p;
- 将q赋值给p
实现代码
/*初始条件:单链表已存在;操作结果:单链表置空,且从内存删除*/
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next;/*p指向第一个结点*/
while(p)/*未到尾部*/
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL;/*头结点指针域为空*/
return OK;
}
p是必须的,否则无法传递p下一个结点
单链表和顺序表的对比
- 存储分配方式
- 顺序表:连续的存储单元
- 单链表:一组任意的存储单元
- 时间性能
- 查找
顺序表:O(1)
单链表:O(n) - 插入删除
顺序表:O(n)
单链表:找出目标位置后,O(1)
- 查找
- 空间性能
若线性表多查找,少插删,宜采用顺序存储结构;若多查删,宜采用单链表结构
若线性表的元素个数变化大或不确定时,宜采用单链表;若确定或事先明确个数,用顺序存储效率更高
静态链表
起源
c有指针,而有些面向对象语言如java、c#等启用了对象引用机制,间接实现了指针的作用;而有些语言如basic、python等没有指针,如何实现链表结构呢?
解决思路:用数组代替指针,来描述单链表
定义
用数组描述的链表叫做静态链表,或曰“游标实现法”。
数组的元素都是由两个数据域组成的,data(数据)和cur(游标)
即数组的每个下标都对应一个data和一个cur
- 数据域data:用来存放数据元素,也就是我们要处理的数据。
- 游标cur:相当于单链表中的next指针,存放该元素的后继在数组中的下标。
注:此处的游标cur不是数组的索引,而是数组索引之外的又一个值
循环链表(circular linked list)
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成了一个环
解决了一个问题:即如何从链表一个结点出发,访问到链表的全部结点。
p=rearA->next;/*保存A的头结点,即1*/
rearA->next=rearB->next->next;/*指向B的首元结点,赋值给rearA的指针域,即2*/
q=rearB->next;
rearB->next=p;/*将A头结点赋给rearB指针域,即3*/
free(q);/*释放q*/
循环链表和单链表的差异
循环的判断条件
单链表:判断p->next是否为空
循环链表:判断p->next不等于头结点,则循环未结束
尾指针rear
引入: 有头结点时,访问第一个结点用 O(1),但访问最后一个结点用O(n)
有了尾指针以后,访问第一个结点和最后一个结点的时间均为O(1)了
尾指针:rear
头指针:rear->next
双向链表
在单链表的每个结点中,再设置一个指向其前驱结点的指针域
即在双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱
代码描述
/*双向链表*/
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior;/*前驱指针*/
struct DulNode *next;/*后继指针*/
}DulNode, *DuLinkList;
双向链表的插入操作
- 先s的前驱和后继
- 后结点的前驱
- 前结点的后继
s->prior=p;/*1*/
s->next=p->next;/*2*/
p->next->prior=s/*3*/
p->next=s;/*4*/
双向链表的删除操作
- 后继赋前结点后继
- 前驱赋后结点前驱
p->prior->next=p->next;
p->next->prior=p->prior;
free(q)