基本概念
时间复杂度
略
空间复杂度
略
基本结构
| 数组 | 链表 | |
|---|---|---|
| 读取 | O(1) | O(n) |
| 插入 | O(n) | O(1) |
| 删除 | O(n) | O(1) |
https://www.cnblogs.com/QG-whz/p/5152963.html#_label4_0
1. 数组
数组是一种线性数据结构,使用一组连续的内存空间存储一组具有相同类型的数据。
特性:
- 随机访问,即通过下标快速定位到数组中的元素,且时间复杂度是O(1)
- 插入数据和删除数据效率低
- 编译时预先申请内存空间,数组空间利用率低,不能动态拓展
- 在不够使用的时候需要扩容,涉及到需要把旧数组中的所有元素向新数组中搬移
题:
前缀和
计算并返回⼀个索引区间之内的元素和,适用于数组值固定
用 n + 1长数组记录前项和,
class NumArray {
vector<int> presum;
public:
NumArray(vector<int>& nums) {
int n = nums.size();
presum.resize(n + 1);
presum[0] = 0;
for(int i=1; i<=n; ++i){
presum[i] = presum[i - 1] + nums[i - 1];
}
}
int sumRange(int left, int right) {
return presum[right + 1] - presum[left];
}
};
https://leetcode.cn/problems/range-sum-query-immutable/
https://leetcode.cn/problems/range-sum-query-2d-immutable/
除自身外的乘积
给定数组,返回数组,每个元素是输入数组的除自身外的积,不许用除法
首先计算元素左侧的前缀积,ans[i] = ans[i - 1] * nums[i - 1]; 然后反向循环,设后缀积r = 1; ans[i] * = r; r * = nums[i];
差分数组
数组长度固定,多次在区间内加相同常数,求最终数组。适用于变化前后区间内部元素差值不变的情况
// 差分数组⼯具类
class Difference {
// 差分数组
private int[] diff;
/* 输⼊⼀个初始数组,区间操作将在这个数组上进⾏ */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
构建与原数组等长的差分数组,处理区域变化时仅对头尾(+1)做加减,最终恢复正常数组时反向操作
https://leetcode.cn/problems/car-pooling/
https://leetcode.cn/problems/corporate-flight-bookings/
快慢指针(索引)
快慢指针解决有序数组的去重(快指针探路,慢指针填数)
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n = nums.size();
if(n < 2)return n;
int slow = 0;
for(int fast = 0; fast < n; ++fast){
if(nums[slow] != nums[fast])
nums[++slow] = nums[fast]; 注意++i还是i++
}
return slow + 1;
}
};
https://leetcode.cn/problems/remove-duplicates-from-sorted-array/comments/
https://leetcode.cn/problems/remove-element/submissions/
https://leetcode.cn/problems/move-zeroes/submissions/
左右指针(索引)
盛水容器
两个指针指向两头,由于短板效应,两条边的最短一条决定了面积,此时如果长边内移则面积一定减少,所以就让短边内移
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1;
int area = 0;
while(left < right){
area = height[left] < height[right] ?
max(area, (right - left) * height[left++]):
max(area, (right - left) * height[right--]);
}
return area;
}
https://leetcode.cn/problems/container-with-most-water/
三数之和
给出数组中a + b + c = 0的全部情况(不能重复,ijk不相同)
先排序,然后可以遍历i(0~n-2),使用双指针,其中left=i+1,right=n-1,二者往内移动,注意跳过三个指针重复的情况,去掉ai > target的情况。
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> anns;
if(nums.empty() || nums.size() < 3)return anns;
std::sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size() - 2; ++i){
if(nums[i] > 0)return anns;
if(i > 0 && nums[i] == nums[i - 1])continue;
int left = i + 1;
int right = nums.size() - 1;
while(left < right){
if(nums[i] + nums[left] + nums[right] == 0){
anns.push_back({nums[i], nums[left], nums[right]});
while(left < right && nums[left] == nums[left + 1])left++;
while(left < right && nums[right] == nums[right - 1])right--;
left++;
right--;
}
else if(nums[i] + nums[left] + nums[right] > 0){
right--;
}
else if(nums[i] + nums[left] + nums[right] < 0){
left++;
}
}
}
return anns;
}
https://leetcode.cn/problems/3sum/submissions/
最长回文子串
回文串是中心对称的,只需要设置中心,用左右指针向两边扩散即可,当左右指针值相同时,就是回文,注意中心可以是一个元素,也可以是俩,遍历中心即可。
class Solution {
public:
string longestPalindrome(string s) {
for(int i = 0; i < s.size(); ++i){
func(s, i, i);
func(s, i, i + 1);
}
return s.substr(left, len);
}
private:
int left = 0;
int right = 0;
int len = 0;
void func(string& s, int i, int j){
while(i >= 0 && j < s.size() && s[i] == s[j]){
if(j - i + 1 > len){
len = j - i + 1;
left = i;
}
i--;
j++;
}
}
};
复杂度 O ( n 2 ) O(n^2) O(n2)
https://leetcode.cn/problems/longest-palindromic-substring/
2. 链表
链表,也是一种线性数据结构,为了保证链表中元素的连续性,一般使用一个指针来找到下一个元素。
特性:
- 任意位置插入元素和删除元素的速度快
- 内存利用率高,不会浪费内存
- 链表的空间大小不固定,可以动态拓展
- 随机访问效率低,时间复杂度为0(N)
链表主要分为以下几类:
- 单向链表,包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
- 双向链表,也叫双链表,双向链表中不仅有指向后一个节点的指针,还有指向前一个节点的指针,这样可以从任何一个节点访问前一个节点,当然也可以访问后一个节点。
- 循环链表,中第一个节点之前就是最后一个节点,反之亦然。循环链表的无边界使得在这样的链表上设计算法会比普通链表更加容易。
双指针
删除链表的倒数第 n 个结点
前后指针差n位即可。
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* tmp = new ListNode(0, head);
ListNode* left = tmp;
ListNode* right = head;
for(int i = 0; i < n; ++i){
right = right->next;
}
while(right){
right = right->next;
left = left->next;
}
left->next = left->next->next;
ListNode* ans = tmp->next;
delete tmp;
return ans;
}
https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
翻转链表

