算法学习——分治

divide and conquer


分治,字面意思是“分而治之”,就是把一个复杂的1问题分成两个或多个相同或相似的子问题,再把子问题分成更小的子问题直到最后子问题可以简单地直接求解,原问题的解即子问题的解的合并,这个思想是很多高效算法的基础,例如排序算法(快速排序,归并排序),傅里叶变换(快速傅里叶变换)等。

基本步骤

  • 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
  • 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
  • 合并:将各个子问题的解合并为原问题的解

使用场景

  1. 该问题的规模缩小到一定的程度就可以容易的解决。
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解。没有这个条件,就不能分治了
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。如果不独立,则用动态规划比较好

Leetcode题

148. 排序链表

func sortList(head *ListNode) *ListNode {
    return mergeSort(head)
}

func mergeSort(root *ListNode) (head *ListNode) {
	if root == nil {
		return nil
	} else if root.Next == nil {
		return root
	}

	slow, fast := root, root.Next
	for fast != nil && fast.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next
	}

	end := slow
    end.Next = nil // 一定要做,需要断开这个,不然死循环(爆栈)
	slow = slow.Next
    
	left := mergeSort(root)
	right := mergeSort(slow)
	return merge(left, right)
}

func merge(left, right *ListNode) *ListNode {
    pre := &ListNode{}
    cur := pre
    for left != nil && right != nil {
        if left.Val < right.Val {
            cur.Next = left
            cur = left
            left = left.Next
        } else {
            cur.Next = right
            cur = right
            right = right.Next
        }
    }

    if left != nil {
        cur.Next = left
    }
    if right != nil {
        cur.Next = right
    }
    return pre.Next 
}

// 不使用递归的做法,空间复杂度O(1)
func mergeSortLoacl(head *ListNode) *ListNode {
    h, length, interval := head, 0, 1
    for h != nil {
        length++
        h = h.Next
    }
    
    res := &ListNode{}
    res.Next = head
    pre := res
    for interval < length { // 结束条件, interval为每次归并的子序列长度1,2,4...
        pre, h = res, res.Next // 每次都从头开始
        for h != nil { // 是否遍历完链表
            h1, i := h, interval
            for h != nil && i > 0 { // 找h1部分
                h = h.Next
                i--
            }
            if i > 0 { // h1长度都不够interval了,直接跳出了(h2不可能存在,都不用合并了)
                break
            }
            h2, j := h, interval
            for h != nil && j > 0 { // 找h2部分
                h = h.Next
                j--
            }
            c1, c2 := interval, interval - j // c2 大小需要减去j是因为h2有可能不够interval的长度
            for c1 > 0 && c2 > 0 { // 合并h1 h2,pre就是那个辅助节点
                if h1.Val < h2.Val {
                    pre.Next, h1 = h1, h1.Next
                    c1--
                } else {
                    pre.Next, h2 = h2, h2.Next
                    c2--
                }
                pre = pre.Next
            }
            if c1 > 0 {
                pre.Next = h1
            } else {
                pre.Next = h2
            }
            // pre需要指向最后的h,需要拼接上才行,要不然会断开,因为不一定h就是指向最后
            // 比如说合并完第一个和第二个,第三个和第四个需要连接上,也就是pre.Next需要指向第三个
            // 开头,也就是h指向的位置
            for c1 > 0 || c2 > 0 {
                pre = pre.Next
                c1, c2 = c1 - 1, c2 - 1
            }
            pre.Next = h
        }
        interval *= 2
    }
    return res.Next
}

23. 合并K个升序链表

类似的想法,不过这里用递归就好了,用Local也可以,不过不需要pre.Next = h,需要变一下才行。

// 这里就是递归形式而已,比较方便
func mergeKLists(lists []*ListNode) *ListNode {
    length := len(lists)
    if length == 0 {
        return nil
    }
    return merge(lists, 0, length - 1)
     
}

