主頁 >  其他 > ??3萬字《演算法 + 資料結構》刷了 3333 道演算法題后的一點總結??(建議收藏)

??3萬字《演算法 + 資料結構》刷了 3333 道演算法題后的一點總結??(建議收藏)

2021-07-23 08:00:45 其他

文章目錄

  • 1??前言:追憶我的刷題經歷
  • 2??演算法和資料結構的重要性
    • 👪1、適用人群
    • 🎾2、有何作用
    • 📜3、演算法簡介
    • 🌲4、資料結構
  • 3??如何開始持續的刷題
    • 📑1、立軍令狀
    • 👩????👩2、培養興趣
    • 🚿3、狂切水題
    • 💪🏻4、養成習慣
    • 🈵5、一周出師
  • 4??簡單資料結構的掌握
    • 🚂1、陣列
    • 🎫2、字串
    • 🎇3、鏈表
    • 🌝4、哈希表
    • 👨?👩?👧5、佇列
    • 👩?👩?👦?👦6、堆疊
    • 🌵7、二叉樹
    • 🌳8、多叉樹
    • 🌲9、森林
    • 🍀10、樹狀陣列
    • 🌍11、圖
  • 5??簡單演算法的入門
    • 💨1、線性列舉
    • 🥖2、線性迭代
    • 💤3、簡單排序
    • 🥂4、二分列舉
    • 🥢5、雙指標
    • 🍴6、差分法
    • 🅱7、位運算
    • 🧡8、貪心
    • ?9、遞回分治
    • 🚊10、簡單動態規劃
  • 6??刷題順序的建議
    • 👨?👦1、入門演算法
    • 👩?👧?👦2、初級演算法
    • 👩?👩?👧?👦3、中級演算法
  • 7??系統學習演算法和資料結構
    • 🚍1、進階動態規劃
    • 🪐2、強勁圖論搜索
    • 0??3、進階初等數論
    • 🛑4、進階計算幾何
    • 📏5、字串的匹配
    • 🎄6、高級資料結構

1??前言:追憶我的刷題經歷

??大學的時候比較瘋狂,除了上課的時候,基本都是在機房刷題,當然,有時候連上課都在想題目,紙上寫好代碼,一下課就沖進機房把代碼敲了,目的很單純,為了沖排行榜,就像玩游戲一樣,享受霸榜的快感,
??當年主要是在 杭電OJ (HDOJ)北大OJ(POJ) 這兩個在線平臺上刷題,那時候應該還沒有(LeetCode、洛谷、牛客 這些主流的刷題網站),后來參加作業以后,剩余的時間不多了,也就沒怎么刷了, 但是 演算法思維 也就是靠上大學那四年鍛煉出來的,


??當年題目少,刷題的人也少,所以勉強還沖到過第一,現在去看已經 58 名了,可見 長江后浪推前浪,前浪 S 在沙灘上,時勢造英雄啊!

??北大人才輩出,相對題目也比較難,所以明顯有點 心有余而力不足 的感覺,刷的相對就少很多,而且這個 OJ 也沒什么人維護了,看我的簽名,當時竟然還想著給博客引點流,現在估計都沒什么人去那個網站了吧,如果有,記得評論區告訴我,一起緬懷一下逝去的青春,

🙉飯不食,水不飲,題必須刷🙉

C語言免費動漫教程,和我一起打卡!
🌞《光天化日學C語言》🌞

LeetCode 太難?先看簡單題!
🧡《C語言入門100例》🧡

LeetCode 太簡單?演算法學起來!
🌌《夜深人靜寫演算法》🌌

在這里插入圖片描述

2??演算法和資料結構的重要性

👪1、適用人群

  • 這篇文章會從 「演算法和資料結構」 零基礎開始講,所以,如果你是演算法大神,可以盡情在評論區嘲諷我哈哈,目的當然是幫助想要涉足演算法領域,或者正在找作業的朋友,以及將要找作業的大學生,更加有效快速的掌握演算法思維,能夠在職場面試和筆試中一展身手,
  • 這篇文章中,我會著重講解一些常見的 「演算法和資料結構」 的設計思想,并且配上動圖,主要針對面試中常見的問題和新手朋友們比較難理解的點進行決議,當然,后面也會給出面向演算法競賽的提綱,如果有興趣深入學習的歡迎在評論區留言,一起成長交流,
  • 零基礎學演算法的最好方法,莫過于刷題了,任何事情都是需要堅持的,刷題也一樣,沒有刷夠足夠的題,就很難做出系統性的總結,所以上大學的時候,我花了三年的時間來刷題, 作業以后還是會抽點時間出來刷題,

千萬不要用作業忙來找借口,時間擠一擠總是有的,

  • 我現在上班地鐵上一個小時,下班地鐵又是一個小時,比如這篇文章的起草,就是在 地鐵 上完成的,如何利用這兩個小時的時間,做一些有建設性的事情,才是最重要的,刷抖音一個小時過得很快,刷題也是同樣的道理,
  • 當然,每天不需要花太多時間在這個上面,把這個事情做成一個規劃,按照長期去推進,反正也沒有 KPI 壓力,就當成是作業之余的一種消遣,還能夠提升思維能力,

所以,無論你是 小學生中學生高中OIer大學ACMer職場人士,只要想開始,一切都不會太晚!

🎾2、有何作用

  • 我們平常使用的 智能手機、搜索引擎、網站、操作系統、游戲、軟體、人工智能,都大量地應用了 「演算法與資料結構」 的知識,以及平時你用到的各種庫的底層實作,也是通過各種演算法和資料結構組合出來的,所以可以說,有程式的地方,就有江湖 演算法,有演算法就一定會有對應的資料結構,
  • 如果你只是想學會寫代碼,或許 「演算法與資料結構」 并不是那么重要,但是想要往更深一步發展,「演算法與資料結構」 是必不可少的,

??現在一些主流的大廠,在面試快結束的時候都會 奉上一道演算法題,如果你敲不出來,可能你的 offer 年包就打了 七折,或者直接與 offer 失之交臂,都是有可能的(因為我自己也是萬惡的面試官,看到候選人的演算法題寫不出來我也是操碎了心,但是我一般會給足容錯,比如給三個演算法題,挑一個寫,任意寫出一個都行),

  • 當然,它不能完全代表你的編碼能力,因為有些演算法確實是很巧妙,加上緊張的面試氛圍,想不出來其實也是正常的,但是你能確保面試官是這么想的嗎?我們要做的是十足的準備,既然決定出來,offer 當然是越高越好,畢竟大家都要養家糊口,房價又這么貴,如果能夠在演算法這一塊取得先機,也不失為一個捷徑,

所以,你問我演算法和資料結構有什么用?我可以很明確的說,和你的年薪息息相關,

  • 當然,面試中 「演算法與資料結構」 知識的考察只是面試內容的一部分,其它還有很多面試要考察的內容,當然不是本文主要核心內容,這里就不做展開了,

📜3、演算法簡介

  • 演算法是什么東西?
  • 它是一種方法,一種解決問題的方案,
  • 舉個例子,你現在要去上班,可以選擇 走路、跑步、坐公交、坐地鐵、自己開車 等等,這些都是解決方案,但是它們都會有一些衡量指標,讓你有一個權衡,最后選擇你認為最優的策略去做,
  • 而衡量的指標諸如:時間消耗、金錢消耗、是否需要轉車、是否可達 等等,

時間消耗就對應了:時間復雜度
金錢消耗就對應了:空間復雜度
是否可達就對應了:演算法可行性

  • 當然,是否需要轉車,從某種程度上都會影響 時間復雜度 或者 空間復雜度

