0%

增长黑客

第1章:概念

定义

以数据驱动营销,以市场指导产品,通过技术化手段贯彻增长目标的人。

增长目标

『AARRR』转化漏斗模型

  • Acquisition 获取用户
  • Activation 激发活跃
  • Retention 提高留存
  • Revenue 增加收入
  • Referral 传播推荐

能力特质

(Andy Johns的增长黑客生涯):facebook小挂件,向潜在用户发邮件广告推广,Twitter的着陆页改进(关注注册登录),新用户注册推荐用户,开发内部群发邮件系统,Quora捉摸用户行为模式,归纳为标准流程,引导其他用户执行流程

  • 数据为王:具有数据思维,一切工作建立在数据分析之上
  • 专注目标:时刻围绕增长目标展开,不断的测试、改进,学习、再测试
  • 关注细节:对细小改动做出评估
  • 富于创意:缜密分析+天马行空的增长方案
  • 信息通透:深入理解产品用户的活跃渠道,关注新生渠道和业界趋势

一切以数据说话

  • 明确分析目的

  • 了解数据来源的相关信息

    • 强调核心指标:建立在品类特性和自身提供服务的核心价值之上
    • 确定上报机制:时机、内容、技术实践形式
  • 数据分析方法

    • 定性分析:对事物性质进行判断
    • 定量分析: 对事物数量做统计

团队定位

游走在产品、运营、研发、设计、用户研究之间的多面手,比产品更关注能带来数据增长的功能特性,比运营更倾向于数据中探索新的增长机会,比设计师更少关注感观层面的艺术性,长于用户研究

招聘经验以及发展要素

  • 要求:具备技术、产品、营销思维、传播和分享的精神(案例为Teambition , WIFI万能钥匙的招聘经验)
  • 如何培养:熟悉产品和技术的市场影响,有产品干和市场思维,培养横向跨界知识,纵向某一领域专攻。软实力为:热情、聪明、好奇、收集资源、影响力、心态开放、内心强大、轻微强迫症
  • 常用的工具箱:Google Analytics , Mixpanel , KissMeteics , UserCycle , Customer.io , Optimize.ly , Basscamp , 友盟

第2章:产品

PMF

所以,应当先做好产品的验证,达到了PMF状态再全力推广。

  • 也就是产品与市场的完美切合度,Product / market fit。从早期用户得到反馈,利用最小的成本持续改进产品。
  • 成功案例: instagram的转型,砍掉冗余功能,把用户喜欢的照片分享功能拿出来做
  • 失败案例: 叮咚小区,前期粗暴的线下广告投放,但是产品的基础功能不完善,用户体验极差。

需求

  • 需求是真实存在的还是伪需求(QQ邮箱将邮件附件单列出来,但几乎没人用)
  • 需求是否属于刚需
  • 需求是否足够大:估计目标用户的基数、消费能力、意愿预算;评估进入市场的原有规模和新的规模、借助排行榜和搜索热度了解需求(YY 向视频直播平台转变)
  • 需求的变现能力如何

MVP

也就是最小化的可行产品来验证需求。同时应当注意,在产品早期应当尽快适配新环境,案例:多拉口袋的iOS8崩溃问题。

  • 基本功能: 在最短时间内完成,如Dropbox通过仅完成宣传视频,Groupon 通过WordPress建站,排版软件制作礼券等,大众点评3天做出网页,数据来源于旅游手册,微信游戏近似于图片拼凑视觉稿。这几种常见的方式:伪造网站,不提供实际的功能;用户点击按钮后告知正在开发,留下电子邮箱;调研用户呼声最高的功能,然后成为种子用户;基于微信做MVP的开发(无需适配,分发方便,便于收集反馈,数据可以沉淀,开发成本低)
  • 反馈渠道:内部反馈机制等
  • 官方公告: 群体公告 + 单用户通知
  • 自动升级: 启动时提示更新
  • 使用行为统计

第3章:获取用户

种子用户

通过冷启动来获取第一批用户和制作过程,早期的种子用户的质量将决定产品初期的氛围、运营走向、以及日后的发展策略,案例如:知乎的早期邀请制度,哔哩哔哩的答题机制,小米的种子用户反馈。

但是一定要警惕『产品蝗虫』,也就是目标客户之外的围观群众,他们将占用原有的目标用户名额,伤害到社区的生态,抠一些没用的『细节』,行为会对产品的决策产生误导。

从最笨的事情做起

  • 聚美优品,写BB霜的软文,提供化妆品的高质量美图
  • Strikinfly,寻找超级粉丝,线上线下聊天
  • 云笔记,邀请内测用户成立反馈协助群
  • Airbnb , 挨家挨户的上门拍照

社交红利与数据抓取

与社交网络整合,获取用户量、关系链和行为数据(Spotify用facebook 登陆,Zynga用Facebook的开放平台制作社交游戏)

第三方社交网络的账号登陆,达成移动应用的分享和回流(啪啪,使用微博QQ登陆,挖掘关系链潜力)

数据抓取,可以保证产品初期启动的数据量,以及抢占先机,或者争取资源(微软关闭空间主页服务的时候,网易迅速提供『一键搬家』,追TA抓取唱吧的用户头像)

内容营销

其作用是吸引流量,培养潜在用户,劝诱转化,所以应当讲用户爱听的故事,如官方微博输出用户喜欢的图文(知乎)。

注意事项:明确目标受众,持续稳定输出高质量内容,标题的撰写技巧,保证文章长度(2400),数据分享与互动(有让人讨论的空间,如知乎刊发有争议的文章),选择合适的发布渠道。

搜索的优化

分为搜索引擎的优化和应用商店的优化。

搜索引擎的优化案例,Facebook 针对用户资料的页面优化,twitter自动提交热门标签给搜索引擎,TochCrunch通过wiki百科建立反向链接。

