数据结构之线性表(C语言)
一.线性表简述
1.定义:
线性表是具有相同数据结构类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。
注:在线性表中,出表头和表尾元素之外,每个元素有且仅有一个直接前驱和直接后继,表头元素无直接前驱,表尾元素无直接后继。
2.线性表有两类存储结构:
线性存储和链式存储。
线性存储结构:顺序表。
链式存储结构:单链表,双链表,循环链表,静态链表。其中静态链表借助数组实现,其他由指针实现。
3.线性表的基本操作
InitList(&L):初始化表。构造一个空的线性表。
Length(L):求表长。
LocateElem(L,e):按值查找操作。
GetElem(L,i):按位查找。
ListInsert(&L,i,e):插入操作。在L表的第i个位置插入元素e。
ListDelete(&L,i,&e):删除操作。删除表中第i个位置的值,并用e返回删除元素的值。
注:基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同;
& 表示C++中的引用调用,在C语言中采用指针效果相同。
二.顺序表
顺序存储结构,在内存空间中连续。
顺序表分为静态分配顺序表和动态分配顺序表。静态分配顺序表事先定义好顺序表的最大长度,数据溢出报错;动态分配顺序表在数据溢出时开辟新空间。
1.1顺序表的定义:
#define MaxSize 10//宏定义最大长度
typedef struct
{
ElemType data[MaxSize];//用静态数字存放数据元素,ElemType改成自己需要的数据类型即可,eg:int
int Length;//顺序表的长度
}SqList;//sequence
1.2初始化:
void InitList(SqList &L)
{
for(int i =0;i<MaxSize;i++)
{
L.data[i]=0;//初始化。
}
L.length=0;//将表长置0
}
int main()
{
SqList L;
InitList(L);
return 0;
}
2.1动态分配定义
头文件 : #include<stdlib.h>
#define InitList 10
typedef struct
{
int *data;//指示动态分配数组的指针
int MaxSize;//顺序表的最大容量
int length;//顺序表的当前长度
}SqLsit;
2.2动态分配实现
使用malloc函数实现实现动态(时间开销大),用realloc也可实现,但初学者建议用malloc,realloc自学。
void InitList(SqList &L)
{
L.data=(int *)malloc(InitSize * sizeof(int));
//malloc函数返回值是指针,需要强制转换为自己需要的类型
//InitList为默认增加长度,sizeof()为数据类型所占用空间
//InitSize * sizeof(int)即分配10个int类型长度。
L.length=0;//长度赋空
L.MaxSize=InitSize;//初始化时,分配默认长度
}
2.3动态增加数组长度
void IncreaseSize(SqLsit &L,int len)
{
int *p=L.data;//将L中的数据地址存到P中。
L.data=(int *)malloc((L.MaxSize+len)*sizeof(int));
//动态分配一个MaxSize+len的连续内存空间(重点连续)
for(int i =0;i<L.length;i++)
{
L.data[i]=p[i];//放到新空间中
}
L.MaxSize = L.MaxSize+len;//最大空间值改变
free(p);//将原L内存空间P释放
}
3.顺序表的插入操作
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int Length;
}SqList;
//插入操作函数
void ListInsert(SqList &L,int i,int e)//位置i插入元素e
{
for(int j = L.length;j>=i;j--)
{
L.data[j]=L.data[j-1];//注意数组内元素的起始下标为0
}
L.data[i-1]=e;//插入元素e
L.length++;//长度+1
}
int main()
{
SqList L;
InitList(L);//初始化
ListInsert(L,3,3);//在第三个位置插入元素3
return 0;
}
4.顺序表的删除操作
bool ListDelete(SqList &L,int i,int &e)
{
if(i<1||i>L.length)//保证i的有效性,此处的i为第几个,非数组内的下标1
{
return false;
}
e=L.data[i-1];//注意下标
for(int j=i;j<L.length;j++)
{
L.data[j-1]=L.data[j];//向前覆盖
}
L.length--;
return true;
}
int main()
{
SqList L;
InitList(L);//初始化
int e = -1;//需要用函数将e的值带回来。
if(ListDelete(L,3,e))
{
printf("%d",e);
}else
{
printf("i不合法,删除失败");
}
return 0;
}
5.顺序表的按位查找操作
按值查找,只需用for循环遍历搜索即可。
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int Length;
}SqList;
int GetElem(SqList L,int i)//查找操作
{
return L.data[i-1];
}
int main()
{
SqList L;
InitList(L);//初始化
int x=GetElem(L,i);//查找第i位的元素
return 0;
}
3.思考
malloc实现原理,realloc使用,进行增删改查的时间复杂度和空间复杂度。
三.链表
链表不需要使用地址连续的存储单元,不要求逻辑上相邻的元素在物理位置上也相邻。存储结构能反映数据之间的逻辑关系。解决了顺序表的插入删除困境,却失去随机存取的优点。
3.1单链表
1.定义单链表
typedef struct LNode{
ElemType data;//每个结点存放一个数据元素(数据域)
struct LNode *next;//指针指向下一个结点(指针域)
}LNode,*LinkList;//分别表示结点和链表,其实互用也可,为了规范
/*
注:typedef <数据类型> <别名> typedef 关键字 —— 数据类型重命名
上面代码等价:
typedef struct LNode LNode;
typedef struct LNode *LinkList;
要表示一个单链表时,只需声明一个头指针 L ,指向单链表的第一个结点:
LNode * L;与 LinkList L; 是声明一个指向单链表第一个节点的指针。
后者代码可读性更强。
*/
LNode * p = (LNode *) malloc(sizeof(LNode));
//增加一个新的结点:在内存中申请一个结点所需空间,并用指针 p 指向这个结点
2.建立单链表
头插法建表(带头结点):
LinkList List_HeadInsert(LinkList %L)//头插法顺序逆向
{
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));//创建头结点
L->next=null;//初始为空表
scanf("%d",&x);
while(x!=9999)//设置结束点
{
s=(LNode*)malloc(sizeof(LNode));//此处用lnode强调这是一个结点
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
尾插法建表(带头结点):
每次均需要循环,时间复杂度O(n^2)
bool InitList(LinkList &L)
{
L=(LNode *)malloc(sizeof(LNode));
if(L==null)//无内存
return false;
L-next=null;
return true;
}
bool ListInsert(LinkList &L,int i,ElemType e)//重点在于每次查找到第i-1个元素,
{
if(i<1)
return false;
LNode *p;
int j = 0;
p=L;
while(p!=null &&j<i-1)//每次都从头开始之后遍历,时间复杂度为 O(n^2)
{
p=p->next;
j++;
}
if(p==null)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
int main()
{
LinkList L;
InitList(L);
}
不带头结点的单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//初始化链表
bool InitList(LinkList &L)
{
L=null;//空表,无任何结点
return true;
}
int main()
{
LinkList L;
InitList(L);//初始化
............
}
注:不带头结点,写代码更麻烦 对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑 对空表和非空表的处理需要用不同的代码逻辑;简言之,带头结点好。
3.单链表的插入操作
按位序插入(带头结点)
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
找到第 i-1 个结点, 将新结点插入其后;头结点可以看作 “第0个”结点
bool ListInsert(LinkList &L,int i,ElemType e)
{
if(i<1)
return false;
LNode *p;
int j = 0;
p=L;
while(p!=null &&j<i-1)
{
p=p->next;
j++;
}
if(p==null)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
1. s->next=p->next;
2. p->next=s;//将结点s连接到p之后
//注:1与2顺序一定不能颠倒,会死循环s;
return true;//插入成功
}
不带头结点
bool ListInsert(LinkList &L,int i,ElemType e)
{
if(i<1)
return false;
if(i==1)//当修改的为第一个元素时,需要改变头结点的指针
{
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s;//改变头结点
return true;
}
LNode *p;
int j=1;
p=L;
while(p!=null&&j<i-1)
{
p=p->next;
j++;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p->next=s;//s连接到p之后
return true;
}
指定结点的前插操作
在p结点之前插入元素e
bool InsertPriorNode (LNode *p,ElemType e)
常规做法是循环查找p的前驱q,再对q进行后插
技巧(先进行后插,再交换元素值):
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;
s->data=p->data;
p->next=s;
p->data=e;
return true;
}
4.单链表按位序删除(带头结点)
找到第 i-1 个结点,将其指针指向第i+1个结点,并释放第i个结点。
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
bool ListDelete(LinkList &L,int i,ElemType &e)
{
if(i<1)
return false;
LNode *p;
int j = 0;
p=L;
while(p!=null&&j<i-1)
{
p=p->next;
j++;
}
if(p==null)i值不合法
return false;
if(p->next==null)
return false;
LNode *q=p->next;
e=q->data;
p->next=q->next;//相当于p->next=p->next->next;
free(q);//释放结点存储空间
return true;
}
/*
不带头结点时,删除第一个结点只需要让L=L->next;
*/
3.2双链表
在单链表的条件下增加前驱结点。
1定义:
typedef struct DNode{
ElemType data;//数据域
struct DNode *prior,*next;//前驱与后继结点
}DNode,*DLinkList;
2初始化
bool InitDLinkList(DLinkList &L)
{
L = (DNode *)malloc(sizeof(DNode));//分配一个头结点
if(L==null)
return false;
L-prior=null;//头结点的prior永远指向null
L->next=null;
return true;
}
int main()
{
DLinkList L;
InitDLinkList(L);
}
3.判断双链表是否为空
bool Empty(DLinkList L)
{
if(L->next==null)
return true;//空则返回正确
return false;
}
4.双链表的插入操作
p结点之后插入s;
bool InsertNextDNode(DNode *p,DNode *s)
{
s->next=p->next;
s->prior=p;
if(p->next!=null)//判断p是否为最后一个结点,防止空指针错误
p->next->prior=s;//p的后继结点的前驱部分设置为s
p->next=s;
}
注:链表的所有插入操作都可转化为后插操作。
5.双链表的删除操作
删除p结点的后继结点
bool DeleteNextDNode(DNode *p)
{
if(p==null)
{
return false;
}
DNode *q=p->next;//p的下一个结点
if(q==null)//p没有后继
return false;
p->next=q->next;
if(q->next!=null)//判断q是否为最后一个结点
{
q->next->prior=p;
}
free(q);
return true;
}
/*核心代码:
DNode *q = p->next;
p->next=q->next;
q->next->prior=p;
*/
3.3循环链表
1.循环单链表
在单链表的条件下将最后一个结点的后继指向头结点
从一个结点出发,可以找到其他任何一个结点
typedef struct LNode
{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
//初始化
bool InitList(LinkList &L)
{
L=(LNode *)malloc(sizeof(LNode *));
if(L==null)
return false;
L->next=L;//后继结点指向头结点,构成循环。
return true;
}
//判空,判断表尾结点道理一样
bool Empty(LinkList L)
{
if(L->next ==L)
return true;
return false;
}
2.循环双链表
//初始化循环双链表
bool InitDLinkList(DLinklist &L)
{
L=(DNode *)malloc(sizeof(DNode));
if(L==null)
return false;
L->prior=L;
L->next=L;
return true;
}
//p结点之后插入S结点
bool INsertNextDNode(DNode *p,DNode *s)
{
s->next=p->next;
s->prior=p;
p->next->prior=s;
p->next=s;
}
//删除p的后继结点q:
p->next=q->next;
q->next->prior=p;
free(q);
4.顺序表和链表的选择
需扩容:
顺序表 | 链表 | |
---|---|---|
弹性(可扩容) | √ | |
增、删 | √ | |
查询 | √ |