畢業半年, 平時工做老是關注業務、架構,而卻愈來愈少關注性能, 也再也沒有作過任何涉及算法的工做了node
但願有時間把這些拉下的東西拾起來,畢竟不管是使用什麼語言,從事什麼行業,只要是程序員,算法纔是真正的基礎。程序員
題目來自leetcode,代碼語言一般爲C/C++,後期可能個別題目會用Golang算法
每道題都會闡述儘量多的思路及不一樣思路的效率對比,以及每種思路的代碼實現數組
萬事開頭難,但堅持下去其實更難。架構
(2018.2.3)ide
題目1:給定和,獲取加數性能
描述: 給定一個整數數組,以及一個整數,已知這個整數是數組內某兩個元素的和,如今須要找到,並返回這兩個元素的索引,例如:優化
整數數組:{11, 7, 12, 2}
spa
整數 :9
3d
返回結果:{1, 3}
(假定結果必定存在於給定數組,而且不須要考慮存在多組結果)
題目連接:https://leetcode.com/problems/two-sum/description/
解答:
解法1:
最傳統的方法就是挨個查找,判斷結果,具體步驟是:
每一趟都從下圖第一個元素(11)開始, 固定住第一個元素不變,計算當前元素與其後每一個元素的值之和,判斷是否等於目標整數
這種方法,最壞的狀況下,數組內每一對元素都會被計算一次,所以時間複雜度爲O(N * N)
代碼:
vector<int> twoSum(vector<int>& nums, int target) { vector<int> ret; int sz = nums.size(); for(size_t i = 0; i < sz - 1; ++i) { for(size_t j = i + 1; j < sz; ++j) { if(nums[i] + nums[j] == target) { ret.push_back(i); ret.push_back(j); return ret; } } } return ret; }
解法2:
上面的貪婪法能夠將步驟分解爲兩部分,外層循環和內層循環,每當外層循環執行一步,內層循環都須要逐個遍歷剩下的元素,執行一趟時間複雜度爲O(N)的過程,在整個過程當中,外層循環是沒法優化的,而內層的循環做爲優化,能夠考慮用空間換取時間的思路:即事先對整個數組創建索引,使得每一趟的內層循環再也不是遍歷,而是精確查找,使得算法的時間複雜度由O(N*N)變爲O(N*1)
具體步驟是:
代碼:
// O(n) vector<int> twoSum_better1(vector<int>& nums, int target) { int sz = nums.size(); map<int, int> dic_map; // 先創建索引 for(size_t i = 0; i < sz; ++i) { dic_map[nums[i]] = i; } vector<int> ret; // 開始查找 for(size_t i = 0; i < sz; ++i) { int pos_val = target - nums[i]; // 要查找的數據 int pos_key = dic_map[pos_val]; if(pos_key != i && (nums[i] + nums[pos_key]) == target ) { // 找到 ret.push_back(i); ret.push_back(pos_key); break; } } return ret; }
解法3(創建索引的過程能夠分解到外層循環的每一步當中):
一開始無索引,每一步都在索引中查找目標元素,若是沒有,則將當前元素存入索引,並迭代至下一步
代碼:
// O(n), 速度最優, 但總體跟上一種方法在同一數量級內 vector<int> twoSum_better2(vector<int>& nums, int target) { int sz = nums.size(); map<int, int> dic_map; vector<int> ret; // 開始查找 for(size_t i = 0; i < sz; ++i) { int pos_val = target - nums[i]; // 要查找的數據 int pos_key = dic_map[pos_val]; if(pos_key != i && (nums[i] + nums[pos_key]) == target) { ret.push_back(i); ret.push_back(pos_key); break; }else { dic_map[nums[i]] = i; // 添加索引 } } return ret; }
總結:
坑:
比較少
優化:
比較一、二、3方法,
1方法時間複雜度爲O(N*N),最低效,
二、3方法總體時間複雜度都是O(N),區別在於 2方法是在一開始就創建了完整的索引,而3方法則是在迭代的過程當中逐步創建索引
在悲觀的狀況下,二、3方法效率是相同的
在樂觀的狀況下,3方法只須要向索引中存放一個元素,所以相對來講更高效
題目2:鏈表求和
描述: 給定兩個非空鏈表,每一個鏈表表明一個整數,而鏈表的每一個結點則表明每一位,此外規定鏈表爲倒序排列,求兩鏈表所表明的正數之和對應的鏈表。例如:
整數鏈表:( 2->4->3 ) + ( 5->6->4 ) (至關於 342 + 465 )
返回結果:(7->0->8) (至關於 807 )
鏈表結點:
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */
題目連接:https://leetcode.com/problems/add-two-numbers/description/
解答:
解法一:(不徹底正確的方法)
step1:分別遍歷兩鏈表,按照倒序轉化的規則將兩個鏈表轉化成兩個整數 O(N) * 2 (藉助棧)
step2: 兩個整數相加 O(1)
step3: 相加之和轉化爲鏈表 O(N)
代碼:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { stack<int> s1; stack<int> s2; // l一、l2元素入棧 ListNode* p1 = l1; ListNode* p2 = l2; while(p1 != NULL) { s1.push(p1->val); p1 = p1->next; } while(p2 != NULL) { s2.push(p2->val); p2 = p2->next; } // 統計兩加數之和 long count1 = 0; long count2 = 0; while(!s1.empty()) { count1 = s1.top() + 10 * count1; s1.pop(); } while(!s2.empty()) { count2 = s2.top() + 10 * count2; s2.pop(); } long sum = count1 + count2; // 生成新鏈表 long res = sum; ListNode* ret_node = NULL; ListNode* pos = NULL; if(res == 0) { ListNode* ret_node = new ListNode(0); return ret_node; } while(res != 0) { int unit = res % 10; res = res / 10; if(ret_node == NULL) { ret_node = new ListNode(unit); pos = ret_node; }else { ListNode* next_node = new ListNode(unit); pos->next = next_node; pos = next_node; } } return ret_node; }
總的時間複雜度爲:
3 * O(N) + O(1) ~= O(N)
可是當兩鏈表所表示的整數很是大,將會致使×××溢出,所以這種方法是有問題的
解法二:
同時遍歷兩個鏈表,遍歷的同時進行相加,生成新的鏈表。思路就像筆算求解多位數之和的過程,比較簡單,主要須要考慮下面幾種狀況便可:
進位問題;
當前位置兩數都有值的狀況;
當前位置一個數有值一個數沒有值的狀況;
代碼:
ListNode* addTwoNumbers_better(ListNode* l1, ListNode* l2) { ListNode* ret_head = NULL; ListNode* pcur = NULL; ListNode* p1 = l1; ListNode* p2 = l2; int addi = 0; // 進位符 while(p1 != NULL || p2 != NULL) { int cur = addi; if(p1 != NULL) { cur += p1->val; p1 = p1->next; } if(p2 != NULL) { cur += p2->val; p2 = p2->next; } if(cur > 9) { addi = 1; cur %= 10; }else { addi = 0; } ListNode* newNode = new ListNode(cur); if(ret_head == NULL) { ret_head = newNode; pcur = newNode; }else { pcur->next = newNode; pcur = pcur->next; } } if(addi == 1) { ListNode* newNode = new ListNode(1); if(ret_head == NULL) { ret_head = newNode; pcur = newNode; }else { pcur->next = newNode; pcur = pcur->next; } } return ret_head; }
總結:
坑:
須要考慮到溢出問題,否則就踩坑了
優化:
O(N), 優化空間比較小
題目3:字符串獲取最長無重複子串的長度
描述: 給定某個字符串,計算其中全部子串中,最長的那個無重複字符的字串的長度。例如:
給定字符串「abcabcbb」, 最長無重複子串是「abc」,長度爲3
給定字符串「bbbbb」, 最長無重複子串是「b」,長度爲1
給定字符串「pwwkew」, 最長無重複子串是「wke」,長度爲3
題目連接:https://leetcode.com/problems/longest-substring-without-repeating-characters/description/
解答:
方法一:(窮舉法)
思路就是列出給定的字符串的全部子串(兩層循環便可),而後篩選出其中的全部無重複的字串,而後取出其中最長的一條
簡單粗暴,效率最低,時間複雜度O(N^2)
代碼略
方法二:(貪婪法)
遍歷一遍字符串,計算每一個字符日後的最長無重複子串,統計出其中最大值。
具體分解成兩層循環,外層循環遍歷每一個元素,
內層循環從當前元素開始日後遍歷,計數,直到遇到第一個重複字符爲止,爲了性能,須要維護一個map供內層循環判斷重複字符使用,下面是代碼:
int lengthOfLongestSubstring(string s) { // 貪婪法 map<char, int> s_map; int max = 0; int cur = 0; for(string::iterator s_it = s.begin(); s_it != s.end(); ++s_it) { for(string::iterator s_itn = s_it; s_itn != s.end(); ++s_itn) { if(s_map[*s_itn] != 0) { // 有重複 if(cur > max) { max = cur; } cur = 0; s_map.clear(); break;// 結束循環 }else { cur += 1; s_map[*s_itn] = 1; } // (坑2)此處的判斷是必須的 if(cur > max) { max = cur; } } } return max; }
進一步優化
相比窮舉法,這種方法的性能要高出很多,可是總的來講性能依然不夠理想,主要體如今:
一、本質上仍是內外兩層循環
二、查找字符時用的集合是map,所以查找效率爲O(logN),而每一個字符的範圍是已知的(0~255),所以能夠用一個數組做爲查找集合,查找效率將能夠提高爲O(1)
解法3(滑動窗口):
首先採用一個長度爲256的順序表做爲查找集合,這樣就能夠將查找的時間複雜度下降爲O(1)
同時維護兩個指針(或者說索引),一前一後協同者日後遍歷,遍歷的過程當中尋找兩索引的最大距離,就好像一個能夠伸縮的窗口在不斷遍歷,這樣就能夠將兩層遍歷減小爲一層,時間複雜度由O(N*N) 下降爲 O(N)
具體步驟以下圖:
p(head)爲窗口的前指針,q(tail)爲窗口的後指針,窗口移動的過程循環能夠分解成兩步:
step1:先向前移動p,不斷拉長窗口,直到遇到重複的字符; (擴大階段)
step2:遇到重複的字符,這時候就須要向前移動q,逐步縮小窗口長度,直到將這個重複的元素剔除; (縮小階段)
在移動的過程當中記錄保存p、q的間距(最大值)
這種方法的總體時間複雜度爲 2 * O(N) ≈ O(N)
這種方法的實現代碼以下:
int lengthOfLongestSubstring_better(string s) { vector<int> s_vec(256, 0); int max = 0; int head = 0; int tail = 0; int length = s.length(); while(head < length && tail < length) { char head_val = s[head]; char tail_val = s[tail]; if(0 == s_vec[head_val]) { // 擴大階段 ++s_vec[head_val]; ++head; max = (head - tail) > max ? (head - tail) : max; }else { // 縮小階段 s_vec[tail_val] = 0; ++tail; } } return max; }
解法4(上一種解法的深度優化):
上一種方法的縮小階段實際上是不必的,咱們能夠直接在查找集合中存入相應的記錄,這樣每次縮小階段,就能夠直接將tail指針跳到上一次出現的該字符的位置,時間就能由O(N)縮減爲O(1)了
代碼以下:
int lengthOfLongestSubstring(string s) { vector<int> s_vec(256, 0); int length = s.length(); int max = 0; for(size_t head = 0, tail = 0; head < length; ++head) { char head_val = s[head]; tail = s_vec[head_val] > tail ? s_vec[head_val] : tail; // 跳轉到head_val字符出現的下一個位置 max = (head - tail + 1) > max ? (head - tail + 1) : max; s_vec[head_val] = head + 1; // (永遠記錄head_val字符出現的下一個位置) } return max; }
總結:
後面3種優化方法本質上其實都是貪婪法的思路,這個題目若是不仔細思考,很難一步到位獲得最優算法~
坑:
坑比較少
優化:
優化空間較大
題目4:獲取兩排序數組的中值
描述: 給定兩個排序好的數組(升序排序),計算兩個數組內全部數的中值。例如:
給定數組1 : [1, 3], 數組2 : [2] , 計算結果爲:2
給定數組1 : [1, 2], 數組2 : [3, 4] , 計算結果爲:2.5
題目連接:https://leetcode.com/problems/median-of-two-sorted-arrays/description/
解答:
解法1:
最直接的作法就是對這兩個數組進行排序,最直接的作法是建立一個臨時數組(或者堆),將兩個數組中的全部元素都存放進去,而後對這個數組進行排序,並找出中值,這種方法的時間複雜度爲
採用臨時數組方式:遍歷一遍O(N) + 排序O(log2N) ,實現代碼略
採用堆的方式: 遍歷一遍 & 建堆O(N) + 查找O(log2N)
解法2(雙指針一趟(半趟)遍歷):
已知兩個數組都是排序好的,那麼其實能夠根據這個特性進行優化,將時間複雜度下降爲O(N)
思路是:維護兩個指針及一個計數器,按從小到大的順序遍歷兩數組,每一步遍歷計數器加1,直到計數器加到中值。
非遞歸實現代碼以下:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) { // 中間位置的肯定 (奇or偶 & 各自的位置) int length1 = nums1.size(); int length2 = nums2.size(); int total = length1 + length2; int pos1 = 0; int pos2 = 0; bool is_odd = true; // 奇數 int val1 = 0; int val2 = 0; if(total % 2 == 0) { // 總數爲偶數, 取中間兩位數的平均值 pos1 = total / 2 - 1; pos2 = total / 2; is_odd = false; }else { pos1 = total / 2; is_odd = true; } int step = 0; // 步數 vector<int>::iterator p1 = nums1.begin(); vector<int>::iterator p2 = nums2.begin(); vector<int>::iterator cur = nums1.begin(); while(p1 != nums1.end() || p2 != nums2.end()) { if(p1 != nums1.end() && (p2 == nums2.end() || *p1 < *p2)) { cur = p1++; }else { cur = p2++; } if(is_odd && step == pos1){ // 找到奇數狀況下的結果 return *cur; } if(!is_odd && step == pos1) { val1 = *cur; }else if(!is_odd && step == pos2) { val2 = *cur; return ((double)val1 + val2) / 2; } ++step; } return 0; }
解法3:
leetcode提供了一種遞歸的方式,時間複雜度能夠達到O(log2N):
假如給定A、B兩個排序數組, 在A中尋找一處索引 i, 在B中尋找一處索引 j, 分別將A、B數組分割成左右兩部分 ;
合併A、B的左半部分;
合併A、B的右半部分;
假如左右兩部分長度相同(總數爲偶數時知足:i+j = (m-i) + (n-j) 總數爲奇數時知足 i + j = (m - i) + (n - j) + 1), 而且左半部分最大的元素比右半部分最小的元素小時(A[i - 1] <= B[j] && B[j - 1] <= A[i])
當以上這兩個條件均知足時 左半部分的最後一個元素和右半部分的第一個元素的平均值就是要求的結果。
將上面兩個條件轉化成:
條件1:j = (m+n+1)/2 - i = halfLen - i (要保證j爲正數, 所以又多了一個條件: n >= m)
條件2:A[i - 1] <= B[j] && B[j - 1] <= A[i]
所以能夠用二分查找的方式, 查找那個合適的i值
這種方法實質上是對元素的一次二分,所以時間複雜度爲 O(log2N), 是這道題目已知的最優解
僞代碼
m = A.length n = B.length // 確保左邊的值更小 if m > n then swap(A, B) swap(m, n) end // 二分查找合適的i iMin = 0 IMax = m halfLen = (m + n + 1) / 2 while(iMin <= iMax) then i = (iMin + iMax) / 2 j = halfLen - i if i < iMax && B[j - 1] > A[i] then // 說明i過小了 iMin = iMin + 1 else if i > iMin && A[i - 1] > B[j] then // 說明i太大了 iMax = iMax - 1 else // 找到合適的i maxLeft = 0 if i == 0 then maxLeft = B[j - 1] else if j == 0 then maxLeft = A[i - 1] else maxLeft = max(A[i - 1], B[j - 1]) end if (m + n) % 2 == 1 then // 奇數,直接返回中值 return maxLeft end maxRight = 0 if i == m then maxRight = B[j] else if j == m then maxRight = A[j] else maxRigth = max(A[j], B[j]) end return (maxLeft + maxRight ) / 2.0 end end
總結:
不仔細推導很可貴出最後一種方法...
坑:
坑比較少
優化:
存在優化空間
題目5:獲取最長迴文字符串
描述: 給定兩個排序好的數組(升序排序),計算兩個數組內全部數的中值。例如:
輸入:"babad" 輸出:"bab"
輸入:"cbbd" 輸出:"bb"
題目連接:https://leetcode.com/problems/median-of-two-sorted-arrays/description/
解答:
解法1:
最簡單的思路,遍歷的同時找對稱點,一旦找到對稱點(考慮,分別處理好aa aba aaa這三種狀況),維護兩個下標分別向前、向後遍歷,找出全部對稱點及每一個對稱點對應的字符串,返回最長的那個,代碼以下:
string getDstStr(string s, size_t& ileft, size_t& iright, size_t sz, size_t pos, int& maxlen) { string ret = ""; if(ileft >= 0 && iright < sz) { while(ileft >= 0 && iright < sz ) { // 坑 if(s[ileft] == s[iright]) { --ileft; ++iright; }else { break; } } ++ileft; --iright; } int len = (iright == pos) ? 0 : 1; string tmp = ""; if(len + 1 + iright - ileft > maxlen) { for(size_t i = ileft; i <= iright; ++i) { tmp += s[i]; } ret = tmp; maxlen = ret.size(); return ret; } return ret; } string longestPalindrome(string s) { // 坑 if(s.size() == 1) { return s; } size_t sz = s.size(); string ret = ""; int maxlen = 0; for(size_t i = 0; i < sz; ++i) { bool is_special = false; // 'bbb'這種狀況 size_t ileft = 0; size_t iright = 0; if(i >= 1 && i < sz && s[i - 1] == s[i + 1]) { // 奇數的狀況abcba ileft = i - 1; iright = i + 1; }else if(i >= 1 && s[i] == s[i - 1]) { // 偶數的狀況abba ileft = i - 1; iright = i; } if(i >= 1 && s[i] == s[i - 1] && s[i] == s[i + 1] ) { // 考慮'bbb'這種狀況 is_special = true; } // 知足條件, 計算長度 if(iright > 0) { if(is_special) { ileft = i - 1; iright = i + 1; string tmp1 = getDstStr(s, ileft, iright, sz, i, maxlen); ileft = i - 1; iright = i; string tmp2 = getDstStr(s, ileft, iright, sz, i, maxlen); if(tmp1.size() > tmp2.size()) { if(!tmp1.empty()) { ret = tmp1; } }else { if(!tmp2.empty()) { ret = tmp2; } } }else { string tmp = getDstStr(s, ileft, iright, sz, i, maxlen); if(!tmp.empty()) { ret = tmp; } } } } // 坑 if(ret.empty()) { return s.substr(0, 1); } return ret; }
思路比較簡單,但實現起來坑不少,時間複雜度O(N^2)
模板