左指针指向null,右指针指向头,使用tmp指针记录右指针的下一个节点,让右指针指向左指针,然后左右指针右移,
ListNode* reverseList(ListNode* head) {
ListNode* left = nullptr;
ListNode* right = head;
while(right){
ListNode* tmp = right->next;
right->next = left;
left = right;
right = tmp;
}
return left;
}
https://leetcode.cn/problems/reverse-linked-list/
链表相交节点
设长链表长度为x = m + a, 短链表长度y = n + a.两个指针px,py分别从头遍历,当py到头时,px走到了y,还差x-y = m-n,而py从长链表开始遍历,px到头时,走过m-n,py同样走了m-n。然后px从短链表出发,px走n步到n,py走n到m-n+n到m,刚刚好在相交的位置。px==py
如果不相交,py到头时,px到y,px走x-y到头,py在x-y,px走y步到头,py走y步到x也到头,此时px==py
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(!headA || !headB)return nullptr;
ListNode* x = headA;
ListNode* y = headB;
while(x != y){
x = x==nullptr? headB: x->next;
y = y==nullptr? headA: y->next;
}
return x;
}
https://leetcode.cn/problems/intersection-of-two-linked-lists/
快慢指针
判断链表中是否有环
慢指针一次走一步,快指针一次走两步,快指针会通过环绕道慢指针后,进而一定会和慢指针相遇
bool hasCycle(ListNode *head) {
if(!head || !head->next)return false;
ListNode* slow = head;
ListNode* fast = head->next;
while(slow != fast){
if(!fast || !fast->next)return false;
slow = slow->next;
fast = fast->next->next;
}
return true;
}
也可以用哈希表。
https://leetcode.cn/problems/linked-list-cycle/
链表是否为回文链表
用快慢指针(差一倍速度)找到中间节点即slow,同时不停入栈,这样在从栈取出元素和slow比较即可
bool isPalindrome(ListNode* head) {
stack<int> stk;
ListNode* fast = head;
ListNode* slow = head;
while(fast != nullptr && fast->next != nullptr){
fast = fast->next->next;
stk.push(slow->val);
slow = slow->next;
}
if(fast != nullptr){
slow = slow->next;
}
while(!stk.empty()){
auto curr = stk.top();
stk.pop();
if(slow->val != curr)return false;
slow = slow->next;
}
return true;
}
https://leetcode.cn/problems/palindrome-linked-list/
递归
递归函数必须要有终止条件,否则会出错;
递归函数先不断调用自身,直到遇到终止条件后进行回溯,最终返回答案。
将两个升序链表合并为一个新的 升序 链表
利用递归法,每次比较两个头节点的大小,并让小头->next指向后部分的结果
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(!list1)return list2;
if(!list2)return list1;
if(list1->val < list2->val){
list1->next = mergeTwoLists(list1->next, list2);
return list1;
}
else{
list2->next = mergeTwoLists(list1, list2->next);
return list2;
}
}
https://leetcode.cn/problems/merge-two-sorted-lists/
链表翻转
递归可以回溯
递归到最后的节点后返回,返回后,此时head是倒数第二个节点,令倒数第二个节点的next的next(最后一个节点)指向自己,然后让倒数第二个节点的next=null。

ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr)return head;
auto newhead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newhead;
}
https://leetcode.cn/problems/reverse-linked-list/
排序
对链表排序:
分治法,用快慢指针二分链表,递归调用排序后,再合并二者
class Solution {
public:
ListNode* merge(ListNode* left, ListNode* right) {
if(!left)return right;
if(!right)return left;
if(left->val <= right->val){
left->next = merge(left->next, right);
return left;
}else{
right->next = merge(right->next, left);
return right;
}
}
ListNode* sortList(ListNode* head) {
if(!head || !head->next)return head;
ListNode* fast = head;
ListNode* slow = head;
ListNode* brk = head;
while(fast != nullptr && fast->next != nullptr){
fast = fast->next->next;
if(fast == nullptr || fast->next == nullptr)brk = slow;
slow = slow->next;
}
brk->next = nullptr;
ListNode* newleft = sortList(head);
ListNode* neright = sortList(slow);
return merge(newleft, neright);
}
};
https://leetcode.cn/problems/sort-list/
3. 散列表
散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值(Value)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)

通过哈希函数,把Key和数组下标进行转换,即可实现快速查询。最简单的哈希函数是按照数组长度进行取模运算:
i
n
d
e
x
=
H
a
s
h
C
o
d
e
(
K
e
y
)
%
A
r
r
a
y
.
l
e
n
g
t
h
index = HashCode (Key) \% Array.length
index=HashCode(Key)%Array.length
但存在哈希冲突,解决办法:
- 开放寻址法,即后移一位
- 链表法

