算法入門推薦:《算法圖解》
介紹一本關於算法基礎的入門級書籍,對於非科班出身的人來說,算法和數據結構的補充還是很有必要的,但是這些東西往往又是很枯燥以致於打消了很多人的積極性,《算法圖解》用python爲編程語言,對於一些基礎性的算法介紹可以說很通俗易懂了,真的很適合入門,同時這篇文章我也結合了一些校招題目來進行一定程度的擴充,有一些用到了C++。
一、二分查找(O(log n))
def binary_search(list,item):
low = 0;
high = len(list)-1
while low <= high :
mid = int((low+high)/2)
guess =list[mid]
if guess == item:
return mid
if guess < item:
low = mid+1
else:
high=mid-1
return None
my_list =[1,3,5,7,9]
print (binary_search(my_list,5))
print (binary_search(my_list,-1))
輸出結果:
2
None
二分查找的考查有很多,例如有一年字節跳動的編程題就是用到了這個方法:
問題描述:
總共有n條長度不等的繩子,可以任意切割,不能拼接,要求切割後得到m條長度相等的繩子,求問得到的這些長度相等的繩子的長度最大值L。
輸入: 繩子的條數n;n條繩子的長度;要求切成的繩子數量m
輸出:切割成相同長度的m條繩子的最大的長度
思路解析:
這道題其實就是二分查找的題目,區別於動態規劃(在接下來的那個板塊我會講到也是割繩子的另一道題),因爲我們知道最長長度的繩子,可以確定最終將剪成的長度只能在0到這個數中間,我們對這個範圍進行二分查找,然後對得到的guess值,我們構造一個函數來判斷是否符合要求,這個函數就是遍歷所有繩子算出能剪出多少段這樣長度的繩子,這個數量大於m則說明短了,這個數量小於m則說明長了,這樣的二分查找還是挺清晰的。
N = input()
N=int (N)
max_len = 0.0
min_len = 0.0
num={}
for i in range(N):
num[i]=int (input())
if num[i]>max_len:
max_len=num[i]
M = input()
M=int (M)
def check(length):
ans = 0
for i in range(N):
ans += (int)(num[i]/length)
return ans
while max_len-min_len>=0.00001:
mid = (max_len+min_len)/2
if(M>check(mid)):
max_len=mid
else:
min_len=mid
print(mid)
示例結果:
5
5
5
8
10
10
45
0.8333301544189453
這道題卡的是精度,要把精度提高才能Access。
二、排序算法
在這本書介紹的常見的排序算法是選擇排序和快速排序,下面是選擇排序的程序:
def findsmallest(arr):
smallest = arr[0]
smallest_index = 0
for i in range(0,len(arr)):
if arr[i]<smallest:
smallest = arr[i]
smallest_index=i
return smallest_index
def selectionsort(arr):
newArr=[0]*7
for i in range(len(arr)):
smallest=findsmallest(arr)
newArr[i]=(arr.pop(smallest))
return newArr
print(selectionsort([5,3,6,2,10,22,45]))
示例結果:
[5, 3, 6, 2, 10, 22, 45]
按照書本的內容,在介紹完選擇排序之後就講了遞歸,相信很多人對於遞歸還是有一定的瞭解,書本的介紹基於棧原理的實現,感興趣的可以去看一下,我就直接進入快速排序了,我將通過一道牛客網上的題目對它進行實現:
問題描述:
爲了找到自己滿意的工作,牛牛收集了每種工作的難度和報酬。牛牛選工作的標準是在難度不超過自身能力值的情況下,牛牛選擇報酬最高的工作。在牛牛選定了自己的工作後,牛牛的小夥伴們來找牛牛幫忙選工作,牛牛依然使用自己的標準來幫助小夥伴們。牛牛的小夥伴太多了,於是他只好把這個任務交給了你。
輸入:
每個輸入包含一個測試用例。每個測試用例的第一行包含兩個正整數,分別表示工作的數量N(N<=100000)和小夥伴的數量M(M<=100000)。接下來的N行每行包含兩個正整數,分別表示該項工作的難度Di(Di<=1000000000)和報酬Pi(Pi<=1000000000)。接下來的一行包含M個正整數,分別表示M個小夥伴的能力值Ai(Ai<=1000000000)。保證不存在兩項工作的報酬相同
輸出:
對於每個小夥伴,在單獨的一行輸出一個正整數表示他能得到的最高報酬。一個工作可以被多個人選擇
代碼如下:
#include<iostream>
#include<algorithm>
using namespace std;
bool cmp(int a[] , int b[])
{
if(a[1]==b[1])
{
return a[0]>b[0];
}
return a[1]>b[1];
}
//use the quicksort algorithm to solve the two dimension array
void quicksort(int **a,int left,int right)
{
int pos1=left;
int pos2=right;
if(left<right)
{
int temp=a[left][0];
int temp1=a[left][1];
while(left<right)
{
while(left<right&&(a[right][0]>temp||(a[right][0]==temp&&a[right][1]>temp1)))
right--;
a[left][0]=a[right][0];
a[left][1]=a[right][1];
while(left<right&&(a[left][0]<temp||(a[left][0]==temp&&a[left][1]<temp1)))
left++;
a[right][0]=a[left][0];
a[right][1]=a[left][1];
}
a[left][0]=temp;
a[left][1]=temp1;
quicksort(a,pos1,left-1);
quicksort(a,left+1,pos2);
}
}
int main()
{
int num_of_work,num_of_worker;
cin>>num_of_work>>num_of_worker;
int **work=new int*[num_of_work];
for(int i=0;i<num_of_work;i++)
{
work[i]=new int[2];
cin>>work[i][0]>>work[i][1];
}
//sort the array by the algorithm in C++,and we get the ascent order
sort(work,work+num_of_work,cmp);
//sort the array by the self function,and we get the descent order
//quicksort(work,0,num_of_work-1);
int *worker=new int[num_of_worker];
int *money=new int[num_of_worker];
for(int i=0;i<num_of_worker;i++)
{
cin>>worker[i];
int cando=worker[i];
for(int j=0;j<num_of_work;j++)
{
if(cando>=work[j][0])
{
money[i]=work[j][1];
break;
}
}
}
for(int i=0;i<num_of_worker;i++)
cout<<money[i]<<endl;
}
對於這道題雖然上述方法只能通過80%,因爲它的複雜度太高了O(mn),使用了暴力搜索的方法。但是上面的代碼示範了怎麼自己寫二維數組的快速排序,同時也示範了怎麼使用庫函數進行二維數組的排序,下面是複雜度低的優化程序(Access程序)
#include<iostream>
#include<algorithm>
using namespace std;
struct job
{
int dif;
int money;
};
struct people
{
int index;
int dif;
int money;
};
bool cmp1(job a,job b)
{
return a.dif<b.dif;
}
bool cmp2(people a,people b)
{
return a.dif<b.dif;
}
bool cmp3(people a,people b)
{
return a.index<b.index;
}
int main()
{
int num_of_job,num_of_people;
cin>>num_of_job>>num_of_people;
job *Job=new job[num_of_job];
people *People =new people[num_of_people];
for(int i=0;i<num_of_job;i++)
{
cin>>Job[i].dif>>Job[i].money;
}
for(int i=0;i<num_of_people;i++)
{
cin>>People[i].dif;
People[i].index=i;
}
sort(Job,Job+num_of_job,cmp1);
sort(People,People+num_of_people,cmp2);
int j=0;int maxmoney=0;
for(int i=0;i<num_of_people;i++)
{
while(j<num_of_job)
{
if(Job[j].dif>People[i].dif)
break;
else
{
maxmoney=max(maxmoney,Job[j].money);
j++;
}
}
People[i].money=maxmoney;
}
sort(People,People+num_of_people,cmp3);
for(int i=0;i<num_of_people;i++)
{
cout<<People[i].money<<endl;
}
}
代碼還是很容易理解的,具體就不講了。
三、散列表
這一部分介紹了散列表的功能,對於基礎實現沒有過多的解釋(其實更多依靠散列函數的實現),我也就簡單講一下,先舉一個簡單的例子,給定N個整數,再給定M個整數,要你檢查出M個數中每個數中是否在N個整數裏有出現過,可能你會直觀地想到遍歷查詢,但對於N和M很大時,顯然是無法承受的(O(MN)),所以可以用Hashtable這個bool數組來記錄N個數裏面每一個數是否出現過,如果出現了,就記爲true,否則爲false,那麼查詢的時候,我們可以直接把輸入的數作爲數組下標就可以得到是否出現的結果,這是一個最簡單的例子,但是,如果我們的輸入不是整數,而是一些字符串等等,那麼就需要用到散列來映射到一個整數上,這就是我們所說的散列表。
散列函數可以有直接定址法,也就是恆等變化或者線性變化:H(key)=key,H(key)=a*key+b;也有除留餘數法:H(key)%mod;
稍加思考便可以注意到:通過除留餘數法可能會有兩個不同的Key得到相同的hash值,他們無法佔用相同的位置,這種情況叫衝突,解決衝突的方法有:線性探查法(當得到的值已經被佔用了就順移到接下來的位置直到有空位,但這種方法容易導致扎推,也就是如果連續若干個位置都被佔用,一定程度會降低效率。)與之相似的有平方探查法,而相對不一樣的就是鏈地址法,也就是如果計算得到的hash值相同,就把所有相同hash值的key連成一條鏈表。
四、廣度優先搜索
def search(name):
search_queue =deque()
search_queue +=graph[name]
searched = []
while search_queue:
person = search_queue.popleft()
if not person in searched:
if person_is_seller(person):
return True
else:
search_queue +=graph[person]
search.append(person)
return False
search("you")
廣度優先搜索通俗一點就是通過隊列把你周圍最近的元素都塞進去,然後一個一個pop出來檢查,每檢查一個,就把這個元素周圍的沒有訪問過的元素塞進隊列裏面,繼續檢查,然後檢查完了標記它爲已檢查。
對於廣度優先搜索,比較常見的問題是迷宮問題,可以看看《算法筆記》的一些介紹,我就列舉一道迷宮的題目補充一下:
問題描述:
給定一個迷宮m*n大小,"*“代表不可以通過的牆壁,而”."代表平地,S代表起點,T代表終點,移動過程中,當前位置只能上下左右移動,求最短路徑。迷宮如下:(S座標是(2,2))
. . . . .
. * . * .
. * S * .
. * * * .
. . . T *
思路解析:
可以用廣度優先搜索通過層次的順序來遍歷,找到最小步數。
代碼:
#include<iostream>
#include<queue>
using namespace std;
const int maxn=1000;
char maze[maxn][maxn];
bool visited[maxn][maxn];
struct node{int x,y;int step;}S,T,Node;
int n,m;
int x[4] = {0,0,1,-1};
int y[4] = {1,-1,0,0};
bool isok(int x,int y)
{
if(x>=n||x<0||y>=m||y<0)
{
return false;
}
if(maze[x][y]=='*')
{
return false;
}
if(visited[x][y]==true)
{
return false;
}
return true;
}
int BFS()
{
queue<node> q;
q.push(S);
while(!q.empty())
{
node top = q.front();
q.pop();
if(top.x==T.x&&top.y==T.y)
{
return top.step;
}
for(int i=0;i<4;i++)
{
int tmpx=top.x+x[i];
int tmpy=top.y+y[i];
if(isok(tmpx,tmpy))
{
Node.x=tmpx;
Node.y=tmpy;
Node.step=top.step+1;
q.push(Node);
visited[tmpx][tmpy]=true;
}
}
}
return -1;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
getchar();
for(int j=0;j<m;j++)
{
maze[i][j]=getchar();
}
maze[i][m+1]='\0';
}
cin>>S.x>>S.y>>T.x>>T.y;
S.step=0;
cout<<BFS();
}
示例:
5
5
…
.*.*.
.*S*.
.***.
…T*
2 2 4 3輸出結果:
11
五、Dijkstra算法
在這裏舉一個比較常見的例題:
問題簡述:
給出N個城市,M條無向邊,每個城市中都有一定數目的救援小組,所有比那的邊權已知,現在給出起點和重點,求起點到終點的最短路徑條數及最短路徑上的救援小組數目之和,如果有多條路徑,輸出數目之和最大的。
思路:
本題在求解最短路徑的同時需要求解另外兩個信息,最短路徑條數和最短路徑上的最大點權值和,因此我們可以令w[u]表示從起點s到頂點u可以得到的最大的點權之和,初始化爲0,令num[u]表示從起點s到頂點u的最短路徑條數,初始化時只有num[s]爲1,其餘num[u]爲0.然後在更新d[v]時同時更新兩個數組,代碼如下:
#include<iostream>
#include<cstring>
using namespace std;
const int MAXV = 510;
const int INF = 1000000000;
//n爲頂點數,m爲邊數,st和ed分別爲起點和終點
//G爲鄰接矩陣,weight爲點權
//d【】記錄最短距離,w【】記錄最大點權之和,num【】記錄最短路徑條數
int n,m,st,ed,G[MAXV][MAXV],weight[MAXV];
int d[MAXV],w[MAXV],num[MAXV];
bool vis[MAXV]={false};
void Dijkstra(int s)
{
fill(d,d+MAXV,INF);
memset(num,0,sizeof(num));
memset(w,0,sizeof(w));
d[s]=0;
w[s]=weight[s];
num[s]=1;
for(int i=0;i<n;i++)
{
int u=-1,MIN=INF;
//找到未訪問的頂點
for(int j=0;j<n;j++)
{
if(vis[j]==false&&d[j]<MIN)
{
MIN=d[j];
u=j;
}
}
if(u==-1) return;
vis[u]=true;
for(int v=0;v<n;v++)
{
if(vis[v]==false&&G[u][v]!=INF)
{
if(d[u]+G[u][v]<d[v])
{
d[v]=d[u]+G[u][v];
w[v]=w[u]+weight[v];
num[v]=num[u];
}
else if(d[u]+G[u][v]==d[v])
{
if(w[u]+weight[v]>w[v])
w[v]=w[u]+weight[v];
num[v]+=num[u];
}
}
}
}
}
int main()
{
cin>>n>>m>>st>>ed;
for(int i=0;i<n;i++)
{
cin>>weight[i];
}
int u,v;
fill(G[0],G[0]+MAXV*MAXV,INF);
for(int i=0;i<m;i++)
{
cin>>u>>v;
cin>>G[u][v];
G[v][u]=G[u][v];
}
Dijkstra(st);
cout<<num[ed]<<" "<<w[ed]<<endl;
}
示例:
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1輸出:
2 4
六、動態規劃
動態規劃最經典的就是揹包問題,包括01揹包和完全揹包問題,這裏重點講一下01揹包問題,01問題涉及推導公式,可以參考《算法圖解》裏面的解釋,但是最終我們可以得到一條普適性的公式(與完全揹包問題的區別也在於這條公式),當然01揹包問題也可以作爲經典的深度優先搜索教案,我們先看一下公式法的寫法,再看看深度優先搜索的方法:
問題描述:
有n件物品,每件物品的重量爲w[i],價值爲c[i]。現有一個容量爲V的揹包,問如何選取物品放入揹包,使得揹包內物品價值最大,每種物品只有1件。
思路:
直接上公式:dp[v]=max(dp[v],dp[v-w[i]]+c[i])
代碼:
#include<iostream>
using namespace std;
const int maxn=100;
const int maxv=1000;
int w[maxn],c[maxn],dp[maxv];
int main()
{
int n,V;
cin>>n>>V;
for(int i=0;i<n;i++)
{
cin>>w[i];
}
for(int i=0;i<n;i++)
{
cin>>c[i];
}
for(int v=0;v<=V;v++)
{
dp[v]=0;
}
for(int i=1;i<=n;i++)
{
for(int v=V;v>=w[i];v--)
{
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
}
}
int max=0;
for(int v=0;v<=V;v++)
{
if(dp[v]>max)
{
max=dp[v];
}
}
cout<<max<<endl;
}
輸出結果:
5 8
3 5 1 2 2
4 5 2 1 3
10
如果採用深度優先搜索方法,可以看到如下:
#include<iostream>
using namespace std;
const int maxn = 30;
int n,V,maxValue = 0;
int w[maxn],c[maxn];
void DFS(int index,int sumW,int sumC)
{
if(index==n)
{
if(sumW<=V&&sumC>maxValue)
maxValue=sumC;
return ;
}
DFS(index+1,sumW,sumC);
DFS(index+1,sumW+w[index],sumC+c[index]);
}
int main()
{
cin>>n>>V;
for(int i=0;i<n;i++)
{
cin>>w[i];
}
for(int i=0;i<n;i++)
{
cin>>c[i];
}
DFS(0,0,0);
cout<<maxValue<<endl;
}
示例:
5 8
3 5 1 2 2
4 5 2 1 3
10
總結:
總的來說,這篇文章是從《算法圖解》這本書的整體框架進行一定的拓展,因爲我之前對於算法有一定的瞭解,所以只花了一天時間看完這本書,但還是覺得書裏面的講解很有趣而且對於自己知識的鞏固有着很好的作用,我也花了一兩天總結這本書的相關算法,也寫在這篇文章裏面,包括一些校招、PAT的題目,因爲我主要以c++爲編程語言,所以出現了很多以c++爲語言的代碼,但是其實與python是一樣的,我們主要理解裏面的算法思想,同時我也沒有提及書本里面的一些拓展知識,也留給讀者們去閱讀的機會。
書本的文件可以到我的GitHub上去下載,同時相應的程序我也打出來,想試一試的人可以到我GitHub上去看看。
GitHub地址:https://github.com/Brian-Liew/Algorithm/tree/master/書籍筆記:《算法圖解》