数据结构知识概括

141 阅读53分钟

@TOC

数据结构和算法简介

数据结构和算法关系:

  • 数据data结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码。
  • 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
  • 程序 = 数据结构 + 算法
  • 数据结构是算法的基础, 换言之,想要学好算法,需要把数据结构学到位。

数据结构包括:线性结构和非线性结构:

线性结构:

  • 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
  • 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
  • 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息(链式存储的内存地址不是连续的,但画出来的图看起来好像是连续的)
  • 线性结构常见的有:数组、队列、链表和栈

非线性结构:

  • 非线性结构包括:二维数组,多维数组,广义表,树结构,图结构

稀疏数组

问题: 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方法是:

  • 记录数组一共有几行几列,有多少个不同的值
  • 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模

在这里插入图片描述 在这里插入图片描述

队列

队列介绍:

  • 队列是一个有序列表,可以用数组或是链表来实现。
  • 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

数组模拟队列:

  • 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
  • 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front会随着数据输出而改变,而 rear则是随着数据输入而改变。(rear 是队列最后[含] front 是队列最前元素[不含])
  • 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析 将尾指针往后移:rear+1 , 当front == rear 【空】 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]

在这里插入图片描述 数组模拟环形队列:

  • 数组模拟队列到达尾部又向前存储的队列称为循环队列,为了避免"假溢出",我们通常采用循环队列。
  • 循环队列无论入队还是出队,队尾、队头加1后都要取模运算,例如入队后队尾后移一位:Q.rear =(Q.rear+1)%Maxsize。
  • %Maxsize主要是为了处理临界状态,即Q.rear向后移动一个位置Q.rear+1后,很有可能超出了数组的下标,这时它的下一个位置其实是0,如果将一维数组画成环形图,如图所示:在这里插入图片描述
  • 队空:Q.front=Q.rear; // Q.rear和Q.front指向同一个位置 队满: (Q.rear+1) %Maxsize=Q.front; // Q.rear向后移一位正好是Q.front 队列中元素个数:(Q.rear-Q.front+Maxsize)% Maxsize 循环队列详解

链表

简介:

  • 链表是有序的列表,但是它在内存中是存储如下:

在这里插入图片描述

  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含 data 域, next 域:指向下一个节点.
  • 如图:发现链表的各个节点不一定是连续存储.
  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
  • 有时在单链表的第一个结点之前附设一个结点,称之为头结点 。 头结点的数据域可以不存储任何信息,也可以存储如线性表长度等类的附加信息。

在这里插入图片描述 链表、头指针、头结点

双向链表应用实例:

  • 单向链表,查找的方向只能是一个方向,而双向链 表可以向前或者向后查找。
  • 单向链表不能自我删除,需要靠辅助节点 ,
  • 而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是需要找到待删除节点的前一个节点

用单向环形链表约瑟夫、约瑟夫环) 问题

什么是跳表:

  • 储存数据时,数组容量有限且有序数组增加元素时效率比较低,所以存储数据时常选用链表。但是链表的查询效率O(n)。有没有可以提高链表查找效率的方式呢?跳表就是这样一种结构。利用空间换时间的方式,提高查询效率。在这里插入图片描述
  • 跳跃表的结构是多层的,通过从最高维度的表进行检索再逐渐降低维度从而达到对任何元素的检索接近线性时间的目的O(logn)。理想的跳表是每一层是下一层元素的1/2,即每个元素跳过2个元素,这样共有log2N层。但是这样插入删除元素就会很复杂,ex 插入一个元素需要更新所有层相关的节点。所以通常的做法:没次向跳表加入一个元素时,用扔硬币的方式决定要不要向上增长一层。
  • 链接:什么是跳表

栈简介:

  • 栈的英文为(stack)
  • 栈是一个先入后出(FILO-First In Last Out)的有序列表。
  • 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
  • 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除

栈的应用场景:

  • 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
  • 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
  • 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
  • 二叉树的遍历。
  • 图形的深度优先(depth一first)搜索法。

前缀(波兰表达式)、中缀、后缀表达式(逆波兰表达式)

前缀表达式:

  • 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
  • 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6

前缀表达式的计算机求值:

从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

中缀表达式:

  • 中缀表达式就是常见的运算表达式,如(3+4)×5-6
  • 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)

后缀表达式:

  • 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
  • 举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –

后缀表达式的计算机求值:

从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

递归

递归简介:

简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁

递归需要遵守的重要规则:

  • 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  • 方法的局部变量是独立的,不会相互影响, 比如n变量
  • 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
  • 递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError,栈溢出)
  • 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

递归经典问题:

  • 迷宫问题
  • 八皇后问题介绍 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。在这里插入图片描述

排序算法

排序算法简介:

  • 排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
  • 排序的分类:
  1. 内部排序: 指将需要处理的所有数据都加载到内部存储器中进行排序。
  2. 外部排序法: 数据量过大,无法全部加载到内存中,需要借助外部存储进行 排序。
  3. 常见的排序算法分类(见下图):在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

算法时间复杂度

数学基础知识:

  • 比如说a的b次方,a叫底数 ,b叫指数a的b次方这个整体为一个结果叫幂

  • 组合(combination)是一个数学名词。一般地,从n个不同的元素中,任取m(m≤n)个元素不管其顺序为一组,叫作从n个不同元素中取出m个元素的一个组合。我们把有关求组合的个数的问题叫作组合问题。 案例:组合总数(total number of combinations)是一个正整数,指从n个不同元素里每次取出0个,1个,2个,…,n个不同元素的所有组合数的总和,即2^n在这里插入图片描述

  • 从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。 案例:公式:全排列数f(n)=n!(定义0!=1),如1,2,3三个元素的全排列为: 1,2,3 1,3,2 2,1,3 2,3,1 3,1,2 3,2,1 共3*2*1=6种。

度量一个程序(算法)执行时间的两种方法;

  • 事后统计的方法:这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
  • 事前估算的方法:通过分析某个算法的时间复杂度来判断哪个算法更优。

时间频度:

  • 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

时间复杂度:

  • 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
  • T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。
  • 计算时间复杂度的方法: 1、用常数1代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1 2、修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n² 3、去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²) 关于两个for循环不嵌套一起的时间复杂度

常见的时间复杂度:

  • 常数阶O(1)
//它的循环的时间复杂度为O(1), 因为无循环
int i;
i=1;
  • 对数阶O(log(2)n)
//由于每次count乘以2之后,就距离n更近了一分。 
//也就是说,有多少个2相乘后大于n,则会退出循环。 
//由2^x=n 得到x=logn。 所以这个循环的时间复杂度为O(logn)。
int count = 1;      
while (count < n){
   count = count * 2;

}
  • 线性阶O(n)
//它的循环的时间复杂度为O(n), 因为循环体中的代码须要执行n次。
int i;      
for(i = 0; i < n; i++){
    
}
  • 线性对数阶O(nlog(2)n)
//由于每次count乘以2之后,就距离n更近了一分。 
//也就是说,有多少个2相乘后大于n,则会退出循环。 
//由2^x=n 得到x=logn。 所以这个循环的时间复杂度为O(logn)。
//此外还在外层加了一层循环所以为O(nlog(2)n)
int count = 1;      
int i;
for(i=0; i < n; i++){
	while (count < n){
   		count = count * 2;
	}
}
  • 平方阶O(n^2)
//它的循环的时间复杂度为O(n^2), 因为循环体中的代码须要执行n^2次。
int i, j;      
for(i = 0; i < n; i++){
    for(j = 0; j < n; j++){
   
    }
}
  • 立方阶O(n^3)
//它的循环的时间复杂度为O(n^3), 因为循环体中的代码须要执行n^3次。
int i, j;      
for(i = 1; i < n; i++)
    for(j = 1; j < n; j++)
        for(j = 1; j < n; j++){

        }
  • 不常见的时间复杂度:递归算法的时间复杂度

递归算法的时间复杂度:

  • 如果递归函数中,只进行一次递归调用,递归深度为depth在每个递归的函数中,时间复杂度为T;总体的时间复杂度为O(T *depth)。
  • 二分查找法:根据数学知识,需要log2n次才能递归到底。因此,二分查找法的时间复杂度为 O(logn)。
int binarySearch(int arr[], int l, int r, int target){
    if( l > r ) return -1;
    
    int mid = l + (r-l)/2; 
    if( arr[mid] == target ) return mid;  
    else if( arr[mid] > target ) 
    return binarySearch(arr, l, mid-1, target);    // 左边 
    else
    return binarySearch(arr, mid+1, r, target);   // 右边

}
  • 求和:在这段代码中比较容易理解递归深度随输入 n 的增加而线性递增,因此时间复杂度为 O (n)。
int sum (int n) {
  if (n == 0) return 0;
  return n + sum( n - 1 )
}
  • 求幂:递归深度为 logn,因为是求需要除以 2 多少次才能到底。
