高頻手撕算法合集來了!

作者 |?L的存在
來源 |?我是程序員小賤

高頻手撕算法合集
數(shù)據(jù)結(jié)構(gòu)
鏈表屬于數(shù)據(jù)結(jié)構(gòu)中的線性結(jié)構(gòu)的一種,我們先看看什么是數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)是:結(jié)構(gòu)的定義+結(jié)構(gòu)的操作
想必大伙兒應(yīng)該玩兒過拼圖,拼圖之前我們先看看說明書,看看包含幾個部分,然后對這些部分進行拼裝,隨后拼好候進行組合直到完成。
一個鏈表是由1個或者多個節(jié)點組成,每個節(jié)點包含兩個信息,一個是數(shù)據(jù)信息,用來存儲數(shù)據(jù),一個是地址信息,用來存儲下個節(jié)點的地址。

鏈表節(jié)點
那在代碼中是什么樣子呢?
struct?Node?{
????int?data;
????struct?Node?*next;
};
鏈表操作
說到鏈表結(jié)構(gòu),我們習(xí)慣性的和數(shù)組聯(lián)系在一起。只是數(shù)組結(jié)構(gòu)在內(nèi)存中是連續(xù)的,而鏈表結(jié)構(gòu)因為指針域的存在,每個節(jié)點在內(nèi)存中存儲的位置未必連續(xù)。下面我們按照數(shù)組的方式給鏈表也編個號。

單鏈表
struct?Node?*insert(struct?Node?*head,?int?ind,?struct?Node?*a);
第一個參數(shù)為待操作的鏈表的頭結(jié)點地址,也就是第一個節(jié)點的地址。 第二個參數(shù)為插入位置。 第三個參數(shù)為指針變量,指向要插入的新節(jié)點。
struct?Node?*insert(struct?Node?*head,?int?ind,?struct?Node?*a)?{
????struct?Node?ret,?*p?=?&ret;
????ret.next?=?head;
????//?從虛擬頭節(jié)點開始向后走?ind?步
????while?(ind--)?p?=?p->next;
????//?完成節(jié)點的插入操作
????a->next?=?p->next;
????p->next?=?a;
????//?返回真正的鏈表頭節(jié)點地址
????return?ret.next;
}

案例
我們看個題吧,定義一個快樂數(shù),什么是快樂數(shù),所謂快樂數(shù)即通過有限次變換后等于1 的數(shù)字。怎么變換呢,給出一個非1的數(shù)字,然后出去位數(shù),求各個位數(shù)的平方和,得到數(shù)字A,假設(shè)A不死1,那就繼續(xù)對元素A的每一位進行平方和,得到數(shù)字B。。。。知道最后能夠=1。
在上面我們介紹了最后一個節(jié)點指向空,可是你有沒有想過如果鏈表的最后一個節(jié)點不是空地址而是指向鏈表中的一個節(jié)點,這不就是環(huán)了?

鏈表環(huán)
使用數(shù)組標(biāo)記的方法。記錄出現(xiàn)過的節(jié)點信息,每次遍歷新節(jié)點就去數(shù)組查看記錄,這樣的時間復(fù)雜度不給力。經(jīng)過第一個節(jié)點,需要在數(shù)組查找0次,第2個節(jié)點,數(shù)組查找1次,第i個節(jié)點,在數(shù)組查找i-1次,直到遍歷第n+1個節(jié)點,查找的總次數(shù)為(n + 1) * n / 2,這樣時間復(fù)雜度為O(n^2)。太慢了,給我優(yōu)化。 快慢指針法。
AB兩位同學(xué)跑步,A同學(xué)速度快,B同學(xué)速度慢,他們并不知道跑道是環(huán)形的,如果是環(huán)形,跑得快的,在足夠的時間終究會從速度慢的B同學(xué)經(jīng)過,形成相遇的情況。如果不是環(huán)形,速度快的先到重點,不會相遇---快慢指針法。

