数据结构知识点整理
数据结构
一、什么是数据结构
-
数据结构是一门研究非数值计算的程序设计问题中计算机的数据元素以及他们之间的关系和运算的学科。
-
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。
-
数据结构是一门研究非数值计算的程序设计问题的学科,主要研究数据的逻辑结构、存储结构和运算方法。
-
数据结构被定义为(D,R)D是数据的有限集合,R是D上的逻辑关系的有限集合。
二、数据的逻辑结构
1、基本概念
- 数据 是描述客观事物的数字、字符以及所有能输入到计算机中并被计算机接受的各种符号集合的统称。
- 数据元素(节点) 一个事物的一组数据,是数据集合中的一个“个体”,是数据的**基本单位*
- 数据项(字段)数据不可分割、具有独立意义的最小数据单位,对数据元素属性的描述
- 数据结构 相互之间存在一种或多种特定关系的数据元素集合
- 数据对象 性质相同的数据元素的集合,是数据的一个子集
- 关键字 一个数据元素中,能够识别该元素的一个或多个数据项
- 主关键字 能够唯一识别数据元素的关键字
2、逻辑结构分类
- 集合 结构中的数据元素除了同属于一种类型外,别无其它关系。(不存在数据之间的关系)
- 线性结构 数据元素之间一对一的关系
-
非线性结构 数据元素之间存在多对多的关系
- **树形结构 ** 数据元素之间一对多的关系
- 图形结构 数据元素之间多对多的关系
三、数据的存储结构
1、基本概念
数据元素及其关系在计算机中的存储表示或实现称为数据的存储结构,也称为物理结构
2、存储结构形式
-
顺序存储
占用连续存储单元,借用元素在存储器中的相对位置来表示数据元素之间的逻辑关系 [最简单的实现:一维数组]
-
链式存储
借助知识元素存储地址的指针来表示数据元素之间的逻辑关系
特点:数据元素不一定存在地址连续的存储单元,存储处理得灵活性较大
-
索引存储
原有存储数据结构的基础上,附加建立一个索引表,索引表的每一项都由关键字和地址组成(索引表反映了按某一个关键字递增或递减排列的逻辑次序)
作用:提高数据的检索速度
-
散列存储
构造散列函数来确定数据存储地址或查找地址
散列存储的内存存放形式称为散列表,也称为哈希表
四、算法
1、基本概念
算法是对特定问题求解步骤的一种描述,是指令的有限序列。其中每一条指令表示一个或多个操作。
2、算法的特性
- 有穷性:一个算法必须在有限的步骤之后结束,并且每一步在有限时间内完成。
- 稳定性:算法的每一条指令必须在有确切的定义,无二义性。
- 可行性:算法所描述的操作可以通过有限次的运算来实现,并得到正确的结果。
- 输入:一个算法具有零个或多个输入。
- 输出:一个算法具有一个或多个输出。
3、算法的目标
- 正确性:算法的执行结果应当满足预先设定的功能和要求。
- 可读性:一个算法应当思路清晰、层次分明、易读易懂。
- 健壮性(鲁棒性):当发生误操作输入非法数据时,应能适当的反应和处理,不至于引起莫名其妙的后果
- 高效性:对同一个问题,执行的时间越短,算法的效率就越高。
- 低存储量:完成相同的功能,执行算法所占用附加存储空间尽可能的少。
4、算法的效率的度量
算法效率的度量可以分为事先估算法和事后统计法。
5、算法效率 --时间复杂度
算法中原操作重复执行的次数的规模n的某个函数f(n),算法的时间复杂度T(n)的数量级可记作: T(n) = 0(f(n))
他表示随着问题规模的扩大,算法执行时间的增加率和f(n)的增长率相同,称为算法的渐进时间复杂度,简称时间复杂度。
6、算法效率 --空间复杂度
一个程序的空间复杂度是指程序运行从开始到结束所需要的存储空间。类似于算法的时间复杂度,吧算法所需存储空间的量度记作: S(n) = 0(f(n))
五、线性表
1、基本概念
线性表的插入、删除操作不受限制。
线性表是由n(n>=0)个类型相同的数据元素a0,a1,…an-1组成的有限序列,记作(a0,a1,…an-1),其中,元素ai的数据类型可以是整数、浮点数、字符或类;n是线性表的元素个数,称为线性表长度。若n=0,则为空表;若n>0,则ai<0<i<n-1)有且仅有一个前驱元素ai-1和后继元素ai+1,a0没有前驱元素,an-1没有后继元素
2、顺序表
2.1 顺序表定义
线性表的顺序存储是用一组地址连续的存储单元依次存储线性表的数据元素,我们把这种形式存储的线性表称为顺序表。顺序表中各个元素的物理顺序和逻辑顺序是一致的。 随机存取指访问时可以按下标随机访问,存储和存取是不一样的。如果是存储,则是指按顺序的,如果是存取,则是可以随机的,可以利用元素下标进行
设a1的存储地址LOC(a1)为首地址B,每个元素占d个存储单元,则i个数据元素的地址为:
LOC(ai)=LOC(a1)+(i-1)x d 1<=i<=n
即: LOC(ai)=B+(i-1)x d 1<=i<=n
只要知道顺序表首地址和每个数据元素所占存储单元的个数,就可以求出第i个数据元素的存储地址。顺序表具有按数据元素的序号随机存取的特点。
2.2 顺序表的基本运算
-
插入运算
表长为n的表,在i (1<=i<=n+1)处插入新结点,之后结点需要后移动n-i+1个元素 平均时间复杂度 O(n)
【1、检查表是否已满 2、插入的位置是否有效 3、必须从表的最后一结点开始移动】
-
删除运算
表长为n的表,在i (1<=i<=n)处删除结点,之后结点需要前移动n-i个元素 平均时间复杂度 O(n)
【1、检查表是否为空 2、删除的位置是否有效】
-
查找运算
元素下标查找,速度最快 时间复杂度O(1)
按值查找 需进行元素值的比较,查找整个表 时间复杂度O(n)
3、线性链表
3.1 线性链表定义
(1)用一组任意的存储单元存储线性表的数据元素,存储单元可以是连续的,也可以不连续,因此,链表中结点的逻辑次序和物理次序不一定相同。
(2) 为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址。data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。不需要事先估计存储空间大小。
3.2 单向链表
每个结点的存储地址是存放在其前驱结点next域中,而开始结点无前驱,故应设头指针head指向开始结点(头结点)。同时,由于最后一个结点无后继,故结点的指针域为空,即NULL。头插法建表(逆序)、尾插法建表(顺序)。增加头结点的目的是算法实现上的方便,但增大了内存开销。
- 单向链表的存取必须从头指针开始
- 头指针:指向链表中第一个结点(头结点或无头结点时的开始结点)的指针。
- 头结点:在开始结点之前附加一个结点。
- 开始结点:在链表中,存储第一个数据元素(a1)的结点。
- 判空:p->next = NULL;
- 缺点:只能顺时针往后寻找其他结点,若要寻找结点的前驱,则需要从表头指针出发。
3.21 单向链表的基本运算
-
插入运算
-
头插法建表(逆序)
从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头结点之后,直至读入结束标志为止
//伪代码 LinkNode *head; //头指针 var x = "数值" LinkNode s = new LinkNode() s->data = x; //数值存到新结点的数据域中 s->next = head; //将头指针所指向的下一个结点的地址,赋给新创建结点的next head = s; //头指针指向s结点
//2,3步颠倒导致新结点地址域指向自己
2. **尾插法建表(顺序)[常用]** 将新结点插到当前单链表的表尾上。增加一个尾指针,使之指向当前单链表的表尾 ```c //伪代码 LinkNode *real; //尾指针 var x = "数值" LinkNode s = new LinkNode() s->data = x; //数值存到新结点的数据域中 real.next = s; //尾指针指向下一个结点的地址 real = s; //当前新结点的地址赋予尾指针 //2.3颠倒导致尾指针不移动
-
-
删除结点
删除p指向线性链表中某结点
(需要查找到p的前驱结点 [时间复杂度 O(n)] )
// 伪代码 LinkNode p; //假定p就是要删除的结点 LinkNode q; //q是的p的前驱结点 q-next = p-next; //q的下一结点直接指向p的下一结点 delete(p); //回收删除结点p
若要删除p的后继结点(假设存在),则可以直接完成
s = p->next; //p的后继结点 p->next = s-next; //p的下一结点指向后两位置的结点 delete(s);
-
查找运算
按序号查找 从链表的第一个元素开始判断当前结点是否是第i个,
时间复杂度O(n)。
按值查找 从链表的第一个元素开始判断当前结点的值是否等于x,
时间复杂度O(n)。
3.3 单循环链表
一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于某一指定指针,如头指针或尾指针等。
3.4 双向链表
在单链表的每个结点里再增加一个指向其直接前驱的指针域。这样就形成的链表中有两个方向不同的链。双向链表一般由头指针唯一确定的,将头结点和尾结点链接起来构成循环链表,并称之为双向链表。
双向链表有一个数据域和两个指针域组成 结点(头指针,数据域,尾指针)
3.41 双向链表的基本运算
-
插入运算
先搞定插入节点的前驱和后继,再搞定后结点的前驱,最后搞定前结点的后继。
//伪代码 linkNode p; //新结点 linkNode q; //前驱结点 p->front = q; //p的头指针指向q p->rear = q->rear; //p的尾指针指向q的尾指针(q的后继结点) q->rear->front = p; //q的后继结点的头指针指向p q->rear = p; //q的后继结点指向p
-
删除运算
可以直接删除当前指针所指向的节点。而不需要像单向链表中,删除一个元素必须找到其前驱
//伪代码
linkNode p; //待删除的结点
p->front->rear = p->rear; //p的前驱结点尾指针指向p的后驱结点
p->rear->front = p->front; //p的后驱结点的头指针指向p的前驱结点
delete p; //删除p结点
4、存储密度
-
存储密度是指结点数本身所占的存储空间大小和整个结点结构所占的存储空间之比,即
存储密度 = 结点数据占的存储位 / 整个实际分配的存储位
由此可见:顺序表的存储密度等于1,链表的存储密度小于1。
-
链式存储比顺序存储占用更多存储空间,原因是链式存储结构增加了存储其后继结点地址的指针域
-
存储空间完全被结点值占用的存储方式称为紧凑存储,否则称为非紧凑存储。
-
存储密度值越大,标识数据所占的存储空间越少。
5、顺序表与链性表对比
-
顺序表
优点:随机存储,密度大(存储空间占用少)
缺点:表的扩充困难,插入、删除要操作大量元素移动
-
链性表
优点:表扩充简单,插入、删除效率高。
缺点:查找效率低,密度小(存储空间占用多)
六、栈
1、基本概念
栈又称为堆栈,是一种特殊的,只能在表的一端进行插入、删除操作的线性表。
栈的逻辑结构和线性表相同,其特点是按”后进先出“的原则进行操作,而且栈的操作被限制在栈顶进行,是一种运算受限制的线性表,允许插入、删除的这一段称为栈顶,另一端称为栈底。
2、顺序栈
2.1 顺序栈定义
利用顺序存储方式实现的栈称为顺序栈。顺序栈是利用地址连续的存储单元依次从栈底到栈顶的元素,同时附设栈顶指针来指示栈顶元素在栈中的位置。
2.2 顺序栈的基本算法
顺序栈的空间分配及初始化,即给顺序栈分配内存空间,并设置好栈顶指针top的初始值-1,构造一个空栈的过程
-
用一维数组实现顺序栈
#define MAXLEN 10 //分配最大栈空间 datatype data[MAXLEN] //datatype 可根据用户需要定义类型 int top; //定义栈顶指针
-
用结构体数组实现顺序栈
#define MAXLEN 10 //分配最大栈空间 typedef stuct //定义结构体 { datatype data[MAXLEN] //datatype 可根据用户需要定义类型 int top; //定义栈顶指针 }SeqStack;
-
进栈
进栈运算是在栈顶位置插入一个新元素x,其算法步骤为:
-
判断栈是否为满,若栈满,作溢出处理。
-
若栈未满,栈顶指针top加1。
-
将新元素x送入栈顶
//伪代码 SeqStack s = new SeqStack(); s.top = -1 //定义栈且初始化栈顶指针 datatype x = "某个引用"; if(MAXLEN-1 = s.top){ //栈满 }else{ s.top++; s.data[s.top] = x; //栈不满则进栈 }
-
-
出栈
出栈运算是指去除栈顶元素,付给某一个指定变量x,其算法步骤为:
-
判断栈是否为空,若栈空,做下溢处理
-
若栈非空,将栈顶元素赋给变量x
-
指针top减1
-
//伪代码
SeqStack s; //栈
datatype x;
if(-1==s.top){
//栈空
}else{
s.data[s.top] = x;
s.top--;
}
3、链栈
3.1 链栈定义
链式存储结构。插入和删除操作仅限制在链头位置上进行。栈顶指针就是链表的头指针。通常不会出现栈满的情况。 不需要判断栈满但需要判断栈空。
通常就用单向链表来实现,由于栈中的操作只能在栈顶进行,所以链表的头部做栈顶是最合适的。
typedef struct stacknode
{
datatype data; //定义栈元素
struct stacknode *next; //定义指向下一个结点的指针
}StackNode;
typedef struct
{
StackNode *top; //定义栈顶指针top
}LinkStack; //定义栈链类型
3.2 链栈的基本算法
链栈的空间分配及初始化,设置好栈顶指针top的初始值NULL,构造一个空栈的过程(不必分配内存)
-
进栈
//伪代码 LinkStack s = new LinkStack(); s.top = null //定义栈且初始化栈顶指针 datatype x = "某个引用"; StackNode *p = new StackNode(); //申请一个新结点空间 p->data = x; p->next = s.top; //将新结点插入到栈顶(p地址域指向上一个结点) s.top = p //修改栈顶指针(指针指向当前结点)
-
出栈
//伪代码
SeqStack s; //栈
datatype x;
if(null==s.top){
//栈空
}else{
StackNode *p = s.top; //定义临时指针p指向栈顶结点
s = p->data; //获取结点值
s.top = p->next; //修改栈顶指针 (指向p的前驱结点)
delete p;
}
4、顺序栈与链栈对比
-
顺序栈
优点:密度大,存储空间占用少
缺点:扩容困难,容易出现栈满
-
链栈
优点:通常不出现栈满的情况
缺点:密度小,存储空间占用多
5、应用例子
七、队列
1、基本概念
队列是一种运算受限的线性表,限制在两个端点进行插入和删除操作的线性表,允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。先进先出(FIFO)。
2、顺序队列
2.1 顺序队列定义
顺序存储结构。当头尾指针相等时队列为空。在非空队列里,头指针始终指向队头前一个位置,而尾指针始终指向队尾元素的实际位置
2.2 顺序队列的基本算法
顺序队列的空间分配及初始化,即给顺序队列分配内存空间,并设置好队头指针与队尾的初始值为-1,构造一个空队列的过程
#define MAXLEN 10 //队列的最大容量
typedef struct
{
datatype Q[MAXLEN];
int front; //定义头指针
int rear; //定义尾指针
}SeqQueue;
SeqQueue *q;
q = new SeqQueue();
//初始化
q.front = -1;
q.rear = -1;
-
入队
if(MAXLEN == q.rear-q.front){ //队满 }else{ q.rear++; //先队尾指针加1 q.Q[q.rear] = x; //元素x进队 }
-
出队
if(q.rear == q.front){ //队空 }else{ q.front++; //先将队头指针加1 x= q.Q[q.front]; //队头元素赋值给x,x在返回出去 }
3、循环顺序队列
3.1 循环顺序队列定义
随着入队出队,猪呢个队列会整体向上移动,这样就造成了队尾指针虽然已经移到最上面,而队列却未真满,这种
现象叫 “假溢出”
为了解决上诉队列的“假溢出”现象,一个方法是将队列的数据区Q[0…MAXLEN-1]看成是首尾相连的环,即将表示队首的Q[0]单元与表示队尾的Q[MAXLEN-1]单元从逻辑上连接起来,形成一个环形表,这就形成循环顺序队列
如无特殊说明,一般情况下都直接将循环顺序队列简称为顺序队列
3.2 循环顺序队列基本算法
-
队空
q.front == q.rear //队空
-
队满
(q.rear+1) %MAXLEN == q.front //队满
-
入队
q.rear = (q.rear+1)%MAXLEN;
-
出队
q.front = (q.front+1)%MAXLEN;
4、链队列
4.1 链队列定义
链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
4.2 链队列基本算法
具有链式存储结构的队列称为链队列,实际上它是一个带有头指针和尾指针的单向链表,该链表一般没有头结点。
链队列的头指针和尾指针使两个独立的指针变量,由于他们分别指向同一个单链表中的首尾结点,所以链队列的整体结构考虑,一般将这两个指针封装在同一个结构体中。
typedef struct linkqueuenode
{
datatype data; // datatype 为链队列中元素的类型
struct linkqueuenode *next;
}LinkQueueNode; // 链队列结点的类型为LinkQueueNode
typedef struct
{
LinkQueueNode *front, *rear; //队头队尾指针的定义
}LinkQueue; // 链队列的类型为LinkQueue
-
初始化
q.front =NULL; //头指针为空 q.rear =NULL; //尾指针为空
-
入队
LinkQueueNode *p = new LinkQueueNode; //开辟新的结点空间 p->data = x; p->next = NULL; if(NULL == q.front){ //若队列为空,则队头直接指向新结点 q.front = p; }else{ //否则将新结点插入队尾 q.rear->next = p; } q.rear = p; //更改队尾指针
-
出队
LinkQueueNode *p = q.front; //获取即将出队结点
if(NULL == p){
//空队列
}else{
q.front = q->next; //否则,将队头结点从队列中断开
if(NULL == q.front){ //若出队是队列的最后一个结点,则将队尾为置空
q.rear = NULL;
}
x = p-> data;
delete p;
}
5、应用例子
八、串
1、串的概念
串(String)是零个或多个字符组成的有限序列 。一般记作:
s = “a1a2…aian”
其中,s是串名,用引号括起来的字符序列为串值,ai(1<=i<=n)是一个任意字符,它称为串的元素,是构成元素的基本单位;i是它在整个串中的序号;n为串的长度,表示串中所包含的字符的字符个数。
2、几个术语
- 长度:串中字符的个数,称为串的长度。
- 空串:长度为零的字符串称为空串。
- 空格串:有一个或朵儿连续空格组成的串称为空格串。
- 串相等:两个串是相等的,是指两个串的长度相等且对应的字符都相等。
- 子串:串中任意连续的字符组成的子序列称为该串的子串。
- 主串:包含子串的串称为该子串的主串。
- 模式匹配:子串的定位运算又称为串的模式匹配,是一种求子串的第一个字符在主串中序号的运算。被匹配的主串称为目标串,子串称为模式。
九、数组和广义表
十、树和二叉树
1、树的定义
树【递归】是n(n>=0)个有限数据元素的集合。在任意一棵非空树T中:
有且仅有一个特定的称为树根的结点(根结点无前驱结点)
当n>=1时,出根结点之外的其余结点被分成m(m>0)个互不相交的集合T1,T2…,Tn,其中每一个集合Ti (1<=i<=m)本身又是一棵树,并且称为根的子树。
2、基本术语
- 结点:树的结点包含一个数据及若干指向其子树的分支。
- 结点的度:结点所拥有的子树称为该结点的度。
- 树的度:树中各个结点度的最大值称为该树的度。
- 叶子(终端结点):度为零的结点称为叶子结点。
- 分支结点:度不为零的结点称为分支结点。
- 兄弟结点:同一父亲结点下的子结点称为兄弟结点。
- 层数:树的根结点的层数为1,其余结点的层数等于它双亲结点的层数加1。
- 树的深度:树中结点最大层数称为树的深度。
- 森林:零棵或有限棵互不相交的树的集合称为森林。
- 有序树:子树有序的树。
- 无序树:子树无序的树。
3、二叉树
3.1 二叉树定义
二叉树可以为空,非空的二叉树中,每个结点至多只有两颗子树,分别称为左子树和右子树,且左、右子树的次序不能任意交换。
3.2 二叉树的性质
-
非空二叉树的第i层上最多有**2i-1**个结点(i>=1)
-
深度为h的二叉树中,最多具有2h-1个结点(h>=1)
-
满二叉树:一棵深度为h,且有2h-1个结点的二叉树称为满二叉树
-
完全二叉树:深度为h、有n个结点的二叉树,当且仅当每一个结点都与深度为h的满二叉树中编号从1至n的结点一一对应时,称此二叉树为完全二叉树。
-
对于一棵有n个结点的完全二叉树,若按满二叉树的同样方法对结点进行编号,则对于任意序号为i的结点:
-
若i=1,则序号为i的结点为根节点。
若i>1,则序号为i的结点的父结点的序号为**【i/2】**。
-
若2i<=n,则序号为i的结点的左孩子结点的序号为2i。
若2i>i,则序号为i的结点无左孩子。
-
若2i+1<=n,则序号为i的结点的右孩子结点的序号为2i+1。
若2i+1>n,则序号为i的结点无右孩子。
-
-
具有·n(n>0)个结点的完全二叉树(包括满二叉树)的深度(h)为**[log2n]+1**
-
对于一棵非空的二叉树,设n0、n1、n2分别表示度为0、1、2的结点个数,则有n0=n2+1。
3.3 二叉树的存储
未完。。。
上一篇: 帧动画多线程加载