前言
我在写一个轻松学习数据结构和算法的系列,结合 iOS 开发的故事,让大家看看工作中有哪些地方会接触到数据结构和算法。
这是本系列的第三篇(下),我们讲讲递归。
在这部分中,我们将讨论几个具体 iOS 开发中应用递归的案例。
以前故事的传递门:
我们先来从一道著名的面试题来说起,这道题目是来自 Google 的面试题,之所以出名,是因为下面这个小故事。
2015 年 6 月 10 日,Homebrew 的作者 @Max Howell (https://twitter.com/mxcl/status/608682016205344768 ) 在 twitter 上发表了如下一内容:
Google: 90% of our engineers use the software you wrote (Homebrew), but you can’t invert a binary tree on a whiteboard so fuck off.
事情大概是说,他去 Google 面试,面试官说:虽然在 Google 有 90% 的工程师用你写的 Homebrew,但是你居然不能在白板上写出翻转二叉树的代码,所以滚蛋吧。
而翻转二叉树就是一道典型的递归题目。我们来看看这道题目的描述:
将如下二叉树,从当前的状态
4
/ \
2 7
/ \ / \
1 3 6 9
变成如下状态
4
/ \
7 2
/ \ / \
9 6 3 1
这道题具体的做法就是:
反转二叉树的左右子树
将左右子树交换
而第 1 步又是一个反转二叉树的问题,所以就可以用递归来处理了。然后再考虑好递归的结束条件(当前结点是空),这道题就可以解决了。
如果你感兴趣,这个网址(https://leetcode.com/problems/invert-binary-tree/)上有这道题目的在线评测,你可以试着做一下。
第二个对于递归的应用来自于真实的开发中,我在工作的时候,发现时不时服务器端的返回的 JSON 合法性会出问题,而因为我们的很多逻辑依赖于这些 JSON 的合法性,所以如果不做检查可能会造成程序逻辑异常,甚至崩溃。
但是检查 JSON 字段合法性的代码其实挺无趣的,都是一堆大量的 [foo isKindOfClass:[NSDictionary class]]
之类的代码,加入大量这样的验证代码,既费时费力,也毫无技术含量。
于是我在开发的时候打算自己定义一个类似 scheme
的东西,来规定 JSON 的合法性。最终我把这个功能实现在了 YTKNetwork 中(https://github.com/yuantiku/YTKNetwork),并把这个功能叫做 JSON Validator。
在使用时,我们只需要返回一个 NSDictionary 表示 JSON 的每个字段的类型,JSON Validator 就可以自动帮我们做检查。
如下是一个例子,我们要求返回的 JSON 里面必须有 id
、imageId
、question
等字段,其中 id
必须是 NSNumber 类型,imageId
必须是包含 NSString 成员的数组类型,而question
必须是一个 NSDictionary。
// 一个较复杂的 JSON 字段定义
- (id)jsonValidator {
return @[@{
@"id": [NSNumber class],
@"imageId": @[
[NSString class]
},
@"time": [NSNumber class],
@"status": [NSNumber class],
@"question": @{
@"id": [NSNumber class],
@"content": [NSString class],
@"contentType": [NSNumber class]
}
}];
}
这个字段定义好之后,我们就可以用 JSON Validator 来检查了。
大家可以想想,如果你要实现这个功能,应该如何实现?
对!就是用递归。
因为 JSON 内容可以无限层级的嵌套,所以 JSON 的检查也必须是递归着一层一层检查。每一层的检查规则和上一层是一样的,所以可以递归调用自己。
以下是这个功能检查的核心代码,完整代码在:https://github.com/yuantiku/YTKNetwork/blob/master/YTKNetwork/YTKNetworkPrivate.m
+ (BOOL)checkJson:(id)json withValidator:(id)validatorJson {
if ([json isKindOfClass:[NSDictionary class]] &&
[validatorJson isKindOfClass:[NSDictionary class]]) {
NSDictionary *dict = json;
NSDictionary *validator = validatorJson;
BOOL result = YES;
NSEnumerator *enumerator = [validator keyEnumerator];
NSString *key;
while ((key = [enumerator nextObject]) != nil) {
id value = dict[key];
id format = validator[key];
if ([value isKindOfClass:[NSDictionary class]]
|| [value isKindOfClass:[NSArray class]]) {
result = [self checkJson:value withValidator:format];
if (!result) {
break;
}
} else {
if ([value isKindOfClass:format] == NO &&
[value isKindOfClass:[NSNull class]] == NO) {
result = NO;
break;
}
}
}
return result;
} else if ([json isKindOfClass:[NSArray class]] &&
[validatorJson isKindOfClass:[NSArray class]]) {
NSArray *validatorArray = (NSArray *)validatorJson;
if (validatorArray.count > 0) {
NSArray *array = json;
NSDictionary *validator = validatorJson[0];
for (id item in array) {
BOOL result = [self checkJson:item withValidator:validator];
if (!result) {
return NO;
}
}
}
return YES;
} else if ([json isKindOfClass:validatorJson]) {
return YES;
} else {
return NO;
}
}
猿题库的做题和解析界面需要复杂的排版,所以我们基于 CoreText 实现了自己的富文本排版引擎。我们的排版引擎对公式、图片和链接有着良好支持,并且支持各种字体效果混排。对于内容中的图片,支持点击查看大图功能,对于内容中的链接,支持点击操作。
下图是我们应用的一个截图,可以看到公式,图片与文字混排良好。
我们是如何设计这个富文本排版引擎的呢?我们在后台实现了一个基于 UBB 的富文本编译器。使用 UBB 的原因是:
UBB 相对于 HTML 来说,虽然功能较简单,但是能完全满足我们对于富文本排版的需求。
做一个 UBB 的语法解析器比较简单,便于我们将 UBB 渲染到各个平台上。
为了简化 iOS 端的实现,我们将 UBB 的语法解析在服务器端完成。服务器端提供了接口,可以直接获得将 UBB 解析成类似 HTML 的 文件对象模型 (DOM) 的树型数据结构。
有了这个树型数据结构,iOS 端渲染就简单多了,无非就是递归遍历树型节点,将相关的内容转换成 NSAttributeString 即可,之后将 NSAttrubiteString 转成 CoreText 的 CTFrame 即可用于界面的绘制。
由于这里面涉及太多我们业务层面的逻辑,在此我就不贴代码了。但是大家可以想像一下这个递归是如何完成的。
递归是一个基础的计算机知识,它很有趣,也很灵活,重要的是它非常强大。希望大家都能够将它应用好,让我们的编程工作都能够高效、优雅的进行。
全文完。
活动推荐:
12月19日,UPYUN·架构与运维大会将巡回深圳,大批享誉业界的技术专家将齐聚一堂,分享技术干货。查看详情&报名参会请【阅读原文】