常用算法框架
1、數據結構的存儲方式底層只有兩種:數組(順序存儲)和鏈表(鏈式存儲)
二者區別:
數組:連續存儲,可以隨機訪問,通過索引可以快速找到對應元素,而且相對節約存儲時間。正因為連續存儲,必須一次性分配內存空間,擴容需要重新分配更大空間,把數據復制過去,從中間插入和刪除必須移動后面的數據
鏈表:元素不連續,靠指針指向下一個元素位置。知道某一個節點的前驅和后驅就可以對該指針刪除或者插入新元素。由于不連續,無法通過索引找到對應元素,不能隨機訪問,每個元素保存前后元素位置的指針,增加存儲空間
2、數據結構的基本操作
數據結構基本操作無非就是遍歷+訪問,再具體一點就是增刪改查
3、動態規劃算法詳解
1)動態規劃一般形式就是求最值
2)動態規劃三要素
- 重疊子問題
- 最優子結構
- 狀態轉移方程
例子:斐波那契數列
1)暴力解法
int fib(int N) {if (N == 1 || N == 2) return 1;return fib(N - 1) + fib(N - 2); }遞歸樹圖:
如果想求出f(20),就需要計算出子問題f(19)和f(18)的值,然后要計算f(19)就需要先計算子問題f(18)和f(17),以此類推。最后遇到f(1)和f(2)的時候,結果已知,直接返回結果,遞歸式不再向下生成。
但是上面有一個問題,那就是會有重復計算的數據,比如f(18)和f(17)都計算了兩遍,下面讓我們對上面進行優化
2)帶備忘錄的遞歸解法
我們創建一個“備忘錄” ,每次計算子問題的答案先記到“備忘錄”中,再返回,每次遇到一個子問題先去“備忘錄”查詢,如果發現問題已經解決,直接取答案,不需要重新計算
示例代碼:
int fib(int N) {if (N < 1) return 0;// 備忘錄全初始化為 0vector<int> memo(N + 1, 0);// 初始化最簡情況return helper(memo, N); } int helper(vector<int>& memo, int n) {// base caseif (n == 1 || n == 2) return 1;// 已經計算過if (memo[n] != 0) return memo[n];memo[n] = helper(memo, n - 1) +helper(memo, n - 2);return memo[n]; }在通過遞歸樹看一下“備忘錄”的作用
以上方法都是“自頂向下”的求解方法,那么是否可以“自底向上”的求解呢?
3)dp迭代求解
示例代碼:
int fib(int n) {if (n == 2 || n == 1)return 1;int prev = 1, curr = 1;for (int i = 3; i <= n; i++) {int sum = prev + curr;prev = curr;curr = sum;}return curr; }?看一下迭代的示例圖:
狀態轉移方程如下:
??4、回溯算法詳解
解決一個回溯問題,實際就是一個決策樹的遍歷過程:
1)路徑:已經做出的選擇
2)選擇列表:可以做的選擇
3)結束條件:到達決策樹底層,無法在做選擇的條件
偽代碼如下:
for (auto &selectItem : selectNum){// 排除不合法的選擇if (track.contains(selectItem))continue;// 做選擇track.add(selectItem);// 進?下?層決策樹backtrack(selectNum, track);// 取消選擇track.removeLast();}例子:全排列
比如給三個數[1,2,3],如何全排列?窮舉先固定第一位為1,然后第二位可以是2,第三位可以是3;然后第二位變成3,第三位變成2……其實這就是回溯算法
回溯樹:
?通過下圖在理解一下棱鏡、選擇列表、結束條件:
示例代碼:
List<List<Integer>> res = new LinkedList<>(); /* 主函數,s輸入一組不重復的數字,返回它們的全排列 */ List<List<Integer>> permute(int[] nums) {// 記錄「路徑」LinkedList<Integer> track = new LinkedList<>();backtrack(nums, track);return res; } // 路徑:記錄在 track 中 // 選擇列表:nums 中不存在于 track 的那些元素 // 結束條件:nums 中的元素全都在 track 中出現 void backtrack(int[] nums, LinkedList<Integer> track) {// 觸發結束條件if (track.size() == nums.length) {res.add(new LinkedList(track));return;}for (int i = 0; i < nums.length; i++) {// 排除不合法的選擇if (track.contains(nums[i]))continue;// 做選擇track.add(nums[i]);// 進入下層決策樹backtrack(nums, track);// 取消選擇track.removeLast();} }5、BFS算法詳解
BFS核心思想:把問題抽象成圖,從一個點開始,向四周開始擴散。一般我們用隊列寫BFS算法,每次將一個節點周圍的所有節點加入隊列。
例題:二叉樹的最小高度
套用BFS框架,顯然root是起點,終點是兩個子節點都是null的節點:
if (cur.left == null && cur.right == null)
// 到達葉?節點
示例代碼:
int minDepth(TreeNode root) {if (root == null) return 0;Queue<TreeNode> q = new LinkedList<>();q.offer(root);// root 本來就是一層,depth 初始化為 1int depth = 1;while (!q.isEmpty()) {int sz = q.size();/* 將當前隊列中的所有節點向四周擴散 */for (int i = 0; i < sz; i++) {TreeNode cur = q.poll();/* 判斷是否到達終點 */if (cur.left == null && cur.right == null)return depth;/* 將 cur 的相鄰節點加入隊列 */if (cur.left != null)q.offer(cur.left);if (cur.right != null)q.offer(cur.right);}/* 這里增加步數 */depth++;}return depth; }6、雙指針技巧框架
1)快慢指針常用算法
a.判斷鏈表是否有環
示例代碼:
bool hasCycle(ListNode *node) {if (node == nullptr || node->next == nullptr){return false;}ListNode *fastNode = node;ListNode *slowNode = node;while(fastNode != nullptr && fastNode->next != nullptr){fastNode = fastNode->next->next;slowNode = slowNode->next;if (fastNode == slowNode){return true;}}return false; }b.尋找單鏈表倒數第k個元素
示例代碼:
ListNode * Test(ListNode *node,int n) {if (node == nullptr || node->next == nullptr){return node;}ListNode *fastNode = node;ListNode *slowNode = node;while(n-- > 0){fastNode = fastNode->next;//指針先走n步}while(fastNode != nullptr && fastNode->next != nullptr){//快慢指針一起走,fastNode和slowNode間距n,fastNode到鏈表尾部,slowNode指向倒數n的節點fastNode = fastNode->next;slowNode = slowNode->next;}return slowNode; }2)左右指針常用算法
a.二分查找
題目:給定一組有序數組,和一個目標值n,進行二分查找,找到返回數組下標,未找到返回-1
示例代碼:
int binarySearch(std::vector<int> &nums,int n) {int left = 0;int right = nums.size()-1;while(left <= right){int mid = left + (right - left)/2;if (nums.at(mid) == n)//如果數組中間值等于目標值,返回mid{return mid;}if (nums.at(mid) > n) //如果數組中間值大于目標值,右區間前移到中間值前一個位置{right = mid - 1;}if (nums.at(mid) < n)//如果數組中間值小于目標值,左區間后移到中間值后一個位置{left = mid + 1;}}return -1; }b.數組反轉
示例代碼:
void reverse(std::vector<int> &nums) {int left = 0;int right = nums.size()-1;while(left < right){int nTemp = nums.at(left);nums[left] = nums[right];nums[right] = nums[left];left++;right--;} }7、二分查找框架詳解
1)正常二分查找(如果有多個找到一個就返回)
示例代碼:見上面例子代碼
2)左側邊界的二分查找(如果有多個返回第一個)
思路:找到目標值不返回,而是收縮右邊邊界(注意越界)
如圖:
示例代碼:
int left_bound(std::vector<int> &nums, int target) {int left = 0, right = nums.size() - 1;// 搜索區間為 [left, right]while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {// 搜索區間變為 [mid+1, right]left = mid + 1;} else if (nums[mid] > target) {// 搜索區間變為 [left, mid-1]right = mid - 1;} else if (nums[mid] == target) {// 收縮右側邊界right = mid - 1;}}// 檢查出界情況if (left >= nums.size() || nums[left] != target)return -1;return left; }3)左側邊界的二分查找(如果有多個返回第一個)
思路:找到目標值不返回,而是收縮左邊邊界(注意越界)
如圖:
示例代碼:?
int right_bound(std::vector<int> &nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] == target) {// 別返回,鎖定右側邊界left = mid + 1;}}// 最后要檢查 right 越界的情況if (right < 0 || nums[right] != target)return -1;return right; }8、滑動窗口框架詳解
適用:求解子串問題
滑動窗口算法的思路是這樣:
- 我們在字符串S中使用雙指針中的左右指針技巧,初始化left = right = 0,把索引左閉右開區間[left, right)稱為一個「窗口」。
- 我們先不斷地增加right指針擴大窗口[left, right),直到窗口中的字符串符合要求(包含了T中的所有字符)。
- 此時,我們停止增加right,轉而不斷增加left指針縮小窗口[left, right),直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同時,每次增加left,我們都要更新一輪結果。
- 重復第 2 和第 3 步,直到right到達字符串S的盡頭。
這個思路其實也不難,第 2 步相當于在尋找一個「可行解」,然后第 3 步在優化這個「可行解」,最終找到最優解,也就是最短的覆蓋子串。左右指針輪流前進,窗口大小增增減減,窗口不斷向右滑動,這就是「滑動窗口」這個名字的來歷
代碼框架:
/* 滑動窗口算法框架 */ void slidingWindow(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;while (right < s.size()) {// c 是將移入窗口的字符char c = s[right];// 右移窗口right++;// 進行窗口內數據的一系列更新.../*** debug 輸出的位置 ***/printf("window: [%d, %d)\n", left, right);/********************/// 判斷左側窗口是否要收縮while (window needs shrink) {// d 是將移出窗口的字符char d = s[left];// 左移窗口left++;// 進行窗口內數據的一系列更新...}} }我們通過例子了解一下滑動窗口使用
例子:
a.最小覆蓋子串
下面畫圖理解一下,needs和window相當于計數器,分別記錄T中字符出現次數和「窗口」中的相應字符的出現次數。
初始狀態:
增加right,直到窗口[left, right)包含了T中所有字符:
?
?現在開始增加left,縮小窗口[left, right):
?直到窗口中的字符串不再符合要求,left不再繼續移動:
之后重復上述過程,先移動right,再移動left…… 直到right指針到達字符串S的末端,算法結束
現在開始套模板,只需要思考以下四個問題:
- 當移動right擴大窗口,即加入字符時,應該更新哪些數據?
- 什么條件下,窗口應該暫停擴大,開始移動left縮小窗口?
- 當移動left縮小窗口,即移出字符時,應該更新哪些數據?
- 我們要的結果應該在擴大窗口時還是縮小窗口時進行更新?
示例代碼:
string minWindow(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;// 記錄最小覆蓋子串的起始索引及長度int start = 0, len = INT_MAX;while (right < s.size()) {// c 是將移入窗口的字符char c = s[right];// 右移窗口right++;// 進行窗口內數據的一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c])valid++;}// 判斷左側窗口是否要收縮while (valid == need.size()) {// 在這里更新最小覆蓋子串if (right - left < len) {start = left;len = right - left;}// d 是將移出窗口的字符char d = s[left];// 左移窗口left++;// 進行窗口內數據的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}}}// 返回最小覆蓋子串return len == INT_MAX ?"" : s.substr(start, len); }b.找所有字母異位詞
示例代碼:
vector<int> findAnagrams(string s, string t) {unordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;vector<int> res; // 記錄結果while (right < s.size()) {char c = s[right];right++;// 進行窗口內數據的一系列更新if (need.count(c)) {window[c]++;if (window[c] == need[c])valid++;}// 判斷左側窗口是否要收縮while (right - left >= t.size()) {// 當窗口符合條件時,把起始索引加入 resif (valid == need.size())res.push_back(left);char d = s[left];left++;// 進行窗口內數據的一系列更新if (need.count(d)) {if (window[d] == need[d])valid--;window[d]--;}}}return res; }總結
- 上一篇: 正冠桩基础系列软件之桩施工情况竣工图视频
- 下一篇: jQuery淘宝服饰精品案例