🌲4、資料結構

  • 對于實作某個演算法,我們往往會用到一些資料結構,
  • 因為我們通常不能一下子把資料處理完,更多的時候需要先把它們放在一個容器或者說快取里面,等到一定的時刻再把它們拿出來,
  • 這其實是一種 「空間換時間」 思想的體現, 恰當使用資料結構可以幫助我們高效地處理資料,
  • 常用的一些資料結構如下:
資料結構應用場景
陣列線性存盤、元素為任意相同型別、隨機訪問
字串線性存盤、元素為字符、結尾字符、隨機訪問
鏈表鏈式存盤、快速洗掉
堆疊先進后出
佇列先進先出
哈希表隨機存盤、快速增刪改查
二叉樹對數時間增刪改查,二叉查找樹、線段樹
多叉樹B/B+樹 硬碟樹、字典樹 字串前綴匹配
森林并查集 快速合并資料
樹狀陣列單點更新,成段求和
  • 為什么需要引入這么多資料結構呢?

??答案是:任何一種資料結構是不是 完美的,所以我們需要根據對應的場景,來采用對應的資料結構,具體用哪種資料結構,需要通過刷題不斷重繪經驗,才能總結出來,

3??如何開始持續的刷題

  • 有朋友告訴我,題目太難了,根本不會做,每次都是看別人的解題報告,

📑1、立軍令狀

  • 所謂 「軍令狀」,其實就是給自己定一個目標,給自己樹立一個目標是非常重要的,有 「目標才會有方向,有目標才會有動力,有目標才會有人生的意義」 ,而軍令狀是貶義的,如果不達成就會有各種懲罰,所以其實你是心不甘情不愿的,于是這件事情其實是無法持久下去的,

事實證明,立軍令狀是不可取的,

  • 啊這……所以我們還是要采用一些能夠持久下去的方法,

👩????👩2、培養興趣

  • 為了讓這件事情能夠持久下去,一定要培養出興趣,適時的給自己一些正反饋,正反饋的作用就是每過一個周期,如果效果好,就要有獎勵,這個獎勵機制可以自己設定,但是 「不能作弊」 ,一旦作弊就像單機游戲修改數值,流失是遲早的事,
  • 舉個例子,我們可以給每天制定一些 「不一樣的目標和獎勵」 ,比如下圖所示:
刷題的第?天目標題數是否完成完成獎勵
11攻擊力 + 10
21防御力 + 10
32出去吃頓好的
42攻擊力 + 29
53防御力 + 60
61攻擊力 + 20
74出去吃頓好的
81防御力 + 50
  • 當然,這個完成獎勵你可以自己定,總而言之,要是對你有傭訓的獎勵才是有意義的,

🚿3、狂切水題

  • 剛開始刷的 300 題一定都是 「水題」 ,刷 「水題」 的目的是讓你養成一個每天刷題的習慣,久而久之,不刷題的日子會變得無比煎熬,當然,刷著刷著,你會發現,水題會越來越多,因為刷題的程序中,你已經無形中不斷成長起來了,
  • 至少這個方法我用過,非常靈驗!推薦刷題從水題開始,

如果不知道哪里有水題,推薦:
?? C語言入門水題:《C語言入門100例》
??C語言演算法水題:《LeetCode演算法全集》

💪🏻4、養成習慣

  • 相信如果切了 300 個 「水題」 以后,刷題自然而然就成了習慣,想放棄都難,這個專業上講,其實叫 沉沒成本,有興趣的可以自行百度,這里就不再累述了,

🈵5、一周出師

  • 基本上如果能夠按照這樣的計劃去執行,一周以后,一定會有識訓,沒有識訓的話,可以來找我,

4??簡單資料結構的掌握

🚂1、數組

記憶體結構:記憶體空間連續
實作難度:簡單
下標訪問:支持
分類:靜態陣列、動態陣列
插入時間復雜度 O ( n ) O(n) O(n)
查找時間復雜度 O ( n ) O(n) O(n)
洗掉時間復雜度 O ( n ) O(n) O(n)

🎫2、字串

記憶體結構:記憶體空間連續,類似字符陣列
實作難度:簡單,一般系統會提供一些方便的字串操作函式
下標訪問:支持
插入時間復雜度 O ( n ) O(n) O(n)
查找時間復雜度 O ( n ) O(n) O(n)
洗掉時間復雜度 O ( n ) O(n) O(n)

🎇3、鏈表

記憶體結構:記憶體空間連續不連續,看具體實作
實作難度:一般
下標訪問:不支持
分類:單向鏈表、雙向鏈表、回圈鏈表、DancingLinks
插入時間復雜度 O ( 1 ) O(1) O(1)
查找時間復雜度 O ( n ) O(n) O(n)
洗掉時間復雜度 O ( 1 ) O(1) O(1)

🌝4、哈希表

記憶體結構:哈希表本身連續,但是衍生出來的結點邏輯上不連續
實作難度:一般
下標訪問:不支持
分類:正數哈希、字串哈希、滾動哈希
插入時間復雜度 O ( 1 ) O(1) O(1)
查找時間復雜度 O ( 1 ) O(1) O(1)
洗掉時間復雜度 O ( 1 ) O(1) O(1)

  • 哈希表相關的內容,可以參考我的這篇文章:
  • 夜深人靜寫演算法(九)- 哈希表

👨?👩?👧5、佇列

記憶體結構:看用陣列實作,還是鏈表實作
實作難度:一般
下標訪問:不支持
分類:FIFO、單調佇列、雙端佇列
插入時間復雜度 O ( 1 ) O(1) O(1)
查找時間復雜度:理論上不支持
洗掉時間復雜度 O ( 1 ) O(1) O(1)

  • 佇列相關的內容,可以參考我的這篇文章:
  • 夜深人靜寫演算法(十)- 單向廣搜

👩?👩?👦?👦6、堆疊

記憶體結構:看用陣列實作,還是鏈表實作
實作難度:一般
下標訪問:不支持
分類:FILO、單調堆疊
插入時間復雜度 O ( 1 ) O(1) O(1)
查找時間復雜度:理論上不支持
洗掉時間復雜度 O ( 1 ) O(1) O(1)

  • 堆疊相關的內容,可以參考我的這篇文章:
  • 夜深人靜寫演算法(十一)- 單調堆疊

🌵7、二叉樹

優先佇列 是 堆實作的,所以也屬于 二叉樹 范疇,它和佇列不同,不屬于線性表,
記憶體結構:記憶體結構一般不連續,但是有時候實作的時候,為了方便,一般是物理連續,邏輯不連續
實作難度:較難
下標訪問:不支持
分類:二叉樹 和 多叉樹
插入時間復雜度:看情況而定
查找時間復雜度:理論上 O ( l o g 2 n ) O(log_2n) O(log2?n)
洗掉時間復雜度:看情況而定

🌳8、多叉樹

記憶體結構:記憶體結構一般不連續,但是有時候實作的時候,為了方便,一般是物理連續,邏輯不連續
實作難度:較難
下標訪問:不支持
分類:二叉樹 和 多叉樹
插入時間復雜度:看情況而定
查找時間復雜度:理論上 O ( l o g 2 n ) O(log_2n) O(log2?n)
洗掉時間復雜度:看情況而定

  • 一種經典的多叉樹是字典樹,可以參考我的這篇文章:
  • 夜深人靜寫演算法(七)- 字典樹

🌲9、森林

  • 比較經典的森林是:并查集,可以參考我的這篇文章:
  • 夜深人靜寫演算法(五)- 并查集

