线性表
1.定义
线性表是具有相同数据类型(每个数据元素所占空间一样大)的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则一般表示为L=(a1,a2,…,ai,ai+1,…,an)。
Q:何时传入参数的引用”&
“? A:对参数的修改结果需要”带回来“。
2.顺序表
(1)定义
用顺序存储的方式实现线性表。知道一个数据元素的大小,用sizeof(ElemType)
。需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
(2)静态分配
#define MaxSize 10
typedef struct
{
ElemType data[MaxSize];
int length; //顺序表的当前长度
}SqList;
//初始化一个顺序表
void InitList(SqList &L)
{
for(int i=0;i<MaxSize;i++)
L.data[i]=0;
L.length=0;
}
int main()
{
SqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//……
return 0;
}
(3)动态分配
#define InitSize 10 //顺序表的初始长度
typedef struct
{
ElemType *data; //指示动态分配数组的指针
int MaxSize;
int length;
}SeqList;
动态申请和释放内存空间:
malloc、free函数:malloc函数的参数指明要分配多大的连续内存空间。
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize);
#include<stdlib.h> //malloc、free函数的头文件
#include<stdio.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 += len; //顺序表最大长度增加len
free(p);
}
int main()
{
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//往顺序表中随便插入几个元素
IncreaseSize(L, 5);
return 0;
}
(4)基本操作
主要包含创建、销毁、增加、删除、更改、查找等操作。对于销毁操作,静态分配的静态数组是由系统自动回收空间,对于动态分配(链表),需要依次删除各个结点(free
操作)。
插入
#include<stdlib.h> //malloc、free函数的头文件
#include<stdio.h>
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int length;
}SqList;
void ListInsert(SqList &L, int i, int e)
{
for (int j = L.length;j >= i;j--)
L.data[j] = L.data[j - 1]; //将第i个元素及以后的元素右移
L.data[i - 1] = e;
L.length++;
}
int main()
{
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
//插入元素
IncreaseSize(L, 5);
return 0;
}
在ListInsert
函数中,还应判断i
的范围是否有效(将函数类型定义为Bool
类型),即:
if(i<1||i>L.length+1) return false;if(L.length>=MaxSize) return false;
问题规模n=L.length
,最好情况是新元素插入到表尾,不需要移动元素,最好时间复杂度O(1),最坏为O(n)。平均情况:
假设新元素插入到任何一个位置的概率相同,即i=1,2,3,…,length+1
的概率都是p=1/(n+1)
;
当i=1
,循环n
次;i=2
,循环n-1
次;i=3
,循环n-2
次;…;i=n+1
,循环0次。
则平均循环次数=np+(n-1)p+…+1*p=(n(n+1)/2)*(1/n+1)=n/2
,故平均时间复杂度为O(n)。
删除
bool ListDelete(SqList &L, int i, int &e)
{
if (i<1 || i>L.length) return false;
e = L.data[i - 1];
for (int j = i;j < L.length;j++) //将第i个位置后的元素前移
L.data[j - 1] = L.data[j];
L.length--; //线性表长度减1
return true;
}
int main()
{
SeqList L; //声明一个顺序表
InitList(L); //初始化顺序表
int e = -1;
if (ListDelete(L, 3, e))
printf("已删除第三个元素,删除的元素值=%d\n", e);
else printf("位序i不合法,删除失败\n");
return 0;
}
时间复杂度为O(n)。
查找
按位查找
GetElem(L,i)
: 获取表L中第i个位置的元素的值,其时间复杂度为O(1)
按值查找
LocateElem(L,e)
:在表L中查找具有给定关键值的元素,其时间复杂度为O(n)
若表内元素有序,可在O(log2n)
的时间内找到。
(5)顺序表的特点
随机访问,即可以在O(1)时间内找到第i
个元素;
存储密度高,每个节点只存储数据元素;
拓展容量和插入、删除操作不方便。
3.单链表
(1)定义
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点:LNode *L
或LinkList L
。(声明一个指针指向单链表第一个结点)
一般来说,若强调这是一个单链表,使用LinkList
;若强调这是一个结点,使用LNode
。
(2)初始化
不带头结点的单链表
bool InitList(LinkList &L)
{
L = NULL; //空表,防止脏数据
return true;
}
带头结点的单链表
bool InitList(LinkList &L)
{
L = (LNode *)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL)
return false; //内存不足,分配失败
L->next = NULL;
return true;
}
(3)基本操作
插入
按位序插入和后插操作
头结点可以看作"第0个结点".
//不带头结点
bool ListInsert(LinkList &L, int i, ElemType e)
{
if (i < 1)
return false;
LNode *p;
int j = 0; //当前p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while (p != NULL && j < i - 1) //循环找到第i-1个结点
{
p = p->next;
j++;
}
//后插操作
if (p == NULL) //i值不合法
return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p之后
return true;
}
如果不带头结点,则插入、删除第1个元素时,需更改头指针L。
前插操作
“偷天换日”:新节点s连到p之后,将p中元素复制到s中,再将p中元素进行覆盖,时间复杂度为O(1).
bool InsertPriorNode(LNode *p, ElemType e)
{
if (p == NULL) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return false;
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中的元素复制到s中
p->data = e; //p中元素覆盖为e
return true;
}
删除
按位序删除
bool ListDelete(LinkList &L, int i, ElemType &e)
{
if (i < 1) return false;
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < n - 1) //循环找到第n-1个结点
{
p = p->next;
j++;
}
if (p == NULL)
return false;
if (p->next == NULL) //第i-1个结点之后已无其他结点
return false;
LNode *q = p->next; //令q指向被删除的结点
e = q->data; //用e返回元素的值
p->next = q->next; //将*q结点从链中断开
free(q);
return true;
}
指定结点的删除
bool DeleteNode(LNode *p)
{
if (p == NULL) return false;
LNode *q = p->next; //令q指向*p的后继结点
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}
【注】如果p是最后一个结点,则只能从表头开始找到p的前驱,时间复杂度是O(n)。
单链表的局限性:无法逆向检索,有时候不太方便。
单链表的建立
尾插法
(1)初始化单链表;
(2)设置变量length记录链表长度;
(3)
while 循环
{
每次取一个数据元素e;
ListInsert(L,length+1,e)插到尾部;
length++;
}
头插法
初始空链表后,需执行L->next=NULL
。
4.双链表
(1)初始化
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 = NULL; //头结点的prior永远指向NULL
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
(2)插入操作
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s)
{
if(p==NULL||s==NULL)
return false;
if(p->next!=NULL) //如果p结点有后继结点
p->next->prior=s;
s->prior=p;
p->next=s;
return true;
}
(3)删除操作
bool DeleteNextDNode(DNode *p)
{
if(p==NULL) return false;
DNode *q=p->next; //找到p的后继结点q
if(q==NULL) return false; //p没有后继
p->next=q->next;
if(q->next!=NULL) q->next->prior=p;
free(q);
return true;
}
(4)遍历
前向遍历/后向遍历,时间复杂度为O(n)。
5.循环链表
单链表表尾结点的next
指针指向NULL
,而循环单链表表尾结点的next
指针指向头结点。
循环单/双链表从一个结点出发,可以找到其他任何一个结点。
6.静态链表
data
表示数据元素,next
表示游标
适用场景:不支持指针的语言;数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
(1)定义静态链表
#define MaxSize 10
struct Node
{
ElemType data;
int next; //下一个元素的数组下标
}
main
函数中struct Node a[MaxSize];
,用数组a作为静态链表。
定义的另一种方式:
#define MaxSize 10 //静态链表的最大长度
typedef struct
{
ElemType data;
int next;
}SLinkList[MaxSize];
这种定义方式等价于:
#define MaxSize 10 //静态链表的最大长度
struct Node
{
ElemType data;
int next;
};
typedef struct Node SLinkList[MaxSize];
即,可用SLinkList
定义”一个长度为MaxSize
的Node
型数组。
在主函数中,声明一个静态链表可用SLinkList a;
语句。
(2)基本操作
初始化静态链表:把a[0]
的next
设为-1。
查找:从头结点出发挨个往后遍历结点。
插入位序为i的结点:
【1】找到一个空的结点,存入数据元素;
注:判断结点是否为空,可在初始化过程中把空闲元素赋某个值;之后查找,若某结点值为-2,说明该结点时空闲的。
【2】从头结点出发找到位序为i-1
的结点;
【3】修改新结点的next
;
【4】修改i-1
号结点的next
。