应用商店的优化案例,如大姨吗,通过副标题的修改推关键词,大众点评『更新跑得快』活动。

文案的撰写

  • 简要概述(100字内)
  • 话题事件
  • 核心特色
  • 主要功能
  • 团队访谈
  • 未来计划
  • 推广活动
  • 转化引导

更多的技巧

  • 捆绑下载:如豌豆荚捆绑下载其APP
  • 排队机制,饥饿营销: Mailbox显示排队人数,Trak.io付费插队,邀请好友插队。
  • 嵌入式代码和小挂件: Youtube分享
  • 线上到线下
  • 海外的扩张: 猎豹,Facebook等

第4章:激发活跃

A/B测试

但注意,不要过度依赖AB测试。

  • 提供两个方案并行测试,
  • 变量唯一排除其他干扰因素,
  • 有判定结果优劣的标准,
  • 移动应用也可以借助HTML5快速验证需求。
  • 几个典型案例如37Signals 『注册高额回报账户』vs 『所有账户享受30天免费使用』,ZAGG 商品的静态图片 vs 360度图片 , OkDork 先注册后进网站的测试,根据行为热点图去掉导航栏的测试。移动端如百姓网利用HTML5容器实现A/B测试。

独辟蹊径的技术

在技术瓶颈下通过巧妙的手段破除用户使用功能的障碍,降低用户活跃的门槛。

  • SKype『伪立体声』,提高清晰度,击败了其他同类服务
  • QQ音乐利用专辑图片实现了IOS的『锁屏歌词』
  • WIFI万能钥匙通过截图加上OCR技术识别WIFI热点,同时利用GPS+云服务获取附近的热点

补贴

有直接的现金补贴,快速获取用户,激发活跃度,也就是如返利和嘀嘀快的的补贴大战。

也有新玩法的红包补贴,通过社交关系链的传播,比如嘀嘀打车的补贴新玩法,打车红包。

游戏化与脚本自动化运营

《游戏改变世界》提到的游戏四大特征: 目标、规则、反馈系统、自愿参与。

比如星巴克的线下会员成长体系,同时还开发了app把线下的游戏化搬到了线上。移动签到应用Foursquare 通过积分排名,徽章挑战和抢夺地主的游戏设计抓取用户忠诚度。滴滴打车提供的『滴米』系统等等。

而产品在早期社区通过团队自己来运营启动,后期就可以通过写脚本来实现自动化运营,一个有趣的运营案例是豌豆荚打造的贴吧神兽,其实就是一个自动聊天的机器人,获得了几十万的关注度。

第5章:提高留存

用户留存率

用户流逝的原因

  • 程序漏洞,性能瓶颈:Color基本流程都跑不通
  • 用户频繁被骚扰: 新浪微博的各种通知、广告、推送、话题等
  • 产品热度消减: 『你画我猜』的日活锐减
  • 更好的替代产品 : eBay被淘宝击败
  • 其他原因

留存率的衡量标准

  • 次日留存(40%)
  • 7日留存(20%)
  • 30日留存(10%)

产品性能优化

优化内容包括:用户终端性能、网络性能、流量压缩、安装包空间、界面设计与布局、其他。

如Facebook为非洲用户的优化,减少了启动速度,提供了预加载,压缩图片,优先家在适合当前分辨率的照片,优化安装包大小,其实就是前边所说的优化内容。

再如 Instagram 的安卓版优化,扁平化设计,重新布局精简架构专注内容,延迟加载不必要的模块(方法追踪method tracing 和时点声明 Timing Statements 技术)。

有损服务——放下不必要的坚持

定义:刻意输出在品质上存在某些损失的服务,换取其他方面的优化。

原则

  • 优先保证核心功能的运转
  • 牺牲的特性越少越好,在条件允许的情况下

几个经典的案例如:QQ农场在高峰期提供的静态默认列表,微信的高峰期不强求信息发送顺序的一致,小米在抢购的时候不提供剩余手机的精确统计(也有可能是饥饿营销),刀塔传奇的大版本升级采用『低清晰度版本』保证用户尽早升级。

引导用户上手 社交维系与社交解绑

Twitter注册完成后的推荐用户,知乎的推荐订阅,都是引导用户快速上手的例子。

社交维系与社交解绑是对立的概念,前者如全民飞机大战,『啪啪』提示新加入的微博好友,Facebook在注销账号页面显示亲密好友的照片,通过社交手段维系用户留存。而same则通过解绑社交的方式,提高留存。

唤醒机制

互联网产品中专为召回流失用户而设计的产品机制。

电子邮件召回EDM

  • 提供奖励,Pocket 用高级付费账户试用来吸引你
  • 告知进展,IFTTT增加Nest支持,Evernote推出商业版等方式
  • 个性化推荐,如知乎的每周精选,淘宝的同类商品。
  • 社交互动提示,如Twitter定期发送未处理消息,Facebook中通知用户『有一张照片中圈了你』。
  • 优秀的第三方服务商包括 TinyLetter,MailChimp等,EDM要注意内容,和取消订阅方式。

消息推送通知

  • 推送授权,推荐试用简单的浮层或者弹窗提示用户获取权限后的使用目的。
  • 徽章通知,也就是角标,利用用户的强迫症。
  • 本地通知,在预设的时间点通知,如刀塔传奇每天的三个时间点推送通知。
  • 地理围栏通知
  • 图片推送通知
  • 表情文字通知

网页内唤醒移动应用

iOS有直接的URL Scheme技术来实现,安卓上有四种方式,分别是:拦截http跳转、自定义scheme、Crome Internet ,内嵌http服务。如知乎,哔哩哔哩等。

第6章:增加收入

免费的世界

