这是我参与更文挑战的第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]]
复制代码
那么,这种两边向中间移动的实际上是一种特殊的双指针,咱们称为对撞指针,你学废了吗(^_^*)
关于数组的一些经典典型的解题思路,就是上面这些了,必定必定要本身看了而后解题,不要直接看代码,才能促进本身对思路的理解哦。