第二章 线性表
一、线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,其一般表示为L=(a1,a2,a3,…,ai,ai+1,…,an)。
线性表的特点:
1.表中元素的个数有限。
2.表中元素具有逻辑上的顺序性,表中元素有其先后次序。
3.表中元素都是数据元素,每个元素都是单个元素。
4.表中元素的数据类型都相同,每个元素占用相同大小的存储空间。
ai是线性表中的第i个元素在线性表中的位序。
a1是唯一的第一个数据元素,称为表头元素;an是唯一的最后一个数据元素,称表尾元素。除第一个元素以外,每个元素有且仅有一个直接前驱;除最后一个元素以外,每个元素有且仅有一个直接后继。
注意:位序下标是从1开始的,数组的下标从0开始。
二、线性表的基本操作
InitList(&L):初始化表。构造一个空表,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并且释放线性表L所占的内存空间。
ListInsert(&L,I,e):插入,在表L的第i个位置插入指定元素e。
ListDelete(&L,I,&e):删除,删除表L的第i个位置元素,并用e返回删除元素的值。
LocateElem(L,e):按值查找,在表L查找具有给定关键字值的元素。
GetElem(L,i):按位查找,获取表L中的第i个位置的元素值。
Length(L):求表长,返回线性表的长度,即L中的数据元素个数。
PrintList(L):输出操作,按前后顺序输出线性表L的所有元素。
Empty(L):判空操作,若L为空表,则返回true,否则返回false。
Ps:什么时候要传入引用‘&’—对参数的修改结果需要‘带回来’。
三、线性表的顺序表示—顺序表
3.1 顺序表的定义
线性表的顺序存储又称为顺序表,用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也是相邻的。顺序表的特点是逻辑顺序与其物理顺序相邻。
顺序存储就是把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
如果线性表第一个元素的存放位置为LOC(L),第二个位置就是LOC(L)+数据元素的大小,
LOC(L)+数据元素大小*2
获取数据元素大小 sizeof(int)
3.2静态分配
#define MaxSize 10 //定义最大长度
typedef struct
{
ElemType data[MaxSize]; //用静态的数组存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)
在静态分配时,由于数组的大小和空间事先已经固定,一旦空间占满,再加入新的数据将会产生溢出,进而导致程序的崩溃。而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用来替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性划分所有空间。
3.3 动态分配
#define InitSize 10 //顺序表的初始长度
typedef struct
{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList; //顺序表的类型定义 动态
动态申请和释放内存空间 malloc、free
L.data=(ElemType *)malloc(sizeof(ElemType )*InitSize);
#include <stdio.h>
#include <stdlib.h>
#define InitSize 10 //默认的最大长度
typedef struct
{
int *data;//指示动态分配数组的指针
int MaxSize;//顺序表的
int length;//顺序表的当前长度
}SeqList;
void InitList(SeqList &L)
{
//用malloc函数申请一片连续的存储空间
L.data=(int *)malloc(InitSize*sizeof(int));
L.length=0;
L.MaxSize=InitSize;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len)
{
int *p=L.data;
L.data=(int *)malloc((L.MaxSize+len)*sizeof(int));
//将数据复制到新区域
for(int i=0;i<L.length;i++)
L.data[i]=p[i];
L.MaxSize=L.MaxSize+len;
free(p);
}
int main()
{
SeqList L;//声明一个顺序表
InitList(L);//初始化顺序表
}
由于要将数据复制到新的区域,时间开销大。
3.4 顺序表的特点
- 随机访问,可以在O(1)时间内找到第i个元素。
- 存储密度高,每个存储节点只存储数据元素。
- 拓展容量不方便。
- 插入删除操作不方便,需要移动大量元素。
四、顺序表的插入和删除
4.1 插入
ListInsert(&L,i,e);插入,在表L的第i个位置插入指定元素e。
bool ListInsert(SqList &L,int i,int e)
{
if (i<1|| i>L.length+1)
return false;
if(L.length>=MaxSize)
return false;
//将第i个元素位置后移
for(int j=L.length;j>=i;j--)
L.data[j]=L.data[j-1];
L.data[i-1]=e;
L.length++;
return true;
}
时间复杂度:
最好时间复杂度:新元素插入表尾,不需要移动元素,O(1)
最坏时间复杂度:新元素插入表头,n个元素后移,O(n)。
平均时间复杂度:新元素插入到任何一个位置的概率相同为p=1/(n+1),O(n)=np+(n-1)p+…+p=n(n+1)/2*p=n/2=O(n)
4.2 删除
ListDelete(&L,i,&e):删除,删除表L第i个元素,并且用e返回。
#include<stdio.h>
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int length;
}SqList;
void InitList(SqList &L)
{
for(int i=0;i<MaxSize;i++)
L.data[i]=0;
L.length=0;
}
bool ListDelete(SqList &L,int i,int &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;
}
int main()
{
SqList L;
InitList(L);
L.data[0]=1;
L.data[1]=2;
L.data[2]=3;
L.data[3]=4;
L.length=4;
int e=-1;
if(ListDelete(L,3,e))
printf("已删除第三个元素%d\n",e);
else
printf("错误%d\n",e);
for(int i=0;i<L.length;i++)
printf("第%d个=%d\n",i+1,L.data[i]);
}
最好情况:删除表尾元素,O(1)
最坏情况:删除表头元素,循环n-1次,O(n)
平均情况:p=1/n,T(n)=(n-1)p+…+p=n(n-1)/2/n=(n-1)/n=O(n)
五、顺序表查找
5.1 顺序表按位查找
GetElem(L,i):按位查找,获取L中的第i个位置的元素的值。
静态分配中:
int GetElem(SqList L,int i)
{
return L.data[i-1];
}
动态分配中:
ElemType GetElem(SeqList L,int i)
{
return L.data[i-1];
}
时间复杂度为O(1)—随机存取的特性
5.2 顺序表按值查找
LocateElem(L,e):按值查找,在L中查找具有给定关键字的元素。
//在L中按值查找第一个元素等于e的元素,并返回位序
int LocateElem(SeqList L,int e)
{
for(int i=0;i<L.length;i++)
if(L.data[i]==e)
return i+1;
return 0;
}
最好情况:目标元素在表头,O(1)
最坏情况:目标元素在表尾,O(n)
平均情况:目标元素出现的概率为1/n,
T(n)=1/n+21/n+…+n1/n=(n+1)/2=O(n )
六、单链表
线性表的链式存储又称单链表,指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表的结点,除了存放元素自身的信息,还需要存放一个指向其后继的指针。 Data为数据域,next指针域,存放指向后继结点的指针
利用链表可以解决顺序表需要大量连续存储单元的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。单链表的元素离散的分布在存储空间中,所以单链表是非随机存取的存储结构。不能找到某个特定的结点,查找结点时,需要从头开始遍历,依次查找。
struct LNode//结点 定义单链表结点类型
{
ElemType data;//数据域 每个结点存放一个数据元素
struct LNode *next;//指针域 指针指向下一个结点
}
struct LNode *p=(struct LNode *)malloc(sizeof(struct LNode));
typedef struct LNode
{
ElemType data;
struct LNode * next;
}LNode,*LinkList;
struct LNode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode;
typedef struct LNode *LinkLIst;
表示一个单链表,只需声明一个头指针L,指向单链表的第一个结点
LNode *L==LinkList L
LinkList 强调这是一个单链表 LNode *强调这是一个结点
6.1 不带头结点的单链表
//不带头结点
typedef struct LNode
{
ElemType data;
struct LNode * next;
}LNode,* LinkList;
//初始化一个空的单链表
bool InitList(LinkList &L)
{
L=NULL;
return true;
}
void test()
{
//声明一个指向单链表的指针
LinkList L;
//初始化
InitList(L)
}
声明指向单链表的指针没有创建一个结点。只有一个头指针指向NULL。
判断单链表是否为空。
bool Empty(LinkList L)
{
return (L==NULL);
}
6.2 带头结点
//带头结点
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L)
{
L=(LNode *)malloc(sizeof(LNode));//分配一个头结点,头指针L指向头结点
if(L==NULL)
return false;
L->next =NULL;//头结点暂时没有结点,头结点不存储数据,只为了以后的操作方便
return true;
}
viod test()
{
LinkList L;//声明一个指向单链表的指针
//初始化一个空表
InitList(L);
}
判断带头结点的单链表是否为空
bool Empty(LinkList L)
{
if(L->next == null)
return true;
else
return false;
}
6.3 按位序插入(带头结点)
ListInsert(&L,i,e):插入操作,在L中的第i个位置插入元素e。(找到i-1个结点,将新结点插入其后)。
bool ListInsert(LinkList &L,int i,int e)
{
if(i<1)
return false;
//指针p指向当前扫描到的结点
LNode *p;
//当前指针指向的是第几个结点
int j=0;
//L指向头结点,头结点是第0个结点(不存数据)
p=L;
//找到i-1个结点
while(p!=NULL && j<i-1)
{
p=p->next;
j++;
}
if(p=NULL)
return false;
//申请一个新的结点
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
//将s结点连到p之后
s->next=p->next;
p->next=s;
return true;
}
6.4 按位序插入(不带头结点)
找到第i-1个结点。
由于不存在第0个结点,因此i=1时需要特殊处理。
#include<stdio.h>
#include<stdlib.h>
typedef struct LNode
{
int data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L)
{
L=NULL;
return true;
}
bool ListInsert(LinkList &L,int i,int e)
{
if(i<1)
return false;
//插入第一个结点
if(i==1)//i=1时需要特殊处理
{
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s;
return true;
}
LNode *p;//指针p指向当前扫描到的结点
int j=1;//当前p指向的是第几个结点
p=L;//p指向第一个结点 不是头结点
while(p!=NULL && j<i-1)
{
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
int main()
{
LinkList L;
InitList(L);
return 0;
}
指定结点的后插操作
bool InsertNextNode(LNode *p,int e)
{
if(p==NULL)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
指定结点前插:
bool InsertPriorNode(LNode *p,int e)
{
if(p==NULL)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
s->next=p->next;
p->next=s;
s->data=p->data;//将p结点中的数据复制到s
p->data=e;
return true;
}
6.5 按位序删除
bool ListDelete(LinkList &L,int i,int &e)
{
if(i<1)
return false;
LNode *p;
int j=0;
p=L;
while(p!=NULL && j<i-1)
{
p=p->next;
j++;
}
if(p==NULL)
return false;
//令q指向被删除的结点
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
6.6 删除指定结点p
bool DeleteNode(LNode *p)
{
if(p==NULL)
return false;
LNode *q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
return true;
}
6.6 按位查找
LNode * GetElem(LinkList L,int i)
{
if(i<0)
return NULL;
//指针p扫描到的当前结点
LNode *p;
int j=0;//当前p指向的是第几个结点
p=L;//L指向头结点
while(p!=NULL && j<i)
{
p=p->next;
j++;
}
return p;
}
6.7 按值查找
//按值查找,找到数据域==e的结点
LNode * LocateElem(LinkList L,int e)
{
LNode *p=L->next;
//从第一个结点开始查找数据域为e的结点
while(p!=NULL && p->data != e)
{
p=p->next;
}
return p;//找到后返回指针p 否则返回NULL
}
6.8 求表长
//求表的长度
int Length(LinkList L)
{
int len=0;
LNode *p=L;
while(p->next!=NULL)
{
p=p->next;
len++;
}
return len;
}
6.9 尾插发建立单链表
给你很多数据元素怎么存入一个单链表中。
1、初始化一个单链表。
2、每次取一个元素,插入到表头/表尾(带头结点)。
//初始化单链表
//设置变量length记录链表的长度
while循环
{
每次取一个数据元素e;
ListInsert(L,Length+1,e);//插到尾部
length++;
}
设置一个尾指针r总是指向最后一个结点,在最后一个结点插入数据。
int main()
{
int x;
int length=0;
LinkList L;
InitList(L);
LNode *s,*r=L;//r为尾指针
scanf("%d",&x);
while(x!=9999)
{
//s指向一个新结点
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;//永远保证r指向最后一个结点
scanf("%d",&x);
length++;
}
r->next=NULL;
return 0;
}
时间复杂度为O(n)
6.10 头插法建立单链表
也是对指定结点的后插操作(头结点)。
int main()
{
int x;
LinkList L;
InitList(L);
LNode *s;
scanf("%d",&x);
while(x!=9999)
{
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return 0;
}
只要初始化 头指针指向NULL
输入:9,11,13 输出:13,11,9 链表的逆置
七、双链表
单链表:无法逆向检索双链表:可进可退
定义:
typedef struct DNode //定义双链表结点类型
{
ElemType data;//数据域
struct DNode *prior,*next;//前驱和后继指针
}DNode,*DLinkList;
7.1 带头结点的双链表初始化
#include<stdio.h>
#include<stdlib.h>
typedef struct DNode //定义双链表结点类型
{
int data;//数据域
struct DNode *prior,*next;//前驱和后继指针
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L)
{
//分配一个头结点
L=(DNode *)malloc(sizeof(DNode));
//内存不足分配失败
if(L==NULL)
return false;
//头结点的prior永远指向NULL
L->prior=NULL;
//头结点之后暂时没有结点
L->next=NULL;
return true;
}
int main()
{
DLinkList L;
InitDLinkList(L);
return 0;
}
判断双链表是否为空(带头结点)
bool Empty(DLinkList L)
{
if(L->next==NULL)
return true;
else
return false;
}
7.2 后插操作
在p结点之后插入s结点(注意修改指针时的顺序)
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s)
{
if(p==NULL || s==NULL)
return false;
s->next=p->next;
if(p->next!=NULL)//如果p结点后面有结点
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
核心代码:
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
7.3 双链表的删除
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p)
{
if(p==NULL)
return false;
//找到p结点的后继结点
DNode *q=p->next;
if(q==NULL)
return false;//p没有后继结点
p->next=q->next;
if(q->next!=NULL)
q->next->prior=p;
free(q);
return true;
}
核心代码:
p->next=q->next;q->next->prior=p;free(q);
销毁双链表
//销毁双链表
void DestoryList(DLinkList &L)
{
//循环释放各个数据结点
while(L->next!=NULL)
{
DeleteNextDNode(L);
}
free(L);//释放头结点
L=NULL;//头指针指向NULL
}
7.4 双链表的遍历
后向遍历:
while(p!=NULL)
{
//对结点p做相应的处理比如打印
p=p->next;
}
前向遍历
while(p!=NULL)
{
//对结点p做相应的处理比如打印
p=p->prior;
}
双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度O(n)。
八、循环单链表
单链表:表尾结点的next指针指向NULL。
循环单链表:表尾结点的next指针指向头结点。
在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。因为单链表是一个环,因此在任何一个位置的插入和删除操作都是等价的,无须判断是否是表尾。
在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意一个结点开始遍历整个链表。
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
bool InitList(LinkList &L)
{
L=(LNode *)malloc(sizeof(LNode));//分配一个头结点,头指针L指向头结点
if(L==NULL)
return false;
L->next =L;//头结点next指向头结点
return true;
}
判断循环链表是否为空
bool Empty(LinkList L)
{
if(L->next==L)
return true;
else
return false;
}
判断结点p是否为循环单链表的表尾结点
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p)
{
if(p->next==L)
return true;
else
return false;
}
单链表:从一个结点出发只能找到后续的各个结点。
循环单链表:从一个结点出发可以找到其他任何一个结点。
8.1 循环双链表
双链表:表头结点的prior指向NULL,表尾的next指向NULL。
循环双链表:表头结点的prior指向表尾结点,表尾的next指向头结点。
bool InitDLinkList(DLinkList &L)
{
//分配一个头结点
L=(DNode *)malloc(sizeof(DNode));
//内存不足分配失败
if(L==NULL)
return false;
//头结点的prior永远指向NULL
L->prior=L;
//头结点之后暂时没有结点
L->next=L;
return true;
}
判空操作:
bool Empty(DLinkList L)
{
if(L->next==L)
return true;
else
return false;
}
判断结点p是否为循环双链表的表尾结点
//判断结点p是否为循环双链表的表尾结点
bool isTail(DLinkList L,DLNode *p)
{
if(p->next==L)
return true;
else
return false;
}
在p结点之后插入s结点
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s)
{
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
}
删除p结点之后的结点q
//删除p的后继结点q
p->next=q->next;
q->next->prior=p;
free(q);
九、静态链表
静态链表借助数组来描述线性表的链式存储结构,结点有数据域data也有指针域next,这里的指针时结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。0号结点为头结点。游标为-1表示已经到达表尾。
#define MaxSize 10//静态链表的最大长度
typedef struct
{
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
}SLinkList[MaxSize];
静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表相同,只需要修改指针,而不需要移动元素。
十、顺序表和链表的比较
逻辑结构:
都属于线性表,都是线性结构。采用顺序存储时,逻辑上相邻的的元素,对应的物理存储位置也相邻。而采用链式存储逻辑上相邻的元素,物理位置则不一定相邻,对应的逻辑关系是通过指针链接来表示的。
物理结构/存储结构:
顺序表可以顺序存取,也可以随机存取,存储密度高(不需要存储指针),链表只能从表头顺序存取元素,不可随机存取,存储密度低。顺序表需要大片连续空间分配不方便,改变容量不方便。链表存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
空间分配:顺序存储在静态分配的情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。但是预先分配过大,可能会导致顺序表后部大量的闲置。动态分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率低。链表的存储结点只在需要时申请分配,操作灵活、高效。
实际中怎么选取存储结构呢?
1、基于存储考虑
难以估计线性表的长度或存储规模时,不宜采用顺序表;链表不用事先估计存储规模,但链表的存储密度较低。
2、基于运算考虑
在顺序表中按序号访问ai的时间复杂度为O(1),而链表中按序号访问的时间复杂度为O(n),因此若经常做的运算是按序号访问数据元素,则显然顺序表优先于链表。
在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点不能忽视。在链表中进行插入、删除操作时,虽然也要找插入位置,但操作主要是比较操作,从这个角度来考虑后者优于前者。
3、基于环境考虑
顺序表容易实现,任何高级语言都有数组类型;链表的操作时基于指针的,相对来讲,前者实现相对简单。
时间复杂度总结:
顺序表:
插入操作:最好情况O(1);最坏情况O(n);平均情况O(n)。
删除操作:最好情况O(1);最坏情况O(n);平均情况O(n)。
按值查找(顺序查找):最好情况O(1);最坏情况O(n);平均情况O(n)。
单链表:
按序号查找:O(n)
按值查找:O(n)
插入操作:时间开销主要是查找i-1个元素 O(n)
删除操作:O(n)
引入头结点的优点:
1.由于第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置的操作和其他位置上的操作一致,无须进行特殊的处理。
2.无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理得到了统一。