- 两数相除 (LeetCode, Medium, No.0029, 2021-10)
- 将数据流变为多个不相交区间 (LeetCode, Hard, No.0352, 2021-10)
- 山峰数组的顶部 (剑指Offer2, Easy, No.0069, 2021-10)
- 排列硬币 (LeetCode, Easy, No.0441, 2021-10)
- 搜索旋转排序数组 (LeetCode, Medium, No.0033, 2021-10)
不使用乘法、除法和 mod 运算符,返回两数相除的整数部分,如 10/3 返回 3。
题目描述
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2
示例 1:
输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = truncate(3.33333..) = truncate(3) = 3
示例 2:
输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = truncate(-2.33333..) = -2
提示:
被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31, 2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/divide-two-integers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:二分查找
class Solution:
def divide(self, dividend: int, divisor: int) -> int:
""""""
INT_MIN, INT_MAX = -2 ** 31, 2 ** 31 - 1
# 按照题目要求,只有一种情况会溢出
if dividend == INT_MIN and divisor == -1:
return INT_MAX
sign = (dividend > 0 and divisor > 0) or (dividend < 0 and divisor < 0)
# 核心操作
def div(a, b):
if a < b:
return 0
cnt = 1
tb = b
while (tb + tb) <= a:
cnt += cnt
tb += tb
return cnt + div(a - tb, b)
ret = div(abs(dividend), abs(divisor))
return ret if sign else -ret
核心操作说明,以 60 / 8 为例:
第一轮 div(60, 8): 8 -> 32 时停止,因为 32 + 32 > 60,返回 4
第二轮 div(28, 8): 8 -> 16 时停止,因为 16 + 16 > 28,返回 2
第三轮 div(8, 8): 8 -> 8 时停止,因为 8 + 8 > 8,返回 1
第三轮 div(0, 8): 因为 0 < 8,返回 0
因此结果为 1 + 2 + 4 = 7
给你一个由非负整数 a1, a2, ..., an 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。
实现 SummaryRanges 类:
SummaryRanges() 使用一个空数据流初始化对象。
void addNum(int val) 向数据流中加入整数 val 。
int[][] getIntervals() 以不相交区间 [starti, endi] 的列表形式返回对数据流中整数的总结。
进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/data-stream-as-disjoint-intervals
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
“进阶”:在插入过程中完成合并操作;
示例
输入:
["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"]
[[], [1], [], [3], [], [7], [], [2], [], [6], []]
输出:
[null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]]
解释:
SummaryRanges summaryRanges = new SummaryRanges();
summaryRanges.addNum(1); // arr = [1]
summaryRanges.getIntervals(); // 返回 [[1, 1]]
summaryRanges.addNum(3); // arr = [1, 3]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]]
summaryRanges.addNum(7); // arr = [1, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]]
summaryRanges.addNum(2); // arr = [1, 2, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]]
summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]
提示:
0 <= val <= 10^4
最多调用 addNum 和 getIntervals 方法 3 * 10^4 次
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/data-stream-as-disjoint-intervals
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:暴力求解(Python)
- 每次
getIntervals
时,先对数组排序,然后依次找出每个不相交的区间;
class SummaryRanges:
def __init__(self):
self.ls = []
def addNum(self, val: int) -> None:
""""""
self.ls.append(val)
def getIntervals(self) -> List[List[int]]:
""""""
ls = sorted(self.ls)
ret = []
l = ls[0]
for i in range(1, len(ls)):
if ls[i] - ls[i-1] > 1: # 判断是否需要合并
ret.append([l, ls[i-1]])
l = ls[i]
ret.append([l, ls[-1]])
return ret
思路2:分情况讨论(模拟,Python)
-
明确每次
addNum
时,区间会发生那些变化:- 情况1:存在一个区间
[l, r]
满足l <= val <= r
; - 情况2:存在一个区间
[l, r]
满足r + 1 == val
; - 情况3:存在一个区间
[l, r]
满足l - 1 == val
; - 情况4:存在两个个区间
[l0, r0]
和[l1, r1]
满足r0 + 1 == val == l1 - 1
,即加入 val 后,会合并为一个区间[l0, r1]
- 情况5:以上均不满足,加入后 val 单独成为一个区间;
- 情况1:存在一个区间
-
这里使用了
SortedDict
降低了代码难度,也可以使用一个有序数组来模拟; -
时间复杂度:
addNum O(NlgN)
、getIntervals O(N)
; -
空间复杂度:
O(N)
;
from sortedcontainers import SortedDict
from bisect import bisect_right, bisect_left
class SummaryRanges:
def __init__(self):
self.ret = SortedDict() # {l: r}
# 加入首尾两个哨兵,防止区间不存在的情况,这样会徒增很多判断
self.ret[-10] = -10
self.ret[10010] = 10010
def addNum(self, val: int) -> None:
ret = self.ret
L = list(self.ret.keys())
R = list(self.ret.values())
# 二分找出 val 的相邻区间
idx = bisect_left(L, val) # idx = ret.bisect_left(val)
pre = L[idx - 1], R[idx - 1]
nxt = L[idx], R[idx]
if pre[0] <= val <= pre[1] or nxt[0] <= val <= nxt[1]: # 情况1
pass
elif pre[1] + 1 == val == nxt[0] - 1: # 情况4
ret.pop(nxt[0])
ret[pre[0]] = nxt[1]
elif pre[1] + 1 == val: # 情况2
ret[pre[0]] = val
elif nxt[0] - 1 == val: # 情况3
ret.pop(nxt[0])
ret[val] = nxt[1]
else: # 情况5
ret[val] = val
def getIntervals(self) -> List[List[int]]:
return list(self.ret.items())[1:-1] # 去除两个哨兵
- 上面的代码中用到了
SortedDict
,示例:
>>> d = SortedDict()
>>> d[3] = 33
>>> d[2] = 22
>>> d[4] = 44
>>> d[6] = 66
>>> d[7] = 77
>>> d
SortedDict({2: 22, 3: 33, 4: 44, 6: 66, 7: 77})
>>> d.bisect_left(4) # 二分查找返回的是插入位置
2
>>> d.bisect_right(4) # left 和 right 的区别是如果插入值已存在,则 left 会插到前面,right 会插到后面
3
找出山脉数组中山峰的下标(保证给出的数组是一个山脉数组)
- 当
N[mid] > N[mid+1]
时,山峰必在左侧;反之,在右侧; - 因为从中间划分后,左右分别满足相反的性质,因此可以使用二分查找;
题目描述
符合下列属性的数组 arr 称为 山峰数组(山脉数组) :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给定由整数组成的山峰数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ,即山峰顶部。
示例 1:
输入:arr = [0,1,0]
输出:1
示例 2:
输入:arr = [1,3,5,4,2]
输出:2
示例 3:
输入:arr = [0,10,5,2]
输出:1
示例 4:
输入:arr = [3,4,5,1]
输出:2
示例 5:
输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2
提示:
3 <= arr.length <= 10^4
0 <= arr[i] <= 10^6
题目数据保证 arr 是一个山脉数组
进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/B1IidL
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
Python
class Solution:
def peakIndexInMountainArray(self, arr: List[int]) -> int:
""""""
left, right = 1, len(arr) - 2
ans = 0
while left <= right:
mid = left + (right - left) // 2
if arr[mid] > arr[mid + 1]: # 山峰在左侧
ans = mid # 目前已知 mid 位置的值是最大的,因为保证 arr 是一个山脉数组,所以一定会来到这个分支
right = mid - 1
else: # 山峰在右侧
left = mid + 1
return ans
你总共有 n 枚硬币,并计划将它们按阶梯状排列。对于一个由 k 行组成的阶梯,其第 i 行必须正好有 i 枚硬币。阶梯的最后一行 可能 是不完整的。
给你一个数字 n ,计算并返回可形成 完整阶梯行 的总行数。
示例 1:
输入:n = 5
输出:2
解释:因为第三行不完整,所以返回 2 。
提示:
1 <= n <= 2^31 - 1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/arranging-coins
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:二分查找
- 因为时间复杂度为
O(logN)
,所以直接在[1, n]
的范围里找即可
class Solution:
def arrangeCoins(self, n: int) -> int:
left, right = 1, n
while left < right:
mid = (left + right + 1) // 2
if mid * (mid + 1) <= 2 * n:
left = mid
else:
right = mid - 1
return left
思路2:数学
- 解方程
$(1+x)*x/2 = n$ ; - 去掉小于 0 的解,保留:$x=(-1+\sqrt{8n+1})/2$
class Solution:
def arrangeCoins(self, n: int) -> int:
return int((-1 + (8 * n + 1) ** 0.5) / 2)
在一个旋转过的有序数组中搜索某值,若存在返回下标,否则返回 -1。
-
“二分”的本质是两段性,而不是单调性;即只要二分后,左边满足某个性质,右边不满足某个性质,即可使用二分;
-
比如本题二分后,有前半段满足 >= nums[0],而后半段不满足;
题目描述
整数数组 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,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4
进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二分查找(Python)
- 将数组从中间分开成左右两部分时,一定有一部分的数组是有序的。
class Solution:
def search(self, nums: List[int], target: int) -> int:
if not nums:
return -1
L = len(nums) - 1
l, r = 0, L
while l <= r:
mid = l + (r - l) // 2 # 中点下标
if nums[mid] == target:
return mid
if nums[0] <= nums[mid]: # [0, mid) 是有序的
# 如果目标在[0, mid),则将搜索范围缩小到 [0,mid-1],反之 [mid+1,L]
if nums[0] <= target < nums[mid]:
r = mid - 1
else:
l = mid + 1
else: # (mid, L] 是有序的
# 同理,如果目标在(mid, L],则将搜索范围缩小到 [mid+1,L],反之 [0,mid-1]
if nums[mid] < target <= nums[L]:
l = mid + 1
else:
r = mid - 1
return -1