当哈希冲突过多时,就需要扩容操作:
3. 当HashMap.Size 大于等于 HashMap的当前长度Capacity 乘以 HashMap的负载因子LoadFactor(默认值为0.75f),进行扩容。
4. 创建一个新的Entry空数组,长度是原数组的2倍。重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。
twoSum
如果假设输入一个数组 nums 和一个目标和 target,请你返回 nums 中能够凑出 target 的两个元素的值,
使用hashtable记录nums[i]和i,然后遍历nums,查询target - nums[i]是否存在,复杂度O(n)
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashtable;
for(int i = 0; i < nums.size(); ++i){
auto it = hashtable.find(target - nums[i]);
if(it == hashtable.end()){
hashtable[nums[i]] = i;
}
else{
return {i, it->second};
}
}
return {};
}
https://leetcode.cn/problems/two-sum/
无重复的最长字符子串
滑窗法:用散列表或者unordered_set(可快速查询)存储字符是否出现,左指针右移一位,就把原值从unordered_set删除,然后右指针向右移动至出现重复字符。
int lengthOfLongestSubstring(string s) {
unordered_set<char> contain;
int right = -1;
int ans = 0;
int n = s.size();
for(int left = 0; left < n; ++left){
if(left != 0){
contain.erase(s[left - 1]);
}
while(right + 1 < n && !contain.count(s[right + 1])){
contain.emplace(s[++right]);
}
ans = max(ans, right - left + 1);
}
return ans;
}
时间复杂度O(n)
https://leetcode.cn/problems/longest-substring-without-repeating-characters/
4. 栈stack
- 线性数据结构
- 先入后出
栈最主要的功能就是回溯。
回溯法解决组合问题
电话号码字符组合
组合问题是经典需要回溯功能的问题,可以用栈实现。本题中使用递归来回溯号码,使用栈来配合
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> ans;
if(digits.size() == 0)return ans;
unordered_map<char, string> phonemap{
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
string combination;
back(ans, combination, 0, digits, phonemap);
return ans;
}
void back(vector<string>& ans, string& combination, int index, string& digits, unordered_map<char, string>& phonemap){
if(index == digits.size()){
ans.push_back(combination);
}
else{
char x = digits[index];
string strings = phonemap.at(x);
for(auto& s: strings){
combination.push_back(s);
back(ans, combination, index + 1, digits, phonemap);
combination.pop_back();
}
}
}
};
https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
括号匹配问题
括号是否有效
栈先进后出,正好符合括号匹配:
使用哈希map记录左右括号,左为key。左括号入栈,如果不是左括号,就判断是否是栈顶左括号对应的右括号。注意初始时就是右括号的情况,
bool isValid(string s) {
int n = s.size();
if(n % 2 == 1)return false;
unordered_map<char, char> charmap({{'(', ')'},
{'{', '}'},
{'[', ']'}});
stack<char> stk;
for(auto& c: s){
if(charmap.count(c)){
stk.push(c);
}
else if(stk.empty() || c != charmap[stk.top()]){ //注意初始时就是右括号的情况 stk.empty()
return false;
}
else{
stk.pop();
}
}
return stk.empty();
}
https://leetcode.cn/problems/valid-parentheses/
单调栈
给定序列,求每个元素 左边或右边 比他大或小 的第一个元素(或距离)。
最简单就是两层遍历,第一层遍历元素,第二层遍历比这个元素大或小的最近元素。可以用单调栈,把复杂度降成O(n),但是只遍历一遍,如何知道当前元素是不是过去元素的最近某元素呢?可以用栈来存储。
每日温度
给定每日温度序列,给出每天的下一个最近的更高温度的日期差
从栈口到栈底为递增,遍历元素时,与栈头元素比较,如果大于栈头,说明栈头元素的右侧最近更高温找到了,将栈头弹出并记录结果;如果小于等于栈头,说明当前元素不是任何过去元素的更高温,就入栈。
vector<int> dailyTemperatures(vector<int>& temperatures) {
int m = temperatures.size();
vector<int> ans(m, 0);
stack<int> stk;
for(int i = 0; i < m; ++i){
while(!stk.empty() && temperatures[i] > temperatures[stk.top()]){
int idx = stk.top();
ans[idx] = i - idx;
stk.pop();
}
stk.push(i);
}
return ans;
}
https://leetcode.cn/problems/daily-temperatures/submissions/
接雨水
求接雨水的面积,关键是找凹槽,就是一个柱子左边的更高柱和右边的更高柱围成的凹槽。
int trap(vector<int>& height) {
stack<int> stk;
int ans = 0;
stk.push(0);
for(int i = 1; i < height.size(); ++i){
while(!stk.empty() && height[i] > height[stk.top()]){
int idx = stk.top();
stk.pop();
if(!stk.empty()){ // 说明左边有更高的柱子,可以组成凹槽
int h = min(height[i], height[stk.top()]) - height[idx];
int w = i - stk.top() - 1;
ans += h * w;
}
}
stk.push(i);
}
return ans;
}
最大矩阵

遍历每个高度,以当前高度为高,找到其宽度左右界,计算面积,找最大。
可以用单调栈优化,遍历每个高度,只要当前高度大于等于栈顶就入栈(因为当前高度大于原栈顶,所以宽度的右界不确定),当当前高度小于栈顶时,说明栈顶的高度对应的宽度右界确定了(i), 所以让栈顶出栈为高,宽为i - stk.top - 1,计算最大面积。注:在原向量的左右插入0,可以避免很多判断。
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> stk;
heights.insert(heights.begin(), 0);
heights.push_back(0);
int ans = 0;
for(int i = 0; i < heights.size(); ++i){
while(!stk.empty() && heights[stk.top()] > heights[i]){
int h = heights[stk.top()];
stk.pop();
int w = i - stk.top() - 1; # 注意这里是i - stk.top() - 1
ans = max(ans, h * w);
}
stk.push(i);
}
return ans;
}
};
https://leetcode.cn/problems/largest-rectangle-in-histogram/
5. 队列queue
- 线性数据结构
- 先入先出
- 队列的出口端叫作队头(front),队列的入口端叫作队尾
6. 树
满二叉树要求所有分支都是满的;
而完全二叉树只需保证最后一个节点之前的节点都齐全即可,即满二叉树的部分
存储方式:
- 链表
- 数组,注意数组是按满二叉树的位置存放,缺失的节点就空出来,假设一个父节点的下标是parent,那么它的左孩子节点下标就是2×parent +1;右孩子节点下标就是2×parent + 2

