找出陣列中兩數之和為指定值的所有整數對
一,問題描述
給定一個整型陣列(陣列中的元素可重複),以及一個指定的值。打印出陣列中兩數之和為指定值的 所有整數對
思路1:可以用hash表來儲存陣列中的元素,這樣我們取得一個數後,去判斷sum - val 在不在陣列中,如果在陣列中,則找到了一對二元組,它們的和為sum,該演算法的缺點就是需要用到一個hash表,增加了空間複雜度。
思路2:同樣是基於查詢,我們可以先將陣列排序,然後依次取一個數後,在陣列中用二分查詢,查詢sum -val是否存在,如果存在,則找到了一對二元組,它們的和為sum,該方法與上面的方法相比,雖然不用實現一個hash表,也沒不需要過多的空間,但是時間多了很多。排序需要O(nLogn),二分查詢需要(Logn),查詢n次,所以時間複雜度為O(nLogn)。
思路3:該方法基於第2種思路,但是進行了優化,在時間複雜度和空間複雜度是一種折中,但是演算法的簡單直觀、易於理解。首先將陣列排序,然後用兩個指向陣列的指標,一個從前往後掃描,一個從後往前掃描,記為first和last,如果 fist + last < sum 則將fist向前移動,如果fist + last > sum,則last向後移動。
二,演算法分析
一共有兩種方法來求解。方法一藉助排序,方法二採用HashSet
方法一:
先將整型陣列排序,排序之後定義兩個指標left和right。left指向已排序陣列中的第一個元素,right指向已排序陣列中的最後一個元素
將 arr[left]+arr[right]與 給定的元素比較,若前者大,right--;若前者小,left++;若相等,則找到了一對整數之和為指定值的元素。
此方法採用了排序,排序的時間複雜度為O(NlogN),排序之後掃描整個陣列求和比較的時間複雜度為O(N)。故總的時間複雜度為O(NlogN)。空間複雜度為O(1)
方法二:
依次遍歷整型陣列,對整型陣列中的每一個元素,求解它的suplement(expectedSum-arr[i]).suplement就是指定的值減去該陣列元素。
如果該元素的 suplement不在HashSet中,則將該元素新增到HashSet。
如果該元素的suplement在HashSet中,說明已經找到了一對整數之和為指定值的元素。
該方法使用了HashSet,故空間複雜度為O(N),由於只需要掃描一遍整型陣列,故時間複雜度為O(N)
三,完整程式碼實現:
import java.util.Arrays; import java.util.HashSet; public class ExpectSumOfTwoNumber { public static void expectSum_bySort(int[] arr, int expectSum) { if(arr == null || arr.length == 0) return; Arrays.sort(arr); int left = 0, right = arr.length - 1; while(left < right) { if(arr[left] + arr[right] > expectSum) right--; else if(arr[left] + arr[right] < expectSum) left++; else//equal { System.out.println(arr[left] + " + " + arr[right] + " = " + expectSum); left++; right--; } } } public static void expectSum_bySet(int[] arr, int expectSum) { if(arr == null || arr.length == 0) return; HashSet<Integer> intSets = new HashSet<Integer>(arr.length); int suplement; for (int i : arr) { suplement = expectSum - i; if(!intSets.contains(suplement)){ intSets.add(i); }else{ System.out.println(i + " + " + suplement + " = " + expectSum); } } } //hapjin test public static void main(String[] args) { int[] arr = {2,7,4,9,3}; int expectSum = 11; expectSum_bySet(arr, expectSum); System.out.println("************"); expectSum_bySort(arr, expectSum); System.out.println("----------------"); int[] arr2 = {3,7,9,1,2,8,5,6,10,5}; int expectSum2 = 10; expectSum_bySet(arr2, expectSum2); System.out.println("**********"); expectSum_bySort(arr2, expectSum2); } }
問題描述:給定一個整型的陣列,找出其中的兩個數使其和未指定的值,返回兩個數的陣列下標(假定是無序陣列,陣列元素各不相同,要求時間複雜度為O(n),n為陣列長度,可以使用輔助空間)
分析:時間複雜度是O(n),即是掃描一遍陣列,不可巢狀掃描。必須全部陣列掃描和查詢全部,掃描+查詢=O(n),可以使用資料結構雜湊表,雜湊表的查詢的時間複雜度是O(1)。
理一理程式碼思路
(1). 因為輸出是陣列下標,那麼就讓陣列下標為雜湊表中的值,陣列的值為雜湊表中的鍵,掃描一遍陣列,put進HashMap,程式碼如下(時間複雜度O(n),n為陣列大小):
HashMap<Integer, Integer> map=new HashMap<Integer, Integer>();
for(int i=0;i<nums.length;i++)
{
map.put(nums[i], i);
}
(2).第二遍掃描就是要查找出值,這裡要做兩個判斷,一個就是兩個結果數的值不能相同,這是題目要求的,還有一個就是判斷HashMap是否有值,程式碼如下(最壞情況的時間複雜度是O(n),n為陣列大小):
for(int i=0;i<nums.length;i++)
{
// 得到第二個數的值
int two=target-nums[i];
// 如果存在第二個數的陣列下標&&結果的兩個數不是同一個數的值
if(map.containsValue(two)&&target!=2*two)
{
result[0]=i;
result[1]=map.get(two);
// 返回找到的兩個數的陣列下標
return result;
}
}
整合程式碼最終如下
/**
* 使用輔助空間(使用雜湊表,時間複雜度是O(n),空間複雜度:O(n),n是陣列大小)
* @param nums
* @param target
* @return 沒有找到的話陣列中數值就是{-1,-1},否則找到,其實我想返回null的,但是覺得返回null不禮貌,因為null有毒
*/
public static int[] findTwo3(int[] nums, int target)
{
// 結果陣列
int[] result={-1,-1};
// 目標是陣列下標,所以鍵值對為<數值,數值對應陣列下標>,這裡要說一下,雜湊表的查詢的時間複雜度是O(1)
HashMap<Integer, Integer> map=new HashMap<Integer, Integer>();
// 1.掃描一遍陣列,加入雜湊表,時間複雜度是O(n)
for(int i=0;i<nums.length;i++)
{
map.put(nums[i], i);
}
// 2.第二次掃描,目標值-當前值,差值作為key,看看map裡有木有,沒有就下一個迴圈,直到陣列掃描完畢或找到value,所以最壞情況的時間複雜度是O(n)
for(int i=0;i<nums.length;i++)
{
// 得到第二個數的值
int two=target-nums[i];
// 如果存在第二個數的陣列下標&&結果的兩個數不是同一個數的值
if(map.containsValue(two)&&target!=2*two)
{
result[0]=i;
result[1]=map.get(two);
// 返回找到的兩個數的陣列下標
return result;
}
}
// 沒有找到
return result;
}
思考:假如陣列中有重複的話,那麼上面的演算法將失效,因為資料結構HashMap<Interget,Interget>已經不合適,這樣我覺得可以使用HashMap<Interget,List<Interget>>來解決。
問題描述:給定一個整型陣列,是否能找出其中的兩個數使其和為某個指定的值?(假定是無序陣列)
解法一:暴力破解(窮舉法,不提倡)
/**
* 暴力破解
* (窮舉,時間複雜度:O(n^2),正常是不會用這個滴,假如只是為了快速解題,對時間沒有限制,用這個最簡單)
*
* @param nums
* @param target
*/
public static void findTwo1(int[] nums, int target)
{
int one, two;
for (int i = 0; i < nums.length; i++)
{
one = nums[i];
two = target - one;
for (int j = 0; j < nums.length; j++)
{
if (i != j)
{
if (two == nums[j])
{
System.out.println("one:" + one + " two:" + two);
return;
}
}
}
}
System.out.println("找不到這兩個數");
}
解法二:二分法(相當於用兩個指標)
/**
* 兩個指標二分查詢
* (排序時間複雜度為O(nlog(n)),while最多O(N),所以最終程式的時間複雜度為:O(nlo(n)))
*
* @param nums
* @param target
*/
public static void findTwo2(int[] nums, int target)
{
// 1.排列(用的是Dual-Pivot Quicksort(快速排序),時間複雜度為O(nlog(n)))
Arrays.sort(nums);
// 2.類二分查詢
int left = 0;
int right = nums.length - 1;
while (left < right)
{
if (nums[left] + nums[right] > target)
{// 太大 right減少
right--;
}
else if (nums[left] + nums[right] < target)
{// 太小left增加
left++;
}
else
{// 找到結果,結束查詢
System.out.println("one:" + nums[left] + " two:" + nums[right]);
return;
}
}
System.out.println("找不到這兩個數");
}
問題描述:
設計一個類,包含如下兩個成員函式:
Save(int input) 插入一個整數到一個整數集合裡。
Test(int target) 檢查是否存在兩個數和為輸入值。如果存在著兩個數,則返回true,否則返回false
允許整數集合中存在相同值的元素分析:
與[演算法學習]給定一個整型陣列,找出兩個整數為指定整數的和(2)不同,這裡需要算出的是存不存在這兩個數,可以在上一篇的基礎上修改一下資料結構,HashMap
理一理程式碼思路
(1). 寫Save(int input)。這個就簡單了,只需判斷是否存在input為key,有就value+1,沒有就value=1。程式碼如下:
public void Save(int input)
{
int count = 0;
if (map.containsKey(input))
{
count = map.get(input);
}
map.put(input, count + 1);
}
(2). 檢查是否存在兩個數和為輸入值。上面的分析已經講得差不多,這裡就直接貼程式碼。程式碼如下:
public boolean Test(int target)
{
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext())
{
int one = iterator.next();
int two = target - one;
System.out.println("one:"+one+" two:"+two);
if (map.containsKey(two))
{
// two<<1等價於two*2
if (!(target ==two<<1 && map.get(two) == 1))
{
return true;
}
}
}
return false;
}
整合程式碼最終如下
import java.util.HashMap;
import java.util.Iterator;
public class TwoNumOfSum3
{
// key:數值,value:數值對應的個數
HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
/**
* 插入一個整數到一個整數集合裡
* @param input
*/
public void Save(int input)
{
int count = 0;
if (map.containsKey(input))
{
count = map.get(input);
}
map.put(input, count + 1);
}
/**
* 檢查是否存在兩個數和為輸入值
* @param target
* @return 如果存在著兩個數,則返回true,否則返回false
*/
public boolean Test(int target)
{
Iterator<Integer> iterator = map.keySet().iterator();
while (iterator.hasNext())
{
int one = iterator.next();
int two = target - one;
System.out.println("one:"+one+" two:"+two);
if (map.containsKey(two))
{
if (!(target ==two<<1 && map.get(two) == 1))
{
return true;
}
}
}
return false;
}
/**
* @param args
*/
public static void main(String[] args)
{
TwoNumOfSum3 t=new TwoNumOfSum3();
t.Save(5);
t.Save(10);
t.Save(4);
t.Save(7);
System.out.println(t.Test(12));
}
}
時間複雜度O(n)的解法
我們可以用一個雜湊表或陣列或bitmap(後兩者要求陣列中的整數非負)來儲存sum-x的值, 這樣我們就只需要遍歷陣列兩次即可找到和為指定值的整數對。這種方法需要O(n) 的輔助空間。如果直接用陣列或是bitmap來做,輔助空間的大小與陣列中的最大整數相關, 常常導致大量空間浪費。比如原陣列中有5個數:1億,2億,3億,4億,5億。sum為5億, 那麼我們將bitmap中的sum-x位置1,即第4億位,第3億位,第2億位,第1億位,第0位置1. 而其它位置都浪費了。
如果使用雜湊表,雖然不會有大量空間浪費,但要考慮衝突問題。
時間複雜度為O(nlogn)的解法
我們來考慮一種空間複雜度為O(1),而且實現也很簡單的演算法。首先,將陣列排序。 比如排序後得到的陣列a是:-2 -1 0 3 5 6 7 9 13 14。然後使用low和high 兩個下標指向陣列的首尾元素。如果a[low]+a[high] > sum,那麼說明a[high] 和陣列中的任何其它一個數的和都一定大於sum(因為它和最小的a[low]相加都大於sum)。 因此,a[high]不會與陣列中任何一個數相加得到sum,於是我們可以直接不要它, 即讓high向前移動一位。同樣的,如果a[low]+a[high]
< sum,那麼說明a[low] 和陣列中的任何其它一個數的和都一定小於sum(因為它和最大的a[high]相加都小於sum)。 因此,我們也可以直接不要它,讓low向前移動一位。如果a[low]+a[high]等於sum, 則輸出。當low小於high時,不斷地重複上面的操作即可。
首先要明白這道題目的考查點是什麼,
一是大家首先要對計算機原理的底層細節要清楚、要知道加減法的位運算原理和知道計算機中的算術運算會發生越界的情況,
二是要具備一定的面向物件的設計思想。
首先,計算機中用固定數量的幾個位元組來儲存的數值,所以計算機中能夠表示的數值是有一定的範圍的,為了便於講解和理解,我們先以byte 型別的整數為例,它用1個位元組進行儲存,表示的最大數值範圍為-128到+127。-1在記憶體中對應的二進位制資料為11111111,如果兩個-1相加,不考慮Java運算時的型別提升,運算後會產生進位,二進位制結果為1,11111110,由於進位後超過了byte
型別的儲存空間,所以進位部分被捨棄,即最終的結果為11111110,也就是-2,這正好利用溢位的方式實現了負數的運算。-128 在記憶體
中對應的二進位制資料為10000000,如果兩個-128相加,不考慮Java運算時的型別提升,運算後會產生進位,二進位制結果為1,00000000,由於進位後超過了byte型別的儲存空間,所以進位部分被捨棄,即最終的結果為00000000,也就是0,這樣的結果顯然不是我們期望的,這說明計算機中的算術運算是會發生越界情況的,兩個數值的運算結果不能超過計算機中的該型別的數值範圍。由於Java中涉及表示式運算時的型別自動提升,我們無法用byte型別來做演示這種問題和現象的實驗,
大家可以用下面一個使用整數做實驗的例子程式體驗一下:
int a = Integer.MAX_VALUE;
int b = Integer.MAX_VALUE;
int sum = a + b;
System.out.println(“a=”+a+”,b=”+b+”,sum=”+sum);
先不考慮long型別,由於int的正數範圍為2的31次方,表示的最大數值約等於2*1000*1000*1000,
也就是20億的大小,
所以,要實現一個一百億的計算器,我們得自己設計一個類可以用於表示很大的整數,並且提供了與另外一個整數進行加減乘除的功能,大概功能如下:
()這個類內部有兩個成員變數,一個表示符號,另一個用位元組陣列表示數值的二進位制數
()有一個構造方法,把一個包含有多位數值的字串轉換到內部的符號和位元組陣列中
()提供加減乘除的功能
- publicclass BigInteger //表示很大的整數
- {
- int sign; //識別符號號
- byte[] val; //用位元組陣列表示數值的二進位制數
- //構造方法,把一個包含有多位數值的字串轉換到內部的符號和位元組陣列中
- public Biginteger(String val)
- {
- sign = ;
- val = ;
- }
- //+-*%方法
- public BigInteger add(BigInteger other){}
- public BigInteger subtract(BigInteger other){}
- public BigInteger multiply(BigInteger other){}
- public BigInteger divide(BigInteger other){}
- }
備註:要想寫出這個類的完整程式碼,是非常複雜的,如果有興趣的話,可以參看jdk 中自帶的
java.math.BigInteger 類的原始碼。面試的人也知道誰都不可能在短時間內寫出這個類的完整程式碼的,他要的
是你是否有這方面的概念和意識,他最重要的還是考查你的能力,所以,你不要因為自己無法寫出完整的
最終結果就放棄答這道題,你要做的就是你比別人寫得多,證明你比別人強,你有這方面的思想意識就可
以了,畢竟別人可能連題目的意思都看不懂,什麼都沒寫,你要敢於答這道題,即使只答了一部分,那也
與那些什麼都不懂的人區別出來,拉開了距離,算是矮子中的高個,機會當然就屬於你了。另外,答案中
的框架程式碼也很重要,體現了一些面向物件設計的功底,特別是其中的方法命名很專業,用的英文單詞很
精準,這也是能力、經驗、專業性、英語水平等多個方面的體現,會給人留下很好的印象。