快慢指針
int?hasCycle(struct?Node?*head)?{
????if?(head?==?NULL)?return?0;
????//?p?是慢指針,q?是快指針
????struct?Node?*p?=?head,?*q?=?head;
????//?每次循環(huán),p?走1步,q?走2步
????do?{
????????????p?=?p->next;
????????????q?=?q->next;
????????????if?(q?==?NULL)?return?0;
????????????q?=?q->next;
????????}?while?(p?!=?q?&&?q);
????return?p?==?q;
}
二分查找初探
說到二分查找,這里就有個笑話了。
二分查找就該這樣學(xué)
二分查找基礎(chǔ)
最簡單的二分算法即在一個有序數(shù)組中,查找一個數(shù)字X是否存在。注意有序性。那么如何在數(shù)組中查找一個數(shù)。
從頭到尾一個一個查找,找到即有數(shù)字x。 二分算法即通過確定一個區(qū)間,然后查找區(qū)間的一半和x比較,如果比x大則在x前半段查找。如果比x小則在后半段查找,只需要log2n的比較即可確定結(jié)果。

二分初探
int?BinarySerach(vector<int>&?nums,int?n,?int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?(left+right)/2;
????????if?(nums[mid]?==?target)?return?mid;
????????else?if?(nums[mid]?left?=?mid?+?1;
????????else?right?=?mid-1;
????}
????return?-1;
}
如果right和left比較的時候,兩者之和可能溢出。那么改進的方法是mid=left+(right-left)/2.還可以繼續(xù)優(yōu)化,我們將除以2這種操作轉(zhuǎn)換為位運算mid=left+((right-left)>>1).
哪有這么簡單的事兒,大多數(shù)的筆試面試中可能會出現(xiàn)下面的幾種情況。
二分的各種變種

