基础:两个基础,主要是结构体和内存(malloc)要掌握,要多敲代码。
学习方法:第一步:先听并主动思考;第二部:尝试练习;第三部:多敲代码练习
结构体的作用:分类排列,使程序更有结构性,在大量程序时使用。
数据元素之间存在的某种关系:线性关系,层次关系,网状关系
学习数据结构的意义:1、提高编程能力,训练思维,数据结构比较抽象;2、开发中,使用数据结构使开发成品的可复用性,可维护性,可读行以及开发效率都会清晰提高。
数据结构研究计算机数据间关系;
包括数据的逻辑结构和存储结构及其操作;
数据:数据即信息的载体,是能够输入到计算机中并且被计算机识别、存储和处理的符号总成;
数据元素:是数据的基本单位,又称之为记录。一般,数据元素由若干基本项(或称字段、域属性)组成;(实际数据结构就是研究数据元素之间的关系)
表示数据运算之间的抽象关系
按每个元素可能具有的直接前趋数和直接后继数将数据结构分为“线性结构”和“非线性结构”两大类。
集合——元素属于集合,所有元素都属于一个集合,没有其他关系,就叫集合
线性结构——一个对一个,如线性表、栈、队列
树形结构——一个对多个,如树(前趋有一个后继可以有很多个)
图状结构——多个对多个(也叫网状结构)前趋和后继都可以有很多个
存储结构:逻辑结构在计算机中的具体实现方法
存储结构是通过计算机语言所编制的程序来实现的,因而是依赖具体的计算机语言的。
顺序存储(Sequential Storage):将数据结构中各元素按照其逻辑顺序存放于存储器的一片连续的存储空间中。
如一维数组,如表L=(a1,a2,.....,an)的顺序
链式存储(重点):将数据结构中各元素分布到存储器的不同点,用地址(或链指针)方式建立他们之间的联系;比如:
数据结构中元素之间的关系在计算机内部很大程度上是通过地址或指针来建立的。
在存储数据的同事,建立一个附加的索引表,即索引存储结构=数据文件+索引表。
根据数据元素的特殊字段(称为关键字key),计算数据元素的存放地址;然后数据元素按地址存放。
线性表定义:线性表是包含若干数据元素的一个线性序列
记为L=(a0,....ai-1,ai,ai+1...an-1)
L为表明,ai(0<=i<=n-1)为数据元素;
n为表长,n大于0时,线性表L为非空表,否则为空表;
线性表L可用二元组形式描述:L = (D,R)
即线性表L包含数据元素集合D和关系集合R
D={ai | ai∈datetype , i = 0,1,2, ........n-1,n≥0}
R={
关系符
表示任意相邻的两个元素之间的一种先后次序关系
ai是ai+1的直接前趋,ai+1是ai的直接后继
例如:设有一个顺序表L={1,2,3,4,5,6}; 他们的关系图如下:
①——②——③——④——⑤——⑥
使用二元组描述L=(D,R)
D={1,2,3,4,5,6}{n=6}
R={<1,2>,<2,3>,<3,4>,<4,5>,<5,6>}
顺序表特征
①表头无前趋
②表尾无后继
③中间任意一个元素有且仅有一个直接前趋,有且仅有一个直接后继。
比如:若将线性表L=(a0,a1,......,an-1)中的各元素依次存储于计算机的一片联系的存储空间。
设LOC(ai)为ai的地址,Loc(a0)=b,每个元素占d个单元,则:Loc(ai)=b+i*d
顺序存储结构的特点
在逻辑上相邻的元素ai,ai+1,其存储位置也是相邻的。
对数据元素的查找比较方便快捷
存储密度高
存储密度D=(数据结构中元素所占存储空间)/(整个数据结构所占空间)
顺序存储结构缺点:对表的插入和删除等运算的时间复杂度较差,表的插入和删除运算用的时间较长
在c语言中,可借助于一维数组类型来描述线性表的顺序存储结构
例如:
#define N 100
typedef int data_t;//将data_t定义成int类型,方便后面修改使用
typedef struct//这个typedef意思为sqlist是struct结构体,*sqlink为结构体指针。
{
date_t data[N];//表的存储空间
int last;//顺序表的最后一个下标值
} sqlist,*sqlink;
sqlink list_create();//创建一个顺序表
{
sqlink L;
L = (sqlink)malloc(seziof(sqlist));
if (L == NULL)
return NULL;//申请空间失败
memset(L, 0, sizeof(sqlist));
L->last = -1;
}
设线性表L=(a0,a1,....an-1),对L的基本运算步骤一般为:
①建立一个空表:list_create(L)
sqlist *create(){
sqlink L//设置指针变量L
L = (sqlink)malloc(sizeof(sqlist));//为L申请空间
if (L == NULL){
printf("the list is error\n");
return L;
}
memset(L, 0, sizeof(sqlist));
L->last = -1;
return L;
}
②置空表:list_clear(L)
int list_clear(sqlink L) {
memset(L,0,sizeof(sqlist));
L->last = -1;
return 0;
}
③判断表是否为空:list_empty(L).若表为空,返回值为1,否则返回0
int list_empty(sqlink L) {
if (L->last == -1) {
return 1;
} else {
return 0;
}
}
④求表长:length(L)
int list_length(sqlink L) {
if (L == NULL) {
printf("malloc is faild\n");
return 0;
}
return (L->last+1);
}
⑤取表中某个元素:Getlist(L,i),即ai。要求0<=i<=length(L)-1
date_t list_Getlist(sqlink L, int i) {
if (L == NULL) {
}
if (L->last == -1) {
printf("the list is empty");
return 0;
}
if (i < 0 || i > last+1) {
printf("i is faild\n");
return 0;
}
return L->date[i-1];
}
⑥定位运算:Locate(L,x)。确定元素x在表L中的位置(或元素x的序号)。
当x在线性表L当中,返回x在线性表L中的位置的值即x的下标,否则返回-1.
int list_Locate(sqlink L,date_t vol) {
int i = 0;
if (L == NULL) {
printf("the L is empty\n");
return 0;
}
while(date[i] != vol) {
i++;
if (i > L->last) {
printf("the list not have vol\n");
return -1;
}
}
return i;
}
⑦插入:Insert[L,x,i).意思为将元素x插入到表L中第i个元素ai之前,且表长+1;
int list_insert(sqlink L,date_t vol,int i) {
int a;
if (L == NULL) {
printf("the list is NULL\n");
return -1;
}
a = L->last;
while (a > (i-1) || a == (i-1)) {
L->date[a+1] = L->date[a];
a--;
}
L->date[a] = vol;
L->last++;
return 0;
}
代码分层:一个顺序表一般包含三个文件,分别为一个sqlist.h文件(作用主要是定义函数及运算),两个为.c文件,例如:sqlist.c(实现函数) test.c(调用函数,作用主要是功能的实现)
这样写的好处是结构清晰,而且软件复用性高(软件复用主要体现在自己以后可以重复利用,包括现在的项目以及其他的项目,也可以共享给同事用)。
⑧删除:delate(L,i)删除表L中的第i个位置的内容,表长-1;
int list_delate(sqlink L, int i) {
if (L == NULL) {
printf("the list is NULL\n");
return -1;
}
while ((i-1) < L->last || (i-1) == L->last) {
L->date[i-1] = L->date[i];
i++;
}
L->last--;
return 0;
}
优点:存储密度高,能够随机存取
缺点:①要求系统提供一片较大的连续存储空间。
②插入、删除等运算耗时,且存在元素在存储器中成片移动的现象。
将线性表L = (a0, a1,......an-1)中各元素分布在存储器的不容存储块,称为结点,通过地址或指针建立元素之间的联系,例如:
结点date域存放数据元素ai,而next域是一个指针,指向ai的直接后继所在的结点。
单链式表存储通常设置一个头结点,方便查找
结点类型描述
typedef int date_t;
typedef struct node
{
date_t date;//结点的数据域,存放元素位置
struct node *next;//结点的后继指针域,指向下一个元素
}listnode,*linklist;//listnode A; linklist p = &A.
说明:listnode = typedef struct node listnode
*linklist = typedef struct node *linklist
例如:设指针p指向链表中结点ai
获取ai,写作p->date; 取ai+1写作:p->next->date;
若指针p的值为NULL,则它不指向任何结点,此时p->date或p->next->date是错误的,会出现段错误(一般出现段错误的原因是指针的非法使用或者地址越界等)
可以调用C语言中malloc()函数向系统申请结点的存储空间,例如:
链表创建函数:
linklist list_create() {
linklist p;
p = (linklist)malloc(sizeof(listnode));
//则创建一个类型为linklist的结点,且该节点的地址已经存入指针变量p中
if (p == NULL) {
printf("malloc is faild\n");
return -1;
}
p->next = NULL;
p->date = 0;
return p;
}
链表判断是否为空函数:为空返回1,否则返回0
int list_empty(linklist P) {
if (p == NULL) {
printf("the list is NULL\n");
return -1;
}
if (p->next == NULL) {
return 1;
} else {
return 0;
}
}
置空链表
int clear(linklist P) {
linklist q;
if (P == NULL) {
printf("P is NULL\n");
return -1;
}
q = p;
while(q->next != NULL) {
q->date = 0;
q = q->next;
}
p->date = 0;
p->next = NULL;
free(p);
}
链表末尾插入函数:
linklist list_insert(linklist P,date_t vol) {
linklist q,n;
q = (linklist)malloc(sizeof(listnode));
if (q == NULL) {
printf("malloc q faild\n");
return -1;
}
q->date = vol;
q->next = NULL;
n = P;
while(n->next != NULL) {
n = n->next;
}
n->next = q;
return = P;
}
链表插入函数:
linklist list_insert(linklist P,date_t vol,int i) {
if (P == NULL) {
printf("the P is NULL\n");
return -1;
}
linglist q,m;
m = P;
q = (linklist)malloc(sizeof(listnode));
if (q == NULL) {
printf("malloc is faild\n");
return -1;
}
int a,b=0;
` q->next = NULL;
q->date = vol;
while(m != NULL) {
m = m->next;
b++;
}
if (i > b) {
printf("the num is faild\n");
return -1;
}
if (P->next == NULL) {
P = q;
}else {
for (a = 0;a < i;a++) {
m = m->next;
}
m->next = q->next;
m->next = q;
}
return P;
}
栈的定义:限制在一端进行插入操作和删除操作的线性表(俗称堆栈)
允许进行操作的一端称为栈顶,另一固定端称为栈底
当栈中没有元素时称为“空栈”。特点是:后进先出;
顺序栈是顺序表中的一种,具有顺序表同样的存储结构,由数组定义,配合用数组下标表示的栈顶指针(即相对指针)完成各种操作;
typedef int date_t;//定义栈中数据元素的数据类型
typedef struct
{
date_t *date;//用指针指向栈的存储空间
int maxlen;//当前栈的最大元素个数
int top;//指示栈顶位置(数组下标)的变量
}sqstack;//顺序栈 类型定义
插入操作和删除操作均在链表头部进行,链表尾部就是栈底,栈顶指针就是头指针。
typedef int date_t;//定义栈中数据元素数据类型
typedef struct node_t{
date_t date;//数据域
struct node_t *next;//链接指针域
}linkstack_t;//链栈类型定义
队列是限制在两端进行插入操作和删除操作的线性表
允许进行存入操作的一端称为队尾
允许尽心删除操作的一端称为队头
当线性表中没有元素时,称为空队
特点是:先进先出
typedef int date_t;//定义队列中的数据元素的数据类型
#define N 120//定义队列的大小
typedef struct//创建结构体
{
date_t date[N];//创建数据
int front,rear;//指示队头位置和队尾位置的指针
}sqlink;//顺序队列类型定义
front指向队头元素的位置
rear指向队尾元素的下一个位置
在队列操作过程中,为了提高效率,以调整指针代替队列元素的移动,并将数组作为循环队列的操作空间
为区别空队和满队,满队元素个数比数组元素个数少一个。
在链式队列中,插入操作在队尾进行,删除操作在队头进行,由队头指针和队尾指针控制队列的操作。
typedef int date_t;
typedef struct node_t
{
date_t date;
struct node_t *next;
}linknode_t, *linklist_t;
typedef struct
{
linklist_t front, rear;
}linkqueue_t;
树是n(n≥0)个节点的有限集合T,它满足两个条件:
有且仅有一个特定的称为根(Root)的节点
其余节点可以分为m(m≥0)个互不相交的有限结合T1、T2、......、Tm,其中每一个集合又是一颗树,并称为其根的子树。
表示法:树形表示法,目录表示法
一个节点的子树的个数称为该节点的度数
一棵树的度数是指该树中节点的最大度数。
度数为零的节点称为树叶或终端节点
度数不为零的节点称为分支节点
除根节点外的分支节点称为内部节点。
一个几点系列k1,k2,......,ki,ki+1,......kj,并满足ki是ki+1的父节点,就称为一条从k1到kj的路径
路径的长度为j-1,即路径中的边数。
路径中前面的节点是后面节点的祖先,后面节点是前面节点的子孙
节点的层数等于父节点的层数加一,根节点的层数定义为1.
树中节点层数的最大值称为该树的高度或深度。
若树中每个节点的各个子树的排列为从左到右,不能交换,即兄弟之间是有序的,则该树称为有序树
m(m≥0)颗互不相交的树的集合称为森林
树去掉根节点就称为森林,森林加上一个新的根节点就称为一个树
树中任何节点都可以有零个或多个直接后继节点(子节点),但最多只有一个直接前趋节点(父节点),根节点没有前趋节点,叶节点没有后继节点。
二叉树:是n(n≥0)个节点的有限集合
或者是空集(n = 0)
或者是由一个根节点以及两棵互不相交、分别对称为左子树和右子树组成
严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
二叉树性质:二叉树第i层上的节点最多为2i-1个(2的i次方减一个)
深度为k(k≥1)的二叉树最多有2的k次方减一个节点。
满二叉树:深度为k(k≥1)时有2的k次方减一个节点的二叉树。
完全二叉树:只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置。
具有n个节点的完全二叉树的深度为
(log2n)+1或log2(n+1).
顺序存储结构:完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点。设完全二叉树的节点数为n,某节点编号为i;
当i>1(不是根节点)时,有父节点,其编号为i/2;
当2*i≤n时,有左孩子,其编号为2*i,否则没有左孩子,本身是叶节点。
当i为奇数且不为1时,有左兄弟,其编号为i-1,否则没有左兄弟;
当i为偶数且小于n时,有右兄弟,其编号为i+1,否则没有右兄弟;
有n个节点的完全二叉树可以用有n+1个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为0的元素不用。
利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树可以通过添加虚节点构成完全二叉树,然后用数组存储,这要浪费一些存储空间。
typedef int date_t;//定义新的数据类型
typedef struct node_t
{
date_t date;
struct node_t *lchild, *rchild//两个指针分别指向左右孩子
}bitree_t;//结构体名称
bitreet *root;
//二叉树由根节点指针决定。
沿某条搜素路径周游二叉树,对树中的每一个节点访问依次且仅访问一次。
二叉树是非线性结构,每个节点有两个后继,则存在如何遍历即按照什么样的搜索路径进行遍历的问题
由于二叉树的递归性质,遍历算法也是递归的。三种基本的遍历算法如下:
先访问树根,再访问左子树,最后访问右子树;
先访问左子树,再访问树根,最后访问右子树;
先访问左子树,再访问右子树,最后访问树根;
若二叉树为空树,则空操作;否则
访问 根节点
先序遍历左子树
先序遍历右子树
设记录表L = (R1R2......RN),其中Ri(1≤i≤n)为记录,对给定的某个值k,在表L中确定key=k的记录的过程,称为查找。
若表L中存在一个记录了Ri的key=k,记为Ri.key=k,则查找成功,返回该记录在表中的序号i(或Ri的地址),否则(查找失败),返回0(或地址NULL)。
查找方法有:顺序查找、折半查找、分块查找、Hash表查找等等。查找算法的优劣将影响到计算机的使用效率,应根据应用场合选择相应的查找算法。
对于查找算法,主要分析器Y(n)。查找过程是key的比较过程,时间主要耗费在各记录的key与给定k值的比较上。比较次数越多,算法效率越差(即T(n)量级越高),故用“比较次数”刻画算法的T(n)。
一般以“平均查找长度”来衡量T(n)。
平均查找长度ASL:对给定k,查找表L中记录比较次数的期望值(或平均值),即:
Pi为查找就安排Ri的概率。等概率情况下Pi=1/n;Ci为查找Ri时key的比较次数(或查找次数)。
算法思路
对给定值k,逐步确定待查记录所在区间,每次将搜索空间减少一半,知道查找成功或失败为止。
设两个游标low、high,分别指向当前查找表的上界(表头)和下界(表尾)。mid指向元素中间。
设记录表长为n,将表的n个记录分为b=[n/s]个块,每块s个记录(最后一块记录数可以少于s个),即
且表分块有序,即第i(1≤i≤b-1)块所有记录的key小于第i+1块中记录的key,但块内记录可以无序。
理想的查找方法是:对给定的k,不经任何比较便能获取所需的记录,其查找的时间复杂度为常数及O(C);
这就要求在建立记录表的时候,确定记录的key与其存储地址之间的关系f,即使key与记录的存放地址H相对应:比如
当要查找key=k的记录时,通过关系f就可得到相应记录的地址而获取记录,从而免去了key的比较过程。
这个关系f就是所谓的Hash函数(或称散列函数、杂凑函数),记为H(key)。
它实际上是一个地址映像函数,其自变量为记录的key,函数值为记录的存储地址(或称Hash地址)
不同的key可能得到同一个Hash地址,即当key1!=key2时,可能有H(key1)= H(key2),此时称key1和key2为同义词。这种现象称为“冲突”或碰撞,因为一个数据单位只可存放一条记录。(小概率事件)
一般选取Hash函数只能做到使冲突尽可能减少,确不能完全避免,这就要求在出现冲突之后,寻求适当的方法来解决冲突记录的存储问题
Hash表的查找
根据选取的Hash函数H(key)和处理冲突的方法,将一组记录(R1R2......Rn)映像到记录的存储空间,所得到的记录表称为Hash表,如图:
选取(或构造)Hash函数的方法很多,原则是尽可能将记录均匀分布,以减少冲突现场的发生。一下介绍几种常用的构造方法。
直接地址法
平方取中法
叠加法
保留除数法
随机函数法
保留除数法又称质数除余法,设Hash表空间长度为m,选取一个不大于m的最大质数p,令:H(key)=key%p
若取p=19,同样对上面给定key集合K,有:
key:28 35 63 77 105
H(key)=key%19: 9 16 6 1 10
H(key)的随机度就好多了。
稳定排序和非稳定排序
设文件f=(R1......Ri......Rj)中记录Ri、Rj(i≠j,i、j = 1......n)的key相等,即Ki = kj;
若在排序前Ri领先于Rj,排序后Ri仍领先于Rj,则称这种排序是稳定的,其含义是没有破坏原本已有序的次序。
内排序和外排序
若待排文件在计算机的内存储器中,且排序过程也在内存中进行,称这种排序为内排序。
若排序中的文件存入外存储器,排序过程借助于内外存数据交换(或归并)来完成,则称这种排序为外排序。
内排序可以归纳为以下五类:
插入排序
交换排序
选择排序
归并排序
插入排序有以下几种排序方式:
直接插入排序
折半插入排序
链表插入排序
Shell(希尔)排序
......