2018今日头条春招面试题

1、打印蛇形矩阵

给定二维数组a[N][M],按照回字形打印数组中的数值。
例如:java

1 2 3
4 5 6
7 8 9

打印为 1 2 3 6 9 8 7 4 5

思路一:一圈一圈地走,如上例中,最外圈是一圈,正方形边长为3;而后5是一圈,边长为1node

思路二:碰见障碍立马拐弯,从(0,0)处往左走,走到快出界的时候往下走,往下走到快要出界的时候往右走,往右走到已经访问过的元素(也就是1)的时候,往左走。python

思路一代码:mysql

n = 3
m = 3
a = [[0] * m for _ in range(n)]
cnt = 0
for i in range(n):
    for j in range(m):
        a[i][j] = cnt
        cnt += 1
        print(a[i][j], end=' ')
    print()
print('====')


def go(n, m):
    x, y = 0, 0
    while m > 0 and n > 0: 
        for i in range(y, y + m):
            print(a[x][i], end=' ')
        for i in range(x + 1, x + n):
            print(a[i][y + m - 1], end=' ')
        for i in range(y + n - 2, y - 1, -1):
            print(a[x + n - 1][i], end=' ')
        for i in range(x + n - 2, x, -1):
            print(a[i][y], end=' ')
        print()
        m -= 2
        n -= 2
        x += 1
        y += 1


go(n, m)

思路二代码:web

n = 3
m = 3
a = [[0] * m for _ in range(n)]
cnt = 0
for i in range(n):
    for j in range(m):
        a[i][j] = cnt
        cnt += 1
        print(a[i][j], end=' ')
    print()
print('====')


def legal(x, y):
    return n > x >= 0 and m > y >= 0


def go():
    x, y = 0, 0
    dir = ((0, 1), (1, 0), (0, -1), (-1, 0))
    d = 0
    vis = [[False] * m for _ in range(n)]
    for i in range(m * n):
        print(a[x][y], end=' ')
        vis[x][y] = True
        xx, yy = x + dir[d][0], y + dir[d][1]
        if not legal(xx, yy) or vis[xx][yy]:
            d = (d + 1) % 4
            xx, yy = x + dir[d][0], y + dir[d][1]
        x, y = xx, yy


go()

延伸问题:双蛇形打印面试

1 2 3
4 5 6
7 8 9

打印为:1 2 3 6 5 4 7 8 9

1 2 3 4 
5 6 7 8
9 a b c
d e f g

打印为:1 2 3 4 8 c b a       7 6 5 9 d e f g

这个问题若是再按照思路一就会很复杂,按照思路二依旧直观简单:只须要让两条蛇同时出发,每次一块儿走一格便可。redis

2、LRU算法实现

有元素若干,每次用元素的时候,须要把元素放入到缓存里面,这个缓存最大只能存储N个元素,要求O(1)复杂度实现LRU算法。
方法一:使用平衡二叉树,树中每一个结点都有一个时间值,每次增删只须要O(lgN)复杂度算法

方法二:双向链表,链表中存储的元素从左往右就是按照时间递增顺序排列的,定义Node以下sql

class Node:
    Node prev;
    Node next;
    int value;

为了经过value快速找到Node,使用HashMap存储value到Node的映射。
当访问元素x时,经过HashMap找到x对应的Node,让Node的prev直连Node的next,Node本身去作tail
当元素个数达到N的时候,删除head结点,并让head向后移动一格。数据库

方法三:单向链表,这个问题其实使用单向链表 也是能够的,虽然稍微麻烦一些。
要想使用单向链表,hashMap里面就要存储上一个结点而不能存储其自身,如2-3-4,HashMap中的3对应2号结点,这样就能够方便的让2号结点直接指向4号结点。当访问3的时候,须要更改4号结点在HashMap中对应的结点。

Leetcode上有此问题:https://leetcode.com/problems/lru-cache/description/

import java.util.HashMap;
import java.util.Map;

class LRUCache {
class Node {
    int key, value;

    Node(int key, int value) {
        this.key = key;
        this.value = value;
    }

    Node next;
    Node prev;
}

class LinkedList {
    Node head = new Node(0, 0);
    Node tail = new Node(0, 0);