互联网与免费的午餐

互联网可以降低信息传播的承办本,海量用户摊薄了边际成本,人的本性对免费产品更感兴趣以及更喜欢『免费』的国人。

免费又赚钱的策略

  • 基本功能免费,高级功能收费的Freemium策略,比如Evernote,QQ,Flickr等等。
  • 交叉补贴,《征途》的游戏免费,道具收费。
  • 三方市场的流量变现,例子就是搜狗的『三级火箭』策略,通过输入法来带动搜狗浏览器下载,利浏览器下载带动搜狗搜索的使用量(虽然我巨恶心搜狗的这个行为,但是这种变现的方式确实是最佳方式)
  • 开源代码的盈利可能,如厂商定制开发收费,比如卖技术说明文档,或者接受用户捐赠(其实这个不算是运营了,纯粹是情怀)
  • 公司上市或者是被收购

免费午餐的终结

在产品足够好的情况下,可以试着完全掐断免费。

重定向广告

也就是cookie和url分析,业界对此存在两种声音,一种认为增加了收入,另一种则担心用户隐私的泄露。

几个营销案例

Wet Seal

『和友人一起逛街』的社交购物,虚拟的DIY服务,Etsy根据社交资料精准推荐礼物。

罗辑思维的微信月饼

这也是一次粉丝经济的催生,分别尝试了找人代付,多人代付,随机送礼,加入游戏思维的方式完成了一次精彩的营销。

面对盗版的营销方式

腾讯给『非法』的QQ会员提供了八折开通会员的服务,而CleanmyMac 给盗版用户提供半价的优惠。面对盗版的变惩为奖的几个原则是:绝不责备用户,给予合理补偿,提供转化便利

建立商业智能系统

用数据可视化的方式辅助企业管理,如百姓网建立内部的数据化营销系统,提高了业务增长。

第7章:病毒营销

核心指标

K因子:评判病毒传播的覆盖面,等于感染率*转化率。

病毒传播周期: 用户发出病毒邀请,到新用户完成转化所花费的时间。(引爆点提到的个别人物法则,附着力法则和环境威力法则)。

几个案例

坏事传千里,百度的Bug营销,利用一个假装的bug,百度云获得了大量的新注册用户和铁杆粉丝,以及完整的流程体验,大量的用户个人文件。

借势营销,利用时机,比如各种旅游产品借助阿里的去啊做热点营销,猎豹移动专门推出的春节抢票版。segmentFault的光棍节程序员闯关秀。(如今打开segmentfault.com/game会有一个新的闯关游戏,不过也是很难的)

构建产品体系外的病毒循环,追TA的整蛊朋友圈游戏。三大考验

  • 创意来源
  • 生命周期
  • 产品契合度:知乎的财务包子铺与知乎的《金钱有术》。

产品内置的传播因子。

  • 外置的传播因子有几个缺陷,渠道特性与产品特性不匹配,获得的用户参差不齐;渠道传播与下载转化之间可能存在断链(朋友圈可能屏蔽一些下载);渠道传播的策划固然成功,但是用户可能对产品本身缺乏兴趣。所以借用内置的传播因子可能更好,如AirDrop的推荐产品解锁高级功能,美图秀秀内建的可分享到朋友圈的小功能。

邮件提醒增强传播效率

  • SpringSled 好友邀请提高排队名次,通过有效的邮件跟踪提高用户数量。

病毒传播中的用户心理把握

  • 喜爱:游戏,文学等大ip的作品,转化率比较高。
  • 逐利:如Groupon的邀请好友奖励10美元等。
  • 互惠: 是追逐利益的一种变体,基于理性经济人假说的传统经济学认为,经济行为主体是单纯追求个人利益最大化的,人们的复杂性为和社会参与,都基于成本收益的计算。如dropbox的推广获得额外空间。
  • 求助:手机游戏,免费游戏,续命或者是买体力要钱。
  • 炫耀: 支付宝每年一度的晒单
  • 稀缺: 稀缺资源会引起争抢,如Gmail,Dribbble初期仅允许被邀请者发表作品。
  • 害怕失去或错过: EverMemo 的互相扫描二维码解锁高级功能。
  • 懒惰: 严肃的商业内容,大多数用户倾向于复制部分文字和链接再发送,偏重知识性娱乐性的网站,人们优先使用分享按钮分享他们看到的东西。

第8章:案例集

最后,作者复盘了几个完整的案例集,Airbnb ,Tinder ,Github,美丽说,外卖库,每一个都值得看几遍,了解一个成功的增长是如何做到的。

1. 字符串问题具有广泛性特点

  • 字符串可以看成字符类型的数组,与数组排序、查找、调整有关
  • 很多其他类型的面试题可以看做字符串类型的面试题

2. 字符串的概念

  • 回文
  • 子串(连续)
  • 子序列(不连续)
  • 前缀树(Trie树)
  • 后缀树和后缀数组
  • 匹配
  • 字典序

3. 字符串的操作

  • 与数组有关的操作:增删改查
  • 字符的替换
  • 字符串的旋转

4. 字符串题目的常见类型

4.1. 规则判断

  • 判断字符串是否符合整数规则
  • 判断字符串是否符合浮点数规则
  • 判断字符串是否符合回文字符串规则

4.2. 数字运算

  • int和long类型表达整数范围有限,所以经常用字符串实现大整数
  • 与大整数有关的加减乘除操作,需要模拟笔算的过程

4.3. 与数组操作有关的类型

  • 数组有关的调整,排序等操作需要掌握
  • 快速排序的划分过程需要掌握和该写

4.4. 字符计数类型

  • 哈希表
  • 固定长度的数组,C/C++(256长度),Java(65535长度)
  • 滑动窗口问题
  • 寻找无重复字符子串问题
  • 计算变位词问题

