給定二維數組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
有元素若干,每次用元素的時候,須要把元素放入到緩存裏面,這個緩存最大隻能存儲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()); } } }
給定字符串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個元素和後兩個元素進行排序,而後歸併之......
而自下而上的歸併也是能夠的:先將元素兩兩排序,如:一、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()
有一個隨機器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。這四種狀況的個數分別爲:
這樣咱們就製造出了利用率爲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」這道題了。此次面試又問了這個問題。我感受這個問題挺無聊的,由於很顯然是哈希算法,只不過哈希的方法不同罷了。
短網址(Short URL),顧名思義就是在形式上比較短的網址。目前已經有許多相似服務,藉助短網址您能夠用簡短的網址替代原來冗長的網址,讓使用者能夠更容易的分享連接。例如:http://t.cn/SzjPjA
。自從twitter推出短網址(shorturl),繼之國內各大微博跟風,google公開goo.gl使用API,短網址之風愈演愈烈.不得不說這是一個新興又一大熱門web2.0服務.
短連接的好處:
一、內容須要:發微博時有字數限制;
二、用戶友好:便於記憶,直接輸入如URL進行訪問;
三、便於管理。
其中便於管理體如今:
短網址能夠在咱們項目裏能夠很好的對開放級URL進行管理。有一部分網址能夠會涵蓋暴力,廣告等信息,這樣咱們能夠經過用戶的舉報,徹底管理這個鏈接將不出如今咱們的應用中,應爲一樣的URL經過加密算法以後,獲得的地址是同樣的。
咱們能夠對一系列的網址進行流量,點擊等統計,挖掘出大多數用戶的關注點,這樣有利於咱們對項目的後續工做更好的做出決策。
百度搜索「短網址」三個字會看到許多API,深入說明了「短網址」技術有多麼流行,那麼「短網址」API內部是如何實現的呢?
把長的字符串映射成短的字符串一定是一個從多映射到少的過程。從多映射到少必然會形成多對一現象,因此把「多」的映射成了「少」的很容易,直接計算哈希值就能夠,把少的映射成多的,就必需要經過準確的映射。經過短URL必須可以找到惟一的長URL。這就要解決哈希衝突。
解決哈希衝突有哪些辦法呢?
短連接服務就是將一段長的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(實例個數)。
另外,也可將長連接與短連接映射關係的存儲進行分片,因爲沒有一箇中心化的存儲位置,所以須要開發額外的服務,用於查找短連接對應的原始連接的存儲節點,這樣才能去正確的節點上找到映射關係。
MySQL有哪些數據庫引擎?
如何用MySQL存儲樹形結構?
TCP三次握手都是什麼意思?
進程和線程有何區別?
進程間通訊方式有哪些?
啥叫協程?
Redis多進程跟單進程的區別是什麼?
Redis和Memcache有哪些區別?
在個人簡歷上,寫到了本身熟悉MySQL和Redis。而面試官問的關於MySQL和Redis的許多問題,我都沒能答出來,場面一度十分尷尬。
我說:我只停留在用過、會用的層面上,沒有深刻研究這些細節。
面試官問我:爲何不去研究這些細節呢?
我說:在學校沒有這種需求,等我工做以後能夠慢慢學,在學校有更重要的東西要學。何況,許多技術都是無底洞,假若把時間都花在鑽研某個庫上面,那就性價比過低了。
面試官說:有的書粗略一翻便可,有的書卻須要逐字逐句仔細研讀。技術也是同樣,有些技術應該深究,有些技術會用便可。不能由於技術是無底洞,就對全部的技術都淺嘗輒止。像一些工做以後必然要用到的東西,好比MySQL、Redis,在學校越熟悉越好,對底層瞭解越多越好,這樣當你走上工做崗位,跳過了學習的時間,才能迅速脫穎而出。工做並不像大家想的那麼輕鬆,工做以後也沒有那麼多空閒時間供你學習。大家在學校其實空閒時間已經算多的了。到了我這年齡,再想學習一門新東西是很慢的,無論學什麼東西都要趁年輕。
確實,許多知識我只是停留在會用的層面上,這是不行的,要對技術瞭如指掌,要追求深入。