    LinkedList() {
        head.next = tail;
        tail.prev = head;
    }
}

LinkedList li = new LinkedList();
Map<Integer, Node> ma = new HashMap<>();
int cap = 0;
int len = 0;

public LRUCache(int capacity) {
    this.cap = capacity;
}

public int get(int key) {
    Node node = ma.get(key);
    if (node == null) return -1;
    remove(node);
    pushback(node);
    return node.value;
}

void pushback(Node node) {
    Node prev = li.tail.prev;
    prev.next = node;
    node.next = li.tail;
    li.tail.prev = node;
    node.prev = prev;
}

void remove(Node node) {
    Node prev = node.prev;
    Node next = node.next;
    prev.next = next;
    next.prev = prev;
}

public void put(int key, int value) {
    Node node = ma.get(key);
    if (node != null) {
        node.value = value;
        remove(node);
        pushback(node);
    } else {
        this.len++;
        if (this.len > this.cap) {
            this.len--;
            ma.remove(li.head.next.key);
            remove(li.head.next);
        }
        node = new Node(key, value);
        ma.put(key, node);
        pushback(node);
    }
}

public static void main(String[] args) {
    LRUCache cache = new LRUCache(2 /* capacity */);
    cache.put(1, 1);
    cache.put(2, 2);
    cache.get(1);       // returns 1
    cache.put(3, 3);    // evicts key 2
    cache.get(2);       // returns -1 (not found)
    cache.put(4, 4);    // evicts key 1
    cache.get(1);       // returns -1 (not found)
    cache.get(3);       // returns 3
    cache.get(4);       // returns 4
}
}

实际上,Java中的LinkedHashMap、LinkedHashSet这两个数据结构自己就是链表+哈希,因此实现LRU彻底能够直接用现成的数据结构。

import java.util.LinkedHashMap;

class LRUCache {
LinkedHashMap<Integer, Integer> ma = new LinkedHashMap<Integer, Integer>();
int capacity = 0;

public LRUCache(int capacity) {
    this.capacity = capacity;
}

public int get(int key) {
    Integer ans = ma.get(key);
    if (ans == null) return -1;
    ma.remove(key);
    ma.put(key, ans);
    return ans;
}

public void put(int key, int value) {
    ma.remove(key);
    ma.put(key, value);
    if (ma.size() > capacity) {
        ma.remove(ma.keySet().iterator().next());
    }
}
}

3、字符串split函数的实现

给定字符串s,指定字符c,要求实现相似python中str.split(c)的效果

def split(s, c):
    a = []
    now = ""
    for i in s:
        if i == c:
            a.append(now)
            now = ""
        else:
            now += i
    a.append(now)
    return a


for s, c in (("abccabc", "c"),
             ('c', 'c'),
             ('cc', 'c')):
    print(s.split(c))
    print(split(s, c))
    print()

面试官接着问:若是传入的c是一个字符串,该如何实现?
方法:使用KMP算法,才能实现O(N)复杂度。每次只要识别一个字符串,就要切割一次。

4、单链表排序

给定一个单向链表h,要求O(Nlg(N))复杂度实现链表排序。

方法一:归并排序。
若是是双向链表,是可使用快排的。可是这道题是单向链表,除了快排、堆排序、归并排序,其他排序算法大都是O(N^2)的。
归并排序做用于数组的时候,空间复杂度为O(N),由于须要额外的空间来存储排序后的结果。
而归并排序做用于链表的时候,空间复杂度为O(1),由于链表能够直接更改指针,没有必要使用额外空间。
链表归并排序算法描述以下:首先求出链表的中央结点,对先后两部分链表进行排序,而后归并之。

import random


class Node:
    def __init__(self, value, next):
        self.value = value
        self.next = next


def generate_list():
    h = Node(0, None)
    now = h
    for i in range(10):
        x = random.randint(0, 100)
        node = Node(x, None)
        now.next = node
        now = node
    return h.next


def get_length(h):
    s = 0
    while h:
        s += 1
        h = h.next
    return s


def get_middle(h):
    n = get_length(h)
    s = 0
    while s < n // 2 - 1:
        h = h.next
        s += 1
    return h


