在前一个星期,学习了动态规划和贪心算法,虽说这俩的题目非常多,来不及完全做完,正好留下了充足的时间来进行巩固,贪心的问题基本都能够用动态规划的思想来进行解决。
二分查找是之前就了解得比较多的一种算法。二分搜索的步骤为:
- 从数组中间的元素开始,如果中间的元素正好是目标值,搜索结束
- 如果目标值大于或小于中间的元素,则在大于或小于中间的元素的那一半继续搜索
代码模板为
//二分查找伪代码模版
while (left <= right) {
mid = (left + right) / 2;
if (array[mid] === target) return result;
else if (array[mid] < target) left = mid + 1;
else right = mid - 1;
}
704.二分查找
方法1:递归
- 思路:先找到中间位置,判断是否是需要寻找的目标值,如果是就返回,不是的话判断目标值和中间元素的大小,然后继续向左右子树递归寻找
- 复杂度:时间复杂度
O(logn)
,空间复杂度O(logn)
,递归栈大小
var search = function (nums, target) {
return search_interval(nums, target, 0, nums.length - 1)
};
function search_interval(nums, target, left, right) {
if (left > right) {
return -1
}
let mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) {//判断目标值和中间元素的大小
return mid
} else if (nums[mid] < target) {//递归寻找目标元素
return search_interval(nums, target, mid + 1, right)
} else {
return search_interval(nums, target, left, mid - 1)
}
}
方法2.非递归
- 思路:定义
left
、right
指针,比较目标元素和中间元素的大小,然后不断缩小左右指针的范围继续寻找目标元素 - 复杂度:时间复杂度
O(logn)
,空间复杂度O(1)
var search = function (nums, target) {
let left = 0,
right = nums.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
return mid;
} else if (target < nums[mid]) {//比较目标和中间元素的大小,然后不断缩小left和rihgt指针的范围
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
};
35.搜索插入位置
时间复杂度O(logn)
,空间复杂度O(1)
var searchInsert = function(nums, target) {
const n = nums.length;
let left = 0, right = n - 1, ans = n;
while (left <= right) {
let mid = ((right - left) >> 1) + left;
if (target <= nums[mid]) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
};
69.Sqrt(x)
方法一:二分法
- 思路:从0-x不断二分,直到
- 复杂度分析:时间复杂度
O(logx)
,即为二分查找需要的次数。空间复杂度O(1)
var mySqrt = function (x) {
let left = 0
let right = x
while (left <= right) {
let mid = left + ((right - left) >> 1)//中间位置索引 x>>1 表示除以2并取整,缩小一下遍历的范围
if (mid * mid <= x) {
left = mid + 1
} else {
right = mid - 1
}
}
return right
};
方法2.牛顿迭代
- 思路:
r = ( r + x / r ) / 2
- 复杂度分析:时间复杂度
O(logx)
。空间复杂度O(1)
var mySqrt = function(x) {
let r = x
while (r ** 2 > x) r = ((r + x / r) / 2) | 0//取整
return r
};
300.最长递增子序列
方法1:二分查找+贪心
- 思路:准备tail数组存放最长上升子序列,核心思想就是越小的数字越要往前放,这样后面就会有更多的数字可以加入tails数组。将nums中的数不断加入tail,当nums中的元素比tail中的最后一个大时 可以放心push进tail,否则进行二分查找,让比较小的数二分查找到合适的位置,让后面有更多的数字与这个数形成上升子序列
- 复杂度:时间复杂度
O(nlogn)
,n为nums的长度,每次二分查找需要logn,所以是总体的复杂度是O(nlogn)
。空间复杂度是O(n)
,tail数组的开销
var lengthOfLIS = function (nums) {
let n = nums.length;
if (n <= 1) {
return n;
}
let tail = [nums[0]];//存放最长上升子序列数组
for (let i = 0; i < n; i++) {
if (nums[i] > tail[tail.length - 1]) {//当nums中的元素比tail中的最后一个大时 可以放心push进tail
tail.push(nums[i]);
} else {//否则进行二分查找
let left = 0;
let right = tail.length - 1;
while (left < right) {
let mid = (left + right) >> 1;
if (tail[mid] < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
tail[left] = nums[i];//将nums[i]放置到合适的位置,此时前面的元素都比nums[i]小
}
}
return tail.length;
};
方法2.动态规划
-
思路:
dp[i]
表示选择nums[i]
,并且以nums[i]
结尾的最长上升子序列的长度。两层循环,i:1~nums.length
,j:0~i
,如果nums[i] > nums[j]
,则构成一个上升对,dp[i]
就从dp[i]
,dp[j]+1
两个种选择较大者,最后返回dp数组总的最大数 -
复杂度分析:时间复杂度
O(n^2)
,n是nums的长度,外层需要循环n次,dp[i]
需要从dp[0~i-1]
,所以复杂度是O(n^2)
。空间复杂度是O(n)
,即dp数组的空间
const lengthOfLIS = (nums) => {
let dp = Array(nums.length).fill(1);
let result = 1;
for(let i = 1; i < nums.length; i++) {
for(let j = 0; j < i; j++) {
if(nums[i] > nums[j]) {//当nums[i] > nums[j],则构成一个上升对
dp[i] = Math.max(dp[i], dp[j]+1);//更新dp[i]
}
}
result = Math.max(result, dp[i]);//更新结果
}
return result;
};
4.寻找两个正序数组的中位数
- 思路:数组合并之后在排序的复杂度是
O((m+n) log(m+n))
不符合题意,题目要求的是O(log (m+n))
,我们一看到logn的复杂度就联想到了二分。二分长度较小的数组,找到这个数组二分的位置,在根据这个二分的位置和两个数组的总长度找到另一个数组二分的位置,比较这两个位置的四个数是否满足交叉小于等于,不满足继续二分,满足就找到了解 - 复杂度:时间复杂度
O(log( min(m,n)) )
,m、n分别是nums1和nums2的长度。每次二分循环的长度都会少一半,只要二分比较短的数组即可。空间复杂度O(1)
var findMedianSortedArrays = (nums1, nums2) => {
let len1 = nums1.length, len2 = nums2.length
if (len1 > len2) return findMedianSortedArrays(nums2, nums1)//对nums1和nums2中长度较小的二分
let len = len1 + len2//总长
let start = 0, end = len1 //进行二分的开始和结束位置
let partLen1, partLen2
while (start <= end) {
partLen1 = (start + end) >> 1//nums1二分的位置
partLen2 = ((len + 1) >> 1) - partLen1//nums2二分的位置
//L1:nums1二分之后左边的位置,L2,nums1二分之后右边的位置
//R1:nums2二分之后左边的位置,R2,nums2二分之后右边的位置
//如果左边没字符了,就定义成-Infinity,让所有数都大于它,否则就是nums1二分的位置左边一个
let L1 = partLen1 === 0 ? -Infinity : nums1[partLen1 - 1]
//如果左边没字符了,就定义成-Infinity,让所有数都大于它,否则就是nums2二分的位置左边一个
let L2 = partLen2 === 0 ? -Infinity : nums2[partLen2 - 1]
//如果右边没字符了,就定义成Infinity,让所有数都小于它,否则就是nums1二分的位置
let R1 = partLen1 === len1 ? Infinity : nums1[partLen1]
//如果右边没字符了,就定义成Infinity,让所有数都小于它,否则就是nums1二分的位置
let R2 = partLen2 === len2 ? Infinity : nums2[partLen2]
if (L1 > R2) {//不符合交叉小于等于 继续二分
end = partLen1 - 1
} else if (L2 > R1) {//不符合交叉小于等于 继续二分
start = partLen1 + 1
} else { // L1 <= R2 && L2 <= R1 符合交叉小于等于
return len % 2 === 0 ?
(Math.max(L1, L2) + Math.min(R1, R2)) / 2 : //长度为偶数返回作左侧较大者和右边较小者和的一半
Math.max(L1, L2) //长度为奇数返回作左侧较大者
}
}
}