4.5. 动态规划类型

  • 最长公共子串
  • 最长公共子序列
  • 最长回文子串
  • 最长回文子序列

4.6. 搜索类型

  • 宽度优先搜索
  • 深度优先搜索

4.7. 高级算法与数据结构解决的问题

  • Manacher算法解决最长回文子串问题
  • KMP算法解决字符串匹配问题
  • 前缀树结构
  • 后缀树和后缀数组
  • 通常面试中很少出现

5. 代码练习

5.1. 拓扑结构相同子树

LeetCode 572. 另一个树的子树

对于两棵彼此独立的二叉树A和B,请编写一个高效算法,检查A中是否存在一棵子树与B树的拓扑结构完全相同。
给定两棵二叉树的头结点A和B,请返回一个bool值,代表A中是否存在一棵同构于B的子树。

最优解法为二叉树序列化 + KMP算法

对比KMP算法太复杂了,也可以考虑将问题转化为比较两个树是否相等的思路,利用递归来解答就简单很多。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package personalwebsite.string;

/**
* Created by liyou on 16/2/21. 拓扑结构相同子树问题
* <p>
* [LeetCode 572. 另一个树的子树](https://leetcode-cn.com/problems/subtree-of-another-tree/)
* <p>
* 对于两棵彼此独立的二叉树A和B,请编写一个高效算法,检查A中是否存在一棵子树与B树的拓扑结构完全相同。
* 给定两棵二叉树的头结点A和B,请返回一个bool值,代表A中是否存在一棵同构于B的子树。
*/
public class SubTreeDemo {

// 递归法
public boolean isSubtree(TreeNode s, TreeNode t) {
if (t == null) return true; // t 为 null 一定都是 true
if (s == null) return false; // 这里 t 一定不为 null, 只要 s 为 null,肯定是 false
return isSubtree(s.left, t) || isSubtree(s.right, t) || isSameTree(s, t);
}

/**
* 判断两棵树是否相同
*/
public boolean isSameTree(TreeNode s, TreeNode t) {
if (s == null && t == null) return true;
if (s == null || t == null) return false;
if (s.val != t.val) return false;
return isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}

// 最优解法为二叉树序列化 + KMP算法
public boolean chkIdentical(TreeNode t1, TreeNode t2) {
String t1Str = serialByPre(t1);
String t2Str = serialByPre(t2);
return getIndexOf(t1Str, t2Str) != -1;
}

private String serialByPre(TreeNode head) {
if (head == null) {
return "#!";
}

String res = head.val + "!";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}

// KMP
private int getIndexOf(String s, String m) {
if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
return -1;
}
char[] ss = s.toCharArray();
char[] ms = m.toCharArray();

int[] nextArr = getNextArray(ms);
int index = 0;
int mi = 0;
while (index < ss.length && mi < ms.length) {
if (ss[index] == ms[mi]) {
index++;
mi++;
} else if (nextArr[mi] == -1) {
index++;
} else {
mi = nextArr[mi];
}
}
return mi == ms.length ? index - mi : -1;
}

private int[] getNextArray(char[] ms) {
if (ms.length == 1) {
return new int[]{-1};
}
int[] nextArr = new int[ms.length];
nextArr[0] = -1;
nextArr[1] = 0;
int pos = 2;
int cn = 0;
while (pos < nextArr.length) {
if (ms[pos - 1] == ms[cn]) {
nextArr[pos++] = ++cn;
} else if (cn > 0) {
cn = nextArr[cn];
} else {
nextArr[pos++] = 0;
}
}
return nextArr;
}

static class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
}

5.2. 词语变形

LeetCode 242. 有效的字母异位词

所谓变形词就是两个字符串中的字符出现的种类一样,次数也一样。

对于两个字符串A和B,如果A和B中出现的字符种类相同且每种字符出现的次数相同,则A和B互为变形词,请设计一个高效算法,检查两给定串是否互为变形词。
给定两个字符串A和B及他们的长度,请返回一个bool值,代表他们是否互为变形词。

测试样例:
“abc”,3,”bca”,3
返回:true

关键点在于使用哈希表做字符计数(可以用固定长度的数组代替哈希表结构,时间复杂度为O(n),额外空间复杂度为O(n))

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
package personalwebsite.string;

/**
* Created by liuzhif on 16/2/21.词语变形
* <p>
* [LeetCode 242. 有效的字母异位词](https://leetcode-cn.com/problems/valid-anagram/)
* <p>
* 对于两个字符串A和B,如果A和B中出现的字符种类相同且每种字符出现的次数相同,则A和B互为变形词,请设计一个高效算法,检查两给定串是否互为变形词。
* 给定两个字符串A和B及他们的长度,请返回一个bool值,代表他们是否互为变形词。
* 测试样例:
* "abc",3,"bca",3
* 返回:true
*/
public class WordTransformDemo {

public static void main(String[] args) {
WordTransformDemo demo = new WordTransformDemo();
String a = "ac";
String b = "bb";
System.out.println(demo.isAnagram(a, b));
}

public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;

int[] array = new int[26];
for (int i = 0; i < s.length(); i++) {
array[s.charAt(i) - 'a']++;
array[t.charAt(i) - 'a']--;
}

for (int j : array) {
if (j != 0) return false;
}

return true;
}

}

5.3. 两串旋转

LeetCode 796. 旋转字符串

所谓旋转词就是一个字符串,左边任意长度的子串挪到右边去,都叫这个字符串的旋转词。

如果对于一个字符串A,将A的前面任意一部分挪到后边去形成的字符串称为A的旋转词。比如A=”12345”,A的旋转词有”12345”,”23451”,”34512”,”45123”和”51234”。对于两个字符串A和B,请判断A和B是否互为旋转词。
给定两个字符串A和B及他们的长度lena,lenb,请返回一个bool值,代表他们是否互为旋转词。