def sort(h):
    if h is None or h.next is None: return h
    print("排序以前")
    print_list(h)
    m = get_middle(h)
    # 咬断链表
    t = m
    m = m.next
    t.next = None
    # 排序链表
    h = sort(h)
    m = sort(m)
    # 归并链表
    a = Node(0, None)
    now = a
    while h and m:
        if h.value < m.value:
            now.next = h
            h = h.next
            now = now.next
        else:
            now.next = m
            m = m.next
            now = now.next
    if h:
        now.next = h
    if m:
        now.next = m
    print("排序以后")
    print_list(a.next)
    return a.next


def print_list(h):
    while h:
        print(h.value, end=" ")
        h = h.next
    print()


l = generate_list()
print_list(l)
print_list(sort(l))

这时,面试官就说了,递归的方式是不错,但能不能不递归呢?
我说:用栈啊,本身实现栈,其实也至关于递归。
面试官说:很差,归并排序还有另一种方式你可知道?归并排序是自上而下的排序。自上而下的思路是:欲对8个元素进行排序,先对前4个元素和后4个元素排序,而后归并之;欲对4个元素进行排序,先对前2个元素和后两个元素进行排序,而后归并之......
而自下而上的归并也是能够的:先将元素两两排序,如:一、2排序,三、4排序,5,6排序,7,8排序;而后将元素四四排序,也就是将相邻两个排序好的组进行合并,即12组跟34组合并,56组跟78组合并;最后要将1234组跟5678组进行合并。
我说:原来如此,你咋那么牛逼!

import random
import math


class Node:
    # 定义链表结点
    def __init__(self, value, next):
        self.value = value
        self.next = next


def generate_list(sz):
    # 产生长度为sz的随机链表
    h = Node(0, None)
    now = h
    for i in range(sz):
        x = random.randint(0, 100)
        node = Node(x, None)
        now.next = node
        now = node
    return h.next


def get_length(h):
    # 求单向链表的长度
    s = 0
    while h:
        s += 1
        h = h.next
    return s


def merge(x, y):
    # 合并链表x和y,返回合并后的链表的头结点和尾结点
    i, j = x, y
    ans = Node(0, None)
    now = ans
    while i is not None and j is not None:
        if i.value < j.value:
            now.next = i
            now = now.next
            i = i.next
        else:
            now.next = j
            now = now.next
            j = j.next
    if i:
        now.next = i
    if j:
        now.next = j
    while now.next:
        now = now.next
    return ans.next, now


def get_section(h, step):
    # 获取一个章节,它的长度为step,要返回它的head,mid,tail三部分
    s = 0
    now = h
    half_step = step >> 1
    prev = None
    while now and s < half_step:
        prev = now
        now = now.next
        s += 1
    if now is None:
        return h, None, None
    mid = now
    prev.next = None
    s = 0
    while now and s < half_step:
        prev = now
        now = now.next
        s += 1
    if now is None:
        return h, mid, None
    next_section = now
    prev.next = None  # 截断
    return h, mid, next_section


def handle(head, step):
    # 对链表head执行step步数的归并
    ans = Node(0, head)
    now = ans
    while now and now.next:
        h, m, next_section = get_section(now.next, step)
        section_head, section_tail = merge(h, m)
        now.next = section_head
        section_tail.next = next_section
        now = section_tail
    return ans.next


def sort(h):
    step = 2
    sz = 2 ** math.ceil(math.log2(get_length(h)))
    while step <= sz:
        h = handle(h, step)
        step <<= 1
    return h


def to_array(h):
    # 链表转数组
    a = []
    while h:
        a.append(h.value)
        h = h.next
    return a


def multiple_case():
    # 多组测试用例
    for i in range(1000):
        sz = random.randint(3, 100)
        l = generate_list(sz)
        a = sorted(to_array(l))
        b = to_array(sort(l))
        print(a == b)


multiple_case()

5、随机数生成

有一个随机器f,它可以等几率生成0,1,2,3,4共5个数值;要求用此随机器构造一个能够等几率生成[0,6]之间数值的随机器。问如何实现?对于你给出的实现,平均须要调用随机器f多少次才能产生一个[0,6]之间的数值?

思路:这道题在搜狐面试的时候问过一次,当时脑壳短路没想出来,面试结束后半小时才豁然开朗追悔莫及,把这个问题从各类角度、翻来覆去玩了个遍。因此这道题不可能忘记的。

