目录

实现思路

代码展示

代码讲解

实现思路

思路一

思路二

代码展示


两元素之和实现思路

数组中两元素之和(简称两数之和)到底讲的是什么东西呢?它讲的是,在一个数组中,寻找两个元素的和,该和等于目标数字,并返回两元素索引。比如数组{1,4,2,3},我们的目标数字是7(寻找7)。那很显然,4+3=7,那么就返回{1,3},这分别是元素4和3在数组中的索引。

我们现在知道了它是什么东西,那该如何实现它呢?我们考虑使用最简单直接的方法:暴力组合。该方法的思想是,把数组里面所有元素都组合一遍,看看哪个和等于目标数字。比如在上面这个数组中我们尝试:1+4等于7么?不等于,就尝试下一个组合。1+2!=7,1+3!=7,4+2!=7,4+3==7。于是返回4,3在数组中的索引。如果找遍了数组都没有找到正确的组合,就说明目标数字根本不以两数之和的形式存在于数组中,于是返回0和0,表示没组合到。

注意,这里是组合,不是排列,如果在代码中写的是排列的话,虽然对结果没影响,但是程序要花更多无用功,浪费了电脑资源。比如,如果使用排列的方式的话,以上过程在代码中就会变成:1+4!=7,1+2!=7,1+3!=7,4+1!=7,4+2!=7,4+3==7。足足多用了一次判断!不要看这里只有一次,当数组变大之后,冗余的判断会呈倍数增长。因此为了避免这种浪费,我们使用组合。


代码展示

package advance.algorithm;

public class SumOfTwo {
	public int sum = 0; 

	public int[] twoSum(int[] arr,int target) {
		for (int i = 0; i < arr.length - 1; i++) {
			for (int j = i + 1; j < arr.length; j++) {
				this.sum = arr[i] + arr[j];//尝试两数组合
				if (this.sum == target) {
					int[] result={i,j};
					return result;//返回这两个数的索引
				}
			}
		}return new int[] {0,0};//配对失败的返回值
	}
	//测试
	public static void main(String[] args) {
		SumOfTwo obj=new SumOfTwo();
		int[] arr = { 1, 3, 5, 7 };//原始数组
		int[] R=obj.twoSum(arr,12);
		for(int e:R)//循环打印数组R里的元素
			System.out.print(String.format("index %d; ", e));
	}
}

代码讲解

我们说说如何才能让程序执行“组合”而不是排列。在上段代码中,我们看到内嵌的for循环中,循环变量j范围是从i+1到arr.length。我们这次用上段代码中的数组arr={1,3,5,7}作例子。我们的目标数字是12。

如果循环变量j的范围是从0到arr.length,那么这就是排列。过程是这样的:arr[0]+arr[1]!=12, arr[0]+arr[2]!=12, arr[0]+arr[3]!=12, arr[1]+arr[0]!=12, arr[1]+arr[1]!=12, arr[1]+arr[2]!=12, arr[1]+arr[3]!=12, arr[2]+arr[0]!=12, arr[2]+arr[1]!=12, arr[2]+arr[2]!=12, arr[2]+arr[3]==12。

足足有五次冗余!不过如果按照范围从i+1-arr.length的话,就没有这五个冗余判断,大大节省了计算机算力。


寻找单个元素实现思路

这个算法是什么呢?这个算法可以在一个数组中寻找到落单的数。比如这里有数组{4,1,4,6,1},显然,这里落单的数是6,因此该算法最后返回值为6

这里提供了两种实现思路,第一种比较代码量比较大,但是结构很稳固。第二种你没有听错,只有一行!这是纯数学思路,通过异或的特性来完成计算。

思路一

我们试着理解第一种思路。首先我们把数组里面的元素从小到大依次排列,这样相同的数字就会紧挨在一起。于是我们基本的想法就有了:如果两个相邻的数字相等,计数器就加1,表示他不是一个落单的数字。如果两个数不相等,我们就先看计数器是否为0,如果为0,就说明这个数是落单的数,我们返回它就好了;如果不为0,说明这个数的上个数和他相等,因此我们就把计数器重置为0,开始下一个计数。