测试样例:
“cdab”,4,”abcd”,4
返回:true

关键点在于生成str1+str1大字符串,再利用KMP算法判断是否包含str2(最优时间复杂度为O(n))

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package personalwebsite.string;

/**
* Created by LeeeYou on 2016/3/13. 两串旋转练习题
* <p>
* [LeetCode 796. 旋转字符串](https://leetcode-cn.com/problems/rotate-string/)
* <p>
* 如果对于一个字符串A,将A的前面任意一部分挪到后边去形成的字符串称为A的旋转词。比如A="12345",A的旋转词有"12345","23451","34512","45123"和"51234"。对于两个字符串A和B,请判断A和B是否互为旋转词。
* 给定两个字符串A和B及他们的长度lena,lenb,请返回一个bool值,代表他们是否互为旋转词。
* 测试样例:
* "cdab",4,"abcd",4
* 返回:true
*/
public class RotationStrDemo {

public static void main(String[] args) {
RotationStrDemo demo = new RotationStrDemo();
System.out.println(chkRotation("love", 5, "elo", 5));
System.out.println(demo.rotateString("love", "elo"));
}

public static boolean chkRotation(String str1, int n, String str2, int m) {
if (n != m) return false;
String a = str1 + str1;
return getIndexOf(a, str2) != -1;
}

// KMP Algorithm
public static int getIndexOf(String s, String m) {
if (s.length() < m.length()) {
return -1;
}
char[] ss = s.toCharArray();
char[] ms = m.toCharArray();
int si = 0;
int mi = 0;
int[] next = getNextArray(ms);
while (si < ss.length && mi < ms.length) {
if (ss[si] == ms[mi]) {
si++;
mi++;
} else if (next[mi] == -1) {
si++;
} else {
mi = next[mi];
}
}
return mi == ms.length ? si - mi : -1;
}

public static int[] getNextArray(char[] ms) {
if (ms.length == 1) {
return new int[]{-1};
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int pos = 2;
int cn = 0;
while (pos < next.length) {
if (ms[pos - 1] == ms[cn]) {
next[pos++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[pos++] = 0;
}
}
return next;
}

public boolean rotateString(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}
}

5.4. 句子的逆序

LeetCode 151. 翻转字符串里的单词

对于一个字符串,请设计一个算法,只在字符串的单词间做逆序调整,也就是说,字符串由一些由空格分隔的部分组成,你需要将这些部分逆序。
给定一个原字符串A和他的长度,请返回逆序后的字符串。

测试样例:
“dog loves pig”,13
返回:”pig loves dog”

1. 两次翻转法:关键点在于实现将字符串局部所有字符逆序的函数f
2. 系统api也很好用🐶
3. 双端队列法,牛得一批

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package personalwebsite.string;

import java.util.*;

/**
* Created by LeeeYou on 2016/3/13. 句子的逆序练习题
* <p>
* [LeetCode 151. 翻转字符串里的单词](https://leetcode-cn.com/problems/reverse-words-in-a-string/)
* <p>
* 对于一个字符串,请设计一个算法,只在字符串的单词间做逆序调整,也就是说,字符串由一些由空格分隔的部分组成,你需要将这些部分逆序。
* 给定一个原字符串A和他的长度,请返回逆序后的字符串。
* 测试样例:
* "dog loves pig",13
* 返回:"pig loves dog"
*/
public class ReverseDemo {

public static void main(String[] args) {
ReverseDemo demo = new ReverseDemo();

String str1 = "dog loves pig";
String str2 = "I'm a pig.";
String str3 = " a good example ";

System.out.println(demo.reverseWords(str1));
System.out.println(demo.reverseWords(str2));
System.out.println(demo.reverseWords(str3));

System.out.println();
System.out.println(demo.reverseWords2(str1));
System.out.println(demo.reverseWords2(str2));
System.out.println(demo.reverseWords2(str3));

System.out.println();
System.out.println(demo.reverseWords3(str1));
System.out.println(demo.reverseWords3(str2));
System.out.println(demo.reverseWords3(str3));
}

// 双端队列法
public String reverseWords3(String s) {
int left = 0, right = s.length() - 1;
while (left <= right && s.charAt(left) == ' ') ++left; // 去掉字符串开头的空白字符
while (left <= right && s.charAt(right) == ' ') --right; // 去掉字符串末尾的空白字符

Deque<String> d = new ArrayDeque<>();
StringBuilder word = new StringBuilder();

while (left <= right) {
char c = s.charAt(left);
if ((word.length() != 0) && (c == ' ')) {
// 将单词 push 到队列的头部
d.offerFirst(word.toString());
word.setLength(0);
} else if (c != ' ') {
word.append(c);
}
++left;
}
d.offerFirst(word.toString());
return String.join(" ", d);
}

// 系统api
public String reverseWords2(String s) {
if (s.isEmpty()) return null;
List<String> list = Arrays.asList(s.split("\\s+"));
Collections.reverse(list);
return String.join(" ", list);
}

// 两次翻转法
public String reverseWords(String s) {
if (s.isEmpty()) return null;
String[] strArray = reverse(s).split("\\s+"); // 按空格切割字符串
String[] result = new String[strArray.length];
for (int i = 0; i < strArray.length; i++) {
result[i] = reverse(strArray[i]);
}
return String.join(" ", result);
}

public String reverse(String str) {
int len = str.length();
char[] chars = str.toCharArray();
int i = 0;
while (i < len / 2) {
swap(chars, i, len - i - 1);
++i;
}
return new String(chars);
}

public void swap(char[] chars, int i, int j) {
chars[i] = (char) (chars[i] ^ chars[j]);
chars[j] = (char) (chars[i] ^ chars[j]);
chars[i] = (char) (chars[i] ^ chars[j]);
}
}

5.5. 字符串移位(要求:时间复杂度为O(n),额外空间复杂度为O(1))

对于一个字符串,请设计一个算法,将字符串的长度为len的前缀平移到字符串的最后。
给定一个字符串A和它的长度,同时给定len,请返回平移后的字符串。

测试样例:
“ABCDE”,5,3
返回:”DEABC”

关键点在于先将0i部分的字符逆序,再将i+1n部分的字符逆序,最后整体逆序调整

大部分字符串交换相关的题目是活用局部逆序函数组合的过程

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

public static String stringTranslation(String A, int n, int len) {
if (n == 1)
return A;

if (len <= 0 || len > n - 1)
return A;

char[] chars = A.toCharArray();

reverse(chars, 0, len - 1);
reverse(chars, len, n - 1);
reverse(chars, 0, n - 1);

return String.valueOf(chars);
}

public static void reverse(char[] chas, int start, int end) {
char tmp;
while (start < end) {
tmp = chas[start];
chas[start] = chas[end];
chas[end] = tmp;
start++;
end--;
}
}

5.6. 拼接最小字典序

对于一个给定的字符串数组,请找到一种拼接顺序,使所有小字符串拼接成的大字符串是所有可能的拼接中字典序最小的。
给定一个字符串数组strs,同时给定它的大小,请返回拼接成的串。

测试样例:
[“abc”,”de”],2
“abcde”

最优解的时间复杂度O(N*logN),其实质是一种排序的实现。如果str1+str2<str2+str1,则str1放在前面,否则str2放在前面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public static class MyComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
return (s1 + s2).compareTo(s2 + s1);
}
}

