一个经典编程面试题的“终极”“隐退”

本文探讨了一道经典的编程面试题,要求使用字典将输入字符串分割成单词。原文提出两种解法,时间复杂度分别为O(2^n)和O(n^2)。作者提出了新的优化解法,将时间复杂度降低到O(n),并解释了优化的理由,包括避免重复计算、简化时间复杂度估算和提高执行效率。此外,还针对找到所有成功分词匹配的需求提供了另一种解法。
摘要由CSDN通过智能技术生成


写下这篇文章是因为看到了一篇很有趣的技术文章: 一个经典编程面试题的“隐退”


为不愿意去读原文的读者考虑,我先对原文做一个简单的总结:

这是一个在Endeca、 Google 和LinkedIn等多家公司服务过的编程面试题,题目内容如下

给定一个输入的字符串和一个包含各种单词的字典,用空格将字符串分割成一系列字典中存在的单词。举个例子,如果输入字符串是“applepie”而字典中包含了所有的英文单词,那么我们应该得到返回值“apple pie”。


对原文中提到的简单解法在此略过,原文提到了两种有趣的解法。


一种是通用解法,时间复杂度为O(2^n)

String SegmentString(String input, Set<String> dict) {
  if (dict.contains(input)) return input;
  int len = input.length();
  for (int i = 1; i < len; i++) {
    String prefix = input.substring(0, i);
    if (dict.contains(prefix)) {
      String suffix = input.substring(i, len);
      String segSuffix = SegmentString(suffix, dict);
      if (segSuffix != null) {
        return prefix + " " + segSuffix;
      }
    }
  }
  return null;
}


一种是高效解法,时间复杂度为O(n^2)

Map<String, String> memoized;
 
String SegmentString(String input, Set<String> dict) {
  if (dict.contains(input)) return input;
  if (memoized.containsKey(input) {
    return memoized.get(input);
  }
  int len = input.length();
  for (int i = 1; i < len; i++) {
    String prefix = input.substring(0, i);
    if (dict.contains(prefix)) {
      String suffix = input.substring(i, len);
      String segSuffix = SegmentString(suffix, dict);
      if (segSuffix != null) {
        memoized.put(input, prefix + " " + segSuffix);
        return prefix + " " + segSuffix;
}
}
memoized.put(input, null);
return null;
}


高效解法的代码有一些小瑕疵,一是漏写了一个"}",另外一个是以下这行代码是多余的:

memoized.put(input, prefix + " " + segSuffix);

根据此解法的逻辑,当任何一次方法调用执行到了这行代码,即某个后缀被成功地分词匹配,则必然导致着全体输入串的成功分词匹配。在这个时候保存成功匹配的值是没有意义的,因为此后它不会有机会被从Map中查询了。


但是让我觉得到此为止这个题目还不能真正“隐退”的真正原因并不在此,而是在于这个高效解法仍然有优化的空间,可以将时间复杂度从O(n^2)提升到O(n)。


我的解法1

	Map<String, String> memoized = new HashMap<String, String>();
	String SegmentString(String input, Set<String> dict) {
		if (dict.contains(input)) return input;
		if (memoized.containsKey(input)) {
			return memoized.get(input);
		}
		int len = input.length();
		for (int i = 1; i < len; i++) {
			String prefix = input.substring(0, i);
			if (dict.contains(prefix)) {
				String suffix = input.substring(i, len);
				if (!memoized.containsKey(suffix)) {
					String segSuffix = SegmentString(suffix, dict);
					if (segSuffix != null) 
						return prefix + " " + segSuffix;
				}
			}
		}
		memoized.put(input, null);
		return null;
	}


与原文中的高效解法相比,最主要的改进在于,Map表中仅保存无法匹配的字符串后缀,对于Map表中已保存的字符串后缀,不再递归调用SegmentString方法。

这绝不仅仅是一个将这几行代码移到那里的小把戏,基于以下三个原因,这个改动是非常重要的。

1) 我们将一些中间结果(如无法匹配的字符串后缀)缓存在Map中的动机是什么?在原文解法中,缓存是为了避免重复的计算。在我的解法中,缓存是为了同时避免重复计算以及降低算法复杂度。

2) 我相信,原文中的两个解法的时间复杂度的计算过程已经难住了不少人(有兴趣的人可以自行验证,本文不打算就此进行详细说明)。但这一点没什么可自豪的。恰恰因为它们的解法不够简明,所以才会造成估算时间复杂度的困难。在我的解法中,保证了对任何一个字符串后缀只会调用一次SegmentString方法,因此给定一个长度为n的字符串,对此方法的调用不会超过n次。它的时间复杂度是非常易于估算的。

3) 最重要的一点是,代码执行的时间和空间效率确实会有明显提高。因为递归调用的次数降低了整整一个数量级。


以上的所有解法都是基于原文提出的解题要求:找到任意一个成功的分词匹配。在实际应用中,找到所有的成功分词匹配经常是有必要的。这是一个更难的要求,在这个时候,缓存成功的匹配结果就是必要的了。我也针对对这个需求做了小小尝试,解法如下:


我的解法2

	Map<String, List<String>> cached = new HashMap<String, List<String>>();
	List<String> AdvancedSegmentString(String input, Set<String> dict) {
		List<String> list=null;
		
		if (dict.contains(input)) {
			list = new ArrayList<String>();
			list.add(input);
		}
		int len = input.length();
		for (int i = 1; i < len; i++) {
		    String prefix = input.substring(0, i);
		    if (dict.contains(prefix)) {
		    	String suffix = input.substring(i, len);
		    	List<String> segSuffix = cached.containsKey(suffix)? cached.get(suffix) : AdvancedSegmentString(suffix, dict);
		    	if (segSuffix != null) {
		    		if (list == null)
		    			list = new ArrayList<String>();
		    		for (int j=0; j<segSuffix.size(); j++) {
		    			list.add(prefix + " " + segSuffix.get(j));
		    		}
		    	}
		    }
		}
		cached.put(input, list);
		return list;
	}

注: 这个解法是以不考虑可能的解法个数过多导致内存溢出为前提的。

我觉得综合我的解法1和我的解法2,才是这个编程题的真正“终极隐退”。当然也许我仍然没有达到这一目标,我很欢迎有人能证明这一点。递进式地挖掘编程之美,正是编程的魅力所在。




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值