這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰前端
咱們來看一題很常見的求和數組問題: 給定一個整數數組nums和一個目標值target,請你在該數組中找出和爲目標值的那兩個整數,並返回他們的數組下標。算法
像上面這道題目,咱們在作算法的時候常常碰到,那麼咱們日常會用什麼方法去解呢?數組
也就是咱們能想到的最直接的算法,是什麼呢?沒錯,他就是"雙循環"(哭出聲,沒學前腦子裏真的只有循環再循環),讓咱們用代碼來實現一下這個函數:markdown
function getTarget(nums, target) {
// 空數組就返回
if(!nums) {
return
}
// 對數組進行兩次遍歷
for(let i=0; i < nums.length; i++) {
for(let j=0; j< nums.length;j++) {
// 首先要排除同一個元素兩次相加,再者兩個數相加爲目標值
if(i !== j && nums[i] + nums[j] === target) {
// 返回一個數組,分別是第一個元素,第二個元素,第一個下標, 第二個下標
let arr = [nums[i],nums[j],i,j]
console.log(arr);
return arr
}
}
}
}
getTarget([2,3,2,11], 4)
// [2, 2, 0, 2]
複製代碼
上面的例子可能有沒考慮所有的狀況,就是展現一下這個思路,這種解法的優勢在於,不用考慮不少,就是一個一個算過去,可是這也形成了時間複雜度的提高,通過上一篇算法的數據結構基礎的學習,咱們都知道,暴力解法的時間複雜度是爲O(n^2)的,那隨着n的變化,極可能形成超時,那有沒有辦法解決這個問題呢?那麼咱們考慮,是否是能夠經過空間複雜度來下降時間複雜度呢?答案天然是能夠的,那麼下面咱們就來介紹這種思路。數據結構
這裏的Map實際是一個對象,這個對象存儲了對應元素和下標,也就是將數組中的元素變成了對象,key是數組元素的值,value是數組元素的下標,那麼到底怎麼使用呢?仍是使用上面的例子:函數
首先咱們定義一個空對象Map,當咱們查找一個元素時,假如target-nums[i]
不可以在對象的key中找到,那麼就將{nums[i]:i}
存入對象中,若是能找到,那麼咱們就能夠返回所須要的數和下標了,看下面這張圖👇:post
所以,咱們能夠用代碼將上面的思路實現出來:性能
function getTarget(nums, target) {
// 空數組就返回
if(!nums) {
return
}
let Map = {}
// 對數組進行遍歷
for(let i=0; i < nums.length; i++) {
// 假如在Map中能找到對應的值,就返回對應的結果
if( Map[target-nums[i]] !== undefined ) {
console.log([target-nums[i], nums[i],Map[target-nums[i]], i]);
return [nums[target-nums[i]], nums[i],Map[target-nums[i]], i]
}
// 若是不能,就將這個值存入Map
Map[nums[i]] = i
}
}
getTarget([2,3,2,11], 4)
// [2, 2, 0, 2]
複製代碼
其實,另外使用ES6中的Map也能夠解決,思路是同樣的,這裏貼一下Map的定義,方便你們理解學習
Map 對象保存鍵值對,而且可以記住鍵的原始插入順序。任何值(對象或者原始值) 均可以做爲一個鍵或一個值
複製代碼
我把本身的思路放在這裏跟你們探討一下:ui
// 其實就是把Map變成了Map的實例化對象,別的都沒什麼差異
function getTarget(nums, target) {
// 空數組就返回
if(!nums) {
return
}
const map = new Map()
for(let i =0; i < nums.length; i++) {
if(map.get(target - nums[i]) !== undefined) {
console.log([target-nums[i], nums[i],Map[target-nums[i]], i]);
return [nums[target-nums[i]], nums[i],Map[target-nums[i]], i]
}
map.set(nums[i], i)
}
}
getTarget([2,3,2,11,2,45,2,2,2], 4)
// [2, 2, 0, 2]
複製代碼
你們有更好的思路歡迎交流
雙指針解法很少說了吧,基本人人好評,在數組中運用得當,不光能解題,還能提升效率,下面咱們就來在題目中用雙指針來解題吧!
咱們來看一題合併並排序數組問題: 給你兩個有序整數數組 nums1 和 nums2,請你將 nums2 合併到 nums1 中,使 nums1 成爲一個有序數組。。
傳統解法是不少人第一次作這個題目時,輕易想到的,不就是把兩個數組合並,而且排個序,這又啥難的?看代碼:
function mergeTwoArr(nums1, nums2) {
nums1 = [...nums1, ...nums2]
nums1.sort();
return nums1
}
let nums1 = [1,4,7], nums2 = [2,3,9]
let arr = mergeTwoArr(nums1, nums2)
console.log(arr);
複製代碼
以前看評論有看到過,"排序這個事情前端來作也太簡單了吧!",哈哈,沒錯,有對應的方法就是好啊,不過sort的本質,其實是對整個數組進行遍歷,一個個比較大小,這樣其實是很長的一個過程,若是數組很大,會形成性能上的佔用,MDN上也有這樣的描述:因爲它取決於具體實現,所以沒法保證排序的時間和空間複雜性。
先來講一下什麼是雙指針,顧名思義,他有兩個指針,分別指向,咱們經過一張圖來體會一下,就能明白了👇:
由於題目是須要把nums2併入nums1,而且二者都是有序數組,所以咱們能夠考慮把指針指向二者的尾部,具體的思路看下圖:
看懂了圖,咱們接下來用代碼來實現一下這個雙指針:
function mergeTwoArr(nums1,m, nums2,n) {
// 定義兩個指針分別指向原數組尾部,一個變量用於指向合併後的尾部
let needle1 = m-1, needle2 = n-1, allNum = m+n-1
while(needle1 >= 0 && needle2 >= 0) {
if(nums1[needle1] >= nums2[needle2]) {
// 將二者的更大的那個元素放入尾部
nums1[allNum] = nums1[needle1]
// needle1往前移一位
needle1--
// 最後的位置往前移一位
allNum--
}else {
// 將二者的更大的那個元素放入尾部
nums1[allNum] = nums2[needle2]
// needle1往前移一位
needle2--
// 最後的位置往前移一位
allNum--
}
}
// 若是nums2有的元素比nums1的最小的元素更小,且總元素更多,那麼此時會剩下幾個元素
while(needle2 >= 0) {
// 將nums2剩下的元素都放入nums1的前面
nums1[allNum] = nums2[needle2]
allNum--
needle2--
}
}
let nums1 = [3,4,7], nums2 = [2,3,9,11,15]
mergeTwoArr(nums1,3, nums2,5)
console.log(nums1, nums2);
// [2, 3, 3, 4, 7, 9, 11, 15]
// [2, 3, 9, 11, 15]
複製代碼
咱們再來看一題升級的兩數求和的問題--三數求和:給你一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出全部知足條件且不重複的三元組。
針對這題咱們用傳統方法的話就是用三次遍歷,可是這樣直接把時間複雜度弄到了O(n^3)的程度,那就很耗費性能了,而此時咱們應該考慮用剛纔學到的指針來解這道題目:
其實三數求和,咱們能夠將其認定爲固定一個數,而後另外兩個數與這個固定的數的和爲0便可,另外兩個數經過指針來指定,能夠從固定數的後一個數和整個數組的最後一個數兩個位置出發,若是三數相加的結果大於0,證實須要將最後一個指針往前挪一位,若是小於0,須要將前面的指針日後移一位,看到這裏你們也必定發現了,沒錯,第一步重要工做是對數組進行從小到大的排序,爲了幫助你們理解,我畫了下面的圖👇:
各部門注意了,這裏強調一個點,接下來咱們須要往中間挪動指針,爲了不出現重複數組,左右側指針每次挪動一位,咱們都要和前一位判斷是否相等,若是相等則下一位,若是不相等再重複上述過程,直到1號和2號指針指向了同一個數,那麼固定數的這一輪循環纔算走完,接下來固定第二個數,一樣要判斷是否相等,直到出現不相等的數才固定,接下來重複上述整個過程。接下來咱們看一下編碼實現過程:
function getThreeSum(nums) {
// 定義一個空數組用來存放最終結果
let arr = []
// 對數組進行排序
nums = nums.sort((a, b) => {
return a-b
});
const arrLength = nums.length
// 對數組進行遍歷,遍歷到倒數第三個數就行,後面兩個正好指針
for(let i = 0; i < arrLength-2; i ++) {
// 定義左右指針
let left = i + 1, right = arrLength - 1
// 每次循環先判斷當前數與前一個數是否相等,相等就下一個循環
if(i > 0 && nums[i] === nums[i-1]) {
continue
}
while(left < right) {
// 若是和小於0
if(nums[i] + nums[left] + nums[right] < 0) {
// 左指針右移一位
left++
// 而且判斷下一位與當前位是否相等
while(left<right&& nums[left] === nums[left-1] ) {
left++
}
}else if(nums[i] + nums[left] + nums[right] > 0) {
// 右指針左移一位
right--
// 而且判斷前一位與當前位是否相等
while(left<right&& nums[right] === nums[right+1] ) {
right--
}
}else {
// 把這一組結果放到數組中
arr.push([nums[i], nums[left], nums[right]])
// 接下來就是肯定下一組數據了
left++
right--
// 而且判斷下一位與當前位是否相等
while(left<right&& nums[left] === nums[left-1] ) {
left++
}
// 而且判斷前一位與當前位是否相等
while(left<right&& nums[right] === nums[right+1] ) {
right--
}
}
}
}
// 返回最終結果
console.log(arr);
return arr
}
getThreeSum([1, 5, 7, 3,-5, -4, -10, 9, 11, -4, 0])
// [[-10, 1, 9],[-10, 3, 7],[-5, -4, 9],[-5, 0, 5],[-4, 1, 3]]
複製代碼
那麼,這種兩邊向中間移動的實際上是一種特殊的雙指針,咱們稱爲對撞指針,你學廢了嗎(^_^*)
關於數組的一些經典典型的解題思路,就是上面這些了,必定必定要本身看了而後解題,不要直接看代碼,才能促進本身對思路的理解哦。