原题:
TinyURL是一种URL简化服务, 比如:当你输入一个URL https://leetcode.com/problems/design-tinyurl 时,它将返回一个简化的URL http://tinyurl.com/4e9iAk.
要求:设计一个 TinyURL 的加密 encode 和解密 decode 的方法。你的加密和解密算法如何设计和运作是没有限制的,你只需要保证一个URL可以被加密成一个TinyURL,并且这个TinyURL可以用解密方法恢复成原本的URL。
解法:
随机数
大致框架:
# # class Codec:
# def encode(self, longUrl: str) -> str:
# """Encodes a URL to a shortened URL.
# """
# def decode(self, shortUrl: str) -> str:
# """Decodes a shortened URL to its original URL.
# """
代码:
import random
class Codec:
"""
URL简化类:将一个长url转化为一个短url(转换后的 URL路径 为6位数的字母或数字)
转化算法:使用 62 位由大小写字母和数字构成的字符串集合 作为 【62进制位表】,
可表示的 6位url 数量为 62 ** 6 == 586亿多,几亿次以内的调用不用担心
字典键冲突,不够用的话可以再增加进制位数。
"""
_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
_dict = {}
_key = ''.join(random.sample(_chars, 6))
@classmethod
def get_rand(cls):
return ''.join(random.sample(cls._chars, 6))
@classmethod
def encode(cls, long_url):
"""
将一个 长URL 编码为一个 短URL【随机固定长度加密】。
使用 random 模块 随机从 62 位字符串中选取 6 个作为 URL短路径。
因为使用了随机数,根据 short_url 来预测字典大小几乎是不可能的,数据更加安全。
"""
while cls._dict.get(cls._key):
cls._key = cls.get_rand()
cls._dict[cls._key] = long_url
return 'http://tinyurl.com/' + cls._key
@classmethod
def decode(cls, short_url):
"""
将一个 短URL 解码为一个 长URL。
直接根据 url字符串后6位,从字典中取值。
"""
return cls._dict[short_url[19:]]
# Your Codec object will be instantiated and called as such:
# codec = Codec()
# codec.decode(codec.encode(url))
使用简单的计数 [Accepted]
为了加密 URL,我们使用计数器 ( i ) (i) (i) ,每遇到一个新的 URL 都加一。我们将 URL 与它的次数 i i i 放在哈希表 HashMap 中,这样我们在稍后的解密中可以轻易地获得原本的 URL。
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 第几个出现作为关键字进行加密,将这个出现次序看做 62 进制,并将每一位映射到一个长度为 62 位的表中对应的字母作为哈希值。此方法中,我们使用一系列整数和字母表来加密,而不是仅仅使用数字进行加密。
public class Codec {
String chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
HashMap<String, String> map = new HashMap<>();
int count = 1;
public String getString() {
int c = count;
StringBuilder sb = new StringBuilder();
while (c > 0) {
c--;
sb.append(chars.charAt(c % 62));
c /= 62;
}
return sb.toString();
}
public String encode(String longUrl) {
String key = getString();
map.put(key, longUrl);
return "http://tinyurl.com/" + key;
count++;
}
public String decode(String shortUrl) {
return map.get(shortUrl.replace("http://tinyurl.com/", ""));
}
}
分析:
可加密的 URL 数目还是依赖于 int 的范围。因为相同的 countcount 在出现次序溢出整数范围后仍然会出现。
加密后 URL 的长度不一定更短,但某种程度上与 longURL 的出现次序相对独立。比方说产生的 URL
长度按顺序会是 1(62次),2(62次)。这个算法的表现比较好,因为相同的加密结果只有在溢出整数后才会发生,这个范围非常大。
如果出现重复,下一次产生的加密结果还是能通过某种计算被预测出来。
hashcode ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200626171113106.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTE1NTU1,size_16,color_FFFFFF,t_70)
public class Codec {
Map<Integer, String> map = new HashMap<>();
public String encode(String longUrl) {
map.put(longUrl.hashCode(), longUrl);
return "http://tinyurl.com/" + longUrl.hashCode();
}
public String decode(String shortUrl) {
return map.get(Integer.parseInt(shortUrl.replace("http://tinyurl.com/", "")));
}
}
分析
可加密 URL 的数目由int 决定,因为hashCode 使用整数运算。
加密后 URL 的平均长度与 \text{longURL}longURL 的长度没有直接关联。
hashCode() 对于不同的字符串不一定产生独一无二的加密后
URL。像这样对于不同输入产生相同输出的过程叫做冲突。因此,如果加密字符串的数目增加,冲突的概率也会增加,最终导致算法失效。
下图展示了不同对象映射到相同的 hashcode,以及对象越多冲突概率越大。
可能几个字符串加密后冲突就会发生,会远比 \text{int}int 要小。这与生日悖论类似,也就是如果有23个人,存在 2 个人同一天生日的概率达到 50%,如果有 70 个人,这一概率会高达 99.9%。
这种方法中,很难根据前面产生的 URL 结果预测后面加密 URL 的答案。