資料結構與演算法中有那些奇技淫巧?
之前我也寫過一兩篇與演算法技巧相關的文章
一些常用的演算法技巧總結
【演算法技巧】位運算裝逼指南
今天的這篇文章,算是一種補充,同時會列舉一些常見的演算法題,如何用這些技巧來解決,通過使用這些方法,可以讓一些演算法題變的更加簡單。
1、用 n & (n - 1)消去 n 最後的一位 1
在 n 的二進位制表示中,如果我們對 n 執行
n = n & (n - 1)
那麼可以把 n 左右邊的 1 消除掉,例如
n = 1001
n - 1 = 1000
n = n & (n - 1) = (1001) & (1000) = 1000
這個公式有哪些用處呢?
其實還是有挺多用處的,在做題的時候也是會經常碰到,下面我列舉幾道經典、常考的例題。
(1)、判斷一個正整數 n 是否為 2 的冪次方
如果一個數是 2 的冪次方,意味著 n 的二進位制表示中,只有一個位 是1,其他都是0。我舉個例子,例如
2^0 = 0.....0001
2^1 = 0.....0010
2^2 = 0....0100
2^3 = 0..01000
.....
所以呢,我們只需要判斷N中的二進位制表示法中是否只存在一個 1 就可以了。按照平時的做法的話,我們可能會對 n 進行移位,然後判斷 n 的二進位制表示中有多少個 1。所以做法如下
boolean judege(int n) { int count = 0; int k = 1; while (k != 0) { if ((n & k) != 0) { count++; } k = k << 1; } return count == 1; }
但是如果採用 n & (n - 1) 的話,直接消去 n 中的一個 1,然後判斷 n 是否為 0 即可,程式碼如下:
boolean judege(int n){
return n & (n - 1) == 0;//
}
而且這種方法的時間複雜度我 O(1)。
(2)、整數 n 二進位制中 1 的個數
對於這種題,我們可以用不斷著執行 n & (n - 1),每執行一次就可以消去一個 1,當 n 為 0 時,計算總共執行了多少次即可,程式碼如下:
public int NumberOf12(int n) { int count = 0; int k = 1; while (n != 0) { count++; n = (n - 1) & n; } return count;
(3)、將整數 n 轉換為 m,需要改變多少二進位制位?
其實這道題和(2)那道題差不多一樣的,我們只需要計算 n 和 m 這兩個數有多少個二進位制位不一樣就可以了,那麼我們可以先讓 n 和 m 進行異或,然後在計算異或得到的結果有多少個 1 就可以了。例如
令 t = n & m
然後計算 t 的二進位制位中有多少 1 就可以了,問題就可以轉換為(2)中的那個問題了。
2、雙指標的應用
在之前的文章中 ,我也有講過雙指標,這裡我在講一下,順便補充一些例子。
(1)、在連結串列中的應用
對於雙指標,我覺得用的最對的就是在連結串列這裡了,比如“判斷單鏈表是否有環”、“如何一次遍歷就找到連結串列中間位置節點”、“單鏈表中倒數第 k 個節點”等問題。對於這種問題,我們就可以使用雙指標了,會方便很多。我順便說下這三個問題怎麼用雙指標解決吧。
例如對於第一個問題
我們就可以設定一個慢指標和一個快指標來遍歷這個連結串列。慢指標一次移動一個節點,而快指標一次移動兩個節點,如果該連結串列沒有環,則快指標會先遍歷完這個表,如果有環,則快指標會在第二次遍歷時和慢指標相遇。
對於第二個問題
一樣是設定一個快指標和慢指標。慢的一次移動一個節點,而快的兩個。在遍歷連結串列的時候,當快指標遍歷完成時,慢指標剛好達到中點。
對於第三個問題
設定兩個指標,其中一個指標先移動k個節點。之後兩個指標以相同速度移動。當那個先移動的指標遍歷完成的時候,第二個指標正好處於倒數第k個節點。
有人可能會說,採用雙指標時間複雜度還是一樣的啊。是的,空間複雜度和時間複雜度都不會變,但是,我覺得采用雙指標,更加容易理解,並且不容易出錯。
(2)、遍歷陣列的應用
採用頭尾指標,來遍歷陣列,也是非常有用的,特別是在做題的時候,例如我舉個例子:
題目描述:給定一個有序整數陣列和一個目標值,找出陣列中和為目標值的兩個數。你可以假設每個輸入只對應一種答案,且同樣的元素不能被重複利用。
示例:
給定 nums = [2, 7, 11, 15], target = 9
因為 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
其實這道題也是 leetcode 中的兩數之和,只是我這裡進行了一下改版。對於這道題,一種做法是這樣:
從左到右遍歷陣列,在遍歷的過程中,取一個元素 a,然後讓 sum 減去 a,這樣可以得到 b,即 b = sum - a。然後由於陣列是有序的,我們再利用二分查詢,在陣列中查詢 b 的下標。
在這個過程中,二分查詢的時間複雜度是 O(logn),從左到右掃描遍歷是 O(n),所以這種方法的時間複雜度是 O(nlogn)。
不過我們採用雙指標的方法,從陣列的頭尾兩邊向中間夾擊的方法來做的話,時間複雜度僅需為 O(n),而且程式碼也會更加簡潔,這裡我給出程式碼吧,程式碼如下:
public int[] twoSum1(int[] nums, int target) {
int[] res = new int[2];
int start = 0;
int end = nums.length - 1;
while(end > start){
if(nums[start] + nums[end] > target){
end--;
}else if(nums[start] + nums[end] < target){
start ++;
}else{
res[0] = start;
res[1] = end;
return res;
}
}
return res;
}
這個例子相對比較簡單,不過這個頭尾雙指標的方法,真的用的挺多的。
3、a ^ b ^ b = a 的應用
兩個相同的數異或之後的結果是 0,而任意數和 0 進行異或的結果是它本身,利用這個特性,也是可以解決挺多題,我在 leetcode 碰到過好幾道,這裡我舉一些例子。
(1)陣列中,只有一個數出現一次,剩下都出現兩次,找出出現一次的數
這道題可能很多人會用一個雜湊表來儲存,每次儲存的時候,記錄 某個數出現的次數,最後再遍歷雜湊表,看看哪個數只出現了一次。這種方法的時間複雜度為 O(n),空間複雜度也為 O(n)了。
我們剛才說過,兩個相同的數異或的結果是 0,一個數和 0 異或的結果是它本身,所以我們把這一組整型全部異或一下,例如這組資料是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出現了一次,其他都出現了兩次,把他們全部異或一下,結果如下:
由於異或支援交換律和結合律,所以:
1^2^3^4^5^1^2^3^4 = (1^1)^(2^2)^(3^3)^(4^4)^5= 0^0^0^0^5 = 5。
通過這種方法,可以把空間複雜度降低到 O(1),而時間複雜度不變,相應的黛米如下
int find(int[] arr){
int tmp = arr[0];
for(int i = 1;i < arr.length; i++){
tmp = tmp ^ arr[i];
}
return tmp;
}
總結
這陣子由於自己也忙著複習,所以並沒有找太多的例子,上面的那些題,有些在之前的文章也是有寫過,這裡可以給那些看過的忘了的複習一些,並且也考慮到可能還有一大部分人沒看過。
所以呢,希望看完這篇文章,以後遇到某些題,可以多一點思路,如果你能用上這些技巧,那肯定可以大大降低問題的難度。
如果你覺得這篇內容對你挺有啟發,為了讓更多的人看到這篇文章:不妨
1、點贊,讓更多的人也能看到這篇內容
2、關注公眾號「苦逼的碼農」,主要寫演算法、計算機基礎之類的文章,裡面已有100多篇原創文章
大部分的資料結構與演算法文章被各種公眾號轉載相信一定能讓你有所收穫
我也分享了很多視訊、書籍的資源,以及開發工具,歡迎各位的關注我的公眾號:苦逼的碼農,第一時間閱讀我的文章