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