算法学习DAY01

目录

一、算法优劣的核心指标

二、常数时间操作

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

 算法小白,不足之处,欢迎指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/33728.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第27课 原理图的简介

什么是原理图&#xff1f; 原理图就是由元器件连接而成的电路图&#xff0c;它表征了你所设计电路的基本原理。 原理图上的所有元器件&#xff0c;都要从你所画好的元器件符号库中调用。元器件的信息会显示在原理图上&#xff0c;如型号、位号、特性值等。 按照我们的设计&am…

WPF/C#:数据绑定到方法

在WPF Samples中有一个关于数据绑定到方法的Demo&#xff0c;该Demo结构如下&#xff1a; 运行效果如下所示&#xff1a; 来看看是如何实现的。 先来看下MainWindow.xaml中的内容&#xff1a; <Window.Resources><ObjectDataProvider ObjectType"{x:Type local…

【2024.6.25】今日 IT之家精选新闻

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

电源集成:智能真无线耳机设计中的通信接口

真无线耳机&#xff08;TWS 耳机&#xff09;由于电池寿命更长、功能更强大、设计更吸引人以及价格更优惠&#xff0c;因此继续变得更具吸引力。随着耳机制造商专注于小型化和设计改进&#xff0c;并迅速采用功能来增强用户体验&#xff0c;他们能够在强大且竞争激烈的市场中吸…

提示缺少Microsoft Visual C++ 2019 Redistributable Package (x64)(下载)

下载地址&#xff1a;这个是官网下载地址&#xff1a;Microsoft Visual C 2019 Redistributable Package (x64) 步骤&#xff1a; 第一步&#xff1a;点开链接&#xff0c;找到下图所示的东西 第二步&#xff1a;点击保存下载 第三步&#xff1a;双击运行安装 第四步&#xf…

AI大模型企业应用实战(23)-Langchain中的Agents如何实现?

0 前言 这将所有可用的代理按照几个维度进行分类。 预期模型类型 用于聊天模型&#xff08;接收信息&#xff0c;输出信息&#xff09;或 LLM&#xff08;接收字符串&#xff0c;输出字符串&#xff09;。这主要影响所使用的提示策略。 支持聊天历史记录 这些代理类型是否…

智慧互联:Vatee万腾平台展现科技魅力

随着科技的迅猛发展&#xff0c;我们的生活正逐渐变得智能化、互联化。在这个信息爆炸的时代&#xff0c;一个名为Vatee万腾的平台正以其独特的魅力&#xff0c;引领我们走向一个更加智能的未来。 Vatee万腾&#xff0c;这个名字本身就充满了对科技未来的憧憬与期待。作为一家专…

[网络安全产品]---EDR

what EDR&#xff08;Endpoint Detection and Response&#xff0c;端点检测和响应&#xff09;,这是一种技术或者说是解决方案&#xff0c;它记录端点上的行为&#xff0c;使用数据分析和基于上下文的信息检测来发现异常和恶意活动&#xff0c;并记录有关恶意活动的数据&…

vue2 antd 开关和首页门户样式,表格合计

1.首页门户样式 如图 1.关于圆圈颜色随机设置 <a-col :span"6" v-for"(item, index) in menuList" :key"index"><divclass"circle":style"{ borderColor: randomBorderColor() }"click"toMeRouter(item)&qu…

游戏AI的创造思路-技术基础-深度学习(1)

他来了&#xff0c;他来啦&#xff0c;后面歌词忘了~~~~~ 开谈深度学习&#xff0c;填上一点小坑&#xff0c;可又再次开掘大洞 -.-b 目录 1. 定义 2. 深度学习的发展历史和典型事件 3. 深度学习常用算法 3.1. 卷积神经网络&#xff08;CNN&#xff09; 3.1.1. 算法形成过…

Redis-主从复制的准备工作-准备三台redis服务器

文章目录 1、新建三个redis配置文件&#xff0c;用于定义每个服务的专属配置1.1、复制文件redis.conf到redis安装目录下1.2、关闭redis_common.conf中的 aof 功能1.1.1、新建 redis_6379.conf1.1.2、新建 redis_6380.conf1.1.3、新建 redis_6381.conf 2、启动三个服务器2.1、后…

Electron运行报错

安装&#xff1a; npm install --save-dev electron 1&#xff1a;报错&#xff1a; electron Unable to find Electron app at 2&#xff1a; ReferenceError: require is not defined in ES module scope, you can use importinstead 在ES模块作用域中没有定义ReferenceErr…

Python重拾

1.Python标识符规则 字母&#xff0c;下划线&#xff0c;数字&#xff1b;数字不开头&#xff1b;大小写区分&#xff1b;不能用保留字&#xff08;关键字&#xff09; 2.保留字有哪些 import keyword print(keyword.kwlist)[False, None, True, and,as, assert, async, await…

2023 联邦推荐系统综述

本博客结合2023年发表的综述文章&#xff0c;对近期一些联邦推荐文章进行总结&#xff0c;综述原文&#xff1a; SUN Z, XU Y, LIU Y, et al. A Survey on Federated Recommendation Systems[J]. 2023.https://doi.org/10.48550/arXiv.2301.00767 引言 最近&#xff0c;已有许多…

【linux网络(六)】IP协议详解

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Linux从入门到精通⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学更多操作系统知识   &#x1f51d;&#x1f51d; Linux网络 1. 前言2. IP协议报…

链家房屋数据爬取与预处理-大数据采集与预处理课程设计

芜湖市链家二手房可视化平台 成品展示 重点说明 1.数据特征数量和名称、数据量 数据特征数量&#xff1a;14&#xff1b; 名称&#xff1a;小区名、价格/万、地区、房屋户型、所在楼层、建筑面积/平方米、户型结构、套内面积、建筑类型、房屋朝向、建筑结构、装修情况、梯户…

(上位机APP开发)调用华为云命令API接口给设备下发命令

一、功能说明 通过调用华为云IOT提供的命令下发API接口,实现下面界面上相同的功能。调用API接口给设备下发命令。 二、JavaScript代码 function sendUnlockCommand() {var requestUrl = "https://9bcf4cfd30.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/60…

Springboot 整合 DolphinScheduler(一):初识海豚调度

目录 一、什么是 DolphinScheduler 二、DolphinScheduler 的特性 三、DolphinScheduler 核心架构 四、单机环境部署流程 1、下载安装包 2、上传至服务器&#xff0c;解压缩 3、单机启动 4、登录 dolphinscheduler UI 5、配置数据库【非必需】 &#xff08;1&#xff…

前端:Nuxt3 + Vuetify3 + Element Plus + 添加常用插件

想要开发一个网站&#xff0c;并且支持SEO搜索&#xff0c;当然离不开我们的 Nuxt &#xff0c;那通过本篇文章让我们一起了解一下。让我们一起来构建下 Nuxt3 集成其它插件 目录 安装 Nuxt3&#xff0c;创建项目 一、搭建脚手架 二、添加 Vuetify 3 2.1、安装 Vuetify 3 …

如何将一个web端程序打包成一个pc端程序(exe文件)?

如何将一个Web端程序打包成一个PC端程序&#xff0c;例如一个可执行的EXE文件&#xff0c;是许多开发者常见的需求。下面将详细解释如何使用Nativefier工具将Web端程序打包成PC端程序的具体步骤。 目录 下载并安装Node.js验证Node.js和npm的安装安装Nativefier使用Nativefier打…