最直观的思路就是均匀产生不少个数字,从这么多个数字里面选取7个数字分别表示[0,6]之间的整数。那么如何均匀产生不少个数字呢?答:f()*10+f()会产生25种数字,随意选取8种表示[0,6]之间的数字便可,当没有命中[0,6]之间的数字对应的值时,继续调用f()*10+f()

while 1:
    x=f()*5+f()
    if x<7:return x

那么这么作,平均须要多少次才能产生一个[0,6]之间的随机数呢?
答:
调用2次就跳出循环的几率为7/25;
调用4次才跳出循环的几率为(18/25)*7/25;
调用6次才跳出循环的几率为(18/25)*(18/25)*7/25
......
把次数乘以几率累加起来就获得了跳出循环时已经执行了多少次循环,这个问题是一个等比数列乘以等差数列求和问题,求解方法是错位相减法,结果是50/7。实际上是没有必要算的,由于这是典型的几何分布:每次成功的几率为p,那么它的指望执行次数就是1/p。通过以上计算,大约须要7次才能跳出循环。
那么可否优化一下呢?咱们能够一共产生了25种结果,这25种结果咱们并无充分利用起来(7/25的利用率过低了)。

while 1:
    x=f()*5+f()
    if x<21:
        return x//3

这个程序一次跳出循环的几率p=21/25,平均调用f()的次数为$\frac{1}{p}\times 2=\frac{50}{21}$,大约须要2次多点。优化效果可谓明显!

到这里难道就可以中止思考的步伐吗?不能,咱们的目标是要站在极高极远处审视问题,要把问题看得通透完全。

这个问题等价于:给定一个5进制的数字,这个数字的每一位都是[0,4]中的一个整数,要求把这个数字转化为7进制数字。好比:调用f() m次,那么产生7进制数字的位数为$b=floor(log_7 5^m)$,咱们能够建立一个长度为b的数组,一次性存储下来这b个元素,这样之后再须要随机数的时候,无需生成,直接返回便可!那么一次性跳出循环的几率为p=$\frac{5^m-5^m mod 7^b}{5^m}$,生成一个随机数平均须要尝试的次数为cnt=$\frac{1}{p}\times \frac{m}{b}$,咱们的目标就是要使得cnt最小化。

那么问题来了,如何使得生成一个随机数尝试次数尽可能少呢?最少尝试次数是多少呢?

首先,咱们须要定义清出问题的输入是m,n。表示用m进制随机数构造n进制随机数,求一个数值cnt(表示最少尝试次数)。

import math
import matplotlib.pyplot as plt
import numpy as np

"""
从极限上考虑,最少须要log_m(n)次
"""
m = 5
n = 7
print("最少调用次数为:", math.log(n, m))
i = int(math.ceil(math.log(n, m)))
sz = 5000
a = np.zeros(sz)
while i < sz:
    # target_count表示生成m进制的个数
    target_count = math.floor(math.log(m ** i, n))
    # p表示一次就能跳出循环的几率
    p = (m ** i - (m ** i) % (n ** target_count)) / (m ** i)
    # 1/p表示循环指望进行的次数,每次循环调用i次f(),产生target_count个随机数
    cnt = 1 / p * i / target_count
    a[i] = cnt
    i += 1
plt.scatter(list(range(sz)), a)
plt.show()
arg = np.argsort(a)
for i in range(10):
    print(i, arg[i], a[arg[i]])

根据图像能够看出最优解集中在1.2附近,由此联想到$log_5 7$.这里面的规律就是:由n随机数生成m随机数,须要执行n随机数的最少次数的下界为:$log_n m$

彷佛到了这里就已经能够结束了,可是事物的变化是无穷无尽的。像俄罗斯套娃同样,通用的状况包含特殊的状况,特殊的状况又包括更特殊的状况。上面一直在讨论由m随机器生成n随机器,前提条件是m随机器产生的m个数是等几率的。那么若是m随机器产生的m个数是不等几率的呢?那就须要使用m随机器构造等几率随机器。

