#数据结构与算法
- 课程目标
- 课程内容
- 一、数据结构与算法概述
- 1.1 什么是数据结构
- 1.2 数据结构分类
- 逻辑结构的分类
- 物理结构的分类
- 1.3什么是算法
- 1.4算法初体验
- 二、 算法分析
- 2.1 算法的时间复杂的分析
- 2.1.1 函数渐进增长
- 2.1.2 算法的时间复杂度
- 2.1.2.1 大O记法
- 2.1.2.2 常见的O阶
- 2.1.2.3 函数调用的时间复杂度分析
- 2.1.2.4 最坏情况
- 2.2算法中的空间复杂度分析
- 2.2.1java中常见的内存占用
- 2.2.2 算法的空间复杂度
- 三、 排序
- 3.1简单排序
- 3.1.1Comparable接口介绍
- 3.1.2冒泡排序
- 3.1.3选择排序
- 3.1.4插入排序
- 3.2高级排序
- 3.2.1希尔排序
- 3.2.2归并排序
- 3.2.3快速排序
- 3.2.4排序的稳定性
- 四、线性表
- 4.1 顺序表
- 4.2 链表
- 4.3栈
- 4.4队列
- 五、符号表
- 六、二叉树入门
- 6.1树的基本定义
- 6.2树的相关术语
- 6.3二叉树的基本定义
- 七、堆
- 7.1堆的定义
- 7.2堆排序实现步骤
- 八、优先队列
- 8.1最大优先队列
- 8.2最小优先队列
- 8.3索引优先队列实现
- 九、树的进阶—平衡树
- 9.1 2-3查找树
- 9.1.1 2-3查找树的定义
- 9.1.2 2-3树的性质
- 9.2 红黑树
- 9.2.1 红黑树的定义
- 9.2.2平衡化
- 9.2.2.1左旋
- 9.2.2.2右旋
- 9.2.2.3颜色反转
- 9.2.2.4根结点的颜色总是黑的
- 9.3 B-树
- 9.3.1 B树的特征
- 9.3.2 B树在磁盘文件中的应用
- 9.4 B+树
- 9.4.1 B+树和B树的对比
- 9.4.2 B+树在数据库中的应用
- 9.4.2.1 未建立主键索引查询
- 9.4.2.2 建立主键索引查询
- 9.4.2.2 区间查询
- 十、并查集
- 10.1并查集结构
- 十一、图的入门
- 11.1图的实际应用
- 11.2图的定义及分类
- 11.3无向图
- 11.3.1图的相关术语
- 11.3.2 图的存储结构
- 11.3.2.1邻接矩阵
- 11.3.2.2邻接表
- 11.3.3 图的搜索
- 11.3.3.1深度优先搜索
- 11.3.3.2广度优先搜索
- 十二、图的进阶
- 12.1有向图的定义及相关术语
- 12.2拓扑排序
- 12.3加权无向图
- 12.4最小生成树
- 12.4.1最小生成树定义及相关约定
- 12.4.2最小生成树原理
- 12.4.2.1树的性质
- 12.4.2.2切分定理
- 12.4.3贪心算法
- 12.4.4 Prim算法
- 12.4.4.1 Prim算法的实现原理
- 12.4.5 kruskal算法
- 12.5 加权有向图
- 12.6最短路径
- 12.6.1最短路径定义及性质
- 12.6.2松弛技术
- 12.6.3 Dijstra算法实现
课程目标
如果你想让自己的编程能力有质的飞跃,不再停留于调用现成的API,而是追求更完美的实现,那么这门课程就是你的必修课,因为程序设计=数据结构+算法。
通过对基础数据结构和算法的学习,能更深层次的理解程序,提升编写代码的能力,让程序的代码更优雅,性能更高。
课程内容
- 数据结构和算法概述
- 算法分析
- 排序
- 线性表
- 符号表
- 树
- 堆
- 优先队列
- 并查集
- 图
一、数据结构与算法概述
1.1 什么是数据结构
官方解释:数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科。
大白话:数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和储存数据。
1.2 数据结构分类
传统上,我们可以把数据结构分为逻辑结构和物理结构两大类。
逻辑结构的分类
逻辑结构是从具体问题中抽象出的模型,是抽象意义上的结构,按照对象中数据元素之间的关系分类。
- 集合结构:集合中的元素除了属于同一个集合外,没有其他联系。
- 线性结构:线性结构中的元素之间存在一对一的关系。
- 树形结构:树形结构中的元素存在一对多的层次关系。
- 图形关系:图形结构的数据元素是多对多的关系。
物理结构的分类
逻辑结构在计算机中真正的表示方式(映像),称为物理结构,也可以叫做存储结构。常见的存储结构有顺序储存结构、链式存储结构。
1.3什么是算法
官方解释:算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。
大白话:根据一定的条件,对一些数据进行计算,得到需要的结果。
1.4算法初体验
在程序中我们可以用不同的算法解决相同的问题,而不同的算法成本也不同。总体上,一个优秀的算法追求以下两个目标:
- 花最少的时间完成需求;
- 占有最少的内存空间完成需求;
例1:计算1~100的和:
解法1:累加
解法2:使用等差数列公式
例2:计算n的阶乘:
解法1:递归调用
解法2:从1×到n
二、 算法分析
2.1 算法的时间复杂的分析
事后分析估算方法:不建议,通常需要花费大量的时间和精力,测试完了如果发现是非常糟糕的算法,那么之前所做的事情就白费了。
事前分析估算方法:主要取决于以下几个要素:
- 算法采用的策略和方案;
- 编异产生的代码质量;
- 问题的输入规模;
- 机器执行指令的速度。
抛开计算机硬件,软件的因素,一个程序运行时间依赖于算法的好坏和问题的输入规模。
2.1.1 函数渐进增长
- 算法函数中的常数可以忽略;
- 算法函数中的最高次幂的常数因子可以忽略;
- 算法函数中的最该次幂越小,算法效率越高。
2.1.2 算法的时间复杂度
2.1.2.1 大O记法
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n))。它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模n的某个函数。
推导大O阶的表示法有以下几个规则:
- 用常数1取代运行时间中的所有加法常数;
- 在修改后的运行次数中,只保留高阶项;
- 如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;
2.1.2.2 常见的O阶
- 线性阶 一个for
- 平方阶 二个for
- 立方阶 三个for
- 对数阶 :随着输入规模n的增大,不管底数为多少,他们的增长趋势是一样的,所以我们会忽略底数。
int i=1,n=100;
while(i<n){
i=i*2;
}//2^x=n;则x=log(2)n
- 常数阶
2.1.2.3 函数调用的时间复杂度分析
2.1.2.4 最坏情况
最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非特别指定,我们提到的运行时间都指的是最坏情况下的运行时间。
2.2算法中的空间复杂度分析
2.2.1java中常见的内存占用
- 基本数据类型内存占用情况:
- 计算机访问内存的方式都是一次一个字节(8位)
- 一个引用(机器地址)需要8个字节来表示
- 创建一个对象,比如new Date(),除了Date对象内部存储的数据占用内存,该对象本身也有内存开销,每个对象自身开销是16个字节,用来保存对象的头信息。
- 一般内存的使用,如果不够8个字节,都会被自动填充为8字节。
- java中数组被限定为对象,他们一般会因为记录长度二需要额外的内存,一个原始数据类型数组一般需要24字节的头信息(16个自己对象的开销,4字节用于保存长度以及4个填充字节)再加上保存值所需要的内存。
2.2.2 算法的空间复杂度
了解了java的内存最基本的机制,就能够有效地帮助我们估计大量程序的内存使用情况。
例如:对指定的数组进行反转
解法1:使用temp变量
解法2:使用temp[]数组
三、 排序
3.1简单排序
3.1.1Comparable接口介绍
//定义比较规则
@Override
public int compareTo(Student o){
return this.getAge()-o.getAge();
}
3.1.2冒泡排序
排序原理:
- 比较相邻的元素,如果一个元素比另一个元素大,就交换这两个元素的位置。
- 对每一对相邻元素做同样的工作,从第一对元素开始到结尾的最后一对元素。最终最后位置的元素就是最大值。
public static void sort(int[] a){
for(int i=a.length-1;i>0;i++){
for(int j=0;j<i;j++){
if(a[j]>a[j+1]){
int temp=0;
temp=a[j+1];
a[j+1]=a[j];
a[j]temp;
}
}
}
}
3.1.3选择排序
排序原理:
- 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引
- 交换第一个索引处和最小值所在的索引处的值
public static void sort(int[] a){
for(int i=0;i<=a.length-1;i++){
int minIndex=i;
for(int j=i+1;j<a.length;j++){
if(a[minIndex]>a[j]){
minIndex=j;
}
}
int temp=a[i];
a[i]=a[minIndex];
a[minIndex]=a[i];
}
}
3.1.4插入排序
排序原理:
- 把所有的元素分为两组,已经排序的和未排序的;
- 找到未排序的组中的第一个元素,向已经排序的组中进行插入;
- 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位;
public static void sort(int[] a){
for(int i=1;i<a.length;i++){
for(int j=i;j>0;j--){
if(a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
}else{
break;
}
}
}
}
3.2高级排序
3.2.1希尔排序
排序原理:
- 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
- 对分好组的每一组数据完成插入排序;
- 减小增长量,最小减为1,重复第二步操作。
增量h的确定:
int h=1;
while(h<5){
h=2h+1;//3,7
}
//循环结束后我们就可以确定h的最大值
//h的减小规则为:
h=h/2;
public static void sort(int[] a){
int N=a.length;
int h=1;
while(h<N/2){
h=h*2+1;
}
while(h>=1){
for(int i=h;i<N;i++){
for(int j=i;j>=h;j-=h){
if(a[j-h]>a[j]){
int temp=a[j-h];
a[j-h]=a[j];
a[j]=a[j-h];
}else{
break;
}
}
}
h/=2;
}
}
3.2.2归并排序
排序原理:
- 尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
- 将相邻的两个子组进行合并成一个有序的大组;
- 不断的重复步骤2,直到最终只有一个组为止。
public static void sort(int[] a,int left,int right){
if(left>=right){
return;
}
//中间索引
int center=(left+right)/2;
sort(a,left,center);
sort(a,center+1,right);
merge(a,left,center,right);
}
public static void merge(int[] a,int left,int center,int right){
int[] tepArr=new int[data.length];
int mid=center+1;
int third=left;
int tmp=left;
while(left<=center&&mid<=right){
if(a[left]<=a[mid]){
tmpArr[third++]=a[left++];
}else{
tmpArr[third++]=a[mid++];
}
}
while(mid<=right){
tmpArr[third++]=a[mid++];
}
while(left<=center){
tmpArr[third++]=a[left++];
}
//将临时数组中的内容拷贝回原数组
while(tmp<=right){
a[tmp]=tmpArr[tmp++];
}
}
3.2.3快速排序
排序原理:
- 首先设定一个分界值,通过该分界值将数组分成左右两部分;
- 将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
- 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
- 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
while(i<j){
while(temp<=arr[j]&&i<j){
j--;
}
while(temp>=arr[i]&&i<j){
i++;
}
if(i<j){
t=arr[j];
arr[j]=arr[i];
arr[i]=t;
}
}
//将基准为与i和j相等位置的数字交换
temp=arr[low];
arr[low]=arr[i];
arr[i]=temp;
//递归调用左半数组
quickSort(arr,low,j-1);
//递归调用右半数组
quickSort(arr,j+1,high);
}
3.2.4排序的稳定性
稳定性的定义:数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的。
稳定性的意义:如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销。
常见排序算法的稳定性:
稳定:冒泡排序,插入排序,归并排序
不稳定:选择排序,希尔排序,快速排序
四、线性表
线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
- 第一个数据元素没有前驱,这个数据元素被称为头结点;
- 最后一个数据元素没有后继,这个数据元素被称为尾结点;
- 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。
线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表和链表。
4.1 顺序表
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
例如:java中的ArrayList实现
java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。
- 是否用数组实现;
- 有没有扩容操作;
- 有没有提供遍历方式;
4.2 链表
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。
例如:java中的LinkedList实现
java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法
- 底层是否用双向链表实现;
- 结点类是否有三个域
相关问题:链表反转;中间值问题;判断链表是否有环;有环链表入口问题;约瑟夫问题
4.3栈
栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈。
相关问题:括号匹配问题;逆波兰表达式求值问题
4.4队列
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。
五、符号表
符号表最主要的目的就是将一个键和一个值联系起来,符号表能够将存储的数据元素是一个键和一个值共同组成的键值对数据,我们可以根据键来查找对应的值。
六、二叉树入门
符号表的增删查操作,随着元素个数N的增多,其耗时也是线性增多的,时间复杂度都是O(n),为了提高运算效率,接下来我们学习树这种数据结构。
6.1树的基本定义
树是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树具有以下特点:
- 每个结点有零个或多个子结点;
- 没有父结点的结点为根结点;
- 每一个非根结点只有一个父结点;
- 每个结点及其后代结点整体上可以看做是一棵树,称为当前结点的父结点的一个子树。
6.2树的相关术语
结点的度:一个结点含有紫薯的个数;
叶结点:度为0的结点称为叶节点,也可以叫做终端结点
分支结点:度不为0的结点,也可以叫非终端结点
结点的层次:从根结点开始,根结点对的层次为1,根的直接后继层次为2,以此类推
结点的层次编号:将树中的结点,按照从上层到下层,从左到右的层序排成一个线性序列,把他们编成连续的自然数。
树的度:树中所有结点度的最大值
树的高度(深度):树中结点的最大层次
森林:m(m>=0)个互不相交的树的集合,将一颗非空树的根结点点删去,树就变成了一个森林;给森林增加一个统一的根结点,森林就变成一棵树。
孩子结点:一个结点的直接后继
双亲结点(父结点):一个结点的直接前驱
兄弟结点:同一双亲结点的孩子结点间互称兄弟结点
6.3二叉树的基本定义
二叉树就是度不超过2的树(每个结点最多有两个子结点)
满二叉树:一个二叉树,每一个层的结点都达到最大值
完全二叉树:叶节点只能出现在下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
相关问题:层序遍历;前序遍历;后序遍历;中序遍历;二叉树的最大深度问题;折纸问题
七、堆
7.1堆的定义
堆是数据结构中一类特殊数据结构的统称,堆通常可以被看做是一颗完全二叉树的数组对象。
堆的特性:
- 它是完全二叉树
- 通常用数组实现;按层序顺序放入数组中;根节点在位置1,它的子结点在位置2,3……如果一个结点的位置为k,则它的父结点的位置为[k/2],而它的两个子结点的位置则分别为2k和2k+1。这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层,就令k等于k/2,向下一层就令k等于2k或2k+1。
- 每个结点都大于等于它的两个子结点;这里要注意堆中仅仅规定了每个结点大于等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟我们之前学习的二叉查找树是有区别的。
7.2堆排序实现步骤
实现步骤:
- 构造堆;
- 得到堆顶元素,这个值就是最大值;
- 交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置;
- 对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶;
- 重复2~4这个步骤,直到堆中剩一个元素为止。
八、优先队列
优先队列按照其作用不同,可以分为以下两种:
8.1最大优先队列
可以获取并删除队列中最大的值;基于大根堆实现
8.2最小优先队列
可以获取并删除队列中最小的值;基于小根堆来实现
8.3索引优先队列实现
在之前实现的最大优先队列和最小优先队列,他们可以分别快速访问到队列中最大元素和最小元素,但是他们有一个缺点,就是没有办法通过索引访问已存在于优先队列中的对象,并更新它们。为了实现这个目的,在优先队列的基础上,学习一种新的数据结构,索引优先队列。接下来我们以最小索引优先队列举列。
九、树的进阶—平衡树
之前我们学习过二叉查找树,发现它的查询效率比单纯的链表和数组的查询效率要高很多,大部分情况下,确实是这样的,但不幸的是,在最坏情况下,二叉查找树的性能还是很糟糕。
生如果成的树都像完全二叉树那样,那么即使在最坏情况下,查找的效率依旧会很好。
9.1 2-3查找树
为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。确切的说,我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链),而现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应着其中保存的键所分割产生的一个区间。
9.1.1 2-3查找树的定义
一棵2-3查找树要么为空,要么满足满足下面两个要求:
2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。
3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。
9.1.2 2-3树的性质
- 任意空链接到根结点的路径长度都是相等的
- 4-结点变换成3结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1.
- 2-3树与普通二叉查找树最大区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。
9.2 红黑树
我们前面介绍了2-3树,可以看到2-3树能保证在插入元素之后,树依然保持平衡状态,它的最坏情况下所有子结点都是2-结点,树的高度为lgN,相比于我们普通的二叉查找树,最坏情况下树的高度为N,确实保证了最坏情况下的时间复杂度,但是2-3树实现起来过于复杂,所以我们介绍一种2-3树思想的简单实现:红黑树。
红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:
红链接:将两个2-结点连接起来构成一个3-结点;黑链接:则是2-3树中的普通链接。
确切的说,我们将3-结点表示为由由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这种表示法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。
9.2.1 红黑树的定义
红黑树是含有红黑链接并满足下列条件的二叉查找树:
- 红链接均为左链接;
- 没有任何一个结点同时和两条红链接相连;
- 该树是完美黑色平衡的,即任意链接到根结点的黑链接数量相同。
9.2.2平衡化
在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以我们需要对这些情况通过旋转进行修复,让红黑树保持平衡。
9.2.2.1左旋
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
前提:当前结点为h,它的右子结点为x;
左旋过程:
- 让x的左子结点变为h的右子结点:h.right=x.left;
- 让h成为x的左子结点:x.left=h;
- 让h的color属性变为x的color属性值:x.color=h.color;
- 让h的color属性变为RED:h.color=true;
9.2.2.2右旋
当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
前提:当前结点为h,它的左子结点为x;
右旋过程:
- 让x的右子结点成为h的左子结点:h.left = x.right;
- 让h成为x的右子结点:x.right=h;
- 让x的color变为h的color属性值:x.color = h.color;
- 让h的color为RED;
9.2.2.3颜色反转
当一个结点的左子结点和右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。
9.2.2.4根结点的颜色总是黑的
由于根结点不存在父节点,所以每次插入操作后,我们都需要把根结点的颜色设置为黑色。
9.3 B-树
前面我们已经学习了二叉查找树、2-3树以及它的实现红黑树。2-3树中,一个结点做多能有两个key,它的实现红黑树中使用对链接染色的方式去表达这两个key。接下来我们学习另外一种树型结构B树,这种数据结构中,一个结点允许多于两个key的存在。
B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。
9.3.1 B树的特征
B树中允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择一个参数M,来构造一个B树,我们可以把它称作是M阶的B树,那么该树会具有如下特点:
- 每个结点最多有M-1个key,并且以升序排列;
- 每个结点最多能有M个子结点;
- 根结点至少有两个子结点;
在实际应用中B树的阶数一般都比较大(通常大于100),所以,即使存储大量的数据,B树的高度仍然比较小,这样在某些应用场景下,就可以体现出它的优势。
9.3.2 B树在磁盘文件中的应用
在我们的程序中,不可避免的需要通过IO操作文件,而我们的文件是存储在磁盘上的。计算机操作磁盘上的文件是通过文件系统进行操作的,在文件系统中就使用到了B树这种数据结构。
文件系统的设计者利用了磁盘预读原理,将一个结点的大小设为等于一个页(1024个字节或其整数倍),这样每个结点只需要一次I/O就可以完全载入。那么3层的B树可以容纳102410241024差不多10亿个数据,如果换成二叉查找树,则需要30层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在10亿个数据中查找目标值,只需要小于3次硬盘读取就可以找到目标值,但红黑树需要小于30次,因此B树大大提高了IO的操作效率。
9.4 B+树
B+树是对B树的一种变形树,它与B树的差异在于:
- 非叶节点仅具有索引作用,也就是说,非叶子结点只储存key,不存储value。
- 树的所有叶节点构成一个有序链表,可以按照key排序的次序遍历全部数据。
9.4.1 B+树和B树的对比
B+树的优点在于:
- 由于B+树在非叶子结点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。
- B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。
B树的优点在于:
由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。
9.4.2 B+树在数据库中的应用
在数据库的操作中,查询操作可以说是最频繁的一种操作,因此在设计数据库时,必须要考虑到查询的效率问题,在很多数据库中,都是用到了B+树来提高查询的效率;
在操作数据库时,我们为了提高查询效率,可以基于某张表的某个字段建立索引,就可以提高查询效率,那其实这个索引就是B+树这种数据结构实现的。
9.4.2.1 未建立主键索引查询
执行select * from user where id=18,需要从第一条数据开始向下查询
9.4.2.2 建立主键索引查询
根据主键索引在二叉树上查询
9.4.2.2 区间查询
执行select * from user where id>=12 and id<=18,如果有了索引,由于B+树的叶子结点形成了一个有序链表,所以我们只需要找到id为12的叶子结点,按照遍历链表的方式顺序往后查即可,效率非常高。
十、并查集
并查集是一种树型的数据结构,并查集可以高效地进行如下操作:查询元素p和元素q是否属于同一组合并元素p和元素q所在的组。
10.1并查集结构
并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:
- 每个元素都唯一的对应一个结点;
- 每一组数据中的多个元素都在同一颗树中;
- 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
- 元素在树中并没有子父级关系的硬性要求;
十一、图的入门
11.1图的实际应用
在现实生活中,有许多应用场景会包含很多点以及点与点之间的连接,而这些应用场景我们都可以用图这种数据结构去解决。
11.2图的定义及分类
定义:图是由一组顶点和一组能够将两个顶点相连的边组成的
特殊的图:
- 自环:一条连接一个顶点和其自身的边
- 平行边:连接同一对顶点的两条边;
图的分类:按照连接两个顶点的边的不同,可以 把图分为以下两种:
- 无向图:边仅仅连接两个顶点,没有其他含义
- 有向图:边不仅连接两个顶点,并且具有方向;
11.3无向图
11.3.1图的相关术语
相邻顶点:当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点。
度:某个顶点的度就是依附于该顶点的边的个数。
子图:是一幅图的所有边的子集(包含这些边依附的顶点)组成的图。
路径:是由边顺序连接的一系列的顶点组成
环:是一条至少含有一条边且终点和起点相同的路径
连通图:如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称之为连通图
连通子图:一个非连通图由若干连通的部分组成,每一个连通的部分都可以称为该图的连通子图
11.3.2 图的存储结构
要表示一幅图,只需要表示清楚以下两部分内容即可:
- 图中所有的顶点;
- 所有连接顶点的边;
常见的图的存储结构有两种:邻接矩阵和邻接表
11.3.2.1邻接矩阵
- 使用一个二维数组adj,把索引的值看做是顶点;
- 如果顶点v和顶点w相连,我们只需要将adj[v] [w]和adj[w] [v]的值设置为1,否则设置为0即可。
很明显,邻接矩阵这种存储方式的空间复杂度是V^2的,如果我们处理的问题规模比较大的话,内存空间极有可能不够用。
11.3.2.2邻接表
- 使用一个二维数组,把索引看做是顶点;
- 每个索引处adj存储了一个队列,该队列中储存的是所有与该顶点相邻的其他顶点
很明显,邻接表的空间并不是是线性级别的,所以后面我们一直采用邻接表这种存储形式来表示图。
11.3.3 图的搜索
在很多情况下,我们需要遍历图,得到图的一些性质,例如,找出图中与指定的顶点相连的所有顶点,或者判定某个顶点与指定顶点是否相通,是非常常见的需求。
有关图的搜索,最经典的算法有深度优先搜索和广度优先搜索,接下来我们分别讲解这两种搜索算法。
11.3.3.1深度优先搜索
所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点。
11.3.3.2广度优先搜索
所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找兄弟结点,然后找子结点。
十二、图的进阶
在实际生活中,很多应用相关的图都是有方向性的,最直观的就是网络,可以从A页面通过链接跳转到B页面,那么a和b连接的方向是a->b,但不能说是b->a,此时我们就需要使用有向图来解决这一类问题,它和我们之前学习的无向图,最大的区别就在于连接是具有方向的,在代码的处理上也会有很大的不同。
12.1有向图的定义及相关术语
定义:有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。
出度:由某个顶点指出的边的个数称为该顶点的出度。
入度:指向某个顶点的边的个数称为该顶点的入度。
有向路径:由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。
有向环:一条至少含有一条边,且起点和终点相同的有向路径。
一副有向图中两个顶点v和w可能存在以下四种关系:
- 没有边相连;
- 存在从v到w的边v—>w;
- 存在从w到v的边w—>v;
- 既存在w到v的边,也存在v到w的边,即双向连接;
12.2拓扑排序
给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级。
如果我们要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在。
12.3加权无向图
加权无向图是一种为每条边关联一个权重值或是成本的图模型。这种图能够自然地表示许多应用。在一副航空图中,边表示航线,权值则可以表示距离或是费用。在一副电路图中,边表示导线,权值则可能表示导线的长度即成本,或是信号通过这条先所需的时间。此时我们很容易就能想到,最小成本的问题,例如,从西安飞纽约,怎样飞才能使时间成本最低或者是金钱成本最低?
加权无向图中的边我们就不能简单的使用v-w两个顶点表示了,而必须要给边关联一个权重值,因此我们可以使用对象来描述一条边。
12.4最小生成树
之前学习的加权图,我们发现它的边关联了一个权重,那么我们就可以根据这个权重解决最小成本问题,但如何才能找到最小成本对应的顶点和边呢?最小生成树相关算法可以解决。
12.4.1最小生成树定义及相关约定
图的生成树是它的一棵含有其所有顶点的无环连通子图,一副加权无向图的最小生成树它的一棵权值(树中所有边的权重之和)最小的生成树。
约定:只考虑连通图。最小生成树的定义说明它只能存在于连通图中,如果图是不联通的,那么分别计算每个连通图子图的最小生成树,合并到一起称为最小生成森林。
12.4.2最小生成树原理
12.4.2.1树的性质
- 用一条边接树中的任意两个顶点都会产生一个新的环;
- 从树中删除任意一条边,将会得到两颗独立的树。
12.4.2.2切分定理
要从一副连通图中找出该图的最小生成树,需要通过切分定理完成。
切分:将图的所有顶点按照某些规则分为两个非空且没有交集的集合。
横切边:连接两个属于不同集合的顶点的边称之为横切边。
切分定理:在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图中的最小生成树。
注意:一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边。
12.4.3贪心算法
贪心算法是计算图的最小生成树的基础算法,它的基本原理就是切分定理,使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边。如果图有V个顶点,那么需要找到V-1条边,就可以表示该图的最小生成树。
计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于保存切分和判定权重最小的横切边的方式。
12.4.4 Prim算法
我们学习第一种计算最小生成树的方法叫Prim算法,它的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中。
Prim算法的切分规则:把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合。
12.4.4.1 Prim算法的实现原理
Prim算法始终将图中的顶点切分成两个集合,最小生成树顶点和非最小生成树顶点,通过不断的重复做某些操作,可以逐渐将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点都加入到最小生成树中。
12.4.5 kruskal算法
kruskal算法是计算一副加权无向图的最小生成树的另外一种算法,它的主要思想是按照边的权重(从小到大)处理它们,将边加入最小生成树中,加入的边不会与已经加入最小生成树的边构成环,直到树中含有V-1条边为止。
kruskal算法和prim算法的区别:Prim算法是一条边一条边的构造最小生成树,每一步都为一棵树添加一条边。kruskal算法构造最小生成树的时候也是一条边一条边地构造,但它的切分规则是不一样的。它每一次寻找的边会连接一片森林中的两棵树。如果一副加权无向图由V个顶点组成,初始化情况下每个顶点都构成一棵独立的树,则V个顶点对应V棵树,组成一片森林,kruskal算法每一次处理都会将两棵树合并为一棵树,直到整个森林中只剩一棵树为止。
12.5 加权有向图
之前学习的加权无向图中,边是没有方向的,并且同一条边会同时出现在该边的两个顶点的邻接表中,为了能够处理含有方向性的图的问题,我们需要加权有向图。
12.6最短路径
定义:在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径。
12.6.1最短路径定义及性质
性质:
- 路径具有方向性;
- 权重不一定等价于距离。权重可以是距离、时间、花费等内容,权重最小指的是成本最低
- 只考虑连通图。一副图中并不是所有的顶点都是可达的,如果s和t不可达,那么它们之间也就不存在最短路径,为了简化问题,这里只考虑连通图。
- 最短路径不一定是唯一的。从一个顶点到达另外一个顶点的权重最小的路径可能会有很多条,这里只需要找出一条即可。
最短路径树:给定一副加权有向图和一个顶点s,以s为起点的一棵最短路径树是图的一副子图,它包含顶点s以及从s可达的所有顶点。这棵有向树的根结点为s,树的每条路径都是有向图中的一条最短路径。
12.6.2松弛技术
松弛这个词来源于生活:一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以放松了。
边的松弛:放松边v->w意味着检查从s到w的最短路径是否先从s到v。如果不是则放弃这条边。
顶点的松弛:只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。
12.6.3 Dijstra算法实现
Disjstra算法的实现和Prim算法很类似,构造最短路径树的每一步都是向这棵树中添加一条新的边,而这条新的边是有效横切边队列中的权重最小的边。
两者之间的区别。
- 在图论中,Prim算法解决的问题是连通无向有权图中最小生成树问题,而Dijkstra算法解决的问题是源点到目标点的最短路径问题。
- 虽然这两个算法在添加新结点时,都是选择“距离最短”的结点加入集合,但是Prim算法中,“距离最短”是指未访问的结点到已经访问的所有结点距离最小,即将已经访问的结点视为一个整体,将距离最小的结点加入到已访问的集合中;而在Dijkstra算法中,“距离最短”是指所有未访问结点(通过已访问的结点)到源点距离最小。