程式設計師面試常問的小演算法總結
前言
本文快速回顧了面試常考的演算法,用作面試複習,事半功倍。
需要說明的是,由於演算法的程式碼實現主要注重思路的清晰,下方有程式碼實現的文章主要以Python為主,Java為輔,對於Python薄弱的同學敬請不用擔心,幾乎可以看作是虛擬碼,可讀性比較好。如實在有困難可以自行搜尋Java程式碼
此外,關於演算法的文章之後也會單獨開設演算法專欄進行總結,敬請期待。
面試知識點複習手冊
全複習手冊文章導航
已釋出知識點複習手冊
- Java基礎知識點面試手冊
- Java容器(List、Set、Map)知識點快速複習手冊
- Java併發知識點快速複習手冊(上)
- Java併發知識點快速複習手冊(下)
- Java虛擬機器知識點快速複習手冊(上)
- Java虛擬機器知識點快速複習手冊(下)
- 快速梳理23種常用的設計模式
- Redis基礎知識點面試手冊
- Leetcode題解分類彙總(前150題)
- 面試常問的小演算法總結
- 查詢演算法總結及其部分演算法實現Python/Java
- 排序演算法實現與總結Python/Java
- HTTP應知應會知識點複習手冊(上)
- HTTP應知應會知識點複習手冊(下)
- ......等(請檢視全複習手冊導航)
-----正文開始-----
圖的最短路徑演算法
Floyd最短路演算法(多源最短路)
參考:https://www.cnblogs.com/ahalei/p/3622328.html
上圖中有4個城市8條公路,公路上的數字表示這條公路的長短。請注意這些公路是單向的。我們現在需要求任意兩個城市之間的最短路程,也就是求任意兩個點之間的最短路徑。這個問題這也被稱為“多源最短路徑”問題。
現在需要一個數據結構來儲存圖的資訊,我們仍然可以用一個4*4的矩陣(二維陣列e)來儲存。
核心程式碼:
for(k=1;k<=n;k++) for(i=1;i<=n;i++) for(j=1;j<=n;j++) if(e[i][j]>e[i][k]+e[k][j]) e[i][j]=e[i][k]+e[k][j];
這段程式碼的基本思想就是:
最開始只允許經過1號頂點進行中轉,接下來只允許經過1和2號頂點進行中轉……允許經過1~n號所有頂點進行中轉,求任意兩點之間的最短路程。用一句話概括就是:從i號頂點到j號頂點只經過前k號點的最短路程。
Dijkstra最短路演算法(單源最短路)
參考:http://blog.51cto.com/ahalei/1387799
指定一個點(源點)到其餘各個頂點的最短路徑,也叫做“單源最短路徑”。例如求下圖中的1號頂點到2、3、4、5、6號頂點的最短路徑。
仍然使用二維陣列e來儲存頂點之間邊的關係,初始值如下。
我們還需要用一個一維陣列dis來儲存1號頂點到其餘各個頂點的初始路程,如下。
將此時dis陣列中的值稱為最短路的“估計值”。
既然是求1號頂點到其餘各個頂點的最短路程,那就先找一個離1號頂點最近的頂點。通過陣列dis可知當前離1號頂點最近是2號頂點。當選擇了2號頂點後,dis[2]的值就已經從“估計值”變為了“確定值”,即1號頂點到2號頂點的最短路程就是當前dis[2]值。
既然選了2號頂點,接下來再來看2號頂點有哪些出邊呢。有2->3和2->4這兩條邊。先討論通過2->3這條邊能否讓1號頂點到3號頂點的路程變短。也就是說現在來比較dis[3]和dis[2]+e[2][3]的大小。其中dis[3]表示1號頂點到3號頂點的路程。dis[2]+e[2][3]中dis[2]表示1號頂點到2號頂點的路程,e[2][3]表示2->3這條邊。所以dis[2]+e[2][3]就表示從1號頂點先到2號頂點,再通過2->3這條邊,到達3號頂點的路程。
這個過程有個專業術語叫做“鬆弛”。鬆弛完畢之後dis陣列為:
接下來,繼續在剩下的3、4、5和6號頂點中,選出離1號頂點最近的頂點4,變為確定值,以此類推。
最終dis陣列如下,這便是1號頂點到其餘各個頂點的最短路徑。
//Dijkstra演算法核心語句
for(i=1;i<=n-1;i++)
{
//找到離1號頂點最近的頂點
min=inf;
for(j=1;j<=n;j++)
{
if(book[j]==0 && dis[j]<min)
{
min=dis[j];
u=j;
}
}
book[u]=1;
for(v=1;v<=n;v++)
{
if(e[u][v]<inf)
{
if(dis[v]>dis[u]+e[u][v])
dis[v]=dis[u]+e[u][v];
}
}
}
-
M:邊的數量
-
N:節點數量
通過上面的程式碼我們可以看出,這個演算法的時間複雜度是O(N^2)。其中每次找到離1號頂點最近的頂點的時間複雜度是O(N)
優化:
-
這裡我們可以用“堆”(以後再說)來優化,使得這一部分的時間複雜度降低到O(logN)。
-
另外對於邊數M少於N2的稀疏圖來說(我們把M遠小於N2的圖稱為稀疏圖,而M相對較大的圖稱為稠密圖),我們可以用鄰接表來代替鄰接矩陣,使得整個時間複雜度優化到O( (M+N)logN)。
-
請注意!在最壞的情況下M就是N2,這樣的話MlogN要比N2還要大。但是大多數情況下並不會有那麼多邊,因此(M+N)logN要比N2小很多。
用鄰接表代替鄰接矩陣儲存
參考:http://blog.51cto.com/ahalei/1391988
略微難懂,請參考原文
總結如下:
可以發現使用鄰接表來儲存圖的時間空間複雜度是O(M),遍歷每一條邊的時間複雜度是也是O(M)。如果一個圖是稀疏圖的話,M要遠小於N^2。因此稀疏圖選用鄰接表來儲存要比鄰接矩陣來儲存要好很多。
漢諾塔
參考圖例:https://www.zhihu.com/question/24385418/answer/89435529
關鍵程式碼:
def move(n,a,b,c):
if n == 1:
print(a, "--->", c)
else:
move(n-1,a,c,b)
print(a, "--->", c)
move(n-1,b,a,c)
楊輝三角
參考:https://blog.csdn.net/zmy_3/article/details/51173580
關鍵程式碼(巧用python中的yield):
註釋:N加上一個0之後,最後一個數是1+0,直接就等於1,不會有0
def triangles():
N=[1]
while True:
yield N
N.append(0)
N=[N[i-1] + N[i] for i in range(len(N))]
n=0
for t in triangles():
print(t)
n=n+1
if n == 10:
break
迴文數/迴文串
解法一:暴力
return a == a[::-1]
解法二:分字串和數字
# 字串/數字
a = len(s)
i = 0
while i <= (a/2):
if s[i] == s[a-1-i]:
i += 1
else:
return False
return True
# 數字
def isPalindrome(x):
if x < 0:
return False
temp = x
y = 0
while temp:
y = y*10 + temp%10
temp /= 10
return x == y
斐波拉契數列(Fibonacci)
def fibonacci(): # 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
f = [0] * MAXSIZE
f[0] = 1
f[1] = 1
for i in range(2, MAXSIZE):
f[i] = f[i-1] + f[i-2]
return f
fibs = [1,1]
for i in range(8):
fibs.append(fibs[-2] + fibs[-1])
最大子序列與最大子矩陣問題
陣列的最大子序列問題
給定一個數組,其中元素有正,也有負,找出其中一個連續子序列,使和最大。
參考自己的部落格:https://blog.csdn.net/qqxx6661/article/details/78167981
可以理解為動態規劃:
dp[i] = dp[i-1] + s[i] (dp[i-1] >= 0)
dp[i] = s[i] (dp[i-1] < 0)
可以用標準動態規劃求解也可以用直接方法求解,但思路都是動態規劃
最大子矩陣問題
給定一個矩陣(二維陣列),其中資料有大有小,請找一個子矩陣,使得子矩陣的和最大,並輸出這個和。
參考:https://blog.csdn.net/kavu1/article/details/50547401
思路:
原始矩陣可以是二維的。假設原始矩陣是一個3 * n 的矩陣,那麼它的子矩陣可以是 1 * k, 2 * k, 3 * k,(1 <= k <= n)。 如果是1*K,這裡有3種情況:子矩陣在第一行,子矩陣在第二行,子矩陣在第三行。如果是 2 * k,這裡有兩種情況,子矩陣在第一、二行,子矩陣在第二、三行。如果是3 * k,只有一種情況。
為了能夠找出最大的子矩陣,我們需要考慮所有的情況。假設這個子矩陣是 2 * k, 也就是說它只有兩行,要找出最大子矩陣,我們要從左到右不斷的遍歷才能找出在這種情況下的最大子矩陣。如果我們把這兩行上下相加,情況就和求“最大子段和問題” 又是一樣的了。
KMP演算法
原理參考:
http://www.ruanyifeng.com/blog/2013/05/Knuth–Morris–Pratt_algorithm.html
移動位數 = 已匹配的字元數 - 對應的部分匹配值
"部分匹配值"就是"字首"和"字尾"的最長的共有元素的長度。以"ABCDABD"為例,
- "A"的字首和字尾都為空集,共有元素的長度為0;
- "AB"的字首為[A],字尾為[B],共有元素的長度為0;
- "ABC"的字首為[A, AB],字尾為[BC, C],共有元素的長度0;
- "ABCD"的字首為[A, AB, ABC],字尾為[BCD, CD, D],共有元素的長度為0;
- "ABCDA"的字首為[A, AB, ABC, ABCD],字尾為[BCDA, CDA, DA, A],共有元素為"A",長度為1;
- "ABCDAB"的字首為[A, AB, ABC, ABCD, ABCDA],字尾為[BCDAB, CDAB, DAB, AB, B],共有元素為"AB",長度為2;
- "ABCDABD"的字首為[A, AB, ABC, ABCD, ABCDA, ABCDAB],字尾為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
實現參考自己的部落格:
https://blog.csdn.net/qqxx6661/article/details/79583707
LCS/最長公共子序列/最長公共子串
參考自己的部落格:
https://blog.csdn.net/qqxx6661/article/details/79587392
最長公共子序列LCS
動態規劃狀態轉移方程式
最長公共迴文子串
動態規劃狀態轉移方程式
求圓周率
from random import random
from math import sqrt
from time import clock #計算程式執行時間
DARTS=1200 #拋灑點的個數
#DARTS=5000
#DARTS=20000
#DARTS=1000000
hists=0 #拋灑點在1/4(半徑為1)圓內點的個數
clock()
for i in range(1,DARTS):
x,y=random(),random()
dict=sqrt(x**2+y**2)
if dict<=1.0:
hists=hists+1 #隨機設點,若拋灑點在1/4圓內,則dice+1
pi=4*(hists/DARTS)
print("PI的值是 %s" %pi)
print("程式執行的時間是 %-5.5ss" %clock())
大數問題(加減乘除)
加法
對應位置相加,考慮進位,前面不夠補0
L1 = "2649821731631836529481632803462831616487712734074314936141303241873417434716340124362304724324324324324323412121323164329751831"
L2 = "1045091731748365195814509145981509438583247509149821493213241431431319999999999999999999999999999999999999999999999999341344779"
# 長度強行扭轉到一致,不夠前面補0
max_len = len(L1) if len(L1) > len(L2) else len(L2)
l1 = L1.zfill(max_len)
l2 = L2.zfill(max_len)
a1 = list(l1)
a2 = list(l2)
# 99+99最大3位所以多一位
result_list = [0] * (max_len + 1)
# 長度一致 每個對應的位置的相加的和 %10 前一位補1 如果>10 否則0
for index in range(max_len - 1, -1, -1):
index_sum = result_list[index + 1] + int(a1[index]) + int(a2[index])
less = index_sum - 10
result_list[index + 1] = index_sum % 10
result_list[index] = 1 if less >= 0 else 0
# 若第一位為0,去除
if result_list[0] == 0:
result_list.pop(0)
# 轉為str的list
result = [str(i) for i in result_list]
print(''.join(result))
減法
和相加十分類似
就是按照我們手寫除法時的方法,兩個數字末位對齊,從後開始,按位相減,不夠減時向前位借一。
最終結果需要去除首端的0.如果所有位都是0,則返回0。
乘法
大數乘法問題及其高效演算法:
https://blog.csdn.net/u010983881/article/details/77503519
方法:
- 模擬小學乘法:最簡單的乘法豎式手算的累加型;
自己實現的:https://blog.csdn.net/qqxx6661/article/details/78119074
- 分治乘法:最簡單的是Karatsuba乘法,一般化以後有Toom-Cook乘法;
見下方
- 快速傅立葉變換FFT:(為了避免精度問題,可以改用快速數論變換FNTT),時間複雜度O(N lgN lglgN)。具體可參照Schönhage–Strassen algorithm;
- 中國剩餘定理:把每個數分解到一些互素的模上,然後每個同餘方程對應乘起來就行;
- Furer’s algorithm:在漸進意義上FNTT還快的演算法。不過好像不太實用,本文就不作介紹了。大家可以參考維基百科
方法2:
參考:
https://blog.csdn.net/jeffleo/article/details/53446095
Karatsuba乘法(公式和下面程式碼實現的不同,但是原理相同,可以直接背下方程式碼)
核心語句:
long z2 = karatsuba(a, c);
long z0 = karatsuba(b, d);
long z1 = karatsuba((a + b), (c + d)) - z0 - z2;
return (long)(z2 * Math.pow(10, (2*halfN)) + z1 * Math.pow(10, halfN) + z0);
完整程式碼:
/**
* Karatsuba乘法
*/
public static long karatsuba(long num1, long num2){
//遞迴終止條件
if(num1 < 10 || num2 < 10) return num1 * num2;
// 計算拆分長度
int size1 = String.valueOf(num1).length();
int size2 = String.valueOf(num2).length();
int halfN = Math.max(size1, size2) / 2;
/* 拆分為a, b, c, d */
long a = Long.valueOf(String.valueOf(num1).substring(0, size1 - halfN));
long b = Long.valueOf(String.valueOf(num1).substring(size1 - halfN));
long c = Long.valueOf(String.valueOf(num2).substring(0, size2 - halfN));
long d = Long.valueOf(String.valueOf(num2).substring(size2 - halfN));
// 計算z2, z0, z1, 此處的乘法使用遞迴
long z2 = karatsuba(a, c);
long z0 = karatsuba(b, d);
long z1 = karatsuba((a + b), (c + d)) - z0 - z2;
return (long)(z2 * Math.pow(10, (2*halfN)) + z1 * Math.pow(10, halfN) + z0);
}
除法
Leetcode原題(用位運算加速了手動除法)
https://blog.csdn.net/qqxx6661/article/details/79723357
為了加速運算,可以依次將被除數減去1,2,4,8,..倍的除數,本質上只是用位運算加速了手動除法
計算機計算乘除法的原理
位運算除法
https://blog.csdn.net/zdavb/article/details/47108505
最小生成樹
圖解Prim演算法和Kruskal演算法:
https://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html
兩種方法的時間複雜度
Prim:
這裡記頂點數v,邊數e
- 鄰接矩陣:O(v2)
- 鄰接表:O(elog2v)
Kruskal:
elog2e e為圖中的邊數
# coding=utf-8
import sys
class Graph(object):
def __init__(self, maps):
self.maps = maps
self.nodenum = self.get_nodenum()
self.edgenum = self.get_edgenum()
def get_nodenum(self):
return len(self.maps)
def get_edgenum(self):
count = 0
for i in range(self.nodenum):
for j in range(i):
if self.maps[i][j] > 0 and self.maps[i][j] < 9999:
count += 1
return count
def kruskal(self):
res = []
if self.nodenum <= 0 or self.edgenum < self.nodenum - 1:
return res
edge_list = []
for i in range(self.nodenum):
for j in range(i, self.nodenum):
if self.maps[i][j] < 9999:
edge_list.append([i, j, self.maps[i][j]]) # 按[begin, end, weight]形式加入
edge_list.sort(key=lambda a: a[2]) # 已經排好序的邊集合
group = [[i] for i in range(self.nodenum)]
for edge in edge_list:
for i in range(len(group)):
if edge[0] in group[i]:
m = i
if edge[1] in group[i]:
n = i
if m != n:
res.append(edge)
group[m] = group[m] + group[n]
group[n] = []
return res
def prim(self):
res = []
if self.nodenum <= 0 or self.edgenum < self.nodenum - 1:
return res
res = []
seleted_node = [0]
candidate_node = [i for i in range(1, self.nodenum)]
while len(candidate_node) > 0:
begin, end, minweight = 0, 0, 9999
for i in seleted_node:
for j in candidate_node:
if self.maps[i][j] < minweight:
minweight = self.maps[i][j]
begin = i
end = j
res.append([begin, end, minweight])
seleted_node.append(end)
candidate_node.remove(end)
return res
if __name__ == "__main__":
# 讀取第一行的n
n = int(sys.stdin.readline().strip(''))
cun_list = list(map(int,sys.stdin.readline().strip('').split(' ')))
dp = [[0 for __ in range(n)] for __ in range(n)]
for i in range(n):
for j in range(n):
if i == j:
dp[i][j] = 0
continue
dp[i][j] = max(cun_list[i],cun_list[j])
print(dp)
graph = Graph(dp)
# print('鄰接矩陣為\n%s' % graph.maps)
# print('節點資料為%d,邊數為%d\n' % (graph.nodenum, graph.edgenum))
# print('------最小生成樹kruskal演算法------')
# print(graph.kruskal())
# print('------最小生成樹prim演算法')
# print(graph.prim())
print()
-----正文結束-----
關注我
我是蠻三刀把刀,目前為後臺開發工程師。主要關注後臺開發,網路安全,Python爬蟲等技術。
來微信和我聊聊:yangzd1102
Github:https://github.com/qqxx6661
原創部落格主要內容
- 筆試面試複習知識點手冊
- Leetcode演算法題解析(前150題)
- 劍指offer演算法題解析
- Python爬蟲相關技術分析和實戰
- 後臺開發相關技術分析和實戰
同步更新以下部落格
1. Csdn
擁有專欄:Leetcode題解(Java/Python)、Python爬蟲開發、面試助攻手冊
2. 知乎
https://www.zhihu.com/people/yang-zhen-dong-1/
擁有專欄:碼農面試助攻手冊
3. 掘金
https://juejin.im/user/2119514149109095
4. 簡書
https://www.jianshu.com/u/b5f225ca2376
個人公眾號:Rude3Knife
如果文章對你有幫助,不妨收藏起來並轉發給您的朋友們~