算法练习-二分查找
求开方
69.X的平方根(easy)
题目 给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
提示:
0 <= x <= 231 - 1
分析
根据题目归类使用二分法,找出某个数的平方为x,或者是两个数平方范围内有x。由于想x^2可能会超出范围,所以使用x/temp与temp进行比较。之后利用二分法比较,大于temp的向左取区间,小于temp的包含左区间向右取区间,等于的直接返回。如果是到最后左右区间重合则可以直接返回区间。还可以多添加一个条件,直接判断右区间是不是小于temp,因为平方和本来就远大于X,第一个小于的,那一定就是平方根的左边界。
var mySqrt = function (x) {
if (x <= 1) return x;
else {
let end = Math.floor(x / 2)
let start = 0;
let mid = 0;
while (start <= end) {
mid = Math.ceil((start + end) / 2)
if (end <= x / end) return end;
if (mid > x / mid) end = mid - 1;
else if (mid == x / mid) return mid;
else {
start = mid;
}
}
}
};
查找区间
34. 在排序数组中查找元素的第一个和最后一个位置(Medium)
题目:给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109
分析
这个题,我本来的想法是,用二分法找到目标值,然后向两边扩展找到区间范围。但是这样写的话,时间复杂度就会超出来了,题目要求log(n).所以可以分两步走,先找左区间,再找右区间。还是用二分法,不过用两次,左边二分法找到目标值记录一下,然后左移,如果是大于那就向左走,小于就像右走。右边的区间查找同理。
var searchRange = function (nums, target) {
let n = nums.length;
if (n == 0) return [-1, -1]
let start = 0;
let end = n - 1;
let mid = 0;
let first = -1
let last = -1;
//两次二分法,先找左边界,再找右边界
while (start <= end) {
mid = Math.floor((start + end) / 2)
if (nums[mid] == target) {
first = mid;
end = mid - 1;
} else if (nums[mid] > target) {
end = mid - 1
} else {
start = mid + 1
}
}
start = 0
end = n - 1;
while (start <= end) {
mid = Math.floor((start + end) / 2)
if (nums[mid] == target) {
last = mid;
start = mid + 1
} else if (nums[mid] > target) {
end = mid - 1
} else {
start = mid + 1
}
}
if (first == last && last == -1) return [-1, -1]
else return [first, last]
};
旋转数组查找数字
81. 搜索旋转排序数组 II(medium)
题目:已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
你必须尽可能减少整个操作步骤。
示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104
分析
这一题的关键,在于断开区间。需要判断那个中间值在哪个区间里。因为数组是旋转的,那就会是升-降-升,这样的排序。所以我们在判断时需要注意划分两种情况。一:中间值在升之前,那么左边是升区间,判断数是不是在这个范围里,如果不在,就取另外一个区间。二:中间值在降-升,这是中间值右边的区间是升区间,那么判断,目标值是否在改区间里,在的话取这一段,否则取另一段。总的思想是把复杂的区间简单化。排除简单的,再去划分复杂的。最后如果区间重叠还是没找到,只能是不存在了。但是如果有相等但不是target的情况。左右区间直接缩小。
var search = function (nums, target) {
// 首先找到数组中的最小值,判断中间点是在翻转的左边还是翻转的右边
let start = 0;
let end = nums.length - 1;
let mid = 0;
while (start <= end) {
mid = Math.floor((start + end) / 2)
if (nums[mid] == target) return true;
else if (nums[start] == nums[mid] && nums[mid] == nums[end]) {
start++;
end--;
}
else if (nums[start] <= nums[mid]) {
// 线在左边
if (nums[start] <= target && target < nums[mid]) {
end = mid - 1
} else {
start = mid + 1
}
} else {
// 线在右边
if (nums[mid] < target && target <= nums[end]) {
start = mid + 1
} else {
end = mid - 1
}
}
}
return false;
};
练习
154. 寻找旋转排序数组中的最小值 II(medium)
题目:已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
示例 1:
输入:nums = [1,3,5]
输出:1
示例 2:
输入:nums = [2,2,2,0,1]
输出:0
提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转
分析
这个题目和上面一个题目一样。不过他是在翻转数组里找最小的那个值。考虑时间复杂度,还是用二分法,这次是记录升区间的最小值,然后直接排除升区间,因为要满足,在中间的那种情况。同样有相等值的情况,左右区间值相等的话,跟最小值比较,然后同步向中间靠拢。
var findMin = function (nums) {
let start = 0;
let end = nums.length - 1;
let min = nums[0];
let mid=0
while (start <= end) {
mid = Math.floor((start + end) / 2);
// 取区间
if (nums[start] == nums[mid] && nums[start] == nums[end]) {
if (nums[mid] < min) min = nums[mid]
start++;
end--
} else {
if (nums[start] <= nums[mid]) {
if (min > nums[start]) {
min = nums[start]
}
start = mid + 1
}
else if (nums[mid] <= nums[end]) {
if (nums[mid] < min) {
min = nums[mid]
}
end = mid - 1;
}
}
}
return min
};
540. 有序数组中的单一元素(medium)
题目:给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
提示:
1 <= nums.length <= 105
0 <= nums[i] <= 105
分析
看时间需求,那么这道题需要使用二分法。那么可以这样分析,如果中间索引mid是奇数,那么他前面就会奇数个数值,如果他和左边索引对应的值相等,那么左边就都是出现两次的数。因为如果有一个出现一次,那么一定会有另外一个落单的情况,此时区间右移start=mid+1,如果不相等,那么落单的值一定出现在左边,取end=mid-1。如果是偶数,那么左边一定有偶数个值,如果与左边相等,那区间在左边end=mid-1,如果不相等就是在右边start=mid,一定要包含这个mid
var singleNonDuplicate = function (nums) {
let start = 0;
let end = nums.length - 1;
while (start <= end) {
let mid = Math.floor((start + end) / 2)
if (start == end) return nums[start]
if (mid % 2 == 1) {
// 奇数
if (nums[mid] == nums[mid - 1]) {
start = mid + 1
}
else {
end = mid - 1
}
} else {
// 偶数
if (nums[mid] == nums[mid + 1]) {
start = mid + 2
}
else {
end = mid
}
}
}
};
进阶
4. 寻找两个正序数组的中位数(hard)
题目:给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
nums1.length == m
nums2.length == n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
分析
首先,这个题比较抽象,根据需要的时间复杂度来看,只能使用二分法。但是呢,他的目的是找中位数,如果m+n是奇数,那直接取中间的数,如果是偶数,那么就取中间的两个数的平均值。这里有一个快捷地方法无论奇数还是偶数,可以都取第(m+n+1)/2和(m+n+2)/2大的平均数。可以算一下。取实际下标的时候则是需要减一,因为这里判断的是第几大。先取第(m+n+1)/2大的数,设他为K,那么此时可以找num1中k/2大的,和nnums2中k/2大的,小的那个明显不是第K大的值,因为2(k/2-1)<k-1的,他最大也就能到第k-1大,此时小的数组,start区间start右移一位,k-k/2,因为区间右移了,已经有部分小的值放到外面了。之后再重复上述操作,最后当k=1的时候,取第一个大的,在两个数的start下标那里取小的值就可以了。如果中途有某个数组已经取完了,直接在另一个数组取完剩下的全部值。另一个数可以用同样的方法取出来。
var findMedianSortedArrays = function (nums1, nums2) {
let m = nums1.length;
let n = nums2.length;
let left = Math.floor((m + n + 1) / 2)
let right = Math.floor((m + n + 2) / 2)
return (fen(nums1, 0, nums2, 0, left) + fen(nums1, 0, nums2, 0, right)) / 2
};
function fen(nums1, start1, nums2, start2, k) {
let k2 = Math.floor(k / 2);
if (k2 > nums1.length || k2 > nums2.length) k2 = nums1.length < nums2.length ? nums1.length : nums2.length
if (start1 >= nums1.length || nums1.length == 0) return nums2[start2 + k - 1]
else if (start2 >= nums2.length || nums2.length == 0) return nums1[start1 + k - 1]
if (k == 1) {
return nums1[start1] < nums2[start2] ? nums1[start1] : nums2[start2]
}
if (nums1[start1 + k2 - 1] <= nums2[start2 + k2 - 1]) {
// 数组一数据右移
start1 += k2;
} else {
start2 += k2;
}
if (start1 > nums1.length) k2 = nums1.length;
if (start2 > nums2.length) k2 = nums2.length;
return fen(nums1, start1, nums2, start2, k - k2)
}