每天一个算法(简单)
?
文章目錄
- 前言
- 一.排序算法
- 二.查找算法
- 2.1 哈希查找
- 2.1.1 兩數(shù)之和
- 2.1.2 兩個數(shù)組的交集
- 2.1.3 日志速率限制器
- 2.2 最長公共前綴
- 2.3 KMP
- 2.3.1 實(shí)現(xiàn) strStr()
- 2.4 二分查找
- 前言
- 2.4.1 搜索插入位置
- 2.4.2 Sqrt(x)
- 2.4.2 有效的完全平方數(shù)
- 2.4.3 第一個錯誤的版本
- 2.5 存在重復(fù)元素2
- 三.數(shù)論相關(guān)
- 前言
- 3.1 快速冪
- 3.3 反轉(zhuǎn)
- 3.3.1 整數(shù)反轉(zhuǎn)(非常重要,涉及溢出)
- 3.3.2 回文數(shù)
- 3.3.3 回文排列
- 3.4 映射規(guī)則
- 3.4.1 羅馬字符轉(zhuǎn)數(shù)字
- 3.4.2 同構(gòu)字符串
- 3.4.3 單詞規(guī)律
- 3.5 大數(shù)運(yùn)算
- 3.5.1 加1
- 3.5.2 二進(jìn)制求和
- 3.5.3 階乘后的零
- 3.6 快樂數(shù)
- 3.7 素數(shù)
- 3.7.1 計數(shù)素數(shù)
- 3.8 各位相加
- 3.9 丑數(shù)
- 四.棧
- 4.1 有效的括號
- 五.線性表和鏈表
- 前言
- 5.1 指針
- 5.1.1 合并兩個有序鏈表
- 5.1.1 合并兩個有序數(shù)組
- 5.1.2 刪除排序數(shù)組中的重復(fù)項
- 5.1.2 刪除排序鏈表中的重復(fù)元素
- 5.1.3 移除元素
- 5.1.4 驗證回文串
- 5.1.5 環(huán)形鏈表
- 5.1.6 回文鏈表
- 5.1.7 刪除鏈表中的節(jié)點(diǎn)
- 5.2 最后一個單詞的長度
- 5.3 雙指針
- 5.3.1 相交鏈表
- 5.3.2 反轉(zhuǎn)鏈表
- 5.3.3 匯總區(qū)間
- 5.3.4 會議室
- 5.3.5 移動零
- 5.4 棧和隊列
- 5.4.1 用隊列實(shí)現(xiàn)棧
- 5.4.3 用棧實(shí)現(xiàn)隊列
- 5.5 區(qū)域和檢索 - 數(shù)組不可變
- 六.動態(tài)規(guī)劃
- 6.1 最大子序列和
- 6.2 爬樓梯
- 6.3 楊輝三角
- 6.4 楊輝三角2
- 6.5 買賣股票的最佳時機(jī)
- 6.6 買賣股票的最佳時機(jī)2
- 6.7 比特位計數(shù)
- 七. 樹
- 前言
- 7.1 樹的遍歷
- 7.1.1 二叉樹的中序遍歷
- 7.1.2 相同的樹
- 7.1.3 對稱二叉樹
- 7.1.4 二叉樹的最大深度
- 7.1.5 將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
- 7.1.6 平衡二叉樹
- 7.1.7 二叉樹的最小深度
- 7.1.8 路徑總和
- 7.1.8 二叉樹的所有路徑
- 7.1.9 翻轉(zhuǎn)二叉樹
- 7.2 二叉搜索樹的最近公共祖先
- 7.3 最接近的二叉搜索樹值
- 八. 位運(yùn)算
- 8.1 只出現(xiàn)一次的數(shù)字
- 8.2 Excel表列名稱
- 8.3 顛倒二進(jìn)制位
- 8.4 2的冪
- 8.4 3的冪
- 8.5 丟失的數(shù)字
- 九. 字符串
- 9.1 最短單詞距離
- 十. 博弈論
- 10.1 nim游戲
前言
此篇文章的目的是在于建立一個可以協(xié)同配合的工作流,我希望對于一個算法的理解包含以下內(nèi)容:詳盡的分析,程序的結(jié)構(gòu),代碼。文章中包含前兩個部分,然后在本地完成代碼。編寫代碼是否需要再附上結(jié)構(gòu),很難說,我現(xiàn)在想的是只加上主干,再附上一個鏈接即可
一.排序算法
二.查找算法
2.1 哈希查找
2.1.1 兩數(shù)之和
題目:給定一個整數(shù)數(shù)組 nums 和一個整數(shù)目標(biāo)值 target,請你在該數(shù)組中找出 和為目標(biāo)值 target 的那 兩個 整數(shù),并返回它們的數(shù)組下標(biāo)。
你可以假設(shè)每種輸入只會對應(yīng)一個答案。但是,數(shù)組中同一個元素在答案里不能重復(fù)出現(xiàn)。
暴力解法:兩層循環(huán)遍歷所有的下標(biāo)組合
優(yōu)化:對于時空復(fù)雜度分別為n方和1的問題,優(yōu)先考慮空間換時間
-在遍歷的同時,記錄一些信息,以省去一層循環(huán),這是“以空間換時間"的想法
-需要記錄已經(jīng)遍歷過的數(shù)值和它所對應(yīng)的下標(biāo),可以借助查找表實(shí)現(xiàn)
-查找表有兩個常用的實(shí)現(xiàn):
·哈希表
·平衡二叉搜索樹
因為不需要維護(hù)結(jié)果的順序,因此使用哈希表
本質(zhì)上,此問題是尋找target - x是否存在于數(shù)組中,暴力解法的問題就在于尋找target-x的時間復(fù)雜度太高,剛好哈希表查找的復(fù)雜度是o(1)
我犯了一個錯誤,雖然是找target-x, 但不是將target-x存到哈希表中,而是將數(shù)組中的元素逐個存到哈希表中。極端一點(diǎn),我可以先將數(shù)組的元素使用哈希表存儲,再遍歷數(shù)組尋找target-x
程序上需要注意的是需要使用高級程序語言提供的哈希表,不需要自己構(gòu)建哈希表
哈希表的使用
簡單介紹哈希表:
哈希表是基于鍵、值對存儲的數(shù)據(jù)結(jié)構(gòu),底層一般采用的是列表(數(shù)組)
使用除留余數(shù)法確定數(shù)據(jù)在哈希表中的位置,需要選擇一個p,對P取余。P還決定了哈希表的長度,P要盡可能的原理2的冪
使用線性探測法處理沖突,比如兩個數(shù)的哈希值相等,a占據(jù)位置之后,b以1為單位向后順延
對于python,字典就是哈希表
enumerate()返回的是一個引用類型的變量,打印出來是這個變量的地址,屬于enumerate類,需要類型轉(zhuǎn)換為list.每個元素的順序是,(下標(biāo),元素)
2.1.2 兩個數(shù)組的交集
分析:很容易得出,目標(biāo)是求出同時在兩個數(shù)組的元素,且這個元素唯一
我最開始認(rèn)為只需要將一個數(shù)組哈希化,但這樣處理重復(fù)元素就很麻煩了。除非把a(bǔ)ns數(shù)組也給哈希化,這樣在推入一個新的交集元素之前才能以O(shè)(1)的性能判斷該交集元素是否已經(jīng)存在于ans. 哈希化的過程除了統(tǒng)計元素之外,也同時可以去除重復(fù)元素
2.1.3 日志速率限制器
又重新想了一下什么叫每x秒一次,從數(shù)值上來說,如果t發(fā)生了,那么下一次發(fā)生在t+x,這就是每x秒一次
2.2 最長公共前綴
編寫一個函數(shù)來查找字符串?dāng)?shù)組中的最長公共前綴。
如果不存在公共前綴,返回空字符串 “”。
法一:橫向掃描
依次遍歷字符串?dāng)?shù)組中的每個字符串,對于每個遍歷到的字符串,更新最長公共前綴,當(dāng)遍歷完所有的字符串以后,即可得到字符串?dāng)?shù)組中的最長公共前綴。
第一個字符串就作為最長公共前綴,再去遍歷下一個字符串,更新最長公共前綴。一直迭代下去
時間:0(mn)
法二:縱向掃描
同上
這種方法最符合我的思考習(xí)慣
法三:分治法
法四:二分查找
2.3 KMP
2.3.1 實(shí)現(xiàn) strStr()
題目:實(shí)現(xiàn) strStr() 函數(shù)。
給你兩個字符串 haystack 和 needle ,請你在 haystack 字符串中找出 needle 字符串出現(xiàn)的第一個位置(下標(biāo)從 0 開始)。如果不存在,則返回 -1 。
法一:暴力解法
思路很明確,雙層循環(huán)。遍歷主串的每一位,每訪問到某一位時,遍歷副串的每一位。只要有一個不對,退出內(nèi)層循環(huán)
法二:KMP
以后再說
2.4 二分查找
前言
終止條件while l<=r, 為什么是小于等于???
A:
(1)對于偶數(shù)長度序列,當(dāng)查找第一或者倒數(shù)第一個元素時,會出現(xiàn)l=r的情況,此時l或者r指向的元素就是target
(2)對于奇數(shù)長度序列,當(dāng)查找第二個或者倒數(shù)第二個元素時,同上
由此需要將中止條件設(shè)為while l<=r
2.4.1 搜索插入位置
題目:給定一個排序數(shù)組和一個目標(biāo)值,在數(shù)組中找到目標(biāo)值,并返回其索引。如果目標(biāo)值不存在于數(shù)組中,返回它將會被按順序插入的位置。
請必須使用時間復(fù)雜度為 O(log n) 的算法。
在一個有序數(shù)組中進(jìn)行按值查找
這二分查找做下來,感覺處理邊界不是很清晰
比如說退出循環(huán)的條件是left <= right,為什么后面返回left的值就可以了,left的值表示的是索引,不是數(shù)組下標(biāo)
在不同情況下,究竟是在target<nums[mid]返回left,還是target>nums[mid]返回left
這些問題都需要解決
2.4.2 Sqrt(x)
題目:給你一個非負(fù)整數(shù) x ,計算并返回 x 的 算術(shù)平方根 。
由于返回類型是整數(shù),結(jié)果只保留 整數(shù)部分 ,小數(shù)部分將被 舍去 。
注意:不允許使用任何內(nèi)置指數(shù)函數(shù)和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
由于 xx 平方根的整數(shù)部分 \textit{ans}ans 是滿足 k^2 \leq xk
2
≤x 的最大 kk 值,因此我們可以對 kk 進(jìn)行二分查找,從而得到答案。
二分查找的下界為 0,上界可以粗略地設(shè)定為 xx。在二分查找的每一步中,我們只需要比較中間元素 mid 的平方與 xx 的大小關(guān)系,并通過比較的結(jié)果調(diào)整上下界的范圍。由于我們所有的運(yùn)算都是整數(shù)運(yùn)算,不會存在誤差,因此在得到最終的答案 ans 后,也就不需要再去嘗試ans + 1
一般來說,二分查找的結(jié)果由mid來表示,之前找插入位置那個問題返回left是因為插入的位置不是mid的位置,而是left的位置
2.4.2 有效的完全平方數(shù)
一開始還沒有意識到這是個查找問題,只是想到將原來的乘法問題轉(zhuǎn)換成乘法問題,但本質(zhì)上還是要找出一個符合條件的結(jié)果
一直以來對于二分查找不是特別熟悉
left <= right???
A:這是因為序列長度可能是奇數(shù)或者偶數(shù),當(dāng)left > right,對上述兩種情況,循環(huán)都應(yīng)該結(jié)束
mid的更新方式???
A:有兩種。一般使用的是left + (right - low) / 2, 但實(shí)際上稍微合并一下就是第二種形式,(left + right) / 2
結(jié)果在哪里????
A:一般情況下,left指針表示最終的結(jié)果
2.4.3 第一個錯誤的版本
題目:你是產(chǎn)品經(jīng)理,目前正在帶領(lǐng)一個團(tuán)隊開發(fā)新的產(chǎn)品。不幸的是,你的產(chǎn)品的最新版本沒有通過質(zhì)量檢測。由于每個版本都是基于之前的版本開發(fā)的,所以錯誤的版本之后的所有版本都是錯的。
假設(shè)你有 n 個版本 [1, 2, …, n],你想找出導(dǎo)致之后所有版本出錯的第一個錯誤的版本。
你可以通過調(diào)用 bool isBadVersion(version) 接口來判斷版本號 version 是否在單元測試中出錯。實(shí)現(xiàn)一個函數(shù)來查找第一個錯誤的版本。你應(yīng)該盡量減少對調(diào)用 API 的次數(shù)。
分析:
第一時間想到了二分查找,同時更新指針的依據(jù)要調(diào)整。就兩句話:
不過,mid的值的計算我不太熟悉,又搞錯了,以low指針的值作為base
mid = (high - low) // 2 + low
2.5 存在重復(fù)元素2
題目:給定一個整數(shù)數(shù)組和一個整數(shù) k,判斷數(shù)組中是否存在兩個不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 絕對值 至多為 k。
分析:
下標(biāo)之差在K以內(nèi)的重復(fù)元素。等價于在長度為K+1的數(shù)組里存在重復(fù)元素。但是是否需要
原理很簡單,但寫成程序卻很巧妙不容易想到
我之前的想法是每次向字典加入長度為K+1的元素,但是這里采用了一種類似于滾動的效果,先逐步加入元素,每次加入都要進(jìn)行一次比較
當(dāng)i <= k時,字典的長度從0 增長到k+1,每次增加元素都要判斷是否存在重復(fù)
當(dāng)i = k+1時,字典長度變?yōu)?k + 2, 為了維持窗口的長度,移除一個元素,移除元素的key對應(yīng)于原數(shù)組的小標(biāo) i-k-1,然后比較此時的i是否存在重復(fù)。除i以外的元素前面已經(jīng)比較過了,一定不存在重復(fù)。
三.數(shù)論相關(guān)
前言
對于一個大數(shù),直接處理往往是很復(fù)雜的。通常,使用因式分解對大數(shù)進(jìn)行拆分,對每一個質(zhì)因數(shù)進(jìn)行處理
自然數(shù)通常可以寫成質(zhì)因數(shù)相乘的形式
3.1 快速冪
https://blog.csdn.net/qq_19782019/article/details/85621386
這篇文章有非常詳細(xì)地推導(dǎo)過程,最終給出了一個非常簡單的結(jié)論:
最后求出的冪結(jié)果實(shí)際上就是在變化過程中所有當(dāng)指數(shù)為奇數(shù)時底數(shù)的乘積
PS:需要注意的一點(diǎn)是,為了能夠統(tǒng)一算法,也就是核心思想就是每一步都把指數(shù)分成兩半,而相應(yīng)的底數(shù)做平方運(yùn)算,分兩種情況處理:
當(dāng)指數(shù)為偶數(shù)時,指數(shù)減半,底數(shù)平方
當(dāng)指數(shù)為奇數(shù)時,抽出一個底數(shù)的一次方,另外一部分按照情況1處理
3.3 反轉(zhuǎn)
3.3.1 整數(shù)反轉(zhuǎn)(非常重要,涉及溢出)
題目:給你一個 32 位的有符號整數(shù) x ,返回將 x 中的數(shù)字部分反轉(zhuǎn)后的結(jié)果。如果反轉(zhuǎn)后整數(shù)超過 32 位的有符號整數(shù)的范圍 [?2312^{31}231, 2312^{31}231 ? 1] ,就返回 0。假設(shè)環(huán)境不允許存儲 64 位整數(shù)(有符號或無符號)
像反轉(zhuǎn)類的問題,可以馬上想到使用棧來存儲整數(shù)的每一位。當(dāng)然也可以不增加棧這一額外的空間,無論怎樣,反轉(zhuǎn)問題都涉及到兩個專業(yè)名詞,【彈出】, 【推入】
即將整數(shù)當(dāng)前的最后一位彈出整數(shù),再推入反轉(zhuǎn)數(shù)
其表達(dá)式也非常熟悉了:
取模的時候涉及到另外一個問題,負(fù)數(shù)的取模和取余
我熟悉的是兩個整數(shù),取模和取余的結(jié)果是一樣的,余數(shù)是在除數(shù)范圍內(nèi),比較符合直覺。兩個負(fù)數(shù),符號約掉了
當(dāng)符號不一致的時候,高級程序語言就有一些設(shè)定,求模運(yùn)算c = -2(向負(fù)無窮方向舍入),求余運(yùn)算則c = -1(向0方向舍入)a = -7;b = 4, 求模時r = 1,求余時r = -3
當(dāng)符號不一致時,結(jié)果不一樣。求模運(yùn)算結(jié)果的符號和b(除數(shù))一致,求余運(yùn)算結(jié)果的符號和a(被除數(shù))一致
經(jīng)過測試,在C/C++, C#, JAVA, PHP這幾門主流語言中,%運(yùn)算符都是做取余運(yùn)算,而在python中的%是做取模運(yùn)算
取模:商向負(fù)無窮方向,余數(shù)符號和除數(shù)一致
取余:商向0方向,余數(shù)符號和被除數(shù)一致
python確實(shí)特別,尤其是在本題的場景下,如果x<0,那么余數(shù)為正,顯然是無法反映x的最后一位數(shù)字,需要利用算式新余數(shù) = 余數(shù) - 除數(shù) 來計算新的余數(shù)。負(fù)數(shù)取余真的反直覺
python負(fù)數(shù)的取模和整除都需要特別注意,取模不像正數(shù)可以直接得到最后一位,整除會向下取整
還有一點(diǎn)就是負(fù)數(shù)的反轉(zhuǎn),每次取最后一位都是取的負(fù)數(shù),把推入的公式走一遍就清楚了
題目限制了反轉(zhuǎn)數(shù)字的上下限,推入操作可能會造成溢出問題。一方面是x10造成的溢出,一方面是加上彈出的那一個數(shù)字造成的溢出
因此,在進(jìn)行推入之前,需要進(jìn)行兩方面的判斷,只要有一個部分造成溢出,那么就return
通過推導(dǎo)得到判斷是否溢出的條件:
3.3.2 回文數(shù)
題目:給你一個整數(shù) x ,如果 x 是一個回文整數(shù),返回 true ;否則,返回 false 。
回文數(shù)是指正序(從左向右)和倒序(從右向左)讀都是一樣的整數(shù)。例如,121 是回文,而 123 不是。
解法一:將整數(shù)轉(zhuǎn)為字符串
只需要比較字符串的前半段和后半段是否一致即可,但是需要額外的存儲空間
解法二:將數(shù)字本身反轉(zhuǎn)
和之前反轉(zhuǎn)數(shù)字一樣,需要考慮溢出的問題
如果直接進(jìn)行反轉(zhuǎn),很明顯會遇到溢出
于是可以通過只反轉(zhuǎn)一半的數(shù)字來規(guī)避這個問題,反轉(zhuǎn)問題是有序的,存在彈出和推入兩個操作,從數(shù)字的末端開始操作
迭代終止條件的選擇:
對于回文數(shù)而言,迭代過程中,右邊一半是一定小于左邊的,當(dāng)中間數(shù)字被推入右邊,那么右邊就會大于等于左邊,此時就可以終止
回文數(shù)一定是要等到處理中間位置的數(shù)字時才可以進(jìn)行判斷
相較于整數(shù)反轉(zhuǎn),不需要處理負(fù)數(shù)取模確實(shí)簡單了不少
3.3.3 回文排列
題目:給定一個字符串,判斷該字符串中是否可以通過重新排列組合,形成一個回文字符串。
分析:需要分析回文序列的特點(diǎn),在本問題上也就是單詞頻率
奇數(shù)序列:頻率中只有一個是奇數(shù)頻率
偶數(shù)序列:頻率中全部是偶數(shù)頻率
3.4 映射規(guī)則
3.4.1 羅馬字符轉(zhuǎn)數(shù)字
需要建立一張轉(zhuǎn)換表
其次,因為存在4,9等特殊情況,從左到右掃描字符串,判斷當(dāng)前字符和下一位字符代表數(shù)字的大小關(guān)系
其他情況,從左到右相加即可
3.4.2 同構(gòu)字符串
題目:給定兩個字符串 s 和 t,判斷它們是否是同構(gòu)的。
如果 s 中的字符可以按某種映射關(guān)系替換得到 t ,那么這兩個字符串是同構(gòu)的。
每個出現(xiàn)的字符都應(yīng)當(dāng)映射到另一個字符,同時不改變字符的順序。不同字符不能映射到同一個字符上,相同字符只能映射到同一個字符上,字符可以映射到自己本身。
解法:使用兩個哈希表維護(hù)兩個字符串的雙射關(guān)系
3.4.3 單詞規(guī)律
題目:給定一種規(guī)律 pattern 和一個字符串 str ,判斷 str 是否遵循相同的規(guī)律。
這里的 遵循 指完全匹配,例如, pattern 里的每個字母和字符串 str 中的每個非空單詞之間存在著雙向連接的對應(yīng)規(guī)律。
分析:
這題干真抽象。不給示例根本看不懂。其實(shí)就是判斷字符與字符串之間是否恰好對應(yīng)
簡單說就是在兩個哈希表中需要形成這樣的關(guān)系,a:b, b:a, 必須同時滿足
3.5 大數(shù)運(yùn)算
3.5.1 加1
題目:給定一個由 整數(shù) 組成的 非空 數(shù)組所表示的非負(fù)整數(shù),在該數(shù)的基礎(chǔ)上加一。
最高位數(shù)字存放在數(shù)組的首位, 數(shù)組中每個元素只存儲單個數(shù)字。
你可以假設(shè)除了整數(shù) 0 之外,這個整數(shù)不會以零開頭。
需要注意的是,此問題只進(jìn)行加1操作,因此發(fā)生進(jìn)位的情況很有限
1.全為9,那么最前面進(jìn)一位,后面全改0
2.找到9之前第一個非9元素,進(jìn)一位,非9元素后面全改0。此時,非9元素后面一定是全為9的
情況2涵蓋了9零散或者連續(xù)分布的情況
3.5.2 二進(jìn)制求和
題目:給你兩個二進(jìn)制字符串,返回它們的和(用二進(jìn)制表示)。
輸入為 非空 字符串且只包含數(shù)字 1 和 0。
主要是實(shí)現(xiàn)加法的機(jī)制:
carry表示進(jìn)位的值,比如說十進(jìn)制里邊,9+1進(jìn)位為1,那么carry = 1.
對齊末位,每一位的計算結(jié)果為carry + a[i] + b[i] % 2,這個2表示進(jìn)行的2進(jìn)制運(yùn)算。每一位計算的進(jìn)位為carry + a[i] + b[i] // 2.這就是加法運(yùn)算的核心
另外有兩點(diǎn),對齊末位,在程序中通過反轉(zhuǎn)實(shí)現(xiàn)
兩個加數(shù)不一定同樣長,采取高位補(bǔ)0
3.5.3 階乘后的零
題目:給定一個整數(shù) n ,返回 n! 結(jié)果中尾隨零的數(shù)量。
提示 n! = n * (n - 1) * (n - 2) * … * 3 * 2 * 1
解法:
對于任意一個 n! 而言,其尾隨零的個數(shù)取決于展開式中 1010 的個數(shù),而 10可由質(zhì)因數(shù) 2 * 5 而來,因此 n!的尾隨零個數(shù)為展開式中各項分解質(zhì)因數(shù)后「2的數(shù)量」和「5的數(shù)量」中的較小值。
3.6 快樂數(shù)
題目:編寫一個算法來判斷一個數(shù) n 是不是快樂數(shù)。
「快樂數(shù)」定義為:
對于一個正整數(shù),每一次將該數(shù)替換為它每個位置上的數(shù)字的平方和。
然后重復(fù)這個過程直到這個數(shù)變?yōu)?1,也可能是 無限循環(huán) 但始終變不到 1。
如果 可以變?yōu)?1,那么這個數(shù)就是快樂數(shù)。
如果 n 是快樂數(shù)就返回 true ;不是,則返回 false 。
解法:這個題很有意思
據(jù)分析,有三種可能的情況:
通過上面的表格可以看出,4位及以上的數(shù)字通過操作會變回三位數(shù),再操作一次就落到243以內(nèi),也就是說最壞的情況下,算法可能會在 243 以下的所有數(shù)字上循環(huán),然后回到它已經(jīng)到過的一個循環(huán)或者回到 1,不會無止境的變大
也就是說序列不是成環(huán)就是變成1的循環(huán)
之前我們做過循環(huán)鏈表的判定,可以使用雙指針法(快慢)來判斷是否成環(huán)。區(qū)別在于,循環(huán)鏈表在判斷之前已經(jīng)存在,本題需要在生成序列的過程中判斷是否成環(huán)
或者說本題不需要生成序列,因為后面的節(jié)點(diǎn)由前面的節(jié)點(diǎn)生成,只需要更新雙指針的值即可。
3.7 素數(shù)
3.7.1 計數(shù)素數(shù)
經(jīng)典的素數(shù)篩問題哈
首先是素數(shù)的定義,在正整數(shù)范圍內(nèi),大于1并且只能被1和自身整除的數(shù)
法一:遍歷 2-n-1的所有數(shù),看n是否能被整除
法二:對法一的范圍進(jìn)行優(yōu)化,優(yōu)化到 2-sqrt(n)的范圍
PS:從法三開始,需要準(zhǔn)備一個數(shù)組,保存i是否為素數(shù)的結(jié)果,默認(rèn)全為素數(shù)。從法三開始,將法一法二的除法問題,變成了從小至大的乘法問題
官方解釋
法三:埃氏篩
前兩種方法求解每個數(shù)是否是質(zhì)數(shù)的操作都是獨(dú)立的,存在大量重復(fù)計算。該問題的基本開銷在于判斷一個數(shù)是否是質(zhì)數(shù),時間復(fù)雜度基于總共需要做多少次這樣的操作
埃氏篩基于這樣一個事實(shí),如果 x是質(zhì)數(shù),那么大于 x的 x的倍數(shù) 2x,3x, 一定不是質(zhì)數(shù)。
介紹什么是合數(shù):
因此,我們設(shè)isPrime[i]表示數(shù) i 是不是質(zhì)數(shù),如果是質(zhì)數(shù)則為1,否則為0。從小到大遍歷每個數(shù),如果這個數(shù)為質(zhì)數(shù),則將其所有的倍數(shù)都標(biāo)記為合數(shù)(除了該質(zhì)數(shù)本身),即0,這樣在運(yùn)行結(jié)束的時候我們即能知道質(zhì)數(shù)的個數(shù)。
法四:優(yōu)化了法三標(biāo)記合數(shù)的起點(diǎn),法三從2x開始,實(shí)際上應(yīng)該從xx開始標(biāo)記合數(shù)。因為比x小的因數(shù)一定在之前就標(biāo)記過了。比如74,這一看47是不存在的,因為4不是質(zhì)數(shù),但是4是合數(shù),可變形為2*14,這就肯定標(biāo)記過了
法五:線性篩\歐拉篩
按照法三的方法,45可以寫成315或者59,他們顯然是相等的,符合法三要求的在x*x之后,但是這屬于冗余的操作
在邏輯上有很大的改動:
(1)單獨(dú)維護(hù)一個數(shù)組,只存儲已經(jīng)篩出的素數(shù)
(2)標(biāo)記合數(shù)要針對每一個數(shù),只標(biāo)記質(zhì)數(shù)集合中每個元素與 x相乘的結(jié)果,
說實(shí)話,這個流程一走下來,我立馬就想到了DP,果然可以使用dp的思想
基于DP
法六:
3.8 各位相加
題目:給定一個非負(fù)整數(shù) num,反復(fù)將各個位上的數(shù)字相加,直到結(jié)果為一位數(shù)。
分析:這是之前數(shù)字反轉(zhuǎn)問題的進(jìn)階版,數(shù)字反轉(zhuǎn)需要掌握每一位的推入和彈出。
官方給了一種魔法般地數(shù)學(xué)解釋,只能說牛逼
3.9 丑數(shù)
題目:丑數(shù) 就是只包含質(zhì)因數(shù) 2、3 和 5 的正整數(shù)。
給你一個整數(shù) n ,請你判斷 n 是否為 丑數(shù) 。如果是,返回 true ;否則,返回 false 。
分析:
首先,在大數(shù)運(yùn)算上,將大數(shù)表示成冪的乘積的形式非常普遍,這是一個重要思路。另外,在程序上,n反復(fù)除以2,3,5是我一開始沒想到的
while n % factor == 0:
n //= factor
四.棧
4.1 有效的括號
題目:給定一個只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判斷字符串是否有效。
有效字符串需滿足:
左括號必須用相同類型的右括號閉合。
左括號必須以正確的順序閉合。
特殊情況:空字符串在題干要求下為真
觀察字符串,左右括號是不會混亂排列的,最右邊的左括號最先匹配,最左邊的右括號最先匹配。后入先出的這種模式就可以使用棧這種數(shù)據(jù)結(jié)構(gòu)
五.線性表和鏈表
前言
模式識別:需要移動左右兩頭的問題可以考慮雙指針
5.1 指針
前言:雙指針問題需要考慮清楚兩個指針分別在什么情況下移動,怎樣移動。fast指針會將每個元素都遍歷到,因此就不要考慮slow指針是否也要判斷元素,不需要,slow指針只表示位置
5.1.1 合并兩個有序鏈表
題目:將兩個升序鏈表合并為一個新的 升序 鏈表并返回。新鏈表是通過拼接給定的兩個鏈表的所有節(jié)點(diǎn)組成的。
很容易想到,使用雙指針法來解決,但不想線性表,鏈表要求不增加額外的存儲空間
特殊情況:一個鏈表為空的情況
在處理鏈表問題時,總是增加一個頭結(jié)點(diǎn)來統(tǒng)一插入、刪除等操作,這樣在處理邊界的時候可以更方便,不需要額外的代碼
這里需要自己定義鏈表結(jié)點(diǎn)這一數(shù)據(jù)機(jī)構(gòu)
雙指針法的核心思路是:永遠(yuǎn)只比較兩個指針?biāo)赶虻脑?#xff0c;結(jié)果符合要求的那個指針向后移動
5.1.1 合并兩個有序數(shù)組
題目:給你兩個按 非遞減順序 排列的整數(shù)數(shù)組 nums1 和 nums2,另有兩個整數(shù) m 和 n ,分別表示 nums1 和 nums2 中的元素數(shù)目。
請你 合并 nums2 到 nums1 中,使合并后的數(shù)組同樣按 非遞減順序 排列。
注意:最終,合并后數(shù)組不應(yīng)由函數(shù)返回,而是存儲在數(shù)組 nums1 中。為了應(yīng)對這種情況,nums1 的初始長度為 m + n,其中前 m 個元素表示應(yīng)合并的元素,后 n 個元素為 0 ,應(yīng)忽略。nums2 的長度為 n 。
在解法上,雙指針法已經(jīng)非常熟悉了。針對本題,因為數(shù)組1預(yù)留了包函數(shù)組2的存儲空間,所以采用從后往前掃描的雙指針法可以不增加額外的存儲空間
注意,雙指針法那個指針符合條件,那個指針才移動
嚴(yán)格來說,上述反向進(jìn)行的遍歷需要3個指針
5.1.2 刪除排序數(shù)組中的重復(fù)項
題目:給你一個有序數(shù)組 nums ,請你 原地 刪除重復(fù)出現(xiàn)的元素,使每個元素 只出現(xiàn)一次 ,返回刪除后數(shù)組的新長度。
不要使用額外的數(shù)組空間,你必須在 原地 修改輸入數(shù)組 并在使用 O(1) 額外空間的條件下完成。
法一:
對于有序數(shù)組,如果允許額外存儲,那么可以直接遍歷數(shù)組,只通過相鄰兩個元素的比較結(jié)果進(jìn)行操作,不同則記錄,相同則跳過
但不允許額外存儲的條件下呢?
設(shè)置快慢指針,慢指針負(fù)責(zé)指出可覆蓋位置,快指針負(fù)責(zé)遍歷數(shù)組
從下標(biāo)為1的位置開始比較是因為,如果重復(fù),至少是從下標(biāo)為1的位置開始覆蓋。另外,比較相鄰元素取的是通過后一個元素與前一個元素比較,也就是fast 和 fast-1比較
同時,這樣的設(shè)計背景下,慢指針是可以充當(dāng)新數(shù)組的長度,也就是遍歷原數(shù)組slow長度即是沒有重復(fù)元素的新數(shù)組
特殊的,需要考慮數(shù)組長度為0的情況
5.1.2 刪除排序鏈表中的重復(fù)元素
題目:存在一個按升序排列的鏈表,給你這個鏈表的頭節(jié)點(diǎn) head ,請你刪除所有重復(fù)的元素,使每個元素 只出現(xiàn)一次 。
返回同樣按升序排列的結(jié)果鏈表。
相較于數(shù)組,刪除重復(fù)元素之后需要覆蓋,對于鏈表只需要重新鏈接即可,也不需要頭結(jié)點(diǎn)來統(tǒng)一操作
不要怕鏈表的這些操作
5.1.3 移除元素
題目:給你一個數(shù)組 nums 和一個值 val,你需要 原地 移除所有數(shù)值等于 val 的元素,并返回移除后數(shù)組的新長度。
不要使用額外的數(shù)組空間,你必須僅使用 O(1) 額外空間并 原地 修改輸入數(shù)組。
元素的順序可以改變。你不需要考慮數(shù)組中超出新長度后面的元素。
法一:
還是使用5.1.2的方法,由fast指針遍歷數(shù)組,如果該元素不是目標(biāo),則寫到slow的位置,slow和fast都向前移動
否則,fast向前移動
這一方法我覺得別扭的地方在于,假設(shè)有連續(xù)的元素都不是目標(biāo),但是還是需要寫一遍。可以這樣,判斷fast和slow是否相等,實(shí)際上只要出現(xiàn)一次目標(biāo),fast和slow必發(fā)生錯位
法二:
產(chǎn)生的背景是,如果要移除的元素恰好在數(shù)組的開頭,可以直接將最后一個元素 55 移動到序列開頭,取代元素 1
這個優(yōu)化在序列中val 元素的數(shù)量較少時非常有效
此方法,雙指針在一次循環(huán)中只有一個指針會發(fā)生移動,由left遍歷數(shù)組,如果發(fā)現(xiàn)目標(biāo),right向左移動,left不動
否則,沒發(fā)現(xiàn)目標(biāo),left向右移動
這樣感覺就消除了前面非目標(biāo)連續(xù)元素的問題
5.1.4 驗證回文串
題目:給定一個字符串,驗證它是否是回文串,只考慮字母和數(shù)字字符,可以忽略字母的大小寫。
說明:本題中,我們將空字符串定義為有效的回文串。
法一:反轉(zhuǎn)字符串
字符串里面可能有空格,標(biāo)點(diǎn)符號,他并不是簡單地abba這種形式
因為涉及到兩個指針的移動,極端情況下指針會移動到串尾,因此每次移動完之后都需要判斷指針位置是否符合條件
5.1.5 環(huán)形鏈表
題目:給定一個鏈表,判斷鏈表中是否有環(huán)。
如果鏈表中有某個節(jié)點(diǎn),可以通過連續(xù)跟蹤 next 指針再次到達(dá),則鏈表中存在環(huán)。 為了表示給定鏈表中的環(huán),我們使用整數(shù) pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環(huán)。注意:pos 不作為參數(shù)進(jìn)行傳遞,僅僅是為了標(biāo)識鏈表的實(shí)際情況。
如果鏈表中存在環(huán),則返回 true 。 否則,返回 false 。
解法:雙指針法
利用快慢指針,慢指針一次走一步,快指針一次走兩步
比較反直覺的一點(diǎn)是,快指針有可能會超過慢指針,但總會相遇在同一個位置
如果說不存在環(huán),則快指針一定先到達(dá)鏈表尾部,要么本身為空,要么next為空
5.1.6 回文鏈表
分析:我選擇最簡單的一種方式,把節(jié)點(diǎn)的值取出來,就等價于處理回文數(shù)組了,經(jīng)典的雙指針法
實(shí)際上,可以直接使用快慢指針法,但是要麻煩許多
1.通過快慢指針的慢指針將鏈表后半段反轉(zhuǎn)
2.判斷是否回文
3.通過快指針恢復(fù)鏈表
5.1.7 刪除鏈表中的節(jié)點(diǎn)
題目:請編寫一個函數(shù),用于 刪除單鏈表中某個特定節(jié)點(diǎn) 。在設(shè)計函數(shù)時需要注意,你無法訪問鏈表的頭節(jié)點(diǎn) head ,只能直接訪問 要被刪除的節(jié)點(diǎn) 。
題目數(shù)據(jù)保證需要刪除的節(jié)點(diǎn) 不是末尾節(jié)點(diǎn) 。
分析:可惜,沒有注意到待刪除節(jié)點(diǎn)不是末尾結(jié)點(diǎn)
先將待刪除節(jié)點(diǎn)的val變成下一個節(jié)點(diǎn)的val,再刪除下一個節(jié)點(diǎn)。鏈表節(jié)點(diǎn)的信息也就兩個嘛,val和next
5.2 最后一個單詞的長度
題目:給你一個字符串 s,由若干單詞組成,單詞前后用一些空格字符隔開。返回字符串中最后一個單詞的長度。
單詞 是指僅由字母組成、不包含任何空格字符的最大子字符串。
反向遍歷即可
因為題干已經(jīng)說明,字符串至少含有一個單詞,也就不需要考慮沒有單詞的情況
5.3 雙指針
5.3.1 相交鏈表
題目:給你兩個單鏈表的頭節(jié)點(diǎn) headA 和 headB ,請你找出并返回兩個單鏈表相交的起始節(jié)點(diǎn)。如果兩個鏈表沒有交點(diǎn),返回 null 。
圖示兩個鏈表在節(jié)點(diǎn) c1 開始相交:
題目數(shù)據(jù) 保證 整個鏈?zhǔn)浇Y(jié)構(gòu)中不存在環(huán)。
注意,函數(shù)返回結(jié)果后,鏈表必須 保持其原始結(jié)構(gòu) 。
解法:雙指針
雙指針的更新策略非常巧妙,如果能讓兩個指針齊頭并進(jìn),那么他們一定能同時到達(dá)相交點(diǎn)。但是兩個單鏈表的長度不一樣
但是,兩個鏈表共享其中一段,只有交點(diǎn)前面一段不一樣。如果說一個指針走過自己這一段,再走另一個指針走的那一段,以交點(diǎn)為界,那么總體來講,兩個指針就走過了相等的長度
5.3.2 反轉(zhuǎn)鏈表
題目:給你單鏈表的頭節(jié)點(diǎn) head ,請你反轉(zhuǎn)鏈表,并返回反轉(zhuǎn)后的鏈表。
解析:嚴(yán)格來說應(yīng)該是3個指針,需要同時記錄當(dāng)前節(jié)點(diǎn),前一節(jié)點(diǎn),后一節(jié)點(diǎn)的地址,也就是鏈表操作中的“先連后斷”, 連接之后再更新指針
5.3.3 匯總區(qū)間
題目:給定一個無重復(fù)元素的有序整數(shù)數(shù)組 nums 。
返回 恰好覆蓋數(shù)組中所有數(shù)字 的 最小有序 區(qū)間范圍列表。
nums = [0,1,2,4,5,7]
[“0->2”,“4->5”,“7”]
分析:經(jīng)典的雙指針法。重點(diǎn)是找到high的位置。判斷條件在題干中分析可以得出。low和high分別指向區(qū)間的左右。
5.3.4 會議室
題目:給定一個會議時間安排的數(shù)組 intervals ,每個會議時間都會包括開始和結(jié)束的時間 intervals[i] = [starti, endi] ,請你判斷一個人是否能夠參加這里面的全部會議。
分析:核心在于判斷區(qū)間是否有重疊。重點(diǎn)是要先進(jìn)行排序,笑死
5.3.5 移動零
題目:給定一個數(shù)組 nums,編寫一個函數(shù)將所有 0 移動到數(shù)組的末尾,同時保持非零元素的相對順序。
分析:數(shù)組要進(jìn)行元素的按值移動還是比較困難的。使用雙指針法處理,要對兩個指針的功能和操作定義清楚
left: 左邊為非零數(shù),一直指向已處理好的序列的尾部
right: 左邊直至left為止,為0,一直指向待處理序列的頭部
什么是處理好的??不含0
什么是待處理的??含0
總的來說,就是將left的0與right的非0進(jìn)行交換,left找0,right找非0
這樣說還是挺抽象的,具象一點(diǎn):
我一直覺得別扭的地方在于,如果從一開始直接遇到情況1,不就意味著left和right都指向同一個元素嗎??他們有必要進(jìn)行交換嗎??
A:這是一種必要的開銷。只要進(jìn)行過一次0的移動,left和right必然會錯開。當(dāng)然,如果從開頭就是連續(xù)的非0,那么left和right的確會發(fā)生看起來沒必要的交換
5.4 棧和隊列
5.4.1 用隊列實(shí)現(xiàn)棧
題目:請你僅使用兩個隊列實(shí)現(xiàn)一個后入先出(LIFO)的棧,并支持普通棧的全部四種操作(push、top、pop 和 empty)。
分析:假設(shè)使用兩個隊列來模擬棧
入棧:隊列1存儲當(dāng)前的棧內(nèi)元素,并是指指針指向隊首元素;隊列2存儲即將入棧的元素
將隊列1的元素出隊列1,入隊列2,并交換兩個隊列以維持上述的定義,更新指針
其他操作基于隊列1完成即可,主要就是通過兩個隊列實(shí)現(xiàn)入棧操作
5.4.3 用棧實(shí)現(xiàn)隊列
分析:同樣的序列分別推入棧和隊列,彈出序列恰好相反,那么將棧的彈出序列再推入另一個棧,再彈出,不就和隊列的彈出序列相同了嗎
基于這一點(diǎn),可以維護(hù)兩個棧倆實(shí)現(xiàn)隊列的效果。棧1負(fù)責(zé)彈出,棧2負(fù)責(zé)推入。
出棧:如果棧1不空,則彈出棧頂元素;如果棧1為空,將棧2元素全部彈出并推入棧1,再彈出棧頂元素
5.5 區(qū)域和檢索 - 數(shù)組不可變
題目:給定一個整數(shù)數(shù)組 nums,求出數(shù)組從索引 i 到 j(i ≤ j)范圍內(nèi)元素的總和,包含 i、j 兩點(diǎn)。
實(shí)現(xiàn) NumArray 類:
NumArray(int[] nums) 使用數(shù)組 nums 初始化對象
int sumRange(int i, int j) 返回數(shù)組 nums 從索引 i 到 j(i ≤ j)范圍內(nèi)元素的總和,包含 i、j 兩點(diǎn)(也就是 sum(nums[i], nums[i + 1], … , nums[j]))
分析:當(dāng)數(shù)組確定,使用函數(shù)多次求解區(qū)間和是沒有必要的。每次求解區(qū)間和都需要遍歷,在初始化的時候可以一次性求出階梯和,階梯和通過裁切可以得到區(qū)間和
階梯和怎么求???
A:創(chuàng)建一個存儲階梯和的數(shù)組,先加入元素0,遍歷原數(shù)組,每次選擇一個元素和階梯和數(shù)組的[-1]位相加
六.動態(tài)規(guī)劃
6.1 最大子序列和
題目:給定一個整數(shù)數(shù)組 nums ,找到一個具有最大和的連續(xù)子數(shù)組(子數(shù)組最少包含一個元素),返回其最大和。
我的問題在于對狀態(tài)方程的定義,我的定義是:
f(i)表示到第i個元素為止的數(shù)組的最大子序和,由此寫出的狀態(tài)轉(zhuǎn)移方程是,f(i) = max( f[i-1], f[i-1] + nums[i] )
正確的定義是:
f(i) 代表以第 i 個數(shù)結(jié)尾的「連續(xù)子數(shù)組的最大和, 因此有
f(i)=max{f(i?1)+nums[i],nums[i]}
我的定義就不滿足連續(xù)子序列的連續(xù),對于第i個元素,只有兩種狀態(tài),要么被包含進(jìn)子序列,要么不被包含,自成一個序列。之所以只有兩種狀態(tài),還是因為對f(i)的定義,必須要以第i個元素結(jié)尾
這也是我最迷惑的地方,子序列的位置是不固定的,這種不固定就導(dǎo)致有些反直覺。比如說對于數(shù)組[1,2,3,-4,5,6]。按照正確的定義,f(3)的最大子序和是2,但我總感覺應(yīng)該是6
我做了這樣一個嘗試,將數(shù)組[1,2,3,-4]按照上述兩種定義方式寫出求解f(i)時涉及到的子序列,我發(fā)現(xiàn),按照我的定義,f[i-1] + nums[i] 不一定成立,因為f(i-1)表示的最大子序列的最后一個元素不一定是nums[i-1]
按照正確的定義,f(i?1)+nums[i] 和 nums[i] 確實(shí)能夠形成兩個連續(xù)的序列
寫出數(shù)組[1,2,3,-4]按照正確定義涉及到的連續(xù)子序列,我發(fā)現(xiàn)實(shí)際上它是包含了數(shù)組的所有連續(xù)子序列
我想應(yīng)該這樣總結(jié),通過計算所有以第i個元素結(jié)尾的連續(xù)序列,最終我們就計算了全部的連續(xù)序列,進(jìn)而就可以求得其中和最大的連續(xù)序列
通過測試,不能將f(i-1)寫入狀態(tài)轉(zhuǎn)移方程,他的值總是最大的
6.2 爬樓梯
假設(shè)你正在爬樓梯。需要 n 階你才能到達(dá)樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數(shù)。
6.3 楊輝三角
題目:給定一個非負(fù)整數(shù) numRows,生成「楊輝三角」的前 numRows 行。
在「楊輝三角」中,每個數(shù)是它左上方和右上方的數(shù)的和。
生成楊輝三角的原理非常簡單,只是說一般分析是寫成等腰三角形,但寫成程序,就寫成直角三角形,以此來確定計算關(guān)系
6.4 楊輝三角2
思路同上,理解了如何生成楊輝三角即可
6.5 買賣股票的最佳時機(jī)
題目:給定一個數(shù)組 prices ,它的第 i 個元素 prices[i] 表示一支給定股票第 i 天的價格。
你只能選擇 某一天 買入這只股票,并選擇在 未來的某一個不同的日子 賣出該股票。設(shè)計一個算法來計算你所能獲取的最大利潤。
返回你可以從這筆交易中獲取的最大利潤。如果你不能獲取任何利潤,返回 0
解法:典型的動態(tài)規(guī)劃,在這個問題上,注意,只進(jìn)行一次交易
如果寫出如同分析“最大子序和”那樣的數(shù)對,感覺很難判斷是否包含了所有的選擇。在上面的分析中,對f(i)的定義實(shí)際上是前i-1個數(shù)中的最小值,當(dāng)然,硬要說的話,將其定義為到第i個數(shù)的最大收益也不是不行。主要是要想清楚最大收益產(chǎn)生于什么時候
6.6 買賣股票的最佳時機(jī)2
題目:給定一個數(shù)組 prices ,其中 prices[i] 是一支給定股票第 i 天的價格。
設(shè)計一個算法來計算你所能獲取的最大利潤。你可以盡可能地完成更多的交易(多次買賣一支股票)。
注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
解析:這一次就能進(jìn)行多次交易,但是交易不能重疊
收集所有上行區(qū)段的利潤即可
6.7 比特位計數(shù)
題目:給你一個整數(shù) n ,對于 0 <= i <= n 中的每個 i ,計算其二進(jìn)制表示中 1 的個數(shù) ,返回一個長度為 n + 1 的數(shù)組 ans 作為答案。
分析:
對于2的冪,已經(jīng)知道可以通過x&(x-1)來判斷。要計算i的二進(jìn)制表示中1的個數(shù),可以反復(fù)進(jìn)行與運(yùn)算。但是以前就知道 i 中1的個數(shù)和處于其之前的2的冪有關(guān)
那就可以使用DP的思想來處理,首先定義dp[i] 為數(shù)i的1的個數(shù)
實(shí)際上可以構(gòu)造這樣的狀態(tài)轉(zhuǎn)移方程:
dp[i] = dp[i - 當(dāng)前最大2的冪] + dp[當(dāng)前最大2的冪]
dp[2的冪] 都為1,因此可以將上式表示為:
dp[i] = dp[i - 當(dāng)前最大2的冪] + 1
七. 樹
前言
先講講dfs,這個算法我接觸的比較早,算法思想也符合我的思維方式。但是具體到代碼層面,就和我的理解出現(xiàn)了偏差。
我的理解是,棧里面存的應(yīng)該是一條可回溯的路徑,比如說對于這樣一張無向圖
我認(rèn)為最開始棧里邊存的是A C D,從A開始按照規(guī)律向相連的最深處遍歷,當(dāng)D沒有相鄰邊之后,回溯到C,看C還有沒有未訪問的相鄰邊,再回溯到A
很明顯,對于每一個節(jié)點(diǎn)我們需要訪問到相鄰節(jié)點(diǎn),如果暫時只取一個節(jié)點(diǎn)推入棧,那么應(yīng)該以什么方式選擇呢?等到回溯至此的時候,又以同樣的方式選擇一個節(jié)點(diǎn)推入棧嗎?可想而知有多混亂
我想我產(chǎn)生這種想法的理由是,我總是認(rèn)為棧里面應(yīng)該存dfs的路徑。但是是否可以這樣,通過棧頂元素的彈出來確定dfs的路徑呢?棧里面存儲節(jié)點(diǎn),每次遍歷當(dāng)前節(jié)點(diǎn)的所有相鄰節(jié)點(diǎn),推入沒有訪問過得節(jié)點(diǎn),同時更新訪問數(shù)組,記錄已經(jīng)訪問的節(jié)點(diǎn)。所以棧里面沒有重復(fù)元素,都是唯一的,這樣也就保證了訪問不會重復(fù)
為什么這種方法可以達(dá)到dfs的效果?通過上述方法,相當(dāng)于每彈出一個節(jié)點(diǎn),就會推入與之相鄰的未訪問節(jié)點(diǎn)。那么,彈出元素之間的關(guān)系就是層層向深處推進(jìn)的關(guān)系,a[i-1]和a[i]相鄰,a[i]和a[i+1]相鄰
stack數(shù)組是用來達(dá)到dfs的核心,visit數(shù)組是輔助stack,保證不重復(fù)
樹的遍歷區(qū)別于圖,我們對樹的遍歷的順序做了規(guī)范,先序中序后序。
如果說使用dfs思想遍歷二叉樹,將節(jié)點(diǎn)推入stack默認(rèn)順序為先左子樹再右子樹,那么遍歷順序就等價于“根右左”
關(guān)于遞歸回溯的理解
7.1 樹的遍歷
7.1.1 二叉樹的中序遍歷
題目:給定一個二叉樹的根節(jié)點(diǎn) root ,返回它的 中序 遍歷。
這是個老問題了,一般考慮使用遞歸,比較好理解
遞歸都可以改成迭代,也就是循環(huán)
兩種方式是等價的,區(qū)別在于遞歸的時候隱式地維護(hù)了一個棧,(一直朝左子樹的最深處,到時候要回退)而我們在迭代的時候需要顯式地將這個棧模擬出來,其他都相同
以回溯為重點(diǎn)考慮的話,是否可以這樣理解
1.root不空,則一直尋找最深的左子樹為空的節(jié)點(diǎn)
2.輸出這個節(jié)點(diǎn),將其作為新的根
3.如果root的右子樹為空則回溯,即彈出棧頂元素如果root的右子樹不為空,則重復(fù)上述過程
但是在代碼層面并不會直接去判斷root的右子樹是否為空,核心代碼會先找最深、且左子樹為空的節(jié)點(diǎn),然后將root更新為該節(jié)點(diǎn)的右節(jié)點(diǎn)
感覺這塊有點(diǎn)亂,TNND
7.1.2 相同的樹
題目:判斷兩棵樹是否相同
既要進(jìn)行數(shù)值比較,又要進(jìn)行結(jié)構(gòu)比較
乍一看,似乎個圖算法用到bfs和dfs一樣,但是兩棵樹就需要用到兩個棧或者兩個隊列
dfs可以用遞歸來做,遞歸就需要遞歸出口和遞歸表達(dá)式
遞歸出口的范式一般是:如果…,返回…
如果兩個跟都為空則相同
如果其中一個為空則不同
如果兩個的值不等則不同
數(shù)值和結(jié)構(gòu)的比較都包含了
遞歸表達(dá)式基于樹的這種分形結(jié)構(gòu),根比較完之后,就要確定左子樹和右子樹的情況
對于dfs的迭代版本,需要考慮循環(huán)條件,循環(huán)中何時退出。毫無疑問,只有整個迭代完成才能判斷兩棵樹相同
7.1.3 對稱二叉樹
題目:給定一個二叉樹,檢查它是否是鏡像對稱的。
總結(jié)一下,先序中序后序遍歷二叉樹通常使用遞歸來實(shí)現(xiàn),本質(zhì)上使用的是dfs思想,遞歸隱式地維護(hù)了一個棧。之后“相同的樹”這一題也是用遞歸解決,那么實(shí)際上遞歸通過不同的結(jié)構(gòu)可以改變遍歷的順序
因此,熟悉遞歸的結(jié)構(gòu)就尤為重要
遞歸出口:考慮高度為2的二叉樹
這個問題和“相同的樹”比較類似
7.1.4 二叉樹的最大深度
題目:給定一個二叉樹,找出其最大深度。
二叉樹的深度為根節(jié)點(diǎn)到最遠(yuǎn)葉子節(jié)點(diǎn)的最長路徑上的節(jié)點(diǎn)數(shù)。
還是可以沿用上述的dfs思想,如果我們知道了左子樹和右子樹的最大深度 l 和 r,那么該二叉樹的最大深度即為max(l,r)+1,而左子樹和右子樹的最大深度又可以以同樣的方式進(jìn)行計算。因此我們可以用「深度優(yōu)先搜索」的方法來計算二叉樹的最大深度。具體而言,在計算當(dāng)前二叉樹的最大深度時,可以先遞歸計算出其左子樹和右子樹的最大深度,然后在 O(1) 時間內(nèi)計算出當(dāng)前二叉樹的最大深度。遞歸在訪問到空節(jié)點(diǎn)時退出。
細(xì)品為何在root為空時返回0:明顯,root為空,這一層不存在,當(dāng)然返回0;但是root不為空,這一層肯定就是+1.所以總體來看它是一層一層進(jìn)行的計算
7.1.5 將有序數(shù)組轉(zhuǎn)換為二叉搜索樹
題目:給你一個整數(shù)數(shù)組 nums ,其中元素已經(jīng)按 升序 排列,請你將其轉(zhuǎn)換為一棵 高度平衡 二叉搜索樹。
高度平衡 二叉樹是一棵滿足「每個節(jié)點(diǎn)的左右兩個子樹的高度差的絕對值不超過 1 」的二叉樹。
法一:遞歸
首先是BST二叉搜索樹的定義,從節(jié)點(diǎn)的值的角度出發(fā),根比左子樹的要大,比右子樹的要小
7.1.6 平衡二叉樹
題目:給定一個二叉樹,判斷它是否是高度平衡的二叉樹。
本題中,一棵高度平衡二叉樹定義為:
一個二叉樹每個節(jié)點(diǎn) 的左右兩個子樹的高度差的絕對值不超過 1 。
解法:不是要構(gòu)造平衡二叉樹。平衡二叉樹的定義是,任意結(jié)點(diǎn)的左右子樹的高度差不超過1
仍然是用遞歸的方式,核心在于如何判斷是否平衡
第二點(diǎn)不容易想到
7.1.7 二叉樹的最小深度
題目:給定一個二叉樹,找出其最小深度。
最小深度是從根節(jié)點(diǎn)到最近葉子節(jié)點(diǎn)的最短路徑上的節(jié)點(diǎn)數(shù)量。
說明:葉子節(jié)點(diǎn)是指沒有子節(jié)點(diǎn)的節(jié)點(diǎn)。
又是一個基于DFS,使用遞歸完成的問題
遞歸出口有3個:
遞歸表達(dá)式為:
如果存在子樹,則將其作為根,返回其高度和最小深度的比較結(jié)果,取更小的那個更新最小深度
7.1.8 路徑總和
題目:
給你二叉樹的根節(jié)點(diǎn) root 和一個表示目標(biāo)和的整數(shù) targetSum ,判斷該樹中是否存在 根節(jié)點(diǎn)到葉子節(jié)點(diǎn) 的路徑,這條路徑上所有節(jié)點(diǎn)值相加等于目標(biāo)和 targetSum 。
葉子節(jié)點(diǎn) 是指沒有子節(jié)點(diǎn)的節(jié)點(diǎn)。
解法:
還是一樣,使用DFS的思想,用遞歸來完成
遞歸出口有兩個:
遞歸表達(dá)式:
如果根存在子樹,那么需要看左子樹或者右子樹是否存在滿足條件的情況
7.1.8 二叉樹的所有路徑
題目:給你一個二叉樹的根節(jié)點(diǎn) root ,按 任意順序 ,返回所有從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的路徑。
葉子節(jié)點(diǎn) 是指沒有子節(jié)點(diǎn)的節(jié)點(diǎn)。
分析:老辦法,DFS+遞歸實(shí)現(xiàn)。
遞歸出口要理解還是很容易的,但是在代碼上有些我感覺反直覺的東西
就是說當(dāng)開始遍歷root的右子樹的時候,為什么不會在路徑上出現(xiàn)左子樹的節(jié)點(diǎn)。在我的直覺上,我認(rèn)為會出現(xiàn)左子樹的節(jié)點(diǎn)???
A:其原因在于遞歸函數(shù)的參數(shù),我們傳遞進(jìn)去的當(dāng)前額路徑path。在上述代碼中,else塊的兩個實(shí)參path是一樣的,此時意味著在進(jìn)行分叉,而分叉之前的路徑是一樣的。舉個簡單的例子,形如,
通過手動推算,開始訪問root 1的左子樹和右子樹的path都是1->,然后進(jìn)入遞歸
7.1.9 翻轉(zhuǎn)二叉樹
題目:翻轉(zhuǎn)一棵二叉樹。
分析:將一顆二叉樹進(jìn)行鏡像翻轉(zhuǎn)。二叉樹問題普遍需要遍歷,遍歷通常又是用遞歸來實(shí)現(xiàn)。
遞歸出口我總結(jié)下來,需要考慮最簡單地情況:
1.根不存在
2.根存在
3.根的子樹不存在子樹了
如上,最多到高度為二的二叉樹就能把出口描述清楚,感覺還是很微妙
之前有道題,相同二叉樹,需要判斷一棵樹是否對稱,思路類似
7.2 二叉搜索樹的最近公共祖先
題目:給定一個二叉搜索樹, 找到該樹中兩個指定節(jié)點(diǎn)的最近公共祖先。
百度百科中最近公共祖先的定義為:“對于有根樹 T 的兩個結(jié)點(diǎn) p、q,最近公共祖先表示為一個結(jié)點(diǎn) x,滿足 x 是 p、q 的祖先且 x 的深度盡可能大(一個節(jié)點(diǎn)也可以是它自己的祖先)。”
例如,給定如下二叉搜索樹: root = [6,2,8,0,4,7,9,null,null,3,5]、
分析:
首先是二叉搜索樹的定義,也叫二叉排序樹。我們通過遞歸定義二叉排序樹,先構(gòu)造新的節(jié)點(diǎn),若root為空,則直接插入;否則,若val小于根的val,則插入左子樹,否則插入右子樹
一次遍歷真的很秀,很優(yōu)雅
(1)p, q不存在咋辦?
不可能。題干說了,樹中指定的兩個節(jié)點(diǎn),找就完事了
(2)p,q在什么時候分叉?
首先,他們一定會分叉。根據(jù)bst的性質(zhì),分叉的節(jié)點(diǎn)一個比直接雙親大,一個小。這也就意味著,當(dāng)root不能同時大于或者小于兩點(diǎn),則該root為最近的公共祖先。從此刻開始,兩個節(jié)點(diǎn)分道揚(yáng)鑣
7.3 最接近的二叉搜索樹值
題目:給定一個不為空的二叉搜索樹和一個目標(biāo)值 target,請在該二叉搜索樹中找到最接近目標(biāo)值 target 的數(shù)值。
注意:
給定的目標(biāo)值 target 是一個浮點(diǎn)數(shù)
題目保證在該二叉搜索樹中只會存在一個最接近目標(biāo)值的數(shù)
分析:
我最開始只有一點(diǎn)不確定,是否需要遍歷所有節(jié)點(diǎn)????
A:不需要。通過數(shù)軸可以確定。如果target < root.val, 那么最接近的值一定在root的左子樹。
反證法證明:如果在右子樹,那么這個值一定大于root.val,而root.val才是最接近的值。矛盾
八. 位運(yùn)算
8.1 只出現(xiàn)一次的數(shù)字
題目:給定一個非空整數(shù)數(shù)組,除了某個元素只出現(xiàn)一次以外,其余每個元素均出現(xiàn)兩次。找出那個只出現(xiàn)了一次的元素。
說明:
你的算法應(yīng)該具有線性時間復(fù)雜度。 你可以不使用額外空間來實(shí)現(xiàn)嗎?
利用了異或運(yùn)算的交換律。異或運(yùn)算,相同為0,不同為1,另外,a^a = 0
0 ^ a = a。0就相當(dāng)于與乘法運(yùn)算中的1
8.2 Excel表列名稱
題目:給你一個整數(shù) columnNumber ,返回它在 Excel 表中相對應(yīng)的列名稱。
例如:
A -> 1
B -> 2
C -> 3
…
Z -> 26
AA -> 27
AB -> 28
解法:應(yīng)該想到他的模式,每26位要進(jìn)一位。進(jìn)一步想到考察進(jìn)位機(jī)制
本質(zhì)上就是10進(jìn)制轉(zhuǎn)26進(jìn)制,那就是 :除進(jìn)制取余倒排
有點(diǎn)反直覺的地方在于實(shí)現(xiàn)數(shù)字和字符串的轉(zhuǎn)換,其實(shí)使用純數(shù)字來表示可能更好理解,這也就意味著莫以為可以是(26),所以某個數(shù)字就成了
(26)(26),由此看看怎么實(shí)現(xiàn)轉(zhuǎn)換
有點(diǎn)麻煩的是這個0,不管是10進(jìn)制還是二進(jìn)制都有0,10進(jìn)制轉(zhuǎn)二進(jìn)制,余數(shù)為0是不用特殊處理的。
8.3 顛倒二進(jìn)制位
題目:顛倒給定的 32 位無符號整數(shù)的二進(jìn)制位。
提示:
請注意,在某些語言(如 Java)中,沒有無符號整數(shù)類型。在這種情況下,輸入和輸出都將被指定為有符號整數(shù)類型,并且不應(yīng)影響您的實(shí)現(xiàn),因為無論整數(shù)是有符號的還是無符號的,其內(nèi)部的二進(jìn)制表示形式都是相同的。
在 Java 中,編譯器使用二進(jìn)制補(bǔ)碼記法來表示有符號整數(shù)。因此,在 示例 2 中,輸入表示有符號整數(shù) -3,輸出表示有符號整數(shù) -1073741825。
解析:輸入是一個二進(jìn)制數(shù),只是要注意一點(diǎn),當(dāng)他作為二進(jìn)制數(shù)時,高位的0會被省去,比如000111,在實(shí)際使用的時候就變成111
8.4 2的冪
題目:給你一個整數(shù) n,請你判斷該整數(shù)是否是 2 的冪次方。如果是,返回 true ;否則,返回 false 。
如果存在一個整數(shù) x 使得 n == 2x ,則認(rèn)為 n 是 2 的冪次方。
分析:
我一直對位運(yùn)算不熟悉,沒怎么用過。之前只是用過運(yùn)算的相關(guān)性質(zhì),0相當(dāng)于乘法中的1
n & (n - 1)用于移除最低位的1
實(shí)際上只有兩種情況:(1)不需要借位時,最低位的1必然是在末位,那么n和n-1的其他位相同,與運(yùn)算之后n的其他位不變,末位變0;(2)需要借位時,必然從最低位的1借出,那么從最低位的1開始直至末尾,n和n-1不同,與運(yùn)算之后,這些為全為0,也移除了最低位的1
在本題中,2的冪的二進(jìn)制表示必然只有一個1,那么移除最低位的1之后此數(shù)必然變?yōu)?
8.4 3的冪
沒有辦法像求解2的冪那樣直接使用位運(yùn)算,但是x的冪的特點(diǎn)是,(1)首先是正整數(shù) (2)一直除3都能整除
8.5 丟失的數(shù)字
題目:給定一個包含 [0, n] 中 n 個數(shù)的數(shù)組 nums ,找出 [0, n] 這個范圍內(nèi)沒有出現(xiàn)在數(shù)組中的那個數(shù)。
分析:首先是這個題目看得人有點(diǎn)懵逼。[0,n]有n+1個數(shù)字,但是只包含了n個,范圍在[0,n+1],找出缺的那一個
我選擇了位運(yùn)算的解法,比較喜歡異或運(yùn)算,核心就這兩個:x^x = 0
x^0 = x
如果在原序列后面再補(bǔ)上[0,n+1]這n+1個數(shù),依次進(jìn)行異或,因為缺失的數(shù)字只出現(xiàn)了一次,那么最后的結(jié)果就是這個確實(shí)的數(shù)字
巧妙地地方在于,并不需要真的往數(shù)組補(bǔ)上n+1個數(shù),因為數(shù)字范圍和下標(biāo)是一致的,因此讓數(shù)組下標(biāo)參與異或即可,最后再補(bǔ)上len(nums)即可。真的非常優(yōu)雅
九. 字符串
9.1 最短單詞距離
題目:給定一個單詞列表和兩個單詞 word1 和 word2,返回列表中這兩個單詞之間的最短距離。
分析:
我最初的想法是,先找第一個,再找第二個,然后計算距離,但是處理不了這種情況
a…a…b…b
此問題的關(guān)鍵在于,必須要在遍歷的過程中同時找兩個目標(biāo),實(shí)時更新他們的位置
如上例,前面有重復(fù)的單詞1,一直沒有找到單詞2,那么處于后位的單詞1肯定離單詞2更近
十. 博弈論
10.1 nim游戲
題目:你和你的朋友,兩個人一起玩 Nim 游戲:
桌子上有一堆石頭。
你們輪流進(jìn)行自己的回合,你作為先手。
每一回合,輪到的人拿掉 1 - 3 塊石頭。
拿掉最后一塊石頭的人就是獲勝者。
假設(shè)你們每一步都是最優(yōu)解。請編寫一個函數(shù),來判斷你是否可以在給定石頭數(shù)量為 n 的情況下贏得游戲。如果可以贏,返回 true;否則,返回 false 。
分析:
推廣開來,將整個游戲進(jìn)程視作k個回合,每回合各抓一次。
后手要贏就得保證在最后一個回合開始前石子總數(shù)是4的倍數(shù)。
先手要贏就得破壞這個條件,在先手抓后,使總的石子數(shù)變成4的倍數(shù)。也就是把超過4n的那部分抓掉
進(jìn)一步講,從一開始結(jié)局就是肯定的
當(dāng)規(guī)則形成,游戲中只有最精明的玩家時,結(jié)局是注定的
總結(jié)
以上是生活随笔為你收集整理的每天一个算法(简单)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云服务器,价格其实不便宜,但为什么还要用
- 下一篇: getElementByClassNam