二叉查找树(二叉排序树): 如果左子树不为空,则左子树上所有节点的值均小于根节点的值,如果右子树不为空,则右子树上所有节点的值均大于根节点的值, 左、右子树也都是二叉查找树。
当所有节点的值是有序时,二叉查找树就退化了,此时需要二叉树的自平衡了。二叉树自平衡的方式有多种,如红黑树、AVL树、树堆等
深度优先遍历(前序遍历、中序遍历、后序遍历)
- 递归
- 非递归,使用栈
中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
func(root, ans);
return ans;
}
void func(TreeNode* root, vector<int>& ans){
if(!root)return;
func(root->left, ans);
ans.push_back(root->val);
func(root->right, ans);
}
};
https://leetcode.cn/problems/binary-tree-inorder-traversal/
翻转二叉树
TreeNode* invertTree(TreeNode* root) {
if(!root)return root;
TreeNode* left = invertTree(root->left);
TreeNode* right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
https://leetcode.cn/problems/invert-binary-tree/
由前序与中序结果恢复二叉树
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
递归:以前序首元素为root(并构造root),查找root在中序的位置(可以用map记录中序键值对),计算左子树的长度,递归出root->left与root->right结果
class Solution {
private:
unordered_map<int, int> umap;
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i = 0; i < inorder.size(); ++i){
umap[inorder[i]] = i;
}
return func(preorder, inorder, 0, preorder.size() - 1, 0, inorder.size() - 1);
}
TreeNode* func(vector<int>& preorder, vector<int>& inorder, int pleft, int pright, int ileft, int iright){
// root - -
// - root -
if(pleft > pright)return nullptr;
int preorderRootidx = pleft;
int rootVal = preorder[preorderRootidx];
int inorderRootIdx = umap[rootVal];
int lenLeft = inorderRootIdx - ileft;
TreeNode* root = new TreeNode(rootVal);
root->left = func(preorder, inorder, pleft + 1, pleft + lenLeft, ileft, inorderRootIdx - 1);
root->right = func(preorder, inorder, pleft + lenLeft + 1, pright, inorderRootIdx + 1, iright);
return root;
}
};
https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
树的路径中根节点到叶子结点长度和为target的路径
深度优先前序遍历,设置一个临时路径path,递归法:根节点值插入path,target -= 根节点值,判断根节点是不是叶子节点且target=0,是则将path加入结果,否则递归根节点的左子树和右子树,然后path删根节点
class Solution {
vector<vector<int>> ret;
vector<int> path;
public:
void func(TreeNode* root, int target){
if(root == nullptr)return;
path.emplace_back(root->val);
target -= root->val;
if(root->left == nullptr && root->right == nullptr && target == 0){
ret.emplace_back(path);
}
func(root->left, target);
func(root->right, target);
path.pop_back();
}
vector<vector<int>> pathSum(TreeNode* root, int target) {
func(root, target);
return ret;
}
};
https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/description/
二叉树找公共节点
给俩节点,找出他俩最深的公共节点,可以是本节点。
深度优先,递归,后序遍历
设递归函数func,返回值表示root是否包括这俩节点中的任意一个。如果root=null就返回false。递归左子树获取结果lans和右子树lans,如果左子树右子树都包含则返回true或者左子树或右子树含且root==这俩节点中的一个,返回true。否则返回左子树||右子树含不含||root等于任一节点。
class Solution {
public:
TreeNode* ans;
bool func(TreeNode* root, TreeNode* p, TreeNode* q){
if(root == nullptr)return false;
bool l = func(root->left, p, q);
bool r = func(root->right, p, q);
if((l && r) || ((l || r) && (root->val == p->val || root->val == q->val))){
ans = root;
}
return l || r || root->val == p->val || root->val == q->val;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
func(root, p, q);
return ans;
}
};
https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/
广度优先遍历(层序遍历)
使用队列,令出队节点的左右子节点分别入队即可。
翻转二叉树
TreeNode* invertTree(TreeNode* root) {
if(!root)return root;
queue<TreeNode*> Que;
Que.push(root);
while(!Que.empty()){
auto t = Que.front();
Que.pop();
auto tmp = t->left;
t->left = t->right;
t->right = tmp;
if(t->left)Que.push(t->left);
if(t->right)Que.push(t->right);
}
return root;
}
https://leetcode.cn/problems/invert-binary-tree/
返回二叉树的 层序遍历,结果为逐层[[3],[9,20],[15,7]]
仍是利用队列来广度遍历,唯一不同是,需要记录queue的大小,多一层循环
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
if (!root) {
return ans;
}
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int n = q.size();
vector<int> level;
for(int i=0; i < n; ++i){
TreeNode* tmp = q.front();
q.pop();
if(tmp->left){
q.push(tmp->left);
}
if(tmp->right){
q.push(tmp->right);
}
level.push_back(tmp->val);
}
ans.push_back(level);
}
return ans;
}
https://leetcode.cn/problems/binary-tree-level-order-traversal/
腐烂橘子
在给定的 m x n 网格 grid 中, 0 代表空单元格;1 代表新鲜橘子;2 代表腐烂的橘子。每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
BFS 可以看成是层序遍历。从某个结点出发,BFS 首先遍历到距离为 1 的结点,然后是距离为 2、3、4…… 的结点。因此,BFS 可以用来求最短路径问题。BFS 先搜索到的结点,一定是距离最近的结点。
实际上本题就是求腐烂橘子到所有新鲜橘子的最短路径。一开始,找出所有腐烂的橘子,放入队列,作为第 0 层的结点。然后进行 BFS 遍历,每个结点的相邻结点可能是上、下、左、右四个方向的结点,注意判断结点位于网格边界的特殊情况。
int orangesRotting(vector<vector<int>>& grid) {
int counter = 0;
queue<pair<int, int>> q;
for(int i = 0; i < grid.size(); ++i){
for(int j = 0; j < grid[0].size(); ++j){
if(grid[i][j] == 1)counter++;
else if(grid[i][j] == 2)q.push({i, j});
}
}
int ans = 0;
while(counter > 0 && !q.empty()){
ans++;
int n = q.size();
for(int i = 0; i < n; ++i){
auto x = q.front();q.pop();
int r = x.first;
int c = x.second;
if(r - 1 >= 0 && grid[r - 1][c] == 1){
grid[r - 1][c] = 2;
counter--;
q.push({r - 1, c});
}
if(r + 1 < grid.size() && grid[r + 1][c] == 1){
grid[r + 1][c] = 2;
counter--;
q.push({r + 1, c});
}
if(c - 1 >= 0 && grid[r][c - 1] == 1){
grid[r][c - 1] = 2;
counter--;
q.push({r, c - 1});
}
if(c +1 < grid[0].size() && grid[r][c + 1] == 1){
grid[r][c + 1] = 2;
counter--;
q.push({r, c + 1});
}
}
}
if(counter > 0)return -1;
return ans;
}
递归
树的深度
左右子树的最大深度 + 1 = root的深度
int maxDepth(TreeNode* root) {
if(!root)return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
https://leetcode.cn/problems/maximum-depth-of-binary-tree/
对称二叉树
比较根节点的左右值是否相同,如果相同,进一步比较左的左和右的右是否相同,以及左的右和右的左是否相同。
bool isSymmetric(TreeNode* root) {
if(!root)return true;
return func(root->left, root->right);
}
bool func(TreeNode* left, TreeNode* right){
if(!left && !right)return true;
if(!left || !right)return false;
if(left->val != right->val){
return false;
}
return func(left->left, right->right) && func(left->right, right->left);
}
https://leetcode.cn/problems/symmetric-tree/
树的直径
就是树的最长路径,路径长度可以被看作由某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。
利用递归+后续遍历,计算某节点的左子树的最深值,和右子树的最深值,更新结果,返回本节点的深度(max(L,R)+1)

class Solution {
public:
int diameterOfBinaryTree(TreeNode* root) {
func(root);
return ans;
}
int func(TreeNode* root){
if(!root)return 0;
int L = func(root->left);
int R = func(root->right);
ans = max(ans, L + R);
return max(L, R) + 1;
}
private:
int ans = 0;
};
https://leetcode.cn/problems/diameter-of-binary-tree/
合并二叉树

先序遍历+递归
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if(!root1)return root2;
if(!root2)return root1;
TreeNode* node = new TreeNode(root1->val + root2->val);
node->left = mergeTrees(root1->left, root2->left);
node->right = mergeTrees(root1->right, root2->right);
return node;
}
https://leetcode.cn/problems/merge-two-binary-trees/
B是不是A的子树
空树不是子树
判别函数func递归判断AB是否相同,主函数递归func(A, B) || A.left和B || A.right和B
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
if(A == nullptr || B == nullptr)return false;
return func(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
}
bool func(TreeNode* A, TreeNode* B){
if(B == nullptr)return true;
if(A == nullptr || A->val != B->val)return false;
return func(A->left, B->left) && func(A->right, B->right);
}
};
https://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/description/
验证是不是二叉搜索树
二叉搜索树:左子树的值都比根节点小,右子树的值都大,所有左子树和右子树自身必须也是二叉搜索树。
递归来看,设置上下界,对于左子树,上界会变成根节点值,对于右子树,下界会变成根节点值。如果不符合上下界就false,否则递归左右子树
#include<climits>
class Solution {
public:
bool func(TreeNode* root, long long min_, long long max_){
if(root == nullptr) return true;
if(root->val <= min_ || root->val >= max_) return false;
return func(root->left, min_, root->val) && func(root->right, root->val, max_);
}
bool isValidBST(TreeNode* root) {
return func(root, LONG_MIN, LONG_MAX);
}
};
https://leetcode.cn/problems/validate-binary-search-tree/description/
7. 二叉堆
二叉堆本质上是一种完全二叉树,二叉堆的所有节点都存储在数组中,分为两个类型。
最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值
最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值
二叉堆的根节点叫作堆顶。最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。
插入节点:新节点插在最后,然后与父节点比较大小,进行上浮,逐级进行上述操作。
删除根节点:最后一个节点补上,然后与子节点比较,逐级下沉
把一个无序的完全二叉树调整为二叉堆:从最后一个非叶子节点开始,将所有非叶子节点与其子节点对比并下沉。
堆的插入和删除操作,时间复杂度是O(logn)。但构建堆的时间复杂度是O(n)。
二叉堆是实现堆排序及优先队列的基础
优先队列
优先队列不遵循先入先出的原则,而是分为两种情况。
最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队
优先队列入队和出队的时间复杂度也是O(logn)
以用最大堆来实现最大优先队列,每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点
堆排序
把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。
循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。最终的数组就是排序结果。
整体的时间复杂度是O(nlogn)
8.图
最短路径
迪杰斯特拉算法(Dijkstra)算法
int edges[N][N]; // 存放所有的边,代表从i到j的距离
int dist[N]; // 记录当前所有点到起点的距离
int visited[N]; // 标记当前的点是否被踢出
求start到end的最近路径。设置集合S和U,S集合包含已求出的最短路径的点(以及相应的最短长度),U集合包含未求出最短路径的点(以及Astart到该点的路径,没有直接相连 初始时为∞)
从U中找到最短路径,start->x ,将x设置为已访问,遍历未访问的点y,如果start->x + x->y < start->y,则更新start->y
http://t.csdn.cn/InOac
经典算法
1. 动态规划
- 在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决
剪绳子
长为n的绳剪m段,段长是整数,求每段的乘积和的最大值
遍历长度,当前长度x下做二分,即j, x-j,遍历j, m a x ( j ∗ x − j , j ∗ d p [ x − j ] ) max(j * x-j, j * dp[x-j]) max(j∗x−j,j∗dp[x−j]) 在二分和进一步划分中选最大
int cuttingRope(int n) {
vector<int> dp(n + 1);
for(int i = 2; i < n + 1; ++i){
int tmp = 0;
for(int j = 1; j <= i - 1; ++j){
tmp = max(tmp, max(j * (i - j), j * dp[i - j]));
}
dp[i] = tmp;
}
return dp[n];
}
https://leetcode.cn/problems/jian-sheng-zi-lcof/description/
爬楼梯
设爬x层楼,有方法f(x)个,则f(x) = f(x-1) + f(x-2),因为每次只能爬 1 级或 2 级
int climbStairs(int n) {
int dp[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; ++i){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
https://leetcode.cn/problems/climbing-stairs/
买卖股票
遍历卖出的天,最大利润就是在卖出天前的最低价格。
int maxProfit(vector<int>& prices) {
int minprice = 1e6;
int ans = 0;
for(int i = 0; i < prices.size(); i++){
ans = max(ans, prices[i] - minprice);
minprice = min(prices[i], minprice);
}
return ans;
}
https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/
最大子数组和
找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和
以每一位为结尾的子数组的最大和,等于以上一位的结果和本位值的关系,如果上一位的结果小于0,则本位结果就是本位值,否则就是上一位的结果和本位值的和
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp;
dp.resize(nums.size());
dp[0] = nums[0];
for(int i = 1; i < nums.size(); ++i){
if(dp[i - 1] < 0){
dp[i] = nums[i];
}
else{
dp[i] = nums[i] + dp[i - 1];
}
}
int ans = dp[0];
for(auto i: dp){
ans = max(ans, i);
}
return ans;
}
};
https://leetcode.cn/problems/maximum-subarray/
最大的非空连续子数组的乘积
找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字)
设置以nums[i]为结尾的最大连续子数组乘积为imax,遍历更新imax与ans。由于nums[i]为负数时会让变换imax的符号,让他变成imin,所以设置imin,在负数时交换二者。转移方程就是imax = max(imax * nums[i], nums[i]);
imin = min(imin * nums[i], nums[i]);
int maxProduct(vector<int>& nums) {
int imax = 1;
int imin = 1;
int ans = -100;
for(int i = 0; i < nums.size(); ++i){
if(nums[i] < 0){
int tmp = imax;
imax = imin;
imin = tmp;
}
imax = max(imax * nums[i], nums[i]);
imin = min(imin * nums[i], nums[i]);
if(imax > ans) ans = imax;
}
return ans;
}
https://leetcode.cn/problems/maximum-product-subarray/description/
路径数量
从矩阵左上角走到右下角,每次只能向下或向右,总共多少种方法
走到ij的方法数量 = 走到左边的数量 + 走到右边的数量
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1));
for(int i = 1; i < m; ++i){
for(int j = 1; j < n; ++j){
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
https://leetcode.cn/problems/unique-paths/
最小路径
从网格 grid找出一条从左上角到右下角的路径,使得路径上的数字总和为最小(每次只能向下或者向右移动一步)
从原点到当前点ij的最短路径 = min(到左边点的最短路径,到上边点的最短路径)+ 当前点的值,注意边界
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
for(int i = 0; i < grid.size(); ++i){
for(int j = 0; j < grid[0].size(); ++j){
if(i == 0 & j == 0)continue;
else if(i == 0){
grid[i][j] += grid[i][j - 1];
}
else if(j == 0){
grid[i][j] += grid[i - 1][j];
}
else{
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1]);
}
}
}
return grid[grid.size() - 1][grid[0].size() - 1];
}
};
https://leetcode.cn/problems/minimum-path-sum/
不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种
设总有g(n)个二叉搜索树,对于i为根时,长为n的二叉搜索树数量f(i, n)就等于左边的g(i - 1) * 右边的g(n - i)
class Solution {
public:
int numTrees(int n) {
// g n = sum f(i, n)
// f(i, n) = g(i - 1) * g(n - i)
// g n = sum g(i - 1) * g(n - i)
// g 0 = 1, g 1 = 1
vector<int> ans(n + 1, 0);
ans[0] = 1;
ans[1] = 1;
for(int i = 2; i <= n; ++i){
for(int j = 1; j <= i; ++j){
ans[i] += ans[j - 1] * ans[i - j];
}
}
return ans[n];
}
};
https://leetcode.cn/problems/unique-binary-search-trees/
单词划分
给定字符串,给出一堆单词,问能不能用单词组成字符串(可重复)
设dp【i】表示字符串的0 ~ 1 - 1能不能被组成,遍历i,在遍历j(0~i),如果dp【j】且子串s【j: i】在单词中出现了,说明dp【i】就是true
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> uset;
for(auto& i: wordDict){
uset.insert(i);
}
vector<bool> dp(s.size() + 1);
dp[0] = true;
for(int i = 1; i <= s.size(); ++i){
for(int j = 0; j < i; ++j){
if(dp[j] && (uset.find(s.substr(j, i - j)) != uset.end())){
dp[i] = true;
break;
}
}
}
return dp[s.size()];
}
};
https://leetcode.cn/problems/word-break/submissions/
编辑距离
给两个单词,单词1改成单词2最少需要几步?可替换、删除、插入。