我们用上面数组演示一下这个思路。在数组从小到大排序后,数组变成了:arr={1,1,4,4,6}。arr[0]==arr[1], cnt+=1; arr[1]!=arr[2], cnt!=0, so cnt=0. arr[2]==arr[3], cnt+=1; arr[3]!=arr[4], cnt!=0, so cnt=0。

这里我们遇到了一个小问题。arr[4]之后就没有元素了,无法继续进行比较。因此,我们要考虑另外一种情况,即落单的数字在数组末尾的情况。我们设立一个判断,如果一个数不等于他的下一个数,且下一个数处在数组的最大索引,那我们就直接返回下一个数,不需要再去进行下一次比较。

当然另还有一种情况。如果数组中只有一个元素,我们就无法使用上述方法,所以我们要在这个方法之前新建一个判断:如果数组长度等于1,那么就直接返回数组中唯一的元素。

思路二

我们现在看一下第二种方法,异或逻辑运算(exclusive or)。什么是异或呢?那我们要先了解一下与其相关的逻辑运算符,他们是:和(and),或(or)以及非(not),都属于布尔代数的范畴,也就是说我们接下来的演示用到的是二进制数。我们用1表示True,0表示False。

和:1 and 1=1, 1 and 0=0, 0 and 0=0 

或:1 or 1=1, 1 or 0=1, 0 or 0=0

非:not 1=0, not 0=1(一元运算符)

异或:1 xor 1=0, 1 xor 0=1, 0 xor 0= 0

异或也叫半加运算,它和或逻辑运算符比较相似,不过就如他的英文名一样,只有在一个1(True)一个0(False)的情况下才会返回1(True),其他情况都返回0(False)。

很多读者到这里可能就不太理解:数组中的数都是十进制的啊,这个是True和False,好像八竿子打不着关系呢。true和false可以表示为1,0,他们可以表示所有二进制数,而二进制数又能转化成十进制数,所以异或处理的数的类型并不重要,因为他们可以互相转换,同理,我们也可以使用代数来演示逻辑运算。我们现在来看一下异或满足的运算律。

交换律:a xor b= b xor a

归零律:a xor a=0

恒等律:a xor 0=a

结合律:a xor (b xor c)= (a xor b) xor c

自反律:a xor b xor a=b(可以根据交换律,归零律和恒等律推出)

其实对于以上运算律大家可以自己代数进去,都是可以成立的。利用以上运算律,我们就可以弄懂那一行代码了。我们还是拿数组{4,1,4,6,1}做例子。根据自反律,我们知道前面三个元素异或运算的结果是1,那么我们再作一次自反,这样我们就得到6。当然,这些运算律只是便于我们人类计算和理解而已,计算机实际上不会用这些,它就是一个一个数去老老实实地去算。


代码展示

package advance.algorithm;

import java.util.Arrays;

public class SingleNumber {
	public static int solution1(int[] arr) {//more robust than solution2 but much more complicated
		Arrays.sort(arr);
		int len=arr.length;
		if(len==1)
			return arr[0];
		int cnt=0;
		for(int i=0;i<len;i++) {
			if (arr[i]==arr[i+1]) 
				cnt++;
			else if(arr[i]!=arr[i+1]) {
				if(cnt==0) {
					return arr[i];
				}else if(i+1==len-1) {
					return arr[i+1];
				}else {
					cnt=0;
				}
			}
		}return 0;
	}

	public static int solution2(int[] arr) {
		int single=0;for(int e:arr)single^=e;return single;
	}
	
	public static void main(String[] args) {
		int[] arr= {2,2,1,4,4,1,5};
		System.out.println(solution1(arr));
		//System.out.print(solution2(arr));
	}
}