首页 最长公共前缀
文章
取消

最长公共前缀

问题

https://leetcode-cn.com/problems/longest-common-prefix

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ”“。

示例 1:

输入: [“flower”,”flow”,”flight”]
输出: “fl”

示例 2:

输入: [“dog”,”racecar”,”car”]
输出: “”
解释: 输入不存在公共前缀。

说明: 所有输入只包含小写字母 a-z 。

解法

这是一个 easy 的题目,自己做出来了,但看到题解后发现原来还有很多清奇思路。

横向比较

LCP(S1,S2...Sn) 表示字符串 S1,S2...Sn 的最长公共前缀,那么一定有 LCP(S1,S2...Sn)=LCP(LCP(LCP(S1,S2),S3),…Sn)。可以先让第一个字符串作为公共前缀,和第二个比较得到一个公共前缀,用这个新公共前缀和第三个字符串比较得到一个新的公共前缀……一直到公共前缀为空或比较完最后一个字符串。公共前缀在比较过程中会越来越小。

有几种情况需要处理:

  • 字符串数组为空
  • 字符串数组只有1个字符串
  • 公共前缀为空串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
	"fmt"
	"math"
)

func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	if len(*data) == 1 {
		return (*data)[0]
	}
	lcp := (*data)[0]
	for i := 1; i < len(*data); i++ {
		lcp = getCommonPrefix(&lcp, &(*data)[i])
		if lcp == "" {
			return lcp
		}
	}
	return lcp
}

func getCommonPrefix(s1, s2 *string) string {
	minLen := int(math.Min(float64(len(*s1)), float64(len(*s2))))
	compareIdx := 0
	for compareIdx < minLen {
		if (*s1)[compareIdx] == (*s2)[compareIdx] {
			compareIdx++
			continue
		}
		break
	}
	return (*s1)[:compareIdx]
}

func main() {
	s1 := "book"
	s2 := "bool"
	r1 := getCommonPrefix(&s1, &s2)
	fmt.Printf("%s %s,common prefix:%s \n", s1, s2, r1)  // bo

	r2 := longestCommonPrefix(&[]string{"book", "bool", "bob", "bor", "abc"})
	fmt.Printf("lcp: %s \n", r2)  // 空字符串

	r3 := longestCommonPrefix(&[]string{"book", "bool", "bob", "bor"})
	fmt.Printf("lcp: %s \n", r3)  // bo
}

时间复杂度 \(O(mn)\),m为字符串的平均长度,n为数组长度;空间复杂度 \(O(1)\)。

上面的:

1
2
3
4
5
6
7
8
9
10
if len(*data) == 1 {
  return (*data)[0]
}
lcp := (*data)[0]
for i := 1; i < len(*data); i++ {
  lcp = getCommonPrefix(&lcp, &(*data)[i])
  if lcp == "" {
    return lcp
  }
}

其实代码可以简化为:

1
2
3
4
5
6
7
lcp := (*data)[0]
for i := 0; i < len(*data); i++ {
  lcp = getCommonPrefix(&lcp, &(*data)[i])
  if lcp == "" {
    return lcp
  }
}

只是后者比前者多一次无谓的自己和自己比的检测,从效率角度来说不推荐。

纵向比较

我们最常想到的就是纵向比较,即纵向着一个一个比较字符,直到某一列(索引)上有字符串有不同字符为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	size := len(*data)
	if size == 1 {
		return (*data)[0]
	}
	tmpStr := (*data)[0]
	tmpLength := len(tmpStr)
	for i := 0; i < tmpLength; i++ {
		char := tmpStr[i]
		for j := 1; j < size; j++ {
			if i == len((*data)[j]) || (*data)[j][i] != char {
				// 某个字符串已经到头了,或者遍历到的字符在某个字符串上不匹配,可以退出了
				return tmpStr[:i]
			}
		}
	}
	// 数组内字符串都一样
	return tmpStr
}

时间复杂度仍然是 \(O(mn)\),空间复杂度仍然为 \(O(1)\)。

分治