func merge(lists []*ListNode, left, right  int) *ListNode {
    if left == right {
        return lists[left]
    }
    mid := (right + left) / 2
    h1 := merge(lists, left, mid)
    h2 := merge(lists, mid + 1, right)
    return mergeTwo(h1, h2)
}

func mergeTwo(root1, root2 *ListNode) *ListNode {
    pre := &ListNode{}
    cur := pre
    for root1 != nil && root2 != nil {
        if root1.Val < root2.Val {
            cur.Next = root1
            root1 = root1.Next
        } else {
            cur.Next = root2
            root2 = root2.Next
        }
        cur = cur.Next
    }
    if root1 != nil {
        cur.Next = root1

    }
    if root2 != nil {
        cur.Next = root2
    }
    return pre.Next
}

395. 至少有K个重复字符的最长子串

func longestSubstring(s string, k int) int {
    if len(s) < k  { 
        return 0
    }
    if k < 2 {
        return len(s)
    }
    arr:= make([]int, len(s))
    for i := 0; i < len(s); i++ { // 转化为数组会快一下
        arr[i] = int(s[i] - 'a')
    }

    return helper(arr, 0, len(s) - 1, k)
}

func helper(arr []int, left, right, k int) int{
    if right - left + 1 < k {
        return 0
    }
    cout := make([]int, 26)
    for i := left; i <= right; i++ { // 统计数量
        cout[arr[i]]++
    }

    for right - left + 1 >= k && cout[arr[left]] < k  { // 刨掉左边少于k的字符串
        left++
    }

    for right - left + 1 >= k && cout[arr[right]] < k  { // 刨掉右边少于k的字符串
        right--
    }

    for i := left; i <= right; i++ {
        if cout[arr[i]] < k { // 如果有一个不对劲就以这个为分界线划分数组,递归寻找最长的字符串
            return max(helper(arr, left, i - 1, k), helper(arr, i + 1, right, k))
        }
    }

    return right - left + 1 // 如果都大于k,那就直接返回这个长度罗
}


func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

剑指 Offer 51. 数组中的逆序对

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

这两个题都是类似的,都是在分治归并的过程中找到问题的解

// Offer 51
func reversePairs(nums []int) int {
    res := 0

    var helper func(left, right int) 
    var merge func(left, mid, right int) 

    helper = func(left, right int) {
        if left >= right {
            return
        }
        mid := (right - left) / 2 + left
        helper(left, mid)
        helper(mid + 1, right)
        merge(left, mid, right)
    }

    merge = func(left, mid, right int) {
        if left == right {
            return
        }
        arr := make([]int, right - left + 1)
        i, j, k := left, mid + 1, 0
        for i <= mid && j <= right {
            if nums[j] < nums[i] {
                res += mid - i + 1
                arr[k] = nums[j]
                j++
            } else {
                arr[k] = nums[i]
                i++
            }
            k++
        }
        for i <= mid {
            arr[k] = nums[i]
            i++
            k++
        }
        for j <= right {
            arr[k] = nums[j]
            j++
            k++
        }
        for i = 0; i < k; i++ {
            nums[left + i] = arr[i]
        }
    }

    helper(0, len(nums) - 1)
    return res 

}
func countSmaller(nums []int) []int {
    
    res := make([]int, 0)
    if len(nums) == 0 {
        return res
    }
    
    tmp := append([]int{}, nums...)
    sort.Ints(tmp)
    
    for _, v := range nums {
        index := findIndex(tmp, v)
        if tmp[index] < v {
            res = append(res, index + 1)
            if index + 2 < len(tmp) {
                tmp = append(tmp[:index + 1], tmp[index + 2:]...)
            } else {
                tmp = tmp[:index + 1]
            }
        } else {
            res = append(res, 0)
            if len(tmp) > 1 {
                tmp = tmp[1:]
            }
        }
    }
    return res
}

func findIndex(nums []int, target int) int {
    left := 0
    right := len(nums) - 1
    
    for ; left < right; {
        mid := left + (right - left + 1) / 2
        
        if nums[mid] >= target {
            right = mid - 1
        } else {
            left = mid
        }
    }
    return left
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值