数据结构与算法——线性表(完结)
作者:互联网
线性表
定义和基本操作
定义
线性表是具有相同
数据类型的n(n大于0)个数据元素的有限序列
,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为
$$
L = (a1, a2, ..., ai, ai+1, ..., an)
$$
ai
时线性表中第i个元素线性表中的位序
。
a1
是表头元素;
an
是表尾元素;
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后驱。
基本操作
一个数据结构的基本操作是指最核心、最基本的操作。其它较复杂的操作可通过调用其基本操作来实现。线性表的主要操作如下:
// 1. 初始化表。构造一个空的线性表
InitList(&L)
// 2. 求表长,返回线性表L的长度,即L中数据元素的个数
Length(L)
// 3. 按值查找操作。在表L中查找具有给定关键字值得元素
LocateElem(L, e)
// 4. 按位查找操作。获取表L中第i个位置的元素的值
Tips:
- 对数据的操作——创建销毁、增删改查
- c语言函数的定义需要指定参数类型
- 实际开发中,可根据实际需求定义其它的基本操作
- 函数名和参数的形式、命名都可以改变(Reference:严蔚敏版)
- 当需要对参数的修改结果“带回来”的时候传入引用
&
为什么要实现对数据结构的基本操作?
- 团队合作编程,你定义的数据结构要让别人可以很方便的使用。
- 将常用的操作或运算封装成函数,避免重复工作,降低出错风险。
总结
线性表的物理结构
线性表的顺序表示
线性表的定义
线性表的顺序存储又叫做顺序表。用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
假定线性表的元素类型为ElemType,则线性表的顺序存储类型描述为:
#define MAXSIZE 50
typedef struct {
ElemType data[MAXSIZE];
int length;
}
此种表示方式是由缺陷的。如果当第51个元素到来时,它是没有地方可以插入的。也就是说,一旦空间占满,在加入新的数据就会产生溢出,进而导致程序崩溃。
所以引出动态分配。
动态分配时,存储空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就需要开辟一块更大的存储空间,用以替代原来的存储空间,从而达到扩充数据空间的目的,而不需要为线性表一次性的划分所有空间。
#define INITSIZE 50
typedef struct {
// 动态分配数组的指针
ElemType *data;
// 数组的最大容量
int maxsize;
// 数组的当前个数
int length;
}
C的初始动态分配语句为
L.data = (ElemType *)malloc(sizeof(ElemType) * INITSIZE)
C++的初始化分配语句为
L.data = new ElemType[INITSIZE]
顺序表的最主要特点是随机访问,即通过首地址和元素需要可在时间O(1)内找到指定的元素。
顺序表的存储密度高,每个节点只存储数据元素。
顺序表逻辑相邻的元素物理上也相邻,所以插入和删除操作需要移动大量的元素。
顺序表上基本操作的实现
插入操作
boll ListInsert(SqList &L, int i, ElemType e) {
if (i < 1 || i > L.length)
return false;
if (L.length > MAXSIZE)
return false;
for(int j = L.length; j >= i; j--) {
L.data[j] = L.data[j-1]
}
L.data[i-1] = e;
L.length++;
return true;
}
删除操作
bool ListDelete(SqList &L, int i, int &e) {
if (i < 1 || i > L.length)
return false;
e = data[i-1];
for(int j = i; j < L.length; j++) {
L.data[j-1] = L.data[j];
}
L.length--;
return true;
}
最好情况:删除表尾元素,时间复杂度为O(1)
最坏情况:删除表头元素,时间复杂度为O(n)
平均情况:i=1循环n-1次;i=2循环n-2次
故为 0 + 1 + 2 + ... + n-1 平均为(n-1) / 2
按值查找
int LocateElem(SqList L, ElemType e) {
for(int i=0;i<L.length;i++) {
if(L.data[i] == e)
return i+1;
}
return 0;
}
最好情况:目标元素在表头,循环一次 O(1)
最坏情况:目标元素在表尾,循环n次 O(n)
平均情况:(1 + 2 + ... + n)/n = (n+1)/n
按位查找
静态分配
ElemType GetElem(SqList L, int i) {
if (i < 1 || i > L.length) {
return null;
}
return L.data[i-1];
}
《数据结构》考研初试中,手写代码可以直接使用“==”,无论是结构体类型还是基本结构类型。
线性表的链式存储
单链表的定义
线性表的链式存储又称单链表,它是通过一组任意的存储单元来存储线性表中的数据元素。
优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存储,要耗费一定空间存放指针。
单链表节点类型的描述如下:
// 定义单链表节点类型
typedef struct LNode {
// 数据域
ElemType data;
// 指针域
struct LNode *next;
}LNode, *LinkList;
// 增加一个新的节点:在内存中申请一个节点所需空间,并用指针p指向这个节点
struct LNode *p = (struct LNode*) malloc(sizeof(struct LNode));
typedef的作用是对数据类型重命名。
使用LinkList时表示 强调这是一个单链表
使用LNode时表示 强调这是一个节点
不带头节点的单链表初始化
bool InitList(LinkList &L) {
// 空表,暂时还没有任何节点
L = NULL;
return true;
}
int main() {
// 声明一个指向单链表的指针
LinkList L;
// 初始化一个空表
InitList(L);
}
不带头节点,写代码更麻烦
对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑。对空表和非空表的处理需要用不同的代码逻辑。
带头节点的单链表初始化
bool InitList(LinkList &L) {
// 分配一个头节点
L = (LNode *)malloc(sizeof(LNode));
// 内存不足,分配失败
if(L == NULL)
return false;
// 头节点之后暂时还没有其它的节点
L->next = NULL;
return true;
}
int main() {
// 声明一个指向单链表的指针
LinkList L;
// 初始化一个空表
InitList(L);
}
单链表上基本操作的实现
按位序插入(带头结点)
// 在第i个位置插入元素e
bool ListInsert(LinkList &L, int i, ElemType e) {
if(i<1)
return false;
// 指针p指向当前扫描到的结点
LNode *p;
// 当前p指向的是第几个结点
int j = 0;
// L指向头结点,头结点是第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;
s->next = p->next;
p->next = 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;
}
// 指针p指向当前扫描到的结点
LNode *p;
// 当前p指向的是第几个结点
int j = 1;
// L指向第一个结点
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;
s->next = p->next;
p->next = s;
return true;
}
指定结点的后插操作
bool InsertNextNode(LNode *p, ElemType e) {
if(p==NULL)
return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
if(s==NULL)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
指定结点的前插操作
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;
p->next =s;
s->data = p->data;
p->data = e;
return true;
}
按位序删除(带头结点)
bool ListDelete(LinkList &L, int i, ElemType &e) {
if(i<1)
return false;
LNode *p;
// 当前p指向的是第几个结点
int j = 0;
// 指向头结点
p = L;
while (p != NULL && j < i-1) {
p = p->next;
j++;
}
if (p == NULL || p->next == NULL)
return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
指定结点删除
bool DeleteNode(LNode *p) {
if(p == NULL)
return false;
LNode *q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}
单链表的局限性:无法逆向检索,有时候不太方便。
按位查找
// 按位查找,返回第i个元素(带头结点)
LNode* GetElem(LinkList L, int i) {
if (i < 0)
return NULL;
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}
按值查找
LNode* LocateElem(LinkList L,ElemType e) {
LNode *p = L->next;
// 从第一个结点开始查找数据域为e的结点
while(p != NULL && p->data !=e) {
p = p->next;
}
return p;
}
求表的长度
int Length(LinkList L) {
int len = 0;
LNode *p = L;
while(p->next != NULL) {
p = p->next;
len++;
}
return len;
}
我们看出了单链表无法逆向检索的缺点,即找到p结点的前驱结点,所以引出了双链表。
双链表
双链表结点中有两个指针prior和next,分别指向前驱结点和后继结点。
双链表中结点类型描述如下:
typedef struct DNode {
ElemType data;
struct DNode *prior;
struct DNode *next;
}DNode, *DLinkList;
双链表的初始化
// 初始化双链表
bool InitDLinkList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode));
// 内存不足则分配失败
if (L == NULL) {
return false;
}
// 头结点的prior指针永远指向null
L->prior = NULL;
// 头结点之后暂时还没有其它的结点
L->next = NULL;
return true;
}
双链表的插入
// 在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s) {
// 判断非法参数
if (p == NULL || s == NULL) {
return false;
}
s->next = p->next;
// 如果p没有后继结点
if (p->next->prior != NULL) {
p->next->prior = s;
}
p->next = s;
s->prior = p;
return true;
}
双链表的删除
// 删除p结点的后继结点q
bool DeleteNextDNode(DNode *p) {
if (p == NULL)
return false;
DNode *q = p->next;
if(q == NULL)
return false;
p->next = q->next;
if (q->next != NULL)
q->next->prior = p;
free(q);
return true;
}
双链表的遍历
// 后向遍历
while (p != NULL) {
// 对结点p做相应处理
p = p->next;
}
// 前向遍历
while (p != NULL) {
// 对结点p做相应处理
p = p->prior;
}
// 前向遍历(跳过头结点)
while (p->prior != NULL) {
// 对结点p做相应处理
p = p->prior;
}
双链表不可随机存取,按位查找、按值查找操作都只能用遍历方式实现。时间复杂度为O(n)
循环链表
循环单链表
表尾结点next指回了头结点。
初始化循环单链表
// 初始化循环单链表
bool InitList(LinkList &L) {
L = (LNode *) malloc(sizeof(LNode));
if (L == NULL)
return false;
L->next = L;
return true;
}
// 判断循环单链表是否为空
bool Empty(LinkList L) {
return L->next == L
}
// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p) {
return p->next == L;
}
循环双链表
静态链表
定义
借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址,又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
优点:增删操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变。
适用场景:
- 不支持指针的低级语言;
- 数据元素数量固定不变的场景(如操作系统的文件分配表FAT)
标签:结点,NULL,return,线性表,next,完结,数据结构,data,LNode 来源: https://www.cnblogs.com/Gazikel/p/16369623.html