定义
- 线性表是n个具有相同数据类型的数据元素的有限序列
- n=0时,为空表
- 第一个数据元素只有一个唯一的直接后继
- 最后一个数据元素只有一个唯一的直接前驱
- 其他元素有且仅有一个直接前驱和一个直接后继
特点
- 元素有限;
- 全部整数组成的序列不是线性表,因为元素不是有限的
- 元素具有逻辑上顺序性 ,各元素有先后顺序;
- 表中元素都是数据元素,每个元素都是单个元素;
- 各元素数据类型相同,每个元素所占存储空间相同。
基本操作
InitList(&L):初始化线性表,构造一个空表
DestroyList(&L):销毁线性表,释放线性表所占内存空间
ClearList(&L):清空线性表,置为空表
ListEmpty(L):判断线性表是否为空表
ListLength(L):求表长
GetElem(L, i, &e):按位查找,获取第i个元素的值
LocateElem(L, e):按值查找,在线性表中查找第一个满足值为e的元素
PriorElem(L, cur_e, &pre_e):获取当前元素的前驱
NextElem(L, cur_e, &next_e):获取当前元素的后继
ListInsert(&L, i, e):在线性表的第i个位置插入元素e
ListDelete(&L, i, &e):删除线性表的第i个位置的元素,并用e返回该元素的值
ListTraverse(L, visit()):依次对线性表的每个数据元素调用visit()
线性表的顺序存储结构
- 线性表的顺序存储需要一组地址连续的存储单元;
- 顺序表使得逻辑上相邻的元素在物理位置上也相邻;
- 线性表的第i个数据元素的a_i的存储位置为
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) × l 其 中 l 是 每 个 数 据 元 素 所 占 的 内 存 空 间 LOC(a_i)=LOC(a_1)+(i-1)\times{l} \\ 其中l是每个数据元素所占的内存空间 LOC(ai)=LOC(a1)+(i−1)×l其中l是每个数据元素所占的内存空间 - 表中元素的逻辑顺序与其物理顺序相同;
- 线性表的顺序存储结构是一种随机存取的存储结构。
顺序表的特点
- 随机访问,可在O(1)内找到第i个元素;
- 存储密度高,每个结点只存储数据元素;
- 扩展容量不方便;
- 插入、删除不方便,需要移动大量数据。
顺序表的描述
// ———————线性表的静态分配顺序存储结构——————
#define MAXSIZE 50
typedef struct{
ElemType data[MAXSIZE]; // 顺序表的元素
int length; // 当前长度
}SqList;
//——————-线性表的动态分配顺序存储结构——————-
#define INIT_SIZE 100
typedef struct{
ElemType *elem; // 存储空间的基址
int length; // 当前长度
int MaxSize; // 数组最大容量
}SeqList;
顺序表的操作
顺序表结构体
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OVERFLOW -1
#define INIT_SIZE 100
#define LIST_INCREASE 10
typedef struct{
int *data;
int length;
int MaxSize;
}SeqList;
初始化顺序表
Status InitList(SeqList &L){
// malloc申请一片连续的存储空间
L.data = (int *)malloc(sizeof(int) * InitSize);
if (! L.data)
return ERROR;
L.length = 0;
L.MaxSize = INIT_SIZE;
return OK;
}
初始动态分配语句中malloc,申请一整片连续的存储空间,会返回指向这片存储空间开始地址的指针
增加动态数组的空间
Status IncreaseSize(SeqList &L, int len){
int *p = L.data;
L.data = (int *)malloc((L.MaxSize + len)*sizeof(int))
if (! L.data)
return ERROR;
for(int i=0; i<L.length; i++){
L.data[i] = p[i];
}
L.MaxSize = L.MaxSize + len;
free(p);
return OK;
}
// 使用realloc
Status IncreaseSize(SeqList &L, int len){
newbase = (int *)realloc(L.data, (L.MaxSize + len)*sizeof(int));
if (! newbase)
return ERROR;
L.data = newbase;
L.MaxSize += len;
return OK;
}
顺序表的插入
在顺序表L的第i个位置插入元素e,在最后一个元素插入不需要移动其他元素位置
Status ListInsert(SeqList &L, int i, ElemType e){
if (i < 1 && i > L.length+1)
return ERROR;
if (L.length >= L.MaxSize){
if (! IncreaseSize(L, LIST_INCREASE))
return OVERFLOW;
}
for (int j=L.length; j>=i; j—){
L.data[j] = L.data[j-1] ;
}
L.data[i-1] = e;
L.length++;
return OK;
}
- 顺序表插入的时间复杂度:
- 最好:O(1),在表尾插入元素;
- 最坏:O(n),在表头插入元素,需要移动n次;
- 平均:O(n+1),平均移动n/2次。
顺序表的删除
删除顺序表L的第i个位置的元素,在删除最后一个元素不需要移动其他元素。
Status ListDelete(SeqList &L, int i, ElemType &e){
if (i < 1 && i > L.length)
return ERROR;
e = L.data[i-1]
for (int j=i; j<L.length; j++){
L.data[j-1] = L.data[j];
}
L.length—;
return OK;
}
- 顺序表的删除的时间复杂度:
- 最好:O(1),删除表尾元素;
- 最坏:O(n),删除表头元素,移动n-1次;
- 平均:O(n),平均移动(n-1)/2。
顺序表的查找
在顺序表中查找第一个元素值为e的元素
int LocateList(SeqList L, ElemType e){
for (int j=0; j<L.length; j++){
if (L.data[j] == e)
return j+1;
}
return 0;
}
- 顺序表的查找的时间复杂度:
- 最好:O(1),查找的元素在表头;
- 最坏:O(n),查找的元素在表尾;
- 平均:O(n),查找的平均次数(n+1)/2。
线性表的链式存储结构
- 不需要使用地址连续的存储单元,逻辑上相邻的元素物理位置上不一定相邻;
- 插入和删除元素不需要移动元素,只需修改指针;
- 不可随机存取。
单链表
定义
- 通过一组任意的存储单元来存储线性表中的数据元素;
- 对每个链表结点,需要存放元素自身信息和一个指向其后继的指针。
单链表的描述
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
// LNode 强调这是结点
// LinkList 强调这是链表
- 头结点和头指针的区别:
- 头指针始终指向链表的第一个结点;
- 头结点是带头结点的链表的第一个结点,结点内通常不存储信息;
- 引入头结点的优点:
- 使链表在第一个数据结点上的操作和表的其他位置操作一致;
- 无论链表是否为空,其头指针都指向头结点的非空指针,空表和非空表的处理得到统一。
单链表的基本操作
单链表的初始化
初始化一个空表
LinkList InitLink(){
LinkList L;
L = (LinkList)malloc(sizeof(LNode));
if (! L)
return ERROR;
L->next = NULL;
return L;
}
单链表的建立
头插法
从空表开始,生成新结点,将新结点插入到当前链表的表头
Status List_HeadInsert(LinkList &L){
LNode *s;
int num, x, i=0;
printf(“输入链表长度:”);
scanf(“%d”, &num);
while(i < num){
s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return ERROR;
printf(“输入第%d个数据:”, i);
scanf(“%d”, x);
s->data = x;
s->next = L->next;
L->next = s;
i++;
}
return OK;
}
- 读入数据的顺序与生成链表元素的顺序相反
- 头插法建立链表的时间复杂度:
- 每个结点插入时间为O(1);
- 建立的链表表长为n,总时间复杂度为O(n)。
尾插法
建立链表时将新生成的结点插入到表尾
Status List_TailInsert(LinkList &L){
LNode *s, *r=L;
int num, x, i=0;
printf(“输入链表长度:”);
scanf(“%d”, &num);
while(i < num){
s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return ERROR;
printf(“输入第%d个数据:”, i);
scanf(“%d”, x);
s->data = x;
r->next = s;
r = s;
}
r->next = NULL;
return OK;
}
- 尾插法建立链表的时间复杂度与头插法相同。
单链表的查找
按序号查找
从第一个结点开始,直到找到第i个结点为止
LNode *GetElem(LinkList L, int i){
int j = 1;
LNode *p = L->next;
if (i == 0)
return L;
if (i < 0)
return NULL;
while (p && j<i){
p = p->next;
j++;
}
return p;
}
- 按序查找操作的时间复杂度是O(n)
按值查找
LNode * LocateElem(LinkList L, ElemType e){
LNode *p = L->next;
while (p!=NULL && p->data!=e){
p = p->next;
}
return p;
}
- 按值查找的时间复杂度是O(n)
单链表的插入
后插
将值为x的结点插入到链表的第i个位置
先找到待插入结点的前驱结点,再在其后面插入新结点
Status InsertLink(LinkList &L, int i, ElemType e){
LNode *p, *s;
s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return ERROR;
p = GetElem(L, i);
s->next = p->next;
p->next = s;
}
- 插入结点的主要时间开销在于查找插入位置
- 平均时间复杂度为O(n),最好的时间复杂度是O(1)
前插
在某个结点前面插入到一个新结点
解法一:找到第i-1个结点,采用后插操作
解法二:找到第i个结点,执行后插操作后,交换前后结点的值
// 解法二
Status InsertPrior(LinkList &L, int i, ElemType e){
LNode *s, *p;
int temp;
s = (LNode *)malloc(sizeof(LNode));
if (s == NULL)
return ERROR;
p = GetElem(L, i);
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
return OK;,
}
单链表的删除
删除第i个结点
Status DeleteNode(LinkList &L, int i, ElemType &e){
LNode *p = L;
LNode *q;
// 先找到第i-1个结点,p指向第i个结点的前驱
while (p->next && j<i-1){
p = p->next;
++j;
}
if (!(p->next) && j>i-1)
return ERROR;
q = p->next;
p->next = q->next;
e = q->data;
free(q);
return OK;
}
求表长
包含头结点的链表在计算表长时不需要将头结点计算进去
int LinkLength(LinkList L){
int len = 0;
LNode *p = L;
if (L==NULL || L->next)
return 0;
while (p->next){
p = p->next;
len++;
}
return len;
}
静态链表
- 借用数组来描述线性表的链式存储结构
- 链表的指针是数组的下标
- 静态链表需要分配一块连续的内存空间
静态链表的描述
#define MaxSize 50
typedef struct{
ElemType data;
int next; // 下一元素的数组下标
}SLinkList[MaxSize];
- 静态链表以next=-1为链表结束标志
- 静态链表的增删只需要修改next,不需要移动元素
双链表
- 双链表结点钟有两个指针prior和next,分别指向其前驱和后继
双链表的描述
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
基本操作
插入操作
Status ListInsert_Dul(DLinkList &L, int i, ElemType e){
DNode *p, *s;
// 查找插入位置操作与单链表一致
if (!p=GetElem_Dul(L, i))
return ERROR;
s = (DNode *)malloc(sizeof(DNode));
if (!s)
return ERROR;
s->data = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
删除操作
Status ListDelete_Dul(DLinkList &L, int i, ElemType &e){
DNode *p;
if (!p=GetElem_D(L, i))
return ERROR;
e = p->data;
p->next->prior = p->prior;
p->prior->next = p->next;
free(p);
return OK;
}
循环链表
- 链表表尾结点指针指向表头结点;
- 从表中任一结点出发都可以到达表中其他结点;
- 到达表尾结点的标志为
p->next = L
。
循环双链表
- 循环双链表头结点的prior指针需要指向表尾结点;
- 当循环双链表为空表时,头结点的next和prior都指向头结点自身L。
参照 严蔚敏《数据结构(C语言版)》
王道考研 《数据结构考研复习指导》