算法编程题-分割数组、判断二分图


本文将对两道LeetCode原题、面试高频题分割数组、判断二分图进行介绍,并且给出golang语言的实现、LeetCode运行截图和复杂度分析。

分割数组

原题描述

给定一个数组,将其划分为两个连续的子数组left和right,要求left中的所有元素都小于等于right中的所有元素,且两个子数组都是非空状态且left子数组的长度要尽可能小。

思路简述

左边子数组的所有数字都要小于等于右边子数组的所有数组,等价于左边子数组的最大值要小于等于右边子数组的最小值。因此,可以在预处理阶段计算出leftMax数组,使得leftMax[i]为数组nums[:i+1]段的最大值,计算rightMin,使得rightMin[i]表示数组num[i:]段的最小值。这样在正式的遍历过程中,从左往右遍历i,遇到的第一个i使得leftMax[i]<=right[i+1]的i就是我们要找的分割线,即i本身及其左边的部分是左子数组,剩余的部分是右子数组。

代码实现

func partitionDisjoint(nums []int) int {
    n := len(nums)

	leftMax := make([]int, n)
	maxv := nums[0]
	for i := 0; i < n; i++ {
		maxv = max(maxv, nums[i])
		leftMax[i] = maxv
	}

	rightMin := make([]int, n)
	minv := nums[n - 1]
	for i := n - 1; i >= 0; i-- {
		minv = min(minv, nums[i])
		rightMin[i] = minv
	}

	for i := 0; i < n - 1; i++ {
		if leftMax[i] <= rightMin[i + 1] {
			return i + 1
		}
	}
	return -1
}

在这里插入图片描述

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

判断二分图

原题描述

给定一个用二维数组表示的无向图,一共有n个节点,节点的编号从0到n-1,二维数组graph中的每一项graph[i]表示节点i的所有邻居节点的编号,判断该图是否是一个二分图。一个二分图值得是对于数组中的每一条边,其两个端点都落在不同的节点集合中,即能够将所有节点划分为两个群,在群内所有边,所有的边刚好连接着两个群。

方法一、判断环路
思路简述

笔者想到的一种思路是判断环路的情况,如果是一个零环,即图中没有环,那么事实上这个图可以看成是一棵树,那么只需要将奇数层的节点放在一起,偶数层的节点放在一起,就可以达到二分图的一个目的。再来看奇数环,以下图为例,是无论如何也达不到二分图的目的。
在这里插入图片描述
再来看偶数环,如下图,对于六边环,可能的分组可以是{1, 3, 5}和{2, 4, 6}。
在这里插入图片描述
所以可以通过判断图中是否存在环,以及存在的环是奇数环还是偶数环来判断是否是一个二分图,判环可以使用深度优先搜索(广度优先搜索也行)来实现。

代码实现
func isBipartite(graph [][]int) bool {
	// 事实上可以通过判环来决定是否是二分图
	n := len(graph)
	depthes := make([]int, n)
	ans := true
	var dfs func(cur, parent, depth int)
	dfs = func(cur, parent, depth int) {
		if depthes[cur] > 0 {
			if depth - depthes[cur] >= 2 && (depth - depthes[cur]) % 2 == 1 {
				ans = false
			}
			return
		}
		depthes[cur] = depth
		for _, neightbor := range graph[cur] {
			if neightbor == parent {
				continue
			}
			dfs(neightbor, cur, depth + 1)
			if !ans {
				return
			}	
		}
	}
	for i := 0; i < n; i++ {
		if depthes[i] == 0 {
			dfs(i, -1, 1)
			if !ans {
				return ans
			}
		}
	}
	return ans
}

在这里插入图片描述

复杂度分析
  • 时间复杂度: O ( M + N ) O(M+N) O(M+N),M和N分别是图的边数和点数
  • 空间复杂度: O ( N ) O(N) O(N),N为图的点数
方法二、染色法
思路简述

染色法是一种经典的用来解决此类问题的算法。具体思路就是遍历图,从第一个点开始,该点先被染色为红色,然后其所有的邻居节点都应该被染色为绿色,然后继续以所有邻居节点遍历。如果某点已经有颜色且与将要染色的颜色不一样,那么就说明无法达到二分图的一个目的。在遍历的过程中,可以使用深度优先搜索,也可以使用广度优先搜索,具体参考代码实现。

代码实现
package codes

func partitionDisjoint(nums []int) int {
    n := len(nums)

	leftMax := make([]int, n)
	maxv := nums[0]
	for i := 0; i < n; i++ {
		maxv = max(maxv, nums[i])
		leftMax[i] = maxv
	}

	rightMin := make([]int, n)
	minv := nums[n - 1]
	for i := n - 1; i >= 0; i-- {
		minv = min(minv, nums[i])
		rightMin[i] = minv
	}

	for i := 0; i < n - 1; i++ {
		if leftMax[i] <= rightMin[i + 1] {
			return i + 1
		}
	}
	return -1
}