dp[i][j]代表word1到i位置转换成word2到j位置需要最少步数。如果ij对应字母一样,则dp不变。如果不同,需要同三种方法中找一个步数最少的。注意,dp做了填充,所以比较字母时需要-1
以word1为"horse",word2为"ros",且dp[5][3]为例,即要将word1的前5 个字符转换为 word2的前3个字符,也就是 horse 转换为ros因此有:
(1)dp[i-1][j-1],即先将 word1 的前4个字符 hors 转换为 word2 的前2个字符ro,然后将第五个字符由e替换为s
(2) dp[i][j-1],即先将 word1 的前 5个字符 horse 转换为 word2 的前 2个字符ro,然后在末尾补充一个s,即插入操作
(3)dp[i-1][j],即先将word1 的前4个字符 hors 转换为 word2 的前3 个字符ros,然后删除 word1的第 5个字符
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.size();
int n2 = word2.size();
vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1));
for(int i = 1; i < n1 + 1; ++i){
dp[i][0] = i;
}
for(int i = 1; i < n2 + 1; ++i){
dp[0][i] = i;
}
for(int i = 1; i < n1 + 1; ++i){
for(int j = 1; j < n2 + 1; ++j){
if(word1[i - 1] == word2[j - 1]){
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = min(min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
}
}
}
return dp[n1][n2];
}
};
https://leetcode.cn/problems/edit-distance/
打家劫舍
给一串数,挑选几个,让最终结果最大,但是不能连着选。
设dp[i]就是在前i个数中选出的最大结果,dp[i]来源有俩,当不选i时,和dp【i - 1】一样,当选了i时,i-1就不能选了,只能是dp【i-2】+ nums[i-1]的金额
class Solution {
public:
int rob(vector<int>& nums) {
vector<int> dp(nums.size() + 1);
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2; i < nums.size() + 1; ++i){
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
return dp[nums.size()];
}
};
https://leetcode.cn/problems/house-robber/
兑换硬币
给定硬币面值,以及总金额,要求用最少数量组成总金额。
dp【i】表示组成总金额i时用到的最少数量。其实就是遍历面值,找出最小的dp【i-当前面值】+1
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1);
dp[0] = 0;
for(int i = 1; i <= amount; i++){
dp[i] = amount + 1;
for(auto x: coins){
if(i >= x)
dp[i] = min(dp[i], dp[i - x] + 1);
}
}
return dp[amount] != amount + 1 ? dp[amount] : -1;
}
};
https://leetcode.cn/problems/coin-change/
最长严格递增子序列的长度
一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
dp[i] 表示以nums[i]为结尾的最长严格递增子序列的长度。对于dp[i],从i之前的所有元素中找到比nums[i]小的j对应的dp[j]中最大的,再加一

