算法编程题-分割数组、判断二分图
本文将对两道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)
168万+

被折叠的 条评论
为什么被折叠?