该问题除了满足 LCP(S1,S2...Sn)=LCP(LCP(LCP(S1,S2),S3),…Sn) 外,其实也满足 LCP(S1,S2…Sn)=LCP(LCP(S1,…Sk),LCP(Sk+1…Sn)),其中 1<k<n

这正好适合用分治法和递归来解决问题,但是 k 怎么取呢?那么取个中间值吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	size := len(*data)
	if size == 1 {
		return (*data)[0]
	}
	if size == 2 {
		return getCommonPrefix(&(*data)[0], &(*data)[1])
	}
	mid := size >> 1
	left := (*data)[:mid]
	right := (*data)[mid:]
	leftLCP := longestCommonPrefix(&left)
	rightLCP := longestCommonPrefix(&right)
	return getCommonPrefix(&leftLCP, &rightLCP)
}

func getCommonPrefix(s1, s2 *string) string {
	minLen := int(math.Min(float64(len(*s1)), float64(len(*s2))))
	compareIdx := 0
	for compareIdx < minLen {
		if (*s1)[compareIdx] == (*s2)[compareIdx] {
			compareIdx++
			continue
		}
		break
	}
	return (*s1)[:compareIdx]
}

时间复杂度仍然是 \(O(mn)\),空间复杂度仍然为 \(O(mlogn)\)。

前缀比较

把第一个字符串当成最长公共前缀,查找这个最长公共前缀在其它字符串的索引,索引为0则是前缀,不为0则不是前缀,从右边缩短最长公共前缀,再次尝试,直到最终循环完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	size := len(*data)
	// 查找最小字符串长度
	tmpStr := (*data)[0]
	for i := 1; i < size; i++ {
		for strings.Index((*data)[i], tmpStr) != 0 {
			tmpStr = tmpStr[:len(tmpStr)-1]
		}
	}
	return tmpStr
}

比较最大和最小字符串

先对字符串数组排序,得到最大和最小的字符串。对这两个字符串按字符遍历比较,直到找到字符不同或遍历结束。

注意:遍历的长度最大为最小字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	// 对字符串数组排序
	sort2.Strings(*data)
	// 第一个是最小的
	smallestStr := (*data)[0]
	// 最后一个是最大的
	biggestStr := (*data)[len(*data)-1]

	idx := 0
	for idx < len(smallestStr) && smallestStr[idx] == biggestStr[idx] {
		idx++
	}
	return smallestStr[:idx]
}

二分查找

二分查找???

等等,先想想。

最长公共前缀的长度一定小于等于最短字符串的长度。设最短字符串长度为 minLength,每次查找时取中间值 mid,判断每个字符串的 [:mid] 是否都相同,若相同,则最长公共前缀的长度一定在 [mid,minLength] 区间内。然若有不同,则最长公共前缀的长度在 [0,minLength) 区间内。这样就缩小了查找范围。直到找到正好的最长公共前缀的长度,于是也就找到了最长公共前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func longestCommonPrefix(data *[]string) string {
	if data == nil || len(*data) == 0 {
		return ""
	}
	size := len(*data)
	if size == 1 {
		return (*data)[0]
	}
	// 查找最小字符串长度
	tmpStr := (*data)[0]
	minLength := len(tmpStr)
	for i := 1; i < len(*data); i++ {
		if len((*data)[i]) < minLength {
			minLength = len((*data)[i])
		}
	}
	leftIdx, rightIdx := 0, minLength
	for leftIdx < rightIdx {
		mid := (leftIdx + rightIdx + 1) >> 1 // 取平均值
		if isCommonPrefix(data, mid) {
			// 有公共前缀,往大搜索
			leftIdx = mid
		} else {
			// 没有公共前缀,往小搜索
			rightIdx = mid - 1
		}
	}
	// 最终长度为 left
	return tmpStr[:leftIdx]
}

// 字符串们是否在 length 长度范围内都有公共前缀
func isCommonPrefix(data *[]string, length int) bool {
	tmp := (*data)[0][:length]
	for i := 1; i < len(*data); i++ {
		if (*data)[i][:length] != tmp {
			// 发现有不同的前缀
			return false
		}
	}
	return true
}
本文由作者按照 CC BY 4.0 进行授权