int lengthOfLIS(vector<int>& nums) {
int len = nums.size();
if (len < 2) return len;
vector<int> dp(len, 1);
for(int i = 1; i < len; ++i){
for(int j = 0; j < i; ++j){
if(nums[j] < nums[i]){
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int ans = 1;
for(int i = 0; i < len; ++i){
if(dp[i] > ans) ans = dp[i];
}
return ans;
}
01背包–分割等和子集
判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
先求出总和的一半即target,问题转换为能不能挑出一部分数,使和为target。设置dp[i][j]表示前i个元素的子集的和能不能等于j。
如果num[i] = j,就是true,如果nums[i] < j,就是可以不选第i个元素也可以选dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]],否则就是dp[i][j] = dp[i - 1][j]
bool canPartition(vector<int>& nums) {
int target = 0;
for(auto i: nums){
target += i;
}
if(target & 1 == 1) return false;
target /= 2;
vector<vector<bool>> dp(nums.size(), vector<bool>(target + 1, false));
for(int i = 1; i < nums.size(); ++i){
for(int j = 0; j < target + 1; ++j){
dp[i][j] = dp[i - 1][j];
if(nums[i] == j)
dp[i][j] = true;
else if(nums[i] < j)
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
return dp[nums.size() - 1][target];
}
统计01矩阵中的全为1正方形的数量
设dp[i][j]为:以ij为右下角的正方形的最大边长,而且这个值也等于以ij为右下角的正方形的数量(比如最大边长是3,那以该点为右下角的正方形的数量就是3、2、1一共三个)
当右下角在左边和上边时就看该点是不是1;当右下角所在值是0时dp也=0;当所在值是1时就是下面的递推公式

https://leetcode.cn/problems/count-square-submatrices-with-all-ones/solutions/101706/tong-ji-quan-wei-1-de-zheng-fang-xing-zi-ju-zhen-2/
int countSquares(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
vector<vector<int>> dp(m, vector<int>(n));
int ans = 0;
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j){
if(i == 0 || j == 0){
dp[i][j] = matrix[i][j];
}
else if(matrix[i][j] == 0){
dp[i][j] = 0;
}
else{
dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
ans += dp[i][j];
}
}
return ans;
}
2. 排序
数组中第 k 个最大的元素
可以快排后,返回nums[n - k]。但其实在快排中,每次是以基准为界限,左边小于基准,右边大于基准,基准在数组的位置是准确的,所以想找第k大元素,只需要在快排是判断基准是不是在n-k的位置。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}
int quickSelect(vector<int>& nums, int startid, int endid, int ind){
int randQ = rand() % (endid - startid + 1) + startid;
int tmp = nums[startid];
nums[startid] = nums[randQ];
nums[randQ] = tmp;
int newIndex = func(nums, startid, endid);
if(newIndex == ind){
return nums[newIndex];
}
else{
return newIndex > ind ? quickSelect(nums, startid, newIndex - 1, ind): quickSelect(nums, newIndex + 1, endid, ind);
}
}
int func(vector<int>& nums, int startid, int endid){
int q = nums[startid];
int mark = startid;
for(int i = startid + 1; i <= endid; ++i){
if(nums[i] < q){
mark++;
int tmp = nums[i];
nums[i] = nums[mark];
nums[mark] = tmp;
}
}
nums[startid] = nums[mark];
nums[mark] = q;
return mark;
}
};
https://leetcode.cn/problems/kth-largest-element-in-an-array/
颜色分类
左指针给0左界限,右指针给2做界限,注意i的移动规则,当i>右指针时就停
class Solution {
public:
void sortColors(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
int i = 0;
while(i < nums.size()){
if(i > right)break;
if(nums[i] == 0){
swap(nums, i, left);
left++;
i++;
}
else if (nums[i] == 2){
swap(nums, i, right);
right--;
}
else{
i++;
}
}
}
void swap(vector<int>& nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
};
https://leetcode.cn/problems/sort-colors/
搜索旋转排序数组
给你 旋转后 的数组 nums[4,5,6,7,0,1,2] 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标
二分法,
定理一:只有在顺序区间内才可以通过区间两端的数值判断target是否在其中。
定理二:判断顺序区间还是乱序区间,只需要对比 left 和 right 是否是顺序对即可,left <= right,顺序区间,否则乱序区间。
定理三:每次二分都会至少存在一个顺序区间。(感谢@Gifted VVilburgiX补充)
通过不断的用Mid二分,根据定理二,将整个数组划分成顺序区间和乱序区间,然后利用定理一判断target是否在顺序区间,如果在顺序区间,下次循环就直接取顺序区间,如果不在,那么下次循环就取乱序区间。
int search(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1;
while(left <= right){
int mid = (left + right) / 2;
if(nums[mid] == target) return mid;
if(nums[mid] >= nums[left]){
if(target >= nums[left] && target < nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}else{
if(target > nums[mid] && target <= nums[right]){ //注意,target已经比过了,所有不用=,
left = mid + 1;
}else{
right = mid - 1;
}
}
}
return -1;
}
https://leetcode.cn/problems/search-in-rotated-sorted-array/
3. DFS
() generate
生成所有可能的并且 有效的 括号组合
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> ans;
func(n, ans, 0, 0, "");
return ans;
}
void func(int n, vector<string>& ans, int left, int right, string&& tmp){
if(left == n && right == n){
ans.push_back(tmp);
return;
}
if(left > n || right > n || right > left)return;
func(n, ans, left + 1, right, tmp + '(');
func(n, ans, left, right + 1, tmp + ')');
}
};
https://leetcode.cn/problems/generate-parentheses/
单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

