LeetCode 535. TinyURL 的加密与解密
题目描述


题意分析
实现长网址到短网址的编码与还原,保证
decode(encode(url))得到原网址。题目保证解码的短网址由同一个对象编码产生,因此可以把原网址保存在对象中,让短网址只携带一个查询编号。不同网址需要不同编号,直接使用递增计数器即可。再将编号转成 62 进制,利用数字、大小写字母缩短它的文本表示,最后通过映射表查回完整网址。
解法:哈希映射短链接
核心思路
[!blue]
自增编号生成短码,映射表保存还原关系。id记录已经分配到的编号,code2url保存短码到原网址的映射,url2code保存原网址到短码的映射。后一个方向让重复编码同一网址时复用已有短码,不再增加编号。编码时先查
url2code;若是新网址,将id加一并转为 62 进制,然后同步写入两张表。在计数器表示范围内,递增编号不会重复,标准 62 进制表示又能唯一确定一个编号,因此新短码不会覆盖另一个网址的记录。进制转换每次用余数选出当前最低位字符,再整除 62 处理更高位;除法让编号不断缩小,直到变成零。取余得到的是从低位到高位的顺序,最后反转才是标准表示。
返回的短网址由固定前缀和短码组成,而字符表中没有斜杠,所以解码时取最后一个斜杠之后的部分,便能得到原短码。新网址编码时已经登记
code2url[code] = longUrl,重复网址又复用同一个记录,因此所有题目允许的解码调用都能还原正确网址。
解题步骤
- 重复网址先查询并复用旧短码。
- 新网址增加编号并转换为 62 进制。
- 记录双向映射,返回短网址。
- decode 提取短码,查回原网址。
代码实现
public class Codec {
// 在计数器表示范围内,自增编号为新登记的网址生成不同短码。
private long id = 0;
private final Map<String, String> code2url = new HashMap<>();
private final Map<String, String> url2code = new HashMap<>();
private static final String BASE =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public String encode(String longUrl) {
// 同一个长链接复用旧短码,判重必须在 id++ 之前。
String code = url2code.get(longUrl);
if (code != null) {
return "http://tinyurl.com/" + code;
}
id++;
code = toBase62(id);
// 两张表互为逆映射,必须同时写入。
code2url.put(code, longUrl);
url2code.put(longUrl, code);
return "http://tinyurl.com/" + code;
}
public String decode(String shortUrl) {
// 取最后一个 '/' 之后的部分,不依赖前缀的具体长度。
int idx = shortUrl.lastIndexOf('/');
String code = idx == -1 ? shortUrl : shortUrl.substring(idx + 1);
String url = code2url.get(code);
return url == null ? "" : url;
}
private String toBase62(long x) {
if (x == 0) {
return "0";
}
StringBuilder sb = new StringBuilder();
long v = x;
while (v > 0) {
int r = (int) (v % 62);
sb.append(BASE.charAt(r));
v /= 62;
}
// 逐次取余得到低位,最后反转为标准进制表示。
return sb.reverse().toString();
}
}
type Codec struct {
// 在计数器表示范围内,自增编号为新登记的网址生成不同短码。
id uint64
code2url map[string]string
url2code map[string]string
}
func Constructor() Codec {
return Codec{code2url: make(map[string]string), url2code: make(map[string]string)}
}
func (this *Codec) encode(longUrl string) string {
// 同一个长链接复用旧短码,判重必须在 id++ 之前。
if code, ok := this.url2code[longUrl]; ok {
return "http://tinyurl.com/" + code
}
this.id++
code := toBase62(this.id)
// 两张表互为逆映射,必须同时写入。
this.code2url[code] = longUrl
this.url2code[longUrl] = code
return "http://tinyurl.com/" + code
}
func (this *Codec) decode(shortUrl string) string {
// 取最后一个 '/' 之后的部分,不依赖前缀的具体长度。
idx := -1
for i := len(shortUrl) - 1; i >= 0; i-- {
if shortUrl[i] == '/' {
idx = i
break
}
}
code := shortUrl
if idx != -1 {
code = shortUrl[idx+1:]
}
return this.code2url[code]
}
func toBase62(x uint64) string {
base := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
if x == 0 {
return "0"
}
buf := make([]byte, 0)
for x > 0 {
buf = append(buf, base[x%62])
x /= 62
}
// 逐次取余得到低位,最后反转为标准进制表示。
for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
buf[i], buf[j] = buf[j], buf[i]
}
return string(buf)
}
复杂度分析
- 时间复杂度:
encode期望 $O(L+K)$,长网址哈希需要读取 $L$ 个字符,进制转换和短网址拼接需要 $O(K)$;decode期望 $O(K+1)$,用于提取短码和查询映射。$K$ 为短码长度,域名前缀固定。- 空间复杂度:累计保存网址内容、短码及映射项,按总字符量和登记数量计。
关键点总结
[!green]
- 唯一编号与查询表共同保证还原。
- 重复网址复用是当前实现的行为,由反向映射支持。
- 两张表在新登记时同时更新。
易错点总结
[!yellow]
- 直接把有冲突可能的哈希值当唯一短码且不处理冲突:后写入网址可能覆盖旧映射。
- 只记录网址到短码:无法按短码直接还原。
- 从第一个斜杠之后提取:会把域名也混入短码。
- 字符表长度与进制不一致:余数下标可能无效。
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!