数据结构
根据大话数据结构进行总结
数据结构绪论
一些基本定义与概念
- 计算机解决问题,先从具体问题中抽象出一个适当的数据模型,设计出一个解决此数据模型的算法,然后编写程序,得到一个实际的软件
-数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系 和操作等相关问题的学科 - 程序设计=数据结构=算法
- 数据:是描述客观事物符号,是计算机中可以操作的对象,是指能被计算机识别,比昂输入给计算机处理的符号集合
对于整型,实型等数值类型,可以进行数值计算。对于字符型数据类型,进行非数值的处理。声音,图像,视频等通过编码的手段变成字符数据来处理 - 数据元素: 是组成数据的,有一定意义的基本单位,在计算机中通常作为整体处理,也被称为记录。 如人类中数据元素为人
- 数据项:一个数据元素可以由若干个数据项组成 如人的嘴巴,鼻子,姓名,年龄。性别等
- 数据对象: 是性质相同的数据元素的集合,是数据的子集。如人都有姓名,生日,性别等相同的数据项
- 数据结构: 不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系成为结构。 所以,数据结构,就是相互之间存在一种或多种特定关系的数据元素的集合。
- 为编写好一个好的程序,必须分析待处理对象的特性及各处理对象之间存在的关系。这就是研究数据结构的意义所在
逻辑结构 与物理结构
逻辑结构
- 逻辑结构: 是指数据对象中数据元素之间的相互关系
1.集合结构: 集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系,各个元素之间是平等的
- 线性结构:数据元素是一对一的关系
- 树形结构: 一对多
4.图形结构:多对多
- 以上,逻辑结构是针对具体问题的,是为了解决某个问题,在对问题的理解的基础上,选择一个合适的数据结构表示数据元素之间的逻辑关系
物理结构
- 物理结构:是指数据的逻辑结构在计算机中的存储形式
- 数据的存储结构应正确反映数据元素之间的逻辑关系,关键点
- 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据之间的逻辑关系和物理关系是一致的。
- 链式存储结构 :把数举元素存储在任意的存储单元中,这存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个
指针
存放数据元素的地址,这样通过地址就可以相关联数据元素的位置。如电影无间道之间的关系
总之,逻辑结构是面向问题的,而物理结构是面向计算机的,其基本的目标就是将数据结构及其逻辑关系存储到计算机的内存中。
数据类型
- 数据类型:指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
- 类型用来说明变量或表达式的取值范围和所能进行的操作
- c语言中,按取值的不同,可以分为两类
原子类型 不可再分,包括整型,实型,字符型
结构类型 由若干个类型组合而成,可以再分解 如整型数组是由若干个整型数据构成的
- 抽象是指取出事物具有的普遍性的本质。它是抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,它隐藏了繁杂的细节,只保留实现目标的所需的信息
抽象数据类型 一个数学模型及定义在该模型的一组操作,体现了程序设计中问题分解,抽象和信息隐藏的特性
数据结构是相互之间存在的一种或多种特定关系的数据元素的集合
算法
- 算法:算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作
- 由两种计算1到100累加的算法实现,进行比较,引人深思
- 为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每个操作都完成特定的功能,这就是算法了
算法的特性
- 输入输出:零个或多个输入,一个或多个输出
- 有穷性:自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成
- 确定性 :每一步骤都有确定的含义,不会出现二义性
- 可行性:每一步都呢能够通过执行有限次数完成
算法设计的要求
- 正确性 :无语法错误,对合法输入数据能够产生满足要求的输出结果,对非法的输入数据能够都到满足规格说明的结果
- 可读性 便于阅读,理解,交流
- 健壮性 当输入的数据不合法时,算法也能做出相关处理,而不是产生异常或莫名奇妙的结果
- 时间效率高和存储量低
算法效率的度量方法
- 事后统计方法;这种方法主要通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定效率的高低
有种种缺陷,我们不考虑 - 事前分析估算法 :在计算机程序编制前,依据统计方法对算法进行估算
1.算法采用的策略,方法
- 编译产生的代码质量
- 问题的输入规模
4, 机器执行指令的速度
算法时间复杂度
- 大O记法
- 常数阶 O(1),与n无关
- 线性阶 O(n), 一般与循环有关
- 对数阶 O(logn,) while可能
int count = 1;
while count < n)
{
count = count * 2;
/* 时间复杂度为O(1)的程序步骤序列 */
}
即2的x次=n,反解出x为logn相关
- 平方阶 循环嵌套 O(n^2)
- 常用的时间复杂度所耗费的时间从小到大依次是
常数阶 log阶 线性阶 n倍线性阶 平方阶 立方阶 2^n阶 n!阶 n^n阶
最坏情况与平均情况
- 例子 就是说 假如在一个数组中,找一个元素,最好的时间复杂度时O(1),最坏就是O(n),即在最后一个数组元素中找到,我们期望的,或是从概率的角度看,从中间找到这一情况就是平均情况,但我们一般复杂度的计算是指最坏情况
算法空间复杂度
算法很重要
线性表
- 线性表:零个或多个数据元素的有限序列
- 概念: 直接前驱元素,直接后继元素,位序
- 在较复杂的线性表中,一个元素可以由若干个数据组成
线性表的抽象数据类型
ADT 线性表 (List)
Data
线性表的数据对象集合为 {a1,a2 (⋯⋯,an },每个元素的类型均为 DataTYPe。其中,除第一个元素a₁外,每一个元素有且只有一个直接前驱元素,除了最后一个元素aₙ外,每一个元素 aₙ有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表L。
ListEmpty(L):若线性表为空,返回true,否则返回false。
ClearList(*L):将线性表清空。
GetElem(Lₚi,*e):将线性表L中的第i个位置元素值返回给e。
LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
ListInsert(*L,i,e):在线性表L中的第i个位置插入新元素e。
ListDelete(*L,i,*e) : 删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L):返回线性表L的元素个数。
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
int length; /* 线性表当前长度 */
}SqList;
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
/* 初始化顺序线性表 */
Status InitList(SqList *L)
{
L->length=0;
return OK;
}
/* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(SqList L)
{
if(L.length==0)
return TRUE;
else
return FALSE;
}
/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(SqList *L)
{
L->length=0;
return OK;
}
/* 初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(SqList L)
{
return L.length;
}
- 细细品味最基础的操作,记牢靠了
- 以上这些操作时最基本的,更复杂操作可以用基本操做来实现
线性表的顺序存储结构
- 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
- 通俗说一下,有一组类型形同的数据元素要占点地方
线性表的顺序存储的结构代码
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef struct
{
ElemType data[MAXSIZE]; /* 数组,存储数据元素 */
int length; /* 线性表当前长度 */
}SqList;
- 三个属性
- 存储空间的起始位置,线性表的最大存储量,线性表的当前长度
数组长度与线性表长度的区别
数组长度应大于等于线性表的长度,数组长度一般来说是不变的,线性表随插入·或·删除等操作实时改变
地址计算方法
存取时间性能为O(1),有这一特点的存储结构成为随机存取结构
顺序存储结构的插入与删除
获得元素操作
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值,注意i是指位置,第1个位置的数组是从0开始 */
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length==0 || i<1 || i>L.length)
return ERROR;
*e=L.data[i-1];
return OK;
}
插入操作
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if (L->length==MAXSIZE) /* 顺序线性表已经满 */
return ERROR;
if (i<1 || i>L->length+1)/* 当i比第一位置小或者比最后一位置后一位置还要大时 */
return ERROR;
if (i<=L->length) /* 若插入数据位置不在表尾 */
{
for(k=L->length-1;k>=i-1;k--) /* 将要插入位置之后的数据元素向后移动一位 */
L->data[k+1]=L->data[k];
}
L->data[i-1]=e; /* 将新元素插入 */
L->length++;
return OK;// 可以结合书上的算法思路来理解
}
删除操作
/* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if (L->length==0) /* 线性表为空 */
return ERROR;
if (i<1 || i>L->length) /* 删除位置不正确 */
return ERROR;
*e=L->data[i-1];
if (i<L->length) /* 如果删除不是最后位置 */
{
for(k=i;k<L->length;k++)/* 将删除位置后继元素前移 */
L->data[k-1]=L->data[k];
}
L->length--;
return OK;
}
- 分析可得,综上,读数据时,时间复杂度为O(1),插入或删除时,时间复杂度为O(n),即适合元素个数不太变化,而更多是存取数据的应用
线性表顺序存储结构的优缺点
- 优点 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
可以快速地存取表中任一位置的元素 - 缺点 插入和删除需要移动大量元素
当线性表移动长度变化较大时,难以确定存储空间的容量
造成存储空间的“碎片”
线性表的链式存储结构
- 基础: 一个结点,分为数据域和指针域两部分组成,数据域里存储着数据信息,指针域里存储着指针或称作为链
- 链表的第一个结点的存储位置叫做头指针,最后一个结点的指针指向空,即null
- 在单链表的第一个结点前附设一个结点,称为头结点
- 头指针与头结点的形象描述看53页图片,好看的
- 结点由存放数据元素的数据域和存放后继结点地址的指针域组成
- p->data为一个数据元素,p->next为一个指针,即可以为a(i)的指针域,即a(i)的指针域指向a(i+1),即a->next->data为a(i+1)的数据域!
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; /* 定义LinkList */
单链表的读取
/* 初始条件:链式线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:用e返回L中第i个数据元素的值 */
Status GetElem(LinkList L,int i,ElemType *e)
{
int j;
LinkList p; /* 声明一结点p */
p = L->next; /* 让p指向链表L的第一个结点 */
j = 1; /* j为计数器 */
while (p && j<i) /* p不为空或者计数器j还没有等于i时,循环继续 */
{
p = p->next; /* 让p指向下一个结点 */
++j;
}
if ( !p || j>i )
return ERROR; /* 第i个元素不存在 */
*e = p->data; /* 取第i个元素的数据 */
return OK;
}
- 由以上可以看出,查找比较麻烦,且不知道具体是那个,所以我们用while循环来便利
单链表的插入和删除
- 插入 经典语句
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
- 这个顺序很重要,新手必要搞反了
/* 初始条件:链式线性表L已存在,1≤i≤ListLength(L), */
/* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
Status ListInsert(LinkList *L,int i,ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while (p && j < i) /* 寻找第i个结点 */
{
p = p->next;
++j;
}
if (!p || j > i)
return ERROR; /* 第i个元素不存在 */
s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */
s->data = e;
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
return OK;
}
- 删除 经典语句
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
/* 初始条件:链式线性表L已存在,1≤i≤ListLength(L) */
/* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
Status ListDelete(LinkList *L,int i,ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while (p->next && j < i) /* 遍历寻找第i个元素 */
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
return ERROR; /* 第i个元素不存在 */
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
*e = q->data; /* 将q结点中的数据给e */
free(q); /* 让系统回收此结点,释放内存 */
return OK;
}
- 综上可以看出,当进行删除或者是插入时,只有一开是复杂度是o(n),而如果插入10个加入,后续的复杂度都是o(1)。
单链表的整表创建
头插法
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /* 先建立一个带头结点的单链表 */
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
p->next = (*L)->next;
(*L)->next = p; /* 插入到表头 */
}
}
尾插法
/* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
*L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r=*L; /* r为指向尾部的结点 */
for (i=0; i<n; i++)
{
p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next=p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
单链表的整表删除
/* 初始条件:链式线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next; /* p指向第一个结点 */
while(p) /* 没到表尾 */
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL; /* 头结点指针域为空 */
return OK;
}
单链表结构与顺序结构存储结构的优缺点
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构
- 当线性表中的元素个数变化较大或者根本不知道有多大时,做好用单链表结构
静态链表
- 一些早期的高级编程语言没有指针,他们便想到用数组来来代替指针描述链表,即数组中一个存打他,一个存next,存next的下标我们称为cur,即游标。
/* 线性表的静态链表存储结构 */
typedef struct
{
ElemType data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
- 初始化一个
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
- 具体的请看书63页图
静态链表的插入操作
- 需要解决的问题,如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放
- 每当进行插入时,可以从备用链表上取得第一个结点作为待插入的新节点,一下是代码实现
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的ur */
return OK;
}
return ERROR;
}
静态链表的删除操作
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
静态链表的删除
- 不一定用得上,但思考方式很巧妙,应理解其思想,以备不时只需。
循环链表
- 解决了一个问题,如何从当中一个结点出发,访问到链表的全部结点
- 把两个循环链表合并
p=rearA->next; /* 保存A表的头结点,即① */
rearA->next=rearB->next->next; /* 将本是指向B表的第一个结点(不是头结点)*/
/* 赋值给reaA->next,即② */
q=rearB->next;
rearB->next=p; /* 将原A表的头结点赋值给rearB->next,即③ */
free(q); /* 释放q */
双向链表
- 空间换时间,有良好的对称性,使得把某个结点的前后结点的操作带来了方便,可以有效提高算法的时间性能。
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /*直接前驱指针*/
struct DuLNode *next; /*直接后继指针*/
} DulNode, *DuLinkList;
p->next->prior = p = p->prior->next
s - >prior = p; /*把p赋值给s的前驱,如图中①*/
s -> next = p -> next; /*把p->next赋值给s的后继,如图中②*/
p -> next -> prior = s; /*把s赋值给p->next的前驱,如图中③*/
p -> next = s; /*把s赋值给p的后继,如图中④*/
p->prior->next=p->next; /*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior=p->prior; /*把p->prior赋值给p->next的前驱,如图中②*/
free(p); /*释放结点*/