如何有不均匀的m随机器构造均匀的随机器呢?
举例来讲,不均匀的2随机器,产生0的几率为0.7,产生1的几率为0.3。
若是只用1次,则可能产生0,1,p(0)=0.7,p(1)=0.3
若是用2次,则可能产生00,01,10,11,p(00)=0.49,p(01)=0.21,p(10)=0.21,p(11)=0.09,这样咱们就获得了一个2随机器,当产生00和11的时候丢弃之,当产生01的时候当作产生了0,当产生10的时候当作产生了1。
若是用3次,则可能产生000,001,010,011,100,101,110,111共八种状况,按照几率值进行分类,能够分为四类:0的个数为0,0的个数为1,0的个数为2,0的个数为3。这四种状况的个数分别为:

  • 0的个数为0:1
  • 0的个数为1:$C_3^1$
  • 0的个数为2:$C_3^1$
  • 0的个数为3:1

这样咱们就制造出了利用率为6/8的均匀的3随机器。
至此,咱们又能够提出一个问题:给定不均匀的m随机器,怎么样使利用率尽可能高制造均匀随机器?

下面练习一下,计算一下下面这种方法的指望执行次数:

def ff():
    x=f()
    while x==4:x=f()
    return x
def mine():
    do:
        x=ff()<<2|(ff()&1)
    while(x>7);
    return x

解:一次执行就跳出循环的几率为:p=4/5*4/5*7/8=112/200,平均调用次数为400/112=3次多一点。

6、最火帖子

如何定义“火”?答:最近三天内的赞数、转发数越多,代表越火。
问:如何存储帖子,帖子在数据库中是如何存在的?
问:传入一个帖子,如何计算帖子“火”的程度?
对于大V,粉丝特别多,查询他的粉丝的时候应该怎么作?

7、粉丝列表、关注列表

像微博、知乎这种社交平台,粉丝、关注列表是如何实现的?复杂度如何?如何优化之?(要考虑用户关注、取消关注两种行为)

8、长短URL问题

上次搜狐面试,我没经过。由于在第一题上花的时间太长,面试官只问了我前几个问题。询问其余面试者,说也问“长短URL”这道题了。此次面试又问了这个问题。我感受这个问题挺无聊的,由于很显然是哈希算法,只不过哈希的方法不同罢了。

短网址(Short URL),顾名思义就是在形式上比较短的网址。目前已经有许多相似服务,借助短网址您能够用简短的网址替代原来冗长的网址,让使用者能够更容易的分享连接。例如:http://t.cn/SzjPjA。自从twitter推出短网址(shorturl),继之国内各大微博跟风,google公开goo.gl使用API,短网址之风愈演愈烈.不得不说这是一个新兴又一大热门web2.0服务.

短连接的好处:
一、内容须要:发微博时有字数限制;
二、用户友好:便于记忆,直接输入如URL进行访问;
三、便于管理。

其中便于管理体如今:
短网址能够在咱们项目里能够很好的对开放级URL进行管理。有一部分网址能够会涵盖暴力,广告等信息,这样咱们能够经过用户的举报,彻底管理这个链接将不出如今咱们的应用中,应为一样的URL经过加密算法以后,获得的地址是同样的。
咱们能够对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于咱们对项目的后续工做更好的做出决策。

百度搜索“短网址”三个字会看到许多API,深入说明了“短网址”技术有多么流行,那么“短网址”API内部是如何实现的呢?

把长的字符串映射成短的字符串一定是一个从多映射到少的过程。从多映射到少必然会形成多对一现象,因此把“多”的映射成了“少”的很容易,直接计算哈希值就能够,把少的映射成多的,就必需要经过准确的映射。经过短URL必须可以找到惟一的长URL。这就要解决哈希冲突。
解决哈希冲突有哪些办法呢?

  • x的哈希值是y,结果发现y那里已经放了一个x',因而对xx进行哈希获得yy,发现yy没有碰撞,就找到了xx的哈希值。
    这样全部的哈希内容都存在同一个哈希map里面。
  • 桶哈希:x的哈希值是y,y那里已经有了一个x',那么在x'后面再追加一个x,x'和x造成了一个链表。

一、场景

