文章目录
1.线性表
定义:线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
特点: 同一线性表中的元素必定具有相同特性,数据元素间的关系是线性关系。
存储结构: 顺序存储结构和链式存储结构。
1.1 线性表的类型定义
抽象数据类型线性表的定义: ADT List{ 数据对象D: 数据关系R: 基本操作: InitList(&L) 操作结果:构造一个空的线性表L DestroyList(&L) 初始条件:线性表L已经存在 操作结果:销毁线性表L ClearList(&L) 初始条件:线性表L已经存在 操作结果:将线性表L重置为空表 ListEmpty(L) 初始条件:线性表L已经存在 操作结果:若线性表L为空表,则返回Ture;负责返回False ListInsert(&L,i,e) 初始条件:线性表L已经存在,1<=i<=ListLength(L)+1 操作结果:在L的第i个位置之前插入新的元素e,L的长度加一 ListDelete(&L,i,e) 初始条件:线性表L已经存在,1<=i<=ListLength(L) 操作结果:删除L的第i个数据元素,L的长度减一 }ADT List
2.顺序表
**定义:**顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组
上完成数据的增删查改。
顺序存储定义:把逻辑上相邻的数据元素存储在物理相邻的存储单元中的存储结构。
顺序表的特点:以物理位置相邻表示逻辑关系。任一元素均可随机存取(优点)。
顺序表一般可以分为:
-
静态顺序表:使用定长数组存储。
-
动态顺序表:使用动态开辟的数组存储。
//顺序表的静态存储
typedef int SLDatatype; //SLDatatype可以为任何类型,假设为int
#define N 100
typedef struct SeqList
{
SLDatatype array[N];//定长数组,arry是存放线性表结点的向量空间,下标从0开始
size_t size;//有效数据的个数
}SeqList;
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#define MaxSize 10 //定义最大长度
typedef struct
{
int data[MaxSize];//用静态的数组存放元素
int length;//顺序表的当前长度
}SqList;//顺序表类型定义
//基本操作——初始化一个顺序表
void InitList(SqList &L)
{
int i;
for (i = 0;i < MaxSize;i++)
{
L.data[i] = 0;//将所有数据元素设置为默认初始值(可以省略),不省略内存中会有脏数据
L.length = 0;//顺序表初始长度为0
}
}
int main()
{
SqList L;//声明一个顺序表
InitList(L);//初始化顺序表
//尝试违规打印整个data数组
for (int i = 0;i < MaxSize;i++)
{
printf("data[%d]= %d\n",i,L.data[i]);
}
return 0;
}
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
//顺序表的动态存储
typedef struct SeqList
{
SLDatatype* arry;//指向动态开辟的数组,空间不够则增容
size_t size;//有效数据的个数
size_t capicity;//容量空间大小
}SeqList;
用malloc实现一个动态数组的扩展
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define InitSize 10 //默认的最大长度
//用malloc实现一个动态数组的扩展
typedef struct
{
int* data;//指示动态分配数组的指针
int MaxSize;//顺序表的最大容量
int length;//顺序表的当前长度
}SeqList;
void InitList(SeqList& L)
{
//用malloc函数申请一片连续的存储空间
L.data = (int*)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
// 增加动态数组长度
void IncreaseSize(SeqList &L, int len)
{
int* p = L.data;
L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
for (int i = 0;i < L.length;i++)
{
L.data[i] = p[i];//将数据复制到新的区域
}
L.MaxSize = L.MaxSize + len;//顺序表最大长度增加len
free(p);//释放原来空间
}
int main()
{
SeqList L;//声明一个顺序表
InitList(L);//初始化顺序表
//往顺序表中随便插入几个元素
IncreaseSize(L, 5);
return 0;
}
顺序表的特点:
- 随机访问,即可以在O(1)时间内找到第i个元素;
- 存储密度高,每个节点只存放数据元素;
- 拓展容量不方便(即便是采用动态分配的方式实现,拓展长度的时间复杂度也比较高);
- 插入、删除不方便,需要移动大量元素。
2.1 顺序表的基本运算
-
插入运算(先将后面的元素往后移动,再将前面的元素往前移动)
#include <stdio.h> #define MaxSize 10//定义最大长度 typedef struct { int data[MaxSize]; int length; }SqList; int main() { //声明一个函数 bool ListInsert(SqList & L, int i, int e); void InitList(SqList & L); SqList L;//声明一个顺序表 InitList(L);//初始化顺序表 //插入几个元素 ListInsert(L, 3, 3); return 0; } //初始化顺序表 void InitList(SqList &L) { L.length = 0;//顺序表的初始长度为0 } //顺序表插入操作 bool ListInsert(SqList &L,int i,int e) { if (i<1 || i>L.length + 1)//判断i的范围是否有效 { return false; } if (L.length >= MaxSize)//当前存储空间已满,不能插入 { return false; } for (int j = L.length;j >= i;j--)//将第i个元素及之后的元素后移动 L.data[j] = L.data[j - 1]; } L.data[i - 1] = e;//在位置i出放入要插入的元素e L.length++;//长度加1 return true; }
-
删除运算(先将前面的元素往前移动,再移动后面的元素)
//顺序表删除操作 bool ListDelete(SqList& L, int i, int& e) { if (i<1 || i>L.length)//判断i的范围是否有效 { return false; } e = L.data[i - 1];//将被删除的元素赋给e //将第i个元素前移 for (int j = i;j < L.length;j++) { L.data[j - 1] = L.data[j]; } L.length--;//线性表长度减1 return true; }
-
查找运算(按值查找)
//顺序表按值查找 //在顺序表L中查找第一个元素值等于e的元素,并返回其位序 int LocateElem(SeqList L, int e) { for (int i = 0;i < L.length;i++) { if (L.data[i] == e) { return i + 1; } } return 0; }
3.线性表的链式表示
3.1单链表的建立
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。动态的建立单链表的常用方法有两种:头插法和尾插法建表。
3.1.1尾插法建立单链表
-
定义单链表
typedef struct LNode//定义单链表结点型,LNode是结点型的别名 { int data;//每个结点一个数据元素 struct LNode* next;//指针指向下一个结点 }LNode,*LinkList;//LinkList是指向结点类型别名
-
初始化一个单链表(带头结点)
//初始化一个单链表(带头结) bool InitList(LinkList& L)//LinkList& L是指向指针的引用,可以保持对L指针的修改 { L = (LNode*)malloc(sizeof(LNode));//为头结点分配内存空间 if (L == NULL)//内存不足,分配失败 { return false; } L->data = NULL;//初始化头结点针域为NULL return true; }
-
尾插法建立单链表
尾插法建表是将新结点插到当前链表的表尾上,为此必须增加一个尾指针r,r永远指向当前链表的表尾结点。
LinkList List_TailInsert(LinkList& L)
{
int x;
L = (LinkList)malloc(sizeof(LNode));//建立头结点
LNode* s, * r = L;//r为表尾指针
scanf("%d", &x);//输入结点的值
while (x != 999)//输入999表示结束
{
//在r结点之后插入元素x
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;//r指向新的表尾结点
}
r->next = NULL;//尾结点指针置空
return L;
}
3.1.2 头插法建立单链表
头插法建表是从一个空表开始,重复读入数据,生成新的结点,将读入数据存放到新结点数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。头插法建表虽然简单,但是生成的链表中结点次序和输入的顺序相反。
LinkList List_HeadInsert(LinkList& L)
{
LNode* s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;//初始为空链表
scanf("%d", &x);//输入结点的值
while (x != 999)//输入999表示结束
{
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
}
return L;
}
3.2单链表的查找
-
按位查找
单链表中,即使知道了访问结点的序号i,也不可以像顺序表中那样直接按照序号i访问结点,而只可以从链表的头指针出发,顺着链域next逐个结点往下搜索,知道搜索到第i个结点为止。如果需要找头结点,将头结点看成第0个结点。
具体实现过程如下:
-
指针p指向当前扫描的结点,用j作计数器;
-
指针p的初始值指向头结点,j的初始值为0;
-
当p扫描到下一个结点时,计数器j相应加1,知道j=i,此时指针p指向的结点就是要找的第i个结点。具体算法实现如下。
//单链表按位查找,返回第i个元素(带头结点) LNode* GetElem(LinkList L, int i) { LNode* p;//指针p指向当前扫描的结点 int j = 0;//当前p指向第几个结点 p = L;//L指向头结点,头结点是第0个结点(不存数据) while (p != NULL && j < i) { p = p->next;//扫描下一个结点 j++;//已扫描结点计数器 } if (i == j) { return p; } else { return NULL; } }
-
按值查找
按值查找是从开始结点出发,顺着链逐个将结点的值和给定值key做比较,来完成结点的查找。
//单链表按值查找,找到数据e的结点
LNode* LocateElem(LinkList L, int e)
{
LNode* p = L->next;//从开始结点比较
//从第1个结点开始查找数据域为e的结点
while (p != NULL && p->data != e)
{
p = p->next;//没找到,继续循环
}
return p;
}
3.3 单链表的插入
单链表的存储结构中,元素的插入或删除,只需要修改指针内容,无需移动元素。
-
插入结点点操作(将值为e的新结点插入到单链表第i个位置上)
- 先检查插入位置合法性;
- 找到待插入位置的前驱结点,即第i-1个结点,然在其后插入新结点。其中后插操作可以用InsertNextNode代替。
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList& L, int i, int e)//尾插法建立单链表
{
//判断插入位置是否有效
if (i < 1)
{
return false;
}
LNode* p = GetElem(L,i-1);//找到第i-1个结点
if (p == NULL)//插入的i位置不合法
{
return false;
}
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;//将新结点s的后继结点指向p的后继结点
p->next = s;//将p的后继结点指向新的结点s
return true;
}
-
后插操作:在p结点之后插入元素e
bool InsertNextNode(LNode* p, int e) { if (p == NULL) { return false; } LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL)//内存分配失败 { return false; } s->data = e;//用结点s保存数据元素e s->next = p->next; p->next = s;//将结点s连到p之后 return true; }
-
前插操作:在p结点之前插入元素e
//单链表前插操作(在p结点之前插入元素e) bool InsertPriorNode(LNode* p, int e) { if (p == NULL) { return false; } LNode* s = (LNode*)malloc(sizeof(LNode)); if (s == NULL)//内存分配失败 { return false; } s->next = p->next; p->next = s;//将结点s连到p之后 s->data = p->data;//将p中元素复制到s中 p->data = e;//p中元素覆盖为e return true; }
3.4 单链表的删除
- 单链表的删除:删除指定结点
//单链表的删除:删除指定结点p
bool DeleteNode(LNode* p)
{
if (p == NULL)
{
return false;
}
LNode* q = p->next;//令p指向*p的后继结点
p->data = p->next->data;//和后继结点交换数据域
p->next = q->next;//将*p结点从链表中断开
free(q);//释放存储空间
return true;
}
-
单链表上实现线性表顺出运算
要使删除运算简单,就必须找到被删除结点的前驱,即第i-1个结点*p,然后删除*p的后继。
//删除链表中的第i个结点
bool ListDelete(LinkList& L, int i, int& e)
{
//判断删除位置是否有效
if (i < 1)
{
return false;
}
LNode* p = GetElem(L, i - 1);//找到第i-1个结点
if (p == NULL||p->next==NULL)//删除的i位置不合法或第i-1个结点之后无其他结点
{
return false;
}
LNode* q = p->next;//q指向被删除的结点
e = q->data;//e返回元素值
p->next = q->next;//q结点断开
free(q);
return true;
}