1. 数据结构概念
数据、数据元素、数据项、数据对象、数据结构、逻辑结构【集合、线性结构、树性结构、图结构】、物理结构【顺序存储结构、链式存储结构】
(1) 数据结构
相互之间存在一种或多种特定关系的数据元素的集合。
(2) 逻辑结构
数据元素之间的相互关系。【集合--元素之间没有关系、线性结构--一一对应、树形结构--一对多且有层次、图结构--多对多】
1) 集合
元素之间除了同属一个集合之外没有关系。
2) 线性结构
元素之间是一一对应的关系。除第一个元素外,每个元素都有一个前驱。除最后一个元素外,每个元素都有一个后继。
3) 树形结构
一对多的层次关系。
4) 图结构
元素之间是多对多的关系。
(3) 存储结果
逻辑结构在计算机中的存储。
1) 线性存储
最常见的是数组。
2) 链式存储
链表
2. 算法
算法、算法特性、算法效率度量方法、时间复杂度。
(1) 算法
解决问题的求解步骤的描述,在计算机中表现为指令集的有限序列。并且每个指令集都是一个或多个操作。
(2) 算法特性
1.输入(0或多个输入)、
2.输出(一个或多个输出)、
3.有穷(有限个步骤、每步在接受的时间内完成)、
4.确定(含义确定,无二义性)、
5.可行(每一步都是有限次执行)。
(3) 时间复杂度
1.常数阶[操作步数和数据规模n没关系]、
2.线性阶[a*n+b]、
3.对数阶[longN]、
4.指数阶[n²]
3. 线性表
线性表、线性的顺序存储、顺序存储的插入和删除、线性表链式存储、链式表的读取-插入-删除、单链表的创建-整表删除、静态链表的插入-删除、循环链表、双线链表。
零个或多个数据元素的有限序列。
(1) 线性表的顺序存储
用一段连续的存储单元依次存储线性表的数据元素。【逻辑关系和物理存储关系一致】
- 顺序表的属性
- 线性表的存储地址
- 最大容量MaxSize
- 线性表当前的长度:Length
- 顺序表的读取
用数据和下标可以直接获取元素。Array[i]
- 顺序表存储插入
插入位置之后的元素整体后移
从length到存放位置i,整体向后移动一个位置。然后将新数据插入下标为i的位置上。
- 顺序表存储的删除
删除元素以后的元素整体迁移
删除下标为i位置的数据。从i+1到length的顺序遍历,元素整体向前移动一位。
(2) 线性表的链式存储
结点数据结构【数据域和指针域。其中数域存放数据,指针域指向逻辑上相邻的后继元素的位置】、
1.链表遍历读取
从头结点开始,获取结点,然后根据指针域读取下一个结点,如此重复,直至链表尾部。不轮读取,插入和删除,都需要以上遍历的方式。
从链表的头结点开始,获取结点数据,然后获得指针域指定的结点。如此循环,直到指针域为null位置
2.链表的元素插入
循环链表到指定的位置、假设需要插入结点P和结点q之间,其中结点p->next 指向q。则将新结点e插入p,q之间。S->next = p->next,p->next=s
3.链表的元素删除
循环链表到指定的位置、假设需要删除结点q,结点q的前驱为p,则删除q,需要将q的前驱元素的指针指向q的后继元素,p->next = p->next-next,如果是需要主动释放内存的情况下,需要对q手动释放内存
4.单链表的创建
- 头插法
新创建的结点,始终插入头结点的后面,新元素的指针域指向以前的第一个元素。
- 尾插法
小技巧,使用保留PreNode。每次产生心结点后。将PreNode的指针域指向新结点。
循环过程中,始终记录上次循环的结点preNode【在循环最后执行 preNode= node】,新结点链接到preNode之后、始终将新结点在尾部添加
(3) 静态链表
有点复杂
定义【用数组来描述链表】、结点结构【数据域data和指针域cur】、
1.备用链表
数组中未被使用的结点组成的链表。提供给静态链表新数据使用。
2.首尾结点
第一个结点和最后一个结点数据域不存放数值,只使用指针域,第一个结点的指针域存放备用链表的中第一个未被使用的结点下标地址,最后一个结点的指针域存放第一个存放数据的结点的下标地址。
3.静态链表的插入
假设将元素插入位置第i个位置,
- 寻找插入位置且调整备用链表
获取备用链表的首个空间下标j【同时,将原来备用链表第二个结点的位置cur存放到space[0]的指针域cur】,数值存放到结点arr[j].data中,
- 静态链表插入
新元素的指针域指向原来第i个元素,原来第i-1的元素的指针域指向新元素
找到第i个元素的前继元素(也就是第i-1个元素)的下边为K,然后将原本第i个元素arr[k].cur串到新元素后面arr[j].cur = arr[k].cur,再将新元素位置赋值给第i-1个元素的指针 arr[k].cur =j】
- 静态链表的删除
链表结点删除、调整备用链表
- 结点的后继的后继指针赋值给后继的指针
- 备用链表调整
在静态链表中比较特殊,需要对删除结点进行回收,重写加入到备用链表中,且加入备用链表的表头位置。即s->cur= space[0].cur;space[0].cur=s
(4) 循环链表
单链表的终端链结点的指针域从null修改为指向头指针。称为循环链表。
(5) 双向链表
结点数据结构设计为一个数据域data和两个指针域prior和next,分别指向前驱和后继。对于双向链表的插入和删除仅仅需要考虑前驱和后继两个方向的链表操作而已。本质上和单向链表操作一致。
4. 栈和队列
栈定义、栈的顺序存储、两栈共享空间、栈的链式存储、栈的应用-递归、栈的应用-四则运算表达式求值、队列定义、循环队列(顺序存储)、队列链式存储结构及实现。
(1) 栈定义
只允许在一端进行插入和删除。
1.栈顶和栈底
插入和删除栈顶top、另外一端为栈底
(2) 栈的顺序存储
1.顺序存储栈Top属性
top:栈顶元素的位置,空栈 top=-1,stackSize为栈的大小。Top的最大值为stackSize-1
2.顺序存储栈的操作
入栈push【top++;stack[top]=e】
出栈pop【stack[top];top--】
3.特殊的顺序存储栈--两栈共享空间
- 使用场景【两个栈数据元素类型一致,数据之间存在严格的此消彼长的关系】
- 性质和操作
线性队列的两端分别作为栈低,top1、top2分别为两个栈的top,第一个栈入栈逻辑为stack1[++top1]=e,第二个栈入栈逻辑为stack2[--top2]=e,栈满的条件是top1+1=top2。
(3) 栈的链式存储
节点的数据结构为数据域和指针域。
1.链栈属性
栈的数据结构为top和count。S代表链栈。e代表新元素结点。
Top为栈顶指针、
count表示栈中元素的个数。
2.链栈操作
入栈【e->next = S->top; S->top = e】
出栈【S->top->data; S->top = S.top.next】
(4) 栈的应用
1.递归函数
java虚拟机中栈中每个栈帧代表一个方法,方法之间的调用使用栈来表示之间的关系
2.四则表达式运算
java虚拟机中每个栈帧中都有一个操作数栈,用来计算四则表达式的运算。将中缀表达转成后缀表达
3.后缀运算规则
从左向右遍历每个表达式的每个数字和符号,遇到数字就进栈、遇到符号,就将两个数据从栈中弹出,进行计算,将结果压栈。一直到最终获得结果
(5) 队列
定义【只允许在一端进行插入、在另外一端进行删除的线性表,特征:FIFO】
(6) 队列顺序存储(循环链表)
顺序存储队列,通常为循环链表,不需要移动。
1.循环队列数据元素的 数据结构
data代表线性表,
front:队头元素位置
rear:队尾的下一个位置
2.队列属性判断
空队列:front=rear
满队列:(rear+1)%MaxSize =front
3.队列操作
- 出队
- 判断不为空队
- 获取元素:Q-> data[front];
- 调整front位置:Q->front = (Q-> front + 1 ) %MaxSize
- 入队
- 判断没有队满
- 新元素入队:Q->data [rear]= e
- 调整rear位置:Q->rear= (Q->rear+1)%MaxSize
(7) 队列链式存储
1.队结构
队头指针和尾指针。链队出栈找队头指针,入队找队尾指针。
front 队头指针
rear 队尾指针
Q代表队列
2.数据元素结构
数据域data和指针域next
3.链队操作
- 入队
不用判断为空判断。新结点元素在队列串联进来。将原来队尾结点的指针域指向新结点。同时更新队列的rear为新元素。
- 原来队尾结点的指针域指向新结点 Q->rear->next =s;
- 更新队列的rear为新元素 Q->rear = s;
- 出队
- 判断不为空队:Q->front =!= Q->rear;
- 获取队头数据 Q-> front->data ;
- 调整队头指针 Q->front = Q->front->next;
5. 树
树的定义(结点分类、结点之间的关系、层次、深度、有序树、无序树、森林)、树的存储结构(双亲表示法、孩子表示法、孩子兄弟表示法)、二叉树(定义、特点、特殊二叉树(斜树、满二叉树、完全二叉树)、二叉树的特征)、二叉树存储(顺序存储、二叉链表)、二叉树的遍历(前序遍历、中序遍历和后序遍历、层次遍历)、二叉树的建立、线索二叉树(结点数据结构、线索二叉树实现、线索二叉树后的遍历访问)、树-森林-二叉树的转换、赫夫曼树(定义、原理和编码)
(1) 加强知识点
树知识点分为 树、二叉树、线索二叉树、赫夫曼树
(1) 树定义
n个结点的有限集。N=0时称为空树。在任意一棵非空树中,(1)有且仅有一个特定的结点称为根结点。(2)当N>1的是偶,根结点除外的n-1个结点可以分为互不相交的多个有限集,且每个有限集也是一棵树。 定义的过程中使用了递归。
(2) 树的存储
1.双亲表示法(顺序存储)
数组表示:结点数据元素结构【数据域data,指针域 parent】通过每个结点都记录自己的双亲结点的下标,来表达树形结点元素之间的关系
2.孩子表示法(链式存储)
结点的数据结构为data和孩子的集合表示。孩子的集合表示比较好的结构为链表表示。则结构为【data、first_child、second_child ...】和多重链表结构类似,其中first_child指向孩子的链表的表头,也就是第一个孩子的结点
3.孩子兄弟表示法(链式存储)
结点的数据结构为【data、first_child、right_sib】
这种表示方法的优势:能把任何一棵树转成二叉树的形式
(3) 二叉树定义
- 定义
该树要么是空二叉树,要么是一个根节点和另外两个二叉树构成
- 特征
不存在度大于2的结点、左右子树是有顺序的,不能随意调换、及是一个子树,也要区分是左子树还是子树
(4) 二叉树的存储
- 顺序存储
和树的顺序存储比较、二叉树顺序存储有孩子左右之分。树的存储只指定双亲结点,没有左右孩子之分。
按照完全二叉树进行编号,将数据存储在下标为编号的数组中
- 二叉链表
结点的数据结构【data、l_child、r_child】,每个数据结点存放数据之外,存放左右子树的结点位置
(5) 二叉树的遍历
其中,前序、中序和后序遍历,都使用了递归的方式。且前、中、后指的是根节点和左子树、右子树的访问次序。
- 前序遍历
对于非空二叉树,先访问根结点、然后前序遍历左子树,最后前序遍历右子树
- 中序遍历
同上:左子树->根结点-> 右子树
- 后续遍历
同上:左子树-> 右子树->根结点
- 层序遍历
从树的根结点开始,从上向下逐层遍历。同一层中从左到右的顺序对结点逐个访问
已知前序和后序遍历结果,是不能确定二叉树的结构。
(6) 线索二叉树
对应的是十字链表存储的情况下。
1.定义
指向前驱或者后继的指针称为线索,加上前驱指针和后继指针的二叉链表称为线索链表,相应的二叉树称为线索二叉树
2.线索二叉树的结点数据结构
data【数据域】
l_child【左孩子或者前驱结点】
l_tag【0:代表左孩子 1代表前驱】
r_child【右孩子或者后继】
r_tag【0代表右孩子 1代表后继】
3.线索化过程
小技巧:保留前驱中序遍历的前驱结点。
线索化的核心流程,中序遍历二叉树,
如果p->lchild为空 则l_tag 赋值1 同时,l_child 赋值 前驱结点pre。
如果pre->child 为空,则pre->r_tag 赋值为1, pre->r_child 赋值为 当前结点p,
精妙之处,根据p和前驱pre,同时判定p的左子树是否为空和pre的右子树是否为空
4.线索化的二叉树的遍历
(7) 赫夫曼树
路径的长度【从树的一个结点到另一个结点之间的分支构成两个结点之间的路径。路径上分支的数目叫路径的长度,叶子结点的长度是从根结点到叶子结点的分支的数目,带权叶子结点的程度是叶子结点长度和权值的乘积】树的路径的长度是根到每个叶子结点的数目之和。
两点之间的路径:丛树的一个节点到另外一个节点之间的分支构成两个结点之间的路径。
路径的长度:路径上的分支数目叫路径的长度。
树的长度:就是从根节点到每个结点的路径之和。
带权路径长度WPL计算方式:每个叶子结点的权值*该叶子结点的路径 之和。
1.赫夫曼树
带权路径长度WPL最小的二叉树称作赫夫曼树。
2.赫夫曼树的算法
最小权值的两个子树A、B合并成一个二叉树C,C的权值为权A+权B,将C放入集合红,删除A、B,如此重复,直到所有集合只剩下一棵树,则为赫夫曼树
3.赫夫曼编码
赫夫曼树从根节点到叶子结点,分支中是左孩子代表0,右孩子代表1,这样,每个叶子结点的赫夫曼编码就是从根结点到叶子结点对应的字符
6. 图
图(定义、顶点、边、有序偶、无序偶、弧、度、入度、出度、简单图、无向完全图、有向完全图、稀疏图、稠密图、权、网、子图、邻接点、依附、路径、路径长度、回路(环)、简单路径、简单回路、连通、连通子图、连通分量、强连通分量)、图的存储结构(邻接矩阵、邻接表、十字链表、邻接多重链表、边集数组)、图的遍历(深度优先遍历、广度优先遍历)、最小生成树(定义、普里姆算法、克鲁斯卡尔算法)、最短路径(定义)、拓扑排序(算法)、关键路径(算法、原理、实现)
(1) 加强知识点
(1) 图定义
【由顶点的有穷非空集合和顶点之间的边的集合组成】、路径的长度【路径上边或者弧的数目】、环【第一个顶点和最后一个顶点相同--环】、简单路径【顶点不重复出现的路径称为简单路径】、连通【如果两点之间有路径,则成两点是连通的】、连通图【如果任意两点之间都是连通的,则称为连通图】
(2) 图的存储
1.邻接矩阵
顶点用一维数组来存储、边或者弧用二维数组来存储。这种存储方式称为邻接矩阵,二维数组和离散数学中的矩阵很相似。
矩阵的表示方法:假设顶点在一维数组中顶点A的下标为i,顶点B的下标是j。而在二维数组中第i行代表从A为起点的边/弧。Arr[i][j] == 0 表示AB之间没有边。Arr[i][j] == 1 表示AB之间有条边。无向图的矩阵是对称矩阵。
使用场景:适合对图进行统计,但是不适合对于图的元素的删除和修改的情景。
2.邻接表
顶点由一维数组表示,边由链表表示。链表保存:当前顶点为起点所有边的终点对应顶点在一维数组中的下标。
顶点数据结构【数据域data,指针域:first_edge 从当前顶点出去的边】
边链表结点的数据结构【边终点的结点位置:adjvex、next】。相当于一维数组表示图各个顶点,链表可以表示出度在当前结点的点,也可以表示入度在当前顶点的边。缺点是不能兼顾入度和出度的表示。
邻接表只能按照出度或者入度来表示图。不能兼顾两者。
3.十字链表(有向图)
是邻接表针对有向图的一种优化存储。十字链表是为了便于求得图中顶点的度(出度和入度)而提出来的。它是综合邻接表和逆邻接表形式的一种链式存储结构。
每个顶点对应两条链表,一个是以该顶点我起点的弧的链表、另外一个是以顶点为终点的弧的链表。
- 有向图顶点数据结构
数据域data,顶点的具体数据信息
first_in:指向以该顶点为弧头的第一个弧节点。
first_out:指向以该顶点为弧尾的第一个弧节点。
- 链表的结点的数据结构
tailvex 表示该弧的弧尾顶点在顶点数组xList中的位置
headvex表示该弧的弧头顶点在顶点数组中的位置
headlink表示指向弧头相同的下一条弧
taillink表示指向弧尾相同的下一条弧
- 使用场景
该结构方便统计顶点的入度和出度。有向图的优化存储。 理解:一条边对应两个顶点,分别会在起点顶点统计依次,也会在终点顶点统计一次。缺点在于删除一条边,需要修改两个地方
4.邻接多重表(无向图)
- 设计目标
针对无向图设计
对于无向图,使用邻接表存储,如果删除一条边需要修改边所在的两个顶点在链表中的表示。相当于需要修改两处。邻接多重表仿照十字链表对邻接表进行修改。再边发生变化的情况下,修改一处即可。
一维数组代表顶点,链表代表边(真正意义的表--两个顶点)
- 存储思路
一维数组存放顶点,链表表示边/弧。
- 数据结构
- 顶点数据结构
Data:顶点的具体数据信息
first_edge:
- 链表数据结构
一个结点真正意义代表一条边。
ivex 、Jvex:一条边的两个顶点在顶点一维数组中的位置。
Ilink:依附顶点ivex的下一条边。
Jlink:依附顶点Jvex的下一条边。
5.边集数组
由两个一维数组组成。顶点有一个一位数组。只需要保存顶点本身信息。
另外一个一位数据表示边。
- 边的数据结构为
Begin:边的起点在顶点数组中的位置
End:边的终点在顶点数组中的位置
Weight:边的权重
(3) 图的遍历
从图的某一顶点出发,遍历图中的其余顶点,且使每个顶点仅被访问一次。这一过程叫图的遍历。
1.深度优先遍历
Deep First Search 也称为深度优先搜索,简称DFS
- 定义
原始定义:它从图的某个顶点V出发,访问此顶点,然后从V未被访问的邻接点出发,深度优先遍历图。直至图中所有和V有路径相通的顶点都被访问到。若尚有顶点未被访问到,则另选一个未被访问到的顶点作为起始点,重复上述过程。直到图中所有顶点都被访问到。
以下是简化的定义,其中递归调用完全按照以下定义实现的。
(1)访问顶点v;
(2)从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;
(3)重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
个人理解:访问序号为i的顶点则递归调用(相当于树的前序遍历==》根节点-左子树-右子树)和他相邻的结点,从而达到深度优先调用的效果。
- 实现
有递归实现和非递归实现,非递归可以使用栈协助完成深度优先遍历。
- 递归实现
(1)访问顶点v;visited[v]=1;//算法执行前visited[n]=0
(2)w=顶点v的第一个邻接点;
(3)while(w存在)
if(w未被访问)
从顶点w出发递归执行该算法;
w=顶点v的下一个邻接点;
- 非递归实现
(1)栈S初始化;visited[n]=0;
(2)访问顶点v;visited[v]=1;顶点v入栈S
(3)while(栈S非空)
x=栈S的顶元素(不出栈);
if(存在并找到未被访问的x的邻接点w)
访问w;visited[w]=1;
w进栈;
else
x出栈;
2.广度优先遍历
Breadth First Search 简称BFS
- 定义
(1)从图中某个顶点v出发,访问v。
(2)依次访问v的各个未被访问过得邻接点。
(3)分别从这些邻接点出发依次访问他们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤3,直至图中所有已被访问的顶点的邻接点都被访问到。
- 实现
从广度优先遍历的定义,首先是层次遍历,体现广度。再者,比如V的邻接点A和邻接点B。A先于B被访问,则A的下层邻接点也优先于B的下层邻接点被访问。因为有这个关系,可以使用队列的先进先出特性,来辅助完成广度优先。
广度优先代码的伪代码
(1)顶点v入队列。
(2)当队列非空时则继续执行,否则算法结束。
(3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
(4)查找顶点v的第一个邻接顶点col。
(5)若v的邻接顶点col未被访问过的,则col入队列。
(6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。
直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)。
(4) 最小生成树
针对带权值的图,即网结构。从中选择连接所有顶点,且边的权值之和最小。即为最小生成树。
一个比较常见的应用。N个村庄需要用网线连接,怎样使网线最短。这就是一个求最小生成树的实际例子。
1.普利姆算法
假设N={V,{E}}是连通网,TE是N上最小生成树中边的集合。算法从U={U0}(U0是V的元素),TE={}开始。重复执行以下操作:在所有边满足一个顶点在U中,另一个顶点在V-U中的边,选择一个权值最小的边。作为TE的元素。同时,TE中所有的顶点都放入U中。然后继续上述操作,直到U=V。则T就是N的最小生成树。
确保每次取出的边一个顶点在U中一个顶点在V-U中且权值最小。将取出的边放入TE中。直到V=U位置。则获得最小生成树。
2.克鲁斯卡尔算法
(5) 最短路径
两个顶点之间,经过的权值之和最小的路径。路径上第一个顶点为源点,最后一个顶点为终点。
1.迪杰斯特拉算法
按路径长度递增的次序产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点 v 到其它各顶点的最短路径全部求出为止。
解决步骤描述:
1. 设置辅助数组dist。它的每一个分量dist[i]表示当前找到的从源点 v0到终点 vi的最短路径的长度;
2. 初始状态:
2.1. 若从源点 v0 到顶点 vi有边:dist[i]为该边上的权值;
2.2. 若从源点 v0 到顶点 vi无边:dist[i]为∞。
2.弗洛伊德算法
(6) 拓扑排序
拓扑排序实际上是构造拓扑序列的过程。而拓扑序列,通俗而言,就是顶点的一种排序。
拓扑序列是任务顺利完成的一种顺序。
- AOV网:用顶点代表活动(需要一段时间来完成的工作),用弧代表先后顺序。弧起点的事件需要先于弧终点的事件先发生。所以,AOV网排序的过程是一个严格遵循事件发生先后顺序的过程。
- 拓扑排序算法:从AOV网中选择一个入度为0的结点输出。然后删除此顶点为尾的弧。然后重复上述操作,直至所有的顶点全部输出或者不存在入度为0的顶点位置。(理解特别简单)
基于图的存储有邻接表或者邻接矩阵的区别,具体实现上有些 区别。
如果是邻接表,实现过程中可以使用栈。目的是:避免每次遍历顶点寻找有没有入度为0的顶点。栈在此算法中的应用需要加深理解。
(7) 关键路径
关键路径是解决项目中任务完成最短时间的问题。
1.AOE网
顶点代表事件(一个时刻,标志性的事件发生,是一个时间点上的描述),弧表活动(一段时间内的工作),弧上的权值代表活动所需要的时间。
2.关键活动
路径上活动所持续的时间之和称之为路径长度。从源点到汇点最大长度的路径称之为关键路径。关键路径上的活动称之为关键活动。
3.关键路径的算法
7. 查找
查找定义(查找、查找表、关键字、主关键字、次关键字)、静态查找表(特征)、动态查找表(特征)、顺序表查找、有序查找(折半(二分)查找、插值查找、斐波那契(qi)查找)、索引查找(稠密索引、分块索引、倒叙索引)、二叉排序树(定义、查找操作、插入操作、删除操作)、平衡二叉树AVL(定义、实现原理、代码实现、)、多路查找树(B树)(2-3树、2-3-4树、B树、B+树)、散列(定义、构造方法、冲突处理方法、查找)
(1) 定义
1) 查找
根据给定的某个值,在查找表中确定一个关键值等于给定值的数据元素或记录。
2) 静态查找
1.查询某个特定的元素是否在查找表中
2.检索某个特定的数据元素的各种属性
3) 动态查找
1.查找时插入数据
2.查找时删除数据
(2) 顺序查找
1) 顺序查找定义
也称之为线性查找。从第一个或者最后一个开始,逐一进行关键字和给定值进行比对。如果匹配成功,则查找成功。如果直到最后一个元素也没匹配成功,则查找不成功。个人理解,通过遍历,逐一进行匹配,直至最后一个元素
2) 顺序查找的优化
线性表的遍历,如果是线程存储,需要遍历下标,判断下标是否越界,然后获取元素进行比较。匹配比较过程不能少,但是越界判断是可以优化的。可以将给定元素作为哨兵。循环到哨兵位置为止。如果中途跳出,表明匹配成功,如果直到哨兵位置才跳出,说明匹配失败。
(3) 有序查找
1) 折半查找
又称二分查找。前提是线性表的顺序存储。且元素的关键码有序。
在有序表中,取中间位置的元素作为比较对象。如果给定值和中间记录关键码相等。则查找成功。如果中间记录的关键字大于给定值的大小。则在左半部分继续用二分查找。如果中间记录的关键字小于给定值的大小。则在有半部分继续二分查找。不断重复上述过程。直至成功或者查询所有区域,查找失败。
折半查找是一个递归调用。每次寻找的位置是low和high的1/2。即mid = (1/2) *(low+high)= low+(1/2)*(high-low)
2) 插值查找
插值查找和二分查找区别仅在于比值,不是1/2。而是(Key-a[low])/ (a[high]-a[low]).
即 mid = low = low + ((key-a[low])/(a[high]-a[low])) *(high-low)
3) 斐波那契查找
(4) 线性索引查找
索引就是把关键字和它记录相关联的过程。
索引分为线性索引、树性索引和多级索引。所谓线性索引就是将索引项集合组织成线性结构。线性索引包含稠密索引、分块索引和倒排索引。
1) 稠密索引
线性索引中,每一条记录都对应一个索引项。
对稠密索引而言,索引项按照关键码有序排列。
2) 分块索引
把数据集分成若干个块,并且这些块满足两个条件。
1.分块数据集特点
块内无序,块间有序。
2.索引项数据结构
最大关键码:存储每一块中最大的关键码。
块长:存储块中记录的个数。以便循环时使用。
块首指针:用于指向块首个元素的指针。以便开始对这一块记录进行遍历。
3.分块索引表查找
- 在分块索引表中查找关键字所在的块。【通过折半查找】
- 根据首块指针找到相应的块。根据块长确定顺序表的范围(大小)。然后在顺序表中查找关键码。【因为块内是顺序无序存储的】
3) 倒排索引
(5) 二叉排序树
如果有序的数据需要动态查找。即在查找的过程中需要插入未找到的数据元素或者删除查找到的数据元素,顺序存储的数组是不能很好解决需要。需要使用一种叫二叉排序树的数据结构。
二叉树的查找和顺序查找有相似部分。
(1) 设计目的
二叉排序树主要为了解决有序动态查找。存储是二叉树的形式存储的。
(2) 二叉排序树操作
动态查找,基本的操作有构建、查找、插入、删除
1.查找
关键字和二叉树根节点的值域的数值比较,如果相等就查询成功。如果关键字小于根结点的值,就递归查找根结点的左孩子。如果关键字大于根结点的值。就递归查找根结点的左孩子。查找函数会返回双亲结点。供插入或者删除使用。
2.插入
基本算法:基于查找函数,如果返回为false,则代表没有找到结点。新创建结点s,结点值域赋值为关键字key。和查找位置的双亲结点P的结点数值比较。如果key<双亲的data,则新结点s做P的左子树。如果key>双亲结点p的data,则新结点s做P的右子树。
如果查找函数返回的是false,则双亲结点必然是一个叶子结点。
3.删除
实现分为两层:
第一层:判断key值和树根结点是否相等。如果相等则删除当前结点且调整左右子树,并重接左右子树。如果key<根结点的数值,则递归调用二叉树删除方法,删除左子树。如果key>根结点的数值,则递归调用二叉树删除方法,删除右子树。
第二层:删除根结点,并重接左右子树的方法实现。
删除结点又以下三种可能。
(1)叶子结点【直接删除】
(2)仅有左或者右结点【如果只有左子树,删除后左子树调补到被删除的位置上,如果只有右子树,右子树添补到被删除的结点】
(3)左右子树都有结点
- 思路
找整棵树中序遍历的前驱结点s替换被删除的结点p的数据。然后根据s的子树情况进行小范围调整【s只有两种形态:一种是叶子结点、另外一种是只有左子树】。
- 操作
- s只是叶子结点
则不作调整。
- 假设s还有左子树s->lchild,且s的原来双亲结点为q如果p!=q
则将s的左子树接到q的右孩子。
- 假设s还有左子树s->lchild,且s的原来双亲结点为q如果p==q
则s的左子树接到q的左孩子,q->lchild=s->lchild。
删除稍稍复杂一些。使用直接前驱结点替换被删除的结点。然后调整之前前驱的左子树,重新接入到二叉树。
(6) 平衡二叉树
1) 基本概念
平衡二叉树:定义为递归定义,每个结点为根节点的子树都是平衡二叉树。左右子树的深度不超过1。
最小不平衡子树:距离插入点最近的,且平衡因子绝对值大于1的结点为根节点的子树,我们成为最小不平衡子树。(对于原本平衡二叉树插入新结点而造成的不平衡)
平衡二叉树的目标是在元素结点不变的情况下。尽可能保证深度最小。这样在有序查找的情况下,操作尽可能的少。
2) AVL树实现原理
构建平衡二叉树的基本思想就是:在构建过程中,每当插入一个结点时,检查是否破坏了树的平衡性,若是,则找出最小不平衡树,进行相应的调整。
3) 平衡二叉树的结点数据结构
1.数据域(数据)
2.左子树指针
3.右子树指针
4.平衡因子的数据项bf(相比二叉树结点多的部分)。
4) 平衡二叉树实现算法
本质是:调整最小不平衡树的操作
1.基本操作-左旋、右旋
- 右旋
如下图中左边的最小不平衡二叉树,进行右旋操作即可变为右边中的平衡二叉树。
需要右旋,说明左侧子树深度高,原来的左子树L需要作为跟结点。所以,P需要成为L的右子树。L的右子树为p结点的中序遍历的前驱。所以,L的右子树称为P的左子树。之后将P作为L的右子树。
将整棵树分为三部分。(1)原根节点带着右子树 【P和PR】(2)左子树的右子树 LR (3)左子树去掉右子树的部分【L- LR】
步骤如下:
(1)左子树L的右子树LR作为P的左子树;
(2)然后将P作为L的右子树
- 左旋
同上所述,左旋操作的图示及代码,如下所示
左旋说明右侧的深度高。将原二叉树分为三部分,(1)根节点P带着左子树L(P - R),称之为p (2)右子树R的左子树RL,成为q (3)右子树R去除其左子树RL部分(R - RL),称之为w
具体左移操作如下
(1)将q作为P右子树,成为r
(2)将r作为w的左子树
2.左/右平衡旋转
- 左平衡旋转
左平衡旋转说明新结点添加在左子树。最小不平衡子树T的BF必然是2。整体必然需要右转。根据
- 如果T的左孩子的BF和T的BF符号相同,则最小不平衡子树T需要右转。
- 如果T的左孩子的BF和T的BF符号不同,说明新结点在T左孩子的右子树上。说明需要双旋。(1)T的左孩子的左旋(2)对整个T进行右旋。
- 右平衡旋转
需要对最小不平衡子树的右子树进行平衡旋转操作。说明新结点添加在右子树上。
右平衡旋转说明新结点添加在右子树。最小不平衡子树T的BF必然是-2。整体必然需要左转。
- 如果T的右孩子的BF和T的BF符号相同,说明新结点在T右孩子的右子树上,则最小不平衡子树T仅需要左转。
- 如果T的右孩子的BF和T的BF符号不同,说明新结点在T右孩子的左子树上。说明需要双旋。(1)对T的右孩子的右旋,(2)对整个T进行左旋。
(7) 多路查找树(B树)
B树是一种平衡的多路查找树。结点中最大的孩子数称之为B树的阶。
1) B树的设计目标
2) m阶B树的属性
1.如果根节点不是叶结点,至少有两棵子树。
2.每个非根的分支结点都有k-1个元素和K个孩子;其[m/2] <=K <=m。每个叶子结点n都有k-1个元素。
3.所有叶子结点都位于同一层次。
4.所有分支结点数据结构为。K个孩子指针和k-1个元素的交叉排列。
在B树中查找的过程是一个顺指针查找结点和在节点中查找关键字交叉的过程。
3) 2-3树
- 2-3树定义
2-3树是阶为3的B树。是多路查找树。其中每个结点都有2个孩子(称为2结点)或者三个孩子(称为3结点)。其中2结点包含一个元素和两个孩子或者没有孩子。3结点包含两个元素和三个孩子或者没有孩子。树的所有叶子在同一层次。
- 2-3树插入
插入分为3中情况
(1)对于空树,插一个2结点即可。
(2)插入结点到一个2结点的叶子结点上,本身就是一个元素,将 2结点升为3结点即可。
(3)忘3结点插入一个元素。因为3结点本来就是2-3树最大容量 的结点(已经有两个元素)。因此需要将其拆分,将树中的两 个元素或者插入的元素三者选择其一(三个元素中间值)向上移动一层,其余两个元素称为两个2结点。
拆分的本质:就是如果待插入位置如果是三节点那么就分解它,向上抛掷(3个元素的中间值),如果上面也是三节点,那么仍然需要拆解,向上抛掷,…等到如果抛掷到头节点,如果头节点也是三节点那么说明,这个树高度不够存放这个结构,就需要分解头节点终止操作增加树的高度。因为在往上没有节点可以执行操作。
假定我们记录的数据是1,2,3,4,5,6,7,8,9不失一般性,就从1开始按顺序插入模拟程序运行。
- 2-3树删除
(1)删除非叶子结点
使用中序遍历下直接后继结点key来覆盖当前结点key,在删除用来覆盖的后继结点key。
(2)删除叶子结点
- 删除3结点的叶子节点
直接删除,将3结点转成2结点的叶子结点
- 删除2结点的叶子结点
4) 2-3-4树
5) B树
6) B+树
(8) 散列查找
散列既是一种存储方法,又是一种查找方法。
1) 定义
1.散列技术
在记录的存储位置和它的关键字之间建立一个确定的关系f,使得每个关键字key对应一个存储位置f(key)。f称为散列函数,又称哈希函数。
2.哈希表
使用散列技术将记录存储在一块联系的存储空间内,这块连续的存储空间称为散列表或者哈希表。
2) 散列构造方法
1.直接定址法
F(key) = a * key +b
2.平方取中法
平方取中间三位
3.除留余数法
f(key) =key mod p (p<=m)
4.随机数法
3) 处理散列冲突方法
1.链址法
相同f(key)的值放在同一个链表中。F(key)的位置存放链表的表头。如果冲突,需要在链表中依次匹配,直至最后一个结点。匹配元素。
2.公共溢出区法
对于溢出的部分,不排序放入溢出表(顺序存储的数组)中。寻找发现冲突,则需要在溢出表中整体遍历,顺序查找。
8. 排序
排序(定义、稳定/不稳定排序、内排序/外排序)、冒泡排序(3种)、简单交换排序、直接插入排序、希尔排序、堆排序、归并排序和快速排序。
(1) 定义
1.稳定/不稳定排序
排序前序列,如果两个元素a、b相等,且a在b前面,经过排序算法后,a、b的相对位置不发生变化,则称排序算法是稳定排序。反之,称为不稳定排序。
2.内排/外排
排序的过程中,所有待排序的记录都放在内存中。称之为内排。
3.排序性能
判断排序性能从以下几个角度比较
- 时间性能
- 辅助空间
- 算法复杂度
4.排序分类
内排分为4类
- 交换排序
- 冒泡排序
- 快速排序
- 选择排序
- 简单选择
- 堆排序
- 插入排序
- 直接插入排序
- 希尔排序
- 归并排序
- 归并排序
从复杂度分为两大类
- 简单算法
- 冒泡排序
- 简单选择排序
- 直接插入排序
- 复杂算法
- 冒泡排序
- 堆排序
- 希尔排序
- 归并排序
(2) 冒泡排序
1.冒泡排序(伪冒泡)
两层循环,第一层每次确认一个剩余中最小的元素,然后通过交换,将最小的元素放在剩余位置的第一个位置。
因为冒泡排序的标准定义为相邻的两两比较。而该算法是将剩余所有元素都和哨兵位置比较,然后将最小的数据放在第一个位置。和选择排序的意义类似。因此,称之为伪冒泡。
2.标准冒泡
两层循环,第一层排序控制每轮找到一个本轮最小的元素。第二轮控制从线性表的尾部向头部移动,两两比较,如果逆序,交换位置。
3.冒泡优化
冒泡排序种,如果过程中发现线性表已经有序,则可以中断排序。所以,优化的本质就是及时发现,中断无效的比较。
(3) 简单交换排序
简单交换排序,是伪冒泡排序的一种优化。它的核心思想是多次比较,记录需要交换的位置,一次交换。
(4) 直接插入排序
将待排序序列分为两个序列,分别是有序集合和无序的两个序列。遍历无序的集合中的首元素a[i],存放到哨兵位置a[0],(1)如果a[i]大于有序序列最后一个元素,则a[i]位置不变,a[i]作为有序序列最后一个元素。(2)如果a[i]<有序序列最后一个元素。则a[i]从有序序列尾部向头部循环,找到位置j,满足a[j]<=a[i]<=a[j+1],则将j到i之间的元素向后移动一位。将a[0]的数据赋值给a[j]。循环上述操作,直至无序序列为空。
(5) 希尔排序
希尔排序是直接插入排序的改进,又称为缩小增量排序。
1.设计思想
希尔排序是直接插入的一种优化,而直接排序在基本有序和记录个数少的情况下,效率高。
所以,使得序列基本有序,个数较少,并且比较一次,移动一大步。这就是希尔排序的出发点。
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
注:将记录序列分割成若干个子序列,这样就会:(1)每个序列的元素个数减少(2)分割成若干个子序列,这样进行交换时就可以移动一大步(3)每一遍直接插入排序后,整个序列都变得更加有序【第一个元素是第一个序列中最小值,第二个元素是第二个序列中的最小值。依次类推,基本有序】。
希尔排序算法的特点:
1)缩小增量
2)多遍插入排序
2.实现原理
对n个待排序的数列,取一个小于n的整数gap(步长),将待排序的队列分成若干个子序列。所有距离为gap的倍数的记录放在同一个组中。然后对各组元素进行直接插入排序。一趟下来,每个组的元素都是有序的。然后减小步长的值,重复上述的分组和排序。直至步长为1。真个序列就有序了。
3.步长
3种方法:
1.取素数,每次让 gap- -,直到 gap =1;
2.gap = 需排序的元素个数,每次让 gap /2;
3.gap=需排序的元素个数,每次让 gap /3 +1;
经过大神们的逐一测试,第三种方法效率更高。