​ 短连接服务就是将一段长的URL转换为短的URL,好比利用新浪微博的短连接生成器,可将一段长的URL(http://blog.csdn.net/poem_qianmo/article/details/52344732)转换为一段短的URL(http://t.cn/RtFFvic),用户经过访问短连接便可重定向到原始的URL。

整个交互流程以下:

用户访问短连接:http://t.cn/RtFFvic
短连接服务器t.cn收到请求,根据URL路径RtFFvic获取到原始的长连接:http://blog.csdn.net/poem_qianmo/article/details/52344732
服务器返回302状态码,将响应头中的Location设置为:http://blog.csdn.net/poem_qianmo/article/details/52344732
浏览器从新向http://blog.csdn.net/poem_qianmo/article/details/52344732发送请求

二、设计要点

短连接生成算法

(1)利用放号器,初始值为0,对于每个短连接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),好比第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。

(2)将短连接服务器域名与放号器的62进制值进行字符串链接,即为短连接的URL,好比:t.cn/sBc。

重定向过程

生成短连接以后,须要存储短连接到长连接的映射关系,即sBc -> URL,浏览器访问短连接服务器时,根据URL Path取到原始的连接,而后进行302重定向。映射关系可以使用K-V存储,好比Redis或Memcache。

三、优化方案

算法优化

​ 采用以上算法,对于同一个原始URL,每次生成的短连接是不一样的,这样就会浪费存储空间,由于须要存储多个短连接到同一个URL的映射,若是能将相同的URL映射成同一个短连接,这样就能够节省存储空间了。

(1)方案1:查表

​ 每次生成短连接时,先在映射表中查找是否已有原始URL的映射关系,若是有,则直接返回结果。很明显,这种方式效率很低。

(2)方案2:使用LRU本地缓存,空间换时间

​ 使用固定大小的LRU缓存,存储最近N次的映射结果,这样,若是某一个连接生成的很是频繁,则能够在LRU缓存中找到结果直接返回,这是存储空间和性能方面的折中。

可伸缩和高可用

​ 若是将短连接生成服务单机部署,缺点一是性能不足,不足以承受海量的并发访问,二是成为系统单点,若是这台机器宕机则整套服务不可 用,为了解决这个问题,能够将系统集群化,进行“分片”。

​ 在以上描述的系统架构中,若是发号器用Redis实现,则Redis是系统的瓶颈与单点,所以,利用数据库分片的设计思想,可部署多个发号器实例,每一个实例负责特定号段的发号,好比部署10台Redis,每台分别负责号段尾号为0-9的发号,注意此时发号器的步长则应该设置为10(实例个数)。

​ 另外,也可将长连接与短连接映射关系的存储进行分片,因为没有一个中心化的存储位置,所以须要开发额外的服务,用于查找短连接对应的原始连接的存储节点,这样才能去正确的节点上找到映射关系。

9、Mysql

MySQL有哪些数据库引擎?
如何用MySQL存储树形结构?

10、基础知识

TCP三次握手都是什么意思?
进程和线程有何区别?
进程间通讯方式有哪些?
啥叫协程?

11、Redis

Redis多进程跟单进程的区别是什么?
Redis和Memcache有哪些区别?

12、面试官说

在个人简历上,写到了本身熟悉MySQL和Redis。而面试官问的关于MySQL和Redis的许多问题,我都没能答出来,场面一度十分尴尬。
我说:我只停留在用过、会用的层面上,没有深刻研究这些细节。
面试官问我:为何不去研究这些细节呢?
我说:在学校没有这种需求,等我工做以后能够慢慢学,在学校有更重要的东西要学。何况,许多技术都是无底洞,假若把时间都花在钻研某个库上面,那就性价比过低了。
面试官说:有的书粗略一翻便可,有的书却须要逐字逐句仔细研读。技术也是同样,有些技术应该深究,有些技术会用便可。不能由于技术是无底洞,就对全部的技术都浅尝辄止。像一些工做以后必然要用到的东西,好比MySQL、Redis,在学校越熟悉越好,对底层了解越多越好,这样当你走上工做岗位,跳过了学习的时间,才能迅速脱颖而出。工做并不像大家想的那么轻松,工做以后也没有那么多空闲时间供你学习。大家在学校其实空闲时间已经算多的了。到了我这年龄,再想学习一门新东西是很慢的,无论学什么东西都要趁年轻。

确实,许多知识我只是停留在会用的层面上,这是不行的,要对技术了如指掌,要追求深入。

参考资料

http://www.javashuo.com/article/p-kpqbfmsr-hx.html