数据结构笔记二:线性表
线性表
线性表的定义
线性表是具有相同数据类型的\(n(n\ge 0)\)个数据元素的有限序列,其中\(n\)为表长,当\(n=0\)时线性表是一个空表。若用\(L\)命名线性表,则其一般表示为
\(a_i\)是线性表中的“第i个”元素线性表中的位序(位序从1开始,数组下标从0开始)
\(a_1\)是表头元素;\(a_n\)是表尾元素
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作
InitList(&L ): //初始化表。构造一个空的线性表L,分配内存空间
DestoryList(&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的长度,即L中数据元素的个数
PrintList(); //输出操作。按前后顺序输出线性表L的所有元素值
Empty(L); //判空操作。若L为空表,则返回true,否则返回false;
线性表的顺序表示
顺序表的定义
顺序表——用顺序存储的方式实现线性表。
//定义
//静态分配
#define MaxSize 10 //定义最大长度
typedef struct{
Elemtype data[Maxsize]; //顺序表的元素
int length; //顺序表的当前长度
}Sqlist;
//动态分配
#define InitSize 100 //表长度的初始定义
typedef struct{
Elemtype *data; //指示动态分配数组的知识
int MaxSize,length; //数组的最大容量和当前个数
}SeqList;
//c动态分配语句
L.data=(Elemtype*)malloc(sizeof(Elemtype)*InitSize);
//c++动态分配语句
L.data=new Elemtype[InitSize];
顺序表的特定:
- 随机访问,即可以在\(O(1)\)s时间内找到第i个元素
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便(基表采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入,删除操作不方便,需要移动大量元素
顺序表上的基本操作的实现
插入操作
//静态分配下
bool ListInsert(SqList &L,int i,Elemtype e)
{
if(i<1||i>L.length+1) //判定i的范围是否有效
return false;
if(L.length>=MaxSize) //当前存储空间已满,不能插入
return false;
for(int j=L.length;j>=i;--j) //后移
L.data[j]=L.data[j-1];
L.data[i-1]=e;
L.length++;
return true;
}
最好情况:在表尾插入(即\(i=n+1\)),循环0次;最好时间复杂度为\(O(1)\)
最坏情况:在表头插入(即\(i=1\)),循环n次;最好时间复杂度为\(O(n)\)
平均情况:假设\(p_i(p_i=1/(n+1))\)是在第\(i\)个位置上插入一个结点的概率,则移动结点的平均次数为
插入算法的平均时间复杂度为\(O(n)\)
删除操作
//静态分配下
bool ListDelete(SqList &L,int i,Elemtype& e)
{
if(i<1||i>L.length+1) //判定i的范围是否有效
return false;
e=L.data[i-1]; //赋值给e
for(int j=i;j<L.length;j++)
L.data[j-1]=L.data[j];
L.length--;
return true;
}
最好情况:删除表尾元素(即\(i=n\)),无需移动元素;最好时间复杂度为\(O(1)\)
最坏情况:删除表头元素(即\(i=1\)),需移动除表头元素外的所有元素;最好时间复杂度为\(O(n)\)
平均情况:假设\(p_i(p_i=1/n)\)是在第\(i\)个位置上删除一个结点的概率,则移动结点的平均次数为
删除算法的平均时间复杂度为\(O(n)\)
查找操作(按值查找)
int LocateELem(SqList L,Elemtype e)
{
int i=0;
for(i=0;i<L.Length;++i)
if(L.data[i]==0)
return i+1; · //下标为i的元素值等于e,返回其位序i+1
return 0; //退出循环,说明查找识别
}
最好情况:查找的元素就在表头,仅需比较一次;最好时间复杂度为\(O(1)\)
最坏情况:查找的元素在表尾(或不存在)时,需比较n次;最好时间复杂度为\(O(n)\)
平均情况:假设\(p_i(p_i=1/n)\)是查找的元素在第\(i\)个位置的概率,则移动结点的平均次数为
查找算法的平均时间复杂度为\(O(n)\)
线性表的链式表示
单链表的定义
//单链表中的结点类型
typedef struct Lnode{
Elemtype data; //数据域
struct Lnode* next; //指针域
}Lnode,*LinkList;
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点
为了操作上的方便,在单链表第一个结点之前附加一个头结点。头节点的数据域可以不设任何信息。引入头结点后:
- 在链表的第一个位置上的操作与其他位置的一致,无须进行特殊处理
- 无论链表为空,其头指针都指向头结点的非空指针(空表中头节点的指针域为空),实现空表与非空表的处理统一。
单链表的基本操作
建立操作
头插法
该方法从一个空表开始,生成一个新结点,并将读取到的数据存放到新结点的数据域中,然后讲新结点插入到当前链表的表头。
//带头结点
LinkList List_HeadInsert(LinkList &L)
{
LNode *s;int x;
L=(LinkList)malloc(sizeof(LNode)); //创建头结点
L->next=NULL; //初始为空链表
scanf("%d",&x); //输入结点的值
whille(x!=9999) //输入9999表示结点
{
s=(LNode*)malloc(sizeof(LNode));
s-data=x;
s-next=L->next; //将新结点插入表中,L为头结点
L-next=s;
scanf("%d",&x);
}
return L:
}
平均时间复杂度为\(O(n)\)
头插法应用:链表的逆置
尾插法
该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针人,使其始终指向当前链表的尾结点。
//带头结点
LinkList List_TailInsert(LinkList &L)
{
int x;+
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
whille(x!=9999) //输入9999表示结点
{
s=(LNode*)malloc(sizeof(LNode));
s-data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //尾指针置空
return L:
}
平均时间复杂度为\(O(n)\)
查找操作
按位(序号)查找
LNode *GetElem(LinkList L,int i){ int j=1; //计数,初始为1 LNode *p=L->next; //头结点赋值给p if(i==0) return L; //若i等于0,则返回头结点 if(i<1) return NULL; //若i无效,则返回NUll while(p&&j<i) //第一个结点开始,查找第i个结点 { p=p->next; j++; } return p; //返回第i个结点的指针,若i大于表长,则返回NULL}
平均时间复杂度为\(O(n)\)
按值查找
LNode *LocateElem(LinkList L,ELemtype e){ LNode *p=L->next; while(p!=NULL&&p->data!=e) //第一个结点开始查找data域为e的结点 p=p->next; return p; //找到后返回该结点指针,否则返回NULL}
平均时间复杂度为\(O(n)\)
插入操作
后插操作:
p=GetElem(L,i-1); //查找插入位置的前驱结点s->next=p->next; //第二步p->next=s; //第三步
时间复杂度为\(O(n)\)
扩展:对某一结点进行前插操作
s->next=p->next; //修改指针域,不能颠倒p->next=s;temp=p->data; //交换数据域部分p->data=s=>data;s->data=temp;
时间复杂度为\(O(1)\)
删除操作
p=GetElem(L,i-1); //查找删除位置的前驱结点q=p->next; //令q指向被删除结点p->next=q->next; //将*q结点从链中“断开”free(q); //释放结点的存储空间
时间复杂度为\(O(n)\)
扩展:删除结点*p
q=p->next; //令q指向*p的后续结点p->data=p->next->data; //和后续结点交换数据域(注意是否为最后一个结点)p->next=q->next; //将*q结点从链中“断开”free(q);
时间复杂度为\(O(1)\)
双链表的定义
引入前驱指针
//定义双链表结点类型typedef struct DNode{ Elemtype data; //数据域 struct DNode* prior,*next; //前驱和后继指针 }DNode,*DLinkList;
双链表的基本操作
插入操作
s->next=p->next; //将结点*s插入到*p之后if(p->next!=NULL) //判断是否为最后一个结点 p->next->prior=p;s->next=p;p->next=s;
删除操作
//删除*p后续结点*qp->next=q->next;q->next->priot=p;free(q);
循环链表的定义
循环单链表
表中最后一个结点的指针不是NULL,,而改为指向头结点。
判空条件:是否等于头结点
循环双链表
头结点的prior指针还要指向表尾结点。
当循环双链表为空表是,其头结点和prior域与next域都等于L。
具体操作自己学习实现。
静态链表的定义
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data与指针域next;
这里的指针是结点的相对地址(数组下标),又称游标。
静态链表要预先分配一块连续的内存空间。
typedef struct{ Elemtype data; //存储数据元素 int next; //下一个元素的数组下标}SLinkList[MaxSize]; //定义一个结构体数组
优点:增,删操作不需要大量移动元素
缺点:不能随机存储,只能从头结点开始依次往后查找;容量固定不可变
适用场景:①不支持指针的低级语言 ②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)