//递归深度:logn
//时间复杂度:O(logn)
double pow( double x, int n){
  if (n == 0) return 1.0;
  
  double t = pow(x,n/2);
  if (n %2) return x*t*t;
  return t * t;
}
  • 两次递归调用:递归树中节点数就是代码计算的调用次数。`每个函数调用都花费O(1),递归子树的深度为n。因此时间复杂度为O(n*1)=O(n)``。
// O(2^n) 指数级别的数量级,后续动态规划的优化点
int f(int n){
 if (n == 0) return 1;
 return f(n-1) + f(n - 1);
}

在这里插入图片描述

  • 归并排序 的递归树:归并排序 的递归树深度为logn,归并排序 中每个节点处理的数据规模是逐渐缩小的。归并排序等排序算法中,每一层处理的数据量为 O(n) 级别,同时有 logn 层,时间复杂度便是 O(nlogn)在这里插入图片描述
  • 时间复杂度详解

时间复杂度说明:

  • 常见的算法时间复杂度由小到大依次为:

O(1) < O(logn) < O(n) < O(n*logn) < O(n^2) < O(n^3)

  • 指数时间的关系由小到大依次为:

O(2^n) < O(n!) < O(n^n)

  • 从图中可见,我们应该尽可能避免使用指数阶的算法在这里插入图片描述

O(1)解析:

  • O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话),冲突的话很麻烦的,指向的value会做二次hash到另外一快存储区域。
  • 通俗易懂的例子 什么是O(1)呢,就比如你是一个酒店的管理员,你负责管理酒店的钥匙,你很聪明,你把酒店的100把钥匙放在了100个格子里面存着,并且把格子从1~100进行了编号,有一天有客人来了,酒店老板说,给我拿10号房间的钥匙给我,你迅速从10号格子里面拿出钥匙给老板,速度非常快,这时候你就是一个电脑了,老板跟你说拿几号房房间的钥匙,你只需要看一眼就能知道钥匙在哪里。

O(n)解析:

  • 比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍。
  • 比如常见的遍历算法。 要找到一个数组里面最大的一个数,你要把n个变量都扫描一遍,操作次数为n,那么算法复杂度是O(n).
  • 通俗易懂的例子 突然,有一天,你的老板给你说,你用100个箱子存100把钥匙,太浪费空间了,你能补能把钥匙上编号一下,然后把钥匙要用绳子穿起来,这样我们可以把这个放箱子的地方再装修一个房间出来。你想了一下,是啊,现在房价这么贵,这样能多赚点钱。所以你就不能通过上面的方法来找到钥匙了,老板跟你说,给我拿45号房间的钥匙出来,你就需要从100个钥匙里面挨个找45个房间的钥匙。

O(n^2)解析:

  • 再比如时间复杂度O(n^2),就代表数据量增大n倍时,耗时增大n的平方倍,这是比线性更高的时间复杂度。比如冒泡排序,就是典型的O(n^2)的算法,对n个数排序,需要扫描n×n次。 在这里插入图片描述
  • 用冒泡排序排一个数组,对于n个变量的数组,需要交换变量位置n^2次,那么算法复杂度就是O(n^2).
  • 通俗易懂的例子 随着经济发展越来越好,你的老板把酒店扩大了,有100层每一层有100个房间,当然,你还是你,不过你因为关注我的博客,知道怎么把钥匙排序更好了,你把每一层的钥匙穿在一起,然后一共就有100个用绳子穿起来的钥匙串。然后老板叫你找钥匙的时候,你先要找到楼层的编号,再对应找到房间的编号,所以大概对应的是这样的代码。

O(logn)解析:

  • 再比如O(log n),当数据增大n倍时,耗时增大logn倍(这里的log是以2为底的,比如,当数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度)。二分查找就是O(logn)的算法,每找一次排除一半的可能,256个数据中查找只要找8次就可以找到目标。

  • 通俗易懂的例子 这个就像是有一百把钥匙,你突然觉得,我从头找是不是太慢了,我从中间找,比如我要找到23号的房间钥匙,我从中间切开,找到50编号的位置,然后23在150里面,我再把从中间切开变成25,然后23在125之间,我再切开变成12.5,然后23在12.5~25之间,依次找下去,直到找到钥匙。这种查找钥匙的方法的复杂度就是O(log^n)

O(nlogn)解析

  • O(n log n)同理,就是n乘以logn,当数据增大256倍时,耗时增大256*8=2048倍。这个复杂度高于线性低于平方。归并排序就是O(n log n)的时间复杂度。

算法的空间复杂度

基本介绍:

  • 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
  • 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
  • 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis,memcache)和算法(基数排序)本质就是用空间换时间.
  • 算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)= O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

空间复杂度比较常用的有:O(1)、O(n)、O(n^2):

  • 空间复杂度 O(1)
//如果算法执行所需要的临时空间不随着某个变量n的大小而变化。
//即此算法空间复杂度为一个常量,可表示为 O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
  • 空间复杂度 O(n)
//代码第一行new了一个数组出来,这个数据占用的大小为n,
//代码的2-6行,虽然有循环,但没有再分配新的空间。
//因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

冒泡排序

简介:

  • 冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
  • 因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下 来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

在这里插入图片描述

选择排序

简介:

  • 选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
  • 选择排序思想: 选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从arr[0]到arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1]到arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2]到arr[n-1]中选取最小值,与arr[2]交换,…,第i次从arr[i-1]到arr[n-1]中选取最小值,与arr[i-1]交换,…, 第n-1次从arr[n-2]到arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。在这里插入图片描述

插入排序

简介:

  • 插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
  • 插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。在这里插入图片描述

简单插入排序存在的问题:

数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是: {2,3,4,5,6,6} {2,3,4,5,5,6} {2,3,4,4,5,6} {2,3,3,4,5,6} {2,2,3,4,5,6} {1,2,3,4,5,6} 结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响

希尔排序

简介:

  • 希尔排序的实质就是分组插入排序,该方法又称递减增量排序算法,因DL.Shell于1959年提出而得名。希尔排序是非稳定的排序算法。
  • 基本思想: 1、先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。 2、因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。在这里插入图片描述

希尔排序有两种方式: 移位法(只有在确定出现差异时才移动元素) 交换法。(使用的是冒泡排序,将最大或最小统一移至一侧) 希尔排序的两种方式理解与比较

快速排序

简介:

  • 快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
  • 快速排序从小到大排序:在数组中随机选一个数(默认数组首个元素),数组中小于等于此数的放在左边,大于此数的放在右边,再对数组两边递归调用快速排序,重复这个过程。 在这里插入图片描述

堆排序

简介:

  • 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
  • 基本思想: 利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。在这里插入图片描述

归并排序

简介:

  • 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。在这里插入图片描述 在这里插入图片描述

基数排序

基数排序介绍:

  • 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  • 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
  • 基数排序(Radix Sort)是桶排序的扩展
  • 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
  • 基数排序基本思想: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  • 此外相同的还有计数排序和桶排序。

在这里插入图片描述在这里插入图片描述在这里插入图片描述 排序算法详解

查找算法

在java中,我们常用的查找有四种:

  • 顺序(线性)查找
  • 二分查找/折半查找
  • 插值查找
  • 斐波那契查找

线性查找算法

从头开始遍历,如果找到就返回。

二分查找

简介:

  • 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
  • 要求: ①首先二分查找要求序列必须是有序的其次序列可以随机访问任何有效范围内索引的值,链式存储不能随机访问,必须是顺序存储

插值查找

插值查找原理介绍:

  • 插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。

  • 将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right.key 就是前面我们讲的 findVal 在这里插入图片描述

  • int mid = low + (high - low) * (key - arr[low]) / (arr[high] -arr[low]) ;/插值索引/对应前面的代码公式:int mid = left + (right – left) *(findVal – arr[left]) / (arr[right] – arr[left])

插值查找注意事项:

  • 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
  • 关键字分布不均匀的情况下,该方法不一定比折半查找要好

斐波那契查找

简介:

  • 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
  • 斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近黄金分割值0.618
  • 斐波那契(黄金分割法)原理: 斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示: 在这里插入图片描述

折半查找、插值查找和斐波那契查找

哈希表

哈希:

  • Hash,一般翻译做散列、杂凑,或音译为哈希。
  • 属性:压缩映射
  • 特点:很难找到逆向规律。
  • 平均查找长度与节点个数无关的查找方法

MD5和Hash:

  • 相同处: 1.二者均是多对一的数据加密模式。(也就是说将一定量的数据加密成一个固定长度的数据) 2.二者的加密方式均为单向加密,也就是加密不可逆。 3.二者多用于数据加密和文件以及数据的完整性验证
  • 不同处: 1.MD5目前存在很大的安全隐患,通过一些方法可以将MD5暴力破解,同样对于SHA1的算法现在同样存在这样的问题。 2.SHA2算法由于相对于SHA1来说,加密数据位数的上升大大增加了破解的难度,使得安全性能要远远高于MD5

简介:

  • 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

在这里插入图片描述 解决Hash碰撞冲突方法总结:

  • Hash碰撞冲突:对象Hash的前提是实现equals()和hashCode()两个方法,那么HashCode()的作用就是保证对象返回唯一hash值,但当两个对象计算值一样时,这就发生了碰撞冲突。如下将介绍如何处理冲突,当然其前提是一致性hash。
  • 开放地址法: ①开放地执法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性探测再散列。如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,…kk,-kk(k<=m/2),称二次探测再散列。 ②如果di取值可能为伪随机数列。称伪随机探测再散列。
  • 再哈希法: ①当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。 ②缺点:计算时间增加。比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,第三位,直到不冲突为止
  • 链地址法(拉链法): ①将所有关键字为同义词的记录存储在同一线性链表中。如下:在这里插入图片描述 ②因此这种方法,可以近似的认为是筒子里面套筒子
  • 建立一个公共溢出区: ①假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

拉链法的优缺点:

  • 优点: ①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短; ②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况; ③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间; ④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
  • 缺点: ①指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

树结构

为什么需要树这种数据结构?

  • 数组存储方式的分析 优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
  • 链式存储方式的分析 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。 缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
  • 树存储方式的分析 能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

树的常用术语(结合示意图理解):

  • 节点
  • 根节点
  • 父节点
  • 子节点
  • 叶子节点 (没有子节点的节点)
  • 节点的权(节点值)
  • 路径(从root节点找到该节点的路线)
  • 子树
  • 树的高度(最大层数)
  • 森林 :多颗子树构成森林 在这里插入图片描述

二叉树

树简介:

  • 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
  • 二叉树的子节点分为左节点和右节点。
  • 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。
  • 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。

二叉树遍历的说明:

  • 前序遍历: 先输出父节点,再遍历左子树和右子树。
  • 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树。
  • 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点。
  • 小结: 看输出父节点的顺序,就确定是前序,中序还是后序。

顺序存储二叉树:

  • 从数据存储来看,数组存储方式和树 的存储方式可以相互转换,即数组可 以转换成树,树也可以转换成数组, 看右面的示意图。 在这里插入图片描述

顺序存储二叉树的特点:

  • 顺序二叉树通常只考虑完全二叉树
  • 第n个元素的左子节点为 2 * n + 1
  • 第n个元素的右子节点为 2 * n + 2
  • 第n个元素的父节点为 (n-1) / 2

线索二叉树基本介绍:

  • n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
  • 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  • 一个结点的前一个结点,称为前驱结点
  • 一个结点的后一个结点,称为后继结点
  • 说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况: left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点. right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点. 在这里插入图片描述 堆排序基本介绍:
  • 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
  • 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 :没有要求结点的左孩子的值和右孩子的值的大小关系。
  • 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
  • 大顶堆举例说明在这里插入图片描述
  • 小顶堆举例说明在这里插入图片描述

堆排序的基本思想是:

  • 将待排序序列构造成一个大顶堆
  • 此时,整个序列的最大值就是堆顶的根节点。
  • 将其与末尾元素进行交换,此时末尾就为最大值。
  • 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
  • 可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

赫夫曼树

赫夫曼树基本介绍:

  • 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
  • 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

赫夫曼树几个重要概念和举例说明:

  • 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
  • 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  • WPL最小的就是赫夫曼树

赫夫曼编码基本介绍

  • 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
  • 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
  • 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
  • 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码

原理剖析: 定长编码:

  • 通信领域中信息的处理方式1-定长编码
  • i like like like java do you like a java // 共40个字符(包括空格)
  • 105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应Ascii码
  • 01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
  • 按照二进制来传递信息,总的长度是 359 (包括空格)

变长编码:

  • 通信领域中信息的处理方式2-变长编码
  • i like like like java do you like a java // 共40个字符(包括空格)
  • d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
  • 0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推.
  • 按照上面给各个字符规定的编码,则我们在传输 "i like like like java do you like a java" 数据时,编码就是 10010110100...
  • 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码

赫夫曼编码:

  • 通信领域中信息的处理方式3-赫夫曼编码
  • i like like like java do you like a java // 共40个字符(包括空格)
  • d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
  • 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值.(图后)在这里插入图片描述
  • //根据赫夫曼树,给各个字符 //规定编码 , 向左的路径为0 //向右的路径为1 , 编码如下:
  • o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01
  • 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩) 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
  • 长度为 : 133
    说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9% 此编码满足前缀编码,即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
  • 注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的,

赫夫曼编码压缩文件注意事项:

  • 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化, 比如视频,ppt 等等文件 [举例压一个 .ppt]
  • 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件) [举例压一个.xml文件]
  • 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显

二叉排序树(BST)

解决方案分析:

  • 使用数组: 数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
  • 使用链式存储-链表: 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。
  • 使用二叉排序树

二叉排序树介绍:

  • 二叉排序树:BST: (Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
  • 比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:在这里插入图片描述

删除结点的各种情况分析:

  • 删除叶子节点删除的节点是叶子节点,即该节点下没有左右子节点。 方法:找到该节点的父节点,并判断该节点是父节点的左节点还是右节点,然后直接删除
  • 删除节点有一个子节点,即该节点有左子节点或者右子节点。 方法:找到该节点的父节点,并判断该节点是父节点的左节点还是右节点,然后再判断该节点是有左节点还是右节点,然后将父节点的左或右节点等于该节点的左或右节点
  • 删除节点有两个子节点,即该节点有左子节点和右子节点。 方法:找到该节点的父节点,并判断该节点是父节点的左节点还是右节点,然后再找该节点的左子树中的最小值或者右子树中的最大值。接着删除最小值或最大值这个节点,将删除节点的值赋值给父节点。

平衡二叉树(AVL树)

BST 存在的问题分析:

  • 左子树全部为空,从形式上看,更像一个单链表.
  • 插入速度没有影响
  • 查询速度明显降低(因为需要依次比较), 不能发挥BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢
  • 解决方案-平衡二叉树(AVL) 在这里插入图片描述

AVL树基本介绍:

  • 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高。
  • 具有以下特点:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
  • 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。

旋转方式:

  • 进行旋转就可以将非平衡二叉树转成平衡二叉树

  • 单旋转(左旋转)在这里插入图片描述

  • 单旋转(右旋转)在这里插入图片描述

  • 双旋转(单旋转不能完成平衡二叉树的转换时使用双旋转)在这里插入图片描述

红黑树

比较:

平衡二叉树类型平衡度调整频率适用场景
AVL树查询多,增/删少
红黑树增/删频繁
简介:
  • 红黑树也是一种平衡二叉树,但每个节点有一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍。

  • 因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度<=红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,用红黑树。

性质:在这里插入图片描述

  • 每个节点非红即黑。
  • 不可能有连在一起的红色节点。
  • 根节点是黑的。
  • 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的。
  • 每个红色节点的两个子节点都是黑色。
  • 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
  • 红黑树并不是一个完美平衡二叉查找树,从下图可以看到,根结点P的左子树显然比右子树高,但左子树和右子树的黑结点的层数是相等的,也即任意一个结点到到每个叶子结点的路径都包含数量相同的黑结点。所以我们叫红黑树这种平衡为黑色完美平衡。在这里插入图片描述

红黑树平衡的三种操作:

  • 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。在这里插入图片描述
  • 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。在这里插入图片描述
  • 变色:结点的颜色由红变黑或由黑变红。

30张图带你彻底理解红黑树

多路查找树

二叉树的问题分析:

  • 二叉树的操作效率较高,但是也存在问题, 请看下面的二叉树

  • 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:

  • 问题1:在构建二叉树时,需要多次进行i/o操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响

  • 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度在这里插入图片描述

多叉树:

  • 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
  • 2-3树,2-3-4树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
  • 举例说明(下面2-3树就是一颗多叉树)

在这里插入图片描述 B树的基本介绍:

  • B-tree树即B树,B即Balanced,平衡的意思。

  • B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。

  • 如图B树通过重新组织节点, 降低了树的高度.在这里插入图片描述

  • 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为4k),这样每个节点只需要一次I/O就可以完全载入

  • 将树的度M设置为1024,在600亿个元素中最多只需要4次I/O操作就可以读取到想要的元素, B树(B+)广泛应用于文件存储系统以及数据库系统中

2-3树和2-3-4树基本介绍:

  • 2-3树是最简单的B树结构, 具有如下特点:
  • 2-3树的所有叶子节点都在同一层.(只要是B树都满足这个条件)
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点.
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点.
  • 2-3树是由二节点和三节点构成的树。
  • 除了23树,还有234树等,概念和23树类似,也是一种B树。 如图: 在这里插入图片描述

B树的说明:在这里插入图片描述

  • B树的阶:节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4
  • B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  • 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据.
  • 搜索有可能在非叶子结点结束
  • 其搜索性能等价于在关键字全集内做一次二分查找

B+树的说明:在这里插入图片描述

  • B+树是B树的变体,也是一种多路搜索树。
  • B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
  • 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
  • 不可能在非叶子结点命中
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  • 更适合文件索引系统
  • B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然.

B*树的说明:在这里插入图片描述

  • B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
  • B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3,而B+树的块的最低使用率为B+树的1/2。
  • 从第1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高

为什么要有图:

  • 线性表局限于一个直接前驱和一个直接后继的关系
  • 树也只能有一个直接前驱也就是父节点
  • 当我们需要表示多对多的关系时, 这里我们就用到了图

图的举例说明:

  • 图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点。如图:在这里插入图片描述

图的常用概念:

  • 顶点(vertex)
  • 边(edge)
  • 路径: 比如从 D -> C 的路径有 1、 D->B->C 2、D->A->B->C
  • 无向图: 顶点之间的连接没有方向,比如A-B, 即可以是 A-> B 也可以 B->A . 在这里插入图片描述
  • 有向图: 顶点之间的连接有方向,比如A-B, 只能是 A-> B 不能是 B->A .在这里插入图片描述
  • 带权图: 这种边带权值的图也叫网在这里插入图片描述

图的表示方式有两种:

  • 二维数组表示(邻接矩阵) 邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1....n个点。在这里插入图片描述

  • 链表表示(邻接表)。 1、 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失. 2、邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成在这里插入图片描述

图遍历介绍:

  • 所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:
  • (1)深度优先遍历
  • (2)广度优先遍历

图的深度优先搜索(Depth First Search) :

  • 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
  • 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
  • 显然,深度优先搜索是一个递归的过程

深度优先遍历算法步骤:

  • 访问初始结点v,并标记结点v为已访问。
  • 查找结点v的第一个邻接结点w。
  • 若w存在,则继续执行4,如果w不存在,则回到第1步,将从v的下一个结点继续。
  • 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。
  • 查找结点v的w邻接结点的下一个邻接结点,转到步骤3

广度优先遍历基本思想:

  • 图的广度优先搜索(Broad First Search) 。
  • 类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点

广度优先遍历算法步骤:

  • 访问初始结点v并标记结点v为已访问。
  • 结点v入队列
  • 当队列非空时,继续执行,否则算法结束。
  • 出队列,取得队头结点u。
  • 查找结点u的第一个邻接结点w。
  • 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤: 1、 若结点w尚未被访问,则访问结点w并标记为已访问。 2、 结点w入队列 3 、查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。

常用10种算法

二分查找算法(非递归)

简介:

  • 前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
  • 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
  • 二分查找法的运行时间为对数时间O(㏒₂n),即查找到需要的目标位置最多只需要㏒₂n步,假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)

分治算法

简介:

  • 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
  • 分治算法可以求解的一些经典问题 二分搜索 大整数乘法 棋盘覆盖 合并排序 快速排序 线性时间选择 最接近点对问题 循环赛日程表 汉诺塔

分治算法的基本步骤:

  • 分治法在每一层递归上都有三个步骤:
  • 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  • 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  • 合并:将各个子问题的解合并为原问题的解。

分治算法最佳实践-汉诺塔:

  • 汉诺塔的传说
  • 汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

动态规划算法

动态规划算法介绍:

  • 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
  • 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
  • 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
  • 动态规划可以通过填表的方式来逐步推进,得到最优解.

动态规划算法最佳实践-背包问题: 背包问题:有一个背包,容量为4磅 , 现有如下物品 在这里插入图片描述

  • 要求达到的目标为装入的背包的总价值最大,并且重量不超出
  • 要求装入的物品不能重复
  • 思路分析和图解 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用) 这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。
  • 思路分析和图解 算法的主要思想,利用动态规划来解决。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值。则我们有下面的结果: (1) v[i][0]=v[0][j]=0; //表示 填入表 第一行和第一列是0 (2) 当w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略 (3) 当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
    // 当 准备加入的新增的商品的容量小于等于当前背包的容量, // 装入的方式: v[i-1][j]: 就是上一个单元格的装入的最大值 v[i] : 表示当前商品的价值 v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值 当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :

KMP算法

应用场景-字符串匹配问题:

  • 字符串匹配问题:
  • 有一个字符串 str1= ""帅哥你好好你你好帅哥帅哥帅哥"",和一个子串 str2="帅哥帅"
  • 现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1

暴力匹配算法:

  • 如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
  • 如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
  • 如果失配(即str1[i]! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
  • 用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间。(不可行!)
  • 暴力匹配算法实现.

KMP算法介绍:

  • KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
  • Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法.
  • KMP方法算法就利用之前判断过信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间

部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例:

  - "A"的前缀和后缀都为空集,共有元素的长度为0;

  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",
  长度为1;

  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],
  共有元素为"AB",长度为2;

  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, 
  DABD, ABD, BD, D],共有元素的长度为0

在这里插入图片描述

KMP匹配详细讲解+next数组真正理解

贪心算法

贪心算法介绍:

  • 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法

  • 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果

贪心算法最佳应用-集合覆盖:

  • 假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号 在这里插入图片描述
  • 思路分析: 如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有 2ⁿ -1 个,假设每秒可以计算10个子集, 如图:在这里插入图片描述
  • 思路分析: 使用贪婪算法,效率高: 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合: 1、遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系) 2、将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。 3、重复第1步直到覆盖了全部的地区

贪心算法注意事项和细节:

  • 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
  • 比如上题的算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区
  • 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果K2 的使用成本低于K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的.

普利姆算法(Prim)

应用场景-修路问题:

  • 有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通在这里插入图片描述

  • 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里

  • 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?

  • 思路: 将10条边,连接即可,但是总的里程数不是最小.

  • 正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.

  • 修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。

  • 给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

普里姆算法介绍:

  • 普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图
  • 普利姆的算法如下:
  • ①设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
  • ②若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
  • ③若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中,将边(ui,vj)加入集合D中,标记visited[vj]=1
  • ④重复步骤②,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边

克鲁斯卡尔算法(Kruskal)

应用场景-公交站问题:

  • 某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通在这里插入图片描述

  • 各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里

  • 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?

克鲁斯卡尔算法介绍:

  • 克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
  • 基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
  • 具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

迪杰斯特拉算法(Dijkstra)

应用场景-最短路径问题:

  • 战争时期,胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在有六个邮差,从G点出发,需要分别把邮件分别送到 A, B, C, D, E, F 六个村庄在这里插入图片描述

  • 各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里

  • 问:如何计算出G村庄到 其它各个村庄的最短距离?

  • 如果从其它点出发到各个点的最短距离又是多少?

迪杰斯特拉(Dijkstra)算法介绍:

  • 迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
  • 典型的求单源最短路径算法

迪杰斯特拉(Dijkstra)算法过程:

  • 设置出发顶点为v,顶点集合V{v1,v2,vi...},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di...},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di) 1、从Dis中选择值最小的di并移出Dis集合,同时移出V集合中对应的顶点vi,此时的v到vi即为最短路径 2、更新Dis集合,更新规则为:比较v到V集合中顶点的距离值,与v通过vi到V集合中顶点的距离值,保留值较小的一个(同时也应该更新顶点的前驱节点为vi,表明是通过vi到达的) 3、重复执行两步骤,直到最短路径顶点为目标顶点即可结束

弗洛伊德算法(Floyd)

弗洛伊德(Floyd)算法介绍:

  • 和Dijkstra算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
  • 弗洛伊德算法(Floyd)计算图中各个顶点之间的最短路径
  • 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
  • 弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

弗洛伊德(Floyd)算法图解分析:

  • 设置顶点vi到顶点vk的最短路径已知为Lik,顶点vk到vj的最短路径已知为Lkj,顶点vi到vj的路径为Lij,则vi到vj的最短路径为:min((Lik+Lkj),Lij),vk的取值为图中所有顶点,则可获得vi到vj的最短路径
  • 至于vi到vk的最短路径Lik或者vk到vj的最短路径Lkj,是以同样的方式获得

马踏棋盘算法

马踏棋盘算法简介:

  • 马踏棋盘算法也被称为骑士周游问题
  • 将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格在这里插入图片描述

马踏棋盘游戏代码实现:

  • 马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。

  • 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,如图:走到了第53个,坐标(1,0),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯…… ,思路分析+代码实现在这里插入图片描述

  • 分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化。解决马踏棋盘问题.(优先访问)在这里插入图片描述

  • 使用前面的游戏来验证算法是否正确。