思路
首先7與中間值a[4]比較,發(fā)現(xiàn)小于7,于是在5到9中繼續(xù)查找,中間a[7]=7,但是這個數(shù)7不是第一次出現(xiàn)的。那么我們檢查這個值的前面是不是等于7,如果等于7,說明目前這個值不是第一次出現(xiàn)的7,此時更新rihgt=mid-1。ok我們看看代碼。
int?BinarySerach(vector<int>&?nums,?int?n,int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?left+((right-left)>>1);
????????if?(nums[mid]>value)
????????{
????????????right=mid-1;
????????}?else?if(nums[mid]????????{
????????????left=mid+1;
????????}else
????????{
????????????if((mid==0)||(nums[mid-1]!=value))
????????????{
????????????????return?mid;
????????????}else
????????????{
????????????????left=mid-1;
????????????}
????????}
????return?-1;
}
假設(shè)nums[mid]這個值已經(jīng)是最后一個元素了,那么它肯定是要找到最后一個值。如果nums[mid]的下一個不等于value,那說明nums[mid]就是我們需要找到最后一個等于給定值的值。
int?BinarySerach(vector<int>&?nums,?int?n,int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?left+((right-left)>>1);
????????if?(nums[mid]>value)
????????{
????????????right=mid-1;
????????}?else?if(nums[mid]????????{
????????????left=mid+1;
????????}else
????????{
????????????if((mid==n-1)||(nums[mid+1]!=value))
????????????{
????????????????return?mid;
????????????}else
????????????{
????????????????left=mid+1;
????????????}
????????}
????return?-1;
}
如果nums[mid]小于要查找的值,那么我們需要查找在[mid+1,right]之間,所以此時更新為left=mid+1。 如果nums[mid]大于給定值value,這個時候需要查看nums[mid]是不是我們需要找的第一個值大于等于給定值元素,如果nums[mid]前面沒有元素或者前面一個元素小于查找的值,那么nums[mid]就是我們需要查找的值。 如果nums[mid-1]也是大于等于查找的值,那么說明查找的元素在[left,mid-1]之間,所以我們需要將right更新為mid-1。
int?BinarySerach(vector<int>&?nums,?int?n,int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?left+((right-left)>>1);
????????if?(nums[mid]>value)
????????{
????????????right=mid-1;
????????}?else?if(nums[mid]????????{
????????????left=mid+1;
????????}else
????????{
????????????if((mid==n-1)||(nums[mid+1]!=value))
????????????{
????????????????return?mid;
????????????}else
????????????{
????????????????left=mid+1;
????????????}
????????}
????return?-1;
}
如果nums[mid]小于要查找的值,那么我們需要查找在[mid+1,right]之間,所以此時更新為left=mid+1。 如果nums[mid]大于給定值value,這個時候需要查看nums[mid]是不是我們需要找的第一個值大于等于給定值元素,如果nums[mid]前面沒有元素或者前面一個元素小于查找的值,那么nums[mid]就是我們需要查找的值。 如果nums[mid-1]也是大于等于查找的值,那么說明查找的元素在[left,mid-1]之間,所以我們需要將right更新為mid-1。
int?BinarySerach(vector<int>&?nums,?int?n,int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?left+((right-left)>>1);
????????if?(nums[mid]>=value)
????????{
????????????if(mid==0||nums[mid-1]????????????{
????????????????return?mid;
????????????}else
????????????{
????????????????right=mid-1;
????????????}
????????}else
????????{
????????????left=mid+1;
????????}
????return?-1;
}
如果nums[mid]小于查找的值,那么需要查找的值肯定在[mid+1,right]之間,所以我們需要更新left=mid+1。 如果nums[mid]大于等于給定的value,檢查nums[mid]是不是我們的第一個值大于等于給定值的元素。
int?BinarySerach(vector<int>&?nums,?int?n,int?target)?{
????int?left?=?0,?right?=?n-1;
????while?(left?<=?right)?{
????????int?mid?=?left+((right-left)>>1);
????????if?(nums[mid]>value)
????????{
????????????right=mid-1;
????????}else
????????{
????????????if(mid==n-1||(nums[mid+1]>value))
????????????{
????????????????return?mid;
????????????}else
????????????{
????????????????left=mid+1;
????????????}
????????}
????return?-1;
}
隊列
例子:滑動窗口最大值
火車站買票應(yīng)該都經(jīng)歷過,窗口小姐姐每次服務(wù)排在最前面的那個人,買完票則從頭部離開,后面人往前一步接替離開的人繼續(xù)購票,這就是典型的隊列結(jié)構(gòu)。

隊列定義
假設(shè)將學(xué)生從高年級到低年級排列,隨著時間的推移,高年級同學(xué)從隊列頭部畢業(yè),低年級從尾部進入。大部分學(xué)校都有校隊,假設(shè)小林高三,我高二,小七高一,小林畢業(yè)接班的是我,我畢業(yè),很可能就是小七接班,而當(dāng)我進隊的那一刻,小七即使進的早但是戰(zhàn)斗力沒我高,所以小七是永遠沒計劃被選中啦。所以,縱觀全隊,不僅有著隊列的性質(zhì),也有著單調(diào)的性質(zhì),所以就是單調(diào)隊列。
比較明顯的作用是,用來維護隊列處理順序中的區(qū)間最大值。
滑動窗口沒向后滑動一位,就有一個元素從隊首出隊,同時也會有個元素從隊尾入隊。這個題需要求區(qū)間的最大值:意味著需要維護在隊列處理順序中的區(qū)間最大值,直接上代碼附上注釋。
#define?MAX_N?1000
int?q[MAX_N?+?5],?head,?tail;
void?interval_max_number(int?*a,?int?n,?int?m)?{
????head?=?tail?=?0;
????for?(int?i?=?0;?i?????????//?a[i]?入隊,將違反單調(diào)性的從隊列?q?中踢出
????????while?(head?q[tail?-?1]]?????????q[tail++]?=?i;?//?i?入隊
????????//?判斷隊列頭部元素是否出了窗口范圍
????????if?(i?-?m?==?q[head])?head++;
????????//?輸出區(qū)間內(nèi)最大值
????????if?(i?+?1?>=?m)?{
????????????printf("interval(%d,?%d)",?i?-?m?+?1,?i);
????????????printf("?=?%d\n",?a[q[head]]);
????????}
????}???
????return?;
}
棧與單調(diào)棧
棧結(jié)構(gòu)對應(yīng)于隊列,可以將棧想象為一個只有單出口的羽毛球筒,羽毛球只能從單一的入口放入和取出。假設(shè)我們將1,2,3三個球放進球桶,如果取出來此時就是3,2,1。性質(zhì)就很明顯了,先進后出的結(jié)構(gòu)。

({})
{()}
([)]
(((){}
public?boolean?isValid(String?s)?{
????????Stack?stack?=?new?Stack<>();
????????Map?map?=?new?HashMap<>();
????????char[]?chars?=?s.toCharArray();
????????map.put(')','(');
????????map.put('}','{');
????????map.put(']','[');
????????for(int?i=0;i?????????????if(!map.containsKey(chars[i]))?{
????????????????//為左括號時直接入棧
????????????????stack.push(chars[i]);
????????????}else{
????????????????//為右括號時,如果棧為空或者棧頂與該括號類型不匹配返回false
????????????????if(stack.empty()?||?map.get(chars[i])?!=?stack.pop()){
????????????????????return?false;
????????????????}
????????????}
????????}
????????//字符串遍歷完畢后,如果棧為空返回true,反之返回false
????????return?stack.empty();
????}
遞推套路
在分享遞推之前,先和大家分享與之緊密的數(shù)學(xué)原理:容斥原理。
容斥原理是什么?
假設(shè)有一片草原上,莫名其妙來了一只外星兔子,這種外星兔子呢,第一個月的時候是幼體,第二個月成長為成體,從第三個月開始,成體兔子每個月都會產(chǎn)生出一只克隆體的幼體兔子,而且這種兔子不會衰老,一旦成體以后,就會一直生下去。按照這種情況,請你計算出第 n 個月,草原上有多少只兔子?

六個月兔子情況

確定遞推的狀態(tài),多畫圖前面幾步。 推導(dǎo)遞推公式。 程序的編寫。
我們根據(jù)三步走的方式來闡釋解決兔子的這個問題。
f(n)表示n個月兔子的數(shù)量。 遞推公式(第一個月合第二個月兔子的數(shù)量為1,到了第三個月即等于前面兩個月之和)。 
遞推公式
用 1 元、2 元、5 元、10 元、20 元、50 元和 100元湊成 1000 元錢,總共有多少種方案?
確定遞推狀態(tài),需要分析自變量與因變量,自變量兩個分別為幣種種類和拼湊的錢幣數(shù)量,因變量1個為方案總數(shù),因此我們的狀態(tài)定義為f(i,j),i種錢幣,拼湊j元錢的方案總數(shù)。比如f [3][10]即使用三種錢幣,湊出10元的方案總數(shù)。 假設(shè)我們不使用第三種錢幣,那么此時等價于使用前兩種錢幣拼湊10元錢的方案總數(shù),即f[2][10]。如果使用至少1張5塊錢,那么我們在這些方案中去掉一張5元錢,剩下的方案數(shù)為f[3][5],所以此時的遞推公式為f[3][10] = f[2][10] + f[3][5]。這只是一般情況,假設(shè)我們沒有使用第i種錢幣,拼湊j元的方案為f(i-1,j),代表使用前i-1種錢幣的方案總數(shù)。剩下的使用了第i中錢幣,由于都存在第i錢幣1張,假設(shè)第i種錢幣的面額為val[i],那么此時我們的前i種錢幣,湊j-val[i]的錢數(shù),此時方案總數(shù)為f(i,j-val[i]);所以公式為f(i,j)=f(i-1,j)+f(i,j-val[i])。

動態(tài)規(guī)劃
動態(tài)規(guī)劃通常簡稱DP(dynamic programming),如果按照問題類型來劃分,將分為線性DP、區(qū)間DP,數(shù)位DP等等,每當(dāng)說到動態(tài)規(guī)劃就會想最優(yōu)子結(jié)構(gòu),重疊子問題等等,這些詞匯苦澀難懂,不要慌,再難的問題也是建立在基礎(chǔ)問題上,逐步拆分,這也是動態(tài)規(guī)劃的思想,相信通過下面動態(tài)規(guī)劃四步走的方式,加上習(xí)題的練習(xí),一定會讓你對動態(tài)規(guī)劃有個新的理解。
狀態(tài)定義
其實上面說遞推的時候就已經(jīng)有所涉及狀態(tài)定義,通常在推導(dǎo)的過程中,如果感覺推不下去了,很有可能就是我們的狀態(tài)定義出現(xiàn)了問題。
狀態(tài)轉(zhuǎn)移方程
上面的兩種狀態(tài)定義對應(yīng)這里兩個轉(zhuǎn)移方向。

狀態(tài)轉(zhuǎn)移過程
正確性證明
數(shù)學(xué)歸納法通常采用三步走的方式,常用的正確性證明方法為數(shù)學(xué)歸納法。
狀態(tài)定義
狀態(tài)方程
正確性證明
實現(xiàn)
#define?MAX_V?10000
#define?MAX_N?100
int?v[MAX_N?+?5],?w[MAX_N?+?5];
int?dp[MAX_N?+?5][MAX_V?+?5];
int?get_dp(int?n,?int?W)?{
????//?初始化?dp[0]?階段
????for?(int?i?=?0;?i?<=?W;?i++)?dp[0][i]?=?0;
????//?假設(shè)?dp[i?-?1]?成立,計算得到?dp[i]
????//?狀態(tài)轉(zhuǎn)移過程,i?代表物品,j?代表背包限重
????for?(int?i?=?1;?i?<=?n;?i++)?{
???????????for?(int?j?=?0;?j?<=?W;?j++)?{
????????//?不選擇第?i?種物品時的最大值
????????dp[i][j]?=?dp[i?-?1][j];
????????//?與選擇第?i?種物品的最大值作比較,并更新
????????if?(j?>=?w[i]?&&?dp[i][j]?dp[i?-?1][j?-?w[i]]?+?v[i])?{
????????????dp[i][j]?=?dp[i?-?1][j?-?w[i]]?+?v[i];
????????????}
????????}
????}
????return?dp[n][W];
}
12
貪心
其實我們大學(xué)學(xué)習(xí)的好幾種應(yīng)用都是采用了貪心的算法啊,比如Huffman Coding,Prim最小生成樹等。
有n個盒子排成一行,每個盒子上面有一個數(shù)字a[i],表示最多能向右跳a[i]個盒子;小林站在左邊第一個盒子,請問能否到達最右邊的盒子?比如說:[1, 2, 3, 0, 4] 可以到達第5個盒子;[3, 2, 1, 0, 4] 無法到達第5個盒子;
我們有 m 個糖果和 n 個孩子。我們現(xiàn)在要把糖果分給這些孩子吃,但是糖果少,孩子多(m 我的問題是,如何分配糖果,能盡可能滿足最多數(shù)量的孩子?
對于一個孩子來說,如果小的糖果可以滿足,那么就沒必要用更大的糖果。 對糖果的大小需求的孩子更容易被滿足,所以我們可以從需求小得孩子開始分配糖果。 因為滿足一個需求大的孩子跟滿足一個需求小的孩子,對我們期望值的貢獻是一樣的。 我們每次從剩下的孩子中,找出對糖果大小需求最小的,然后發(fā)給他剩下的糖果中能滿足他的最小的糖果,這樣得到的分配方案,也就是滿足的孩子個數(shù)最多的方案。
#include?
#include?
#include?
using?namespace?std;
/*
解題思路:
遍歷兩邊,首先每個人得一塊糖,第一遍從左到右,若當(dāng)前點比前一個點高就比前者多一塊。
這樣保證了在一個方向上滿足了要求。第二遍從右往左,若左右兩點,左側(cè)高于右側(cè),但
左側(cè)的糖果數(shù)不多于右側(cè),則左側(cè)糖果數(shù)等于右側(cè)糖果數(shù)+1,這就保證了另一個方向上滿足要求。
最后將各個位置的糖果數(shù)累加起來就可以了。
*/
int?candyCount(vector<int>&rating)?{
????int?res?=?0;
????//孩子總數(shù)
????int?n?=?rating.size();
????//糖果集合
????vector<int>?candy(n,?1);
????//從左往右遍歷
????for?(int?i?=?0;i?1;i++)?{
????????if?(rating[i?+?1]?>?rating[i])candy[i?+?1]?=?candy[i]?+?1;
????}
????//從右往左
????for?(int?i?=?n?-?1;i?>?0;i--)?{
????????if?(rating[i?-?1]?>?rating[i]?&&?candy[i?-?1]?<=?candy[i])
????????????candy[i?-?1]?=?candy[i]?+?1;
????}
????//累加結(jié)果
????for?(auto?a?:?candy)?{
????????res?+=?a;
????}
????return?res;
}
//測試函數(shù)
int?main()?{
????vector<int>?rating{1,3,2,1,4,5,2};
????cout?<endl;
????return?0;
}
13
嘮嗑
另外,推薦一個 GitHub,這個 GitHub 整理了幾百本常用技術(shù)PDF,絕大部分核心的技術(shù)書籍都可以在這里找到,GitHub地址:? ? ??https://github.com/iamshuaidi/CS-Book(電腦打開體驗更好),地址閱讀原文直達
推薦閱讀
1、【大學(xué)逆襲之路-開篇】本科掙 30 萬,秋招提前批offer,帥地做對了什么?(附所有知識清單)