public static String findSmallest(String[] strs, int n) {
if (strs == null || strs.length <= 0)
return null;

Arrays.sort(strs, new MyComparator());
StringBuilder sb = new StringBuilder();
for (String str : strs) {
sb.append(str);
}

return sb.toString();
}

5.7. 空格替换

LeetCode 剑指 Offer 05. 替换空格

请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度(小于等于1000),同时保证字符串由大小写的英文字母组成。
给定一个string iniString 为原始的串,以及串的长度 int len, 返回替换后的string。

测试样例:
“Mr John Smith”,13
返回:”Mr%20John%20Smith”
”Hello World”,12
返回:”Hello%20%20World”

关键点在于从后往前依次替换

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
50
51
52
53
54
55
56
57
58
59
package personalwebsite.string;

/**
* Created by LeeeYou on 2016/3/14. 空格替换练习题
* <p>
* [LeetCode 剑指 Offer 05. 替换空格](https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/)
* <p>
* 请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度(小于等于1000),同时保证字符串由大小写的英文字母组成。
* 给定一个string iniString 为原始的串,以及串的长度 int len, 返回替换后的string。
* 测试样例:
* "Mr John Smith”,13
* 返回:"Mr%20John%20Smith"
* ”Hello World”,12
* 返回:”Hello%20%20World”
*/
public class ReplaceDemo {

public static void main(String[] args) {
ReplaceDemo demo = new ReplaceDemo();
System.out.println(demo.replaceSpace("We are happy."));
System.out.println(demo.replaceSpace2("We are happy."));
}

// 系统api
public String replaceSpace(String s) {
StringBuilder build = new StringBuilder();
char[] chars = s.toCharArray();
for (int i = chars.length - 1; i >= 0; i--) {
if (chars[i] == ' ') {
build.insert(0, "%20");
} else {
build.insert(0, chars[i]);
}
}
return build.toString();
}

// 数组
public String replaceSpace2(String s) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') count++;
}
char[] cs = new char[s.length() + count * 2];
int index = cs.length - 1;
for (int i = s.length() - 1; i >= 0; i--) {
char c = s.charAt(i);
if (c == ' ') {
cs[index--] = '0';
cs[index--] = '2';
cs[index--] = '%';
} else {
cs[index--] = c;
}
}
return new String(cs);
}

}

5.8. 合法括号序列判断

LeetCode 20. 有效的括号

对于一个字符串,请设计一个算法,判断其是否为一个合法的括号串。
给定一个字符串A和它的长度n,请返回一个bool值代表它是否为一个合法的括号串。

测试样例:
“(()())”,6
返回:true
测试样例:
“()a()()”,7
返回:false
测试样例:
“()(()()”,7
返回:false

解法1: 栈 + 辅助HashMap
解法2: 栈 + 不用辅助HashMap
解法3: 替换法

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package personalwebsite.string;

import java.util.*;

/**
* Created by LeeeYou on 2016/3/14. 合法括号序列判断练习题
* <p>
* [LeetCode 20. 有效的括号](https://leetcode-cn.com/problems/valid-parentheses/)
* <p>
* 对于一个字符串,请设计一个算法,判断其是否为一个合法的括号串。
* 给定一个字符串A和它的长度n,请返回一个bool值代表它是否为一个合法的括号串。
* 测试样例:
* "(()())",6
* 返回:true
* 测试样例:
* "()a()()",7
* 返回:false
* 测试样例:
* "()(()()",7
* 返回:false
*/
public class BracketsDemo {

public static void main(String[] args) {
BracketsDemo demo = new BracketsDemo();

String s1 = "()[]{}(({}))";
String s2 = "(]";
String s3 = "()";

System.out.println();
System.out.println(demo.isValid(s1));
System.out.println(demo.isValid(s2));
System.out.println(demo.isValid(s3));

System.out.println();
System.out.println(demo.isValid2(s1));
System.out.println(demo.isValid2(s2));
System.out.println(demo.isValid2(s3));

System.out.println();
System.out.println(demo.isValid3(s1));
System.out.println(demo.isValid3(s2));
System.out.println(demo.isValid3(s3));
}

// 栈 + 辅助HashMap
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) {
return false;
}

Map<Character, Character> pairs = new HashMap<Character, Character>() {{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque<Character> stack = new LinkedList<>();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (pairs.containsKey(c)) {
if (stack.isEmpty() || stack.peek() != pairs.get(c)) {
return false;
}
stack.pop();
} else {
stack.push(c);
}
}
return stack.isEmpty();
}

// 栈(不用辅助HashMap)
public boolean isValid2(String s) {
if (s.isEmpty()) return true;
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')');
else if (c == '{') stack.push('}');
else if (c == '[') stack.push(']');
else if (stack.empty() || c != stack.pop()) return false;
}
return stack.empty();
}

