顺序存储结构
代码实现:
开头部分的定义
#include <stdio.h>
#define MAXSIZE 20
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;//status是函数的类型
typedef int ElemType;//ElemType应根据实际类型而定
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqList;
获得元素操作
//以下函数中的parameter i都是元素的位置,不是下标,下标数值是i - 1
//获得元素操作
Status GetElem(SqList L, int i, ElemType* e)
{
if (L.length == 0 || i < 1 || i > L.length)
return ERROR;
*e = L.data[i - 1];
return OK;
}
int main()
{
int k;
ElemType* e = &k;
SqList L = {1, 2, 4, 5, 6, 0};
L.length = 6;
GetElem(L, 2, e);
printf("%d", *e);
getchar();
return 0;
}//输出结果:2
插入操作
注意: 为什么插入操作的函数定义形参与获得元素操作不同呢?
因为如果需要获得元素,使用指向ElemType类型的指针传参即可
但进行插入操作时,如果使用了SqList L,当L在主函数被调用时,实际上并未执行插入的操作,即插入的操作只短暂的存在于这个函数的作用域内,因此应该使用指针SqList *L来传参
//插入操作
Status ListInsert(SqList* L, int i, ElemType e)
{
int d;
if (L->length == MAXSIZE)
return ERROR;
if (i < 1 || i > L->length + 1)
return ERROR;
if (i <= L->length)
{
for (d = L->length - 1; d >= i - 1; d--)
{
L->data[d + 1] = L->data[d];
}
}
L->data[i - 1] = e;
L->length++;
return OK;
}
int main()
{
int k = 3;
ElemType* e = &k;
SqList L = {1, 2, 4, 5, 6, 0};
L.length = 6;
SqList* l = &L;
ListInsert(l, 3, k);
printf("%d", l->data[2]);
getchar();
return 0;
}//输出结果:3
删除操作
//删除操作
Status ListDelete(SqList* L, int i, ElemType* e)
{
int k;
if (L->length == 0)
return ERROR;
if (i < 1 || i > L->length)
return ERROR;
*e = L->data[i - 1];
if (i < L->length)
{
for ( k = i ; k < L->length; k++)
{
L->data[k - 1] = L->data[k];
}
}
L->length--;
return OK;
}
int main()
{
int k;
elemtype* e = &k;
sqlist L = {1, 2, 4, 5, 6, 0};
L.length = 6;
sqlist* l = &L;
listdelete(l, 3, e);
printf("%d", *e);
getchar();
return 0;
}//输出被删除的元素4
插入和删除的时间复杂度分析
最好的情况:插入最后一个位置,或者删除最后一个元素
形象的解释:如同来了一个人来排队,自然排在最后,如果不想排了,自己一个人离开,不影响任何人
最坏的情况:如果要插入到第一个元素或者删除第一个元素
相当于所有人都要向后或者向前移动一个身位,时间复杂度是O(n)
平均的情况:元素插入到第i个位置,或删除第i个元素
此情况需要移动n - 1个元素,根据概率原理,最终移动次数和最中间的元素移动次数相等,为n - (1 + n) / 2, 即(n - 1) / 2
总结
可以得出,平均时间复杂度O(n)
优点
1.无须为表示表中逻辑关系而增加额外的存储空间
2.可以快速的任意存取表中任意位置的元素
缺点
1.插入和删除操作需要移动大量元素(平均)
2.当线性表长度变化较大时,难以确定存储空间的容量
3.造成存储空间的碎片
链式存储结构
结构定义
存储数据元素信息的域称为数据域,存储后继位置的域称为指针域
指针域存储的信息称作指针或链,两部分信息组成的存储映像,称为节点
n个结点链结成一个链表,即为线性表的链式存储结构
链表中第一个结点存储位置称为头指针,线性链表 最后一个节点为NULL,
为方便操作,会在单链表的第一个结点前附设一个结点,称为头结点
头结点数据域可不存储任何信息,也可以存储线性表的长度等附加信息
头指针与头结点异同
头指针
- 头指针是指向第一个结点的指针(头结点不是第一个结点)
- 头指针具有标识作用,常用来冠以链表名字
- 无论链表是否为空,头指针不可为空,头指针是链表必要元素
头结点
- 为了操作方便和统一而设立,放在第一元素的结点之前,其数据域一般无意义(或存放链表长度)
- 设置头结点后对在第一元素结点前插入结点和删除第一结点与其他结点的操作就统一了
代码实现
单链表描述
#include <stdio.h>
#define ERROR 0
#define OK 1
typedef int ElemType;
typedef int Status;
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef Node *LinkList;//或者写为typedef struct Node *LinkList
单链表的读取
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;//如果不设置p,采用直接用L->next,当函数进入while loop时,会产生逻辑错误,称p为工作指针
p = L->next;
j = 1;
while (L->next && j < i)
{
p = p->next;
++j;
}
if (!p || j > i)
return ERROR;
*e = p->data;
return OK;
}
int main()
{
Node n[10];
int k;
ElemType *e = &k;
LinkList l = n;
for (int i = 0; i < 10; ++i)
{
n[i].data = i;
if (i < 9)
n[i].next = n + i + 1;
else
n[i].next == NULL;
}
int i = 3;
GetElem(l, i, e);
printf("%d", k);
return 0;
}//输出3,即从第一个结点开始(非头结点)
//从头开始查找,直到第i个元素为止,算法时间复杂度取决于i的位置,当i = 1时不需要遍历,若i = n,需要遍历n - 1次才可以,时间复杂度O(n)
//核心思想:工作指针后移
单链表的插入
思路:将s插入p和p->next之间,使s->next和p->next的指针做出一点改变
s->next = p->next;
p->next = s;//顺序不能改变
//如果交换相当于s->next = s,导致含有a(i + 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;
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
int main()
{
Node n[10];
LinkList l = n;
for (int i = 0; i < 10; ++i)
{
n[i].data = i;
if (i < 9)
n[i].next = n + i + 1;
else
n[i].next == NULL;
}
int e = 66;
int i = 3;
ListInsert(&l, i, e);
printf("%d", n[2].next->data);
return 0;
}//输出66
单链表的删除
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;
q = p->next;
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
单链表的整表创建
头插法
//随机产生n个元素的值,建立带表头结点的单链线性表(头插法)
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;
p->next = (*L)->next;
(*L)->next = p;
}
}
int main()
{
LinkList l;
CreateListHead(&l, 10);
return 0;
}
尾插法
//尾插法
void CreateListTail(LinkList *L, int n)
{
LinkList p, r;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for (int i = 0; i < n; i++)
{
p = (LinkList)malloc(sizeof(Node));
p->data = rand() % 100 + 1;
r->next = p;
r = p;
}
r->next = NULL;
}
int main()
{
LinkList l;
CreateListTail(&l, 10);
return 0;
}
单链表的整表删除
//单链表的整表删除
Status ClearList(LinkList *L)
{
LinkList p, q;
p = (*L)->next;
while (p)
{
q = p->next;//记录p的指针域,使下一个结点是谁得到了记录
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
int main()
{
LinkList l;
CreateListTail(&l, 10);
ClearList(&l);
return 0;
}
单链表结构与顺序存储结构优缺点
存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能
- 查找
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在找出某位置指针后,插入和删除时间为O(1)
空间性能
- 顺序存储结构需要预分配存储空间,分大浪费,分小溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数不受限
结论
- 频繁查找,进行插入与删除较少,采用顺序存储,例如游戏开发,对于用户注册的个人信息,绝大多数情况需要读取,而装备列表需要随时增加或删除,可以使用单链表结构
- 若线性表中的元素个数变化较大或者根本不知道有多大,最好用单链表。如果事先知道,例如一年12个月,采用顺序结构