题目:不重复打印排序数组中相加和为给定值的所有三元组
分析
本题题目并无太多文字游戏,简单来说就是寻找一个排好序数组中的三个不同位置的数字满足三数之和等于给定值。
解法1:普通遍历
正如同题目所说既然我们是要寻找三个数字,并且三个数字满足三数之和等于给定值,因此暴力求解就完事了
public void findThreeNumbers(int value, int[] a) {
for (int i = 0; i < a.length - 2; i++)
for (int j = i + 1; j < a.length - 1; j++)
for (int k = j + 1; k < a.length; k++) {
if (a[i] + a[k] + a[k] == value) {
System.out.println(a[i] + " " + a[j] + " " + a[k]);
}
}
}
分析
嗯,好像没什么问题,很舒服的写完了,这段代码除了会报错和时间复杂度高一点似乎别的就没什么问题了。但是作为算法题,可是不能容忍的,我们再想想为什么会报错。首先,题目给定是排序数组这一条件我们没有用上,再者如果数组有重复元素呢?比如
a=[1,1,2,2,3,3],k=6
这种情况下你可以想一下你会打印多少次重复组合,你也可以这样写完之后再进行过滤一遍(例如用上hashset等等),但是感觉好麻烦。因此我们先解决重复输出的问题
public static void findThreeNumbers(int value, int[] a) {
for (int i = 0; i < a.length - 2; i++) {
if (i == 0 || a[i] != a[i - 1]) {
for (int j = i + 1; j < a.length - 1; j++) {
if (j == i + 1 || a[j] != a[j - 1]) {
for (int k = j + 1; k < a.length; k++) {
if ((a[k]==j+1||a[k]!=a[k-1])&&a[i] + a[j] + a[k] == value) {
System.out.println(a[i] + " " + a[j] + " " + a[k]);
}
}
}
}
}
}
}
额,写的好多,你们看吧反正我是不会看了,运行好像没问题(不要在意这些细节)。就先认为我们解决了重复数值的问题,好的那么最终总结如下
时间复杂度O(n3),空间复杂度O(1),如果是具体的算法题的话整个题目的得分情况不会超过50%。
是的,就是这么残忍,你暴力了算法,算法也会暴力你。因此你的得分不会高于一半,就是这么真实。
解法2:利用排序数组特征进行优化
我们在上一步采用了暴力的手段达到目的,你有没有记得我好像偶然提到排序数组这一条件我们没有用上。确实,排序数组这一条件可以说是十分强大,在一些的算法题目中排序和未排序是两种截然不同的方法。你可能想到我可以手动排序,但是当数字范围或者数字特征不明确的时候一旦手动排序就意味着你的算法时间复杂度至少是O(n*logn)。好了不废话了,我们看一下借助排序数组怎么优化算法性能。首先贴上代码
public class Solution {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
//读取输入
int length=scanner.nextInt();
int k=scanner.nextInt();
int[] array=new int[length];
for(int i=0;i<length;i++)
array[i]=scanner.nextInt();
scanner.close();
Test.findThreeNumbers(k, array);
}
public static void fun1(int k,int[] array) {
if(array==null||array.length<3)return ;
for(int i=0;i<array.length-2;i++) {
//寻找不同的数字选座第一个元素
if(i==0||array[i]!=array[i-1]) {
fun2(k-array[i], array, i, i+1, array.length-1);
}
}
}
public static void fun2(int k,int[]array,int before,int start,int end) {
int l=start;
int r=end;
//左右指针不能相遇
while(l<r) {
//左右夹击
if(array[l]+array[r]<k)l++;
else if (array[l]+array[r]>k) r--;
else {
//找到了不同的组合,打印出来
if(l==before+1||array[l]!=array[l-1]) {
System.out.println(array[before]+" "+array[l]+" "+array[r]);
}
l++;
r--;
}
}
}
借助代码我们一点点讨论,首先是三个函数第一个是主函数,主要用作接受输入,是整个程序的框架。第二个函数就是按照位置顺序选取数值假设选取的是i,然后在剩下的数组中选取两个数字且两个数字之和为k-i。第三个函数就是执行在数组中选取值为k-i的两个数字
总的来说,改进后算法整体思路为先从数组中选取一个指定元素(假设元素值为i),之后再在剩余数组中选取和为k-i的两个元素(k为给定值)
介绍完思路,接下来我们就主要围绕第三个函数做介绍,即如何从剩余数组中选取和为k的两个数字(不允许暴力破解!)。
数组是有序的,也就是说从左往右是逐渐增大,从右往左是值逐渐减少(这不是废话吗( ̄_ ̄ ))。我们使用两个指针(可以是下标,指针主要是表达方便),分别指向第一个元素和最后一个元素(简称左指针和右指针)。也就是说,我们用两个指针指向了最大值和最小值,并且任意数组中两元素之和我们都可以在遍历一遍数组的情况下获得。就是因为这个神奇的效果,因此我们在找和为给定值的两个元素只需要O(n)的时间复杂度。这里我要啰嗦一下为什么暴力破解的O(n2)可以变为O(n)。暴力破解是选取一个元素后,剩下的元素再逐一与选定的元素相加。但是这个过程中我们忽略了数组本身的特征
假设有a=[1,2,3,4,5,6],求和为6的两个元素
简单的题目如上,按照暴力破解我们先选取1,然后与2,3,4,5,6逐渐相加,再选择2 与3,4,5,6相加,再选择3…。但是我们发现实际上到3的时候再与后面数字相加则结果必定会大于6,但程序还是和你一样傻傻的执行直到选择到6然后退出。但是选择了双指针之后,我们可以发现1和6相加大于6那么也就是可以知道1后面的任何元素和6相加都不可能等于6了,也就是说我们可以将数据的检索范围缩小1~5。那么当我们看到1+5=6的时候,是要动一动了,因为在剩下的区间中仍然可能存在和为6的两个元素,其实也就是左边向前走一步,右边向左走一步。为什么?
我们已经知道当前位置的元素之和等于6,那么1之后的左指针指向的元素再和当前右指针指向的元素相加结果一定会大于6,因此右指针向左前进。其他情况下,当两个数字之和大于6的时候则将右指针向左挪动(因为从右到左数值减小),两数之和小于6的时候将让左指针向右走一步(数组从左到右数值增大)。
好了,具体流程基本上说完了,最后讲一下终止条件和去重操作。终止条件很简单,左右两个指针不能相遇(相遇了不就带来重复搜索了吗)。去重呢?这里我们知道数据是排好序的,那么重复值一定会报团取暖。具体做法为选定第一个元素时候我们判断他和前一个元素是否相同(记得排除第一个位置),在剩下数组选取第二和第三个元素时候记得将第二个元素和第三个元素判断是否与之前元素相同。好了,整个算法到此结束。
这里啰嗦好多,时间上只需要查一下书或者上网找一下三数之和问题都会有相关解答。我这里主要想表达的就是利用双指针这一个过程,双指针的使用在很多题目中都有发挥,尤其在链表和数组中。我们在学习的也是不同的思维过程和工具,或者这就是学习算法的收获吧。