在 LeetCode 上有这么一个题:
设计一个 TinyURL 的加密 encode 和解密 decode 的方法。你的加密和解密算法如何设计和运作是没有限制的,你只需要保证一个URL可以被加密成一个TinyURL,并且这个TinyURL可以用解密方法恢复成原本的URL。
这个题一般来说最简单的做法就是直接给传入的 url 生成一个标识符然后存到哈希表里,然后用标识符再从哈希表取出来。官方的题解1正是如此:
public class Codec {
Map<Integer, String> map = new HashMap<>();
int i = 0;
public String encode(String longUrl) {
map.put(i, longUrl);
return "http://tinyurl.com/" + i++;
}
public String decode(String shortUrl) {
return map.get(Integer.parseInt(shortUrl.replace("http://tinyurl.com/", "")));
}
}
这个算法存在什么缺点呢,官方题解也给出了答复:
可以加密解密的 URL 数目受限于 int 所能表示的范围。
如果超过 int 个 URL 需要被加密,那么超过范围的整数会覆盖之前存储的 URL,导致算法失效。
URL 的长度不一定比输入的 longURL 短。它只与加密的 URL 被加密的顺序有关。
这个方法的问题是预测下一个会产生的加密 URL 非常容易,因为产生几个 URL 后很容易推测出生成的模式。
那么我们也用这种 生成标识符,并用标识符作为短URL 的思路来做这个题。
一、处理思路
官方给了 5 种处理方法,使用简单的计数、使用出现次序加密、使用hashcode、使用随机数、随机固定长度加密。在评论区里我们看到这样一个评论:
“相同 longUrl 应该返回相同的 shortUrl 吧,这些题解都没这样做”
我们的处理方法是 —— 利用 32 位的 MD5 对源地址进行处理。MD5 的编码总是固定的,这样一来既满足了题目的要求,又完成了评论区的想法。
处理思路展示
假设我们要处理的网址 URL = https://blog.csdn.net/qq_24251607
既然是加密,那就应该有 密 的样子,这里引入 key 的设计。我们让 key = Tamayo0914,当然单纯的短URL实现也可以不做这个设计。然后把他们拼接起来:
之后我们对 realURL 进行 32位 的 MD5 编码处理:
然后把 md5 划分成 4个 长度均为8的字符串,将他们视为 16进制 数值,与 0x3FFFFFFF 做与运算得到长30位数值:
接下来只展示操作其中一段数值,实际上本方法最终会产生4个短URL
将与 0011 1101 进行6次与运算。之所以选择这个数值,是因为我们后面要将运算的结果和62个字符对应,而 0011 1101 的十进制值为 61 ,这样能够恰好保证计算的结果范围是 0~61 共62个值。
这样处理之后,原来的30位数值就拆成了6个数字:52,1,52,9,28,20。我们将其与 62位 字符数组相匹配,获得对应的字符即可。62位字符指的是0-9a-zA-Z,如图所示:
那么我们就得到了 经过short处理后的 URL —— Q1Q9sk,将其存放到 HashMap里就可以了。本质上还是官方题解的思路,只是在加密转换上有了更清晰的处理方式。
二、代码分析
- 首先我们要定义存储结构和字符集以及字符集的取值方法:
static HashMap<String, String> urls = new HashMap<>();
static String[] chars = new String[]{
"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b",
"c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L",
"M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X",
"Y", "Z"};
public static String getChar(int index) {
return chars[index];
}
- 编写主体函数(MD5 代码在后面提供)
public static String[] toShortAndSave(String str, String key) {
// 拼接后的 url
String realUrl = key + str;
// md5 长度为 32 位
String md5 = MD5Utils.code(realUrl);
// md5 会被拆分为 4 部分 , 每部分均被转译成一种处理结果
String[] shortUrl = new String[4];
// 对 md5 的4个部分分别处理
for (int beginIndex = 0; beginIndex < 4; beginIndex++) {
// 预定义该部分的处理结果
String out = "";
// 字符串截取的两个坐标
int endIndex = beginIndex + 1;
// 裁剪 8 位
String strPart = md5.substring(beginIndex * 8, endIndex * 8);
// 与0x3FFFFFFF 进行位运算 获得30位长度的数字编码
long index = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(strPart, 16);
// 将30位的数字编码拆分成6个部分 每次处理5位
for (int i = 0; i < 6; i++) {
// 与 0x0000003D 与运算,相当于与运算后5位,获得对应的字符下标
int idx = (int) (Long.valueOf("0000003D", 16) & index);
// 获得下标对应字符
out += getChar(idx);
// 数字右移5位 继续处理
index = index >> 5;
}
// out 即为处理后的一种short方案,可以存储到数据库等存储介质中,这里为了方便展示直接存储到 hashMap 里
urls.put(out, str);
// 一共返回4种方案 分别赋值
shortUrl[beginIndex] = out;
}
return shortUrl;
}
- 实际上写完1和2就已经实现了所需要的功能,为了展现出 网址 的样子,我们再编写一个方法进行拼接
public static String[] head(String[] shortUrls) {
String[] res = new String[4];
int idx = 0;
for (String e : shortUrls) {
res[idx++] = "http://t.cn/" + e;
}
return res;
}
- 编写测试方法查看输出结果
public static void main(String[] args) {
String url = "https://blog.csdn.net/qq_24251607";
String key = "Tamayo0914";
System.out.println("目标url:" + url);
System.out.println("临时key:" + key);
String[] res = toShortAndSave(url, key);
System.out.println("短网址处理结果:");
for (String e : res) {
System.out.println(e);
}
String[] head = head(res);
System.out.println("短URL展示:");
for (String e : head) {
System.out.println(e);
}
System.out.println("网址映射:");
for (String e : res) {
System.out.println("短网址 = " + e + " -> 实际网址 = " + urls.get(e));
}
}
- 输出结果
目标url:https://blog.csdn.net/qq_24251607
临时key:Tamayo0914
短网址处理结果:
Q1Q9sk
8AZZBl
RBlAVl
kIdUFl
短URL展示:
http://t.cn/Q1Q9sk
http://t.cn/8AZZBl
http://t.cn/RBlAVl
http://t.cn/kIdUFl
网址映射:
短网址 = Q1Q9sk -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = 8AZZBl -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = RBlAVl -> 实际网址 = https://blog.csdn.net/qq_24251607
短网址 = kIdUFl -> 实际网址 = https://blog.csdn.net/qq_24251607
三、完整代码
URL短处理代码:
import java.util.*;
public class study {
static HashMap<String, String> urls = new HashMap<>();
static String[] chars = new String[]{
"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b",
"c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L",
"M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X",
"Y", "Z"};
public static String getChar(int index) {
return chars[index];
}
public static String[] toShortAndSave(String str, String key) {
String realUrl = key + str;
String md5 = MD5Utils.code(realUrl);
String[] shortUrl = new String[4];
for (int beginIndex = 0; beginIndex < 4; beginIndex++) {
String out = "";
int endIndex = beginIndex + 1;
String strPart = md5.substring(beginIndex * 8, endIndex * 8);
long index = Long.valueOf("3FFFFFFF", 16) & Long.valueOf(strPart, 16);
for (int i = 0; i < 6; i++) {
int idx = (int) (Long.valueOf("0000003D", 16) & index);
out += getChar(idx);
index = index >> 5;
}
urls.put(out, str);
shortUrl[beginIndex] = out;
}
return shortUrl;
}
public static void main(String[] args) {
String url = "https://blog.csdn.net/qq_24251607";
String key = "Tamayo0914";
System.out.println("目标url:" + url);
System.out.println("临时key:" + key);
String[] res = toShortAndSave(url, key);
System.out.println("短网址处理结果:");
for (String e : res) {
System.out.println(e);
}
String[] head = head(res);
System.out.println("短URL展示:");
for (String e : head) {
System.out.println(e);
}
System.out.println("网址映射:");
for (String e : res) {
System.out.println("短网址 = " + e + " -> 实际网址 = " + urls.get(e));
}
}
public static String[] head(String[] shortUrls) {
String[] res = new String[4];
int idx = 0;
for (String e : shortUrls) {
res[idx++] = "http://t.cn/" + e;
}
return res;
}
}
MD5Utils代码(直接引用的他人作品):
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Utils {
/**
* MD5加密类
*
* @param str 要加密的字符串
* @return 加密后的字符串
*/
public static String code(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[] byteDigest = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < byteDigest.length; offset++) {
i = byteDigest[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
System.out.println(code("1"));
}
}
四、短地址冲突
1. 会出现短地址冲突吗
实际上将长的完整信息编译处理成短的信息,本身就是一个删除了大量原有信息的操作,就像 hash 函数一样,总是会有冲突的。本位示例的源地址长度为33,可以包含数字+符号+大小写字母,我们将他强行变更为6位的长度,意味着有27位的数值信息被我们抹去了。在大量的长地址转换成短地址的时候,自然会出现短地址冲突的问题。
2. 怎么避免短地址冲突
本文提到的处理方案同时存在3个方案来避免短地址的冲突问题。
- 我们不只是单纯的对原URL进行短地址处理,我们还对其附加了一个字段 key,在一定程度上使得生成的短地址会有所不同,如果每次操作的 key 不一样,那产生的短地址理论上也不一样。
- 使用 MD5 对数值进行编码。数学证明,MD5编码产生的结果一共有 2128种,即便是有限的个数,但是也尽可能地让编码出的结果发散开来。
- 对同一个MD5值编译出 4 个短地址,我们在进行 HashMap 的 put 操作时,其实可以先尝试 get 我们产生的短地址,如果重复就换一个,毕竟我们产生了4个短地址,总会有1个短地址是没有被使用的。
- 在这种情况下如果还是发生了短地址冲突,除了考虑其他的短地址算法,还应当考虑是否需要设置短地址的过期策略,定时清空存储介质中短地址的映射关系。