🍀10、樹狀陣列

  • 樹狀陣列是用來做 單點更新,成端求和 的問題的,有關于它的內容,可以參考:
  • 夜深人靜寫演算法(十三)- 樹狀陣列

🌍11、圖

記憶體結構:不一定
實作難度:難
下標訪問:不支持
分類:有向圖、無向圖
插入時間復雜度:根據演算法而定
查找時間復雜度:根據演算法而定
洗掉時間復雜度:根據演算法而定

1、圖的概念

  • 在講解最短路問題之前,首先需要介紹一下計算機中圖(圖論)的概念,如下:
  • G G G 是一個有序二元組 ( V , E ) (V,E) (V,E),其中 V V V 稱為頂點集合, E E E 稱為邊集合, E E E V V V 不相交,頂點集合的元素被稱為頂點,邊集合的元素被稱為邊,
  • 對于無權圖,邊由二元組 ( u , v ) (u,v) (u,v) 表示,其中 u , v ∈ V u, v \in V u,vV,對于帶權圖,邊由三元組 ( u , v , w ) (u,v, w) (u,v,w) 表示,其中 u , v ∈ V u, v \in V u,vV w w w 為權值,可以是任意型別,
  • 圖分為有向圖和無向圖,對于有向圖, ( u , v ) (u, v) (u,v) 表示的是 從頂點 u u u 到 頂點 v v v 的邊,即 u → v u \to v uv;對于無向圖, ( u , v ) (u, v) (u,v) 可以理解成兩條邊,一條是 從頂點 u u u 到 頂點 v v v 的邊,即 u → v u \to v uv,另一條是從頂點 v v v 到 頂點 u u u 的邊,即 v → u v \to u vu

2、圖的存盤

  • 對于圖的存盤,程式實作上也有多種方案,根據不同情況采用不同的方案,接下來以圖二-3-1所表示的圖為例,講解四種存盤圖的方案,

1)鄰接矩陣

  • 鄰接矩陣是直接利用一個二維陣列對邊的關系進行存盤,矩陣的第 i i i 行第 j j j 列的值 表示 i → j i \to j ij 這條邊的權值;特殊的,如果不存在這條邊,用一個特殊標記 ∞ \infty 來表示;如果 i = j i = j i=j,則權值為 0 0 0
  • 它的優點是:實作非常簡單,而且很容易理解;缺點也很明顯,如果這個圖是一個非常稀疏的圖,圖中邊很少,但是點很多,就會造成非常大的記憶體浪費,點數過大的時候根本就無法存盤,
  • [ 0 ∞ 3 ∞ 1 0 2 ∞ ∞ ∞ 0 3 9 8 ∞ 0 ] \left[ \begin{matrix} 0 & \infty & 3 & \infty \\ 1 & 0 & 2 & \infty \\ \infty & \infty & 0 & 3 \\ 9 & 8 & \infty & 0 \end{matrix} \right] ?????019?08?320?30??????

2)鄰接表

  • 鄰接表是圖中常用的存盤結構之一,采用鏈表來存盤,每個頂點都有一個鏈表,鏈表的資料表示和當前頂點直接相鄰的頂點的資料 ( v , w ) (v, w) (v,w),即 頂點 和 邊權,
  • 它的優點是:對于稀疏圖不會有資料浪費;缺點就是實作相對鄰接矩陣來說較麻煩,需要自己實作鏈表,動態分配記憶體,
  • 如圖所示, d a t a data data ( v , w ) (v, w) (v,w) 二元組,代表和對應頂點 u u u 直接相連的頂點資料, w w w 代表 u → v u \to v uv 的邊權, n e x t next next 是一個指標,指向下一個 ( v , w ) (v, w) (v,w) 二元組,
  • 在 C++ 中,還可以使用 vector 這個容器來代替鏈表的功能;
    vector<Edge> edges[maxn];

3)前向星

  • 前向星是以存盤邊的方式來存盤圖,先將邊讀入并存盤在連續的陣列中,然后按照邊的起點進行排序,這樣陣列中起點相等的邊就能夠在陣列中進行連續訪問了,
  • 它的優點是實作簡單,容易理解;缺點是需要在所有邊都讀入完畢的情況下對所有邊進行一次排序,帶來了時間開銷,實用性也較差,只適合離線演算法,
  • 如圖所示,表示的是三元組 ( u , v , w ) (u, v, w) (u,v,w) 的陣列, i d x idx idx 代表陣列下標,
    在這里插入圖片描述
  • 那么用哪種資料結構才能滿足所有圖的需求呢?
  • 接下來介紹一種新的資料結構 —— 鏈式前向星,

4)鏈式前向星

  • 鏈式前向星和鄰接表類似,也是鏈式結構和陣列結構的結合,每個結點 i i i 都有一個鏈表,鏈表的所有資料是從 i i i 出發的所有邊的集合(對比鄰接表存的是頂點集合),邊的表示為一個四元組 ( u , v , w , n e x t ) (u, v, w, next) (u,v,w,next),其中 ( u , v ) (u, v) (u,v) 代表該條邊的有向頂點對 u → v u \to v uv w w w 代表邊上的權值, n e x t next next 指向下一條邊,
  • 具體的,我們需要一個邊的結構體陣列 edge[maxm]maxm表示邊的總數,所有邊都存盤在這個結構體陣列中,并且用head[i]來指向 i i i 結點的第一條邊,
  • 邊的結構體宣告如下:
struct Edge {
    int u, v, w, next;
    Edge() {}
    Edge(int _u, int _v, int _w, int _next) :
        u(_u), v(_v), w(_w), next(_next) 
    {
    }
}edge[maxm];
  • 初始化所有的head[i] = -1,當前邊總數 edgeCount = 0
  • 每讀入一條 u → v u \to v uv 的邊,呼叫 addEdge(u, v, w),具體函式的實作如下:
void addEdge(int u, int v, int w) {
    edge[edgeCount] = Edge(u, v, w, head[u]);
    head[u] = edgeCount++;
}
  • 這個函式的含義是每加入一條邊 ( u , v , w ) (u, v, w) (u,v,w),就在原有的鏈表結構的首部插入這條邊,使得每次插入的時間復雜度為 O ( 1 ) O(1) O(1),所以鏈表的邊的順序和讀入順序正好是逆序的,這種結構在無論是稠密的還是稀疏的圖上都有非常好的表現,空間上沒有浪費,時間上也是最小開銷,
  • 呼叫的時候只要通過head[i]就能訪問到由 i i i 出發的第一條邊的編號,通過編號到edge陣列進行索引可以得到邊的具體資訊,然后根據這條邊的next域可以得到第二條邊的編號,以此類推,直到 next域為 -1 為止,
for (int e = head[u]; ~e; e = edges[e].next) {
    int v = edges[e].v;
    ValueType w = edges[e].w;
    ...
}
  • 文中的 ~e等價于 e != -1,是對e進行二進制取反的操作(-1 的的補碼二進制全是 1,取反后變成全 0,這樣就使得條件不滿足跳出回圈),

5??簡單演算法的入門

  • 入門十大演算法是 線性列舉、線性迭代、簡單排序、二分列舉、雙指標、差分法、位運算、貪心、分治遞回、簡單動態規劃,
  • 對于這十大演算法,我會逐步更新道這個專欄里面:《LeetCode演算法全集》,

💨1、線性列舉

  • 列舉一般可以理解成一個回圈,具體回圈里面做什么事情,要根據實際問題而定,舉個例子:

LeetCode 344. 反轉字串

