数据结构——2.线性表
考纲
- 线性表的定义
- 线性表的基本操作及在顺序存储及链式存储上的实现
- 各种变形链表(循环链表、双向链表、带头结点的链表等)的表示和基本操作的实现
- 递归过程的特点及实现方法
- 栈和队列的基本概念;栈和队列的顺序存储结构、链式储存结构及其存储特点
- 栈和队列的应用
- 循环队列的判满、判空方法
- 特殊矩阵的压缩储存
一、线性表的定义
1、定义(了解)
- 线性表是具有相同数据类型的 n(n≥0) 个数据元素的有限序列。n为表长,空表:n=0。
2、特点(了解)
- 表中元素:
- 个数有限
- 有逻辑上的顺序性
- 是数据元素
- 数据类型相同,占有相同大小的存储空间
- 具有抽象性(逻辑关系)
3、tips
- 区分:线性表是逻辑结构,表示元素间一对一的相邻关系。顺序表和链表指存储结构。
二、线性表的基本操作及在顺序存储及链式存储上的实现
(一)顺序表(帮助理解时间复杂度,不掌握)
(1)顺序表存储结构
- 假设线性表L存储的起始位置为LOC(A),sizeof(ElemType)是每个数据元素所占用的存储空间的大小,则L所对应的顺序存储如下:
- 顺序存储类型分配语句
- 静态分配
#define MaxSize 50 typedef struct{ ElemType data[MaxSize]; //顺序表的元素 int length; //顺序表当前长度 }SqList; //顺序表类型定义
- 动态分配
#define InitSize 100 typedef struct{ ElemType *data; //指示动态分配数组的指针 int length,MaxSize; //当前个数和最大容量 }SeqList; //顺序表类型定义
- 动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
- 静态分配
(2)顺序表的基本操作
- 分配
//头文件
#include<stdio.h>
#include<stdlib.h>
//状态量
#define OK 1
#define ERROR 0
typedef int Status; //Status是函数的类型,其值是函数结果状态代码
#define MAXSIZE 100 //初始分配量
typedef int ElemType; //ElemType的类型根据实际情况而定
typedef struct{
ElemType *elem; //存储空间基址,动态分配
//ElemType data[MAXSIZE] //静态分配
int length; //存放顺序表的长度
}SqList;
注(天勤):考试中用得最多的顺序表定义并不是结构型定义,而是如下:
int A[MAXSIZE]; //存储表中元素的数组A,类型可变
int n;
- InitList(&L)
初始化表。构造一个空的线性表
Status InitList(SqList* L){
L -> elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType));
if(!L -> elem) exit(ERROR);
L -> length = 0;
return OK;
}
- Length(L)
求表长,返回线性表L的长度,即L中数据元素的个数
int Length(SqList L){
return L.length;
}
- LocateElem(L,e)
按值查找。在L中找具有给定关键字值的元素
顺序表:平均时间复杂度O(n)(表头O(1),表尾O(n))
Status LocalElem(SqList L, ElemType *e){
int i;
for(i=0; i<L.length; i++){
if(L.elem[i] == e)
return i+1; //返回第几位
}
return ERROR; //查找失败
}
- GetElem(L,i)
按位查找。获取表L中第i个位置的元素的值。
Status GetElem(SqList L, int i, ElemType *e){
if(L.length==0 || i<1 || i>L.length){
return ERROR; //判断范围
}
*e = L.elem[i-1];
return OK;
}
- ListInsert(&L,i,e)
插入操作。在L中第i个位置上插入指定元素e。
顺序表:平均时间复杂度O(n)。(表尾O(1),表头O(n))
Status ListInsert(SqList *L, int i, ElemType e){
if (L->length >= MAXSIZE){ //存储空间已满
return ERROR;
}
if (i<1 || i>L->length+1){ //判断i范围是否有效
return ERROR;
}
for(int j=L->length; j>=i; j--){ //将第i个及之后的元素后移
L->elem[j] = L->elem[j-1];
}
L->elem[i-1] = e; //i处放入w
L->length++; //长度加1
return OK;
}
- ListDelete(&L,i,e)
删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
顺序表:平均时间复杂度O(n)。(表尾O(1),表头O(n))
Status ListDelete(SqList *L, int i, ElemType *e){
if(L->length == 0){ //线性表为空
return ERROR;
}
if(i<1 || i>L->length){ //i范围是否有效
return ERROR;
}
*e = L->elem[i-1];
for(int j=i; j<L->length; j++){
L->elem[j-1] = L->elem[j];
}
L->length--; //长度减1
return OK;
}
- PrintList(L)
输出操作。按前后顺序输出线性表L的所有元素值。
void PrintList(SqList L){
for(int i=0; i<L.length; i++){
printf("%d ",L.elem[i]);
}
printf("\n");
}
- Empty(L)
判空操作。若L为空表,则返回true,否则返回false。
Status Empty(SqList L) {
if (L.length == 0)
return OK;
else
return ERROR;
}
- DestroyList(&L)
销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
void DestroyList(SqList *L){
free(L->elem);
L->elem = NULL;
L->length = 0;
}
区分:清空操作
Status ClearList(SqList *L){
L -> length = 0;
return OK;
}
(二)单链表(掌握)
1. 单链表存储结构
- 带头结点
typedef int ElemType;
//构造结点
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
LNode *
:强调这是一个结点LinkList
:强调这是一个单链表
2. InitList(&L)
//初始化带头结点
Status InitList(LinkList L){
L = (LinkList)malloc(sizeof(LNode));
if (L == NULL) //内存不足分配失败
return ERROR;
L->next = NULL; //将单链表的头结点指针域为空
return OK;
}
//初始化不带头结点
Status InitList(LinkList *L){
L = NULL; //空表,暂时还没有任何结点,防止脏数据
return OK;
}
3. Length(L)
时间复杂度O(n)
//带头结点
int Length(LinkList *L){
int len=0; //统计表长
LNode *p = L;
while(L!=NULL){
p=p->next;
len++;
}
return len;
}
//不带头结点
int Length(LinkList L)
{
int len = 0;
if(L == NULL)//首先判断第一个结点是否为空
return len;
else
len++;
while(L->next != NULL){
len++;
L = L->next;
}
return len;
}
4. LocateElem(L,e)
时间复杂度O(n)
//带头结点
LNode* LocateElem(LinkList L, ElemType e){
LNode *p = L->next; //指针p指向第一个结点
while (p!=NULL && p->data!=e)//从第一个结点开始查找data域为e的结点
p = p->next; //查到最后一个元素为NULL为止
return p; //返回查出来的指针p
}
//不带头结点
LNode* LocateElem(LinkList L, ElemType e){
LinkList p = L;
int j = 0;
while (p){
j++;
if (p->data == e)
return p;
p = p->next; //更新p
}
return NULL; //查找失败,返回-1
}
5. GetElem(L,i)
时间复杂度O(n)
//带头结点
LNode *GetElem(LinkList *L, int i){
if (i<0) //判断要插入的结点位置是否存在
return NULL;
LNode *p;
int j=0;
p=L;
/*王道书版本
int j=1;
LNode *p = L->next;
if(i==0)
return L;
if(i<1)
return NULL;
*/
while (p!=NULL && j<i){//通过循环找到要找的第i个结点
p = p->next;
j++;
}
return p;//返回指针p
}
//不带头结点(待测)
ElemType GetElem(LinkList L, int i){
LNode *p = L;
if(i < 0 || i > Length(L))
return NULL;
for(int j = 1; j < i; j++){
p = p->next;
}
return p;
}
6. ListInsert(&L,i,e)
(1)头插法(关键代码while内可能单独选填)
时间复杂度:每点插入时间为O(1),表长n则总复杂度为O(n)
//头插法建立单链表(有头结点)
LinkList List_HeadInsert(LinkList L){ //逆向建立
LNode *s;
ElemType x;
//代替:InitList(L); //创建头结点,初始为空
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; //初始为空【尾插可要可不要】
//代替结束
scanf("%d",&x);
while(x!=9999){ //输入9999表示结束
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
//不带头结点(待测试)
LinkList List_HeadInsert(LinkList L){
InitList(L);
LNode *s;
ElemType x;
scanf("%d",&x);
if(x != 9999){ //对第一个结点赋值
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
L->data = x;
scanf("%d",&x);
while(e != 9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L;
L = s;
scanf("%d",&x);
}
}
return L;
}
(2)尾插法(关键代码while内可能单独选填)
时间复杂度和头插法一致
//尾插法建立单链表(带头结点)
LinkList List_TailInsert(LinkList L){ //正向建立
ElemType x;
L = (LinkList)malloc(sizeof(LNode));//建立头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d", &x);
while(x!=9999){
//后插思维
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
//不带头结点(待测试)
LinkList List_TailInsert(LinkList L){
InitList(L);
LNode *s,*r;
ElemType x;
scanf("%d",&x);
if(e != 9999){ //对第一个结点赋值
L = (LinkList)malloc(sizeof(LNode));
L->data = x;
r = L;
scanf("%d",&x);
while(e != 9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d",&x);
}
}
r->next = NULL;
return L;
}
(3)前插
查找第i-1个元素时时间复杂度O(n)。若在给定的结点后面插入新结点,则时间复杂度仅为O(1)。
//前插(在p结点前插入)
Status InsertPriorNode(LNode *p, ElemType e){
if(p==NULL) //GetElem返回NULL时
return ERROR;
LNode *s = (LNode *)malloc(sizeof(LNode));
if(s==NULL)
return ERROR;
s->next = p->next;
p->next = s; //新结点s连到p之后
s->data = p->data; //将p中元素复制到s中
p->data = e; //p中元素覆盖为e
return OK;
}
/*//王道书版本
Status InsertPriorNode(LNode *p,LNode *s){
if(p==NULL || s==NULL)
return ERROR;
s->next = p->next;
p->next = s; //s连到p之后
ElemType temp = p->data; //交换数据域部分
p->data = s->data;
s->data = temp;
return OK;
}*/
(4)后插
//后插(在p结点后插入)
Status InsertNextNode(LNode *p, ElemType e){
if(p==NULL)
return ERROR;
LNode *s = (LNode *)malloc(sizeof(LNode));
if (s==NULL)
return ERROR;
//代替:p = GetElem(L,i-1);
s->data = e; //保存e
s->next = p->next; //将结点s连到p之后
p->next = s;
return OK;
}
(5)按位序插入(带/不带头结点)
//按位序插入(带头结点)(//不带头结点)【前插】
LinkList ListInsert(LinkList L, int i,ElemType e){
if(i<1)
return ERROR;
/*不带头结点:需要对在第一个结点插入时进行特殊处理
if (i==1){ //需要更改头指针
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s;
return OK;
}
*/
//代替:LNode *p = GetElem(L, i-1);//找到第i-1个结点
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
//int j=1; //不带头结点时
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p = p->next;
j++;
}
//代替结束
//代替:return InserNextNode(p,e);
if(p==NULL) //i不合法
return ERROR;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s; //将结点s连到p之后
return OK;
//代替结束
}
7. ListDelete(&L,i,e)
(1)按位序删除
//按位序删除(带头结点)
Status ListDelete(LinkList *L, int i, ElemType *e){
if(i<1)
return ERROR;
//代替:LNode *p = GetElem(L, i-1);//找到第i-1个结点
LNode *p;
int j = 0;
p = L;
while(p!=NULL && j<i-1){ //循环找到i-1个结点
p = p->next;
j++;
}
//代替结束
if(p == NULL)
return ERROR;
if(p->next = NULL) //第i-1个结点后无别的结点
return ERROR;
LNode *q = p->next; //q指向被删除结点
e = q->data; //用e返回值
p->next = q->next; //将*q结点从链中断开
free(q); //释放空间
return OK;
}
//不带头结点(待测)
Status ListDelete(LinkList *L, int i, ElemType *e)
{
if(i<1 || i>Length(L))//位置不在表中则返回false
return ERROR;
if(L==NULL)//表为空则返回false
return ERROR;
if(i==1){
LNode* p = L;
e = p->data;
LNode* q = L->next;
free(p);//释放p结点
L = q;
return OK;
}
LNode *p = GetElem(L, i-1);
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return OK;
}
(2)按节点删除
//删除指定结点p(局限性:无法逆向检索)
Status DeleteNode(LNode *p){
if(p == NULL)
return ERROR;
LNode *q = p->next; //q指向*p的后继结点
p->data = p->next->data; //和后继结点交换数据域
p->next = q->next; //*q结点从链中断开
free(q); //释放后继结点的存储空间
return OK;
}
8. PrintList(L)
void PrintList(LinkList L){
while(L){
printf("%d ",L->data);
L=L->next;
}
printf("\n");
}
9. Empty(L)
//判断是否为空(不带头结点)
Status Empty(LinkList L){
if (L == NULL)
return OK;
else
return ERROR;
}
/*或者
Status Empty(LinkList L){
return(L==NULL);
}*/
10. DestroyList(&L)
void DestroyList(LinkList L) {
LinkList p = L;
while (p){
L = L->next;
free(p);
p = L;
}
}
(三)双链表
1. 结点类型
typedef struct DNode{
ElemType data; //数据域
struct DNode *prior,*next; //前驱和后继指针
}DNode, *DLinklist;
2. 插入操作(选填)
①s->next = p->next; //将结点*s插入到结点*p之后
②p->next->prior = s;
③s->prior = p;
④p->next = s;
顺序不唯一,但①②两步必须在④步之前。否则*p的后继结点的指针就会丢掉。
3. 删除操作(选填)
①p->next = q->next;
②q->next ->prior = p
free(q)
(四)循环单链表(选填)
- “对循环单链表不设头指针而仅设尾指针,从而使操作效率更高。”
- 原因:若设的是头指针,对表尾进行操作需要O(n)的时间复杂度,而若设的是尾指针r,r->next即为头指针,对表头与表尾进行操作都只需要O(1)的时间复杂度。
(五)循环双链表
(六)静态链表
(1)静态链表结构类型
#define MaxSize 50
typedef struct{
ElemType data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
(2)静态链表和单链表的对应关系(推导)
三、顺序表和链表对比、优缺点
(一)比较
(二)实际选取
通常较稳定的顺序表选择顺序存储,而频繁进行插入、删除的线性表(即动态性较强)宜选择链式存储。