1、JVM内存的划分:![](https://img-blog.csdnimg.cn/dc0a5b7c1ebb44a8977d9962cc068495.png)
一维数组是一个引用变量。在Java中,数组属于引用数据类型,它们的变量存储的是数组对象的引用,而不是数组本身的值。
当我们声明一个一维数组时,实际上是在内存中开辟了一段连续的存储空间,用于存储数组元素的值。而数组变量本身存储的是指向该存储空间的引用,也就是数组对象的内存地址。
null在Java中表示“空引用”,也就是一个不指向对象的引用。null的作用类似于C引用中的NULL(空指针),都是表示一个无效的内存位置,因此不能对这个内存进行任何读写操作。一旦尝试,就会抛出如上错误。
平时所说的栈,其实就是指JAVA的虚拟机栈。局部变量存在栈里。堆一般用来存储对象的。
1、程序计数器(PC Register):它是JVM中一块较小的内存区域,用于保存下一条指令的地址,即正在执行的程序的行号或指令地址。当线程执行一个方法时,程序计数器会记录下当前执行的方法的地址。当线程被切换后再回来继续执行时,程序计数器就能够定位到线程上一次执行的位置,从而继续执行。
2、虚拟机栈(JVM Stack):虚拟机栈也是JVM中一块内存区域,用于存储方法的局部变量、操作数栈、动态链接、返回地址以及其他的一些信息。每当一个方法被调用时,都会在虚拟机栈上创建一个新的栈帧,用于保存这个方法的信息。当方法执行完毕后,对应的栈帧就会被销毁,释放掉它所占用的内存空间。虚拟机栈也具有一定的大小限制,当栈空间不足时,就会抛出栈溢出(StackOverflow)异常。
3、本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似,不同之处在于本地方法栈用于存储本地方法(即用其他语言编写的方法)的局部变量信息。在一些JVM实现中,本地方法栈和虚拟机栈是一起的。
4、堆(Heap):堆是JVM所管理的最大内存区域,用于存储创建的对象。所有通过new关键字创建的对象都是在堆上保存的。堆是在JVM启动时被创建,随着JVM的退出而销毁,当堆中的数据不再被引用时,会被JVM的垃圾回收机制自动清除。
5、方法区(Method Area):方法区是JVM中用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域。其中,方法编译出的字节码就是保存在这个区域中。方法区也具有一定的大小限制,当方法区空间不足时,就会抛出OutOfMemory异常。
在JVM中,这些内存区域的大小和位置都是由JVM自己进行管理的。JVM会根据需要动态地分配和回收这些内存空间,以保证应用程序能够正常运行。
二、int [ ] array1 = { 1, 2, 3, 4 }; int [ ] array2 = array1; 所以,引用可以指向引用吗?
不可以!!!
至少也应该是,引用指向了array1这个引用指向的对象。
在Java中,数组是一种特殊的对象,它是由同一类型的元素构成的有序集合。Java中所有的对象都有一个对应的类,但是数组是一个特例,它没有对应的类,而是由Java语言规范定义的一种数据结构。
数组在内存中的存储方式和普通对象非常相似,都是在堆上分配内存空间,并由垃圾回收器来管理内存。而且,数组也具有对象的一些特性,比如可以作为方法的参数和返回值,可以赋值给对象变量等等。
虽然数组没有对应的类,但是它们仍然是Java中的对象,因为它们具有对象的行为和特性。因此,在Java中,数组既不是一个基本数据类型,也不是一个类,而是一种特殊的对象类型。
三、传引用(地址),一定会改变实参吗?
对象一定是在堆上的,引用变量不一定是在栈上。
这个代码里面,你甚至可以直接
public static int[] func3(){
return new int[]{1,2,3,4};
}
对象一定是在堆上的,引用变量不一定是在栈上——这句话是在讨论Java或类似的面向对象编程语言中的内存管理。
在Java中,对象是在堆上分配内存空间的,而引用变量是指向该对象的内存地址的指针。因此,对象的生命周期由垃圾回收器管理,而引用变量的生命周期由程序控制。
引用变量可以存储在堆上,也可以存储在栈上。如果一个引用变量是一个局部变量,它将存储在调用栈中,也就是栈上。当方法结束时,这个引用变量也会被清除。
但是,如果一个引用变量是一个实例变量或静态变量,它将存储在堆上。这是因为实例变量和静态变量是与对象和类相关联的,它们的生命周期和对象和类的生命周期相同。因此,它们不能存储在栈上。
因此,引用变量不一定是在栈上,但对象一定是在堆上的。
四、借助工具类——Arrays,我们可以实现很多对数组的操作。
ctr+鼠标点击----->跳转到该方法的定义处。
在Java开发中,通常会使用一些工具类,这些工具类中包含了一些静态方法,这些方法通常是用于完成某些通用功能的,比如字符串操作、文件读写等等。
通常情况下,工具类可以被认为是一个全是静态方法的类。工具类通常用于提供一些通用的方法,这些方法可以被其他类直接调用而无需实例化工具类对象。因此,工具类中的方法通常都是静态的。
例如,Java中的Math类就是一个典型的工具类,它提供了一些数学运算相关的方法,例如sin、cos、sqrt等等。这些方法都是静态方法,可以直接通过类名调用,而无需创建Math对象。
需要注意的是,虽然工具类中的方法都是静态的,但是工具类本身并不是静态的。也就是说,我们仍然可以创建工具类的实例对象,但是这些对象通常没有实际意义,因为它们不能被用于调用工具类中的静态方法。
另外,如果一个类中只有一个静态方法,我们通常不会把它称作工具类,而是直接将该方法定义为静态方法即可。工具类通常包含多个静态方法,而且这些方法通常与某个特定的功能或领域相关联。
当你在使用这些工具类的方法时,按住Ctrl键并点击方法名时,通常会跳转到该方法的定义处。如果这个方法是你自己编写的,那么就会跳转到你编写该方法的代码处。如果这个方法是从库中导入的,那么就会跳转到库中该方法的源代码处(如果该库是包含源代码的话)。
这个功能通常被称为“跳转到定义”或“跳转到源代码”,可以让开发者方便地查看某个方法的具体实现,了解其具体功能和实现细节。
现在,让我们来自己实现一下其中的toString方法:
public static void main(String[] args) {
int [] array={1,2,3,4,5,6,7,8,9};
func4(array); //先看看人家的方法
String ret=myToString(array);
System.out.println(ret); //用我们自己的方法打印出来看看
}
public static String myToString(int[] array){
String ret="[";
for(int i=0;i<array.length;i++){
ret+=array[i];
if(i!=array.length-1) {
ret += ", ";
}
}
ret+="]";
return ret;
}
public static void func4(int[] array){
String mytoString=Arrays.toString(array);
System.out.println(mytoString);
}
五、拷贝
在JAVA中,拷贝(Copy)是指将一个对象的值复制到另一个对象中。在基础方法方面,JAVA中提供了两种拷贝方式:
1.浅拷贝(Shallow Copy):将对象的字段值复制到新对象中。如果字段的值是引用类型,则只复制引用而不复制引用指向的对象。在JAVA中,可以通过实现Cloneable接口并重写clone()方法来实现浅拷贝。
2.深拷贝(Deep Copy):将对象及其引用的对象的值全部复制到新对象中。在JAVA中,可以通过实现Serializable接口并将对象序列化再反序列化来实现深拷贝。
需要注意的是,浅拷贝只是复制了对象的引用而没有复制对象本身,因此修改新对象中引用的对象的值会影响原对象中引用的对象的值。而深拷贝会复制对象及其引用的对象的值,因此修改新对象中引用的对象的值不会影响原对象中引用的对象的值。
但是,不急,我们还没学到那么深,先来看一些基础的代码拷贝:
1、通过for循环拷贝。
public static void main(String[] args) {
int[] array5 ={1,2,4,5,32};
int [] copy=new int[array5.length];
for(int j=0;j<array5.length;j++){
copy[j]=array5[j];
System.out.print(array5[j]+" ");
}
}
在Java中,数组【】中是可以放变量的。这是有别于C语言的。
在Java中,定义数组的大小可以使用变量,只要这个变量是一个整数类型的值,且它在定义数组的时候已经被初始化了。例如,下面的代码定义了一个长度为n的整型数组:
int n = 5;
int[] arr = new int[n];
在这个例子中,变量n保存了数组的长度,它是一个整数类型的值,并且在定义数组的时候已经被初始化了。因此,可以使用变量n来定义数组的大小。
需要注意的是,由于数组在定义的时候已经确定了大小,因此在定义之后不能再改变数组的大小。如果需要动态地改变数组的大小,可以考虑使用Java中的集合类(如ArrayList)来代替数组。
注意,
int [] array1 = {1, 2, 3, 4, 5, 6 };
int [] array2=array1;
System.out.println(Arrays.toString(array2));
这个东西不是拷贝!!!
2.使用System.arraycopy()方法进行数组拷贝:
注意不要让数组越界了!!!
int[] arr1 = {1, 2, 3};
int[] arr2 = new int[arr1.length];
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
//2倍扩容
int[] arr1 = {1, 2, 3};
int[] arr2 = new int[arr1.length*2];
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
//2倍扩容
int[] arr1 = {1, 2, 3};
int[] arr2 = new int[arr1.length*2];
System.arraycopy(arr1, 0, arr2, 0, arr1.length*2);
//但是这里就不可以了,因为你拷贝不了那么长,数组就越界了!
3.使用Arrays.copyOf()方法进行数组拷贝:
int[] arr1 = {1, 2, 3};
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
//此时相当于扩容了
int[] copy = Arrays.copyOf(arr1, arr1.length*2);
//【1,3) 左闭右开
int[] copy2 = Arrays.copyOfRange(arr1, 1,3);
//能拷贝多少拷贝多少
int[] copy2 = Arrays.copyOfRange(arr1, 1,13);
public static void main(String[] args) {
func7();
}
public static void func6(int[] array){
int[] arr1 = {1, 2, 3};
int[] arr2 = new int[arr1.length];
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
}
public static void func7(){
int[] arr1 = {1, 2, 3,4,5,6};
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
for(int i=0;i<arr2.length;i++){
System.out.print(arr2[i]+" ");
}
System.out.println();
//此时相当于扩容了
int[] copy = Arrays.copyOf(arr1, arr1.length*2);
for(int i=0;i<copy.length;i++){
System.out.print(copy[i]+" ");
}
System.out.println();
//【1,3) 左闭右开
int[] copy2 = Arrays.copyOfRange(arr1, 1,3);
for(int i=0;i<copy2.length;i++){
System.out.print(copy2[i]+" ");
}
System.out.println();
//能拷贝多少拷贝多少
int[] copy3 = Arrays.copyOfRange(arr1, 1,28);
for(int i=0;i<copy3.length;i++){
System.out.print(copy3[i]+" ");
}
System.out.println();
}
重点解释一下我们最后一个代码,这里使用了Java中的Arrays.copyOfRange()方法来复制一个数组的一部分。它的第二个参数指定了复制到哪个位置结束,但是如果这个位置超过了被复制数组的长度,那么超过的部分就会被填充为该类型的默认值。在这种情况下,因为超过的部分被填充为0,所以程序不会抛出异常,并且可以打印出来。
但是需要注意的是,虽然程序不会抛出异常,但是这并不意味着这段代码是正确的。在实际编程中,我们应该避免访问数组范围之外的元素,因为这样可能会导致意料之外的结果和不可预知的行为。
4.使用ArrayList的构造函数或clone()方法进行列表拷贝:
ArrayList<String> list1 = new ArrayList<String>();
list1.add("a");
list1.add("b");
ArrayList<String> list2 = new ArrayList<String>(list1);
// 或者
ArrayList<String> list3 = (ArrayList<String>)list1.clone();
六、数组的二分查找:![](https://img-blog.csdnimg.cn/7af6401d63244fca8dbcd377e7bfd973.png)
private static int binarySearch0(int[] a, int fromIndex, int toIndex,
int key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
这个是Java提供的库方法的二分查找代码的之一,我们来实现一下自己的二分查找:
public static int binarySearch(int[] array,int key) {
int left = 0;
int right = array.length-1;
while (left <= right) {
int mid = (left+right) / 2;
if(array[mid] < key) {
left = mid+1;
}else if(array[mid] > key) {
right = mid-1;
}else {
return mid;
}
}
return -1;//没有一个下标等于负数
}
仔细观察一下,你有没有发现我们的代码和Java的代码有些不一样?
这里,它用的是“>>>”,int mid = (low + high) >>> 1;
而我们用的是“/”。
为啥呢?
在二分查找中,我们需要计算中间值,用 "(low + high) / 2" 可能会出现整数除法舍去小数的问题,导致结果偏向 low,而不是中间值。因此,我们可以使用无符号右移运算符 ">>>" 来避免这个问题,它将结果向右移动并用零填充最高位,得到一个正确的中间值。这样,我们就可以使用这个中间值进行查找。而且“位运算符”的效率更高、运算速度更快。
总之,使用无符号右移 ">>>" 能够确保二分查找算法的正确性和稳定性、高效性,而使用 "/" 会导致可能出现的错误。
还有这里: return -(low + 1);
这段代码用于处理目标元素在数组中不存在的情况。(我们都知道数组的下标是没有负数的)
具体而言,假设目标元素在已排序的数组中应该插入的位置为pos,那么当二分查找无法找到目标元素时,返回 -(pos + 1) 就可以将目标元素应该插入的位置编码为一个负数作为返回值。这个编码方式的好处是可以通过返回值的正负性和绝对值来表示查找结果的状态和位置。
在代码中,low表示已排序的数组的左端点(即最后一次low的地方),也就是下标最小的位置。因此,当二分查找无法找到目标元素时,代码返回 -(low + 1),表示目标元素应该插入的位置为 low,但由于要进行取反操作,因此返回值是负数。
由此可见,Java真的是非常的方便,很多东西都不用我们自己去实现了。这还只是面向对象的冰山一角罢了,当我们对Java了解的越深,就会越发现Java库方法和工具类的便利。Java提供了很多已经封装好的库方法和工具类,可以直接调用,避免了我们自己编写很多重复的代码。
比如,Java提供了很多字符串操作的方法,可以方便地对字符串进行处理;
Java提供了Arrays类,其中包含了很多有用的方法,比如用于排序的sort()方法、用于查找元素的binarySearch()方法等等。我们可以使用这些方法来处理数组,而不需要自己手动编写排序算法或查找算法,这样可以大大提高我们的开发效率。
此外,Java的库方法和工具类还提供了很多其他有用的功能,比如日期时间处理、正则表达式、异常处理等等。这些方法和工具类都可以让我们更加便捷地编写高质量的Java程序。
Java还提供了集合框架,可以快速地对数据进行操作和处理;
同时,Java的IO库和网络库也提供了很多方便的方法,可以用来读写文件、发送网络请求等等
。这些库方法和工具类的存在,让Java编程变得更加简单和高效,让我们可以更专注于解决问题本身,而不是处理代码细节。
七、一些练习:
(1)、/* 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值target的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。*/
public static void main(String[] args) {
int[] nums={2,7,11,15};
int target = 9;
func2(nums,target);
}
public static void func2(int [] nums,int traget){
int right=nums.length-1;
int left=0;
for(left=0;left<right;left++){
for(right=nums.length-1;right>left;right--){
if(nums[left]+nums[right]==traget){
System.out.println("[ "+left +" , " +right+" ]");
}
}
right=nums.length-1;
}
}
在这里我一开始犯了一个错误,就是初始化的时候先把right初始化为了0,这导致了一个问题,就是它连第一层循环都进不去!所以大家也要警醒,最好在一开始赋初值的时候就赋上正确的值。
或者,你也可以这样写:
public static int[] func2(int[] array,int key) {
for (int i = 0; i < array.length-1; i++) {
for (int j = i+1; j < array.length; j++) {
if(array[i] + array[j] == key) {
return new int[]{i,j}; //没有名字的数组
}
}
}
return new int[]{-1,-1}; //没有名字的数组
}
这里是可以返回一个数组的,如果没有匹配的,就返回-1,因为不存在负数下标。
注意,这里的循环为啥要从j=i+1开始?因为这样就可以避免target=4,输出[ 0 , 0 ]了(即i和j可能代表同意个元素的情况)。
(2)、/* 给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 【 n/2 】 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。*/
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
String input = scan.nextLine();
// 将字符串类型的数组转换为整型数组
String[] numsStr = input.split(" ");
int[] nums = new int[numsStr.length];
for (int i = 0; i < numsStr.length; i++) {
nums[i] = Integer.parseInt(numsStr[i]);
}
func4(nums);
}
public static void func4(int[] arr){
int right=arr.length-1;
int left=0;
int count=0;
int con=arr[left];
for(left=0;left<arr.length;left++){
for(right=arr.length-1;right>=0&&right!=left;right--){
if(arr[left]==arr[right]){
count++;
}
}
if(count<=arr.length/2&&left!=arr.length-1){
con=arr[left+1];
count=0;
}
right=arr.length-1;
}
if(count>=arr.length/2){
System.out.println(con);
}
else {
System.out.println("不存在多数元素");
}
}
上面是我自己写的代码,但是好像时间复杂度太大了,因为它有两个循环,当数组元素多的时候,计算量就会变得太大了。下面是另一种实现方法:
public static void func4(int[] arr) {
int candidate = 0;
int count = 0;
for (int num : arr) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
System.out.println(candidate);
}
上面这种算法的思想是使用一个候选元素和一个计数器来遍历整个数组,如果当前数字等于候选元素,则将计数器加1,否则将计数器减1。当计数器归零时,重置候选元素为当前数字,并将计数器设为1。在遍历完成后,如果候选元素出现的次数大于数组长度的一半,则该元素即为多数元素。
这个算法的时间复杂度为 O(n),空间复杂度为 O(1)。
然而,该算法的正确性仅在假设多数元素确实存在的情况下成立。如果数组中没有多数元素,该算法会错误地输出一个元素作为多数元素,因为它没有对多数元素是否存在进行验证。因此,在使用该算法时,需要先确保数组中确实存在多数元素。
这个题还有一个更容易想的解法!就是先给整个数组进行排序,位于中间的那个数字一定是我们要找的数字,因为题目有这样一句前提:"你可以假设数组是非空的,并且给定的数组总是存在多数元素。"
据此,我们还可以想到另一个思路:一个数组:{ 1 ,2,2,1,2,2,2} 的多数元素是2,那么运用抵消的想法,可以让一个2和一个1抵消,这样,最后剩着的那个数就是多数元素。
(3)、 /* 给你一个整数数组 arr,请你判断数组中是否存在连续三个元素都是奇数的情况:如果存在,请返回 true ;否则,返回 false 。
示例 1:
输入:arr = [2,6,4,1]
输出:false
解释:不存在连续三个元素都是奇数的情况。
示例 2:
输入:arr = [1,2,34,3,4,5,7,23,12]
输出:true
解释:存在连续三个元素都是奇数的情况,即 [5,7,23] 。*/
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
String input = scan.nextLine();
// 将字符串类型的数组转换为整型数组
String[] numsStr = input.split(" ");
int[] nums = new int[numsStr.length];
for (int i = 0; i < numsStr.length; i++) {
nums[i] = Integer.parseInt(numsStr[i]);
}
boolean con = func5(nums);
System.out.println(con);
}
public static boolean func5(int[] arr) {
int i=0;
for(i=0;i<arr.length-2;){
if(arr[i]%2==1&&arr[i+1]%2==1){
if(arr[i+2]%2==1){
return true;
}
else {
i+=3;
}
}
else {
i++;
}
}
return false;
}
}
这里一定要注意的就是数组下标的越界问题!!!
也可以这样:
public static boolean func3(int[] array) {
int count = 0;
for (int i = 0; i < array.length; i++) {
if(array[i] % 2 != 0) {
count++;
if(count == 3) {
return true;
}
}else {
count = 0;
}
}
return false;
}
(4)、调整数组顺序使得奇数位于偶数之前。调整之后,不关心大小顺序。
如数组:[1,2,3,4,5,6]
调整后可能是:[1, 5, 3, 4, 2, 6]
这是我一开始写的代码,是有问题的,你能发现问题出在哪里吗?
public static void main(String[] args) {
int[] array={1,2,3,4,5,6};
func1(array);
}
/*调整数组顺序使得奇数位于偶数之前。调整之后,不关心大小顺序。
如数组:[1,2,3,4,5,6]
调整后可能是:[1, 5, 3, 4, 2, 6]*/
public static void func1(int[] array){
int left =0;
int right=array.length-1;
while(left<right){
if(array[left]%2==0&&array[right]%2!=0){
int temp=array[left];
array[left]=array[right];
array[right]=temp;
}
left++;
right--;
}
for(int j=0;j<array.length;j++){
System.out.print(array[j]+" ");
}
}
如果一个数组全是奇数,那么left就一直++吗?循环着循环着,它很快就会越界不是吗?
所以我们还是应该给每个循环都加一个前提条件:
public static void func1(int[] array) {
int left = 0;
int right = array.length-1;
while (left < right) {
while (left < right && array[left] % 2 != 0) {
left++;
}
while (left < right && array[right] % 2 == 0) {
right--;
}
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
}
public static void main1(String[] args) {
int[] array = {2,4,6,8,7};
func1(array);
System.out.println(Arrays.toString(array));
}
博主的一个同学写了这样的一段代码,也可以实现这个问题,思路比较清奇,可以借鉴一下,类似于冒泡排序的样子(count 标记未交换的偶数,遇见奇数,和偶数交换):
public static void sortArr(int[] arr) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] % 2 == 1) {
count ++;
int tmp = arr[i];
arr[i] = arr[count -1];
arr[count -1] = tmp;
}
}
}
(5)/* 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。*/
public static void main(String[] args) {
int [] arr=new int[]{1,2,3,4,5,6,5,4,3,2,1};
func3(arr);
}
public static void func3(int [] arr){
int right=arr.length-1;
int left=0;
for(left=0;left<right;left++){
for(right=arr.length-1;right>left;right--){
if(arr[left]!=0&&arr[right]!=0&&arr[left]==arr[right]){
arr[left]=0;
arr[right]=0;
break;
}
}
right=arr.length-1;
}
for(int i=0;i<=arr.length-1;i++){
if(arr[i]!=0)
System.out.println(arr[i]);
}
}
还有一种用“ ^ ”操作符做的方法,更简单:
public static int find(int[] array) {
int ret = array[0]; //或者,ret=0也可以,但是注意,不可以是别的数字!!!
for (int i = 1; i < array.length; i++) {
ret ^= array[i];
}
return ret;
}
这个方法妙在何处呢?
大家知道,相同的数互相异或,得到的结果时0,0^一个不为0的数,结果是那个数本身。现在,题目说只有一个”单身狗“,那么我们用^来解决简直不要太简单!
那么如果是一组数字,里面有两个只出现了一次的数字,要我们找出来怎么办?
分组!
怎么分?
比如是这个数组:{ 2 ,2 ,5,3,6,5,6,4},其中,3和4都只出现了一次,是“同病相怜的两只单身狗”,那么我们就可以根据3和4的不同,把它们分到两个不同组中,再每个组分别" ^ " 。
3的二进制序列:0011
4的二进制序列:0100
3 ^ 4 :0111
找到对应位不同的位数(3和4异或结果为1的位数)来划分组别。
真是巧妙的解法!