// 替换法
public boolean isValid3(String s) {
while (s.contains("()") || s.contains("[]") || s.contains("{}")) {
if (s.contains("()")) {
s = s.replace("()", "");
}
if (s.contains("{}")) {
s = s.replace("{}", "");
}
if (s.contains("[]")) {
s = s.replace("[]", "");
}
}
return s.length() == 0;
}

}

5.9. 最长无重复字符子串

LeetCode 3. 无重复字符的最长子串

对于一个字符串,请设计一个高效算法,找到字符串的最长无重复字符的子串长度。
给定一个字符串A及它的长度n,请返回它的最长无重复字符子串长度。保证A中字符全部为小写英文字符,且长度小于等于500。

测试样例:
“aabcb”,5
返回:3

关键点在于利用滑动窗口,不断向右查找最长不重复子串

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
50
package personalwebsite.string;

import java.util.HashSet;
import java.util.Set;

/**
* Created by LeeeYou on 2016/3/14. 最长无重复字符子串练习题
* <p>
* [LeetCode 3. 无重复字符的最长子串](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/)
* <p>
* 对于一个字符串,请设计一个高效算法,找到字符串的最长无重复字符的子串长度。
* 给定一个字符串A及它的长度n,请返回它的最长无重复字符子串长度。保证A中字符全部为小写英文字符,且长度小于等于500。
* 测试样例:
* "aabcb",5
* 返回:3
*/
public class LongestSubstringDemo {

public static void main(String[] args) {
LongestSubstringDemo demo = new LongestSubstringDemo();

String s1 = "abcabcbb";
String s2 = "rfrxkfrb";

System.out.println(demo.lengthOfLongestSubstring(s1));
System.out.println(demo.lengthOfLongestSubstring(s2));
}

// 滑动窗口
public int lengthOfLongestSubstring(String s) {
// 哈希集合,记录每个字符是否出现过
Set<Character> set = new HashSet<>();
int n = s.length();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
for (int i = 0; i < n; ++i) {
if (i != 0) {
set.remove(s.charAt(i - 1)); // 左指针向右移动一格,移除一个字符
}
// 从i位置开始不断地移动右指针找不重复的最长字串
while (rk + 1 < n && !set.contains(s.charAt(rk + 1))) {
set.add(s.charAt(rk + 1));
++rk;
}
ans = Math.max(ans, rk - i + 1); // 第 i 到 rk 个字符是一个极长的无重复字符子串
}
return ans;
}

}

5.10. 删除特定的字符

输入两个字符串,从第一字符串中删除第二个字符串中所有的字符。例如,输入”They are students.”和”aeiou”,则删除之后的第一个字符串变成”Thy r stdnts.”。

要编程完成这道题要求的功能可能并不难。毕竟,这道题的基本思路就是在第一个字符串中拿到一个字符,在第二个字符串中查找一下,看它是不是在第二个字符串中。如果在的话,就从第一个字符串中删除。但如何能够把效率优化到让人满意的程度,却也不是一件容易的事情。也就是说,如何在第一个字符串中删除一个字符,以及如何在第二字符串中查找一个字符,都是需要一些小技巧的。

首先我们考虑如何在字符串中删除一个字符。由于字符串的内存分配方式是连续分配的。我们从字符串当中删除一个字符,需要把后面所有的字符往前移动一个字节的位置。但如果每次删除都需要移动字符串后面的字符的话,对于一个长度为n的字符串而言,删除一个字符的时间复杂度为O(n)。而对于本题而言,有可能要删除的字符的个数是n,因此该方法就删除而言的时间复杂度为O(n2)。

事实上,我们并不需要在每次删除一个字符的时候都去移动后面所有的字符。我们可以设想,当一个字符需要被删除的时候,我们把它所占的位置让它后面的字符来填补,也就相当于这个字符被删除了。在具体实现中,我们可以定义两个指针(pFast和pSlow),初始的时候都指向第一字符的起始位置。当pFast指向的字符是需要删除的字符,则pFast直接跳过,指向下一个字符。如果pFast指向的字符是不需要删除的字符,那么把pFast指向的字符赋值给pSlow指向的字符,并且pFast和pSlow同时向后移动指向下一个字符。这样,前面被pFast跳过的字符相当于被删除了。用这种方法,整个删除在O(n)时间内就可以完成。

接下来我们考虑如何在一个字符串中查找一个字符。当然,最简单的办法就是从头到尾扫描整个字符串。显然,这种方法需要一个循环,对于一个长度为n的字符串,时间复杂度是O(n)。

由于字符的总数是有限的。对于八位的char型字符而言,总共只有2^8=256个字符。我们可以新建一个大小为256的数组,把所有元素都初始化为0。然后对于字符串中每一个字符,把它的ASCII码映射成索引,把数组中该索引对应的元素设为1。这个时候,要查找一个字符就变得很快了:根据这个字符的ASCII码,在数组中对应的下标找到该元素,如果为0,表示字符串中没有该字符,否则字符串中包含该字符。此时,查找一个字符的时间复杂度是O(1)。其实,这个数组就是一个hash表。

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

