数据结构【基础知识点总结】
一、数据
- 数据(Data)是信息的载体,它能够被计算机识别、存储和加工处理。它是计算机程序加工的原料,应用程序处理各种各样的数据。
- 数据就是计算机加工处理的对象,它可以是数值数据,也可以是非数值数据。数值数据是一些整数、实数或复数,主要用于工程计算、科学计算和商务处理等;
- 非数值数据包括字符、文字、图形、图像、语音等。
二、数据元素
- 数据元素(Data Element)是数据的基本单位。在不同的条件下,数据元素又可称为元素、结点、顶点、记录等。
- 这些数据项可以分为两种:一种叫做初等项,如学生的性别、籍贯等,这些数据项是在数据处理时不能再分割的最小单位;另一种叫做组合项,如学生的成绩,它可以再划分为数学、物理、化学等更小的项。通常,在解决实际应用问题时是把每个学生记录当作一个基本单位进行访问和处理的。
三、数据对象
- 数据对象(Data Object)或数据元素类(Data Element Class)是具有相同性质的数据元素的集合。
- 在某个具体问题中,数据元素都具有相同的性质(元素值不一定相等),属于同一数据对象(数据元素类)
四、数据结构
- 数据结构研究的三个方面:
- (1)数据集合中各数据元素之间所固有的逻辑关系,即数据的逻辑结构;
- (2)在对数据进行处理时,各数据元素在计算机中的存储关系,即数据的存储结构;
- (3)对各种数据结构进行的运算。如增删改查。
- 数据结构是指相互有关联的数据元素的集合。
- 数据的逻辑结构包含:
(1)表示数据元素的信息;
(2)表示各数据元素之间的前后件关系。 - 数据的存储结构有顺序、链接、索引等。
- 线性结构条件:
(1)有且只有一个根结点;
(2)每一个结点最多有一个前件,也最多有一个后件。 - 非线性结构:不满足线性结构条件的数据结构。
- 线性结构条件:
五、数据的逻辑结构
- 数据的逻辑结构有以下两大类:
- 线性结构:有且仅有一个开始结点和一个终端结点,且所有结点都最多只有一个直接前驱和一个直接后继。线性表是一个典型的线性结构。栈、队列、串、数组等都是线性结构。
- 非线性结构:在该类结构中至少存在一个数据元素,它具有两个或者两个以上的前驱或后继。
如树和二叉树集合结构和多维数组、广义表、图、堆等数据结构都是非线性结构。
六、基本的数据结构
- 集合结构:数据元素的有限集合。数据元素之间除了“属于同一个集合”的关系之外没有其他关系。
- 线性结构:数据元素的有序集合。数据元素之间形成一对一的关系。
- 树型结构:树是层次数据结构,树中数据元素之间存在一对多的关系。
- 图状结构:图中数据元素之间的关系是多对多的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i7zpngDM-1614861368079)(C:\Users\Administrator\Desktop\interview_python-master\img\基本数据结构.jpg)]
七、数据的存储结构
-
数据的存储结构可采用顺序存储或链式存储的方法。
- 顺序存储方法是把逻辑上相邻的元素存储在物理位置相邻的存储单元中,由此得到的存储表示称为顺序存储结构。顺序存储结构是一种最基本的存储表示方法,通常借助于程序设计语言中的数组来实现。
- 链式存储方法对逻辑上相邻的元素不要求其物理位置相邻,元素间的逻辑关系通过附设的指针字段来表示,由此得到的存储表示称为链式存储结构,链式存储结构通常借助于程序设计语言中的指针类型来实现。
除了通常采用的顺序存储方法和链式存储方法外,有时为了查找的方便还采用
- 索引存储方法
- 列存储方法。
八、算法
- 算法:是指解题方案的准确而完整的描述。
- 算法的基本特征:是一组严谨地定义运算顺序的规则,每一个规则都是有效的,是明确的,此顺序将在有限的次数下终止。特征包括:
(1)可行性;
(2)确定性,算法中每一步骤都必须有明确定义,不充许有模棱两可的解释,不允许有多义性;
(3)有穷性,算法必须能在有限的时间内做完,即能在执行有限个步骤后终止,包括合理的执行时间的含义;
(4)拥有足够的情报。 - 算法的基本要素:
- 一是对数据对象的运算和操作;
- 二是算法的控制结构。
- 指令系统:一个计算机系统能执行的所有指令的集合。
- 基本运算和操作包括:算术运算、逻辑运算、关系运算、数据传输。
- 算法的控制结构:顺序结构、选择结构、循环结构。
- 算法基本设计方法:列举法、归纳法、递推、递归、减斗递推技术、回溯法。
- 算法复杂度:算法时间复杂度和算法空间复杂度。
- 算法时间复杂度是指执行算法所需要的计算工作量。
- 算法空间复杂度是指执行这个算法所需要的内存空间。
(1) 时间复杂度
- 设问题的规模为n,把一个算法的时间耗费T(n)称为该算法的时间复杂度,它是问题规模为n的函数。
常用的算法的时间复杂度的顺序:(比较时只看最高次幂)
执行次数函数 | 阶 | 非正式术语 |
---|---|---|
12 | O(1) | 常数阶 |
2n+3 | O(n) | 线性阶 |
3n2+2n+1 | O(n2) | 平方阶 |
5log2n+20 | O(log2n) | 对数阶 |
2n+3nlog2n+19 | O(nlog2n) | NlogN阶 |
6n3+2n2+3n+4 | O(n3) | 立方阶 |
2n | O(2n) | 指数阶 |
for ( i = 1 , i < = 10 , i++ ) x=x+c; =>O(1)
for ( i = 1 , i < = n , i++ ) x=x+n; =>O(n)
多嵌套一个for,则为 =>O(n^2) 以此类推
真题难点:i = 1,while(i < = n)
i = i * 3;=>O(log3^n)
i = i * 2;=>O(log2^n) 以此类推
九、线性表
- 线性表由一组数据元素构成,数据元素的位置只取决于自己的序号,元素之间的相对位置是线性的。
- 在复杂线性表中,由若干项数据元素组成的数据元素称为记录,而由多个记录构成的线性表又称为文件。
非空线性表的结构特征:- (1)且只有一个根结点a1,它无前件;
- (2)有且只有一个终端结点an,它无后件;
- (3)除根结点与终端结点外,其他所有结点有且只有一个前件,也有且只有一个后件。结点个数n称为线性表的长度,当n=0时,称为空表。
- 线性表的顺序存储结构具有以下两个基本特点:
(1)线性表中所有元素的所占的存储空间是连续的;
(2)线性表中各数据元素在存储空间中是按逻辑顺序依次存放的。
ai
的存储地址为:adr(ai)=adr(a1)+(i-1)k,
,adr(a1)
为第一个元素的地址,k代表每个元素占的字节数。
顺序表的运算:插入、删除。
十、线性链表
- 数据结构中的每一个结点对应于一个存储单元,这种存储单元称为存储结点,简称结点。
- 结点由两部分组成:
- (1)用于存储数据元素值,称为数据域;
- (2)用于存放指针,称为指针域,用于指向前一个或后一个结点。
- 在链式存储结构中,
- 存储数据结构的存储空间可以不连续,
- 各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的。
- 链式存储方式即可用于表示线性结构,也可用于表示非线性结构。
线性链表,head称为头指针,head=null(或0)称为空表,如果是两指针:左指针(llink)指向前件结点,右指针(rlink)指向后件结点。
线性链表的基本运算:查找、插入、删除。
单链表
- 指针域中存储的信息称做指针或链。N个结点链结成一个链表,由于此链表的每一个结点中包含一个指针域,故又称线性链表或单链表。
循环链表
- 循环链表是单链表的变形
循环链表最后一个结点的next指针不为空,而是指向了表的前端。为简化操作,在循环链表中往往加入表头结点。
循环链表的特点是:只要知道表中某一结点的地址,就可搜寻到所有其他结点的地址。
双向链表
-
双向链表是指在前驱和后继方向都能游历(遍历)的线性链表。
-
在双向链表结构中,每一个结点除了数据域外,还包括两个指针域,一个指针指向该结点的后继结点,另一个指针指向它的前趋结点。通常采用带表头结点的循环链表形式。
用指针实现表
- 用数组实现表时,利用了数组单元在物理位置上的邻接关系表示表元素之间的逻辑关系。
- 优点是:
- 无须为表示表元素之间的逻辑关系增加额外的存储空间。
- 可以方便地随机存取表中任一位置的元素。
- 缺点是:
- 插入和删除运算不方便,除表尾位置外,在表的其他位置上进行插入或删除操作都须移动大量元素,效率较低。
- 由于数组要求占用连续的存储空间,因此在分配数组空间时,只能预先估计表的大小再进行存储分配。当表长变化较大时,难以确定数组的合适的大小
- 优点是:
顺序表与链表的比较
- 顺序表的存储空间可以是静态分配的,也可以是动态分配的。链表的存储空间是动态分配的。顺序表可以随机或顺序存取。
- 链表只能顺序存取。顺序表进行插入/删除操作平均需要移动近一半元素。链表则修改指针不需要移动元素。
若插入/删除仅发生在表的两端,宜采用带尾指针的循环链表。存储密度=结点数据本身所占的存储量/结点结构所占的存储总量。顺序表的存储密度= 1,链表的存储密度< 1。
总结:顺序表是用数组实现的,链表是用指针实现的。用指针来实现的链表,结点空间是动态分配的,链表又按链接形式的不同,区分为单链表、双链表和循环链表。
十一、栈和队列
栈:是限定在一端进行插入与删除的线性表,允许插入与删除的一端称为栈顶,不允许插入与删除的另一端称为栈底。
- 栈按照“先进后出”(filo)或“后进先出”(lifo)组织数据,
- 栈具有记忆作用。用top表示栈顶位置,用bottom表示栈底。
- 栈的基本运算:(1)插入元素称为入栈运算;(2)删除元素称为退栈运算;(3)读栈顶元素是将栈顶元素赋给一个指定的变量,此时指针无变化。
队列:指允许在一端(队尾)进入插入,而在另一端(队头)进行删除的线性表。rear指针指向队尾,front指针指向队头。
- 队列是“先进行出”(fifo)或“后进后出”(lilo)的线性表。
- 队列运算包括(1)入队运算:从队尾插入一个元素;(2)退队运算:从队头删除一个元素。
循环队列:s=0表示队列空,s=1且front=rear表示队列满
十二、栈
栈:是限定仅在表尾进行插入或删除操作的线性表。栈是一种后进先出(Last In First Out)/先进后出的线性表,简称为LIFO
(1)用指针实现栈—链(式)栈链式栈
-
无栈满问题,空间可扩充
-
插入与删除仅在栈顶处执行
-
链式栈的栈顶在链头
-
适合于多栈操作
链栈的基本操作
- 1)进栈运算
- 进栈算法思想:
- 1)为待进栈元素x申请一个新结点,并把x赋给 该结点的值域。
- 2)将x结点的指针域指向栈顶结点。
- 3)栈顶指针指向x结点,即使x结点成为新的栈顶结点。
- 进栈算法思想:
具体算法如下:
SNode *Push_L(SNode * top,ElemType x)
{
SNode *p;
p=(SNode*)malloc(sizeof(SNode));
p->data=x;
p->next=top;
top=p;
return top;
}
2)出栈运算
- 出栈算法思想如下:
- 1)检查栈是否为空,若为空,进行错误处理。
- 2)取栈顶指针的值,并将栈顶指针暂存。
- 3)删除栈顶结点。
SNode *POP_L(SNode * top,ElemType *y)
{
SNode *p;
if(top==NULL) return 0;/*链栈已空*/
else{
p=top;
*y=p->data;
top=p->next; free(p);
return top;
}
3)取栈顶元素
void gettop(SNode *top)
{
if(top!=NULL)
return(top->data); /*若栈非空,则返回栈顶元素*/
else
return(NULL); /*否则,则返回NULL*/
}
十三、队列(Queue)
队列:是只允许在表的一端进行插入,而在另一端进行删除的运算受限的线性表。
- 所有的插入均限定在表的一端进行,该端称为队尾(Rear);
- 所有的删除则限定在表的另一端进行,该端则称为队头(Front)。
- 队列具有先进先出(First In First Out,简称FIFO)/后进后出特性。在程序设计中,比较典型的例子就是操作系统的作业排队。
- 队列的顺序存储结构称为顺序队列,顺序队列实际上是运算受限的顺序表,和顺序表一样,顺序队列也是必须用一个数组来存放当前队列中的元素。
- 由于队列的队头和队尾的位置是变化的,因而要设两个指针分别指示队头和队尾**元素在队列中的位置。
循环队列是为了克服顺序队列中“假溢出”,通常将一维数组Sq.elem[0]
到Sq.elem.[MaxSize-1
]看成是一个首尾相接的圆环,即Sq.elem[0]
与Sq.elem .[maxsize-1]
相接在一起。这种形式的顺序队列称为循环队列。 - 用线性链表表示的队列称为链队列。链表的第一个节点存放队列的队首结点,链表的最后一个节点存放队列的队尾首结点,队尾结点的链接指针为空。另外还需要两个指针(头指针和尾指针)才能唯一确定,头指针指向队首结点,尾指针指向队尾结点
- 由于队列的队头和队尾的位置是变化的,因而要设两个指针分别指示队头和队尾**元素在队列中的位置。
十四、 树与二叉树
- 树是一种简单的非线性结构,所有元素之间具有明显的层次特性。
- 在树结构中,每一个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点,简称树的根。每一个结点可以有多个后件,称为该结点的子结点。没有后件的结点称为叶子结点。
- 在树结构中,一个结点所拥有的后件的个数称为该结点的度,所有结点中最大的度称为树的度。树的最大层次称为树的深度。
- 二叉树的特点:(1)非空二叉树只有一个根结点;(2)每一个结点最多有两棵子树,且分别称为该结点的左子树与右子树。
- 二叉树的基本性质:
- 在二叉树的第
k
层上,最多有2k-1(k≥1)
个结点; - 深度为
m
的二叉树最多有2m-1
个结点; - 度为0的结点(即叶子结点)总是比度为2的结点多一个;
- 具有n个结点的二叉树,其深度至少为[log2n]+1,其中[log2n]表示取log2n的整数部分;
- 具有n个结点的完全二叉树的深度为[log2n]+1;
- 设完全二叉树共有n个结点。如果从根结点开始,按层序(每一层从左到右)用自然数1,2,….n给结点进行编号(k=1,2….n),有以下结论:
- 若k=1,则该结点为根结点,它没有父结点;若k>1,则该结点的父结点编号为
int(k/2)
; - 若
2k≤n
,则编号为k的结点的左子结点编号为2k
;否则该结点无左子结点(也无右子结点); - 若
2k+1≤n
,则编号为k的结点的右子结点编号为2k+1;否则该结点无右子结点。 - 满二叉树是指除最后一层外,每一层上的所有结点有两个子结点,则k层上有2k-1个结点深度为m的满二叉树有2m-1个结点。
- 完全二叉树是指除最后一层外,每一层上的结点数均达到最大值,在最后一层上只缺少右边的若干结点。
- 二叉树存储结构采用链式存储结构,对于满二叉树与完全二叉树可以按层序进行顺序存储。
- 若k=1,则该结点为根结点,它没有父结点;若k>1,则该结点的父结点编号为
- 二叉树的遍历:
(1)前序遍历(dlr),首先访问根结点,然后遍历左子树,最后遍历右子树;
(2)中序遍历(ldr),首先遍历左子树,然后访问根结点,最后遍历右子树;
(3)后序遍历(lrd)首先遍历左子树,然后访问遍历右子树,最后访问根结点。
- 在二叉树的第
十五、树
-
分支结点:度不为零的结点
-
内部结点:除根结点之外的分支结点
-
开始结点:根结点又称为开始结点
-
结点的高度:该结点到各结点的最长路径的长度
-
森林:
m(m≥0)
棵互不相交的树的集合。将一棵非空树的根结点删去,树就变成一个森林;
反之,给m棵独立的树增加一个根结点,并把这m棵树作为该结点的子树,森林就变成一棵树。
-
结点的层数和树的深度
- 结点的层数:根结点的层数为1,其余结点的层数等于其双亲结点的层数加1。
- 堂兄弟:双亲在同一层的结点互为堂兄弟。
- 树的深度:树中结点的最大层数称为树的深度。
注意:要弄清结点的度、树的度和树的深度的区别。
树中结点之间的逻辑关系是“一对多”的关系,树是一种非线性的结构
-
树的遍历
- 先序遍历:访问根结点——先序遍历根的左子树——先序遍历根的右子数
- 中序遍历:中序遍历左子树——访问根结点——中序遍历右子树
- 后序遍历:后序遍历左子树——后序遍历右子树——访问根结点
-
最优二叉树(哈夫曼树):一类带权路径长度最短的树,最小两结点数相加的值再与次小结点数合并。
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
- 路径长度:路径上的分支数目称作路径长度。
- 树的路径长度:从树根到每一结点的路径之和。
- 权:赋予某个实体的一个量,是对实体的某个或某些属性的数值描述。在数据结构中,实体有结点(元素)和边(关系)两大类,所以对应有结点权和边权。
- 结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。
- 树的带权路径长度:树中所有叶子结点的带权路径长之和。
- 哈夫曼树:假设有m个权值
{w1,w2,w3,...,wn}
,可以构造一棵含有n个叶子结点的二叉树,每个叶子结点的权为wi
,则其中带权路径长度WPL最小的二叉树称做最优二叉树或哈夫曼树。
已知一棵二叉树的前根序序列和中根序序列,构造该二叉树的过程如下:
-
根据前根序序列的第一个元素建立根结点;
-
在中根序序列中找到该元素,确定根结点的左右子树的中根序序列;
-
在前根序序列中确定左右子树的前根序序列;
-
由左子树的前根序序列和中根序序列建立左子树;
-
由右子树的前根序序列和中根序序列建立右子树。
已知一棵二叉树的后根序序列和中根序序列,构造该二叉树的过程如下:
-
根据后根序序列的最后一个元素建立根结点;
-
在中根序序列中找到该元素,确定根结点的左右子树的中根序序列;
-
在后根序序列中确定左右子树的后根序序列;
-
由左子树的后根序序列和中根序序列建立左子树;
-
由右子树的后根序序列和中根序序列建立右子树。
十六、图
G= ( V , E ) = ( 顶点,边)
无向完全图有n(n - 1)/ 2
个边 ,有向完全图有n(n - 1)
个边 。n表结点。
边无向()
,弧有向<>
迪杰斯特拉(Dijkstra)算法
- 典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。
- 以起始点为中心向外层层扩展,直到扩展到终点为止。
注意:该算法要求图中不存在负权边。
弗洛伊德(Floyd)算法<邻接矩阵求>
- 解决任意两点间的最短路径的一种算法,
- 可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。
- Floyd-Warshall算法的时间复杂度为O(N3),空间复杂度为O(N2)。
普里姆(Prim)算法
- 普里姆算法的基本思想:
- 从连通网络
N= {V,E}
中的某一顶点u0
出发,选择与它关联的具有最小权值的边(u0,v)
,将其顶点加入到生成树顶点集合S中。以后每一步从一个顶点在S中,另一个顶点不在S中的各条边中选择权值最小的边(u,v),把它的顶点加入到集合S中。如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合S中为止。
- 从连通网络
克鲁斯卡尔(Kruskal)算法
- 克鲁斯卡尔算法的基本思想:
- 设有一个有
n
个顶点的连通网络N= {V,E}
,最初先构造一个只有n个顶点,没有边的非连通图T= {V,∅}
,图中每个顶点自成一个连通分支。当在E中选到一条具有最小权值的边时,若该边的两个顶点落在不同的连通分支上,则将此边加入到T中;否则将此边舍去,重新选择一条权值最小的边。如此重复下去,直到所有顶点在同一个连通分支上为止。
- 设有一个有
1、图的基本概念
-
图的定义:G=(V,E),V为顶点集,E为边集。设图有n个顶点,V={v1,v2,v3,…,vn}
-
图的分类:
①有向图:图中任意两个顶点之间的边都是有向边。若顶点M到顶点N的边有方向,称这条边为有向边,也称为弧,用偶序对 < M, N >表示,M表示弧尾,N表示弧头 。
②无向图:图中任意两个顶点之间的边都是无向边。若顶点M到顶点N的边没有方向,称这条边为无向边,用无序偶对(M,N)或(N,M)表示。 -
度:
- 对于有向图来说,与某个顶点相关联的弧的数目称为度(TD);以某个顶点v为弧尾的弧的数目定义为顶点v的出度(OD);以顶点v为弧头的弧的数目定义为顶点的入度(ID) 。度(TD) = 出度(OD) + 入度(ID)。
- 对于无向图,假若顶点v和顶点w之间存在一条边,则称顶点v和顶点w互为邻接点,边(v,w)和顶点v和w相关联。顶点v的度是和v相关联的边的数目,记为TD(v);。
-
图连通:
- 对于有向图G中,如果每一对Vi,Vj,从Vi到Vj和从Vj到Vii都存在路径,则称G是强连通图。
- 对于无向图G,任意一对Vi,Vj都存在路径,则称G是连通图。
- 连通分量:指的是无向图中的极大连通子图。
2、图的存储
- 邻接矩阵和邻接表。这两种均可用于有向图和无向图,但各有优劣。
- 邻接矩阵:两个数组来表示图。一个一维的数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
- 对无向图而言,邻接矩阵一定是对称的,而且对角线一定为零(在此仅讨论无向简单图),有向图不一定如此。
- 在无向图中,任一顶点i的度为第i列所有元素的和,在有向图中顶点i的出度为第i行所有元素的和,而入度为第i列所有元素的和。
- 邻接表:是一种顺序分配和链式分配相结合的存储结构。
- 邻接表由表头结点和表结点两部分组成,其中图中每个顶点均对应一个存储在数组中的表头结点。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
- 表结点存放的是邻接顶点在数组中的索引。对于无向图来说,使用邻接表进行存储也会出现数据冗余,表头结点A所指链表中存在一个指向C的表结点的同时,表头结点C所指链表也会存在一个指向A的表结点。
- 在有向图中,描述每个点向别的节点连的边(点a->点b这种情况)。
- 在无向图中,描述每个点所有的边(点a-点b这种情况)
- 邻接矩阵:两个数组来表示图。一个一维的数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
- 十字链表和邻接多重表。
十字链表是有向图的一种链式存储结构,在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。
邻接多重表是无向图的另一种链式存储结构。在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边顶点同时链接在两个链表中。
邻接矩阵
//邻接矩阵存储结构定义
#define MaxVertexNum 100
typedef char VertexType;
typedef int EdgeType;
typedef struct{
VertexType Vex[MaxVertexNum];
EdgeType Edge[MaxVertexNum][MaxVertexNum];
int vexnum,arcnum;
}MGraph;
在简单应用中,可以直接用二维数组作为图的邻接矩阵(顶点信息等可省略)
邻接表
//邻接表存储结构定义
typedef struct ArcNode{ //边表结点
int adjvex;
struct ArcNode *next;
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data;
ArcNode *first;
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
图的邻接表表示不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,取决于建立邻接表的算法以及边的输入次序。
3、图的遍历
图的遍历是指对图中的所有顶点按照一定的顺序进行访问,且使每一个顶点仅被访问一次。遍历的方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索
//DFS
const int MAXV = 1000; //最大顶点数
const int INF = 1000000000; //设INF是一个很大的数
int n,G[MAXV][MAXV];
bool vis[MAXV] = {false};
void DFS(int u, int depth)
{
vis[u] = true;
for(int v = 0;v<n;v++)
{
if(vis[v]==false&&G[u][v]!=INF)
{
DFS(v,depth+1);
}
}
}
void DFSTrave()
{
for(int u=0;u<n;u++)
{
if(vis[u]==false)
{
DFS(u,1);
}
}
}
广度优先搜索
//BFS
bool inq[MAXV] = {false};
void BFS(int u)
{
queue<int>q;
q.push(u);
inq[u] = true;
while(!q.empty())
{
int u = q.front();
q.pop();
for(int v=0;v<n;v++)
{
if(inq[v]==false&&G[u][v]!=INF)
{
q.push(v);
inq[v] = true;
}
}
}
}
void BFSTrave()
{
for(int u=0;u<n;u++)
{
if(inq[u]==false)
{
BFS(u);
}
}
}
图的应用
图的应用主要有,最小生成树,最短路径,拓扑排序和关键路径。
最小生成树
解决最小生成树问题有两种算法:prim算法(普里姆算法,时间复杂度O(V*V)适合顶点数目较少而边较多的稠密图) 和 kruskal算法(克鲁斯卡尔算法,时间复杂度O(ElogE)适合边数目较少而顶点较多的稀疏图) 。
int map[505][505];
int visit[505];
int N ;//vertice number [0,N-1]
int prim(int path[],int dist[])//minimum spanning-tree
{
for(int i=0;i<N;i++)
{
dist[i] = map[0][i];
}
memset(visit,0,sizeof(visit));
visit[0] = 1;
for(int i=1;i<N;i++)
{
int min = INT_MAX,pos=-1;
for(int j=0;j<N;j++)
{
if(visit[j]==0&&dist[j]<min)
{
min = dist[j];
pos = j;
}
}
visit[pos] = 1;
for(int j=0;j<N;j++)
{
if(visit[j]==0&&map[pos][j]<dist[j])
{
dist[j] = map[pos][j];
path[j] = pos;
}
}
}
int sum = 0;
for(int i=0;i<N;i++)
{
sum+=dist[i];
if(dist[i]==INT_MAX)
{
return -1 ;//不存在最小生成树
}
}
return sum;
}
最短路径
单源最短路径问题使用DIjkstra算法(迪杰斯特拉算法)。
全源最短路径问题使用Floyd算法(弗洛伊德算法)。
int map[505][505];
int visit[505];
int N ;//vertice number [0,N-1]
int s; // start point
int e; //end point
void Dijkstra(int path[],int dist[])// shortest path
{
for(int i=0;i<N;i++)
{
dist[i] = map[s][i];
}
memset(visit,0,sizeof(visit));
visit[s] = 1;
for(int i=1;i<N;i++)
{
int min = INT_MAX,pos=-1;
for(int j=0;j<N;j++)
{
if(visit[j]==0&&dist[j]<min)
{
min = dist[j];
pos = j;
}
}
if(pos==-1)return;
visit[pos] = 1;
for(int j=0;j<N;j++)
{
if(visit[j]==0&&dist[pos]+map[pos][j]<dist[j])
{
dist[j] = dist[pos]+map[pos][j];
path[j] = pos;
}
}
}
}
十七、查找计数
- 顺序查找的使用情况:
- (1)线性表为无序表;
- (2)表采用链式存储结构。
二分法查找只适用于顺序存储的有序表,对于长度为n的有序线性表,最坏情况只需比较log2n次。
十八、排序计数
- 排序是指将一个无序序列整理成按值非递减顺序排列的有序序列。
- 交换类排序法:
- (1)冒泡排序法,需要比较的次数为n(n-1)/2;
- (2)快速排序法。
- 插入类排序法:
- (1)简单插入排序法,最坏情况需要n(n-1)/2次比较;
- (2)希尔排序法,最坏情况需要o(n1.5)次比较。
- 选择类排序法:、
- (1)简单选择排序法, 最坏情况需要n(n-1)/2次比较;
- (2)堆排序法,最坏情况需要o(nlog2n)次比较。
- 交换类排序法:
排序方法 | 平均时间 | 最坏情况 | 辅助存储 | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
简单排序 | O(n2) | O(n2) | O(1) | 稳定 |
直接插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
希尔排序 | O(nlog2n)~O(n2) | O(n2) | O(1) | 不稳定 |
快速排序 | O(nlog2n) | O(n2) | O(log2n) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
排序小结
- 就平均时间性能而言,快速排序最佳。但在最坏情况下不如堆排序和归并排序。(归并排序对n较大时适用)
- 当序列中的记录“基本有序”或n值较小时,直接插入排序是最佳的方法,因此常将它与其他排序方法结合使用,如快速排序、归并排序等。
- 基数排序的时间复杂度也可写成O(d*n),因此它最适用于n值很大而关键字较小的序列。
- 稳定的排序方法:简单排序。不稳定的排序方法:快速排序、堆排序。
一般来说,排序过程中的“比较”是在相邻的两个记录的关键字之间进行的排序方法是稳定的。
-
排序算法大体可分为两种:
- 一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
- 另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
-
排序算法稳定性的简单形式化定义为**如果
Ai = Aj
,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。**保证排序前后两个相等的数的相对顺序不变。- 例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。
-
**排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。**基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
冒泡排序(Bubble Sort)
- 冒泡排序算法的运作如下:
- 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void BubbleSort(int A[], int n)
{
for (int j = 0; j < n - 1; j++) // 每次最大元素就像气泡一样"浮"到数组的最后
{
for (int i = 0; i < n - 1 - j; i++) // 依次比较相邻的两个元素,使较大的那个向后移
{
if (A[i] > A[i + 1]) // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法
{
Swap(A, i, i + 1);
}
}
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大冒泡排序
int n = sizeof(A) / sizeof(int);
BubbleSort(A, n);
printf("冒泡排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行冒泡排序的实现过程如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3umHhFFV-1614861368097)(C:\Users\Administrator\Desktop\interview_python-master\img\冒泡排序.gif)]
使用冒泡排序为一列数字进行排序的过程如右图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RbnHUaeF-1614861368165)(C:\Users\Administrator\Desktop\interview_python-master\img\冒泡排序1.gif)]
冒泡排序的改进:鸡尾酒排序
鸡尾酒排序,也叫定向冒泡排序,是冒泡排序的一种改进。此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能。
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void CocktailSort(int A[], int n)
{
int left = 0; // 初始化边界
int right = n - 1;
while (left < right)
{
for (int i = left; i < right; i++) // 前半轮,将最大元素放到后面
{
if (A[i] > A[i + 1])
{
Swap(A, i, i + 1);
}
}
right--;
for (int i = right; i > left; i--) // 后半轮,将最小元素放到前面
{
if (A[i - 1] > A[i])
{
Swap(A, i - 1, i);
}
}
left++;
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大定向冒泡排序
int n = sizeof(A) / sizeof(int);
CocktailSort(A, n);
printf("鸡尾酒排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
使用鸡尾酒排序为一列数字进行排序的过程如右图所示:
选择排序(Selection Sort)
- 选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
- 注意选择排序与冒泡排序的区别:
- 冒泡排序通过依次交换相邻两个顺序不合法的元素位置,从而将当前最小(大)元素放到合适的位置;
- 选择排序每遍历一次都记住了当前最小(大)元素的位置,最后仅需一次交换操作即可将其放到合适的位置。
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void SelectionSort(int A[], int n)
{
for (int i = 0; i < n - 1; i++) // i为已排序序列的末尾
{
int min = i;
for (int j = i + 1; j < n; j++) // 未排序序列
{
if (A[j] < A[min]) // 找出未排序序列中的最小值
{
min = j;
}
}
if (min != i)
{
Swap(A, min, i); // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
}
}
}
int main()
{
int A[] = { 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }; // 从小到大选择排序
int n = sizeof(A) / sizeof(int);
SelectionSort(A, n);
printf("选择排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代码对序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }进行选择排序的实现过程如右图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ed5SIUxS-1614861368177)(C:\Users\Administrator\Desktop\interview_python-master\img\选择排序.gif)]
选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。
比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。
插入排序(Insertion Sort)
- 对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。
- 插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
插入排序的代码如下:
#include <stdio.h>
// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void InsertionSort(int A[], int n)
{
for (int i = 1; i < n; i++) // 类似抓扑克牌排序
{
int get = A[i]; // 右手抓到一张扑克牌
int j = i - 1; // 拿在左手上的牌总是排序好的
while (j >= 0 && A[j] > get) // 将抓到的牌与手牌从右向左进行比较
{
A[j + 1] = A[j]; // 如果该手牌比抓到的牌大,就将其右移
j--;
}
A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
}
}
int main()
{
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序
int n = sizeof(A) / sizeof(int);
InsertionSort(A, n);
printf("插入排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行插入排序的实现过程如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k82HLATc-1614861368179)(C:\Users\Administrator\Desktop\interview_python-master\img\插入排序.gif)]
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
插入排序的改进:二分插入排序
对于插入排序,如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的次数,我们称为二分插入排序,代码如下:
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
void InsertionSortDichotomy(int A[], int n)
{
for (int i = 1; i < n; i++)
{
int get = A[i]; // 右手抓到一张扑克牌
int left = 0; // 拿在左手上的牌总是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右边界进行初始化
while (left <= right) // 采用二分法定位新牌的位置
{
int mid = (left + right) / 2;
if (A[mid] > get)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i - 1; j >= left; j--) // 将欲插入新牌位置右边的牌整体向右移动一个单位
{
A[j + 1] = A[j];
}
A[left] = get; // 将抓到的牌插入手牌
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大二分插入排序
int n = sizeof(A) / sizeof(int);
InsertionSortDichotomy(A, n);
printf("二分插入排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
插入排序的更高效改进:希尔排序(Shell Sort)
希尔排序,也叫递减增量排序,是插入排序的一种更高效的改进版本。希尔排序是不稳定的排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n^2)的排序(冒泡排序或直接插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
希尔排序的代码如下:
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
// 最优时间复杂度 ---- O(n)
// 平均时间复杂度 ---- 根据步长序列的不同而不同。
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void ShellSort(int A[], int n)
{
int h = 0;
while (h <= n) // 生成初始增量
{
h = 3 * h + 1;
}
while (h >= 1)
{
for (int i = h; i < n; i++)
{
int j = i - h;
int get = A[i];
while (j >= 0 && A[j] > get)
{
A[j + h] = A[j];
j = j - h;
}
A[j + h] = get;
}
h = (h - 1) / 3; // 递减增量
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大希尔排序
int n = sizeof(A) / sizeof(int);
ShellSort(A, n);
printf("希尔排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
以23, 10, 4, 1的步长序列进行希尔排序: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQcZDSfm-1614861368180)(C:\Users\Administrator\Desktop\interview_python-master\img\希尔排序.gif)]
**希尔排序是不稳定的排序算法,**虽然一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2时分成两个子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二个子序列中的8在前面,现在对两个子序列进行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,两个8的相对次序发生了改变。
归并排序(Merge Sort)
- 归并排序的实现分为递归实现与非递归(迭代)实现。
- 递归实现的归并排序是算法设计中分治策略的典型应用,我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行是两两归并,然后四四归并,然后是八八归并,一直下去直到归并了整个数组。
- 归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作,归并操作步骤如下:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
归并排序的代码如下:
#include <stdio.h>
#include <limits.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定
void Merge(int A[], int left, int mid, int right)// 合并两个已排好序的数组A[left...mid]和A[mid+1...right]
{
int len = right - left + 1;
int *temp = new int[len]; // 辅助空间O(n)
int index = 0;
int i = left; // 前一数组的起始元素
int j = mid + 1; // 后一数组的起始元素
while (i <= mid && j <= right)
{
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++]; // 带等号保证归并排序的稳定性
}
while (i <= mid)
{
temp[index++] = A[i++];
}
while (j <= right)
{
temp[index++] = A[j++];
}
for (int k = 0; k < len; k++)
{
A[left++] = temp[k];
}
}
void MergeSortRecursion(int A[], int left, int right) // 递归实现的归并排序(自顶向下)
{
if (left == right) // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
return;
int mid = (left + right) / 2;
MergeSortRecursion(A, left, mid);
MergeSortRecursion(A, mid + 1, right);
Merge(A, left, mid, right);
}
void MergeSortIteration(int A[], int len) // 非递归(迭代)实现的归并排序(自底向上)
{
int left, mid, right;// 子数组索引,前一个为A[left...mid],后一个子数组为A[mid+1...right]
for (int i = 1; i < len; i *= 2) // 子数组的大小i初始为1,每轮翻倍
{
left = 0;
while (left + i < len) // 后一个子数组存在(需要归并)
{
mid = left + i - 1;
right = mid + i < len ? mid + i : len - 1;// 后一个子数组大小可能不够
Merge(A, left, mid, right);
left = right + 1; // 前一个子数组索引向后移动
}
}
}
int main()
{
int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 从小到大归并排序
int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n1 = sizeof(A1) / sizeof(int);
int n2 = sizeof(A2) / sizeof(int);
MergeSortRecursion(A1, 0, n1 - 1); // 递归实现
MergeSortIteration(A2, n2); // 非递归实现
printf("递归实现的归并排序结果:");
for (int i = 0; i < n1; i++)
{
printf("%d ", A1[i]);
}
printf("\n");
printf("非递归实现的归并排序结果:");
for (int i = 0; i < n2; i++)
{
printf("%d ", A2[i]);
}
printf("\n");
return 0;
}
上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行归并排序的实例如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ms14jNeu-1614861368183)(C:\Users\Administrator\Desktop\interview_python-master\img\归并排序.gif)]
堆排序(Heap Sort)
- 堆排序是指利用堆这种数据结构所设计的一种选择排序算法。
- 堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。
我们可以很容易的定义堆排序的过程:
- 由输入的无序数组构造一个最大堆,作为初始的无序区
- 把堆顶元素(最大值)和堆尾元素互换
- 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
- 重复步骤2,直到堆的尺寸为1
堆排序的代码如下:
#include <stdio.h>
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void Heapify(int A[], int i, int size) // 从A[i]向下进行堆调整
{
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 选出当前结点与其左右孩子三者之中的最大值
if (left_child < size && A[left_child] > A[max])
max = left_child;
if (right_child < size && A[right_child] > A[max])
max = right_child;
if (max != i)
{
Swap(A, i, max); // 把当前结点和它的最大(直接)子节点进行交换
Heapify(A, max, size); // 递归调用,继续从当前结点向下进行堆调整
}
}
int BuildHeap(int A[], int n) // 建堆,时间复杂度O(n)
{
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
Heapify(A, i, heap_size);
return heap_size;
}
void HeapSort(int A[], int n)
{
int heap_size = BuildHeap(A, n); // 建立一个最大堆
while (heap_size > 1) // 堆(无序区)元素个数大于1,未完成排序
{
// 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
// 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
Swap(A, 0, --heap_size);
Heapify(A, 0, heap_size); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大堆排序
int n = sizeof(A) / sizeof(int);
HeapSort(A, n);
printf("堆排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
堆排序算法的演示: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DErJyEYT-1614861368185)(C:\Users\Administrator\Desktop\interview_python-master\img\堆排序.gif)]
动画中在排序过程之前简单的表现了创建堆的过程以及堆的逻辑结构。
堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。
比如序列:{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序。
快速排序(Quick Sort)
- 在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见
- 事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
- 从序列中挑出一个元素,作为"基准"(pivot).
- 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
- 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
快速排序 的代码如下:
#include <stdio.h>
// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
// 稳定性 ---------- 不稳定
void Swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
int Partition(int A[], int left, int right) // 划分函数
{
int pivot = A[right]; // 这里每次都选择最后一个元素作为基准
int tail = left - 1; // tail为小于基准的子数组最后一个元素的索引
for (int i = left; i < right; i++) // 遍历基准以外的其他元素
{
if (A[i] <= pivot) // 把小于等于基准的元素放到前一个子数组末尾
{
Swap(A, ++tail, i);
}
}
Swap(A, tail + 1, right); // 最后把基准放到前一个子数组的后边,剩下的子数组既是大于基准的子数组
// 该操作很有可能把后面元素的稳定性打乱,所以快速排序是不稳定的排序算法
return tail + 1; // 返回基准的索引
}
void QuickSort(int A[], int left, int right)
{
if (left >= right)
return;
int pivot_index = Partition(A, left, right); // 基准的索引
QuickSort(A, left, pivot_index - 1);
QuickSort(A, pivot_index + 1, right);
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 从小到大快速排序
int n = sizeof(A) / sizeof(int);
QuickSort(A, 0, n - 1);
printf("快速排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
[](javascript:void(0)😉
使用快速排序法对一列数字进行排序的过程: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IFC9kuN-1614861368189)(C:\Users\Administrator\Desktop\interview_python-master\img\快速排序.gif)]
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?
答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。
十九、查找
1、(基础要点) 查找的基本概念
- 查找:就是在数据集合中寻找满足某种条件的数据对象。
- 关键码:在每个对象中有若干属性,其中有一个属性,其值可唯一地标识这个对象,称为关键码。
- 静态搜索:搜索结构在插入和删除等操作的前后不发生改变。
- 动态搜索:为保持高的搜索效率,搜索结构在执行插入和删除等操作的前后将自动进行调整,结构可能发生变化。
- 静态:有序查找、折半查找、斐波那契搜索
- 动态:有序查找、折半(跳表)、非线性-树
- 查找表:用于查找的数据集合称为查找表,它由同一类型的数据元素组成,可以是一个数组或链表等数据类型。
- 静态查找表和动态查找表: 若在查找的过程中还要对表进行修改(如插入和删除),则相应的表称之为动态查找表,否则称之为静态查找表。
2、(基础要点) 静态查找
2.1 查找对象—线性表
线性表查找的主要方法有
- 顺序查找、
- 折半查找
- 分块查找。
2.2 顺序查找
- 从表的一端开始,顺序扫描线性表,依次将关键字和给定的值进行比较,若当前扫描到的记录的关键字和给定的值相等,则返回成功;若扫描结束后,仍未找到关键字与给定的值相等的记录,则查找失败。
//顺序查找函数
int SeqSearch(SeqList R,int n,KeyType K){
int i= 0;
while(i<n && R[i].ley != k){
i++;
}
if(i>=n){
return -1;
}else{
return i;
}
}
-
成功平均时间复杂度为:O(n);
-
不成功平均查找长度为:n。
注意:顺序查找方法既适用于线性表的顺序存储结构,也适用于线性表的链式存储结构。
2.3 折半查找
- 折半查找又称二分查找,要求线性表是有序的,即表中记录按关键字有序(假设是递增的)。
- 查找对象:有序表
- 折半查找的基本思路是:首先用要查找的关键字K与中间位置的节点的关键字相比较,这个中间记录把线性表分成俩个子表,若比较结果相等则查找完成,若不相等,再根据K与该中间记录关键字的比较大小确定下一步查找哪一个子表,这样递归进行下去,直到找到满足条件的记录或者该线性表中没有这样的记录 。
//折半查找函数,假设该表为递增有序的
int Binsearck(SeqList R,int n,KeyType K){
int left = 0;
int right = n-1;
int mid = left + right/2;
if(R[mid ].key == K){
return mid;
}else if(R[mid ].key > K){
right = mid-1;
}else{
left = mid+1;
}
return -1;
}
- 折半查找的过程可用二叉树来描述,把当前查找区间的中间位置上的记录作为根,左子表和右子表中的记录分别作为根的左子树和右子树,由此得到的二叉树,称为描述半查找的判断树或描述树。
- 成功的折半查找过程恰好是走一条从判断树的根到被查记录的路径,记录比较次数恰为该记录树的层数。
注意:因为折半查找办法需要方便地定位查找区域,所以适合折半查找的存储结构必须具有随机存取特性,因此该查找方法适合于线性表的顺序存储结构,不适合链式存储结构,且要求元素按关键字有序排序。
2.4 斐波那契查找(折半查找的改进)
-
针对的查找对象:有序表
-
1、将所要被查找的数组a[]大小先扩充到斐波那契数列中最接近的数,扩充方法是用数组最后一个元素的值去填充扩充的位置。
-
2、根据斐波那契数列规则:F[k] = F[k-1] + F[k-2]来分割要查找的数组a[],取a[ start + F[k-1] - 1] 为中间位置指针。
2.4 插值查找(折半查找的改进)
-
插值查找是在折半查找的基础上进行优化,将mid的值
修改为
将查找关键字于查找表中的最大最小关键字对比后进行查找。
时间复杂度为O(log n)
3、动态查找
3.1 动态查找的概念
- 动态查找表:表结构在查找过程中动态生成。
- 要求:对于给定值key, 若表中存在其关键字等于key的记录,则查找成功返回(或者删除之);否则插入关键字等于key 的记录。
3.2 动态查找表
3.2.1二叉排序树的定义
- 二叉排序树的定义(Binary Sort Tree或Binary Search Tree):二叉排序树或者是一棵空树,或者是满足下列性质的二叉树:
- (1)若左子树不为空,则左子树上的所有结点的值(关键字)都小于根节点的值;
- (2)若右子树不为空,则右子树上的所有结点的值(关键字)都大于根节点的值;
- (3)左、右子树都分别为二叉排序树。
该图中的树就是一棵二叉排序树。任何一个非叶子结点的左子树上的结点值都小于根结点,右子树上的结点值都大于根结点的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGSCbVbk-1614861368191)(C:\Users\Administrator\Desktop\interview_python-master\img\二叉排序树.png)]
结论:若按中序遍历一棵二叉排序树,所得到的结点序列是一个递增序列。
3.2.2 二叉排序树(BST树)的查找思想
- BST树的查找思想:
- (1)首先将给定的K值与二叉排序树的根节点的关键字进行比较:若相等,则查找成功;
- (2)若给定的K值小于BST树的根节点的关键字:继续在该节点的左子树上进行查找;
- (3)若给定的K值大于BST树的根节点的关键字:继续在该节点的右子树上进行查找。
3.2.3 二叉排序树总结
(1)查找过程与顺序结构有序表中的折半查找相似,查找效率高;
(2)中序遍历此二叉树,将会得到一个关键字的有序序列(即实现了排序运算);
(3)如果查找不成功,能够方便地将被查元素插入到二叉树的叶子结点上,而且插入或删除时只需修改指针而不需移动元素。
3.3 平衡二叉树的介绍
3.3.1 二叉排序树存在的问题
- 问题:
- 左子树全部为空,从形式上看,更像一个单链表。
- 插入速度没有影响 查询速度明显降低(因为需要依次比较), 不能发挥BST 的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
3.3.2 平衡二叉树
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
- 平衡二叉树有以下特点:
- 它是一 棵空树。
- 假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
- 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
- 平衡因子:左子树的高度减去右子树的高度。由平衡二叉树的定义可知,平衡因子的取值只可能为0,1,-1.分别对应着左右子树等高,左子树比较高,右子树比较高。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dnxBo9S-1614861368193)(C:\Users\Administrator\Desktop\interview_python-master\img\平衡二叉树.png)]
3.3.3 平衡二叉树的创建
- 每当在平衡二叉树中插入或者删除一个节点的时候,首先检查其插入路径上的节点是否因为此次插入或者删除操作而导致了不平衡,如果导致了不平衡,那么就先找到插入路径上面距离插入节点最近的平衡因子的绝对值大于1的节点A,再对以节点A为根的子树,在保证二叉树特性的前提下,调整各个节点的位置,使之重新达到平衡状态。
针对平衡二叉树的调整,一共有四种方式进行调整。
- LL旋转(右单旋转):这是因为在节点A的左孩子B上的左子树上面插入了新节点,导致A节点的平衡因子增大,所以要进行LL旋转。
- 旋转方式:以节点B为旋转轴,将节点B旋转到其父节点的位置,B的右子树作为节点A的左子树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLq4QhaS-1614861368194)(C:\Users\Administrator\Desktop\interview_python-master\img\右单旋转.png)]
- RR旋转(左单旋转):由于在节点A的右孩子B的右子树R上插入节点,A节点的平衡因子由-1变为-2,所以需要进行调整。
- 调整方法:以B节点为旋转轴进行旋转,B节点的左子树作为A节点的右子树。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZPmOtjWi-1614861368196)(C:\Users\Administrator\Desktop\interview_python-master\img\左单旋转.png)]
- LR平衡旋转(先左后右双旋转):由于在A节点的左孩子的右子树R上面插入新节点,导致A节点的平衡因子由1变为2,需要进行两次旋转。
- 旋转方法:先向左旋转,后向右旋转,先将A节点的左孩子节点B的右孩子C提升到B节点的位置,然后再把C节点向右旋转提升到C的位置即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVHaoXxR-1614861368197)(C:\Users\Administrator\Desktop\interview_python-master\img\LR平衡旋转.png)]
- RL平衡旋转(先右后左双旋转):由于在A节点的右孩子的左子树上面添加新节点,导致A节点的平衡因子由-1变为-2,所以需要进行双旋转。
- 调整方法:先右旋转后左旋转,先将A节点的右孩子B的左孩子节点C向右旋转提升到B的位置,然后在将C进行向左旋转提升到A的位置即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JNZlFsJ7-1614861368198)(C:\Users\Administrator\Desktop\interview_python-master\img\RL平衡旋转.png)]
3.3.4 平衡二叉树的查找
平衡二叉树的查找过程和二叉排序树的查找过程一样,在查找过程中,和关键字比较的次数不会超过树的最大深度,假设以标示高度为
的平衡二叉树的最少节点数,那么有
,并且有
,可以证明,含有
个节点的平衡二叉树的最大深度是
,所以说平衡二叉树的平均查找长度是
。
3.3.4代码实现
平衡二叉树的节点类型
class AvlNode{
private int data;
AvlNode left;
AvlNode right;
public AvlNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
@Override
public String toString() {
return "AvlNode{" +
"data=" + data +
'}';
}
}
LL旋转(单右旋转)
- 旋转步骤:
- 以当前节点的值创建一个新的节点newNode。
- 把新节点的右子树设置为当前节点的右子树。
- 把新节点的左子树设置为当前节点左孩子的右子树。
- 把当前节点的值设置为当前节点左子树的值。
- 把当前节点的左子树设置为当前节点左子树的左子树。
- 把当前节点的右子树设置为新的节点。
/**
* 对AVL树进行右旋转,因为左边的子树高度大于右边子树的高度
*/
public void rightRotate(){
// 以当前节点的值创建一个新的节点
AvlNode newNode=new AvlNode(this.data);
newNode.right=this.right;
newNode.left=this.left.right;
this.data=this.left.data;
this.left=this.left.left;
this.right=newNode;
}
RR旋转(单左旋转)
- 旋转步骤:(当前节点指的是发生不平衡的那个节点)
- 以当前节点的值创建一个新的节点newNode。
- 把新节点的左子树设置为当前节点的左子树
- 把新节点的右子树设置为当前节点右节点的左子树。
- 把当前节点的值换为右子节点的值。
- 把当前节点的右子树设置为右子树的右子树。
- 把当前节点的左子树设置为新节点。
/**
* 对avl树进行左旋转,因为右边的子树高度太高,需要降低
*/
public void leftRotate(){
// 创建一个新的节点,存储当前节点的值
AvlNode newNode=new AvlNode(this.data);
// 把新的节点的左子树设置为当前节点的左子树
newNode.left=this.left;
// 把新节点的右子树设置为当前节点右子树的左子树
newNode.right=this.right.left;
// 把当前节点的值替换成右子节点的值
this.data=right.data;
// 当前节点的右子树设置成当前节点的右子树的右子树
this.right=this.right.right;
// 当前节点的左子节点设置成新的节点
left=newNode;
}
向avl树中添加一个节点
注意:我们在上面创建平衡二叉树的过程中有四种旋转方式,在这里实现过程中,另外两种双旋转其实也可以分解为单旋转,只是多做一次判断而已,详情请看代码。
/**
* 向二叉排序树中添加一个节点
* @param node 需要添加的节点
*/
public void add(AvlNode node){
// 判断添加的节点是否是空
if(node == null){
return ;
}
// 判断当前节点的值和根节点的值大小
if(node.getData() < this.getData()){
if(this.left == null){
// 如果左子节点为空,直接挂上去即可
this.left=node;
}else {
// 如果不是空,就递归进行向左子树添加
this.left.add(node);
}
}else {
// 判断左子树是否是空,空的话直接添加节点
if(this.right == null){
this.right=node;
}else {
// 不空的话递归在左子树进行添加
this.right.add(node);
}
}
// 当添加完一个节点后,如果右子树的高度-左子树的高度绝对值大于1,就进行调整
// 发生左旋转,,也就是右子树的高度比较高
if(this.rightHeight()-this.leftHeight()>1){
// 如果右子树的左子树的高度大于他的右子树的右子树的高度
// 需要先对右子节点进行右旋转,然后对当前节点进行左旋转
if(right!= null && right.rightHeight()<right.leftHeight()){
// 先右旋转
this.right.rightRotate();
// 左旋转
this.leftRotate();
}else {
this.leftRotate();
}
// 每次添加一个节点判断一次,所以此if语句如果执行,那么结束后必须retrurn
// 不能再向下判断
return;
}
// 当添加完一个节点,发现左子树的高度-右子树的高度的差值>1,说明左子树比右子树高
// 也就是需要进行右旋转操作
if(leftHeight()-rightHeight()>1){
if(this.left!= null && left.rightHeight()>left.leftHeight()){
//先要对当前节点的左节点进行向左的旋转,然后在向右旋转
// 这里针对当前节点的左子节点进行向左旋转
left.leftRotate();
// 针对当前节点进行向右旋转
this.rightRotate();
}else {
// 否则直接进行右旋转
this.rightRotate();
}
}
}
求左右子树的高度
在向平衡二叉树中添加节点的额过程中,我们总是在求子数的高度差,所以在这里建立两个求子数高度的函数。
/**
* 返回右子树的高度
* @return 右子树的高度
*/
public int rightHeight(){
if(right == null){
return 0;
}else {
return right.height();
}
}
/**
* 返回左子树的高度
* @return 左子树的高度
*/
public int leftHeight(){
if(left == null){
return 0;
}else {
return left.height();
}
}
求平衡二叉树的高度
/**
* 返回以当前节点为根的子树的高度
* @return 返回子树的高度
*/
public int height(){
return Math.max(left == null?0:this.left.height(),right == null?0:this.right.height())+1;
}
中序遍历二叉树
// 中序遍历二叉树
public void midOrder(){
if(this == null){
return;
}
if(this.left != null){
this.left.midOrder();
}
System.out.println(this.getData());
if(this.right != null){
this.right.midOrder();
}
}
创建一颗平衡二叉树
class AVLTree{
private AvlNode root;
/**
* 向二叉排序树中添加一个节点
* @param node 待添加而定节点
*/
public void add(AvlNode node){
if(this.root == null){
this.root=node;
}else {
this.root.add(node);
}
}
/**
* 中序遍历二叉树
*/
public void midOrder(){
if(this.root == null){
System.out.println("当前二叉排序树是空树!");
return;
}else {
this.root.midOrder();
}
}
public AvlNode getRoot() {
return root;
}
}
3.4 B-树
3.4.1 m阶B-树的定义
-
i)B-树又称为多路平衡查找树,是一种组织和维护外存文件系统非常有效的数据结构。B-树中所有节点的最大值称为B-树的阶,通常用m表示,从查找速率考虑,要求m>=3.一棵m阶书或者是一棵空树,或者是满足下列要求的m次树:
-
i)所有的叶子节点在同一层,并且不带信息。
-
ii)树中每个节点至多有m课子树(即至多含有m-1个关键字)
-
iii)若根节点不是终端节点,则根节点至少有俩棵子树。
-
iv)除根节点外,其他非叶子节点至少有[m/2]棵子树(即至少含有[m/2]-1个关键字)。
-
v)每个非叶子节点的结构是:
n | p(0) | k(1) | p(1) | k(2) | p(2) | … | k(n) | p(n) |
---|---|---|---|---|---|---|---|---|
关键字,且Ki<Ki+1 | pi-1指向子树根节点的指针,且该子树的的关键点均小于Ki | pi+1指向子树根节点的指针,且该子树的的关键点均小于Ki |
(注:表中括号内的内容为下标)
3.4.2 B-树的查找
- 在B-树中查找给定关键字的方法类似于二叉树上的查找,不同的是在每个记录上确定向下查找的路径不一定是二路的,而是多路的。因为一个节点内的关键字序列是有序的,在一个节点内查找关键字即可以用顺序查找,也可以用折半查找。
3.4 红黑树
3.3.1 红黑树的定义
- 红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
-
性质1. 节点是红色或黑色。
-
性质2. 根节点是黑色。
-
性质3 每个叶节点(NIL节点,空节点)是黑色的。
-
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
-
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
3.5 哈希表的概念
一、 哈希函数的构造方法
构造哈希函数的方法很多。在介绍各种方法之前,首先需要明确什么是“好”的哈希函数。
- 若对于关键字集合中的任何一个关键字,经哈希函数映像到地址集合中任何一个地址的概率是相等的。则称此类哈希函数为均匀的(Uniform)哈希函数。换句话说,就是是关键字经过哈希函数得到一个“随机的地址”,以便使一组关键字的哈希地址均匀分布在整个地址区间中,从而减少冲突。
常用的构造哈希函数的方法有:
直接定址法
- 取关键字或关键字的某个线性函数值为哈希地址。即:
H(Key)=key
或(key)=a*key+b
其中a和b为常数(这种哈希函数叫做自身函数)