算法:分治法

思想

分治法简单来说就是分而治之。其算法复杂度一般用主定理来求。

  • T ( n ) = 2 T ( n / 2 ) + n T(n)= 2T(n/2) + n T(n)=2T(n/2)+n——时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn)
  • T ( n ) = 2 T ( n / 2 ) + 1 T(n)= 2T(n/2) + 1 T(n)=2T(n/2)+1——时间复杂度就是 O ( n ) O(n) O(n)

所以一般来说,面试: O ( n 2 ) − O ( n l o g n ) − O ( n ) O(n^2)-O(nlogn)-O(n) O(n2)O(nlogn)O(n)可以考虑下分治法

题目

归并排序

def merge(left,right):
	if left[-1] < right[0]:
		return left + right
	    n,m = len(a),len(b)
    i,j = 0,0
    new = []
    while i < n and j <m:
        if a[i] < b[j]:
            new.append(a[i])
            i += 1
        else :
            new.append(b[j])
            j += 1
    while i < n:
        new.append(a[i])
        i += 1
    while j < m :
        new.append(b[j])
        j += 1
    return new
def merge_sort(nums):
	if len(nums) <= 1:
		return nums
	mid = len(nums)// 2
	left = merge_sort(nums[:mid])
	right = merge_sort(nums[mid:])
	return merge(left,right)

再写一遍,加深印象。
归并的分治是先分到底,再一步步合并。

打乱数组

[a1,a2,a3,b1,b2,b3]——>[a1,b1,a2,b2,a3,b3]

O ( n 2 ) O(n^2) O(n2)做法:
移动数组,b1往前,b2往前。。。

def shuffle(nums):
    n = len(nums) // 2
    i = n
    k = 1
    while i < 2 * n - 1:
        j =  i 
        while j > k :
            nums[j],nums[j-1] = nums[j-1],nums[j]
            j -= 1
        
        k += 2 
        i += 1
    return  nums

注意下起始点

分治做法:

def switch(num1,num2,l):
    n = len(num1)
    i = n - l 
    for j in range(0,l):
        num1[i],num2[j] = num2[j],num1[i]
        i += 1
    return num1,num2
    

def shuffle1(nums):
    mid = len(nums) // 2
    res = len(nums) % 2
    mid = mid + res
    if len(nums) <= 2:
        return nums
    else:
        l = mid // 2
        nums_left,nums_right = switch(nums[:mid],nums[mid:],l)
        num_s_left = shuffle1(nums_left)
        num_s_right = shuffle1(nums_right)
        return num_s_left + num_s_right
    

与归并不同,这边是分治分治分治

快速乘法

a1a2a3 * b1b2b3b4:
a1a2 | a3
b1b2 | b3b4

同样是分治法,与直接遍历的差别在于中间项少算一次

def quick_plus(x,y):
	if len(str(x)) == 1 or len(str(y)) == 1:
		return x*y
	else:
		n = max(len(str(x)) , len(str(y)))
		pow = n // 2
		a = x // (10 ** pow )
		b = x %  (10 ** pow )
		c = y //  (10 ** pow )
		d = y % (10 ** pow )
		ac = quick_plus(a,c)
		bd = quick_plus(b,d)
		mid_term = quick_plus(a+b,c+d) - ac - bd
		ans = ac *(10 ** (2*pow ) ) + mid_term * (10 ** pow ) + bd
		return ans

数组中第k小的元素

分而治之,并不一定要均等分。比如快速排序,其实就是将找到的pivot位置后切分。
找第k小的元素也可以用分治法。

    def findKthSmallest(self, nums: List[int], k: int) -> int:
        if nums:
            pos = self.partition(nums, 0, len(nums)-1) #最右的元素放到了正确的位置,并且返回下标配
        if k > pos+1:
            return self.findKthSmallest(nums[pos+1:], k-pos-1)
        elif k < pos+1:
            return self.findKthSmallest(nums[:pos], k)
        else:
            return nums[pos] 
# 将最右边的元素放置到正确的位置,  
    def partition(self,nums, l, r):# 看看最右的元素的坐标
        low = l
        while l < r:
            if nums[l] < nums[r]: #当 大小符合,low与l共同移动,并与原来的交换。当不符合时,low停住
                nums[l], nums[low] = nums[low], nums[l]
                low += 1 
            l += 1 #l的目的是为了遍历数组
        nums[low], nums[r] = nums[r], nums[low]
        return low

