一、打印蛇形矩阵
给定二维数组a[N][M],按照回字形打印数组中的数值。
例如:
1 2 3
4 5 6
7 8 9
打印为 1 2 3 6 9 8 7 4 5
思路一:一圈一圈地走,如上例中,最外圈是一圈,正方形边长为3;然后5是一圈,边长为1
思路二:遇见障碍立马拐弯,从(0,0)处往左走,走到快出界的时候往下走,往下走到快要出界的时候往右走,往右走到已经访问过的元素(也就是1)的时候,往左走。
思路一代码:
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)
思路二代码:
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
这个问题如果再按照思路一就会很复杂,按照思路二依旧直观简单:只需要让两条蛇同时出发,每次一起走一格即可。
二、LRU算法实现
有元素若干,每次用元素的时候,需要把元素放入到缓存里面,这个缓存最大只能存储N个元素,要求O(1)复杂度实现LRU算法。
方法一:使用平衡二叉树,树中每个结点都有一个时间值,每次增删只需要O(lgN)复杂度
方法二:双向链表,链表中存储的元素从左往右就是按照时间递增顺序排列的,定义Node如下
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 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 ma = new LinkedHashMap();
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());
}
}
}
三、字符串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)复杂度。每次只要识别一个字符串,就要切割一次。
四、单链表排序
给定一个单向链表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个元素和后两个元素进行排序,然后归并之......
而自下而上的归并也是可以的:先将元素两两排序,如:1、2排序,3、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()
五、随机数生成
有一个随机器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次多一点。
六、最火帖子
如何定义“火”?答:最近三天内的赞数、转发数越多,表明越火。
问:如何存储帖子,帖子在数据库中是如何存在的?
问:传入一个帖子,如何计算帖子“火”的程度?
对于大V,粉丝特别多,查询他的粉丝的时候应该怎么做?
七、粉丝列表、关注列表
像微博、知乎这种社交平台,粉丝、关注列表是如何实现的?复杂度如何?如何优化之?(要考虑用户关注、取消关注两种行为)
八、长短URL问题
上次搜狐面试,我没通过。因为在第一题上花的时间太长,面试官只问了我前几个问题。询问其他面试者,说也问“长短URL”这道题了。这次面试又问了这个问题。我感觉这个问题挺无聊的,因为很显然是哈希算法,只不过哈希的方法不一样罢了。
短网址(Short URL),顾名思义就是在形式上比较短的网址。目前已经有许多类似服务,借助短网址您可以用简短的网址替代原来冗长的网址,让使用者可以更容易的分享链接。例如:http://t.cn/SzjPjA
。自从twitter推出短网址(shorturl),继之国内各大微博跟风,google公开goo.gl使用API,短网址之风愈演愈烈.不得不说这是一个新兴又一大热门web2.0服务.
短链接的好处:
1、内容需要:发微博时有字数限制;
2、用户友好:便于记忆,直接输入如URL进行访问;
3、便于管理。
其中便于管理体现在:
短网址可以在我们项目里可以很好的对开放级URL进行管理。有一部分网址可以会涵盖暴力,广告等信息,这样我们可以通过用户的举报,完全管理这个连接将不出现在我们的应用中,应为同样的URL通过加密算法之后,得到的地址是一样的。
我们可以对一系列的网址进行流量,点击等统计,挖掘出大多数用户的关注点,这样有利于我们对项目的后续工作更好的作出决策。
百度搜索“短网址”三个字会看到许多API,深刻说明了“短网址”技术有多么流行,那么“短网址”API内部是如何实现的呢?
把长的字符串映射成短的字符串必定是一个从多映射到少的过程。从多映射到少必然会造成多对一现象,所以把“多”的映射成了“少”的很容易,直接计算哈希值就可以,把少的映射成多的,就必须要通过准确的映射。通过短URL必须能够找到唯一的长URL。这就要解决哈希冲突。
解决哈希冲突有哪些办法呢?
- x的哈希值是y,结果发现y那里已经放了一个x',于是对xx进行哈希得到yy,发现yy没有碰撞,就找到了xx的哈希值。
这样所有的哈希内容都存在同一个哈希map里面。 - 桶哈希:x的哈希值是y,y那里已经有了一个x',那么在x'后面再追加一个x,x'和x形成了一个链表。
1、场景
短链接服务就是将一段长的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
发送请求
2、设计要点
短链接生成算法
(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。
3、优化方案
算法优化
采用以上算法,对于同一个原始URL,每次生成的短链接是不同的,这样就会浪费存储空间,因为需要存储多个短链接到同一个URL的映射,如果能将相同的URL映射成同一个短链接,这样就可以节省存储空间了。
(1)方案1:查表
每次生成短链接时,先在映射表中查找是否已有原始URL的映射关系,如果有,则直接返回结果。很明显,这种方式效率很低。
(2)方案2:使用LRU本地缓存,空间换时间
使用固定大小的LRU缓存,存储最近N次的映射结果,这样,如果某一个链接生成的非常频繁,则可以在LRU缓存中找到结果直接返回,这是存储空间和性能方面的折中。
可伸缩和高可用
如果将短链接生成服务单机部署,缺点一是性能不足,不足以承受海量的并发访问,二是成为系统单点,如果这台机器宕机则整套服务不可 用,为了解决这个问题,可以将系统集群化,进行“分片”。
在以上描述的系统架构中,如果发号器用Redis实现,则Redis是系统的瓶颈与单点,因此,利用数据库分片的设计思想,可部署多个发号器实例,每个实例负责特定号段的发号,比如部署10台Redis,每台分别负责号段尾号为0-9的发号,注意此时发号器的步长则应该设置为10(实例个数)。
另外,也可将长链接与短链接映射关系的存储进行分片,由于没有一个中心化的存储位置,因此需要开发额外的服务,用于查找短链接对应的原始链接的存储节点,这样才能去正确的节点上找到映射关系。
九、Mysql
MySQL有哪些数据库引擎?
如何用MySQL存储树形结构?
十、基础知识
TCP三次握手都是什么意思?
进程和线程有何区别?
进程间通信方式有哪些?
啥叫协程?
十一、Redis
Redis多进程跟单进程的区别是什么?
Redis和Memcache有哪些区别?
十二、面试官说
在我的简历上,写到了自己熟悉MySQL和Redis。而面试官问的关于MySQL和Redis的许多问题,我都没能答出来,场面一度十分尴尬。
我说:我只停留在用过、会用的层面上,没有深入研究这些细节。
面试官问我:为什么不去研究这些细节呢?
我说:在学校没有这种需求,等我工作之后可以慢慢学,在学校有更重要的东西要学。况且,许多技术都是无底洞,倘若把时间都花在钻研某个库上面,那就性价比太低了。
面试官说:有的书粗略一翻即可,有的书却需要逐字逐句仔细研读。技术也是一样,有些技术应该深究,有些技术会用即可。不能因为技术是无底洞,就对所有的技术都浅尝辄止。像一些工作之后必然要用到的东西,比如MySQL、Redis,在学校越熟悉越好,对底层了解越多越好,这样当你走上工作岗位,跳过了学习的时间,才能迅速脱颖而出。工作并不像你们想的那么轻松,工作之后也没有那么多空闲时间供你学习。你们在学校其实空闲时间已经算多的了。到了我这年龄,再想学习一门新东西是很慢的,不管学什么东西都要趁年轻。
确实,许多知识我只是停留在会用的层面上,这是不行的,要对技术了如指掌,要追求深刻。
参考资料
https://blog.csdn.net/lz0426001/article/details/52370177