??【例題1】撰寫一個函式,將輸入的字串反轉過來,輸入字串以字符陣列 char[] 的形式給出,必須原地修改輸入陣列、使用 O ( 1 ) O(1) O(1) 的額外空間解決這一問題,
??樣例輸入: [ ‘ ’ a ” , “ b ” , “ c ” ] [‘’a”, “b”, “c”] [a,b,c]
??樣例輸出: [ “ c ” , “ b ” , “ a ” ] [ “c”, “b”, “a”] [c,b,a]

a. 思路分析

??翻轉的含義,相當于就是 第一個字符 和 最后一個交換,第二個字符 和 最后第二個交換,… 以此類推,所以我們首先實作一個交換變數的函式 swap,然后再列舉 第一個字符、第二個字符、第三個字符 …… 即可,
??對于第 i i i 個字符,它的交換物件是 第 l e n ? i ? 1 len-i-1 len?i?1 個字符 (其中 l e n len len 為字串長度),swap函式的實作,可以參考:《C語言入門100例》 - 例2 | 交換變數,

b. 時間復雜度

  • 線性列舉為 O ( n ) O(n) O(n),交換變數為 O ( 1 ) O(1) O(1),兩個程序是相乘的關系,所以整個演算法的時間復雜度為 O ( n ) O(n) O(n)

c. 代碼詳解

class Solution {
public:
    void swap(char& a, char& b) {            // (1)
        char tmp = a;
        a = b;
        b = tmp;
    }
    void reverseString(vector<char>& s) {
        int len = s.size();
        for(int i = 0; i < len / 2; ++i) {   // (2)
            swap(s[i], s[len-i-1]);
        }
    }
};
  • ( 1 ) (1) (1) 實作一個變數交換的函式,其中&是C++中的參考,在函式傳參是經常用到,被稱為:參考傳遞(pass-by-reference),即被調函式的形式引數雖然也作為區域變數在堆疊中開辟了記憶體空間
    ,但是這時存放的是由主調函式放進來的實參變數的地址,被調函式對形參的任何操作都被處理成間接尋址,即通過堆疊中存放的地址訪問主調函式中的實參變數,

簡而言之,函式呼叫的引數,可以傳參考,從而使得函式回傳時,傳參值的改變依舊生效,

  • ( 2 ) (2) (2) 這一步是做的線性列舉,注意列舉范圍是 [ 0 , l e n / 2 ? 1 ] [0, len/2-1] [0,len/2?1]

🥖2、線性迭代

  • 每一次對程序的重復稱為一次 “迭代”,而每一次迭代得到的結果會作為下一次迭代的初始值,周而復始,直到問題全部解決,

LeetCode 206. 反轉鏈表

??【例題2】給定單鏈表的頭節點 h e a d head head ,要求反轉鏈表,并回傳反轉后的鏈表頭,
??樣例輸入: [ 1 , 2 , 3 , 4 ] [1,2,3,4] [1,2,3,4]
??樣例輸出: [ 4 , 3 , 2 , 1 ] [4, 3, 2, 1] [4,3,2,1]

  • c++ 版本給出的基礎框架代碼如下:
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
    }
};
  • 這里引入了一種資料結構 鏈表 ListNode
  • 成員有兩個:資料域val和指標域next
  • 回傳的是鏈表頭結點;

a. 思路分析

  • 這個問題,我們可以采用頭插法,即每次拿出第 2 個節點插到頭部,拿出第 3 個節點插到頭部,拿出第 4 個節點插到頭部,… 拿出最后一個節點插到頭部,
  • 于是整個程序可以分為兩個步驟:洗掉第 i i i 個節點,將它放到頭部,反復迭代 i i i 即可,
  • 如圖所示:
  • 我們發現,圖中的藍色指標永遠固定在最開始的鏈表頭結點上,那么可以以它為貧訓,每次洗掉它的next,并且插到最新的頭結點前面,不斷改變頭結點head的指向,迭代 n ? 1 n-1 n?1 次就能得到答案了,

b. 時間復雜度

  • 每個結點只會被訪問一次,執行一次頭插操作,總共 n n n 個節點的情況下,時間復雜度 O ( n ) O(n) O(n)

c. 代碼詳解

class Solution {
    ListNode *removeNextAndReturn(ListNode* now) {                 // (1) 
        if(now == nullptr || now->next == nullptr) {
            return nullptr;                                        // (2) 
        }
        ListNode *retNode = now->next;                             // (3) 
        now->next = now->next->next;                               // (4) 
        return retNode;
    }
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *doRemoveNode = head;                             // (5) 
        while(doRemoveNode) {                                      // (6) 
            ListNode *newHead = removeNextAndReturn(doRemoveNode); // (7) 
            if(newHead) {                                          // (8) 
                newHead->next = head;                              
                head = newHead;                
            }else {
                break;                                             // (9) 
            }
        }
        return head;
    }
};
  • ( 1 ) (1) (1) ListNode *removeNextAndReturn(ListNode* now)函式的作用是洗掉nownext節點,并且回傳;
  • ( 2 ) (2) (2) 本身為慷訓者下一個節點為空,回傳空;
  • ( 3 ) (3) (3) 將需要洗掉的節點快取起來,供后續回傳;
  • ( 4 ) (4) (4) 執行洗掉 now->next 的操作;
  • ( 5 ) (5) (5) doRemoveNode指向的下一個節點是將要被洗掉的節點,所以doRemoveNode需要被快取起來,不然都不知道怎么進行洗掉;
  • ( 6 ) (6) (6) 沒有需要洗掉的節點了就結束迭代;
  • ( 7 ) (7) (7) 洗掉 doRemoveNode 的下一個節點并回傳被洗掉的節點;
  • ( 8 ) (8) (8) 如果有被洗掉的節點,則插入頭部;
  • ( 9 ) (9) (9) 如果沒有,則跳出迭代,

💤3、簡單排序

  • 既然是入門,千萬不要去看快排、希爾排序這種冷門排序,
  • 冒泡排序、選擇排序、簡單插入排序 原理好懂,先看懂再說,其他不管,因為這三者都是基于列舉的,
  • C中有現成qsort排序函式,C++中有現成 sort排序函式,直接拿來用,等演算法進階時再回頭來看快速排序的演算法實作,

【例題3】LeetCode 88. 合并兩個有序陣列

??【例題3】給你兩個有序整數陣列 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2,請你將 n u m s 2 nums2 nums2 合并到 n u m s 1 nums1 nums1 中,使 n u m s 1 nums1 nums1 成為一個有序陣列,初始化 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2 的元素數量分別為 m m m n n n ,你可以假設 n u m s 1 nums1 nums1 的空間大小等于 m + n m + n m+n,這樣它就有足夠的空間保存來自 n u m s 2 nums2 nums2 的元素,
??樣例輸入: n u m s 1 = [ 1 , 2 , 3 , 0 , 0 , 0 ] , m = 3 , n u m s 2 = [ 2 , 5 , 6 ] , n = 3 nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 nums1=[1,2,3,0,0,0],m=3,nums2=[2,5,6],n=3
??樣例輸出: [ 1 , 2 , 2 , 3 , 5 , 6 ] [1,2,2,3,5,6] [1,2,2,3,5,6]

a. 思路分析

這個題別想太多,直接把第二個陣列的元素加到第一個陣列元素的后面,然后直接排序就成,

b. 時間復雜度

  • STL 排序函式的時間復雜度為 O ( n l o g 2 n ) O(nlog_2n) O(nlog2?n),遍歷的時間復雜度為 O ( n ) O(n) O(n),所以總的時間復雜度為 O ( n l o g 2 n ) O(nlog_2n) O(nlog2?n)