package com.leeeyou.test.string;

import java.util.LinkedList;

/**
* Created by leeeyou on 2016/12/20.
*/
public class DeleteStr {
public static void main(String[] args) {
String sourceStr = "public class DeleteStr";
String deleteStr = "cs";
deleteStr(sourceStr, deleteStr);
}

private static void deleteStr(String sourceStr, String deleteStr) {
if (sourceStr == null || deleteStr == null) {
return;
}

int[] deleteCharAsciiArray = new int[256];
char[] deleteCharArray = deleteStr.toCharArray();
for (int i = 0; i < deleteCharArray.length; i++) {
deleteCharAsciiArray[deleteCharArray[i]] = 1;
}

LinkedList<Character> linkedList = new LinkedList<>();
char[] sourceCharArray = sourceStr.toCharArray();
for (int i = 0; i < sourceCharArray.length; i++) {
linkedList.add(sourceCharArray[i]);
}

for (int i = linkedList.size() - 1; i >= 0; i--) {
if (deleteCharAsciiArray[linkedList.get(i)] == 1) {
linkedList.remove(i);
}
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < linkedList.size(); i++) {
sb.append(linkedList.get(i));
}
System.out.print(sb.toString());
}

}

5.11. 字符串的左右移动

例如,输入****so*h*a*ppy**t*oday*,则移动之后的字符串变成***********sohappytoday

利用一个快指针fast和一个慢指针slow从后向前扫描字符串,初始状态下slow记录从后向前第一次出现*的位置,fast指向slow的前一个位置。之后fast开始从后向前扫描,碰到*则跳过,碰到非*则与slow指针所在的位置交换字符且slow索引-1,同时置fast位置为*

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

package com.leeeyou.test.string;

/**
* Created by leeeyou on 2016/12/16.
*/
public class MoveStr {

public static void main(String[] args) {
String s = "*so*h*a*ppy**t*oday*";
moveStr(s);
}

private static void moveStr(String s) {
char[] chars = s.toCharArray();

int slow = chars.length - 1;
while ('*' != chars[slow]) {
--slow;
}

for (int fast = slow - 1; fast >= 0; fast--) {
if ('*' != chars[fast]) {
chars[slow] = chars[fast];
chars[fast] = '*';
slow--;
}
}

System.out.print(chars);
}

}

5.12. 字符串的全排列

从集合依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,如此递归处理;

比如:首先我要打印abc的全排列,就是第一步把a 和bc交换(得到bac,cab),这需要一个for循环,循环里面有一个swap,交换之后就相当于不管第一步了,进入下一步递归,所以跟一个递归函数,完成递归之后把交换的换回来,变成原来的字串 递归方法1(July 方法):

abc 为例子:

  1. 固定a, 求后面bc的全排列: abc, acb。 求完后,a 和 b交换; 得到bac,开始第二轮
  2. 固定b, 求后面ac的全排列: bac, bca。 求完后,b 和 c交换; 得到cab,开始第三轮
  3. 固定c, 求后面ba的全排列: cab, cba

即递归树:

str a b c
   | ab ac    | ba bc   | ca cb

result | abc acb | bac bca | cab cba

字符串全排列

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
package com.leeeyou.test.string;

/**
* Created by leeeyou on 2016/12/15.
*/
public class Permutation {
public static void main(String[] args) {
String s = "1234";
char[] chars = s.toCharArray();
permutation(chars, 0, chars.length - 1);
}

/**
* 从集合依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,如此递归处理.
*
* @param s 字符数组
* @param from 起始位置
* @param to 结束位置
*/
public static void permutation(char[] s, int from, int to) {
if (to <= 1) {
return;
}

if (from == to) {
System.out.println(s);
} else {
for (int i = from; i <= to; i++) {
swap(s, i, from);
permutation(s, from + 1, to);
swap(s, from, i);
}
}
}

public static void swap(char[] s, int i, int j) {
char temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}

5.13. 字符串的回文问题:Manacher算法

最长回文子串问题:给定一个字符串,求它的最长回文子串长度。如果一个字符串正着读和反着读是一样的,那它就是回文串。下面是一个回文串的实例:a aba abba aaaa tattarrattat(牛津英语词典中最长的回文单词)

参考:
最长回文子串——Manacher 算法
Manacher’s ALGORITHM: O(n)时间求字符串的最长回文子串 不指定

5.14. 字符串的包含

题目描述:
假设这有一个各种字母组成的字符串A,和另外一个字符串B,字符串里B的字母数相对少一些。什么方法能最快的查出所有小字符串B里的字母在大字符串A里都有?

比如,如果是下面两个字符串:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPO
答案是true,所有在string2里的字母string1也都有。

如果是下面两个字符串:
String 1: ABCDEFGHLMNOPQRS
String 2: DCGSRQPZ
答案是false,因为第二个字符串里的Z字母不在第一个字符串里。

我们可以对短字串进行轮询,把其中的每个字母都放入一个Hashtable里(我们始终设m为短字符串的长度,那么此项操作成本是O(m)次操作)。
然后轮询长字符串,在Hashtable里查询短字符串的每个字符,看能否找到;如果找不到,说明没有匹配成功。

用散列表实现:
1、hash[26],先全部清零,然后扫描短的字符串,若有相应的置1,
2、计算hash[26]中1的个数,记为m
3、扫描长字符串的每个字符a;若原来hash[a] == 1 ,则修改hash[a] = 0,并将m减1;若hash[a] == 0,则不做处理
4、若m == 0 or 扫描结束,退出循环。

参考:程序员编程艺术:第二章、字符串是否包含问题