1. 线性表的顺序存储结构
用一段连续的存储单元一次存储线性表的数据元素。
结构代码:
const int MAXSIZE = 20; // 存储空间初始分配量
typedef int ElemType; // ElemType类型根据实际情况而定,这里假设为int
typedef struct
{
ElemType data[MAXSIZE]; // 数组存储数据元素
int length; // 线性表当前长度
}SqList;
这里,我们发现描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data。
- 线性表的最大存储容量:数组长度MAXSIZE。
- 线性表的长度:length。
1.1 数组长度与线性表长度的区别:
数组长度是存放线性表存储空间的长度,线性表长度是线性表中数据元素的个数。任意时刻线性表的长度应小于等于数组长度。
其存取性能为O(1),通常把具有这一特点的存储结构成为随机存储结构。
1.2 顺序存储结构的插入与删除
1.2.1 插入操作:
// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)
// 操作结果:在L中第i个位置之前插入新的数据元素e,L长度加1
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if (L->length == MAXSIZE) // 顺序线性表已满
return ERROR;
if (i<1 || i>L->length+1) // 当i不在范围内时,注意是length+1
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.2.2 删除操作:
// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)
// 操作结果:删除在L中第i个位置之前的数据元素e,L长度减1
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if (L->length == 0) // 顺序线性表为空
return ERROR;
if (i<1 || i>L->length) // 当i不在范围内
return ERROR;
if (i<L->length) // 当删除位置不在表尾
{
for (k=i-1; k<L->length; k++) // 将要删除位置后数据元素前移一位
{
L->data[k] = L->data[k+1];
}
}
L->length--;
return Ok;
}
复杂度
最好情况O(1),最差情况O(n),平均复杂度为O((n-1)/2) = O(n)。
这说明,它比较适合于元素个数不太变化,而更多是存取数据的应用。
1.3 线性表顺序存储结构的优缺点:
优点:
- 无需为表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速存取表中任意位置的元素
- 插入删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间容量
- 造成存储空间“碎片”
2. 线性表的链式存储结构
链表中第一个结点的存储位置叫做
头指针。线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
有时,我们为了更加方便的对链表进行操作,会在单链表的第一个结点前附设一个结点,称为
头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。
头结点与头指针的异同:
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般没有意义(也可以放链表长度)
- 有了头结点,对在第一个结点前插入和删除第一个结点,其操作与其他结点的操作就统一了
- 头结点不一定是链表的必要元素
// 线性表的单链表存储结构
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; // 定义LinkList
2.1 单链表的读取
// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)
// 操作结果:用e返回L中第i个数据元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next; // 让p指向链表L的第一个结点
j = 1;
while(p && j<i)
{
p = p->next;
++j;
}
if (!p || j>i)
{
return ERROR; // 第i个元素不存在
}
*e = p->data; // d取第i个元素的数据
return Ok;
}
说白了,就是从头开始找,直到第i个元素为止。时间复杂度取决于i的位置,i=1时不需要遍历,而i=n时,则遍历n-1次才可以。
2.2 单链表的插入与删除
2.2.1 单链表的插入
// 初始条件:顺序线性表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)
{
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的后继
p->next = s; // 将s赋值给p的后继
return Ok;
}
对于单链表的表头和表尾的特殊情况,操作是相同的。
2.2.2 单链表的删除
// 初始条件:顺序线性表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;
*e = q->data; // 将q结点中的数据给e
free(q); // 回收此结点
return Ok;
}
对插入删除数据越频繁的操作,单链表的效率优势就越明显。
2.3 单链表的整表创建
创建单链表的过程就是一个动态生成链表的过程,即从“空表”的初始状态起,依次建立各个元素结点,并逐个插入链表。
2.3.1 头插法
// 随机产生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;// 插入到表头
}
}
2.3.2 尾插法
// 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)
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 = (LinkList)malloc(sizeof(Node)); // 生成新结点
p->data = rand()%100+1;// 随机生成100内数字
r->next = p;// 将表尾结点的指针指向新结点
r = p;// 将当前的新结点定义为表尾结点
}
r->next = NULL;
}
2.4 单链表的整表删除
// 初始条件:顺序线性表L已经存在
// 操作结果:将L重置为空表
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是一个结点,除了数据域还有指针域,free(p)是对整个结点进行删除和内存释放工作。所以如果程序中直接像下面这样写会出问题的(p的地址域已经被释放)
free(p);
p = p->next;
2.5 单链表结构与顺序存储结构的比较
- 频繁查找,很少进行插入删除操作,宜采用顺序存储结构;反之用单链表。比如游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应考虑用顺序存储结构。而游戏中玩家的武器装备列表,随着玩家的游戏过程中,可能随时增加或删除,单链表就可以大展拳脚了。
- 当线性表中元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样就不需要考虑存储空间大小问题。若事先知道大致长度,如一年12个月,这种用顺序结构效率会高很多。
2.6 循环链表
对于非空的循环链表如下:
其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在是它不等于头结点,则循环结束。
2.7 双向链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
// 线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; // 直接前驱指针
struct DulNode *next; // 直接后继指针
}DulNode, *DuLinkList;
对于双向链表中的一个结点p,它的后继的前驱,以及前驱的后继都是自己。
2.7.1 插入操作
插入操作时,并不复杂,但是顺序很重要,不能写反了。
s->prior = p; // 把p赋值给s的前驱,如图中的1
s->next = p->next; // 把p->next赋值给s的后继,如图中2
p->next->prior = s; // 把s赋值给p->next的前驱,如图中3
p->next = s; // 把s赋值给p的后继,如图中的4
2.7.2 删除操作
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);