之前我也寫過一兩篇與算法技巧相關的文章
今天的這篇文章,算是一種補充,同時會列舉一些常見的算法題,如何用這些技巧來解決,通過使用這些方法,可以讓一些算法題變的更加簡單。
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 只出現了一次,其他都出現了兩次,把他們全部異或一下,結果如下:
由於異或支持交換律和結合律,所以:
123451234 = (11)(22)(33)(44)5= 00005 = 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多篇原創文章

大部分的數據結構與算法文章被各種公眾號轉載相信一定能讓你有所收獲

我也分享了很多視頻、書籍的資源,以及開發工具,歡迎各位的關注我的公眾號:苦逼的碼農,第一時間閱讀我的文章。