func isBipartite(graph [][]int) bool {
	// 事实上可以通过判环来决定是否是二分图
	n := len(graph)
	depthes := make([]int, n)
	ans := true
	var dfs func(cur, parent, depth int)
	dfs = func(cur, parent, depth int) {
		if depthes[cur] > 0 {
			if depth - depthes[cur] >= 2 && (depth - depthes[cur]) % 2 == 1 {
				ans = false
			}
			return
		}
		depthes[cur] = depth
		for _, neightbor := range graph[cur] {
			if neightbor == parent {
				continue
			}
			dfs(neightbor, cur, depth + 1)
			if !ans {
				return
			}	
		}
	}
	for i := 0; i < n; i++ {
		if depthes[i] == 0 {
			dfs(i, -1, 1)
			if !ans {
				return ans
			}
		}
	}
	return ans
}


type NodeColor int

const (
	NodeNoColor NodeColor = iota - 1
	NodeRed     
	NodeGreen
)

func isBipartiteV1(graph [][]int) bool {
	n := len(graph)
	colors := make([]NodeColor, n)
	for i := 0; i < n; i++ {
		colors[i] = NodeNoColor
	}
	ans := true
	var dfs func(cur, parent int, color NodeColor)
	dfs = func(cur, parent int, color NodeColor) {
		if colors[cur] != NodeNoColor {
			if colors[cur] != color {
				ans = false
			}
			return
		}
		colors[cur] = color
		for _, neightbor := range graph[cur] {
			if neightbor == parent {
				continue
			}
			dfs(neightbor, cur, color ^ NodeGreen)  // 通过异或操作取相反的颜色
			if !ans {
				return
			}	
		}
	}
	for i := 0; i < n; i++ {
		if colors[i] == NodeNoColor {
			dfs(i, -1, NodeRed)
		}
	}
	return ans
}

在这里插入图片描述

复杂度分析
  • 时间复杂度: O ( M + N ) O(M+N) O(M+N),M和N分别是图的边数和点数
  • 空间复杂度: O ( N ) O(N) O(N),N为图的点数
方法三、并查集
思路简述

还可以使用并查集来解决此类问题。所谓的并查集就是一种基于数组的数据结构,能够实现快速判断两个元素是否在同一个集合里面,以及将两个集合合并的高效的数据结构。在本题中,当遍历到一个节点的时候,可以考虑将该节点的所有邻居节点都合并到一个集合中,如果有一个邻居节点和自身在同一个集合了,说明此时无法将该邻居节点和自身节点放在同一个集合中。
本题在实现并查集时候,利用parent数组来保存每一个节点的父节点,如果一个父节点的值小于零,说明这是该集合的根节点,并且用该值的相反数来记录集合中元素的个数。这样在合并的过程中,优先将元素数量少的合并到元素数量多的,这样做的好处是避免减少往上寻找根节点过程中比较长的路径。笔者曾经在一场笔试里面,如果用普通的并查集是会超时的,只有用这种优化一点的并查集才能正常通过。

代码实现
type UnionSet struct {
	parent []int
}

// 新建一个长度为n的并查集
func NewUnionSet(n int) *UnionSet {
	parent := make([]int, n)
	for i := 0; i < n; i++ {
		parent[i] = -1
	}
	return &UnionSet{parent: parent}
}

// FindRoot 找到其所在的集合的根节点
func (u *UnionSet) FindRoot(node int) int {
	root := node
	for u.parent[root] >= 0 {
		root = u.parent[root]
	}
	return root
}

// MergeRoot 合并两个集合,传入的应该是集合的根节点
func (u *UnionSet) MergeRoot(root1, root2 int) {
	if u.parent[root1] < u.parent[root2] {  // 说明root2集合元素更多
		u.parent[root2] += u.parent[root1]
		u.parent[root1] = root2
		return
	} 
	u.parent[root1] += u.parent[root2]
	u.parent[root2] = root1
}

// Merge 合并两个集合
func (u *UnionSet) Merge(node1, node2 int) {
	root1 := u.FindRoot(node1)
	root2 := u.FindRoot(node2)
	u.MergeRoot(root1, root2)
}

func (u *UnionSet) InSame(node1, node2 int) bool {
	root1 := u.FindRoot(node1)
	root2 := u.FindRoot(node2)
	return root1 == root2
}


func isBipartite(graph [][]int) bool {
	n := len(graph)
	unionSet := NewUnionSet(n)
	for i := 0; i < n; i++ {
		for _, adjNode := range graph[i] {
			if unionSet.InSame(i, adjNode) {
				return false
			}
			unionSet.Merge(graph[i][0], adjNode)
		}
	}
	return true
}

在这里插入图片描述

复杂度分析
  • 时间复杂度: O ( ( M + N ) l o g N ) O((M+N)logN) O((M+N)logN),M为边数,N为点数,考虑并查集查找的开销理想情况下为 O ( l o g N ) O(logN) O(logN)
  • 空间复杂度: O ( N ) O(N) O(N)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值