目录
一、算法优劣的核心指标
二、常数时间操作
1、常见的常数时间的操作
2、位运算
(1)(<<)左移运算符
(2)(>>)右移运算符
(3)(>>>)无符号右移
(4)(&)位与
(5)(|)位或
(6)(^)异或 同或
(7)(~)位非
三、时间复杂度
四、选择排序
五、冒泡排序
六、插入排序
七、注意
八、额外空间复杂度
九、对数器
十、二分法
十一、异或的骚操作
一、算法优劣的核心指标
时间复杂度(流程决定)
额外空间复杂度(流程决定)
常数项时间(实现细节决定)
二、常数时间操作
常数时间操作是指在算法或数据结构中,无论输入规模的大小如何,所需的执行时间都是固定的,即常数时间。这意味着无论输入数据的大小如何增加,操作的执行时间都保持不变。
常数时间操作通常是非常高效的,因为它们不受输入规模的影响。这些操作通常是通过直接访问数据结构的特定位置或执行简单的计算来完成的。例如,访问数组中的特定元素、插入或删除链表的头部元素、执行固定次数的循环等操作都可以被认为是常数时间操作。
1、常见的常数时间的操作
- 常见的算术运算(+、-、*、/、%等)
- 常见的位运算(>>、>>>、<<、|、&、^等)
- 赋值,比较,自增,自减等操作
- 数组寻址操作
和数据量没关系,和数组长度没关系,都是固定时间
反例:LinkedList是个双向链表,每个值之间靠指针指向,不是连续区间,非常数时间操作
2、位运算
正数换算成二进制后的最高位为0,负数的二进制最高位为1
正数的反码和补码都与原码相同
负数的反码为对该数的原码除符号位外各位取反
负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1
负数的原码转补码,补码转原码一样,数值位取反加一
只要有负数,就使用补码进行运算
(1)(<<)左移运算符
数值的补码全部往左移动,符号位和最高位都舍弃,最低位补0
package day01;public class demo {public static void main(String[] args) {System.out.println(5<<3);//输出40}
}
运行结果是20,但是程序是怎么实现的呐?
首先会将5转为2进制表示形式(Java中,整数默认形式是int,也就是32位)
0000 0000 0000 0000 0000 0000 0000 0101 5的二进制形式,然后左移3位,低位补0
0000 0000 0000 0000 0000 0000 0010 1000 转换为十进制数就是40
对于负数的话:
package day01;public class demo {public static void main(String[] args) {System.out.println(-5<<2);//输出-20}
}
1000 0000 0000 0000 0000 0000 0000 0101 -5的原码
1111 1111 1111 1111 1111 1111 1111 1010 -5的反码
1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
1111 1111 1111 1111 1111 1111 1110 1100 -5左移2位
然后将补码转换为原码就可以得到数值
1000 0000 0000 0000 0000 0000 0001 0011 左移之后的补码转反码
1000 0000 0000 0000 0000 0000 0001 0100 原码,转化为十进制数就是-20
(2)(>>)右移运算符
数值的补码向右移,符号位不变(左边补上符号位)
package day01;public class demo {public static void main(String[] args) {System.out.println(5>>2);//输出1}
}
先将为5转为2进制表示形式:
0000 0000 0000 0000 0000 0000 0000 0101 然后右移2位,高位补0
0000 0000 0000 0000 0000 0000 0000 0001 01 转换为10进制数就是1
对于负数:
package day01;public class demo {public static void main(String[] args) {System.out.println(-5>>2);//输出-2}
}
1000 0000 0000 0000 0000 0000 0000 0101 -5的原码
1111 1111 1111 1111 1111 1111 1111 1010 -5的反码
1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
1111 1111 1111 1111 1111 1111 1111 1110 11 右移2位
然后将补码转换为原码就可以得到数值
1000 0000 0000 0000 0000 0000 0000 0001 右移之后的补码转反码
1000 0000 0000 0000 0000 0000 0000 0010 反码转原码,即10进制值为:-2
(3)(>>>)无符号右移
无论参与运算的数字为正数或为负数,在执运算时,都会在高位补0
正数:
package day01;public class demo {public static void main(String[] args) {System.out.println(5>>>2);//输出1}
}
先将为5转为2进制表示形式:
0000 0000 0000 0000 0000 0000 0000 0101 然后右移2位,高位补0
0000 0000 0000 0000 0000 0000 0000 0001 01 转换为10进制数就是1
负数:
package day01;public class demo {public static void main(String[] args) {System.out.println(-5>>>2);//输出1073741822}
}
1000 0000 0000 0000 0000 0000 0000 0101 -5的原码
1111 1111 1111 1111 1111 1111 1111 1010 -5的反码
1111 1111 1111 1111 1111 1111 1111 1011 -5的补码
0011 1111 1111 1111 1111 1111 1111 1110 右移2位,高位补0
然后将补码转换为原码就可以得到数值
1100 0000 0000 0000 0000 0000 0000 0001 右移之后的补码转反码
1011 1111 1111 1111 1111 1111 1111 1110 反码转原码,即10进制值为: 也就是2^1+...+2^29=1073741822
(4)(&)位与
第一个操作数的的第n位与第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
package day01;public class demo {public static void main(String[] args) {System.out.println(5&3);//输出1}
}
将2个操作数和结果都转换为二进制进行比较:
5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011
-------------------------------------------------------------------------------------
1转换为二进制:0000 0000 0000 0000 0000 0000 0000 0001 所以转换为10进制结果为1
关于负数的运算:
package day01;public class demo {public static void main(String[] args) {System.out.println(-4&5);//输出4}
}
将2个操作数和结果都转换为二进制进行比较:
-4转换为二进制:1111 1111 1111 1111 1111 1111 1111 1100 -4(补码)
5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
-------------------------------------------------------------------------------------
4转换为二进制:0000 0000 0000 0000 0000 0000 0000 0100 所以转换为10进制结果为4
(5)(|)位或
一个操作数的的第n位与第二个操作数的第n位如果有一个是1,那么结果的第n为也为1,否则为0
package day01;public class demo {public static void main(String[] args) {System.out.println(5|3);//输出7}
}
将2个操作数和结果都转换为二进制进行比较:
5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011
-------------------------------------------------------------------------------------
1转换为二进制:0000 0000 0000 0000 0000 0000 0000 0111 所以转换为10进制结果为7
关于负数的运算:
package day01;public class demo {public static void main(String[] args) {System.out.println(-5|3);//输出-5}
}
将2个操作数和结果都转换为二进制进行比较:
-5转换为二进制:1111 1111 1111 1111 1111 1111 1111 1011 -5(补码)
3转换为二进制:0000 0000 0000 0000 0000 0000 0000 0011
-------------------------------------------------------------------------------------
-5转换为二进制:1111 1111 1111 1111 1111 1111 1111 1011 (补码)
转化为为原码:1000 0000 0000 0000 0000 0000 0000 0101 10进制数为-5
(6)(^)异或 同或
没有同或符号(很少用到)
异或运算:两个操作数相同,结果为0,不相同,结果为1
同或运算:两个操作数相同,结果为1,不相同,结果为0
package day01;public class demo {public static void main(String[] args) {System.out.println(4^5);//输出1}
}
将2个操作数和结果都转换为二进制进行比较:
5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
4转换为二进制:0000 0000 0000 0000 0000 0000 0000 0100
-------------------------------------------------------------------------------------
1转换为二进制:0000 0000 0000 0000 0000 0000 0000 0001 所以转换为10进制结果为1
关于负数的运算:
package day01;public class demo {public static void main(String[] args) {System.out.println(4^-5);//输出-1}
}
将2个操作数和结果都转换为二进制进行比较:
-5转换为二进制:1111 1111 1111 1111 1111 1111 1111 1011 -5(补码)
4转换为二进制:0000 0000 0000 0000 0000 0000 0000 0100
-------------------------------------------------------------------------------------
-1:1111 1111 1111 1111 1111 1111 1111 1111 (补码)
转化为为原码:1000 0000 0000 0000 0000 0000 0000 0001 10进制数为-1
(7)(~)位非
对补码各位取反,包括符号位
package day01;public class demo {public static void main(String[] args) {System.out.println(~5);//输出-6}
}
5转换为二进制:0000 0000 0000 0000 0000 0000 0000 0101
补码取反:1111 1111 1111 1111 1111 1111 1111 1010
转原码:1000 0000 0000 0000 0000 0000 0000 0110 转换位10进制位-6
关于负数的运算:
package day01;public class demo {public static void main(String[] args) {System.out.println(~(-5));//输出4}
}
-5的补码:1111 1111 1111 1111 1111 1111 1111 1011
补码取反:0000 0000 0000 0000 0000 0000 0000 0100 (正数补码等于原码)
转原码: 0000 0000 0000 0000 0000 0000 0000 0100 转换位10进制位4
三、时间复杂度
如何确定算法流程的总操作数量与样本数量之间的表达式关系?
1.想象该算法流程所处理的数据状况,要按照最差情况来
2.把整个流程彻底拆分为一个个基本动作,保证每个动作都是常数时间操作
3.如果数据量为N,看看基本动作的数量和N是什么关系
如何确定算法流程的时间复杂度?
当完成了表达式的建立,只要把最高阶项留下即可。低阶项都去掉,高阶项的系数也去掉。
记为:O(忽略掉系数的最高项)
例如1:888x^3+999x^2+8818x+90000 时间复杂度为:O(x^3)
例如2:
式1:8x^3+x^2+8x+1 时间复杂度为O(x^3)
式2:100万x^2+100亿x+234234234242342342 时间复杂度为O(x^2)
结果:式2的时间复杂度优于式1
时间复杂度的意义:
抹掉了常数项,低阶项,只剩下了一个最高阶项数啊
那这个东西有什么意义呐?
时间复杂度的意义就在于:当我们要处理的样本量很大很大时,我们会发现低阶项是什么不是最重要的;每一项的系数是什么也不是最重要的。真正重要的就是最高项是什么。
这就是时间复杂度的意义,它是衡量算法流程的复杂程度的一种指标,该指标只与数据量有关,与过程之外的优化无关。
算法流程的常数项:
我们会发现,时间复杂度这个指标,是忽略低阶项和所有常数系数的。难道同样时间复杂度的流程,在实际运行时候就一样的好吗?
当然不是。
时间复杂度只是一个很重要的指标而已。如果两个时间复杂度一样的算法,你还要去在时间上拼优劣,就进入到拼常数时间的阶段,简称拼常数项。
通俗说:两个算法的时间复杂度都是O(n^2),就需要考虑常数项(实现细节),这个时候就不要再去理论分析,直接申请大样本空间,实际去跑程序看看运行时间,因为再往下理论分析需要很大的功力,也没必要,因为我们每个算法流程保证只是拆分到常数时间的动作,不同的动作,虽然都是常数时间的,但也是有快慢的。比如:+ -运算的时间就是要比* /运算时间短,虽然都是固定时间但是加减就是比乘除快,而加减运算一定不如位运算快,和位运算相比差的很远,虽然都是常数运算,但就是不如位运算。所以你会发现,理论分析对个人要求实在太高了,没什么实际意义,所以应该实际去跑程序看运行时间,拼常数项。总之,还是实际运行的结果更能说服人。
常见的时间复杂度:
排名从好到差:O(1) O(logN) O(N) O(N*logN) O(N^2) O(N^3)...O(N^k) O(2^N) O(3^N)...O(K^N) O(N!)
四、选择排序
选择排序(Selection sort)是一种简单直观的排序算法。
它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
选择排序是不稳定的排序方法
选择排序的过程:
arr[0~N-1]范围上,找到最小值所在的位置,然后把最小值交换到0位置。
arr[1~N-1]范围上,找到最小值所在的位置,然后把最小值交换到1位置。
arr[2~N-1]范围上,找到最小值所在的位置,然后把最小值交换到2位置。
arr[N-1~N-1]范围上,找到最小值位置,然后把最小值交换到N-1位置。
估算:
很明显,如果arr长度为N,每一步常数操作的数量,如等差数列一般所以,总的常数操作数量=a*(N^2)+b*N+c(a、b、c都是常数)
所以选择排序的时间复杂度为O(N^2)。
动态图示:
示例代码如下:
package day01;import java.util.Arrays;public class Code01_SelectionSort {public static void selectionSort(int[] arr) {//判断数组是否为空 长度是否大于2if(arr==null||arr.length<2) {return;}//0~N-1//1~N-1//2~N-1for(int i=0;i<arr.length-1;i++) {//控制i~N-1int minIndex=i;for(int j=i+1;j<arr.length;j++) {minIndex=arr[j]<arr[minIndex]?j:minIndex;}swap(arr,i,minIndex);}}//交换操作public static void swap(int[] arr,int i,int j) {int temp=arr[i];arr[i]=arr[j];arr[j]=temp;}
}
五、冒泡排序
冒泡排序是比较基础的排序算法之一,其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
冒泡排序的过程:
在arr[0~N-1]范围上ar[0]和ar[1],谁大谁来到1位置;arr[1]和arr[2],谁大谁来到2位置…arr[N-2]和arr[N-1],谁大谁来到N-1位置
在arr[0~N-2]范围上,重复上面的过程,但最后一步是arr[N-3]和arr[N-2],谁大谁来到N-2位置
在arr[0~N-3]范围上,重复上面的过程,但最后一步是arr[N-4]和arr[N-3],谁大谁来到N-3位置
最后在arr[0~1]范围上,重复上面的过程,但最后一步是arr[0]和arr[1],谁大谁来到1位置
估算:
很明显,如果arr长度为N,每一步常数操作的数量,依然如等差数列一般所以,总的常数操作数量=a*(N^2)+b*N+c(a、b、c都是常数)
所以冒泡排序的时间复杂度为O(N^2)。
动态图示:
示例代码如下:
package day01;import java.util.Arrays;public class Code02_BubbleSort {public static void selectionSort(int[] arr) {if(arr==null||arr.length<2) {return;}//0~N-1//0~N-2//0~N-3for(int e=arr.length-1;e>0;e--) {//控制0~e//第一次 0 1 比较//第二次 1 2比较//第三次 2 3比较for(int i=0;i<e;i++) {if(arr[i]>arr[i+1]) {swap(arr,i,i+1);}}}}//交换操作 不需要中间变量进行交换 后面会将public static void swap(int[] arr,int i,int j) {arr[i]=arr[i]^arr[j];arr[j]=arr[i]^arr[j];arr[i]=arr[i]^arr[j];}
}
六、插入排序
每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
插入排序的过程:
想让arr[0~0]上有序,这个范围只有一个数,当然是有序的。
想让arr[0~1]上有序,所以从arr[1]开始往前看,如果arr[1]<arr[0],就交换。否则什么也不做。
想让arr[0~i]上有序,所以从arr[i]开始往前看,arr[]]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
最后一步,想让arr[0~N-1]上有序,arr[N-1]这个数不停向左移动,一直移动到左边的数字不再比自己大,停止移动。
估算时发现这个算法流程的复杂程度,会因为数据状况的不同而不同。你发现了吗?
如果某个算法流程的复杂程度会根据数据状况的不同而不同,那么你必须要按照最差情况来估计。很明显 在最美情况下 如果arr长度为N 括入排序的每一步常数操作的数量 还是如笔美数列一般银明业, 在取差情优下, 如来a长度为N, 插入排序的每一步常数操作的数重, 还定如等差数列一放所以,总的常数操作数量=a*(N^2)+b*N+c(a、b、c都是常数)所以插入排序排序的时间复杂度为O(N^2)。
动态图示:
示例代码如下:
package day01;public class Code01_InsertionSort {public static void insertionSort(int[] arr) {if(arr==null||arr.length<2) {return;}//0~0有序的//0~i想有序for(int i=1;i<arr.length;i++) {//控制0~i有序//arr[i]往前看,一直交换到合适位置停止//...(>=) ? > >号就行,没必要相等时,再做一次交换 //j>=0判断是否越界for(int j=i-1;j>=0&& arr[j]>arr[j+1];j--) {swap(arr,j,j+1);}}}//i和j是一个位置的话,会出错public static void swap(int[] arr,int i,int j) {arr[i]=arr[i]^arr[j];arr[j]=arr[i]^arr[j];arr[i]=arr[i]^arr[j];}
}
七、注意
1,算法的过程,和具体的语言是无关的。
2,想分析一个算法流程的时间复杂度的前提,是对该流程非常熟悉
3,一定要确保在拆分算法流程时,拆分出来的所有行为都是常数时间的操作。这意味着你写算法时,对自己的用过的每一个系统api,都非常的熟悉。否则会影响你对时间复杂度的估算。
八、额外空间复杂度
你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。
作为输入参数的空间,不算额外空间。
作为输出结果的空间,也不算额外空间。
因为这些都是必要的、和现实目标有关的。所以都不算。
但除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去。这部分空间就是额外空间。
如果你的流程只需要开辟有限几个变量,额外空间复杂度就是O(1)。
九、对数器
情景模拟
你在网上找到了某个公司的面试题,你想了好久,感觉自己会做,但是你找不到在线测试,你好心烦..
你和朋友交流面试题,你想了好久,感觉自己会做,但是你找不到在线测试,你好心烦.
你在网上做笔试,但是前几个测试用例都过了,突然一个巨大无比数据量来了,结果你的代码报错了,如此大的数据量根本看不出哪错了,你好心烦…
所以就有了对数器,有了对数器,你就不用再依赖题目的在线测试页面,你自己就可以改出万无一失的代码
那么对数器应该怎么用?
1,你想要测的方法a
2,实现复杂度不好但是容易实现的方法b(不是最后需要提交的代码,但能保证功能的正确性)
3,实现一个随机样本产生器
4,把方法a和方法b跑相同的随机样本,看看得到的结果是否一样
5,如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a和方法b
6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
随机数组产生器:
//随机数数组生成器 长度随机 值随机public static int[] generateRandomArray(int maxSize,int maxValue) {//Math.random() [0,1) 等概率的返回一个小数//Math.random()*N [0,N) 等概率的返回一个小数//(int)Math.random()*N [0,N-1] 等概率的返回一个整数 为什么是N-1呐? int向上取整,因为取整数,N取不到int[] arr=new int[(int) ((maxSize+1)*Math.random())];//长度随机for(int i=0;i<arr.length;i++) {//可做减法也可以不做,做减法只是为了产生负随机数 目的是让随机数更随机//[-N,M]arr[i]=(int)((maxValue+1)*Math.random())-(int)((maxValue+1)*Math.random());//值随机}return arr;}
复制数组:
//复制数组public static int[] copyArray(int[] arr) {if(arr==null) {return null;}int[] res=new int[arr.length];for(int i=0;i<arr.length;i++) {res[i]=arr[i];}return res;}
比较器方法:
//比较器方法,系统提供的排序方法,一定是正确的//也可以自己写一个排序方法,比如冒泡,插入等等public static void comparator(int[] arr) {Arrays.sort(arr);}
判断两数组:
//判断排序完 两个数组的长度和每个位置上的值一不一样public static boolean isEqual(int[] arr1,int[] arr2) {if((arr1 == null && arr2 != null)||(arr1 != null && arr2 == null)) {return false;}if(arr1==null && arr2==null) {return true;}if(arr1.length != arr2.length) {return false;}for(int i=0;i<arr1.length;i++) {if(arr1[i]!=arr2[i]) {return false;}}return true;}
打印数组:
public static void printArray(int[] arr) {if(arr == null) {return;}for(int i=0;i<arr.length;i++) {System.out.print(arr[i]+" ");}System.out.println();}
主方法:
public static void main(String[] args) {int testTime = 500000;int maxSize=100;int maxValue=100;boolean succeed=true;for(int i=0;i<testTime;i++) {//生成一个随机数组int[] arr1=generateRandomArray(maxSize, maxValue);//将生成的随机数组赋值给arr2int [] arr2=copyArray(arr1);//调用自己的目标排序方法selectionSort(arr1);//调用用于参考的排序方法comparator(arr2);//判断排序后两个数组是否一样if(!isEqual(arr1, arr2)) {succeed=false;//不一样 跟别打印输出 人工干预printArray(arr1);printArray(arr2);break;}}System.out.println(succeed ? "Nice":"Worst");//int[] arr=generateRandomArray(maxSize, maxValue);//printArray(arr);//selectionSort(arr);//printArray(arr);}
这不比你刷题网页的在线测试香?虽然看上去挺麻烦,但这是你一步一步稳扎稳打实现出来的,最可达的方式,看上去慢,但是最稳的最快的
十、二分法
认识二分法:
经常见到的类型是在一个有序数组上,开展二分搜索
但有序真的是所有问题求解时使用二分的必要条件吗?
不
只要能正确构建左右两侧的淘汰逻辑,你就可以二分。
常见问题:
在一个有序数组中,找某个数是否存在
在一个有序数组中,找>=某个数最左侧的位置
在一个有序数组中,找<=某个数最右侧的位置
局部最小值问题
图示说明:
示例代码如下:
package day01;public class Code04_BSExist {public static boolean exist(int[] sortedArr,int num) {if(sortedArr == null|| sortedArr.length == 0) {return false;}int L=0;int R=sortedArr.length-1;int mid=0;while(L<=R) {//mid=(L+R)/2//举个例子,因为mid是整数,当下标L和R是十几亿,几十亿,就会溢出,不安全//mid=L+(R-L)/2 这种写法不会越界溢出,但是不如位运算//N /2 ==> N>>1 因为位运算就是比除运算快//N*2 ==> N<<1//N*2+1 ==> (N<<1) | 1mid=L+((R-L)>>1);//mid=(L+R)/2if(sortedArr[mid] == num) {return true;}else if(sortedArr[mid]>num) {//左侧二分R=mid-1;}else {L=mid+1;//右侧二分}}return sortedArr[L]==num;}
}
例题一:在一个有序数组中,找>=某个数最左侧的位置
package day01;import java.util.Arrays;public class Code05_BSNearLeft {//再arr上,找满足>=value的最左位置public static int nearestIndex(int[] arr,int value) {int L=0;int R=arr.length-1;int index=-1;//记录最左的对号while(L<=R) {int mid=L+((R-L)>>1);//mid=(L+R)/2if(arr[mid] >= value) {index=mid;R=mid-1;}else {L=mid+1;}}return index;}//遍历查询最小值public static int test(int[] arr,int value) {for(int i= 0;i<arr.length;i++) {if(arr[i] >= value) {return i;}}return -1;}public static int[] generateRandomArray(int maxSize,int maxValue) {int[] arr=new int[(int) ((maxSize+1)*Math.random())];for(int i=0;i<arr.length;i++) {arr[i]=(int)((maxValue+1)*Math.random())-(int)((maxValue+1)*Math.random());}return arr;}public static void printArray(int[] arr) {if(arr == null) {return;}for(int i=0;i<arr.length;i++) {System.out.print(arr[i]+" ");}System.out.println();}public static void main(String[] args) {int testTime = 500000;int maxSize=10;int maxValue=100;boolean succeed=true;for(int i=0;i<testTime;i++) {int[] arr=generateRandomArray(maxSize, maxValue);Arrays.sort(arr);int value=(int)((maxValue+1)*Math.random())-(int)((maxValue+1)*Math.random());//printArray(arr);//System.out.println(value);//System.out.println(test(arr,value));//System.out.println(nearestIndex(arr, value));if(test(arr,value)!= nearestIndex(arr, value) ) {printArray(arr);System.out.println(value);System.out.println(test(arr,value));System.out.println(nearestIndex(arr, value));succeed=false;break;}//System.out.println("-----------------------------------");}System.out.println(succeed ? "Nice":"Worst");}
}
部分样例输出:
-55 -35 -27 -3 0 14 67
-16
3
3
-----------------------------------
-56 -4 -2 15 16 37 46 77
-9
1
1
-----------------------------------
-85 -79 -49 -12 -2 22 74
15
5
5
-----------------------------------
31
47
-1
-1
-----------------------------------
10 16 25 42 44 64
42
3
3
-----------------------------------
例题二:局部最小值问题
什么算是局部最小值:
数组第一个小于第二个时,第一个即局部最小值 0 1 2 ... 0就是局部最小值
数组最后一个小于倒数第二个时,最后一个即局部最小值 ... 100 99 99就是最小值
一个数小于左右两边的数时,这个数为局部最小值 100 99 ... 88 86 87 ...99 100 86就是最小值
问题描述:
arr[0~N-1] 无序,相邻不等
先看最左侧和最右侧有没有局部最小,没有的话在考虑中间部分
如果中间存在一个数M,M<M-1并且M<M+1;那直接返回M
如果M>M-1并且M>M+1;则0~M和M~N-1之间一定存在一个局部最小值,则左边和右边都会有局部最小值,往左往右找都可以,然后每次二分,就可以找到目标值
如果M<M-1并且M>M+1,则在M~N-1之间存在一个局部最小值,然后每次二分,就可以找到目标值
如果M>M-1并且M<M+1,则在0~M之间存在一个局部最小值,然后每次二分,就可以找到目标值
示例代码如下:
package day01;public class Code06_BSAwesome {public static int getLessIndex(int[] arr) {if(arr == null|| arr.length == 0) {return -1;}if(arr.length == 1||arr[0]<arr[1]) {return 0;}if(arr[arr.length-1]<arr[arr.length-2]) {return arr.length-1;}int left =1;int right=arr.length-2;int mid=0;while(left<right) {mid=(left+right)/2;if(arr[mid]>arr[mid-1]) {right=mid-1;}else if(arr[mid]>arr[mid+1]) {left=mid+1;}else {return mid;}}return left;}
}
十一、异或的骚操作
异或运算可以记为 无进位相加
因为是无进位相加,所以奇数个1,就是1;偶数个1,就是0,所以就会衍生出关于异或的骚操作
异或运算的性质:
0^N == N N^N == 0
异或运算满足交换律和结合律
题目一:如何不适用额外变量交换两个数
测试案例:
package day01;public class demo {public static void main(String[] args) {int[] arr= {3,1,100};System.out.println("交换前:"+arr[0]+" "+arr[2]);swap(arr,0,2);System.out.println("交换后:"+arr[0]+" "+arr[2]);}public static void swap(int[] arr,int i,int j) {arr[i]=arr[i]^arr[j];arr[j]=arr[i]^arr[j];arr[i]=arr[i]^arr[j];}
}
结果输出:
交换前:3 100
交换后:100 3
文字描述:
int arr[i]=甲;int arr[j]=乙
第一次交换:arr[i]=甲^乙 arr[j]=乙
第二次交换:arr[i]=甲^乙 arr[j]=甲^乙^乙=甲
第三次交换:arr[i]=甲^甲^乙=乙 arr[j]=甲
注:这里的arr[i]和arr[j]的值一样无所谓,但是这两个东西的内存地址不能一样
示例代码:
package day01;public class demo {public static void main(String[] args) {int[] arr= {3,1,100};System.out.println("交换前:"+arr[0]+" "+arr[0]);swap(arr,0,0);System.out.println("交换后:"+arr[0]+" "+arr[0]);}public static void swap(int[] arr,int i,int j) {arr[i]=arr[i]^arr[j];arr[j]=arr[i]^arr[j];arr[i]=arr[i]^arr[j];}
}
结果输出:
交换前:3 3
交换后:0 0
文字描述:
自己和自己异或值会变成0的
arr[0]=arr[0]^arr[0]=0
建议:平时写交换还是老老实实写正常的交换,声明一个中间变量进行交换,讲这个主要是说有这个骚操作。
题目二:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数
思路:定义一个数eor,用这个数分别和arr数组的每一个数异或一遍,最后eor是啥,则这个出现奇数次数的数就是eor 因为N^N=0 N^0=0
示例代码展示:
package day01;public class Code07_EvenTimesOddTimes {//arr中,只有一种数,出现奇数次public static void printOddtimesNum1(int[] arr) {int eor=0;for(int i=0;i<arr.length;i++) {eor^=arr[i];}System.out.println(eor);}public static void main(String[] args) {int[] arr= {1,2,3,1,1,1,1,1,2,2,2,4,4};printOddtimesNum1(arr);}
}
结果输出:
3
位运算技巧:怎么把一个int类型的数,提取出最右侧的1来(该技巧经常被使用)
改变前:0000 0000 0000 0000 1110 1100 0110 0000
改变后:0000 0000 0000 0000 0000 0000 0010 0000
怎么做呐? 就是 N&((~N)+1)
详细过程:
N: 0000 0000 0000 0000 1110 1100 0110 0000
~N: 1111 1111 1111 1111 0001 0011 1001 1111
~N+1: 1111 1111 1111 1111 0001 0011 1010 0000
N&((~N)+1):0000 0000 0000 0000 1110 1100 0010 0000
题目三:一个数组中有两种数a,b出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数
思路:定义一个数eor,用这个数分别和arr数组的每一个数异或一遍,最后eor=a^b,但是我们想分别知道a和b各自是什么,前提a和b一定是不相等的,则eor一定不等于0,则说明eor在某个位置上一定有1,假设第8位有1,则说明a的第8位和b的第8位一定是不一样的,然后将整个数组分为两大类,一类是第8位是1的数组m,一类是第8位是0的数组n,n和m中一存在第8位是1或第8位是0的数,且都是偶数,并且a和b一定分别再m和n里面,且是奇数;最后在声明一个eor'去异或数组n或m,最后eor'的值就是a或b其中一个,则eor^eor'就是另一个数
示例代码演示:
package day01;public class Code07_EvenTimesOddTimes {//arr中,有两种数,出现奇数次public static void printOddtimesNum2(int[] arr) {int eor=0;for(int i=0;i<arr.length;i++) {eor^=arr[i];}//eor=a^b//eor!=0//eor二进制数必然有一个位置上是1int rightOne=eor&(~eor +1);//提取出最右边的1int onlyOne=0;for(int i=0;i<arr.length;i++) {//为什么条件是!=0//arr[1]= 0000001011011000//rightOne=0000000000001000//不能写成==1,因为这只能说是高位上有一个1,状态不全为0//写成==0也可以,!=0选择高位是1的这一边,==0选择高位是0的这一边if((arr[i]&rightOne)!=0) {onlyOne^=arr[i];}}System.out.println(onlyOne+" "+(eor^onlyOne));}public static void main(String[] args) {int[] arr= {1,2,3,1,1,1,1,1,2,2,2,4,4,5,5,5,5,5,5,6,6,6};printOddtimesNum2(arr);}
}
结果输出:
3 6
延申:求二进制数1的个数
示例代码展示:
public static int bit1counts(int N) {int count = 0;// N = 0000 0000 0000 0000 0001 1011 0011 0101 0100//rightOne= 0000 0000 0000 0000 0000 0000 0000 0000 0100//N^rightOne = 011011001101010000//因为异或操作是 相同为0,不同为1while(N!=0) {int rightOne=N&((~N)+1);count++;N^=rightOne;//将末尾的1抹掉//N-=rightOne; 负数的1不是单纯剪掉这么简单,所以用异或}return count;}public static void main(String[] args) {int a=23542;System.out.println(bit1counts(111444));}
结果输出:
9
算法小白,不足之处,欢迎指正