c. 代碼詳解

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        for(int i = m; i < n + m; ++i) {
            nums1[i] = nums2[i-m];               // (1)
        }
        sort(nums1.begin(), nums1.end());        // (2)
    }
};
  • ( 1 ) (1) (1) 簡單合并兩個陣列;
  • ( 2 ) (2) (2) 對陣列1進行排序;

只要能夠達到最終的結果, O ( n ) O(n) O(n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2?n) 的差距其實并沒有那么大,


🥂4、二分列舉

  • 二分一般指二分查找,當然有時候也指代二分列舉,

LeetCode 704. 二分查找

??【例題4】給定 n ( 1 ≤ n ≤ 1 0 4 ) n(1 \le n \le 10^4) n(1n104) 個元素的升序整型陣列 n u m s nums nums 和一個值 t a r g e t target target,求實作一個函式查找 n u m s nums nums t a r g e t target target 的下標,如果查找不到則回傳 ? 1 -1 ?1

  • c++ 版本給出的基礎框架代碼如下,要求實作的是search這個函式,陣列引數傳入的是一個vector<int>,是 STL 中常用的陣列容器,支持動態擴容,vector的動態擴容有興趣了解的,可以參考以下這篇文章:《C/C++ 面試 100 例》(四)vector 擴容策略,
class Solution {
public:
    int search(vector<int>& nums, int target) {
    }
};

a. 思路分析

  • 1)令初始情況下,陣列下標從 0 開始,且陣列長度為 n n n,則定義一個區間,它的左端點是 l = 0 l=0 l=0,右端點是 r = n ? 1 r = n-1 r=n?1
  • 2)生成一個區間中點 m i d = ( l + r ) / 2 mid = (l + r) / 2 mid=(l+r)/2,并且判斷 m i d mid mid 對應的陣列元素和給定的目標值的大小關系,主要有三種:
  • ??2.a)目標值 等于 陣列元素,直接回傳 m i d mid mid
  • ??2.b)目標值 大于 陣列元素,則代表目標值應該出現在區間 [ m i d + 1 , r ] [mid+1, r] [mid+1,r],迭代左區間端點: l = m i d + 1 l = mid + 1 l=mid+1
  • ??2.c)目標值 小于 陣列元素,則代表目標值應該出現在區間 [ l , m i d ? 1 ] [l, mid-1] [l,mid?1],迭代右區間端點: r = m i d ? 1 r = mid - 1 r=mid?1
  • 3)如果這時候 l > r l > r l>r,則說明沒有找到目標值,回傳 ? 1 -1 ?1;否則,回到 2)繼續迭代,

b. 時間復雜度

  • 由于每次都是將區間折半,所以二分查找的時間復雜度為 O ( l o g 2 n ) O(log_2n) O(log2?n)

c. 代碼詳解

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int l = 0, r = nums.size() - 1;      // (1)
        while(l <= r) {                      // (2)
            int mid = (l + r) >> 1;          // (3)
            if(nums[mid] == target) {   
                return mid;                  // (4)
            }else if(target > nums[mid]) {
                l = mid + 1;                 // (5)
            }else if(target < nums[mid]) {
                r = mid - 1;                 // (6)
            }
        }
        return -1;                           // (7)
    }
};
  • ( 1 ) (1) (1) 由于vector的下標和陣列一樣是從 0 開始的,所以初始的左右查找區間端點為 0 和 陣列長度減一,nums.size()回傳的是陣列的長度;
  • ( 2 ) (2) (2) 一直迭代左右區間的端點,直到 左端點 大于 右端點 結束;
  • ( 3 ) (3) (3) >> 1等價于除 2,也就是這里mid代表的是lr的中點;
  • ( 4 ) (4) (4) nums[mid] == target表示正好找到了這個數,則直接回傳下標mid
  • ( 5 ) (5) (5) target > nums[mid]表示target這個數在區間 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 中,所以才有左區間賦值如下:l = mid + 1;
  • ( 6 ) (6) (6) target < nums[mid]表示target這個數在區間 [ l , m i d ? 1 ] [l, mid - 1] [l,mid?1] 中,所以才有右區間賦值如下:r = mid - 1;
  • ( 7 ) (7) (7) 這一步呼應了 ( 2 ) (2) (2),表示這不到給定的數,直接回傳 -1

🥢5、雙指標

  • 接下來,以一個非常經典的面試題【最長不重復子串】為例,展開對雙指標演算法的講解,

【例題5】給定一個長度為 n ( 1 ≤ n ≤ 1 0 7 ) n (1 \le n \le 10^7) n(1n107) 的字串 s s s,求一個最長的滿足所有字符不重復的子串,

a. 初步分析

  • 首先我們分析一下這個問題的關鍵詞,主要有以下幾個:
  • 1) n ≤ 1 0 7 n \le 10^7 n107
  • 2)最長;
  • 3)所有字符不重復;
  • 4)子串;
  • 根據以上的幾個關鍵詞,我們可以得出一些結論,首先,根據 n n n 的范圍已經能夠大致確認這是一個需要 O ( n ) O(n) O(n) 或者 O ( n l o g 2 n ) O(nlog_2n) O(nlog2?n) 的演算法才能解決的問題;其次,“最長” 這個詞告訴我們,可能是一個動態規劃問題或者貪心問題,也有可能是搜索,所以這個關鍵詞給我們的資訊用處不大;而判斷字符是否重復可以用 哈希表 在 O ( 1 ) O(1) O(1) 的時間內判斷;最后,列舉所有 “子串” 的時間復雜度是 O ( n 2 ) O(n^2) O(n2) 的,

b. 樸素演算法

  • 由以上分析,我們可以發現第(1)個 和 第(4)個關鍵詞給我們得出的結論是矛盾的,那么,我們可以先嘗試減小 n n n 的范圍,如果 n ≤ 1000 n \le 1000 n1000 時,怎么解決這個問題呢?
  • 因為最后求的是滿足條件的最長子串,所以我們如果能夠列舉所有子串,那么選擇長度最長的滿足條件的子串就是答案了(這里的條件是指子串中所有字符都不同),

a n s ans ans 記錄我們需要求的最大不重復子串的長度,用一個哈希表 h h h 來代表某個字符是否出現過,演算法描述如下:
??1)列舉子串的左端點 i = 0 → n ? 1 i = 0 \to n-1 i=0n?1
??2)清空哈希表 h h h
??3)列舉子串的右端點 j = i → n ? 1 j = i \to n-1 j=in?1,如果當前這個字符 s j s_j sj? 出現過(即 h [ s j ] = t r u e h[s_j] = true h[sj?]=true),則跳出 j j j 的回圈;否則,令 h [ s j ] = t r u e h[s_j] = true h[sj?]=true,并且用當前長度去更新 a n s ans ans(即 a n s = m a x ( a n s , j ? i + 1 ) ans= max(ans, j - i +1) ans=max(ans,j?i+1));
??4)回到 2);

  • c++ 代碼實作如下:
int getmaxlen(int n, char *str, int& l, int& r) {
    int ans = 0, i, j, len;
    memset(h, 0, sizeof(h));
    for(i = 0; i < n; ++i) {                // 1)
        j = i;
        memset(h, false, sizeof(h));        // 2)
        while(j < n && !h[str[j]]) {
            h[ str[j] ] = true;             // 3)
            len = j - i + 1;
            if(len > ans)
                ans = len, l = i, r = j;
            ++j;
        }
    }
    return ans;
}
  • 1)這一步列舉對應子串的左端點 i i i
  • 2)這一步用于清空哈希表 h h h,其中 h [ s j ] = t r u e h[ s_j ] = true h[sj?]=true 代表原字串的第 j j j 個字符 s j s_j sj? 是否出現在以第 i i i 個字符為左端點的子串中;
  • 3)而第三步可以這么理解:如果字串 s [ i : j ] s[i:j] s[i:j] 中已經出現重復的字符,那么 s [ i : j + 1 ] , s [ i : j + 2 ] , . . . , s [ i : n ? 1 ] s[i:j+1],s[i:j+2], ... , s[i:n-1] s[i:j+1]s[i:j+2],...,s[i:n?1] 必然會有重復字符,所以這里不需要繼續往下列舉,直接跳出第二層回圈即可,
  • 這個演算法執行完畢, a n s ans ans 就是我們要求的最長不重復子串的長度, [ l , r ] [l, r] [l,r] 代表了最長不重復子串在原字串的區間,正確性毋庸置疑,因為已經列舉了所有子串的情況,如果字符集的個數 z z z,演算法的時間復雜度就是 O ( n z ) O(nz) O(nz)
  • 最后奉上一張動圖,代表了上述樸素演算法的求解程序,如圖所示:
  • 字串下標從 0 開始,最長無重復子串為: s [ 1 : 5 ] = b c a e d s[1:5] = bcaed s[1:5]=bcaed,長度為 5,
  • 由于是字串,字符集的個數 z z z 最多 256,所以時間復雜度基本就是 O ( 256 n ) O(256n) O(256n),當 n ≤ 1 0 7 n \le 10^7 n107 時,這個時間復雜度是無法接受的,需要想辦法優化,

c. 優化演算法

  • 如果仔細思考上面樸素演算法的求解程序,就會發現:列舉子串的時候有很多區間是重疊的,所以必然存在許多沒有必要的重復計算,
  • 我們考慮一個子串以 s i s_i si? 為左端點, s j s_j sj? 為右端點,且 s [ i : j ? 1 ] s[i:j-1] s[i:j?1] 中不存在重復字符, s [ i : j ] s[i:j] s[i:j] 中存在重復字符(換言之, s j s_j sj? s [ i : j ? 1 ] s[i:j-1] s[i:j?1] 中某個字符相同),如圖所示:
  • 那么我們沒必要再去檢測 s [ i : j + 1 ] s[i:j+1] s[i:j+1] s [ i : j + 2 ] s[i:j+2] s[i:j+2] s [ i : n ? 1 ] s[i:n-1] s[i:n?1] 這幾個字串的合法性,因為當前情況 s [ i : j ] s[i:j] s[i:j] 是非法的,而這些字串是完全包含 s [ i : j ] s[i:j] s[i:j] 的,所以它們必然也是不合法的,
  • 那么我們可以把列舉的左端點自增,即: i = i + 1 i = i +1 i=i+1,這時,按照樸素演算法的實作,右端點需要重置,即 j = i j = i j=i,實際上這里的右端點可以不動,
  • 可以這么考慮,由于 s j s_j sj? 這個字符和 s [ i : j ? 1 ] s[i:j-1] s[i:j?1] 中的字符產生了重復,假設這個重復的字符的下標為 k k k,那么 i i i 必須滿足 i > k i \gt k i>k,換言之, i i i 可以一直自增,直到 i = k + 1 i = k+1 i=k+1,如圖所示:
  • 利用上述思路,我們重新實作 最長不重復子串 的演算法, c++ 代碼實作如下:
int getmaxlen(int n, char *str, int& l, int& r) {
    int ans = 0, i = 0, j = -1, len;   // 1)
    memset(h, 0, sizeof(h));           // 2)
    while (j++ < n - 1) {              // 3)
        ++h[ str[j] ];                 // 4)
        while (h[ str[j] ] > 1) {      // 5)
            --h[ str[i] ];
            ++i;
        }
        len = j - i + 1;              
        if(len > ans)                  // 6)
            ans = len, l = i, r = j;
    }
    return ans;
}
  • 1)初始化 i = 0, j = -1,代表 s [ i : j ] s[i:j] s[i:j] 為一個空串,從空串開始列舉;
  • 2)同樣需要維護一個哈希表,哈希表記錄的是當前列舉的區間 s [ i : j ] s[i:j] s[i:j] 中每個字符的個數;
  • 3)只推進子串的右端點;
  • 4)在哈希表中記錄字符的個數;
  • 5)當 h[ str[j] ] > 1滿足時,代表出現了重復字符,這時候左端點 i i i 推進,直到沒有重復字符為止;
  • 6)記錄當前最優解 j-i+1
  • 這個演算法執行完畢,我們就可以得到最長不重復子串的長度為 a n s ans ans,并且 i i i j j j 這兩個指標分別只自增 n n n 次,兩者自增相互獨立,是一個相加而非相乘的關系,所以這個演算法的時間復雜度為 O ( n ) O(n) O(n)
  • 利用該優化演算法優化后的最長不重復子串的求解程序如圖所示:
    在這里插入圖片描述
  • 參考這個圖,一個比較通俗易懂的解釋:當區間 [ i , j ] [i, j] [i,j] 中存在重復(紅色)字符時,左指標 i i i 自增;否則,右指標 j j j 自增,

🍴6、差分法

  • 差分法一般配合前綴和,
  • 對于區間 [ l , r ] [l, r] [l,r] 內求滿足數量的數,可以利用差分法分解問題;
  • 假設 [ 0 , x ] [0, x] [0,x] 內的 g o o d n u m b e r good \ number good number 數量為 g x g_x gx?,那么區間 [ l , r ] [l, r] [l,r] 內的數量就是 g r ? g l ? 1 g_r - g_{l-1} gr??gl?1?;分別用同樣的方法求出 g r g_r gr? g l ? 1 g_{l-1} gl?1?,再相減即可;

🅱7、位運算

  • 位運算可以理解成對二進制數字上的每一個位進行操作的運算,
  • 位運算分為 布爾位運算子 和 移位位運算子,
  • 布爾位運算子又分為 位與(&)、位或(|)、異或(^)、按位取反(~);移位位運算子分為 左移(<<) 和 右移(>>),
  • 如圖所示:
  • 位運算的特點是陳述句短,但是可以干大事!

a、位與

??【例題7】給出一個整數,現在要求將這個整數轉換成二進制以后,將末尾連續的1都變成0,輸出改變后的數(以十進制輸出即可),

  • 我們知道,這個數的二進制表示形式一定是:
  • . . . 0 11...11 ? k ...0\underbrace{11...11}_{\rm k} ...0k 11...11??
  • 如果,我們把這個二進制數加上1,得到的就是:
  • . . . 1 00...00 ? k ...1\underbrace{00...00}_{\rm k} ...1k 00...00??
  • 我們把這兩個數進行位與運算,得到:
  • . . . 0 00...00 ? k ...0\underbrace{00...00}_{\rm k} ...0k 00...00??
  • 所以,你學會了嗎?代碼如下:
x &= x - 1;

b、位或

【例題8】給定一個整數 x x x,將它低位連續的 0 都變成 1,

  • 假設這個整數低位連續有 k k k 個零,二進制表示如下:
  • . . . 1 00...00 ? k ...1\underbrace{00...00}_{\rm k} ...1k 00...00??
  • 那么,如果我們對它進行減一操作,得到的二進制數就是:
  • . . . 0 11...11 ? k ...0\underbrace{11...11}_{\rm k} ...0k 11...11??
  • 我們發現,只要對這兩個數進行位或,就能得到:
  • . . . 1 11...11 ? k ...1\underbrace{11...11}_{\rm k} ...1k 11...11??
  • 也正是題目所求,所以代碼實作如下:
