Hot 100(11~20)
Hot 100(11~20)
目錄11.有效的括號
給定一個只包括 '(',')','{','}','[',']' 的字串 s ,判斷字串是否有效。
有效字串需滿足:
左括號必須用相同型別的右括號閉合。
左括號必須以正確的順序閉合。
輔助棧
對於判斷這種左右匹配,可以考慮用棧和雜湊表來輔助。
思路就是遍歷字串中的每個字元,如果遍歷到雜湊表中的key(也就是右括號),並且如果此時的棧為空(棧中沒有與之對應的左括號)或者棧頂不是對應的左括號,則是不匹配的返回false,匹配的話就彈出棧頂。遍歷到左括號直接壓入棧即可。注意特殊情況,匹配肯定是成對匹配,如果字串長度不是偶數,則直接返回。
bool isValid(string s) { if (string.size() % 2 != 0) return false; unordered_map<char, char> hash = { {')', '('}, {']', '['}, {'}', '{'} }; stack<char> stk; for (auto ch : s) { if (hash.count(ch)) // count函式直接返回的是一個數值,如果存在那麼返回1,反之0 { if (stk.empty() || stk.top() != hash[ch]) return false; stk.pop(); } else stk.push(ch); } return stk.empty(); }
時間複雜度O(n),n是字串長度,空間複雜度是O(n+6),用棧儲存字串空間複雜度為n,6則是雜湊表
12.合併兩個連結串列
將兩個升序連結串列合併為一個新的 升序 連結串列並返回。新連結串列是通過拼接給定的兩個連結串列的所有節點組成的。
合併為一個新的連結串列,使用一個指標逐個遍歷比較,然後修改next。要新建一個頭節點,方便返回。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { ListNode* head = new ListNode(-1); ListNode* pre = head; while (l1 != nullptr && l2 != nullptr) { if (l1->val > l2->val) { pre->next = l2; l2 = l2->next; } else { pre->next = l1; l1 = l1->next; } } pre->next = l1 == nullptr ? l2 : l1; return head->next; }
時間複雜度O(n+m),n和m是兩個連結串列的長度, 空間複雜度O(1)
13.括號生成
數字
n
代表生成括號的對數,請你設計一個函式,用於能夠生成所有可能的並且 有效的 括號組合。
看到這個類似全排列的問題,可以用暴力解題,題解思路是列出所有可能的組合,然後再判斷它是否有效。但是這種時間複雜度過高,一般不會考慮。大多數會使用深搜dfs來解題,第一位放什麼,然後往下遞迴,構建二叉樹。
dfs遞迴
關鍵點在於搜尋序列時的當前位,是選擇左括號還是右括號,並且需要生成有效的括號序列。
首先左括號數量不能大於n,當條件成立時新增左括號。其次當右邊括號小於n並且左邊括號大於右括號時,新增右括號。遞迴的結束條件是左右兩遍括號數量都等於n。
引數傳遞是值傳遞,值傳遞的好處就是下一層的結果不會對上一層造成任何影響,因此我們回溯到上一層時,程式會自動幫我們擦除當前層的選擇。
public:
vector<string> res; //記錄答案
vector<string> generateParenthesis(int n) {
dfs(n , 0 , 0, "");
return res;
}
void dfs(int n ,int lc, int rc ,string str)
{
if( lc == n && rc == n) res.push_back(str); //遞迴邊界
else
{
if(lc < n) dfs(n, lc + 1, rc, str + "("); //拼接左括號
if(rc < n && lc > rc) dfs(n, lc, rc + 1, str + ")"); //拼接右括號
}
}
時間複雜度是$O(C_{2n}^n)$。空間複雜度$O(n)$,遞迴2n層,每層常數空間使用,所以漸進複雜為O(n)
15.下一個排列
整數陣列的一個 排列 就是將其所有成員以序列或線性順序排列。
例如,arr = [1,2,3] ,以下這些都可以視作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整數陣列的 下一個排列 是指其整數的下一個字典序更大的排列。更正式地,如果陣列的所有排列根據其字典順序從小到大排列在一個容器中,那麼陣列的 下一個排列 就是在這個有序容器中排在它後面的那個排列。如果不存在下一個更大的排列,那麼這個陣列必須重排為字典序最小的排列(即,其元素按升序排列)。例如,arr = [1,2,3] 的下一個排列是 [1,3,2] 。
類似地,arr = [2,3,1] 的下一個排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一個排列是 [1,2,3] ,因為 [3,2,1] 不存在一個字典序更大的排列。
給你一個整數陣列 nums ,找出 nums 的下一個排列。必須 原地 修改,只允許使用額外常數空間。
這題的題意是按照字典序找出下一個排列。下一個排列比當前排列要大,並且變大的幅度需要最小。如果更改前面的數字,那麼變大的幅度是最大的,所以進行修改後面的數字。
兩次掃描
第一次從後往前尋找第一個逆序。第二次掃描找出比第一個它大的位。
void nextPermutation(vector<int>& nums)
{
int i = nums.size() - 2; // 為了找出後面的第一個逆序
while (i >= 0 && nums[i] >= nums[i + 1]) i --; // 尋找逆序
if (i >= 0)
{
int j = nums.size() - 1; // 尋找第一個比i大的數字
while (nums[i] >= nums[j]) j --;
swap(nums[i], nums[j]);
}
reverse(nums.begin() + i + 1, nums.end()); // 將i後面的數反轉
}
時間複雜度是$O(N)$,N是給定序列長度,最多需要兩次掃描,以及一次反轉。空間複雜度$O(1)$
17.搜尋旋轉排序陣列
整數陣列 nums 按升序排列,陣列中的值 互不相同 。
在傳遞給函式之前,nums 在預先未知的某個下標 k(0 <= k < nums.length)上進行了 旋轉,使陣列變為 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下標 從 0 開始 計數)。例如, [0,1,2,4,5,6,7] 在下標 3 處經旋轉後可能變為 [4,5,6,7,0,1,2] 。
給你 旋轉後 的陣列 nums 和一個整數 target ,如果 nums 中存在這個目標值 target ,則返回它的下標,否則返回 -1 。
二分查詢
最簡單的辦法就是開一個雜湊表記住值和下標,sort排序看看 nums[target] 是否等於 target ,如果不是則返回-1,是則從雜湊表中返回value。但是本題需要用$O(logn)$的方法求解,於是可以用二分的方法。將旋轉後的陣列一分為二,一部分是有序的,另一部分是部分有序的,
int search(vector<int>& nums, int target)
{
int n = nums.size();
if (!n) return -1; // 特判陣列為空或者只有一個數的情況
if (n == 1) return nums[0] == target ? 0 : -1;
int l = 0, r = n - 1;
while (l <= r)
{
int mid = l + r >> 1;
if (nums[mid] == target) return mid;
if (nums[0] <= nums[mid]) // 說明這部分有序
{ // 如果target在0到mid這個區間,更新右邊界
if (nums[0] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
}
else
{
if (nums[mid] < target && target <= nums[n - 1]) l = mid + 1;
else r = mid - 1;
}
}
return -1; // 退出while迴圈說明沒有該target
}
時間複雜度$O(log n)$,空間複雜度$O(1)$
18.在排序陣列中查詢元素的第一個和最後一個位置
給定一個按照升序排列的整數陣列 nums,和一個目標值 target。找出給定目標值在陣列中的開始位置和結束位置。
如果陣列中不存在目標值 target,返回 [-1, -1]。
進階:
你可以設計並實現時間複雜度為 O(log n) 的演算法解決此問題嗎?
最容易讓人想到的解題方式就是直接遍歷,如果沒搜到target,直接返回{-1,-1},如果搜到了,則記下當前下標,直到遍歷比target大的數,返回下標組。但這種解題方式是O(n)的。
這題類似於ACWing上的數的範圍,因為給出的陣列是有序的,所以可以使用二分查詢來優化時間複雜度。
二分查詢
vector<int> searchRange(vector<int>& nums, int target)
{
int n = nums.size();
if (!n) return {-1, -1};
int l = 0, r = n - 1;
int left = 0;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid;
else l = mid + 1;
}
if (nums[l] != target) return {-1, -1};
else
{
left = l;
r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] <= target) l = mid;
else r = mid - 1;
}
return {left, r};
}
return {-1, -1};
}
時間複雜度為$O(logn)$,空間複雜度為$O(1)$
19.組合總和
給你一個 無重複元素 的整數陣列 candidates 和一個目標整數 target ,找出 candidates 中可以使數字和為目標數 target 的 所有 不同組合 ,並以列表形式返回。你可以按 任意順序 返回這些組合。
candidates 中的 同一個 數字可以 無限制重複被選取 。如果至少一個數字的被選數量不同,則兩種組合是不同的。
對於給定的輸入,保證和為 target 的不同組合數少於 150 個。
對於這類尋找可行解的問題,都可以用搜索回溯的方法。從陣列第0位開始,每次選擇該位或不選擇該位,向下搜尋可形成一棵樹。
遞迴函式dfs需要維護三個引數,一是目標值target,每次選擇一位後target減去該位。二是當前的組合combine。三是下標dix,如果選擇當前位,則combine + 1,idx不變(因為可以有重複),如果不選擇該位,則idx + 1。
遞迴結束條件:下標等於candidates的size;
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
vector<vector<int>> ans; // combine里加起來為target則新增到ans裡
vector<int> combine; // 當前組合
dfs(candidates, target, ans, combine, 0); // 下標從0開始
return ans;
}
// 給定的陣列 目標和 答案陣列 當前組合 下標
void dfs(vector<int>& candidates, int target, vector<vector<int>>& ans, vector<int>& combine, int idx)
{
if (idx == candidates.size()) return;
if (target == 0)
{
ans.push_back(combine);
return;
}
// 跳過當前位
dfs(candidates, target, ans, combine, idx + 1);
// 選擇當前位
if (target - candidates[idx] >= 0)
{
combine.push_back(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.pop_back(); // 恢復現場
}
}
時間複雜度$O(S)$,取決於搜尋樹所有葉子節點的深度之和。空間複雜度$O(target)$最壞遞迴target層