目录
一、认识时间复杂度
常数时间的操作:一个操作如果合样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(常读bigO)来表示。具体来说,先要对一个算法流程肥肠熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下实际运行时间,也就是“常数项时间”。
如上图,选择排序:第一次看了N次,比较了N次,交换了1次;第二次看了N-1次,比较了N-1次,交换了1次。。。一共看了N+(N-1)+(N-2)+...+1,比较N+(N-1)+(N-2)+...+1,交换N次。最终加合是aN^2+bN+c,所以上述操作的时间复杂度是O(N^2)。
package class01;
public class Test{
public static void process1(){
int N=1000;
int a=0;
for(int i=0;i<N;i++){
a=2+5;
a=4+7;
a=6*8;
}
}
public static void process2(){
int N=1000;
int a=0;
for(int i=0;i<N;i++){
a=3|6;
a=3&4;
a=4^785;
}
}
}
上述代码理论实践一样的,评价算法的好坏就要比较实际运行时间。
二、排序算法
1.选择排序
时间复杂度为O(N^2),额外空间复杂度O(1)
public static void selectionSort(int[] arr) {
if(arr==null||arr.length<2) {
return;
}
for(int i=0;i<arr.length-1;i++) { // i~N-1
int minIndex=i;
for(int j=i+1;j<arr.length;j++) { // i~N-1上找最小值的下标
minIndex=arr[j]<arr[minIndex]?j:minIndex;
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
2.冒泡排序
时间复杂度为O(N^2),额外空间复杂度O(1)
public static void bubbleSort(int[] arr) {
if(arr==null||arr.length<2) {
return;
}
for(int e=arr.length-1;e>0;e--) {
for(int i=0;i<e;i++) {
if(arr[i]>arr[i+1]) {
swap(arr,i,i+1);
}
}
}
}
//交换arr的i和j位置上的值
public static void swap(int[] arr, int i, int j) {
if(i!=j){
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
}
异或运算可以理解为无进位相加:(0+1=1, 1+1=0(无进位) 0+0=0)
解释通过三次异或交换a和b:
假设a=甲,b=乙
a=a^b // a=甲^乙
b=a^b // b=甲^乙^乙=甲^0=甲
a=a^b // a=甲^乙^甲=甲^甲^乙=0^乙=乙
PS:一定要保证a和b是不一样的,如果一样,最终a=0, b=0
3.插入排序
选择排序和冒泡排序,数据状况对它们不会产生太大的影响,但是对于插入排序,会产生较大的影响。
时间复杂度O(N^2),额外空间复杂度O(1)
public static void insertionSort(int[] arr) {
if(arr==null||arr.length<2) {
return;
}
for(int i=1;i<arr.length;i++) {
for(int j=i-1;j>=0&&arr[j]>arr[j+1];j--) {
swap(arr,j,j+1);
}
}
}
//交换arr的i和j位置上的值
public static void swap(int[] arr, int i, int j) {
if(i!=j) {
arr[i]=arr[i]^arr[j];
arr[j]=arr[i]^arr[j];
arr[i]=arr[i]^arr[j];
}
}
4.二分法的详解与扩展
4.1 在一个有序数组中,找某个数是否存在
想要寻找num是否存在:
每次枚举区间中间的数x,然后和num比较
若x<num,则下次在x的右半区间找
若x>num,则下次在x的左半区间找
若x==num,则找到返回
时间复杂度O(logN)
4.2 在一个有序数组中,找>=某个数最左侧的位置
找>=num最左侧位置也可以使用二分
每次枚举区间中间的数x,然后和num比较
若x<num,则下次在x的右半区间找 => l=mid+1
若x>=num,则下次在x的左半区间找 => r=mid
同理可以二分找>=num最右侧位置
4.3 局部最小值问题(数组元素相邻位置一定不相等)
局部最小:
对于0位置的数,只要小于1位置的数即为局部最小
对于N-1位置的数,只要小于N-2位置的数即为局部最小
对于其他i位置,需要同时小于i-1和i+1位置的数才为局部最小
每次枚举区间中间位置M,然后判断M位置情况
若M为局部最小,则直接返回
若arr[M-1] < arr[M],则在左半区间找
若arr[M] > arr[M+1],则在右半区间找
左边向下进入,右边向上出来,相邻的元素不同,则在区间中一定存在拐点(局部最小)
使用二分的情况:排查性的东西可以二分,左右两侧存在差异性,左边一定有,则可以甩掉右侧。
5.异或运算的性质与扩展
异或拓展:同一个数异或奇数次,最终结果为非零;同一个数异或偶数次,最终结果为零。
面试题1:给定一个数组arr,在其中有且仅有一个数出现了奇数次,其它出现偶数次,要求在O(N)时间内找出这个数。
将数组的每个数异或在一起,由于偶数次的数异或相当于异或0,奇数次的数异或相当于异或该数,本数组只有一个数出现奇数次,所以,最终异或的结果就是这个出现奇数次的数。
给出代码如下:
public static void printOddTimesNum1(int[] arr) {
int eor=0;
for(int cur:arr) {
eor^=eor;
}
System.out.println(eor);
}
面试题2:给定一个数组arr,在其中有且仅有两个数出现了奇数次,其它出现偶数次,要求在O(N)时间内找出这两个数。
0
假设数组中出现奇数次的两个数分别为a和b。
eor是将所有数异或在一起的结果为a^b。
由于eor不等于0,所以一定存在某位为1,则可以根据该为是否为1,分为两类。
将其中任意一类中的所有数异或,others在该位的出现1或0一定为偶数次(那么筛选出来也一定是偶数个),所以一定不影响结果,结果为a或者b,记作eor'。
则最终出现奇数次的两个数为eor'和(eor^eor')
提取出某个数最右边的1:eor&(~eor+1)
给出代码如下:
public static void printOddTimesNum2(int[] arr) {
int eor=0, onlyOne=0;
for(int curNum:arr) {
eor^=curNum;
}
// eor=a^b
// eor!=0
// eor必然有一个位置是1
int rightOne=eor&(~eor+1); // 提取出最右的1
for(int cur :arr) {
if((cur&rightOne)==0) {
onlyOne^=cur;
}
}
System.out.println(onlyOne+" "+(eor^onlyOne));
}
6.对数器的概念与使用
可以用随机样本产生器产生测试用例,在a和b分别跑一遍,然后比较结果是否相等,来保证算法的正确性。
//for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
// Math.random() -> [0,1)所有小数,等概率返回一个
// Math.random()*N -> [0,N)所有小数,等概率返回一个
// (int)(Math.random()*N) -> [0,N-1]所有整数,等概率返回一个
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*Math.random());
}
return arr;
}
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);
int[] arr2=copyArray(arr1);
SelectionSort(arr1);
comparator(arr2);
if(!isEqual(arr1, arr2)) {
succeed=false;
break;
}
System.out.println(succeed?"Nice!":"Fucking fucked!");
}
}
对于代码测试500000次,可以测试算法的正确性。generateRandomArray(),copyArray(),comparator(),isEqual()方法都是自己写的。
generateRandomArray(maxSize, maxValue)
产生长度不大于maxSize,元素在(-maxValue,maxValue)之间的随机数数组。
copyArray(int[] arr)
简单复制数组元素
comparator(int[] arr)
另一个算法来跑样例
isEqual(int[] arr1, int[] arr2)
比较两个结果是否一样