#include <stdio.h>
int main() {
    int x;
    scanf("%d", &x);
    printf("%d\n", x | (x-1) );    // (1)
    return 0;
}
  • ( 1 ) (1) (1) x | (x-1)就是題目所求的 “低位連續零變一” ,

c、異或

【例題9】給定兩個數 a a a b b b,用異或運算交換它們的值,

  • 這個是比較老的面試題了,直接給出代碼:
#include <stdio.h>
int main() {
    int a, b;
	while (scanf("%d %d", &a, &b) != EOF) {
	    a = a ^ b;   // (1)
	    b = a ^ b;   // (2)
	    a = a ^ b;   // (3)
	    printf("%d %d\n", a, b);
	}
	return 0;
}
  • 我們直接來看 ( 1 ) (1) (1) ( 2 ) (2) (2) 這兩句話,相當于b等于a ^ b ^ b,根據異或的幾個性質,我們知道,這時候的b的值已經變成原先a的值了,
  • 而再來看第 ( 3 ) (3) (3) 句話,相當于a等于a ^ b ^ a,還是根據異或的幾個性質,這時候,a的值已經變成了原先b的值,
  • 從而實作了變數ab的交換,

d、左移

  • 對于 x x x 模上一個 2 的次冪的數 y y y,我們可以轉換成位與上 2 y ? 1 2^y-1 2y?1
  • 即在數學上的:
  • x m o d 2 y x \ mod \ 2^y x mod 2y
  • 在計算機中就可以用一行代碼表示:x & ((1 << y) - 1)

🧡8、貪心

  • 貪心,一般就是按照當前最優解,去推算全域最優解,
  • 所以,只有當當前最優解和全域最優解一致時才能用貪心演算法,貪心演算法的證明是比較難的,但是一些簡單的貪心問題會比較直觀,很容易看出來這個能夠這么貪,

?9、遞回分治

  • 分治,就是把問題分成若干子問題求解,子問題解決后,問題就解決了,一般利用遞回實作,屬于初學者比較頭疼的內容,遞回一開始學習的時候,一定要注意全域變數和區域變數的關系,
🙉飯不食,水不飲,題必須刷🙉

還不會C語言,和我一起打卡!
🌞《光天化日學C語言》🌞

LeetCode 太難?上簡單題!
🧡《C語言入門100例》🧡

LeetCode 太簡單?大神盤他!
🌌《夜深人靜寫演算法》🌌

LeetCode 46. 全排列

