现在我们续写上一章博客的内容(即99章博客的内容)
快速排序:
同冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的
不同的是,冒泡排序在每一轮中只把1个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素
并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分,这种思路就叫作分治法
基准元素的选择(看如下例子):
基准元素,英文是pivot,在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边,当然,这是过程,后面会给出图解
我们可以随机选择一个元素作为基准元素(一般选择第一个),并且让基准元素和数列首元素交换位置
元素的交换:
选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元素一边,大于基准元素的都交换到基准元素另一边,也就是过程,后面有图解
双边循环法(看如下例子,不是上面的图片哦):
首先,选定基准元素pivot(这里选择第一个位置,也就是4,或者你认为原来8的位置是4也行),交换后,然后与首位置元素交换,并且设置两个指针left和right,指向数列的最左和最右两个元素
接下来进行第1次循环:
从right指针开始(即right需要比left先移动,这是重合的前提),让指针所指向的元素和基准元素做比较,如果大于或等于pivot,则指针向左移动
如果小于pivot,则right指针停止移动,再切换到left指针
轮到left指针行动,让指针所指向的元素和基准元素做比较,如果小于或等于pivot,则指针向右移 动
如果大于pivot,则left指针停止移动,当他们都停止移动后,然后左右指针指向的元素交换位置
上面就是操作规则,后面都按照这样来操作
由于left开始指向的是基准元素,判断肯定相等,所以left右移1位,所以在程序里面,可以选择直接先向右移动(即通常是+1),但是需要特殊的处理,再程序里会说明 ,这里选择操作+1
由于7>4,left指针在元素7的位置停下,这时,让left和right指针所指向的元素进行交换
接下来,进入第2次循环,重新切换到right指针,向左移动,right指针先移动到8,8>4,继续左移,由于2<4,停止在2的位置
只要重合就操作与首位置交换,所以right一般后面需要进行判断操作
我们可以看到,最后的结果是3,1,2,4,5,6,8,7,所以可以看到原来基准4的左边的确比他小,右边也的确比他大,即前面我们说过的过程就是上面的图解
然后我们会进行再次的分治,当然,再这之前,我们需要以left以及right重合的部分,进行分开,即我们分开成3,1,2,4(包含重合部分)以及5,6,8,7,然后在他们两个之间都自己选择一个基准,再次的排序,如果3,1,2,4,选择了2,以及5,6,8,7,选择了6,过程如下:
单边循环法:
单边循环法只从数组的一边对元素进行遍历和交换
开始和双边循环法相似,首先选定基准元素pivot
同时,设置一个mark指针指向数列起始位置, 这个mark指针代表小于基准元素的区域边界
这里还是以第一个作为基准
接下来,从基准元素的下一个位置开始遍历数组
如果遍历到的元素大于基准元素,就继续往后遍历
如果遍历到的元素小于基准元素,则需要做两件事:
第一,把mark指针右移1位,因为小于pivot的区域边界增大了1
第二,让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素归属于小于pivot的区域
首先遍历到元素7,7>4,所以继续遍历
接下来遍历到的元素是3,3<4,所以mark指针右移1位
随后,让元素3和mark指针所在位置的元素交换,因为元素3归属于小于pivot的区域(很明显,这个区域就是mark之前的位置,包括自身,原来是0,即可以发现,mark相当于前面说的双边循环移动的left,只不过只将right的操作,直接操作遍历了,然后交换了)
即无论使用哪种方式都可以
按照这个思路,继续遍历,后续步骤如图所示
后面然后分开即可,与双边是一样的,他也是使得操作两边,且循环基本一致,所以也是O(nlogn)
我们可以发现,无论是单边循环还是双边循环,最后都要进行交换回来,因为我们需要操作两边的,否则就不符合两边了,你可能会有疑问,不交换好像也行,实际上是不行的,所以一般我们都会交换,在代码中可以体现(有解释)
现在我们来编写代码,你可以试着将代码看完后,再自行编写一下:
package com. lagou ;
import java. util. Arrays ;
public class test4 {
public static int two ( int [ ] arr, int start, int end) {
int pivot;
pivot = start;
int left = start + 1 ;
int right = end;
while ( true ) {
while ( left < right && arr[ right] > arr[ pivot] ) {
right-- ;
}
while ( left < right && arr[ left] < arr[ pivot] ) {
left++ ;
}
if ( left < right) {
int temp = arr[ left] ;
arr[ left] = arr[ right] ;
arr[ right] = temp;
right-- ;
left++ ;
}
if ( left >= right) {
if ( arr[ right] > arr[ pivot] ) {
right-- ;
}
int temp = arr[ right] ;
arr[ right] = arr[ pivot] ;
arr[ pivot] = temp;
break ;
}
}
return right;
}
public static int one ( int [ ] arr, int start, int end) {
int pivot;
pivot = start;
int mark = start;
for ( int i = start + 1 ; i <= end; i++ ) {
if ( arr[ pivot] > arr[ i] ) {
mark++ ;
if ( i != mark) {
int temp = arr[ mark] ;
arr[ mark] = arr[ i] ;
arr[ i] = temp;
}
}
}
int temp = arr[ mark] ;
arr[ mark] = arr[ pivot] ;
arr[ pivot] = temp;
return mark;
}
public static void quickSort ( int [ ] arr, int start, int end) {
if ( start >= end|| arr== null ) {
return ;
}
int mark = one ( arr, start, end) ;
quickSort ( arr, start, mark - 1 ) ;
quickSort ( arr, mark + 1 , end) ;
}
public static void quickSort1 ( int [ ] arr, int start, int end) {
if ( start >= end|| arr== null ) {
return ;
}
int pivot = two ( arr, start, end) ;
quickSort1 ( arr, start, pivot - 1 ) ;
quickSort1 ( arr, pivot + 1 , end) ;
}
public static void main ( String [ ] args) {
int [ ] arr = new int [ ] { 4 , 7 , 3 , 5 , 6 , 2 , 8 , 1 } ;
quickSort ( arr, 0 , arr. length - 1 ) ;
System . out. println ( Arrays . toString ( arr) ) ;
arr = new int [ ] { 4 , 2 , 3 , 5 , 6 , 7 , 8 , 1 } ;
quickSort1 ( arr, 0 , arr. length - 1 ) ;
System . out. println ( Arrays . toString ( arr) ) ;
}
}
至此,单边和双边都操作完毕,他们实际上都是局部的进行操作,先进行分开,然后局部分开,所以使得除了他们自身的移动次数n外(整体来看就是n,因为无论你是否分开,判断的次数,只是从8到4+4而已),只需要操作logn次数即可,因为是分开的吗(前面已经说明过了)
即:快速排序的时间复杂度是:O(nlogn)
堆排序:
堆排序:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法
堆是具有以下性质的完全二叉树
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中:
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2],2i+2写成2*(i+1)也可
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点
将其与末尾元素进行交换,此时末尾就为最大值,然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值
如此反复执行,便能得到一个有序序列了
构造初始堆:
将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,因为他找最大,降序采用小顶堆,因为他找最小)
由于是完全二叉树,所以完全可以一路的定义过去,当我们通过数组创建了一个完全二叉树后,就可以继续下面的步骤了(你都可以解决红黑树,那么这个条件不难吧,具体思路:我们主要的难点就是如何进行连接节点,由于数组的存放,对应公式,即对应节点,我们可以利用这种,来保存一个list集合,然后让list集合也这样的根据公式进行连接,然后再根据公式进行赋值,那么一个数组就变成了二叉树了,后面会给出具体操作的),或者就是一个数组,而不操作二叉树也行,那么就只需要定义数组了,在后面的代码中就以数组为例子
此时我们从最后一个非叶子结点开始(叶结点自然不用调整,这里的第一个非叶子结点 arr.length/2- 1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整,为什么是这样的公式,我们看图,假设总节点为n,我们有四种情况,即叶子节点只有1个,2个,3个,4个,以下标为例子:
如果叶子节点是1个:那么n/2-1,就是第一个非叶子结点,比如4/2-1=1,举例:如果上面就到5,那么4/2-1=1,刚好就是6,即正确
如果叶子节点是2个:那么也是n/2-1,举例:如果上面到9,那么5/2-1,也是6,即正确
如果叶子节点是3个:那么如果是n/2-1,举例:如果上面8后面加上了左节点7,那么6/2-1=2,到8了,即也是第一个(8变成了非叶子节点了)
如果叶子节点是4个,那么如果是n/2-1,举例:如果上面8后面加上了右节点10,那么7/2-1=2,还是8,即也是第一个
实际上我们可以看到,他就是来算出最后一个非叶子节点的,实际上我们也可以通过公式来说明,我们知道,在下标为0来算时,一个非叶子节点的左右节点是2i+1和2i+2,那么如果这两个是最后两个,就说明他们的父节点,就是第一个非叶子节点,所以我们需要得出i的值,很明显,2i+2只需要除以2减1即可,但是前面的却不能,因为其中的1在过来时,是没有操作的,所以这个时候,他需要加上1,或者不除以2减1,那么只需要将2i+2减1即可,那么由于结果相同,且我们又可以发现,使用数组的长度,刚好就是arr.length/2- 1了
我们可以发现,为什么从0开始后,就要使用从1开始的计算方式(也就是个数),因为除法一般对于0开始有缺陷的(因为其他的数,在除2取整时,基本都有两个数的除以对应,而o只有1),否则一般需要加1来保证从1开始的计算方式(后续减1就行),或者通过减1,来满足缺陷,也就是使用下标,而不是个数,所以在前面的计算父节点的以及这里的除法都需要从1开始,否则前者偶数减1(因为3,4只要1),后者奇数加1(因为3,4,之后有减1,需要抵消),这是不同的解决方式,当然,这里是从0开始的,所以这里是后者,而之前的是前者,但无论怎么说本质上是起始数字的不同导致的,前面说明过了,而后者奇数加1的同时,偶数加1也没有问题,所以导致使用数组长度也没有问题,所以就是arr.length/2- 1了
我们可以发现,如果探索本质,需要很多功夫,但是,如果你认为逻辑上是这样,那么只需要知道他们的规律即可,而不需要知道为什么是这样的规律,所以上面的解释可以选择忽略,只需要知道arr.length/2- 1就能求出最后一个非叶子节点即可
上面我们找到了最后一个非叶子节点,所以我们能够得出他的值,并且也能得出他们三个的大小,我们将非叶子节点于他左右节点中最大的进行交换,如果他就是最大的,那么就操作后面的
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换,那么第二个非叶子节点怎么找呢,很简单,数组长度,减去1即可,使得可以找到第二个非叶子节点,然后重新前面的步骤即可,因为堆是完全二叉树,那么他的前一个节点,必然是有左右节点的,否则他是不会出现的
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6
此时,我们就将一个无序序列构造成了一个大顶堆,我们发现,前面的操作都是为了将大的往上面走,并且是所以的非叶子节点,那么执行多少循环呢,一般需要二叉树的高度,你可以发现,将子树的数目认为是n,高度认为是x(从1开始,这里就不从0开始了),那么对应的循环次数就是n+x-1(只会操作被交换的往后面走,所以是x-1,在程序里面会有体现,而-1,代表根节点不用考虑了,因为已经是最大的,只需要考虑其他的高度的地方),一般来说,n个子树,对应的节点数是2n到2n+1,而高度x对应的节点数是x到x^2-1
相反,如果节点数是n,那么就有n/2个子树,和n的平方根个高度(这里取平均),所以总循环次数对于总结点来说就是"n/2+(n的平方根)-1"
那么总体来说,可以认为时间复杂度是O(n),因为n是最大的一个,对于n的平方根来说,在一开始虽然他是比logn要多的,中间少,然后又变多,即后面要多了,并且他们基本都比n/2也要少,长远来看(看图像就知道了),所以对于时间复杂度来说,n的平方根的时间复杂度是他们中是中等的(比logn多,那么时间复杂度就比他差,因为次数多,同理那么时间复杂度就比n/2好,即时间复杂度比n/2低,即比n/2好,因为时间复杂度越低越好),由于没有说明其时间复杂度,所以可以省略,即可以将"n/2+(n的平方根)-1"看成是n
将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆(除了交换后的那个元素),再将堆顶元素与末尾元素交换,得到第二大元素,如此反复进行交换、重建、交换即可
将堆顶元素9和末尾元素4进行交换
重新调整结构,使其继续满足堆定义
再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序,所以我们可以看到,他需要执行数组的次数,但是他的交换是变化的,可以发现,每操作一半数组的长度(跟着的),那么次数大概就会少一个平方根,所以平均看起来,可以发现,就是logn的次数,所以总时间复杂度是O(nlogn)(平均的认为,虽然实际上也是),n代表节点数
具体代码如下(你可以选择看完后,自己编写,因为有很多细节,所以建议先看,当然,若可以的话,也能先写好再看):
package com. lagou ;
import java. util. * ;
public class test5 {
public static void max ( int [ ] array, int start, int last) {
int chi = 2 * start + 1 ;
while ( chi <= last) {
if ( chi + 1 <= last && array[ chi] < array[ chi + 1 ] ) {
chi++ ;
}
if ( array[ start] > array[ chi] ) {
break ;
}
int temp = array[ start] ;
array[ start] = array[ chi] ;
array[ chi] = temp;
start = chi;
chi = 2 * chi + 1 ;
}
}
public static void sort ( int [ ] array) {
for ( int i = array. length / 2 - 1 ; i >= 0 ; i-- ) {
max ( array, i, array. length - 1 ) ;
}
for ( int i = 0 ; i < array. length - 1 ; i++ ) {
int temp = array[ array. length - 1 - i] ;
array[ array. length - 1 - i] = array[ 0 ] ;
array[ 0 ] = temp;
max ( array, 0 , array. length - 1 - 1 - i) ;
}
}
public static void max1 ( int [ ] array, int start, int last) {
int temp = array[ start] ;
int chi = 2 * start + 1 ;
while ( chi <= last) {
if ( chi + 1 <= last && array[ chi] < array[ chi + 1 ] ) {
chi++ ;
}
if ( temp > array[ chi] ) {
break ;
}
array[ start] = array[ chi] ;
start = chi;
chi = 2 * chi + 1 ;
}
array[ start] = temp;
}
public static void sort2 ( int [ ] array) {
for ( int i = array. length / 2 - 1 ; i >= 0 ; i-- ) {
max ( array, i, array. length - 1 ) ;
}
for ( int i = array. length - 1 ; i > 0 ; i-- ) {
int temp = array[ i] ;
array[ i] = array[ 0 ] ;
array[ 0 ] = temp;
max ( array, 0 , i - 1 ) ;
}
}
public class Node {
int key;
Node left;
Node right;
public Node ( int key) {
this . key = key;
}
}
Node root;
public void head ( int [ ] array) {
if ( array != null ) {
List < Node > list = new ArrayList < > ( ) ;
for ( int i = 0 ; i < array. length; i++ ) {
list. add ( new Node ( 0 ) ) ;
}
list. get ( 0 ) . key = array[ 0 ] ;
root = list. get ( 0 ) ;
for ( int i = 0 ; i < list. size ( ) ; i++ ) {
int left = 2 * i + 1 ;
int right = 2 * i + 2 ;
if ( left < list. size ( ) ) {
list. get ( i) . left = list. get ( left) ;
list. get ( left) . key = array[ left] ;
}
if ( right < list. size ( ) ) {
list. get ( i) . right = list. get ( right) ;
list. get ( right) . key = array[ right] ;
}
}
int [ ] result = new int [ list. size ( ) ] ;
int i = 0 ;
Queue < Node > queue = new LinkedList ( ) ;
queue. offer ( root) ;
while ( ! queue. isEmpty ( ) ) {
Node poll = queue. poll ( ) ;
result[ i] = poll. key;
i++ ;
if ( poll. left != null ) {
queue. offer ( poll. left) ;
}
if ( poll. right != null ) {
queue. offer ( poll. right) ;
}
}
String b = "" ;
System . out. print ( "[" ) ;
for ( int ii = 0 ; ii < result. length; ii++ ) {
if ( ii == result. length - 1 ) {
b = b + result[ ii] + "]" ;
} else {
b = b + result[ ii] + "," ;
}
}
System . out. println ( b) ;
} else {
System . out. println ( "没有数组信息" ) ;
}
}
public static void main ( String [ ] args) {
int [ ] arr = { 7 , 6 , 4 , 3 , 5 , 2 , 10 , 9 , 8 } ;
System . out. println ( "排序前:" + Arrays . toString ( arr) ) ;
sort ( arr) ;
System . out. println ( "排序后:" + Arrays . toString ( arr) ) ;
arr = new int [ ] { 7 , 6 , 4 , 3 , 5 , 2 , 10 , 9 , 8 } ;
System . out. println ( "排序前:" + Arrays . toString ( arr) ) ;
sort2 ( arr) ;
System . out. println ( "排序后:" + Arrays . toString ( arr) ) ;
test5 t = new test5 ( ) ;
t. head ( arr) ;
System . out. println ( t. root) ;
}
}
堆排序的时间复杂度是: O(nlogn),前面已经说明过了
计数排序 :
计数排序,这种排序算法是利用数组下标来确定元素的正确位置的
假设数组中有10个整数,取值范围为0~10,要求用最快的速度把这10个整数从小到大进行排序
可以根据这有限的范围,建立一个长度为11的数组,数组下标从0到10,元素初始值全为0
假设数组数据为:9,1,2,7,8,1,3,6,5,3
下面就开始遍历这个无序的随机数列,每一个整数按照其值对号入座,同时,对应数组下标的元素进行加1操作
例如第1个整数是9,那么数组下标为9的元素加1
最终,当数列遍历完毕时,数组的状态如下:
该数组中每一个下标位置的值代表数列中对应整数出现的次数
直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次,0不输出
则顺序输出是:1、1、2、3、3、5、6、7、8、9
计数排序:适合于连续的取值范围不大的数组,可以为0(因为有0这个下标),不连续和取值范围过大会造成数组过大
如果起始数不是从0开始,比如分数排序:
95,94,91,98,99,90,99,93,91,92
数组起始数为90,这样数组前面的位置就浪费了
可以采用偏移量的方式:
比如,起始值为90,那么他们依次的减去90即可,使得可以不用创建很多个数组,当然,如果是无序的,或者说,不知道起始值,那么需要求出最小,在后面会说明
具体代码如下,可以先自己编写一遍:
package com. lagou ;
public class test6 {
public static void Sort ( int [ ] array, int offset) {
int [ ] a = new int [ array. length] ;
for ( int i = 0 ; i < array. length; i++ ) {
a[ array[ i] - offset] ++ ;
}
System . out. print ( "[" ) ;
int ii = 0 ;
for ( int i = 0 ; i < a. length; i++ ) {
while ( a[ i] != 0 ) {
if ( ii == 0 ) {
System . out. print ( i + offset) ;
ii++ ;
a[ i] -- ;
} else {
System . out. print ( "," + ( i + offset) ) ;
a[ i] -- ;
}
}
}
System . out. println ( "]" ) ;
}
public static void main ( String [ ] args) {
int [ ] scores = { 95 , 94 , 91 , 98 , 99 , 90 , 99 , 93 , 91 , 92 } ;
Sort ( scores, 90 ) ;
}
}
计数排序的时间复杂度是O(n+m),当然,上面的先给出最小的,如果需要求出最小,那么基本只能将他们放入堆(最小堆)中来进行求出,只是可能需要nlogn时间复杂度了,当然这是不知道最小的情况,实际上就算我们不知道最小是多少,我们也可以使用空间换取时间,来变成O(n+m),只是不划算而已,特别是数很大时,即数越大越不划算,如果是10000,那么还是基本只能操作nlogn了(如果有其他好的方法,也可以使用),当然nlogn只是操作整体排序而已(因为包含数组),实际上如果只需要求出一个最大,那么也只需要O(n)(就一个,没有数组),在前面的堆的解释中可以知道的,所以计数排序的时间复杂度还是O(n+m),只是在不知道最小的情况下,可能是n+m+k,k是堆第一次排序的次数(前面堆中说明的n)
n:原数据个数(也就是数组的个数)
m:数据操作个数(第二个循环,也就是要操作的数据)
注意:第二个循环,因为0的存在,使得,需要多几次,所以这个m通常大于n,实际上这样也导致,没有必要的空间出现,所以为了解决这样的情况,我们一个会使用范围来操作,使得减少这样的情况,也就是使用后面的桶排序了
桶排序 :
桶排序同样是一种线性时间的排序算法
桶排序需要创建若干个桶来协助排序
每一个桶(bucket)代表一个区间范围,里面可以承载一个或多个元素
桶排序的开始,就是创建这些桶,并确定每一个桶的区间范围具体需要建立多少个桶,如何确定桶的区间范围,有很多种不同的方式,我们这里创建的桶数量等于原始数列的元素数量,除最后一个桶只包含数列最大值外, 前面各个桶的区间按照比例来确定
区间跨度 = (最大值-最小值)/ (桶的数量 - 1)
为什么要创建的桶数量等于原始数列的元素数量,是因为最后一个桶可以更方便的操作最大值,在程序里会说明该问题
程序里也说明了可以不操作最后一个桶,当然,在时间复杂度上,也有原因,后面会说明
那么还有个问题:桶可以很少吗,或者很多吗,实际上都可以,只是由于若桶很少,那么对应的排序就需要更多,因为每多一个元素,就可能需要多排序对应长度的次数(具体参考冒泡排序,也可能增加一些次数,具体参考快速排序),而如果过少,那么占用的空间也非常大,所以我们操作平均,也就是桶数量等于原始数列的元素数量,一般都是这样来操作
那么在程序里面如果操作呢,答:使用公式:(当前值-最小值)*操作的桶数(这里是4,不考虑最后一个)/(最大值-最小值),这是为了确定当前值在桶里面的比例,因为他"操作的桶数/(最大值-最小值)“代表每"桶/值”,即可以认为是每多少桶代表1值(操作除法,一般要使得分母要为1),或者每1值,代表多少桶(也可以反过来说明的,只不过他是1而已,因为通常因为除法),因为他们是对应的关系,就如"位移/时间",一样,每多少位移代表多少时间,或者每多少时间代表多少位移,只是计算具体的方式是位移在上的,从而得出速度,那么这里就代表下标,实际上很明显,因为最后一个数(最大的数)是独有的下标,所以对应的桶的数量一般都有减1来满足下标的情况,所以该公式的桶数量在这里也必然是4,这也是需要最后一个桶的原因,因为区间对比数量来说是少1的,所以在程序中,对应的桶数量要记得减1,否则下标会越界的,在代码中会详细的说明,这里可以先进行了解,实际上是因为区间的问题,所以无论是上面的区间跨度的桶数量,还是公式里面的下标的桶数量,都需要减1,程序里会说明的
假设有一个非整数数列如下:
4.5,0.84,3.25,2.18,0.5
以0.84为例,使用下标来表示就是0.84-0.5=0.34,0.34*4/4=0.34,代表0.34桶(值单位与值单位抵消),由于他先是减去0.5,所以将原来的值认为从0开始,那么0.34是小于1的,那么就是下标0,因为取整,正好也对应于下标,所以程序里就使用该公式来操作具体是那个桶了,而不是区间跨度(代表不是程序的公式)
遍历原始数列,把元素对号入座放入各个桶中
对每个桶内部的元素分别进行排序(显然,只有第1个桶需要排序)
遍历所有的桶,输出所有元素
0.5,0.84,2.18,3.25,4.5
具体的代码可以先看完下面代码后,自己编写:
package com. lagou ;
import java. util. * ;
public class test7 {
public static void Sort ( double [ ] array) {
double max = array[ 0 ] ;
double min = array[ 0 ] ;
for ( int i = 1 ; i < array. length; i++ ) {
if ( array[ i] > max) {
max = array[ i] ;
}
if ( array[ i] < min) {
min = array[ i] ;
}
}
double d = max - min;
List < LinkedList < Double > > l = new ArrayList < > ( array. length) ;
for ( int i = 0 ; i < array. length; i++ ) {
l. add ( new LinkedList ( ) ) ;
}
for ( int i = 0 ; i < array. length; i++ ) {
int num = ( int ) ( ( array[ i] - min) * ( array. length - 1 ) / d) ;
l. get ( num) . add ( array[ i] ) ;
}
for ( int i = 0 ; i < l. size ( ) ; i++ ) {
Collections . sort ( l. get ( i) ) ;
}
double [ ] sortedArray = new double [ array. length] ;
int index = 0 ;
for ( LinkedList < Double > list : l) {
for ( double element : list) {
sortedArray[ index] = element;
index++ ;
}
}
System . out. print ( "[" ) ;
for ( int i = 0 ; i < sortedArray. length; i++ ) {
if ( i == sortedArray. length - 1 ) {
System . out. println ( sortedArray[ i] + "]" ) ;
} else {
System . out. print ( sortedArray[ i] + "," ) ;
}
}
}
public static void main ( String [ ] args) {
double [ ] array = { 4.12 , 6.421 , 0.0023 , 3.0 , 2.123 , 8.122 , 4.12 , 10.09 } ;
Sort ( array) ;
System . out. println ( "最大值是:" + array[ array. length - 1 ] ) ;
array = new double [ ] { 4.12 , 6.421 , 0.0023 , 3.0 , 2.123 , 8.122 , 4.12 , 10.09 } ;
List < LinkedList < Double > > max = max ( array) ;
Array ( max, array) ;
}
public static List < LinkedList < Double > > max ( double [ ] array) {
double max = array[ 0 ] ;
double min = array[ 0 ] ;
for ( int i = 1 ; i < array. length; i++ ) {
if ( array[ i] > max) {
max = array[ i] ;
}
if ( array[ i] < min) {
min = array[ i] ;
}
}
double d = max - min;
List < LinkedList < Double > > l = new ArrayList < > ( array. length) ;
for ( int i = 0 ; i < array. length; i++ ) {
l. add ( new LinkedList ( ) ) ;
}
for ( int i = 0 ; i < array. length; i++ ) {
int num = ( int ) ( ( array[ i] - min) * ( array. length - 1 ) / d) ;
l. get ( num) . add ( array[ i] ) ;
}
System . out. println ( "最大值是:" + l. get ( l. size ( ) - 1 ) . get ( 0 ) ) ;
return l;
}
public static void Array ( List < LinkedList < Double > > l, double [ ] array) {
for ( int i = 0 ; i < l. size ( ) ; i++ ) {
Collections . sort ( l. get ( i) ) ;
}
double [ ] sortedArray = new double [ array. length] ;
int index = 0 ;
for ( LinkedList < Double > list : l) {
for ( double element : list) {
sortedArray[ index] = element;
index++ ;
}
}
System . out. print ( "[" ) ;
for ( int i = 0 ; i < sortedArray. length; i++ ) {
if ( i == sortedArray. length - 1 ) {
System . out. println ( sortedArray[ i] + "]" ) ;
} else {
System . out. print ( sortedArray[ i] + "," ) ;
}
}
}
}
桶排序的时间复杂度是O(n),因为上面对应的操作基本都是操作对应数组长度的次数,也就是说,都省略的话,可以认为是O(n),而单看排序来说,实际上也的确只有n次,因为上面基本是平均的,也就是说,他们的结果实际上每个桶只有一次,所以是O(n),当然,实际上桶排序与快速排序有点类似,因为快速排序是先操作数据,然后局部操作区间,而这里直接的给数据给你了,然后操作区间,所以实际上桶排序节省了快速排序的交换(分开)次数(不是判断次数),也就是没有logn了,所以桶排序也能称为O(n),虽然这样说,基本是不正确的
实际上桶排序中,一般内部都会使用快速排序,但是,由于对应的进行了分配,所以除了比较次数外(因为比较只看长度,与是否分开无关,具体参考快速排序的n的解释),对应的logn中的n首先需要进行除以n(除以的n是桶的个数,也就是将分开的看成快速排序,只是对于整体而言,是除以n的个数),使得操作其对应的数据,即如果桶排序分配均匀,近似是O(n)了,否则如果除以的n是1,那么桶排序就是O(nlogn)了,除以的n是桶的数量,虽然最后一个桶一般只会占用一个,但也是占用了,当然不分配最后一个桶来除以n-1的话,也是近似O(n),因为差别不大,虽然可以使得除以的n变成很大,那么就比O(n)要小了,所以实际上桶的排序是平均起来是O(n),当然,也为了前面说的平衡,所以一般桶数量等于数组元素数量,这样就能最大限度的近似于O(n)了,这也是使用桶数量等于数组元素数量的一种原因
其实,我们可以定义链表大的在前面,所以由于整个数据是n,所以我们只需要判断链表的长度次数,那么实际上也是O(n),只是在输出时,是大的在前面了
但是对应使用的方法是Collections.sort,那么时间复杂度是多少呢,实际上他可以是O(n)(好像不确定,可以百度查看),注意该n只是当前所在的链表的元素个数,所以总体相加,那么就是O(n),该n代表数组长度
综上所述,桶排序的确是O(n),n是数组长度
各个排序比对表:
对于稳定性:冒泡的稳定解释已经说明了,快速之所以不稳定是因为对应的基准问题,实际上这里只会考虑相等的
由于对于的数据结构中,因为不相等的一般会操作false,所以冒泡由于不交换,所以稳定,快速由于不移动,所以不稳定(操作了相等的交换),堆由于不退出,使得交换父节点,所以不稳定,当然,他们都可以设置稳定,只是5总不能是大于5或者小于5吧,即认为稳定不考虑程序里面的相等的情况,或者说,自己解决的情况不算,因为快速可以设置>=进行right移动,使得5,4,5,中不会与5进行交换,所以导致可以稳定,而堆也可以设置>=,来使得退出,使得稳定,具体看对应代码即可
而由于计数和桶,因为只操作保存的数据,一般来说,如果是保存的数据,我们通常认为是有顺序的,所以他们都是稳定的
时间复杂度不用说明了,因为前面都已经说过了
那么现在我们说明空间复杂度,由于冒泡没有其他空间,只有赋值的空间,所以是O(1)
而快速排序由于没有增加数据时,都参与分开操作,所以实际上他的数据是针对与整体排序而言,是O(logn),即保证可以分开即可,而不必要添加相同长度的数组
堆排序也是一个数组,与冒泡一样,所以也是O(1)
计数排序,由于多出一个数组,所以是O(n),在程序里就是10,所以上面显示的就是程序里的
桶排序,由于需要创建桶,且认为桶的容量是数组的容量(保证防止越界,特别是数组),所以我们一般也会认为桶的空间复杂度是O(n)
注意,上面的空间复杂度代表的是提升,而不是总共,当然,就算是总共,由于省略,也就是提升了
至此排序操作完毕,实际上在硬件允许的情况下,我们可以来用空间换取时间(比如桶排序)
字符串匹配 :
字符串匹配这个功能,是非常常见的功能,比如"Hello"里是否包含"el"
Java里用的是indexOf函数(后面有案例),其底层就是字符串匹配算法,主要分类如下:
案例:
package com. lagou ;
public class test11 {
public static void main ( String [ ] args) {
System . out. println ( "hello" . indexOf ( "el" ) ) ;
}
}
BF 算法:
BF 算法中的 BF 是 Brute Force 的缩写,中文叫作暴力匹配算法,也叫朴素匹配算法
这种算法的字符串匹配方式很"暴力",当然也就会比较简单、好懂,但相应的性能也不高
比方说,我们在字符串 A 中查找字符串 B,那字符串 A 就是主串,字符串 B 就是模式串
我们在主串中,检查起始位置分别是 0、1、2…n,模式串长度是m,那么会操作n-m+1次,来看有没有跟模式串匹配的
这里是操作数量,也就是说,不会与前面的与起始数字情况公式有关(因为该公式是围绕下标的,所以该公式与下标有关,下标改变,他的结果也会改变,即通常需要换一种公式)
对应的代码,可以自己先进行操作:
package com. lagou ;
public class test22 {
public static void main ( String [ ] args) {
String i = "hello" ;
String k = "ll" ;
for ( int j = 0 ; j < i. length ( ) - k. length ( ) + 1 ; j++ ) {
boolean equals = i. substring ( j, k. length ( ) + j) . equals ( k) ;
if ( equals == true ) {
System . out. println ( "下标在" + j + "位置找到" ) ;
return ;
}
}
System . out. println ( "没有找到" ) ;
}
}
时间复杂度:
我们每次都比对 m 个字符,要操作 n-m+1 次,所以,这种算法的最坏情况时间复杂度是 O(n*m)
m:为匹配串长度
n:为主串长度
实际上他也有最好的,比如(以完全不匹配为例子,因为运气好的话,自然都只需要一次即可,这里我们考虑完全不匹配,即最多的次数,所以在有些情况下,时间复杂度,有时候就是考虑最多的情况):
n=1,m=1,那么就是1次
n=2,m=1或者2,那么就是2次
n=3,m=1或者3,就是3次,m=2就是4次
n=4,m=1或者4,就是4次,m=2或者3就是6次
很明显,在中间就是坏的情况,而两边就是好的情况,所以实际上最好的情况就是n,但是实际上,一般不会是两边,所以我们通常以m*(n-m+1)=n *m-m^2+m次来说明,一般来说,后面的可以省略,所以是O(n *m),当m是1或者n时,(不能省略,就是n了,如果省略,当m是n时,会变成n^2,这是错误的,所以不能省略,因为这里是对比操作了,对比1来说,对比不省略,实际上是计算,所以我们也说,计算也不操作省略,就如这里的n)
实际上当m是n时,若他不算对应的下标越界,还是可以变成n^2,但这并不理想,所以不会考虑,所以次数一般来说,我们直接计算出来,然后操作,从而不会导致越界
应用:
虽然BF算法效率不高但在实际情况下却很常用,因为:一般主串不会太长,实现简单
RK 算法 :
RK 算法的全称叫 Rabin-Karp 算法,是由它的两位发明者 Rabin 和 Karp 的名字来命名的
BF算法每次检查主串与子串是否匹配,需要依次比对每个字符,所以 BF 算法的时间复杂度就比较高,是O(n*m)
我们对朴素的字符串匹配算法稍加改造,引入哈希算法,时间复杂度立刻就会降低
RK 算法的思路是这样的:我们通过哈希算法对主串中的 n-m+1 个子串分别求哈希值,然后逐个与模式 串的哈希值比较大小
如果某个子串的哈希值与模式串相等,那就说明对应的子串和模式串匹配了(这 里先不考虑哈希冲突的问题)
因为哈希值是一个数字,数字之间比较是否相等是非常快速的,所以模式串和子串比较的效率就提高了,而不用操作对比了
可以设计一个hash算法:
将字符串转化成整数,利用K进制的方式
比如:123的拆解,也就是1进制(0-9),十个数,如果包括0的话,10代表11个数,否则就是10,当然,一般我们都从0开始,且这些数字只是用来方便计算的,二进制也是从0开始,虽然在数学中我们默认从1开始的,但是在进制里实际上是从0开始,即99就是100个数了(包括0),而要跳到上一层的0,即需要加1,我们也可以认为从1开始,也就是说,将第一个认为是特例,这样也行(大多数都是这样)
我们将使用这个方式,100+20+3=123
现在开始,我们认为这样:
小写字母a-z:26进制
大小写字母a-Z:52进制
大小写字母+(0-9,即数字):62进制
以只是小写字母的26进制为例
字符串"abc"转化成hash值的算法是:
a的ASCII码是97,b的ASCII码是98,c的ASCII码是99,正是因为他们的ASCII的值基本是不相同的,所以我们下面的计算方式基本是不会出现相同的,因为他的进制操作跨度比较大(即52,以及62,导致基本不会相同,当然如果都小于26也行,只要不是稍微大点即可,因为容易相同,逻辑思考一下就知道了,因为假设是3,4,那么3 * 4=4*3,很明显,他们一个乘以4,一个是3,4只比3大一点,所以有相等的可能,而如果3去乘的数比乘的4大很多,或者比乘的3小,那么基本不会相同)
那么可以不乘以进制数吗,当然不行,进制数是为了保证位置的,否则cb和bc的结果必然是相同的,但他们却不是相同的字符串
因为若主串是bc,那么cb是不能匹配成功的,再次举例:ad也会等于bc,因为他们相加都是197,所以我们需要进制来保证位置
那么有个问题,上面的进制是跨度足够比较大吗,实际上需要这样的考虑,考虑A和z,A是65,z是122,在上面说的,我们需要考虑最小和最大的界限,比如AA=6552+65,ZZ=90 * 52+90,而aa=97 * 26+97,zz=122 26+97,我们直接的比较,ZZ=4770,aa=2619,AA=3445,zz=3269,很明显,26的次方的最大值,都比52次方的最小值要小,所以他们不会相同,而52若再小点,就不一定了,即得出结论,52的跨度足够大,所以自定义的52可以使用
注意对应的方法可不是二进制的操作,即不是9*26^1+7 *26^0,这样的操作,他只是直接的乘,这就是K进制的方式
上面的结果是:65572+2548+99=68219
字符串"abc"转化成hash值是68219
一般情况下,97是当前主串和从串的最小数字,当然,中间也行,因为负数也不会相同,负数只是操作反向的数字而已,所以也是不会相同的,因为他们的差还是一样的(即不会为0,即不会相同,所以52还是可以)
字符串"abc"转化成hash值的算法是:
0+26+2=28,这样就能减少很多的计算
现在我们来进行实现(可以自己先操作一下):
package com. lagou ;
public class test33 {
public static int strToHash ( String s) {
int hash = 0 ;
for ( int i = 0 ; i < s. length ( ) ; i++ ) {
hash *= 26 ;
hash += ( s. charAt ( i) - 97 ) ;
}
return hash;
}
public static void is ( String s, String b) {
int i = strToHash ( b) ;
for ( int j = 0 ; j < s. length ( ) - b. length ( ) + 1 ; j++ ) {
if ( strToHash ( s. substring ( j, b. length ( ) + j) ) == i) {
System . out. println ( "匹配成功,下标是:" + j) ;
return ;
}
}
System . out. println ( "没有找到" ) ;
}
public static void main ( String [ ] args) {
is ( "avfg" , "fg" ) ;
}
}
我们可以看到,只需要对应的n-m+1次就行,如果m=1,那么就是n次,如果m=n,那么只需要1次,即平均我们可以认为是O(n),而不是之前的O(n*m)了,即这里最坏的情况就是O(n),也就是m=1的时候(最后一个获取,时间复杂度,在没有明确次数时,默认考虑最多的情况,而不是靠运气)
至此,时间复杂度我们变成了O(n)了,n代表对应的次数(n-m+1),比单纯的n要少次数的
注意:这里是没有考虑哈希算法的计算的 时间复杂度的,所以要注意
说到时间复杂度考虑最多的情况,好像之前的我们都没有这样说明,这里来补充,实际上之前的都只是操作数据结构,而数据结构,无论是数组还是链表,还是他们组成的操作,实际上与具体次数没有关系,只是他们的结构操作关系,所以最多和最少是没有任何说明的,那么实际上最多和最少,是体现在查找中,也就是说,无论是之前的查找,还是现在的查找,我们都按最多来计算,数据结构自然也是如此,因为只有查找基本才需要运气成分
时间复杂度:
RK 算法的的时间复杂度为O(n+m),上面不是说O(n),为什么这里需要加上m,这是因为考虑hash算法的存在(注意,哈希算法在代码里面实际上不要考虑,因为一般的哈希算法都是O(1),只是这里我们操作了自定义的,如果非要考虑,那么实际上String的哈希函数操作也是循环,但实际上只会认为是O(1),因为他是基础底层的原因,而在前面我们说过,时间复杂度,只是操作具体的明面上的次数,而不是基础底层的次数,所以哈希算法我们一般认为是O(1),前面说明过了,在时间复杂度的说明那里,如果这里在考虑循环是m的情况下,即也是n *m-m^2+m,但是由于要比原来的要多一个m,因为外面有一个循环,即是n * m-m^2+2m,所以实际上考虑,整体是要差的),导致的,这里我们就只考虑外面的循环,所以需要加上m,那么我们来分析一下具体优化是什么,现在我们来操作对比,所以不能省略:
之前的是n *m-m^2+m,n代表主串长度,m代表字串长度
现在的是:n-m+1+m,即n-m+1+m,n代表主串长度,m代表字串长度,这里只考虑外面的了
我们可以发现,如果m是1,那么之前的是n,现在的是n+1,m是n时,之前的是n,现在的是n+1,即之前的好,如果m>1或者m<n,那么现在的好,所以总体来说,现在的比之前的要好很多,特别是m越来越大的情况,我们看上面的时间复杂度,所以也可以得出是O(n+m),比O(n*m),要好,极端情况下要差,比如m=1或者m=n(因为我计算了对应的哈希值,且他也是直接的对比),其他情况,我一次计算哈希值,顶掉其他的所有次数,当然,由于是看整体,且基本m不会等于1或者n,所以这里整体要好
应用:
适用于匹配串类型不多的情况,比如:字母、数字或字母加数字的组合,以及"大小写字母+数字"(比如前面说明的62进制方式的操作),因为其他的类型,可能会导致计算结果过大,并且没有明确的类似的表ASCII,就算有,因为数字很大(对应的标识,就如a是97,特别是汉字,比如汉字"哈"就是21644,有点大了),也不建议使用
BM 算法:
BF 算法性能会退化的比较严重,而 RK 算法需要用到哈希算法,而设计一个可以应对各种类型字符的哈希算法并不简单,比如上面说明的汉字,在操作后续的计算时,可能计算的结果会非常大,不好保存和计算,就算使用BigInteger来保存,但是因为计算量太大了,所以会导致比较慢,所以这种情况下,并不友好
那么现在我们来操作另外一种算法:
BM(Boyer-Moore)算法,它是一种非常高效的字符串匹配算法,滑动算法
在这个例子里,主串中的 c,在模式串中是不存在的,所以,模式串向后滑动的时候,只要 c 与模式串有重合,肯定无法匹配
所以,我们可以一次性把模式串往后多滑动几位,把模式串移动到 c 的后面
BM 算法,本质上其实就是在寻找这种规律,借助这种规律,在模式串与主串匹配的过程中,当模式串和主串某个字符不匹配的时候,能够跳过一些肯定不会匹配的情况,将模式串往后多滑动几位,从而减少次数
算法原理:
BM 算法包含两部分,分别是坏字符规则(bad character rule)和好后缀规则(good suffix shift)
坏字符规则:
BM 算法的匹配顺序比较特别,它是按照模式串下标从大到小的顺序,倒着匹配的,那么可不可以正着匹配,答:最好不可以,后面会说明情况,当然,这里代表的不是下标,而是匹配的顺序,不要以为是下标,如下图:
我们从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候,我们把这个没有匹配的字符叫作坏字符(主串中的字符),如下图:
字符 c 与模式串中的任何字符都不可能匹配,这个时候,我们可以将模式串直接往后滑动三位,将模式串滑动到 c 后面的位置,再从模式串的末尾字符开始比较,如下图:
坏字符 a (是对应主串的a哦)在模式串中是存在的,模式串中下标是 0 的位置也是字符 a,这种情况下,我们可以将模式串往后滑动两位,让两个 a 上下对齐,然后再从模式串的末尾字符开始,重新匹配,如下图:
注意:上面的a在模式串中是第一个a,也就是说,如果是aabd,那么就是第二个a,因为他的acabd可能是aaabd,那么这样才能保证是可以匹配的,那么从这里可以发现,如果我们是正向的操作,你必须循环模式串(一般是坏字符前面的,在程序里会操作说明的),因为你不能保证该模式串的后面是否还有a,所以我们需要反向的进行操作,主要是为了,刚好找到第一个a即可确定下标来移动,实际上坏字符也是这样,可以使得第一个就是坏字符,而不用全部进行匹配,总而言之,我们使用反方向比正方向要方便许多
当发生不匹配的时候(出现坏字符,那么就是不匹配,这里可没有直接的哈希对比和直接对比,参考前面两个操作即可,一个使用哈希的就是哈希对比,没有使用的就是直接对比,后面会说明为什么不使用哈希对比),我们把坏字符对应的模式串中的字符下标记作 si,如果坏字符在模式串中存在, 我们把这个坏字符在模式串中的下标记作 xi,如果不存在,我们把 xi 记作 -1(代表直接的过去),否则减去xi,代表他们需要移动多少距离,使得刚好对上,那么模式串往后移动的位数就等于 si-xi(下标,都是字符在模式串的下标),如下图:
举例:
第一次
c在模式串中不存在,由于c在模式串中是第二个下标,所以si=2,因为不存在,所以xi=-1,那么结果就是2-(-1)=3,移动3位
所以第一次移动3位
第二次:
a在模式串中存在,所以 xi=0,移动位数是2-0=2,即移动2位
所以第二次移动2位
好后缀规则:
如下图:
从上图看出,坏字符后面还有已经匹配的,我们把已经匹配的在模式串中查找,如果找到了另一个跟主串{u}(主串或者字串的部分串,简称部分串)相匹配的子串{u},那我们就将模式串滑动到子串{u}与主串中{u}对齐的位置,如下图:
很明显,如果只看坏字符的话,他只会移动一次,而这里直接的跳过坏字符移动了,因为既然出现了坏字符,那么若没有找到对应的{u},即前面实际上也不要进行判断匹配的,所以直接的忽略坏字符
如果在模式串中找不到另一个等于主串{u}的子串,且这时不匹配,我们就直接将模式串,滑动到主串中{u}的后面,因为之前的任何一次往后滑动,都会没有匹配主串中{u}的情况,也就必然不会成功,如下图:
过度滑动情况,如下图:
因为上面的主串{u}是两个,那么当只需要一个时,是不用继续滑动的,所以如果还是直接的在后面,那么会产生过度的滑动,所以这时不应该滑动了
简单来说:
当模式串滑动到自身前缀(前面的模式串的字串,一般是主串{u}大小减1,就是前缀的大小的范围,这里只有1,所以前缀是1,否则如果有2的话,那么该前缀可以是2,或者1)与主串中{u}的后缀(只有1,如果有2的话,可以是2或者1)有部分重合的时候,并且重合的部分相等的时候,就有可能会存在完全匹配的情况
因为之前的都是匹配两个,可没有匹配一个的,所以这里需要注意
所以,针对这种情况,我们不仅要看好主串{u}在模式串中,是否有另一个匹配的子串{u},我们还要考察好主串{u}的后缀子串(c),是否存在跟模式串的前缀子串(c)匹配
如何选择坏字符和好后缀:
我们可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数,比如前面的图片中,坏字符只需要移动1为,而好后缀却需要移动3位,所以我们以好后缀为主
因为他们都是一种操作,只要有一个不会匹配,必然是移动最大的,因为就算移动最小的,必然也会匹配失败,因为没有满足另外一个大的移动,而我们操作大的移动,自然会使得更少的操作执行次数
算法实现:
这里先主要说明坏字符:
如果我们拿坏字符,在模式串中顺序遍历查找,这样就会比较低效,在这之前,那么有个问题,坏字符他本身的移动次数对比之前的移动(哈希对比)来说会不会提高总次数吗,答,实际上会
那么又有一个问题,坏字符可以使用哈希对比吗,答:最好不要,因为我们知道,坏字符会进行匹配模式串,也就是说,他需要进行循环操作对比,所以使用哈希对比没有什么作用
现在举个例子:
假设你的主串是abcdaba,模式串是aba,首先我们匹配一开始是不成功的,如果不使用坏字符,需要1次(哈希值),不考虑外面的3次(哈希方法),那么需要移动4次(匹配后移动,所以是包含的,记得结合程序来看),才可以匹配到,然后1次匹配成功,总共4+1=5次(认为对应的哈希是1次的,否则就是3+5 * 3=18次了,不考虑外面的就是15次,而不使用哈希的那么就是7-3+1=5,5*3=15次),而使用坏字符,那么他就不会像我们移动并慢慢的都进行匹配,他先从后面开始匹配,如果直接不成功,那么前面的也就不需要匹配了,所以很明显,他减少了次数,那么他一开始就是1次,但是需要看看是否有相同的,那么也会浪费3次(包括该一次),即总共3次(注意我们说明的都是循环的次数,循环的操作有多个,我们统称为1次,循环里面的循环的次数我们也算),当他直接到dab时,由于最后也是不相同,浪费1次,但是他模式串最后一个是b,在模式串中是可以找到的,那么就会操作2次(包含该1次),然后移动,最后1次,进行3次匹配,使得匹配成功(不是哈希),总共有3+2+3=8次,自然比5次需要更多次数,所以实际上会提高总次数,但是如果按照程序来算的话,8是远远的小于18和15的,所以坏字符大大提高了执行效率(因为我们会根据理论来进行跳跃,而他们却傻傻的移动一步,使得虽然相同的次数,但是没有跳跃的都操作了,使得高两个3,一个2,这里更加的有好处),但是对于哈希算法是O(1)来说是降低的,以后以哈希算法是O(1)这个为主
这里就回到了上面的说明"如果我们拿坏字符,在模式串中顺序遍历查找,这样就会比较低效",这就是原因,即这里我们需要解决对应的遍历查找问题,因为这里占用了3+2=5次的遍历,而哈希值,是直接的匹配成功,我们仔细的观察,实际上他的遍历在某些情况下会多一次,也就是2次的那个地方,因为他的移动需要找到是否相同的来决定,而没有不相同的与单纯的移动是一样的(对于哈希是O(1)来说),所以整体来说,遍历的次数必然大于等于单纯的移动,但是该2次的地方实际上只会增加一次,也就是说,只有一次只差,那么如何解决呢:
我们可以采用散列表,我们可以用一个256数组,来记录每个字符在模式串中的位置,数组下标可以直接对应字符的ASCII码值,数组的值为字符在模式串中的位置,没有的记为-1
也就是说,用他们本身数字的哈希值来操作下标,那么如果主串对应的坏字符在数组里有,只需要去数组里找即可,若返回了对应下标的值(该值是该字符在模式串的下标),那么就说明找到了,而没有返回自然就是-1(-1使得可以直接的移动模式串长度,所以默认为-1),我们简称他为哈希对标,或者哈希对比下标
比如bc[97]=a(也就是3),bc[98]=b(也就是1),bc[100]=d(也就是2),很明显,可以通过坏字符来找到对应的模式串的下标,当然有重复的字母以后面的位置为准(这里是有问题的,在后面会进行解决,程序里会进行说明),即当我们保存时,从第一个保存,那么最后面的自然会操作覆盖,所以也就是使得找到其最后一个,那么你可能会认为256长度的数组,难道不会导致空间过大吗,答:这种想法是正确的,但是你要知道,字符是非常多的,如果不设置非常大的数组,那么可能有些字符保存不了,即出现越界,特别是汉字,当然,汉字我们一般不会操作,所以总体来说,256刚刚好了,当然这里只是测试,你完全可以自己设置少点的空间
那么有个问题,使用这个,可不可以使用哈希对比(前面的哈希操作,是自定义的那个地方)来直接匹配了,答:与前面说的"答:最好不要"不一样,前面由于我们需要利用自身来移动(与自身匹配,来得到坏字符),那么自然不会只移动一位,所以不会有多余的操作(哈希值),并且,我们也知道,实际上这样的判断是可以等于单纯的移动的,只是有些情况会大于,如前面的2次,那么如果操作了这里,很明显,都只需要一次即可,即前面的总次数就是1+1+3=5次了,即比原来的5次相同了(注意:是在好的案例的情况下,特殊的案例在程序里会说明,如果包含特殊案例,我们可以认为是等价的),当然,如果主串或者模式串很长,那么我坏字符自然要比原来的匹配要好,因为我一次就能移动很多次,如果是刚好匹配,那么他不会出现坏字符,那么这些操作也就没有作用,根据这里来说,因为我们可以一次性的就能知道是否存在,所以到那时,可以来操作哈希,即加上哈希,那么每次之前都会进行哈希操作,默认为O(1),这样就能看看是否先匹配了
但是我们不会这样,虽然与前面的不一样,但也只是判断是否存在不一样而已,实际上都需要判断是否有坏字符,那么在这种情况下,必然是需要循环的,所以这个循环可以利用,如果加上了哈希,如果该最后一次循环的次数比总体的哈希次数要少,最好直接的不加上哈希,否则可以加,但是主串越长的情况下,使用哈希越不好,因为其他的次数都需要哈希,如果模式串很长的话,使用哈希好点,因为不用判断是否有坏字符了,所以我们可以认为他们是平等的,但是使用哈希需要代码量,那么使用哈希要差点,所以就不使用哈希了(即哈希对比)
现在我们先来完成坏字符,然后在坏字符中进行修改,使得加上好后缀,并且,这里我会给出如果使用正方向的话,对应的缺点的地方是什么(也就是在那个位置给出解释),这里你可以先看一下然后自己编写试一试:
package com. lagou ;
public class test44 {
public static void generateBC ( String b, int [ ] dc) {
for ( int i = 0 ; i < dc. length; i++ ) {
dc[ i] = - 1 ;
}
for ( int i = 0 ; i < b. length ( ) ; i++ ) {
dc[ b. charAt ( i) ] = i;
}
}
public static void bad ( String a, String b) {
int [ ] array = new int [ 256 ] ;
generateBC ( b, array) ;
for ( int i = 0 ; i < a. length ( ) - b. length ( ) + 1 ; ) {
int j = 0 ;
for ( j = b. length ( ) - 1 ; j >= 0 ; j-- ) {
if ( a. charAt ( j + i) != b. charAt ( j) ) {
break ;
}
}
if ( j < 0 ) {
System . out. println ( "匹配成功,下标是:" + i) ;
return ;
}
int ii = 0 ;
if ( j != b. length ( ) - 1 ) {
char c = a. charAt ( j + i) ;
for ( ii = j - 1 ; ii >= 0 ; ii-- ) {
if ( b. charAt ( ii) == c) {
break ;
}
}
}
if ( j == b. length ( ) - 1 ) {
i += j - array[ a. charAt ( j + i) ] ;
} else {
i += j - ii;
}
}
System . out. println ( "没有找到" ) ;
}
public static void generateBC11 ( int [ ] dc) {
for ( int i = 0 ; i < dc. length; i++ ) {
dc[ i] = - 1 ;
}
}
public static void generateBC12 ( String b, int [ ] dc, char v) {
for ( int i = b. length ( ) - 1 ; i >= 0 ; i-- ) {
if ( b. charAt ( i) == v) {
dc[ v] = i;
break ;
}
}
}
public static void badd ( String a, String b) {
int [ ] array = new int [ 256 ] ;
generateBC11 ( array) ;
for ( int i = 0 ; i < a. length ( ) - b. length ( ) + 1 ; ) {
int j = 0 ;
for ( j = b. length ( ) - 1 ; j >= 0 ; j-- ) {
if ( a. charAt ( j + i) != b. charAt ( j) ) {
break ;
}
}
if ( j < 0 ) {
System . out. println ( "匹配成功,下标是:" + i) ;
return ;
}
String c = b. substring ( 0 , j) ;
generateBC12 ( c, array, a. charAt ( j + i) ) ;
i += j - array[ a. charAt ( j + i) ] ;
if ( i + j <= a. length ( ) - 1 ) {
array[ a. charAt ( j + i) ] = - 1 ;
}
}
System . out. println ( "没有找到" ) ;
}
public static void bad1 ( String a, String b) {
int [ ] array = new int [ 256 ] ;
generateBC ( b, array) ;
for ( int i = 0 ; i < a. length ( ) - b. length ( ) + 1 ; ) {
int j = 0 ;
for ( j = b. length ( ) - 1 ; j >= 0 ; j-- ) {
if ( a. charAt ( j + i) != b. charAt ( j) ) {
break ;
}
}
if ( j < 0 ) {
System . out. println ( "匹配成功,下标是:" + i) ;
return ;
}
int ii = 0 ;
if ( j != b. length ( ) - 1 ) {
char c = a. charAt ( j + i) ;
for ( ii = j - 1 ; ii >= 0 ; ii-- ) {
if ( b. charAt ( ii) == c) {
break ;
}
}
}
int aa;
boolean is = true ;
if ( j == b. length ( ) - 1 ) {
aa = j - array[ a. charAt ( j + i) ] ;
is = false ;
} else {
aa = j - ii;
}
int bb = 0 ;
if ( is == true ) {
int gg = b. length ( ) ;
for ( int kk = j - ( b. length ( ) - 1 - j - 1 ) ; kk >= 0 ; kk-- ) {
if ( b. charAt ( kk) == b. charAt ( j + 1 ) ) {
int oo = 0 ;
for ( int pp = 1 ; pp < b. length ( ) - 1 - j - 1 ; pp++ ) {
if ( b. charAt ( pp) != b. charAt ( j + 1 + pp) ) {
oo = 1 ;
break ;
}
}
if ( oo == 0 ) {
gg = kk;
}
}
}
int p = gg;
if ( gg == b. length ( ) ) {
int tt = 0 ;
for ( int ll = b. length ( ) - 1 - j - 1 , jj = 1 ; ll > 0 ; ll-- , jj++ ) {
if ( b. charAt ( 0 ) == b. charAt ( j + 1 + ll) ) {
tt = jj;
}
}
gg -= tt;
}
if ( p == b. length ( ) ) {
bb = gg;
} else {
bb = j + 1 - gg;
}
}
if ( aa >= bb) {
i += aa;
} else {
i += bb;
}
}
System . out. println ( "没有找到" ) ;
}
public static void bad2 ( String a, String b) {
int [ ] array = new int [ 256 ] ;
generateBC ( b, array) ;
for ( int i = 0 ; i < a. length ( ) - b. length ( ) + 1 ; ) {
int j = 0 ;
for ( j = b. length ( ) - 1 ; j >= 0 ; j-- ) {
if ( a. charAt ( j + i) != b. charAt ( j) ) {
break ;
}
}
if ( j < 0 ) {
System . out. println ( "匹配成功,下标是:" + i) ;
return ;
}
int ii = 0 ;
if ( j != b. length ( ) - 1 ) {
char c = a. charAt ( j + i) ;
for ( int iii = 0 ; iii <= j - 1 ; iii++ ) {
if ( b. charAt ( iii) == c) {
ii = iii;
}
}
if ( j == 0 ) {
ii = - 1 ;
}
}
if ( j == b. length ( ) - 1 ) {
i += j - array[ a. charAt ( j + i) ] ;
} else {
i += j - ii;
}
}
System . out. println ( "没有找到" ) ;
}
public static void bad3 ( String a, String b) {
int [ ] array = new int [ 256 ] ;
generateBC ( b, array) ;
for ( int i = 0 ; i < a. length ( ) - b. length ( ) + 1 ; ) {
int j = 0 ;
for ( j = b. length ( ) - 1 ; j >= 0 ; j-- ) {
if ( a. charAt ( j + i) != b. charAt ( j) ) {
break ;
}
}
if ( j < 0 ) {
System . out. println ( "匹配成功,下标是:" + i) ;
return ;
}
boolean is = true ;
if ( j == b. length ( ) - 1 ) {
is = false ;
}
int bb = 0 ;
if ( is == true ) {
int gg = b. length ( ) ;
for ( int kk = j - ( b. length ( ) - 1 - j - 1 ) ; kk >= 0 ; kk-- ) {
if ( b. charAt ( kk) == b. charAt ( j + 1 ) ) {
int oo = 0 ;
for ( int pp = 1 ; pp < b. length ( ) - 1 - j - 1 ; pp++ ) {
if ( b. charAt ( pp) != b. charAt ( j + 1 + pp) ) {
oo = 1 ;
break ;
}
}
if ( oo == 0 ) {
gg = kk;
}
}
}
int p = gg;
if ( gg == b. length ( ) ) {
int tt = 0 ;
for ( int ll = b. length ( ) - 1 - j - 1 , jj = 1 ; ll > 0 ; ll-- , jj++ ) {
if ( b. charAt ( 0 ) == b. charAt ( j + 1 + ll) ) {
tt = jj;
}
}
gg -= tt;
}
if ( p == b. length ( ) ) {
bb = gg;
} else {
bb = j + 1 - gg;
}
}
i += bb;
}
System . out. println ( "没有找到" ) ;
}
public static void main ( String [ ] args) {
String a = "dffvff" ;
String b = "dvf" ;
System . out. println ( "下标从0开始的哦" ) ;
bad ( a, b) ;
badd ( a, b) ;
bad1 ( a, b) ;
bad2 ( a, b) ;
bad3 ( a, b) ;
}
}
至此,对应的BM算法操作完成,这里我们以最好的时间复杂度O(n/m)(虽然平均起来是O(n+m),当然是说明结合的,否则坏字符可以认为是n,好后缀,也可以是n,好后缀是认为,一般情况下,可能要多点次数)来进行表示,n是主串,m是模式串
应用:
BM算法比较高效,在实际开发中,特别是一些文本编辑器中,用于实现查找字符串功能(特别是不考虑哈希对比为O(1)的情况,那么相对来说是大大提高的)
Trie 树:
Trie 树,也叫"字典树",它是一个树形结构,它是一种专门处理字符串匹配的数据结构,用来解决在一 组字符串集合中快速查找某个字符串的问题
比如:有 6 个字符串,它们分别是:how,hi,her,hello,so,see,我们可以将这六个字符串组成Trie树结构
Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起
其中,根节点不包含任何信息(即上面的表示目录"/"),每个节点表示一个字符串中的字符,从根节点到红色节点的一条路径表 示一个字符串(红色节点为叶子节点)
Trie树的插入:
Trie树的查找:
当我们在 Trie 树中查找一个字符串的时候,比如查找字符串"her",那我们将要查找的字符串分割成单个的字符 h,e,r,然后从 Trie 树的根节点开始匹配,如图所示,蓝色的路径就是在 Trie 树中匹配的 路径
Trie 树是一个多叉树(即多路树或者多路查找树):
虽然是多叉树,但是该叉树的变量基本没有上限,也就是说,单纯的利用链表变量是不行的,我们需要创建对应的类数组来保存其总变量
我们通过一个下标与字符依次映射的数组,来存储子节点的指针
假设我们的字符串中只有从 a 到 z 这 26 个小写字母,我们在数组中下标为 0 的位置,存储指向子节点a 的指针,下标为 1 的位置存储指向子节点 b 的指针,以此类推,下标为 25 的位置,存储的是指向的 子节点 z 的指针
如果某个字符的子节点不存在,我们就在对应的下标的位置存储 null
那么如果保存节点呢,一般是如下:
public class TrieNode {
public char data;
public TrieNode [ ] children = new TrieNode [ 26 ] ;
public boolean isEndingChar = false ;
public TrieNode ( char data) {
this . data = data;
}
}
当我们在 Trie 树中查找字符串的时候,我们就可以通过字符的 ASCII 码减去"a"的 ASCII 码(即当前的保存的字符),迅速找到 匹配的子节点的指针,并使得满足上面数组的长度26
比如,d 的 ASCII 码减去 a 的 ASCII 码就是 3,那子节点 d 的指针就存储在数组 中下标为 3 的位置中
现在我们来进行编写,可以先看完后,自己编写一个:
package com. lagou ;
public class test55 {
public static class TrieNode {
public char data;
public TrieNode [ ] children = new TrieNode [ 26 ] ;
public boolean isEndingChar = false ;
public TrieNode ( char data) {
this . data = data;
}
}
public static TrieNode root = new TrieNode ( '/' ) ;
public static void insert ( String a) {
TrieNode node = root;
for ( int i = 0 ; i < a. length ( ) ; i++ ) {
char c = a. charAt ( i) ;
int i1 = c - 97 ;
if ( node. children[ i1] == null ) {
node. children[ i1] = new TrieNode ( c) ;
}
node = node. children[ i1] ;
}
node. isEndingChar = true ;
}
public static void select ( String a) {
TrieNode node = root;
for ( int i = 0 ; i < a. length ( ) ; i++ ) {
char c = a. charAt ( i) ;
int i1 = c - 97 ;
if ( node. children[ i1] == null ) {
System . out. println ( "没有该字符串" ) ;
return ;
}
node = node. children[ i1] ;
}
if ( node. isEndingChar == true ) {
System . out. println ( "有该字符串" ) ;
} else {
System . out. println ( "没有该字符串" ) ;
}
}
public static void main ( String [ ] args) {
insert ( "hello" ) ;
insert ( "her" ) ;
insert ( "hi" ) ;
insert ( "how" ) ;
insert ( "see" ) ;
insert ( "so" ) ;
select ( "how" ) ;
}
}
时间复杂度:
如果要在一组字符串中,频繁地查询某些字符串,用 Trie 树会非常高效,构建 Trie 树的过程,需要扫描所有的字符串,时间复杂度是 O(n)(n 表示所有要添加的字符串的长度和),但是一旦构建成功之后,后续的查询操作会非常高效,每次查询时,如果要查询的字符串长度是 k,那我们只需要比对大约 k 个节点, 就能完成查询操作,跟原本那组字符串的长度和个数没有任何关系,所以说,构建好 Trie 树后,在其中 查找字符串的时间复杂度是 O(k),k 表示要查找的字符串的长度,即不要考虑类似于主串的长度了
所以说,如果主串特别长,那么最好使用该Trie树,那么比之前的BM算法(以坏字符为例),要高效很多,否则还是使用坏字符,因为他是可以一次就能操作很多次的,而主串少,那么他可能只需要一次就能解决,比如主串是adfgsj,模式串是adfgsh,很明显,一次就能打印"没有找到",而这里却需要先创建主串的数组,然后操作模式串,所以加起来是12次(不考虑主串的创建,也是6次),即,在主串非常少时,使用BM算法(因为他们的移动是看主串的),否则使用Trie树,实际上BM算法平均起来可以认为是哈希对比(默认哈希算法为1),那么他的结果就是n-m+1,即主串越大,次数多,但是实际上他也与模式串有关系,模式串越小,次数也越多(所以这里进行补充,即主串和模式串相对相同的长度使用BM),虽然Trie树需要先创建主串,然后操作模式串,总共加起来就是n+m的次数,但是主串只需要一次创建,也就是说,他可以被其他人利用,所以我们认为他是O(1)(一般可以被其他人利用的数据,我们统称为O(1)),所以这里的Trie树,就是O(m),m是模式串长度,那么相对来说Trie树比较稳定,虽然在上面的n-m+1中,m=n时的1次要快,但胜在稳定,所以无论是BM还是Trie都可以使用,如果需要有时候快点,那么使用BM(一般需要在主串和模式串相对相同的长度),如果想总体快点,那么使用Trie,即实际上不考虑主串的创建,我们最好使用Trie,因为大多数的时候Trie比BM算法要快(除了对应的极端情况,或者某些n与m相差m-1的情况,即m<=n<=2m-1的情况,一般n是大于等于m的,否则自然也是"没有找到",直接跳过了,而Trie却还需要循环,所以实际上是0<=n<=2m-1,空字符就是0,即长度为0,自然也直接退出),当然如果考虑主串的创建的话,那么任何时候(情况)都是比BM算法要慢的
典型应用:
利用 Trie 树,实现搜索关键词的提示功能
我们假设关键词库由用户的热门搜索关键词组成,我们将这个词库构建成一个 Trie 树,既然有创建好了,那么Trie自然比BM算法总体要快(因为这里的主串和模式串相对不相同的长度,即基本没有上面说明的对应情况)
当用户输入其中某个单词的时候,把这个词作为一个前缀子串在 Trie 树中匹配,为了讲解方便,我们假设词库里只有
hello、her、hi、how、so、see 这 6 个关键词
当用户输入了字母 h 的时候,我们就把以 h 为前缀的hello、her、hi、how 展示在搜索提示框内
当用户继续键入字母 e 的时候,我们就把以 he 为前缀的hello、her 展示在搜索提示框内,这就是搜索关键词提示的最基本的算法原理
这里要注意,某些提示下,可能会分词,也就是说,he,可以分成he,h,e,所以he有时候也会出现4个(对这里来说),看如下图中绿色的即可:
所以虽然上面说的he是两个,但是若有分词的话,可能就是4个了
图:
图的概念
图(Graph),是一种复杂的非线性表结构,之所以是复杂的,是因为一个节点可以有不同的子节点,所以与单纯的二叉树之类的数据结构是不同的,因为他们的节点个数基本固定,所以说是复杂的
图中的元素我们就叫做顶点(vertex)
图中的一个顶点可以与任意其他顶点建立连接关系
我们把这种建立的关系叫做边(edge),跟顶点相连接的边的条数叫做度(degree)
图这种结构有很广泛的应用,比如社交网络,电子地图,多对多的关系就可以用图来表示
边有方向的图叫做有向图,比如A点到B点的直线距离,微信的添加好友是双向的
边无方向的图叫无向图,比如网络拓扑图
带权图(weighted graph),在带权图中,每条边都有一个权重(weight),我们可以通过这个权重来表示 一些可度量的值
图的存储:
图最直观的一种存储方法就是,邻接矩阵(Adjacency Matrix)
邻接矩阵的底层是一个二维数组
无向图:如果顶点 i 与顶点 j 之间有边,我们就将 A[ i ] [ j ]和 A[ j ] [ i ]标记为 1
有向图:
如果顶点 i 到顶点 j 之间,有一条箭头从顶点 i 指向顶点 j 的边,那我们就将 A[i] [j]标记为 1
同理,如果有一条箭头从顶点 j 指向顶点 i 的边,我们就将 A[j] [i]标记为 1
当然,上面的2和3是双向的(双向的权重可以不同),但各有对应的值,实际上有值也可以称为指向谁(有值才有指向的,比如1指向2,那么1中包含2这个值(二维数组)
当然,虽然是双向的,但是又何尝不是3对2的单向以及2对3的单向呢,实际上我们可以说,无向图,都是双向的
带权图:
数组中就存储相应的权重
很明显,之前的默认都是1(0代表没有边,或者就没有权的变量(针对类来说)),而带权图可以提高上限,一般权高的代表这条路优先操作,虽然这里只是给出说明而已,这里没有具体操作
我们通过上面的说明(基本是邻接矩阵),来写一个小操作,可以先看一遍,然后自己手动编写:
package com. lagou ;
import java. util. ArrayList ;
import java. util. List ;
public class test66 {
public List list;
int [ ] [ ] i;
int du;
public test66 ( int n) {
i = new int [ n] [ n] ;
du = 0 ;
list = new ArrayList < > ( n) ;
}
public void insertNode ( Object node) {
list. add ( node) ;
}
public void insertEdge ( int a, int b, int c) {
i[ a] [ b] = c;
du++ ;
}
public int getWeight ( int a, int b) {
return i[ a] [ b] ;
}
public Object getNode ( int i) {
return list. get ( i) ;
}
public int getEage ( ) {
return du;
}
public int getNodeAll ( ) {
return list. size ( ) ;
}
public static void main ( String [ ] args) {
int a = 4 ;
String [ ] s = { "v1" , "v2" , "v3" , "v4" } ;
test66 t = new test66 ( a) ;
for ( String c : s) {
t. insertNode ( c) ;
}
t. insertEdge ( 0 , 1 , 2 ) ;
t. insertEdge ( 0 , 2 , 5 ) ;
t. insertEdge ( 2 , 3 , 8 ) ;
t. insertEdge ( 3 , 0 , 7 ) ;
System . out. println ( "结点个数是:" + t. getNodeAll ( ) ) ;
System . out. println ( "边的个数是:" + t. getEage ( ) ) ;
}
}
当然,上面我们可以看到,他只是给出逻辑上的指向,所以只要该二维数组的值是对应的图即可,即手动的设置是否是有向图,还是无向图,还是带权图(这里一般结合前面的,使得不默认为1),所以这里看你如何的添加了(这里就是有向图),当然,这里并没有给出什么限制条件,因为我们只操作主要的,你可以自己进行操作限制,或者修改某些细节,比如添加相同的边,总边数也会增加,我们只需要设置判断是否有权值即可,没有权值,那么增加总边数,否则使得总边数不增加,并覆盖对应的权值(修改成自身的再次的设置的权重)等等,这样的思路也是可以的
邻接表:
用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间
特别是对于无向图来说,如果 A[i] [j]等于 1,那 A[j] [i]也肯定等于 1
而对于他(无向图),实际上,我们只需要存储一个就可以了,也就是说,无向图的二维数组中,如果我们将其用对角线划分为上下两部分,那我们只需要利用上面或者下面这样一半的空间就足够了,另外一半白白浪费掉了 (使得节省空间)
还有,如果我们存储的是稀疏图(Sparse Matrix),也就是说,顶点很多,但每个顶点的边并不多, 那邻接矩阵的存储方法就更加浪费空间了(因为对应的值都是默认值,并没有使用到,在前面的例子中,可以发现,有很多个都是0),比如微信有好几亿的用户,对应到图上就是好几亿的顶点,但是每个用户的好友并不会很多,一般也就三五百个而已,如果我们用邻接矩阵来存储,那绝大部分的存储空间都被浪费了(因为没有使用,加上行是100,列是100,但是其中一行只有一个空间利用了,那么其他的空间自然是浪费的),所以总体而言,无论是无向图还是有向图,在边少的情况下,会浪费空间(虽然无向图,可以节省一半),其中带权图,只是使得权能够突破1而已,所以他是附加在无向图或者有向图的,所以不要理会
针对上面邻接矩阵比较浪费内存空间的问题,我们来看另外一种图的存储方法,邻接表(Adjacency List)
每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点
图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点
在前面的二维数组中,存储的是对于的指向的权重,而集合存储的是所有的顶点,并且该顶点保存的是其本身的值,现在我们将每一个顶点看成一个类,然后该类是一个链表节点,后面连接的块(节点),代表前面顶点所指向的顶点,里面也包括了该路线的权值
如果该点还指向其他顶点,则继续在块后面添加,例如A指向了B,权值是4,那么A后面就加上一块,之后发现A还指向D权值是5,那么就在块尾继续添加一块,其实也就是数组+链表的结构,但是好像他也只是一个指向而已,并不是多条线路,所以从上到下的添加边时,若先添加的是D,那么在后面的打印中,可能是D先出现,你交换位置就知道了(在后面会说明),即他们是一个一个的打印的,而不会将其指向的顶点的指向的顶点进行打印(因为只打印边),即只操作第一级别,当然,这是因为其他的顶点由其他数组下标的值来进行保存的,所以这里是分开的保存,当然,二维数组基本也是,但是总体结合起来就是图了
根据邻接表的结构和图,我们不难发现,图其实是由顶点和边组成的 ,所以我们就抽象出两种类,一个 是Vertex顶点类,一个是Edge边类,注意:这里是将开始的节点称为顶点,顶点指向的顶点称为边类(这里要注意,之所以是边类,在程序里面会说明)
具体代码如下,可以先看一遍,然后自己编写:
package com. lagou ;
import java. util. HashMap ;
import java. util. Iterator ;
import java. util. Map ;
import java. util. Set ;
public class test77 {
public class Vertex {
String name;
Edge next;
public Vertex ( String name, Edge next) {
this . name = name;
this . next = next;
}
}
public class Edge {
String name;
int weight;
Edge next;
public Edge ( String name, int weight, Edge next) {
this . name = name;
this . weight = weight;
this . next = next;
}
}
Map < String , Vertex > map;
public test77 ( ) {
map = new HashMap < > ( ) ;
}
public void insertNode ( String a) {
Vertex aa = new Vertex ( a, null ) ;
map. put ( a, aa) ;
}
public void insertEdge ( String a, String b, int c) {
Vertex vertex = map. get ( a) ;
if ( vertex == null ) {
vertex = new Vertex ( a, null ) ;
map. put ( a, vertex) ;
}
if ( vertex. next == null ) {
vertex. next = new Edge ( b, c, null ) ;
} else {
Edge temp = vertex. next;
while ( true ) {
if ( temp. next == null ) {
temp. next = new Edge ( b, c, null ) ;
return ;
}
temp = temp. next;
}
}
}
public void Select ( ) {
Set < Map. Entry < String , Vertex > > entries = map. entrySet ( ) ;
Iterator < Map. Entry < String , Vertex > > iterator = entries. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
Map. Entry < String , Vertex > entry = iterator. next ( ) ;
Vertex vertex = entry. getValue ( ) ;
Edge edge = vertex. next;
while ( edge != null ) {
System . out. println ( vertex. name + " 指向 " + edge. name + " 权值为:" + edge. weight) ;
edge = edge. next;
}
}
}
public static void main ( String [ ] args) {
test77 graph = new test77 ( ) ;
graph. insertNode ( "A" ) ;
graph. insertNode ( "B" ) ;
graph. insertNode ( "C" ) ;
graph. insertNode ( "D" ) ;
graph. insertNode ( "E" ) ;
graph. insertNode ( "F" ) ;
graph. insertEdge ( "C" , "A" , 1 ) ;
graph. insertEdge ( "F" , "C" , 2 ) ;
graph. insertEdge ( "A" , "B" , 4 ) ;
graph. insertEdge ( "E" , "B" , 2 ) ;
graph. insertEdge ( "A" , "D" , 5 ) ;
graph. insertEdge ( "D" , "F" , 4 ) ;
graph. insertEdge ( "D" , "E" , 3 ) ;
graph. Select( ) ;
}
}
所以通过上面的代码,我们才会说,图是顶点和边来操作的,因为边的指向包含了指向的顶点,并且,这样可以保证数据是一致的
我们也可以发现,实际上被指向的顶点和边是一起的(在一个类里面,并且总体被一个顶点指向)
至此通过前面的说明以及代码的实现,所以我们可以说,这里的数组加链表完全的利用了空间
时间复杂度:
邻接表:访问所有顶点的时间为 O(V)(一个数组),而查找所有顶点的邻居一共需要 O(E)(连接的邻居,自然不会也是数组,所以不会是相乘) 的时间,所以总的时间复杂度是O(V + E),V是顶点数,E是边数(该边数通常比V大,在极端情况下,要比V少一个,比如就一条线)
邻接矩阵:
查找每个顶点的邻居需要 O(V)(二维数组) 的时间,所以查找整个矩阵的时候需要 O(V^2) 的时间,V是顶点数(这里找边数,却要找顶点数,所以是V^2)
图的遍历 :
遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次
当然,前面的代码中,因为先后顺序,所以A顶点信息会先打印出来
前面已经讲过了二叉树的节点遍历(比如广度优先和深度优先(也分为前序,中序,后序))
类似的,图的遍历是指,从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历,遍历过程中得到的顶点序列称为图遍历序列,当前,上面代码中,不是这样的遍历,他只是打印出对应的指向终点而而已
图的遍历过程中,根据搜索方法的不同,又可以划分为两种搜索策略:
深度优先搜索以及广度优先搜索
深度优先搜索(DFS,Depth First Search) :
深度优先搜索,从起点出发,从规定的方向中选择其中一个不断地向前走,直到无法继续为止,然后尝试另外一种方向,直到最后走到终点,就像走迷宫一样,尽量往深处走,这里我们就不会只操作第一级别了,即不会像前面的代码一样,只保证顶点指向的顶点(他操作了分开的保存)
DFS 解决的是连通性的问题,即,给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能从起点连接到终点,起点和终点,也可以指的是某种起始状态和最终的状态,问题的要求并不在乎路径 是长还是短,只在乎有还是没有,所以这里要进行注意
假设我们有这么一个图,里面有A、B、C、D、E、F、G、H,8 个顶点,点和点之间的联系如下图所示, 对这个图进行深度优先的遍历
基本通常依赖栈(Stack),因为其特点是满足这里的后进先出(LIFO)的
第一步,选择一个起始顶点(先不给出终点,这里先来看看他的流程是什么),例如从顶点 A 开始,把 A 压入栈,标记它为访问过(用红色或者黄色标记),并输出到结果中
第二步,寻找与 A 相连并且还没有被访问过的顶点,顶点 A 与 B、D、G 相连,而且它们都还没有被访 问过,我们按照字母顺序处理,所以将 B 压入栈,标记它为访问过,并输出到结果中
第三步,现在我们在顶点 B 上,重复上面的操作,由于 B 与 A、E、F 相连,如果按照字母顺序处理的 话,A 应该是要被访问的,但是 A 已经被访问了,所以我们访问顶点 E,将 E 压入栈,标记它为访问过,并输出到结果中
第四步,从 E 开始,E 与 B、G 相连,但是B刚刚被访问过了,所以下一个被访问的将是G,把G压入 栈,标记它为访问过,并输出到结果中
第五步,现在我们在顶点 G 的位置,由于与 G 相连的顶点都被访问过了,类似于我们走到了一个死胡同,必须尝试其他的路口了
所以我们这里要做的就是简单地将 G 从栈里弹出,表示我们从 G 这里已 经无法继续走下去了,看看能不能从前一个路口找到出路
也就是说,如果发现周围的顶点都被访问了,就把当前的顶点弹出
第六步,现在栈的顶部记录的是顶点 E,我们来看看与 E 相连的顶点中有没有还没被访问到的,发现它们都被访问了(虽然在栈中被弹出,但是访问的标记他还是在的,只是在栈里面没有而已,但是其他已经保存的还是在的),所以把 E 也弹出去
第七步,当前栈的顶点是 B,看看它周围有没有还没被访问的顶点,有,是顶点 F,于是把 F 压入栈, 标记它为访问过,并输出到结果中(下面的F是我加上的,使得图片变的合理)
第八步,当前顶点是 F,与 F 相连并且还未被访问到的点是 C 和 D,按照字母顺序来,下一个被访问的 点是 C,将 C 压入栈,标记为访问过,输出到结果中
第九步,当前顶点为 C,与 C 相连并尚未被访问到的顶点是 H,将 H 压入栈,标记为访问过,输出到结 果中
第十步,当前顶点是 H,由于和它相连的点都被访问过了,将它弹出栈
第十一步,当前顶点是 C,与 C 相连的点都被访问过了,将 C 弹出栈
第十二步,当前顶点是 F,与 F 相连的并且尚未访问的点是 D,将 D 压入栈,输出到结果中,并标记为 访问过
第十三步,当前顶点是 D,与它相连的点都被访问过了,将它弹出栈,以此类推,顶点 F,B,A 的邻居 都被访问过了,将它们依次弹出栈就好了,最后,当栈里已经没有顶点需要处理了,我们的整个遍历结束
即结果就是:A,B,E,G,F,C,H,D
广度优先搜索(BFS,Breadth First Search) :
直观地讲,它其实就是一种"地毯式"层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近 的,依次往外搜索
假设我们有这么一个图,里面有A、B、C、D、E、F、G、H,8 个顶点,点和点之间的联系如下图所示, 对这个图进行深度优先的遍历
依赖队列(Queue),先进先出(FIFO)
一层一层地把与某个点相连的点放入队列中,处理节点的时候正好按照它们进入队列的顺序进行
第一步,选择一个起始顶点(先不给出终点,这里先来看看他的流程是什么),让我们从顶点 A 开始,把 A 压入队列,标记它为访问过(用红色或者黄色标记)
第二步,从队列的头取出顶点 A,打印输出到结果中,同时将与它相连的尚未被访问过的点按照字母大小顺序压入队列,同时把它们都标记为访问过,防止它们被重复地添加到队列中
第三步,从队列的头取出顶点 B,打印输出它,同时将与它相连的尚未被访问过的点(也就是 E 和 F) 压入队列,同时把它们都标记为访问过
第四步,继续从队列的头取出顶点 D,打印输出它,此时我们发现,与 D 相连的顶点 A 和 F 都被标记 访问过了,所以就不要把它们压入队列里
第五步,接下来,队列的头是顶点 G,打印输出它,同样的,G 周围的点都被标记访问过了,我们不做 任何处理
第六步,队列的头是 E,打印输出它,它周围的点也都被标记为访问过了,我们不做任何处理
第七步,接下来轮到顶点 F,打印输出它,将 C 压入队列,并标记 C 为访问过
第八步,将 C 从队列中移出,打印输出它,与它相连的 H 还没被访问到,将 H 压入队列,将它标记为 访问过
第九步,队列里只剩下 H 了,将它移出,打印输出它,发现它的邻居都被访问过了,不做任何事情
第十步,队列为空,表示所有的点都被处理完毕了,程序结束
至此,我们可以发现:
广度优先搜索结果就是:A,B,D,G,E,F,C,H,感觉是总中间往外扩散,所以是广度
深度优先搜索结果就是:A,B,E,G,F,C,H,D,感觉一路找过去,所以是深度
当然他们都是无向图,这里要注意
最短路径问题:
广度优先搜索,一般用来解决最短路径的问题
因为我们知道,前面的操作是无向的,所以可以操作直接的标记,但是在有向的情况下,A只能操作D和G,又因为深度,他只会操作一路找过去,所以并不能确定当前的路径是否是最短的,或者说,很难确定,但是广度,由于是扩散的,所以很容易的知道哪个路径先到终点,那么谁就是最短路径
从上面的图中,可以得出(没有指向的不会操作,之前的都是无向图):
深度优先遍历(搜索)是:A,D,F,C,H,G,E,B
广度优先遍历(搜索)是:A,D,G,F,E,C,B,H
这里,我们就选择,有向图来进行编写代码(作为主要的情况来操作):
我们先编写基础的代码,然后来选择说明深度和广度谁能操作最短路径
可以先看完后,在自行进行编写(以上面的图为例的添加节点):
package com. lagou ;
import java. util. * ;
public class test88 {
public class Vertex {
String name;
Edge next;
boolean b = false ;
public Vertex ( String name, Edge next) {
this . name = name;
this . next = next;
}
}
public class Edge {
String name;
int weight;
Edge next;
public Edge ( String name, int weight, Edge next) {
this . name = name;
this . weight = weight;
this . next = next;
}
}
Map < String , Vertex > map;
public test88 ( ) {
map = new HashMap < > ( ) ;
}
public void insertNode ( String a) {
if ( map. get ( a) == null ) {
Vertex aa = new Vertex ( a, null ) ;
map. put ( a, aa) ;
}
}
public void insertEdge ( String a, String b, int c) {
Vertex vertex = map. get ( a) ;
if ( vertex == null ) {
vertex = new Vertex ( a, null ) ;
map. put ( a, vertex) ;
}
if ( vertex. next == null ) {
vertex. next = new Edge ( b, c, null ) ;
} else {
Edge temp = vertex. next;
Edge temp2 = vertex. next;
while ( true ) {
if ( temp. name == b) {
temp. weight = c;
return ;
}
if ( temp. name. hashCode ( ) > b. hashCode ( ) ) {
if ( temp2 != vertex. next) {
Edge j = new Edge ( b, c, null ) ;
temp2. next = j;
j. next = temp;
} else {
Edge j = new Edge ( b, c, null ) ;
vertex. next = j;
j. next = temp2;
}
return ;
}
if ( temp. next == null ) {
temp. next = new Edge ( b, c, null ) ;
return ;
}
temp2 = temp;
temp = temp. next;
}
}
}
public void Select ( ) {
Set < Map. Entry < String , Vertex > > entries = map. entrySet ( ) ;
Iterator < Map. Entry < String , Vertex > > iterator = entries. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
Map. Entry < String , Vertex > entry = iterator. next ( ) ;
Vertex vertex = entry. getValue ( ) ;
Edge edge = vertex. next;
while ( edge != null ) {
System . out. println ( vertex. name + " 指向 " + edge. name + " 权值为:" + edge. weight) ;
edge = edge. next;
}
}
}
public void Select1 ( String a) {
Set < String > strings = map. keySet ( ) ;
Iterator < String > iterator = strings. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
String next = iterator. next ( ) ;
map. get ( next) . b = false ;
}
String [ ] array = new String [ map. size ( ) ] ;
int i = 0 ;
Vertex vertex = map. get ( a) ;
array[ i] = vertex. name;
i++ ;
Stack < Object > stack = new Stack < > ( ) ;
stack. push ( vertex. name) ;
vertex. b = true ;
while ( true ) {
String na = null ;
Edge edge = vertex. next;
if ( edge != null ) {
while ( edge != null ) {
if ( map. get ( edge. name) . b == false ) {
na = edge. name;
break ;
}
edge = edge. next;
}
if ( na != null ) {
stack. push ( na) ;
array[ i] = na;
i++ ;
map. get ( na) . b = true ;
} else {
stack. pop ( ) ;
}
} else {
stack. pop ( ) ;
}
if ( stack. isEmpty ( ) ) {
break ;
}
String peek = ( String ) stack. peek ( ) ;
vertex = map. get ( peek) ;
}
System . out. print ( "[" ) ;
for ( int ii = 0 ; ii < array. length; ii++ ) {
if ( ii == array. length - 1 ) {
System . out. print ( array[ ii] ) ;
} else {
System . out. print ( array[ ii] + "," ) ;
}
}
System . out. println ( "]" ) ;
}
public void Select2 ( String a) {
Set < String > strings = map. keySet ( ) ;
Iterator < String > iterator = strings. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
String next = iterator. next ( ) ;
map. get ( next) . b = false ;
}
String [ ] array = new String [ map. size ( ) ] ;
int i = 0 ;
Vertex vertex = map. get ( a) ;
Queue < String > queue = new LinkedList < > ( ) ;
queue. offer ( vertex. name) ;
vertex. b = true ;
String poll = queue. poll ( ) ;
array[ i] = poll;
i++ ;
while ( true ) {
Edge edge = vertex. next;
if ( edge != null ) {
while ( edge != null ) {
if ( map. get ( edge. name) . b == false ) {
queue. offer ( edge. name) ;
map. get ( edge. name) . b = true ;
}
edge = edge. next;
}
}
if ( queue. isEmpty ( ) ) {
break ;
}
String poll1 = queue. poll ( ) ;
array[ i] = poll1;
i++ ;
vertex = map. get ( poll1) ;
edge = vertex. next;
}
System . out. print ( "[" ) ;
for ( int ii = 0 ; ii < array. length; ii++ ) {
if ( ii == array. length - 1 ) {
System . out. print ( array[ ii] ) ;
} else {
System . out. print ( array[ ii] + "," ) ;
}
}
System . out. println ( "]" ) ;
}
public static void main ( String [ ] args) {
test88 graph = new test88 ( ) ;
graph. insertNode ( "A" ) ;
graph. insertNode ( "B" ) ;
graph. insertNode ( "C" ) ;
graph. insertNode ( "D" ) ;
graph. insertNode ( "E" ) ;
graph. insertNode ( "F" ) ;
graph. insertEdge ( "C" , "A" , 1 ) ;
graph. insertEdge ( "F" , "C" , 2 ) ;
graph. insertEdge ( "A" , "D" , 5 ) ;
graph. insertEdge ( "A" , "B" , 4 ) ;
graph. insertEdge ( "E" , "B" , 2 ) ;
graph. insertEdge ( "D" , "F" , 4 ) ;
graph. insertEdge ( "D" , "E" , 3 ) ;
graph. Select( ) ;
graph. Select1( "A" ) ;
graph. Select2( "A" ) ;
test88 aa = new test88 ( ) ;
aa. insertNode ( "A" ) ;
aa. insertNode ( "B" ) ;
aa. insertNode ( "C" ) ;
aa. insertNode ( "D" ) ;
aa. insertNode ( "E" ) ;
aa. insertNode ( "F" ) ;
aa. insertNode ( "G" ) ;
aa. insertNode ( "H" ) ;
aa. insertEdge ( "A" , "D" , 3 ) ;
aa. insertEdge ( "A" , "G" , 3 ) ;
aa. insertEdge ( "B" , "A" , 3 ) ;
aa. insertEdge ( "B" , "F" , 3 ) ;
aa. insertEdge ( "C" , "H" , 3 ) ;
aa. insertEdge ( "D" , "F" , 3 ) ;
aa. insertEdge ( "E" , "B" , 3 ) ;
aa. insertEdge ( "F" , "C" , 3 ) ;
aa. insertEdge ( "G" , "E" , 3 ) ;
aa. Select( ) ;
aa. Select1( "A" ) ;
aa. Select2( "A" ) ;
}
}
我们也操作了该有向图:
那么对应的遍历如下:
深度优先遍历(搜索):A,B,D,E,F,C
广度优先遍历(搜索):A,B,D,E,F,C
我们可以发现,他们是一样的,所以在某些时候,他们是可以一样的,所以这里注意即可
继续回到前面的问题,如何得出最短路径,我们知道,深度优先搜索,他是一路找的,从代码里面可以看出,他先选择一个路,直到找不到了才会返回,如果我们使用深度来操作最短,那么我们基本是得不到具体路径的,因为有标志(且由于是按照字母顺序来的,所以如果上面的B指向E,E指向F,那么以A为起点,F为终点,我们只能得到ABEDF,然后因为标志,所以下一次不可能指向或者获得E了,但是实际上最短路径是ADF,即是错误的最短路径,所以我们不会考虑深度),所以深度是基本操作不了最短的,而广度可以,因为他是分散的,只要谁先找到E,那么对应的路径就是最短的,具体代码如下(以上面这个图片为例子):
package com. lagou ;
import java. util. * ;
public class test99 {
public class Vertex {
String name;
Edge next;
Vertex prev;
boolean b = false ;
public Vertex ( String name, Edge next) {
this . name = name;
this . next = next;
}
}
public class Edge {
String name;
int weight;
Edge next;
public Edge ( String name, int weight, Edge next) {
this . name = name;
this . weight = weight;
this . next = next;
}
}
Map < String , Vertex > map;
public test99 ( ) {
map = new HashMap < > ( ) ;
}
public void insertNode ( String a) {
if ( map. get ( a) == null ) {
Vertex aa = new Vertex ( a, null ) ;
map. put ( a, aa) ;
}
}
public void insertEdge ( String a, String b, int c) {
Vertex vertex = map. get ( a) ;
if ( vertex == null ) {
vertex = new Vertex ( a, null ) ;
map. put ( a, vertex) ;
}
if ( vertex. next == null ) {
vertex. next = new Edge ( b, c, null ) ;
} else {
Edge temp = vertex. next;
Edge temp2 = vertex. next;
while ( true ) {
if ( temp. name == b) {
temp. weight = c;
return ;
}
if ( temp. name. hashCode ( ) > b. hashCode ( ) ) {
if ( temp2 != vertex. next) {
Edge j = new Edge ( b, c, null ) ;
temp2. next = j;
j. next = temp;
} else {
Edge j = new Edge ( b, c, null ) ;
vertex. next = j;
j. next = temp2;
}
return ;
}
if ( temp. next == null ) {
temp. next = new Edge ( b, c, null ) ;
return ;
}
temp2 = temp;
temp = temp. next;
}
}
}
public void Select ( ) {
Set < Map. Entry < String , Vertex > > entries = map. entrySet ( ) ;
Iterator < Map. Entry < String , Vertex > > iterator = entries. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
Map. Entry < String , Vertex > entry = iterator. next ( ) ;
Vertex vertex = entry. getValue ( ) ;
Edge edge = vertex. next;
while ( edge != null ) {
System . out. println ( vertex. name + " 指向 " + edge. name + " 权值为:" + edge. weight) ;
edge = edge. next;
}
}
}
public void Select2 ( String a) {
Set < String > strings = map. keySet ( ) ;
Iterator < String > iterator = strings. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
String next = iterator. next ( ) ;
map. get ( next) . b = false ;
}
String [ ] array = new String [ map. size ( ) ] ;
int i = 0 ;
Vertex vertex = map. get ( a) ;
Queue < String > queue = new LinkedList < > ( ) ;
queue. offer ( vertex. name) ;
vertex. b = true ;
String poll = queue. poll ( ) ;
array[ i] = poll;
i++ ;
while ( true ) {
Edge edge = vertex. next;
if ( edge != null ) {
while ( edge != null ) {
if ( map. get ( edge. name) . b == false ) {
queue. offer ( edge. name) ;
map. get ( edge. name) . b = true ;
}
edge = edge. next;
}
}
if ( queue. isEmpty ( ) ) {
break ;
}
String poll1 = queue. poll ( ) ;
array[ i] = poll1;
i++ ;
vertex = map. get ( poll1) ;
edge = vertex. next;
}
System . out. print ( "[" ) ;
for ( int ii = 0 ; ii < array. length; ii++ ) {
if ( ii == array. length - 1 ) {
System . out. print ( array[ ii] ) ;
} else {
System . out. print ( array[ ii] + "," ) ;
}
}
System . out. println ( "]" ) ;
}
public void Select3 ( String a, String b) {
Set < String > strings = map. keySet ( ) ;
Iterator < String > iterator = strings. iterator ( ) ;
while ( iterator. hasNext ( ) ) {
String next = iterator. next ( ) ;
map. get ( next) . b = false ;
}
String [ ] array = new String [ map. size ( ) ] ;
int i = 0 ;
Vertex vertex = map. get ( a) ;
Queue < String > queue = new LinkedList < > ( ) ;
queue. offer ( vertex. name) ;
vertex. b = true ;
String poll = queue. poll ( ) ;
array[ i] = poll;
i++ ;
Vertex l = null ;
while ( true ) {
Edge edge = vertex. next;
if ( edge != null ) {
int o = 0 ;
while ( edge != null ) {
if ( map. get ( edge. name) . b == false ) {
queue. offer ( edge. name) ;
map. get ( edge. name) . b = true ;
}
map. get ( edge. name) . prev = vertex;
if ( edge. name == b) {
l = map. get ( edge. name) ;
o = 1 ;
break ;
}
edge = edge. next;
}
if ( o == 1 ) {
break ;
}
}
if ( queue. isEmpty ( ) ) {
break ;
}
String poll1 = queue. poll ( ) ;
array[ i] = poll1;
i++ ;
vertex = map. get ( poll1) ;
edge = vertex. next;
}
String [ ] aaa = new String [ map. size ( ) ] ;
int oo = 0 ;
for ( int ii = 0 ; ii < aaa. length; ii++ ) {
aaa[ ii] = l. name;
oo++ ;
l = l. prev;
if ( l == null ) {
break ;
}
}
System . out. println ( Arrays . toString ( aaa) ) ;
String [ ] bbb = new String [ oo] ;
for ( int pp = 0 ; pp < bbb. length; pp++ ) {
bbb[ pp] = aaa[ pp] ;
}
String [ ] ccc = new String [ bbb. length] ;
for ( int pp = 0 , ss = ccc. length - 1 ; pp < ccc. length; pp++ , ss-- ) {
ccc[ pp] = bbb[ ss] ;
}
System . out. println ( "路径如下:" + Arrays . toString ( ccc) ) ;
}
public static void main ( String [ ] args) {
test99 graph = new test99 ( ) ;
graph. insertNode ( "A" ) ;
graph. insertNode ( "B" ) ;
graph. insertNode ( "C" ) ;
graph. insertNode ( "D" ) ;
graph. insertNode ( "E" ) ;
graph. insertNode ( "F" ) ;
graph. insertEdge ( "C" , "A" , 1 ) ;
graph. insertEdge ( "F" , "C" , 2 ) ;
graph. insertEdge ( "A" , "D" , 5 ) ;
graph. insertEdge ( "A" , "B" , 4 ) ;
graph. insertEdge ( "E" , "B" , 2 ) ;
graph. insertEdge ( "D" , "F" , 4 ) ;
graph. insertEdge ( "D" , "E" , 3 ) ;
graph. Select( ) ;
graph. Select2( "A" ) ;
graph. Select3( "A" , "E" ) ;
}
}
至此,对应的最短路径操作完毕
时间复杂度:
深度优先搜索的时间复杂度是:O(V+E),v代表边数(因为他的寻找就是根据边的),E代表节点数,也就是顶点数,因为我们还需要进行回退,即出栈,这个循环就是顶点数,那么可以同理,实际上广度优先搜索也是O(V+E),V也是边数,E也是顶点数
即深度优先遍历(搜索,是图的遍历或者搜索)和广度优先遍历(搜索,是图的遍历或者搜索)都是O(V+E)
上面忽略了打印的时间复杂度,否则还要加上O(X),X代表打印代码的执行次数
应用:
广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS,这种算法往往可以大大地提高搜索的效率
社交网络可以用图来表示,这个问题就非常适合用图的广度优先搜索算法来解决,因为广度优先搜索是层层往外推进的
首先,遍历与起始顶点最近的一层顶点,也就是用户的一度好友,然后再遍历与用户 距离的边数为 2 的顶点,也就是二度好友关系,以及与用户距离的边数为 3 的顶点,也就是三度好友关系,即就是这样的说明
算法思维 :
接下来说明的都是思想,而不是算法,只是我们可以利用这些思想,可以更加合理的,或者简便的使用具体算法来实现需求
贪心算法 :
概念 :
贪婪(心)算法(Greedy)的定义:是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法
贪婪算法:当下做局部最优判断,不能回退(能回退的是回溯,最优+回退是动态规划),注意:可能你认为是最好的,但是可能还有更好的,所以这里是看个人的最优判断
由于贪心算法的高效性以及所求得答案比较接近最优结果,贪心算法可以作为辅助算法或解决一些要求结果不特别精确的问题
注意:当下是最优的,并不一定全局是最优的,举例如下:
有硬币分值为10、9、4若干枚,问如果组成分值为18,最少需要多少枚硬币?
采用贪心算法,选择当下硬币分值最大的:10
注意:是当下最大的硬币,也就是说,必须先操作10,所以有如下:
18-10=8,8/4=2
即:1个10、2个4,共需要3枚硬币
但实际上我们知道,选择分值为9的硬币,2枚就够了(18/9=2),所以我们才会说贪心算法是当下最优解,而不是全局的
所以才会说明当下做局部最优判断,有时候也可以认为我们已经操作一部分了,那么现在最优解是什么,但是前面的部分未必是最优解的
如果改成:
有硬币分值为10、5、1若干枚,问如果组成分值为16,最少需要多少枚硬币?
采用贪心算法,选择当下硬币分值最大的:10
16-10=6
6-5=1
即:1个10,1个5,1个1 ,共需要3枚硬币
即为最优解,由此可以看出贪心算法适合于一些特殊的情况,如果能用一定是最优解(且是全局的,即当前就是在最好的情况下进行贪心)
总而言之,就是在一定情况下,做出最优解,而该情况可能是最好的状态,也可能是不好的状态,而贪心不能改变该情况,所以贪心可以是全局最优解,也可以是局部最优解,但是该情况是最好的情况比例是很少的,所以总体来说,贪心就称为局部最优解了(虽然他在极端情况下,也能是全局最优解),以后说明的最优解,我们统称为局部最优解,如果是全局最优解我会特殊说明
实际上从上面可以看出,三个硬币中,如果最大的硬币(10),与第二大的硬币(5或者9),相差很大,那么最优解就更加全局
当然,这不是贪心算法,而是上面说明的本身的情况,即状态,所以第二种有5的,就是最好的状态(当然,还有好的状态,或者不好的状态,我们一般也将不是最好的状态,称为不好的状态,因为局部最优解是全局最优解之外的统称)
经典问题:部分背包
背包问题是算法的经典问题,分为部分背包和0-1背包,主要区别如下:
部分背包:某件物品是一堆,可以带走其一部分
0-1背包:对于某件物品,要么被带走(选择了它),要么不被带走(没有选择它),不存在只带走一 部分的情况
部分背包问题可以用贪心算法求解,且能够得到最优解
以部分背包的问题为例子(作为主要的情况来操作,不是0-1背包的问题):
假设一共有N件物品,第 i 件物品的价值为 Vi ,重量为Wi,一个小偷有一个最多只能装下重量为W的背包,他希望带走的物品越有价值越好,可以带走某件物品的一部分,请问:他应该选择哪些物品?
假设背包可容纳50Kg的重量,物品信息如下表:
贪心算法的关键是贪心策略的选择或者思考 ,比如如下策略的思考:
从上面可以看出,A的价值是最好的,因为换算成重量,那么就是最好的
我们可以将物品按单位重量所具有的价值排序,总是优先选择单位重量下价值最大的物品
按照我们的贪心策略,单位重量的价值排序: 物品A > 物品B > 物品C
因此,我们尽可能地多拿物品A,直到将物品A拿完之后,才去拿物品B,然后是物品C,很明显按照这样的顺序,物品C会只拿一部分,这样,可以使得得到最多的利润,而这样的想法也归属于贪心算法(或者称为贪心,或者称为贪婪,或者称为贪心算法)
具体代码如下,可以先看一下,然后自己进行编写:
package test ;
public class test1 {
public static class Goods {
String name;
double weight;
double price;
String k = "" ;
public Goods ( String name, double weight, double price) {
this . name = name;
this . weight = weight;
this . price = price;
}
}
public void take ( Goods [ ] goods, double a) {
Goods [ ] sort = sort ( goods) ;
double j = 0 ;
for ( int i = 0 ; i < sort. length; i++ ) {
if ( a >= sort[ i] . weight) {
a -= sort[ i] . weight;
System . out. println ( sort[ i] . name + "全部拿走,总共:" + sort[ i] . weight + "kg,总价值" + sort[ i] . price) ;
j += sort[ i] . price;
} else {
System . out. println ( sort[ i] . name + "部分拿走,总共:" + a + "kg,总价值" + a * sort[ i] . price / sort[ i] . weight) ;
j += a * sort[ i] . price / sort[ i] . weight;
a -= a;
break ;
}
}
System . out. println ( "总共得到" + j + "元" ) ;
System . out. println ( "背包剩余容量:" + a + "kg" ) ;
}
public void take1 ( Goods [ ] sort, double a) {
double j = 0 ;
for ( int i = 0 ; i < sort. length; i++ ) {
int ii = 0 ;
Goods g = sort[ ii] ;
while ( true ) {
if ( ii + 1 >= sort. length) {
break ;
}
if ( g. k. equals ( "不存在" ) ) {
g = sort[ ii + 1 ] ;
ii++ ;
continue ;
}
if ( g. price / g. weight < sort[ ii + 1 ] . price / sort[ ii + 1 ] . weight) {
g = sort[ ii + 1 ] ;
}
ii++ ;
}
if ( a >= g. weight) {
a -= g. weight;
System . out. println ( g. name + "全部拿走,总共:" + g. weight + "kg,总价值" + g. price) ;
j += g. price;
} else {
System . out. println ( g. name + "部分拿走,总共:" + a + "kg,总价值" + a * g. price / g. weight) ;
j += a * g. price / g. weight;
a -= a;
break ;
}
g. k = "不存在" ;
}
System . out. println ( "总共得到" + j + "元" ) ;
System . out. println ( "背包剩余容量:" + a + "kg" ) ;
}
public Goods [ ] sort ( Goods [ ] a) {
for ( int i = 0 ; i < a. length - 1 ; i++ ) {
boolean is = true ;
for ( int j = 0 ; j < a. length - 1 - i; j++ ) {
Goods tmp = null ;
if ( ( a[ j] . price / a[ j] . weight) < ( a[ j + 1 ] . price / a[ j + 1 ] . weight) ) {
is = false ;
tmp = a[ j] ;
a[ j] = a[ j + 1 ] ;
a[ j + 1 ] = tmp;
}
}
if ( is) {
break ;
}
}
return a;
}
public static void main ( String [ ] args) {
test1 bd = new test1 ( ) ;
Goods goods1 = new Goods ( "A" , 10 , 60 ) ;
Goods goods2 = new Goods ( "B" , 20 , 100 ) ;
Goods goods3 = new Goods ( "C" , 30 , 120 ) ;
Goods [ ] goodslist = { goods1, goods2, goods3} ;
bd. take ( goodslist, 50 ) ;
bd. take1 ( goodslist, 50 ) ;
}
}
时间复杂度 :
在不考虑排序的前提下(省略了n^2),贪心算法只需要一次循环,所以时间复杂度是O(n),n是商品数量
优缺点:
优点:性能高,能用贪心算法解决的往往是最优解
缺点:在实际情况下能用的不多,用贪心算法解的往往不是最好的,需要自己的逻辑思维,所以贪心算法往往不能求得最优解
所以,实际上得到最优解的解决方案(看起来,并不是一定是最优解,后面会说明,比如斐波那契数列的通项公式),我们都会认为是贪心算法,当然上面只是一种思维,即我们需要得到最好的,那么就可以认为是贪心算法
适用场景 :
若针对一组数据,我们定义了限制值和期望值,我们希望从中选出几个数据,在满足限制值的情况下,使得期望值最大
或者说每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据(局部最优,也可以使得全局最优)
上述情况,大部分能用贪心算法来解决问题
贪心算法的正确性都是显而易见的,也不需要严格的数学推导证明,在实际情况下,用贪心算法解决问题的思路,并不总能给出最优解(特别是全局,因为比例太少,部分局部也不能,比如其对应可以通过数学知识,比如斐波那契数列的通项公式,他是最优解,而贪心算法在对应情况下,是基本得不到比他还要好的最优解的)
简单来说,贪心算法是使得在当前情况下是最优解的思想,只有你认为是最优解的即可,无论是否有其他最优解(比如斐波那契数列的通项公式,他这种情况,单纯的使用贪心算法思想,可能是想不到的),即贪心算法是一种思想,而不是一个算法,只是这个思想形成的算法,我们称为贪心算法
分治算法:
概念 :
分治算法(divide and conquer)的核心思想其实就是四个字,分而治之 ,也就是将原问题划分成 n个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解(快速排序就是一种使用了分治法,即分治算法的排序)
关于分治和递归的区别:
分治算法是一种处理问题的思想,递归是一种编程技巧,分治算法的递归实现中,每一层递归都会涉及这样三个操作:
分解:将原问题分解成一系列子问题 (调用自身,相当于递归三要素中的"函数的功能 ",虽然他可以看成就是相当于递归)
解决:递归地求解各个子问题,若子问题足够小,则直接求解 (有最终返回条件,即终止条件,相当于递归三要素中的"函数的功能加上递归结束条件")
合并:将子问题的结果合并成原问题(相当于递归三要素中的"函数的等价关系式",虽然这里不同的是,他是主要操作合并的,而并不会必须是一个关系式,只需要是一个方程或者具体式子操作即可)
可以看到,实际上分治算法基本需要利用递归,但是并不完全需要递归,虽然递归的具体操作是正好符合他分治的思想而已,但是也可以使用其他方式,比如循环(如快速排序),只要思想是这样的即可,所以上面我才会说"分治算法是一种处理问题的思想,递归是一种编程技巧",只不过该技巧,符合他的主要操作(分治),就如循环是实现次数,而递归是调用自身,但是像这些操作,我们都称为技巧(也是算法,只不过这些大众认为的基本操作,我们也称为技巧而已,像前面的"跟踪算法",以及"定义临时变量"等等都是技巧,虽然也称为算法,只是大多数这样操作能够更好的进行实现具体要求,我们也通常会利用这些技巧来更好的实现具体需求)
比如:
将字符串中的小写字母转化为大写字母
“abcde"转化为"ABCDE”
我们可以利用分治的思想将整个字符串转化成一个一个的字符处理
经典问题 :
上述问题代码如下,可以先看一下,然后自己编写:
package test ;
public class test2 {
public static char toUpCaseUnit ( char c) {
int n = c;
if ( n < 97 || n > 122 ) {
return ' ' ;
}
return ( char ) ( n - 32 ) ;
}
public static char [ ] toUpCase ( char [ ] chs, int i) {
if ( i >= chs. length) return chs;
chs[ i] = toUpCaseUnit ( chs[ i] ) ;
return toUpCase ( chs, i + 1 ) ;
}
public static char [ ] toUpCase1 ( char [ ] chs, int i) {
for ( int a = i; a < chs. length; a++ ) {
chs[ a] = toUpCaseUnit ( chs[ a] ) ;
}
return chs;
}
public static void main ( String [ ] args) {
String ss = "abcde" ;
System . out. println ( test2. toUpCase ( ss. toCharArray ( ) , 0 ) ) ;
System . out. println ( test2. toUpCase1 ( ss. toCharArray ( ) , 0 ) ) ;
}
}
时间复杂度:就是字符串的长度,也就是O(n)
求X^n问题:
比如:2^10,即2的10次幂
一般的解法是循环10次
package test ;
public class test3 {
public static int commpow ( int x, int n) {
int s = 1 ;
while ( n >= 1 ) {
s *= x;
n-- ;
}
return s;
}
public static void main ( String [ ] args) {
System . out. println ( commpow ( 2 , 10 ) ) ;
}
}
该方法的时间复杂度是:O(n),n代表多少次幂
采用分治法:
2^10拆成:
我们看到每次拆成n/2次幂,时间复杂度是O(logn),可能你看不明白,没有关系,看代码就知道了:
package test ;
public class test4 {
public static int dividpow ( int x, int n) {
if ( n == 1 ) {
return x;
}
int half = dividpow ( x, n / 2 ) ;
if ( n % 2 == 0 ) {
return half * half;
} else {
return half * half * x;
}
}
public static void main ( String [ ] args) {
System . out. println ( dividpow ( 2 , 10 ) ) ;
}
}
时间复杂度:
根据拆分情况可以是O(n)或O(logn),为什么可以是O(n),因为如果对应只需要2^1,那么难道不是O(n)吗,当然,这是极端情况下的,因为按照这样说,那么所有的O(logn)的操作都基本可以是O(n),所以总体来说,我们就认为他是O(logn),即不考虑极端情况,除非具体说明(比如这里就说明了),因为"时间复杂度是整体的说明的,而不是投机取巧说明的"(前面说明过了)
优缺点:
优势:将复杂的问题拆分成简单的子问题,解决更容易,另外根据拆分规则,性能有可能提高
劣势:子问题必须要一样,用相同的方式解决(因为是相同的方法,即利用自己)
适用场景 :
分治算法能解决的问题,一般需要满足下面这几个条件:
原问题与分解成的小问题具有相同的模式(相同的自身方法)
原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别
具有分解终止条件,也就是说,当问题足够小时,可以直接求解(即前面说明的返回2)
可以将子问题合并成原问题,而这个合并操作的复杂度不能太高(即前面说明的判断),否则就起不到减小算法总体复杂度的效果了
回溯算法 :
概念:
回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发 现已不满足求解条件时,就"回溯"返回(也就是递归返回),尝试别的路径(虽然可能该路径已经确定,但还是会检验的)
回溯的处理思想,有点类似枚举(列出所有的情况)搜索,我们枚举所有的解,找到满足期望的解,为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段,每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走,人生如果能够回退?那么你想回退到哪个阶段呢?
经典问题 :
N皇后问题:
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击
我们把这个问题划分成 8 个阶段,依次将 8 个棋子放到第一行、第二行、第三行…直到第八行
在放置的过程中,我们不停地检查当前放法,是否满足要求,如果满足,则跳到下一行继续放置棋子,如果不满足,那就再换一种放法,继续尝试(这里就使用了回溯的思想,具体如何使用,我们在代码中进行查看)
具体实现代码如下(可以先看下,然后自己进行编写):
package test ;
public class test5 {
static int QUEENS = 8 ;
int [ ] result = new int [ QUEENS ] ;
int aa = 0 ;
int jj = 0 ;
public void insert ( int a) {
if ( a >= QUEENS ) {
printQueens ( ) ;
aa++ ;
return ;
}
for ( int i = 0 ; i < QUEENS ; i++ ) {
if ( isOK ( a, i) ) {
result[ a] = i;
insert ( a + 1 ) ;
}
}
}
public void printQueens ( ) {
for ( int i = 0 ; i < QUEENS ; i++ ) {
System . out. print ( "|" ) ;
for ( int j = 0 ; j < QUEENS ; j++ ) {
if ( result[ i] == j) {
System . out. print ( "Q|" ) ;
} else {
System . out. print ( "*|" ) ;
}
}
System . out. println ( ) ;
}
System . out. println ( "-----------------------" ) ;
}
public boolean isOK ( int a, int b) {
int left = b - 1 ;
int right = b + 1 ;
int o = jj;
for ( int i = a - 1 ; i >= 0 ; i-- ) {
++ jj;
if ( result[ i] == b) {
return false ;
}
if ( left >= 0 ) {
if ( result[ i] == left) {
return false ;
}
}
if ( right < QUEENS ) {
if ( result[ i] == right) {
return false ;
}
}
left-- ;
right++ ;
}
if ( o== jj) {
jj++ ;
}
return true ;
}
public static void main ( String [ ] args) {
test5 queens = new test5 ( ) ;
queens. insert ( 0 ) ;
System . out. println ( "有" + queens. aa + "个可能" ) ;
System . out. println ( "执行了" + queens. jj + "次循环" ) ;
}
}
至此n皇后说明完毕
时间复杂度:
N皇后问题的时间复杂度为:如果不考虑他的n皇后的本来意义,一般就是O(n^n),若考虑的情况下,我们可以认为是(n!,前面有解释,这里是相乘的阶乘,而不是相加,可能在某些优化下可以达到,并且可能也会可以是n!/2,具体优化可以百度查看)
在程序里面我们一般会将阶乘称为相加或者相乘,一般我们会认为是相乘(而不是相加,当然,在程序里面我们都可以表示,看情况即可)
优缺点:
优点:
回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是,从一组可能的解 中,选择出一个满足要求的解
回溯算法非常适合用递归来实现,在实现的过程中,剪枝操作是提高回溯效率的一种技巧(相当于前面冒泡中的优化,比如其设置标志isSort),利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率
劣势: 效率相对于低(动态规划)
适用场景 :
回溯算法(思想)是个"万金油",基本上能用的动态规划、贪心解决的问题,我们都可以用回溯算法解决
回溯算法相当于穷举搜索,穷举所有的情况,然后对比得到最优解,不过,回溯算法的时间复杂度非常高
是指数级别的,只能用来解决小规模数据的问题,对于大规模数据的问题,用回溯算法解决的执行效率 就很低了
动态规划 :
概念:
动态规划(Dynamic Programming),是一种分阶段求解的方法
动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治) 的方式去解决
首先是拆分问题,我的理解就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现
关键就是这个步骤,动态规划有一类问题就是从后往前推的
有时候我们很容易知道,如果只有一种情况时,最佳的选择应该怎么做,然后根据这个最佳选择往前一步推导,得到前一步的最佳选择 (也类似于该方法的编写,类似于包含递归三要素中的"函数的功能"和"递归结束条件"的作用)
然后就是定义问题状态和状态之间的关系,我的理解是前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式,类似于递归三要素中的"函数的等价关系式",虽然他并不是一定是关系式,但包含关系式)
我们再来看如下说明,我的理解是比如我们找到最优解,我们应该讲最优解保存下来:
为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃
只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度(如后面斐波那契数列的备忘录操作,虽然他本来就是计算,单纯的计算自然归属于最优解,就比如23=6,你在再怎么操作,2 * 3=6就是最优解的,可能有与他同级,但基本不会超过(因为是自己的),比如3 2=6,当然,这个最优解是自己的最优解,不是别人的,因为6也可以是1 *6=6,这里以自己的最优解来说明)
动态规划中有三个重要概念:
1:最优子结构(类似于递归三要素中的"函数的功能",但还是有点差别,可以说是最大的情况)
2:边界(类似于递归三要素中的"递归结束条件")
3:状态转移公式(也可叫做递推方程),即dp方程(类似于递归三要素中的"函数的等价关系式",一般来说递推就是循环操作的分解的递归,比如前面使用循环解决的斐波那契数列就是递推操作,所以递推方程和递归方程是类似的,而之所以分为递归和递推,主要是调用自身是归的意思,所以我们称为递归,而一路执行是推的意思,所以我们称为递推)
要注意,该dp方程可以是多样的,而不仅仅只是一个函数的关系式,特别在后面0-1背包问题中的分析会说明
经典问题 :
再谈斐波那契数列:
优化递归:
通过上边的递归树可以看出在树的每层和上层都有大量的重复计算,可以把计算结果存起来,下次再用的时候就不用再计算了,这种方式叫记忆搜索,也叫做备忘录模式(前面有大致说明,在这个地方"缺点:占用空间较大、如果递归太深,可能会发生栈溢出、可能会有重复计算,可以通过备忘录(动态规划,在说明动态规划时会说明)或递归的其他方式去优化")
现在我们来进行实现,可以先看一下,然后自己进行编写:
package test ;
public class test6 {
static long [ ] sub = new long [ 100 ] ;
public static long fib ( int n) {
if ( n <= 1 ) return n;
if ( sub[ n] == 0 ) {
sub[ n] = fib ( n - 1 ) + fib ( n - 2 ) ;
}
return sub[ n] ;
}
public static long fa ( int n) {
if ( n <= 1 ) return n;
return fa ( n - 1 ) + fa ( n - 2 ) ;
}
public static void main ( String [ ] args) {
System . out. println ( fib ( 50 ) ) ;
System . out. println ( fa ( 50 ) ) ;
}
}
dp方程:
现在我们来编写一个递推的代码(比上面的递归要好,在斐波那契数列中,递推是基本比递归好的,你可以设置在其中进行打印数字,每打印一次,该数字加1,这样来进行验证,很明显,可以发现,上面的递归需要96次,而下面的递推只需要49次),可以自己先看一下,然后编写:
package test ;
public class test7 {
public static int fib ( int n) {
int a[ ] = new int [ n + 1 ] ;
a[ 0 ] = 0 ;
a[ 1 ] = 1 ;
int i;
for ( i = 2 ; i <= n; i++ ) {
a[ i] = a[ i - 1 ] + a[ i - 2 ] ;
}
return a[ i - 1 ] ;
}
public static int fa ( int i) {
if ( i <= 1 ) return i;
int a = 0 ;
int b = 1 ;
for ( int j = 2 ; j <= i; j++ ) {
b = b + a;
a = b - a;
}
return b;
}
public static void main ( String [ ] args) {
System . out. println ( fib ( 9 ) ) ;
System . out. println ( fa ( 9 ) ) ;
}
}
使用动态规划四个步骤:
1:把当前的复杂问题转化成一个个简单的子问题(分治)
2:寻找子问题的最优解法(最优子结构)
3:把子问题的解合并,存储中间状态(这是主要的思想,因为其他的基本于分治类似,因为分治相当于递归,即这里是主要的区别)
4:递归+记忆搜索或自底而上的形成递推方程(dp方程)
时间复杂度 :
新的斐波那契数列(这里是递推的实现方式)实现时间复杂度为O(n)(以递推为例子则是O(n),因为是循环,前面第一次说明斐波那契数列时,就已经说明过了),n代表:你要找的数减去1
优缺点 :
优点:时间复杂度和空间复杂度都相当较低
缺点:难,有些场景不适用
适用场景 :
尽管动态规划比回溯算法高效,但是,并不是所有问题,都可以用动态规划来解决
能用动态规划解决 的问题,需要满足三个特征,最优子结构、无后效性和重复子问题
在重复子问题这一点上,动态规划 和分治算法的区分非常明显,分治算法要求分割成的子问题,不能有重复子问题
而动态规划正好相反(因为保存的存在操作,使得不用再次的计算了),动态规划之所以高效,就是因为回溯算法(因为也是单纯使用分治算法)实现中存在大量的重复子问题
数据结构与算法实战:
接下来给出几个问题来练一练手:
环形链表问题:
给定一个链表,判断链表中是否有环,存在环返回 true ,否则返回 false(头节点为null,也是这个)
分析:
该题可以理解为检测链表的某节点能否二次到达(重复访问)的问题,需要一个容器记录已经访问过的节点,每次访问到新的节点,都与容器中的记录进行匹配,若相同则存在环,那么直接的返回false,若匹配之后没有相同节点,则存入容器,继续访问新的节点,直到访问节点的next指针返回null,那么返回true,至此操作结束,实现简单
时间复杂度为:
遍历整个链表,即O(n),每次遍历节点,再遍历数组进行匹配,O(x),x代表总遍历数组的记录,如果是哈希的操作,那么只需要O(n)即可
这样就不用遍历了,而使得总时间复杂度是n+x(该x是(n-1+1)(n-1)/2=(n^2-n)/2,因为假设是9个节点,那么需要判断从1次到8次为止)
换个思路: 该题可以理解为"追击相遇"问题,如果存在环,跑得快的一定能追上跑得慢的,比如:一快一慢两个运动员,如果在直道赛跑,不存在追击相遇问题,如果是在环道赛跑,快的绕了一 圈肯定可以追上慢的
解法:
代码如下,可以自己先看一遍,然后自行编写:
package test ;
public class test8 {
public static class Node {
int key;
String name;
Node next;
public Node ( int key, String name) {
this . key = key;
this . name = name;
}
}
public static boolean isRing ( Node head) {
if ( head == null ) {
System . out. println ( "没有头节点" ) ;
return false ;
}
Node slow = head;
Node fast = head. next;
while ( fast != null && fast. next != null ) {
if ( slow == fast) {
return true ;
}
fast = fast. next. next;
slow = slow. next;
}
return false ;
}
public static void main ( String [ ] args) {
Node n1 = new Node ( 1 , "张飞" ) ;
Node n2 = new Node ( 2 , "关羽" ) ;
Node n3 = new Node ( 3 , "赵云" ) ;
Node n4 = new Node ( 4 , "黄忠" ) ;
Node n5 = new Node ( 5 , "马超" ) ;
n1. next = n2;
n2. next = n3;
n3. next = n4;
n4. next = n5;
n5. next = n1;
System . out. println ( isRing ( n1) ) ;
}
}
此种算法的时间复杂度为:O(n),因为他是循环链表的,但是可能快的指针能够更快的循环,所以可以认为是n/2-1次,忽略系数,认为是O(n)
0-1背包问题:
有n件物品和一个最大承重为W的背包,每件物品的重量是w[i],价值是v[i]
在保证总重量不超过W的前提下,选择某些物品装入背包,背包的最大总价值是多少?
注意:每个物品只有一件,也就是每个物品只能选择0件或者1件
分析:
假设:W=10(背包重量),有5件物品,重量和价值如下:
w[1]=2,v[1]=6
w[2]=2,v[2]=3
w[3]=6,v[3]=5
w[4]=5,v[4]=4
w[5]=4,v[5]=6
我们一般在后面称为组合,比如w[1]=2,v[1]=6 就是(2,6)组合
由于只能选择1件物品,那么可以有如下:
dp数组的计算结果如下表:
i:选择的物品
j:最大承重
现在我们来解释一下上面的表:
首先是左边,左边是我们选择的物品,上边是最大承重,那么中间的值呢,他代表是该承重最大的价值是多少
从上到下的思考:
首先j是0和1,i是1,由于他们什么都不能选择,自然价值就是0
而j是2和3,i是1,由于他们选择的价值最大是(2,6)组合,所以都是6
那么4(4列那里,包括0的)为什么是中间的值从上到下是:6,9,9,9,9呢
我们第一个自然是分配一个(2,6)组合,而后面只能分配(2,3)组合了因为容量只有4
后面的就不多说,这里给出一个具体例子,即第9个重量,你可以发现中间的值从上到下是6,9,11,13,15,很明显,你可能不知道为什么,这里给出原因:
第一个是6,9-2=7,第二个是9,7-2=5,那么第三个怎么来的,因为由于5-6=-1,超过了重量,所以我们应该重新评估价格,这是因为在前面三个选择中,很明显(6,5)组合价值比(2,3)组合价值高(只能选取一个,且不考虑后续的情况下),所以直接省略了之前的操作,也就是7-2=5,变成7-6=1,总体由9(6+3)价值变成了11(6+5)个价值,然后继续往下,又可以发现,他的(5,4)组合可以结合(2,3)组合,所以又要进行重新判断,至此,你应该知道了上面的表代表什么意思了吧(即其第四个中的13就是6+3+4=13,同理6+3+6=15,因为他们相对的价值是最多的),他的意思也就是说,在考虑前面的物品的情况下的最好价值,比如如果是第4行,那么只考虑前4个物品,所以如果要实现前面的问题,我们只需要到第5行即可(0行忽略,即只考虑i=5的那一行数据),可以得出,即在重量为10的情况下,最好的价值就是15
你可能会有疑惑,单位价值中(2,3)组合必然是比(5,4)组合要好的,我可以将他们的单位价值来排序吗,答:不需要,因为我们是0-1问题,是不看单位价值的(即一个重量是多少价值),只看是否合适重量,才会考虑价值,且只能选择一个,比如我只剩下5个重量,因为只能选择一个(如果只考虑他们两个),那么我们必然是选择5 4,因为反正只能放入一个,即他们都合适重量(因为合适,所以不考虑单位重量),那么才会考虑价值,而4是大于3的,那么我们才会选择5 4
注意:上面只是逻辑的思考,并没有给出具体流程,只是让你知道为什么会是这样的值,而没有给通过流程来得到这样的值
解法:
dp方程:
可以自己看一遍,然后自行编写:
package test ;
public class test9 {
public static int maxValue ( int [ ] values, int [ ] weights, int max) {
if ( values == null || values. length == 0 ) return 0 ;
if ( weights == null || weights. length == 0 ) return 0 ;
if ( values. length != weights. length || max <= 0 ) return 0 ;
int [ ] [ ] dp = new int [ values. length + 1 ] [ max + 1 ] ;
for ( int i = 1 ; i <= values. length; i++ ) {
for ( int j = 1 ; j <= max; j++ ) {
if ( j < weights[ i - 1 ] ) {
dp[ i] [ j] = dp[ i - 1 ] [ j] ;
}
else {
dp[ i] [ j] = Math . max ( ( dp[ i - 1 ] [ j] ) , values[ i - 1 ] + dp[ i - 1 ] [ j - weights[ i - 1 ] ] ) ;
}
}
}
return dp[ values. length] [ max] ;
}
public static void main ( String [ ] args) {
int [ ] values = { 6 , 3 , 5 , 4 , 6 } ;
int [ ] weights = { 2 , 2 , 6 , 5 , 4 } ;
int max = 10 ;
System . out. println ( maxValue ( values, weights, max) ) ;
}
}
package test ;
public class test9 {
public static int maxValue ( int [ ] values, int [ ] weights, int max) {
if ( values == null || values. length == 0 ) return 0 ;
if ( weights == null || weights. length == 0 ) return 0 ;
if ( values. length != weights. length || max <= 0 )
return 0 ;
int [ ] [ ] dp = new int [ values. length] [ max] ;
for ( int i = 0 ; i < values. length; i++ ) {
for ( int j = 0 ; j < max; j++ ) {
if ( j + 1 < weights[ i] ) {
int l = 0 ;
if ( i != 0 ) {
l = dp[ i - 1 ] [ j] ;
}
dp[ i] [ j] = l;
} else {
int ii = 0 ;
int oo = 0 ;
if ( i != 0 ) {
ii = dp[ i - 1 ] [ j] ;
int uu = j - weights[ i] ;
if ( uu < 0 ) {
uu = 0 ;
}
oo = dp[ i - 1 ] [ uu] ;
}
dp[ i] [ j] = Math . max ( ii, values[ i] + oo) ;
}
}
}
return dp[ values. length - 1 ] [ max - 1 ] ;
}
public static void main ( String [ ] args) {
int [ ] values = { 6 , 3 , 5 , 4 , 6 } ;
int [ ] weights = { 2 , 2 , 6 , 5 , 4 } ;
int max = 10 ;
System . out. println ( maxValue ( values, weights, max) ) ;
}
}
时间复杂度为: O(i*j),i代表物品数量,j代表背包重量
可以看出动态规划是计算的值是上次的某个值+这次的值,是一种用空间换时间的算法
至此,我们的数据结构和算法大致说明完毕(选取主要的来进行学习,就比如二叉树那里有很多种数据结构的类型,但我们只是说明主要的情况或者类型)