python调包很方便,数据结构学着总是半途而废,所以一直没有学完。DataWhale的Leetcode练习,对我来说很难,每个题我基本上只会常用库API函数实现,但想着有伴可以一起做这件事,有专业的引导,有拼比,有相互扶持,好像艰难的事情变得不那么难了。【话说的太早,笑到最后才是成功,争取提前1天完成学习内容,这样学习任务重的日子也可以来得及,加油!】
学习目标
了解分治算法
分治算法(divide and conquer, D&C)是一种著名的递归式问题解决方法,一个重要的D&C算法就是快速排序。
使用D&C解决问题的过程包括两个步骤:
- 找出基线条件,这种条件尽可能简单
- 不断将问题分解,直到符合基线条件。
50 Pow(x, n)
问题
求 x n 的 结 果 。 x^n的结果。 xn的结果。
边界情况:
- n = 0→任何值x的0次幂结果为1
- n < 0→转换成1/x的-n次幂的问题
三个思路
【leetcode的讲解太妙,我只能从推导来证明我貌似了解它们的思路了】
- 思路1: x n = x ∗ x ∗ x ∗ . . . ∗ x x^n = x*x*x*...*x xn=x∗x∗x∗...∗x
- 思路2: x n = ( x n / 2 ) 2 = ( ( x n / 4 ) 2 ) 2 = . . . . = ( ( ( ( x 1 ) 2 ) 2 ) . . . ) 2 x^n = (x^{n/2})^2=((x^{n/4})^2)^2=....=((((x^1)^2)^2)...)^2 xn=(xn/2)2=((xn/4)2)2=....=((((x1)2)2)...)2
- 思路3【原理是位运算】:
n = k 0 2 0 + k 1 2 1 + k 2 2 2 + . . . + k m 2 m , k i ∈ 0 , 1 n = k_02^0 + k_12^1 + k_22^2+ ...+k_m2^m,k_i∈{0,1} n=k020+k121+k222+...+km2m,ki∈0,1
x n = x ( k 0 2 0 + k 1 2 1 + k 2 2 2 + . . . + k m 2 m ) = x ( k 0 2 0 + k 1 2 0 ∗ 2 + k 2 2 0 ∗ 2 ∗ 2 + . . . + k m 2 0 ∗ 2 ∗ 2... ∗ 2 ) = x k 0 2 0 ∗ x k 1 2 0 ∗ 2 ∗ x k 2 2 0 ∗ 2 ∗ 2 ∗ . . . ∗ x k m 2 0 ∗ 2 ∗ . . . ∗ 2 = x k 0 2 0 ∗ ( x k 1 2 0 ) 2 ∗ ( ( x k 2 2 0 ) 2 ) 2 ∗ . . . ∗ ( ( ( x k m 2 0 ) 2 ) 2 . . . ) 2 x^n = x^{(k_02^0 + k_12^1 + k_22^2+ ...+k_m2^m)}=x^{(k_02^0 + k_12^0*2 + k_22^0*2*2+...+k_m2^0*2*2...*2)}=x^{k_02^0}*x^{k_12^0*2}*x^{k_22^0*2*2}*...*x^{k_m2^0*2*...*2}=x^{k_02^0}*(x^{k_12^0})^2*((x^{k_22^0})^2)^2*...*(((x^{k_m2^0})^2)^2...)^2 xn=x(k020+k121+k222+...+km2m)=x(k020+k120∗2+k220∗2∗2+...+km20∗2∗2...∗2)=xk020∗xk120∗2∗xk220∗2∗2∗...∗xkm20∗2∗...∗2=xk020∗(xk120)2∗((xk220)2)2∗...∗(((xkm20)2)2...)2
方法代码
'''方法1 迭代处理'''
class Solution(object):
# 由于存在迭代,算法复杂度为O(n)
def myPow(self, x, n):
ans = 1
if n < 0:
x = 1 / x
n = -n
elif n == 0:
return ans
for _ in range(n):
ans *= x
return ans
'''方法2 分治递归'''
class Solution(object):
# 时间复杂度:O(logn),即为递归的层数。
# 空间复杂度:O(logn),即为递归的层数。这是由于递归的函数调用会使用栈空间。
def myPow(self, x, n):
if n < 0:
x = 1 / x
n = -n
elif n == 0:
return 1
p = n // 2
q = self.myPow(x, p)
if n % 2 == 0:
return q * q
else:
return q * q * x
'''方法3 分治迭代'''
class Solution(object):
# 时间复杂度:O(logn),即为对 nnn 进行二进制拆分的时间复杂度。
# 空间复杂度:O(1)O(1)O(1)。
def myPow(self, x, n):
ans = 1
if n < 0:
x = 1 / x
n = -n
elif n == 0:
return ans
_x = x
while n > 0:
if n % 2 == 1:
ans *= _x
_x *= _x
n = n // 2
return ans
结果方法1在测试过程超时,方法2和方法3运行时间和内存消耗都一样,不知道还能怎么减少内存消耗。
53 最大子序和
问题:
在数组中找到具有最大加和的连续子数组,在我们工作中的应用是历史数据中找出连续的充电段或出行段。不知道算法处理效果怎样,如果能显著提升效率当然可以往这个方向思考改进。
思路
1是贪心算法,2是动态规划,3是分治思路。
- 贪心算法:原理是最容易想到的,对比累加后比不累加,如果累加和更大则累加,否则不累加,以当前数为起点;
- 动态规划:原理是累加到前一个数,如果累加值为负则丢弃,从当前值开始累加,如果累加值为正则继续累加;
复杂度
时间复杂度:O(n)O(n),其中 nn 为 nums 数组的长度。我们只需要遍历一遍数组即可求得答案。
空间复杂度:O(1)O(1)。我们只需要常数空间存放若干变量。
- 分治思路:数据不断划分为左右子数组,当子数组长度为1时则iSum,lSum,rSum,mSum可得,然后回推到上级,开始合并左右子数组,针对上级左右子数组,iSum为左右iSum的和,lSum可能是左子数组的llSum,也可能延续到右子数组liSum+rlSum,rSum可能是右子数组rrSum,也可能是从左子数组开始lrSum+riSum,我出问题的地方是mSum,可能结果是lmSum或rmSum或者lrSum+rlSum。针对合并过程取各个SUM最大结果。
方法代码
'''贪心算法'''
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
pre = -inf
ans = -inf
for p in nums:
pre = max(pre + p, p)
ans = max(pre, ans)
return ans
'''动态规划'''
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
for i in range(1,len(nums)):
if nums[i-1] > 0:
nums[i] += nums[i-1]
return max(nums)
'''分治1'''
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def iteration(nums):
if len(nums) == 1:
iSum = lSum = rSum = mSum = sum(nums)
return iSum,lSum,rSum,mSum
else:
l = 0
r = len(nums) - 1
m = (l + r) // 2
lnums = nums[:(m+1)]
rnums = nums[(m+1):]
liSum, llSum,lrSum,lmSum = iteration(lnums)
riSum, rlSum,rrSum,rmSum = iteration(rnums)
iSum = liSum + riSum
lSum = max(llSum, liSum+rlSum)
rSum = max(rrSum, lrSum+riSum)
mSum = max(max(lmSum, rmSum), lrSum+rlSum)
return iSum,lSum,rSum,mSum
return iteration(nums)[3]
'''分治2 【日后多写这种方式】'''
class Solution:
def maxSubArray(self, nums):
def pushup(liSum, llSum,lrSum,lmSum,riSum, rlSum,rrSum,rmSum):
iSum = liSum + riSum
lSum = max(llSum, liSum+rlSum)
rSum = max(rrSum, lrSum+riSum)
mSum = max(max(lmSum, rmSum), lrSum+rlSum)
return iSum,lSum,rSum,mSum
def iteration(nums, l, r):
if l == r:
iSum=lSum=rSum=mSum = nums[l]
return iSum,lSum,rSum,mSum
else:
m = (l + r) // 2
liSum, llSum,lrSum,lmSum = iteration(nums, l, m)
riSum, rlSum,rrSum,rmSum = iteration(nums, m+1, r)
return pushup(liSum, llSum,lrSum,lmSum,riSum, rlSum,rrSum,rmSum)
return iteration(nums, 0, len(nums)-1)[3]
结果方法1和方法2运行时间和内存消耗都一样,方法3运行时间久。
169 多数元素
问题:
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
思路:
1.中位数是多数元素;2.出现频次最多的是多数元素;3.分治
- 中位数方法。注意前提是出现次数大于 ⌊ n/2 ⌋ ,所以先将数据排序,中间位置即所求。
- 计数法。遍历一次数组,计数,再求最高频次的数。
- 分治方法。数据不断划分为左右子数组,当子数组长度为1时则该可得子数组的最高频の数,然后回推到上级,开始合并左右子数组,合并方法是:左右字数组最高频の数相同则取该值,否则比较各数在原始数组中的频次,取高频の数。递归计算求取最后结果。
方法代码
'''中位数方法'''
class Solution:
def majorityElement(self, nums: List[int]) -> int:
nums.sort()
return nums[len(nums) // 2]
''' 计数法'''
class Solution:
def majorityElement(self, nums: List[int]) -> int:
counts = collections.Counter(nums)
return max(counts.keys(), key=counts.get)
'''分治方法1【官方】'''
class Solution:
def majorityElement(self, nums, lo=0, hi=None):
def majority_element_rec(lo, hi):
# base case; the only element in an array of size 1 is the majority
# element.
if lo == hi:
return nums[lo]
# recurse on left and right halves of this slice.
mid = (hi-lo)//2 + lo
left = majority_element_rec(lo, mid)
right = majority_element_rec(mid+1, hi)
# if the two halves agree on the majority element, return it.
if left == right:
return left
# otherwise, count each element and return the "winner".
left_count = sum(1 for i in range(lo, hi+1) if nums[i] == left)
right_count = sum(1 for i in range(lo, hi+1) if nums[i] == right)
return left if left_count > right_count else right
return majority_element_rec(0, len(nums)-1)
'''分治方法2【哎,效率低】'''
class Solution:
def majorityElement(self, nums: List[int]) -> int:
def iteration(nums):
if len(nums) == 1:
return nums[0]
else:
l = 0
r = len(nums)
m = (l + r) // 2
lnums = nums[:m]
rnums = nums[m:]
l_f = iteration(lnums)
r_f = iteration(rnums)
return l_f if sum([1 for i in nums if i==l_f]) > sum([1 for i in nums if i==r_f]) else r_f
return iteration(nums)
结果方法1和方法2运行时间和内存消耗基本一样,方法3是官方的分治运行时间久一点;方法4是我写的分治递归,因为左右分子数组序号出错调试了半天,结果也很不理想,用时最久,看来我要学习官方那样的分治递归写法。
总结
本次学习的主要目标是了解分治算法,分治算法的要点在2点:
- 把问题分块处理,到最小可处理单元时候,需要明确处理方法。【比如求取幂指数中的累乘,最大子序和中的各Sum求取,多数元素中的最高频の数】
- 分块后的合并问题,需要明确处理方法。
【比如最大子序和中的各Sum求取,多数元素中的最高频の数】