可以使用dfs,遍历矩阵每个元素,以之为起始点进行dfs如果true就说明能搜索到;
在dfs时,首先判断是不是终止了,即idx是不是等于word长度,等于就返回word[idx]==board[x][y]的判断;如果不等于终止条件,就需要遍历四个方向(前提是word[idx]=board[x][y],否则说明当前点不匹配直接返回false)在满足合法条件时进行四个方向的dfs递归。
注意需要设置visited矩阵,在word[idx]=board[x][y]时设xy处为true,而后进行四个方向的递归,当四个方向都不匹配说明不该在xy处进行下去了,就让visited[x][y]=false
class Solution {
public:
int diect[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int m;
int n;
bool exist(vector<vector<char>>& board, string word) {
m = board.size();
n = board[0].size();
if(m * n < word.size()) return false;
vector<vector<bool>> visited(m, vector<bool>(n));
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(dfs(i, j, visited, 0, board, word))return true;
}
}
return false;
}
bool dfs(int x, int y, vector<vector<bool>>& visited, int idx, vector<vector<char>>& board, string word){
if(idx == word.size() - 1) return word[idx] == board[x][y];
if(word[idx] == board[x][y]){
visited[x][y] = true;
for(int i = 0; i < 4; ++i){
int newx = x + diect[i][0];
int newy = y + diect[i][1];
if(newx >= 0 && newx < m && newy >= 0 && newy < n && !visited[newx][newy]){
if(dfs(newx, newy, visited, idx + 1, board, word)) return true;
}
}
visited[x][y] = false;
}
return false;
}
};
https://leetcode.cn/problems/word-search/submissions/
岛屿问题
比如岛屿数量,岛屿面积。。。
有个岛屿网格grid,遍历每个点,如何是岛屿就dfs。dfs: 如果超界就返回,如果海洋或遍历过就返回,标记遍历过本点,dfs周围点。
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
int a = area(grid, r, c);
}
}
}
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
如果是岛屿数量问题,那就在最初的遍历时,如果grid==1,就++,因为dfs时会把遍历过的点置2,没遍历过说明是新岛屿。
如果是面积问题,就在dfs时返回面积,return 1 + 周围的dfs。
岛屿周长
岛屿的周长是岛屿与非岛屿(海洋和边界)的交界长度,所以在dfs时,遇到海洋和边界时返回1,遇到遍历过时返回0,遇到岛屿则dfs
class Solution {
public:
int islandPerimeter(vector<vector<int>>& grid) {
int ans = 0;
for(int i = 0; i < grid.size(); ++i){
for(int j = 0; j < grid[0].size(); ++j){
if(grid[i][j] == 1){
ans = dfs(grid, i, j);
}
}
}
return ans;
}
int dfs(vector<vector<int>>& grid, int i, int j){
if(!(i >= 0 && j >= 0 && i < grid.size() && j < grid[0].size())) return 1;
if(grid[i][j] == 0) return 1;
if(grid[i][j] == 2) return 0;
grid[i][j] = 2;
return dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1);
}
};
https://leetcode.cn/problems/island-perimeter/description/
4. BFS
课程表
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
其实就是看每个节点的入度是否为0;首先用列表记录每个点的入度,用map记录每个点的出度节点集合,使用队列来bfs,把入度为0的节点入队,然后bfs队列,找到该入度为0的节点的所有被依赖课程,让他们的入度减一,入度减到0的就进入队列,最后判断下长度就行。
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> ans;
vector<int> indegree(numCourses, 0);
unordered_map<int, vector<int>> map;
queue<int> q;
for(vector<int>& p: prerequisites){
indegree[p[0]]++;
map[p[1]].push_back(p[0]);
}
for(int i = 0; i < numCourses; ++i){
if(indegree[i] == 0) q.push(i);
}
while(!q.empty()){
int learned = q.front();
q.pop();
ans.push_back(learned);
for(int i = 0; i < map[learned].size(); ++i){
int course = map[learned][i];
if(indegree[course] > 0)
indegree[course]--;
if(indegree[course] == 0)
q.push(course);
}
}
if(ans.size() == numCourses)
return ans;
else
return {};
}
https://leetcode.cn/problems/course-schedule-ii/description/
5. 贪心思想
跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
记录最远可跳的位置,迭代每一个元素,对比是否能到达该位置,再更新最远位置
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxlen = 0;
for(int i = 0; i < nums.size(); ++i){
if(maxlen < i)return false;
maxlen = max(maxlen, i + nums[i]);
}
return true;
}
};
https://leetcode.cn/problems/jump-game/
6. 并查集
https://zhuanlan.zhihu.com/p/433996968
用集合中的一个元素代表集合。设置列表fa,列表长度为元素总数,fa[i] 表示i的父节点(把一个集合看做一个树,根节点就是集合的代表),初始时fa[i]=i,
查询操作,递归找root:
int find_root(int i){
if(fa[i] == i) return i;
else return find_root(fa[i]);
}
合并操作:
void merge(int i, int j){
fa[find_root(i)] = find_root(j);
}
骚操作
1. 异或
找出仅出现一次的元素
非空 整数数组中,仅有一个元素出现一次,其他都是两次,找出一次的元素
任何数和 0 做异或运算,结果仍然是原来的数,任何数和其自身做异或运算,结果是 0。异或运算满足交换律和结合律。所以整体异或一遍即可
int singleNumber(vector<int>& nums) {
int ans = nums[0];
if(nums.size() == 1)return ans;
for(int i = 1; i < nums.size(); ++i){
ans ^= nums[i];
}
return ans;
}
https://leetcode.cn/problems/single-number/
求两数的二进制编码的汉明距离
x与y进行异或,即可得到二进制的中间结果,我们需要遍历每一位,如果是1(与1&)则距离加一,然后右移一位。
int hammingDistance(int x, int y) {
int ans = 0;
int tmp = x ^ y;
while(tmp){
ans += tmp & 1;
tmp = tmp >> 1;
}
return ans;
}
https://leetcode.cn/problems/hamming-distance/
2. 投票
数组中出现次数 大于 ⌊ n/2 ⌋ 的元素
候选人(cand_num)初始化为 nums[0],票数 count 初始化为 1。当遇到与 cand_num 相同的数,则票数 count = count + 1,否则票数 count = count - 1。当票数 count 为 0 时,更换候选人,并将票数 count 重置为 1。
遍历完数组后,cand_num 即为最终答案。
int majorityElement(vector<int>& nums) {
// unordered_map<int, int> umap;
// int maxnum = 0;
// int ans = 0;
// for(int x:nums){
// umap[x] += 1;
// if(umap[x] > maxnum){
// maxnum = umap[x];
// ans = x;
// }
// }
// return ans;
int ans = nums[0];
int cnt = 1;
for(int x: nums){
cnt += x == ans? 1: (-1);
if(cnt == 0){
ans = x;
cnt = 1;
}
}
return ans;
// sort(nums.begin(), nums.end());
// return nums[nums.size()/2];
}
其他方法:哈希表,排序
https://leetcode.cn/problems/majority-element/
3. 原地哈希
找到所有数组中消失的数字
含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。找出所有在 [1, n] 范围内但没有出现在 nums 中的数字。
可以用哈希表或者数组记录出现情况,但是空间复杂度O(n),为此,可以在原数组记录。nums长为n,且范围是1~n,所以可以和0~n-1一一映射,遍历一遍数组,另nums[(x - 1)%n]加n,如果有元素小于n,说明元素对应的index + 1就是没出现。
vector<int> findDisappearedNumbers(vector<int>& nums) {
vector<int> ans;
int n = nums.size();
for(int x: nums){
nums[(x - 1)%n] += n;
}
for(int i = 0; i < n; ++i){
if(nums[i] <= n)
ans.push_back(i + 1);
}
return ans;
}
https://leetcode.cn/problems/find-all-numbers-disappeared-in-an-array/
找出重复数字
n个介于0,n-1的数字,找出任意重复。
可以用哈希,遍历一遍,但是需要O(n)空间。可以原地重排,nums[i] == i时,遍历下个数,num[num[i]] = nums[i],即当前数字对应索引的值和当前值相同,说明重复了,否则交换二者
int findRepeatNumber(vector<int>& nums) {
int ans = -1;
int size = nums.size();
int i = 0;
while(i < size){
if(nums[i] == i){
i++;
continue;
}
if(nums[nums[i]] == nums[i]){
return nums[i];
}else{
int tmp = nums[i];
nums[i] = nums[nums[i]];
nums[tmp] = tmp;
}
}
return -1;
}
https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/description/
4. 奇偶规律
整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 。
0的二进制中1的个数是0,奇数的二进制中1的个数是前一个偶数(二进制中1的个数)+ 1。偶数的二进制尾数是0,所以偶数的二进制中1的个数 = 偶数/2的二进制中1的个数。
vector<int> countBits(int n) {
vector<int> ans;
ans.resize(n + 1);
ans[0] = 0;
for(int i = 1; i <= n; ++i){
ans[i] = i%2==0? ans[i / 2]: ans[i - 1] + 1;
}
return ans;
}
https://leetcode.cn/problems/counting-bits/
5. 旋转图像
先转置再镜像
void rotate(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
for(int i = 0; i < m; ++i){
for(int j = 0; j < n; ++j){
if(i > j){
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
}
int mid = n / 2;
int t = (n % 2 == 0)? 2 * mid - 1 : 2 * mid;
for(int i = 0; i < m; ++i){
for(int j = 0; j < mid; ++j){
int tmp = matrix[i][j];
matrix[i][j] = matrix[i][t - j];
matrix[i][t - j] = tmp;
}
}
}
https://leetcode.cn/problems/rotate-image/description/

被折叠的 条评论
为什么被折叠?



