线性表
2.1基础定义:
线性表是由同类型数据元素构成有序序列的线性结构
- 表中元素个数称为线性表的长度。
- 线性表没有元素时,称为空表。
- 表起始位置称表头,表结束位置称表尾。
2.2抽象数据类型:
ADT 线性表(List)
Data
线性表的数据对象集合为(a1,a2,......,an),每个元素的类型均为ElementType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
List MakeEmpty(); 初始化操作,建立一个空的线性表L
ElementType FindKth(int K,List L); 根据位序K,返回相应元素。
int Find(ElementType X,List L); 在线性表L中查找X第一次出现的位置
void Insert(ElementType X,int i,List L); 在位序i前插入一个新元素X
void Delete(int i,List L); 删除指定位序i的元素
int Length(List L); 返回线性表L的长度n
2.3顺序存储:
2.3.1定义:
利用数组的连续存储空间顺序存放线性表的各元素。用一段地址连续地存储单元依次存储线性表的数据元素。
求存储地址:
Loc(ai)=Loc(ai-1)+c;
Loc(ai)=Loc(a1)+(i-1)*c;
其中c为每个数据元素所占的存储单元
2.3.2结构体代码:
#define MAXSIZE 20 //存储空间初始分配量
typedef int ElementType; //ElementType类型可以根据实际情况作改变
typedef struct LNode* List;
struct LNode {
ElementType Data[MAXSIZE]; //数组存储的数据元素数量,最大值为MAXSIZE
int Last; //线性表最后一个元素的下标
};
struct LNode L;
List PtrL;
- 访问下标元素为i的元素:L.Data[i]或Ptrl->Data[i]
- 线性表的长度:L.Last+1或PtrL->Last+1
2.3.3主要操作实现:
1.建表初始化:
建立空的顺序表
List MakeEmpty()
{
List PtrL;
PtrL = (List)malloc(sizeof(struct LNode)); //开辟空间
PtrL->Last = -1; //建立空的顺序表
return PtrL;
}
2.查找:
查找一个指定的元素
int Find(ElementType X, List PtrL)
{
int i = 0;
while (i <= PtrL->Last && PtrL->Data[i] != X)
{
i++;
}
if (i > PtrL->Last) return -1; //若没有找到,返回-1
else return i; //找到后返回存储的位置
}
3.插入:
在第i个元素位置上插入一个值为X的新元素
- 如果插入不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入的元素填入位置i处;
- 表长加1;
void Insert(ElementType X, int i, List PtrL)
{
int j;
if (PtrL->Last == MAXSIZE - 1)
{
printf("线性表已装满"); //验证表空间
return;
}
if (i<1 || i>PtrL->Last + 2)
{
printf("位置不合法"); //检查插入位置合法性
return;
}
for (j = PtrL->Last; j >= i - 1; j--)
{
PtrL->Data[j + 1] = PtrL->Data[j]; //将ai-an倒序向后移动
PtrL->Data[i - 1] = X; //插入新元素
PtrL->Last++; //Last仍指向最后元素
return;
}
}
4.删除:
删除第i个位置的元素。
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减1;
void Delete(int i, List PtrL)
{
int j;
if (i<1 || i>PtrL->Last + 1)
{
printf("不存在第%d个元素",i);
return;
}
for (j = i; j <= PtrL->Last; j++)
{
PtrL->Data[j - 1] = PtrL->Data[j]; //将ai-an顺序向前移动
PtrL->Last--; //Last仍指向最后元素
}
}
2.3.4优缺点
优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间。
- 可以快速地存取表中任一位置的元素。
缺点:
- 插入和删除操作需要移动大量元素。
- 当线性表长度变化较大时难以确定存储空间的容量。
- 造成存储空间的“碎片”。
2.4链式存储
2.4.1定义:
- 每个元素的存储地址任意。
- 使用指针相连接。
- 每个结点包括两个域:数据域、指针域。
2.4.2单链表:
链表中第一个结点的存储位置叫做头指针,最后一个结点指针为“空”(通常用NULL表示)。
我们会在单链表的第一个结点前设置一个“头结点”,可以不存储任何信息。也可以存储如线性表的长度等附加信息,头结点的指针域指向第一个节点的指针。
以下是头指针和头结点的异同:
结构体代码:
typedef int ElementType; //ElementType类型可以根据实际情况作改变
typedef struct LNode* List; //List为struct LNode*别名
struct LNode {
ElementType Data; //结点对应的数据
List Next; //结点的下一个数据
};
struct LNode L;
List PtrL;
主要操作实现:
1.创建:
头插法:
- 声明一个结点p和计数器变量i
- 初始化一个空链表PtrL
- 让PtrL的头结点指针指向NULL,即建立一个带头结点的单链表
- 循环:生成一个新结点赋值给p。随机生成一个数字赋值给p的数字域p->Data.将p插入到头结点与新的结点之间。
//头插法创建单链表
List CreateListHead(int n)
{
List p,PtrL;
int i;
srand(time(0)); //初始化随机数种子
PtrL = (List)malloc(sizeof(LNode)); //整个线性表
PtrL->Next = NULL;
for (i = 0; i < n; i++)
{
p= (List)malloc(sizeof(LNode)); //生成新结点
p->Data = rand() % 100 + 1; //随机生成100以内的数字
p->Next = PtrL->Next;
PtrL->Next = p;
}
return PtrL;
}
用的是类似插队的方法,始终让新结点在第一的位置上。
尾插法:
//尾插法创建单链表
List CreateListTail(int n)
{
List p, r,PtrL;
int i;
srand(time(0)); //初始化随机数种子
PtrL = (List)malloc(sizeof(LNode)); //整个线性表
r = PtrL; //r为指向尾部的结点
for (i = 0; i < n; i++)
{
p = (List)malloc(sizeof(LNode)); //生成新结点
p->Data = rand() % 100 + 1; //随机生成100以内的数字
r->Next = p; //将表尾终端结点的指针指向新节点
r=p; //将当前的新节点定义为表尾终端结点
}
r->Next = NULL; //表示当前链表结束
return PtrL;
}
2.打印:
//打印链表
void PrintOut(List PtrL)
{
List p;
p = PtrL->Next;
while (p)
{
printf("%6d", p->Data);
p = p->Next;
}
printf("\n");
}
3.获取长度:
//获取链表长度
int GetLength(List PtrL)
{
List p = PtrL; //p指向表的第一个结点
int j = 0;
while (p)
{
p = p->Next;
j++;
}
return j;
}
4.查找:
按序号查找:
- 声明一个结点p指向链表第一个结点,初始化i作为计数器。
- i<K时,进行对链表的遍历,让p的指针向后移动,不断指向下一个结点。
- 若查找成功,则返回结点p,否则返回空。
//按序号查找
List FindKth(int K, List PtrL)
{
List p = PtrL; //p指向头结点
int i = 0; //i记录其序号
while (p != NULL && i < K) //结束查找条件
{
p=p->Next;
i++;
}
if (i == K) return p; //找到第K个,返回指针(地址),要用p->Data访问数据
else return NULL; //否则返回空
}
按值查找:
//按值查找
List Find(ElementType X, List PtrL)
{
List p = PtrL;
int cnt=0;
while (p != NULL && p->Data != X) //从头开始遍历链表
{
p = p->Next;
cnt++; //可用作返回值所在的位序
}
return p; //返回对应指针
}
5.插入:
- 先判断i=1的情况,即插入的是表的第一个结点,单独讨论。
- 利用之前的按序号查找KinKth函数,查找到所要插入的结点。
- 利用s->Next=p->Next;p->Next=s;进行插入,先后顺序不能反。
//链表插入
List Insert(ElementType X, int i, List PtrL)
{
List p, s;
if (i == 1) //若要插入的是表的第一个结点,单独讨论
{
s = (List)malloc(sizeof(struct LNode));
s->Data = X;
s->Next = PtrL;
return s;
}
p = FindKth(i - 1, PtrL); //利用按序号寻找函数,找到所要插入的结点
if (p == NULL)
{
printf("参数i错误");
return NULL;
}
else
{
s = (List)malloc(sizeof(struct LNode));
s->Data = X;
s->Next = p->Next;
p->Next = s;
return PtrL;
}
}
6.删除:
删除某个值:
- 先判断i=1的情况,即删除的是表的第一个结点,单独讨论。
- 利用之前的按序号查找KinKth函数,查找到所要删除的结点。
- 利用p->Next = s->Next;进行删除,并且释放s结点。
//链表删除
List Delete(int i, List PtrL)
{
List p, s;
if (i == 1) //若要删除的是表的第一个结点,单独讨论
{
s = PtrL; //s指向第一个结点
if (PtrL != NULL) PtrL = PtrL->Next; //从链表中删除
else return NULL;
free(s); //释放被删除的结点
return PtrL;
}
p = FindKth(i - 1, PtrL); //查找第i-1个结点
if (p == NULL)
{
printf("第%d个结点不存在", i - 1); return NULL;
}
else if (p->Next == NULL)
{
printf("第%d个结点不存在", i); return NULL;
}
else
{
s = p->Next; //s指向第i个结点
p->Next = s->Next; //从链表中删除
free(s); //释放被删除的结点
return PtrL;
}
}
整表删除:
- 声明结点p、q
- 将第一个结点赋值给p
- 循环:将下一结点赋值给q。释放p。将q赋值给p。
//整表删除
bool ClearList(List PtrL)
{
List p, q;
p = PtrL->Next; //p指向第一个结点
while (p) //当p还没到表尾时
{
q = p->Next;
free(p);
p = q;
}
PtrL->Next = NULL; //头结点指针域为空
return true;
}
7.逆置:
//链表逆置
List Inverse(List L)
{
List p, q;
p = L->Next; //p指针指向第一个结点
L->Next = NULL; //头结点指向NULL
while (p != NULL) {
q = p;
p = p->Next;
q->Next = L->Next;
L->Next = q;
}
return L;
}
8.合并:
将非递减有序单链表A和B合并,合并后的单链表C仍然非递减有序。
//链表合并
List Comb(List A, List B)
{
List pa, pb, pc, C;
pa = A->Next;
pb = B->Next;
C = A; pc = C; //C使用原单链表A的头结点
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;
}
}
if (pa) pc->Next = pa; //表A中剩余结点链接在表C之后
else pc->Next = pb; //表B中剩余结点链接在表C之后
free(B); //释放表B头结点
return (C); //返回新链表C头结点
}
单链表与顺序存储对比:
若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。
当线性表元素个数变化较大或者根本不知道多大时,用单链表结构,这样就可以不用考虑存储空间的大小问题。如果事先指导线性表的大致长度,用顺序存储结构效率会提高很多。
2.4.3静态链表:
静态链表的思想是用数组来代替指针,来描述单链表。建立一片空间来存放数组,数组元素由两个数据域构成,分别是data和cur。data用来存放数据元素,游标cur相当于指针的作用,用来存放的是元素的后继在数组中的位置。
结构体代码:
#define MAXSIZE 1000 //假设静态链表的最大长度为1000
typedef int ElementType; //ElementType类型可以根据实际情况作改变
typedef struct
{
ElementType data; //存放的数据
int cur; //游标,为0时表示无指向
}Component,StaticLinkList[MAXSIZE];
数组的第一个元素和最后一个元素不存放数据,未被使用的数组元素称为备用链表。第一个元素cur存放备用链表第一个结点的下标,最后一个元素的cur存放第一个有下标的元素的下标,相当于单链表中的头结点。
主要操作实现:
1.初始化:
//初始化静态链表
void InitList(StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
space[MAXSIZE - 1].cur = 0; //目前静态链表为空,最后一个元素的cur为0
}
2.添加数据:
//给静态链表加入元素
void add(StaticLinkList L)
{
printf("请输入你想添加的数据成员的数量:");
int n;
scanf_s("%d", &n);
for (int i = 1; i <= n; i++)
{
printf("请输入第%d个数据成员:", i);
scanf_s("%d", &L[i].data);
}
L[0].cur = n + 1;
L[n].cur = 0;
L[MAXSIZE - 1].cur = 1;
}
//向链表中插入数据,add表示插入元素的位置,num表示要插入的数据
最后一个有值元素的cur设置为0.
3.获取长度:
//获取长度
int ListLength(StaticLinkList L)
{
int j = 0; //计数器
int i = L[MAXSIZE - 1].cur;
while (i) //当i!=0
{
i = L[i].cur;
j++;
}
return j;
}
4.插入:
在动态链表中,结点的申请和释放可以用malloc()和free()函数进行实现,而静态链表中,由于操作的是数组,我们需要自己手动实现这两个函数。
//结点申请
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur; //当前数组第一个元素的cur存的值 目的是返回第一个备用空间的下标
if (space[0].cur)
space[0].cur = space[i].cur; //由于要拿出一个分量使用,所以要把下一个分量拿出来备用
return i; //返回下标值,这个值是数组头元素的cur存的第一个空闲的下标
}
//插入
int ListInsert(StaticLinkList L, int i, ElementType e)
{
int j, k, l;
k = MAXSIZE - 1; //k是最后一个元素的下标
if (i<1 || i>ListLength(L) + 1)
return 0;
j = Malloc_SLL(L); //获取空闲分量的下标
if (j)
{
L[j].data = e; //将数据赋值给次分量的data
for (l = 1; l <= i - 1; l++) //找到第i个元素之前的位置
k = L[k].cur;
L[j].cur = L[k].cur; //把第i个元素之前的cur赋值给新元素的cur
L[k].cur = j; //把新元素的下标赋值给第i个元素之前元素的cur
PrintOut(L);
return 1;
}
return 0;
}
5.删除:
同样的,我们需要先写出属于我们的free()函数
//结点释放
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; //把第一个元素的cur值赋给将要删除的分量cur
space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur
}
//删除
int ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i<1 || i>ListLength(L))
return 0;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return 1;
}
6.打印:
//打印
void PrintOut(StaticLinkList L)
{
int i = L[MAXSIZE - 1].cur;
while (i) //当i!=0
{
printf("%d,%d ", L[i].data,L[i].cur);
i = L[i].cur;
}
printf("\n");
}
优缺点:
静态链表是为了给没有指针的语言设计的一种能够实现单链表的方法。
2.4.4双向链表:
双链表在单链表的每个节点中,又设置了一个指向其前驱的结点指针域,每个结点都有两个指针域,一个指向直接后继,一个指向直接前继。
结构体代码:
typedef struct DNode{ //定义双链表结点类型
ElemType data; //数据域
struct DNode *prior, *next; //前驱和后继指针
}DNode, *DLinklist;
主要操作实现:
1.初始化:
p->next->prior=p=p->prior->next
//初始化双链表
bool InitDLinkList(DLinklist L)
{
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->prior = NULL; //头结点的prior指针永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
2.插入:
- s->prior=p;
- s->next=p->next;
- p->next->prior=s;
- p->next=s;
bool InsertNextDNode(DNode* p, DNode* s) { //将结点 *s 插入到结点 *p之后
if (p == NULL || s == NULL) //非法参数
return false;
s->next = p->next;
if (p->next != NULL) //p不是最后一个结点=p有后继结点
p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
3.删除:
- p->prior->next=p->next;
- p->next->prior=p->prior;
- free§;
//销毁一个双链表
bool DestoryList(DLinklist& L) {
//循环释放各个数据结点
while (L->next != NULL) {
DeletNextDNode(L); //删除头结点的后继结点
free(L); //释放头结点
L = NULL; //头指针指向NULL
}
}
2.4.5循环链表:
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表就称为单循环链表。
结构体代码:
typedef int ElemType; //ElementType类型可以根据实际情况作改变
typedef struct LNode {
ElemType data;
struct LNode* next;
}DNode, * Linklist;
主要操作实现:
1.单循环:
// 初始化一个循环单链表
bool InitList(LinkList L) {
L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->next = L; //头结点next指针指向头结点
return true;
}
//判断循环单链表是否为空(终止条件为p或p->next是否等于头指针)
bool Empty(LinkList L) {
if (L->next == L)
return true; //为空
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode* p) {
if (p->next == L)
return true;
else
return false;
}
2.双向循环:
typedef struct DNode {
ElemType data;
struct DNode* prior, * next;
}DNode, * DLinklist;
//初始化空的循环双链表
bool InitDLinkList(DLinklist& L) {
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->prior = L; //头结点的prior指向头结点
L->next = L; //头结点的next指向头结点
}
void testDLinkList() {
//初始化循环单链表
DLinklist L;
InitDLinkList(L);
//...
}
//判断循环双链表是否为空
bool Empty(DLinklist L) {
if (L->next == L)
return true;
else
return false;
}
//判断结点p是否为循环双链表的表尾结点
bool isTail(DLinklist L, DNode* p) {
if (p->next == L)
return true;
else
return false;
}
本文为自己在学习数据结构过程中所记录整理的一些笔记,后续将持续更新后面的内容。
主要参考:《大话数据结构》——程杰