?【例題10】給定一個 不含重復數字 的長度小于 7 的陣列 n u m s nums nums ,回傳其 所有可能的全排列,
??樣例輸入: n u m s = [ 1 , 2 , 3 ] nums = [1,2,3] nums=[1,2,3]
??樣例輸出: [ [ 1 , 2 , 3 ] , [ 1 , 3 , 2 ] , [ 2 , 1 , 3 ] , [ 2 , 3 , 1 ] , [ 3 , 1 , 2 ] , [ 3 , 2 , 1 ] ] [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

a. 思路分析

  • 全排列的種數是 n ! n! n!,這是最典型的深搜問題,我們可以把 n n n 個數兩兩建立無向邊(即任意兩個結點之間都有邊,也就是一個 n n n 個結點的完全圖),然后對每個點作為起點,分別做一次深度優先搜索,當所有點都已經標記時,輸出當前的搜索路徑,就是其中一個排列;
  • 這里需要注意的是,回溯的時候需要將原先標記的點的標記取消,否則只能輸出一個排列,
  • 如下圖,代表的是3個數的圖表示:
  • 3個數的全排列的深度優先搜索空間樹如下圖所示:
  • 關于深度優先搜索的更深入內容,可以參考這篇文章:夜深人靜寫演算法(一)- 搜索入門,

b. 時間復雜度

  • 由于每次訪問后會將標記過的節點標記回來,所以時間復雜度就是排列數,即 O ( n ! ) O(n!) O(n!)

c. 代碼詳解

class Solution {
    int hash[6];

    void dfs(int dep, int maxdep, vector<int>& nums, vector<vector<int>>& ans, vector<int>& st) {                                                            // (1)
        if(dep == maxdep) {
            ans.push_back( st );                             // (2)
            return ;
        }

        for(int i = 0; i < maxdep; ++i) {                    
            if(!hash[i]) {                                   // (3)
                hash[i] = 1;                                
                st.push_back(  nums[i] );                   
                dfs(dep + 1, maxdep, nums, ans, st);         // (4)
                st.pop_back();                               // (5)
                hash[i] = 0;                                 // (6)
            }
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ans;            
        memset(hash, 0, sizeof(hash));        
        vector<int> st; 
        dfs(0, nums.size(), nums, ans, st);     
        return ans;
    }
};
  • ( 1 ) (1) (1) dep代表本地遞回訪問第幾個節點,maxdep代表遞回的層數,vector<int>& nums則代表原始輸入陣列,vector<vector<int>>& ans代表結果陣列,vector<int>& st代表存盤當前搜索結果的堆疊;
  • ( 2 ) (2) (2) dep == maxdep代表一次全排列訪問,這時候得到一個可行解,放入結果陣列中;
  • ( 3 ) (3) (3) 如果節點 i i i 未訪問,則將第 i i i 個節點的值塞入搜索結果堆疊,并且用hash陣列標記已訪問;
  • ( 4 ) (4) (4) 繼續遞回呼叫下一層;
  • ( 5 ) (5) (5) 將堆疊頂元素彈出,代表本次訪問的回溯;
  • ( 6 ) (6) (6) 取消陣列標記;

🚊10、簡單動態規劃

LeetCode 746. 使用最小花費爬樓梯

??陣列的每個下標作為一個階梯,第 i i i 個階梯對應著一個非負數的體力花費值 c o s t [ i ] cost[i] cost[i](下標從 0 開始),每當爬上一個階梯,都要花費對應的體力值,一旦支付了相應的體力值,就可以選擇 向上爬一個階梯 或者 爬兩個階梯,求找出達到樓層頂部的最低花費,在開始時,可以選擇從下標為 0 或 1 的元素作為初始階梯,
??樣例輸入: c o s t = [ 1 , 99 , 1 , 1 , 1 , 99 , 1 , 1 , 99 , 1 ] cost = [1, 99, 1, 1, 1, 99, 1, 1, 99, 1] cost=[1,99,1,1,1,99,1,1,99,1]
??樣例輸出: 6 6 6
如圖所以,藍色的代表消耗為 1 的樓梯,紅色的代表消耗 99 的樓梯,

a、思路分析

  • 令走到第 i i i 層的最小消耗為 f [ i ] f[i] f[i]
  • 假設當前的位置在 i i i 層樓梯,那么只可能從 i ? 1 i-1 i?1 層過來,或者 i ? 2 i-2 i?2 層過來;
  • 如果從 i ? 1 i-1 i?1 層過來,則需要消耗體力值: f [ i ? 1 ] + c o s t [ i ? 1 ] f[i-1] + cost[i-1] f[i?1]+cost[i?1]
  • 如果從 i ? 2 i-2 i?2 層過來,則需要消耗體力值: f [ i ? 2 ] + c o s t [ i ? 2 ] f[i-2] + cost[i-2] f[i?2]+cost[i?2]
  • 起點可以在第 0 或者 第 1 層,于是有狀態轉移方程:
  • f [ i ] = { 0 i = 0 , 1 min ? ( f [ i ? 1 ] + c o s t [ i ? 1 ] , f [ i ? 2 ] + c o s t [ i ? 2 ] ) i > 1 f[i] = \begin{cases} 0 & i=0,1\\ \min ( f[i-1] + cost[i-1], f[i-2] + cost[i-2] ) & i > 1\end{cases} f[i]={0min(f[i?1]+cost[i?1],f[i?2]+cost[i?2])?i=0,1i>1?

b. 時間復雜度

  • 狀態數: O ( n ) O(n) O(n)
  • 狀態轉移: O ( 1 ) O(1) O(1)
  • 時間復雜度: O ( n ) O(n) O(n)

c. 代碼詳解

class Solution {
    int f[1100];                                                   // (1)
public:
    int minCostClimbingStairs(vector<int>& cost) {
        f[0] = 0, f[1] = 0;                                        // (2)
        for(int i = 2; i <= cost.size(); ++i) {
            f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]);    // (3)
        }
        return f[cost.size()];
    }
};
  • ( 1 ) (1) (1)f[i]代表到達第 i i i 層的消耗的最小體力值,
  • ( 2 ) (2) (2) 初始化;
  • ( 3 ) (3) (3) 狀態轉移;

有沒有發現,這個問題和斐波那契數列很像,只不過斐波那契數列是求和,這里是求最小值,


6??刷題順序的建議

??然后介紹一下刷題順序的問題,我們刷題的時候千萬不要想著一步到位,一開始,沒有刷滿三百題,姿態放低,都把自己當成小白來處理,
??這里以刷 LeetCode 為例,我目前只刷了不到 50 題,所以我是小白,
??當我是小白時,我只刷入門題,也就是下面這幾個專題,先把上面所有的題目刷完,在考慮下一步要做什么,

👨?👦1、入門演算法

種類鏈接
演算法演算法入門
資料結構資料結構入門
陣列字串專題陣列和字串
動態規劃專題動態規劃入門、DP路徑問題

??當入門的題刷完了,并且都能講述出來自己刷題的程序以后,我們再來看初級的一些演算法和簡單的資料結構,簡單的資料結構就是線性表了,包含:陣列、字串、鏈表、堆疊、佇列 等等,即下面這些專題,

👩?👧?👦2、初級演算法

種類鏈接
演算法初級演算法
堆疊和佇列專題佇列 & 堆疊

??上面的題刷完以后,其實已經算是基本入門了,然后就可以開始系統性的學習了,
??當然,基本如果真的到了這一步,說明你的確已經愛上了刷題了,那么我們可以嘗試挑戰一下 LeetCode 上的一些熱門題,畢竟熱門題才是現在面試的主流,能夠有更好的結果,這樣刷題的時候也會有更加強勁的動力不是嗎!

👩?👩?👧?👦3、中級演算法

種類鏈接
演算法中極演算法
二叉樹專題二叉樹
熱門題熱門題 TOP 100

7??系統學習演算法和資料結構

🚍1、進階動態規劃

文章鏈接難度等級推薦閱讀
夜深人靜寫演算法(二)- 動態規劃入門★☆☆☆☆★★★★★
夜深人靜寫演算法(二十六)- 記憶化搜索★☆☆☆☆★★★★★
夜深人靜寫演算法(十九)- 背包總覽★☆☆☆☆★★★★★
夜深人靜寫演算法(二十)- 最長單調子序列★☆☆☆☆★★★★★
夜深人靜寫演算法(二十一)- 最長公共子序列★☆☆☆☆★★★★★
夜深人靜寫演算法(二十二)- 最小編輯距離★★☆☆☆★★★★☆
夜深人靜寫演算法(十四)- 0/1 背包★☆☆☆☆★★★★☆
夜深人靜寫演算法(十五)- 完全背包★★☆☆☆★★★★☆
夜深人靜寫演算法(十六)- 多重背包★★☆☆☆★★★★☆
夜深人靜寫演算法(二十七)- 區間DP★★★☆☆★★★★☆
夜深人靜寫演算法(二十九)- 數位DP★★★☆☆★★★★★
夜深人靜寫演算法(十七)- 分組背包★★★☆☆★★★☆☆
夜深人靜寫演算法(十八)- 依賴背包★★★★☆★★☆☆☆
夜深人靜寫演算法(六)- RMQ★★★☆☆★★☆☆☆
樹形DP待更新
組合博弈待更新
組合計數DP待更新
四邊形不等式待更新
狀態壓縮DP/TSP待更新
斜率優化的動態規劃待更新
插頭DP待更新

🪐2、強勁圖論搜索


1、深度優先搜索

文章鏈接難度等級推薦閱讀
夜深人靜寫演算法(一)- 搜索入門★☆☆☆☆★★★☆☆
夜深人靜寫演算法(八)- 二分圖最大匹配★★☆☆☆★★☆☆☆
最大團待更新
最小生成樹待更新
樹的分治待更新
迭代加深 IDA*待更新
有向圖強連通分量和2-sat待更新
無向圖割邊割點待更新
帶權圖的二分圖匹配待更新
哈密爾頓回路待更新
最近公共祖先待更新
歐拉回路圈套圈待更新
最小費用最大流待更新
最小樹形圖待更新

2、廣度優先搜索

文章鏈接難度等級推薦閱讀
夜深人靜寫演算法(十)- 單向廣搜★★☆☆☆★★★★☆
夜深人靜寫演算法(二十三)- 最短路★★★☆☆★★★★☆
夜深人靜寫演算法(二十五)- 穩定婚姻★★☆☆☆★★☆☆☆
夜深人靜寫演算法(二十四)- 最短路徑樹★★★☆☆★☆☆☆☆
K 短路待更新
差分約束待更新
拓撲排序待更新
A*待更新
雙向廣搜待更新
最大流 最小割待更新

0??3、進階初等數論

文章鏈接難度等級推薦閱讀
夜深人靜寫演算法(三)- 初等數論入門★★☆☆☆★★★★☆
夜深人靜寫演算法(三十)- 二分快速冪★☆☆☆☆★★★★★
夜深人靜寫演算法(三十一)- 歐拉函式★★★☆☆★★★★★
夜深人靜寫演算法(三十二)- 費馬小定理★★☆☆☆★★★☆☆
夜深人靜寫演算法(三十三)- 擴展歐拉定理★★★☆☆★★★★☆
夜深人靜寫演算法(三十四)- 逆元★★★☆☆★★★★☆
夜深人靜寫演算法(三十五)- RSA 加密解密★★★☆☆★★★★★
夜深人靜寫演算法(三十六)- 中國剩余定理★★☆☆☆★★★☆☆
夜深人靜寫演算法(三十七)- 威爾遜定理★★☆☆☆★★★☆☆
夜深人靜寫演算法(三十八)- 整數分塊★★☆☆☆★★★★☆
盧卡斯定理待更新
狄利克雷卷積待更新
莫比烏斯反演待更新
容斥原理待更新
拉賓米勒待更新
Pollard rho待更新
莫隊待更新
原根待更新
大步小步演算法待更新
二次剩余待更新
矩陣二分快速冪待更新
Polya環形計數待更新

🛑4、進階計算幾何

📏5、字串的匹配

🎄6、高級資料結構


🙉飯不食,水不飲,題必須刷🙉

還不會C語言,和我一起打卡!
🌞《光天化日學C語言》🌞

LeetCode 太難?上簡單題!
🧡《C語言入門100例》🧡

LeetCode 太簡單?大神盤他!
🌌《夜深人靜寫演算法》🌌

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/289587.html

標籤:其他

上一篇:JavaScript常用的8個陣列去重實戰原始碼

下一篇:5G是否被吹過頭了?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more