partition当然也可以用列表解析来写

    def partition(self,nums, l, r):# 看看最右的元素的坐标
        left = [x for x in nums if x < nums[r]]
        right = [x for x in nums if x > nums[r]]
        eq = [x for x in nums if x == nums[r]]
        low = len(left)
        nums = left + eq + right
        return low,nums

这样会额外占用空间,而且多遍历了数组(遍历3遍),会更慢。

53. 最大子序和

三种做法:

  • 暴力法:遍历数组,找到以第i个元素开头最大的那个,与最大值比较
  • 分治法
  • 动态规划

分治法——三部分的合并

  • 左部分最大
  • 右部分最大
  • 中间向左右两边的最大
    def maxSubArray(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return nums[0]
        mid = len(nums)// 2
        left = self.maxSubArray(nums[:mid])
        right = self.maxSubArray(nums[mid:])
        return max(left,right,self.getmid(nums,mid))

    def getmid(self,nums,mid):
        l,r = nums[:mid],nums[mid:]
        i,j = len(l) - 1 ,0
        max_l = -sys.maxsize
        max_r = -sys.maxsize
        sum_l = 0
        while i >= 0:
            sum_l +=  l[i]
            max_l = max(max_l,sum_l)
            i -= 1
        sum_r = 0
        while j < len(r):
            sum_r += r[j]
            max_r = max(max_r,sum_r)
            j += 1
        return max_l +max_r

面试题51. 数组中的逆序对

在归并的时候统计逆序对的数量即可。

    def reversePairs(self, nums: List[int]) -> int:
        return self.reversePairs1(nums)[1]
    def reversePairs1(self, nums: List[int]) -> int:
        if len(nums) < 2 :
            return nums,0
        mid = len(nums) // 2
        left,cnt_l = self.reversePairs1(nums[:mid]) #不仅返回左边有序,返回左边的逆序对数目
        right,cnt_r = self.reversePairs1(nums[mid:]) #返回右边的逆序对数目
        merged,cnt = self.merge(left,right) #最优两边合并时新增的逆序对数目
        cnt += (cnt_l + cnt_r) #汇总求和
        return merged,cnt
    def merge(self,left,right):
        m,n = len(left),len(right)
        cnt = 0
        i,j = 0,0
        res = []
        while i < m and j < n:
            if left[i] <= right[j]: 
                res.append(left[i])
                i += 1
            else:  #当左i 大于 右j
                res.append(right[j])
                cnt += m - i #说明左边i以后的元素都大于j,所以对j而言,有m-i的逆序对。
                j += 1
        if i < m:
            res += left[i:]
        if j < n :
            res += right[j:]
        return res,cnt

最终返回的是一个元组,记录有序的数组以及逆序对的数量

315. 计算右侧小于当前元素的个数

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
输入: [5,2,6,1]
输出: [2,1,1,0]

与计算逆序对类似,不过需要记录的是每个元素的逆序对个数。这个counts数组的和就是逆序对的数目。

    def countSmaller(self, nums: List[int]) -> List[int]:
        def merge_sort(num):
            if len(num) <= 1:
                return num 
            mid = len(num) // 2
            left = merge_sort(num[:mid])
            right = merge_sort(num[mid:])
            return merge(left,right)
        def merge(left,right):
            i,j = 0,0
            m,n = len(left),len(right)
            new = []
            while i < m and j < n:
                if left[i][1] <= right[j][1]:
                    new.append(left[i])
                    ans[left[i][0]] += j #最关键的是这一行
                    i += 1
                else :
                    new.append(right[j])
                    j += 1
            while i < m:
                new.append(left[i])
                ans[left[i][0]] += j
                i += 1
            while j < n:
                new.append(right[j])
                j += 1
            return new
        enums = list(enumerate(nums))
        ans = [0]*len(nums)
        merge_sort(enums)
        return ans    

与逆序对写法不一样的地方

  • 需要对元组进行排序,位置0记录的是每个元素初始时候的位置,位置1是值。即(k,v)的形式
  • 合并时有所不同
    统计逆序对数目:left[i] > right[j] 时:统计 right[j] 元素与 i , i+1 ,…之后的所有逆序对数目。记录的主体是j。因此写法是 cnt += len(right) - j
    本道题:当left[i] <= right[j] 时,统计 left[i]元素 与 0, 1 ,2 … j -1的所有逆序对数目。记录的主体是left[i]。因此写法:ans[left[i][0]] += j
  • 这边函数都是在内部定义的,ans是函数内的变量 如果写成三个函数,ans也要编程参数传递,就会更复杂。写成类的形式估计会舒服很多。

你可能感兴趣的:(算法题目整理)