##ACM模式
头文件:
#include<bits/stdc++.h>
#include<iostream>
using namespace std;
##206. 反转链表
迭代法
C++代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while(curr){
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
递归法
C++代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(!head || !head->next){
return head;
}
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
};
##146. LRU 缓存
哈希表 + 双向链表
C++代码:
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
public:
LRUCache(int _capacity): capacity(_capacity), size(0) {
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head->next = tail;
tail->prev = head;
}
int get(int key) {
if (!cache.count(key)) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
DLinkedNode* node = cache[key];
moveToHead(node);
return node->value;
}
void put(int key, int value) {
if (!cache.count(key)) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode* node = new DLinkedNode(key, value);
// 添加进哈希表
cache[key] = node;
// 添加至双向链表的头部
addToHead(node);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode* removed = removeTail();
// 删除哈希表中对应的项
cache.erase(removed->key);
// 防止内存泄漏
delete removed;
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
DLinkedNode* node = cache[key];
node->value = value;
moveToHead(node);
}
}
void addToHead(DLinkedNode* node) {
node->prev = head;
node->next = head->next;
head->next->prev = node;
head->next = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};
##3. 无重复字符的最长子串
滑动窗口
在本题中实现滑动窗口,主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口:无重复字符的最长字符串。
窗口的起始位置如何移动:如果当前窗口内存在重复字符,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历字符串的指针,窗口的起始位置设置为字符串的起始位置就可以了。
C++代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size() == 0) return 0;
unordered_set<char> table;
int maxStr = 0;
int left = 0; // 滑动窗口起始位置
for(int i = 0; i < s.size(); i++){ // 滑动窗口结束位置
// 移动滑动窗口起始,直到窗口内无重复字符
while (table.find(s[i]) != table.end()){
table.erase(s[left]);
left++;
}
maxStr = max(maxStr,i-left+1);
table.insert(s[i]);
}
return maxStr;
}
};
补充:unordered_set操作
创建 unordered_set 容器,例:
unordered_set<ListNode*> visited;
插入元素,例:
table.insert(temp);
删除元素,例:
table.erase(temp);
查找元素,例:
table.count(temp);
table.find(s[i]) != table.end();
##215. 数组中的第K个最大元素
基于随机选取主元的快速排序
C++代码:
class Solution {
public:
int partition(vector<int>& nums, int left, int right){
int pivot = nums[right];
int i = left - 1; // 让 i 处于 left 前一位,等待
// 从 left 位开始循环,到 (right - 1) 位,因为 right 位是主元
for(int j = left; j <= right - 1; j++){
if(nums[j] <= pivot){ // <= 主元 即前移
i++;
swap(nums[i],nums[j]);
}
}
swap(nums[i + 1], nums[right]);
return i + 1;
}
int randomized_partition(vector<int>& nums, int left, int right){
int pivot = rand() % (right - left + 1) + left;
swap(nums[pivot], nums[right]);
return partition(nums, left, right);
}
void randomized_quicksort(vector<int>& nums, int left, int right){
if(left < right){
int pos = randomized_partition(nums, left, right);
randomized_quicksort(nums, left, pos - 1);
randomized_quicksort(nums, pos + 1, right);
}
}
int findKthLargest(vector<int>& nums, int k) {
randomized_quicksort(nums, 0, nums.size() - 1);
return nums[nums.size() - k];
}
};
补充:rand()
#include <stdlib.h>
int i = rand() % 100 // 在 0-100 内随机产生一个数
##25. K 个一组翻转链表
模拟方法
C++代码:
class Solution {
public:
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail){
ListNode* prev = nullptr;
ListNode* curr = head;
while(prev != tail){
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return {tail, head};
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* hair = new ListNode(0);
hair->next = head;
ListNode* prev = hair;
while(head){
ListNode* tail = prev;
for(int i = 0; i < k; i++){
tail = tail->next;
if(!tail) return hair->next;
}
ListNode* next = tail->next;
pair<ListNode*, ListNode*> result = myReverse(head, tail);
// 将反转后的子链首位重新链接
head = result.first;
tail = result.second;
prev->next = head;
tail->next = next;
// 将 head 放置于剩余未反转子链开头, prev 置于 head 前
prev = tail;
head = tail->next;
}
return hair->next;
}
};
##15. 三数之和
双指针
动画效果如下:
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
依然还是在数组中找到 abc
使得a + b +c =0
,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]
。
接下来如何移动left
和right
呢, 如果nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了,因为数组是排序后了,所以right
下标就应该向左移动,这样才能让三数之和小一些。
如果 nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了,left
就向右移动,才能让三数之和大一些,直到left
与right
相遇为止。
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
C++代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
int target = 0;
for (int first = 0; first < n; first++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[first] > 0) {
return ans;
}
// first去重
// first 第二个数组下标开始时,其值(nums[first]) 等于上一个遍历过的下标(nums[first - 1])时,跳过该值
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
int second = first + 1;
int third = n - 1;
while (second < third) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) third--;
while (right > left && nums[left] == nums[left + 1]) second++;
*/
if (nums[first] + nums[second] + nums[third] > 0) {
third--;
}
else if (nums[first] + nums[second] + nums[third] < 0) {
second++;
}
else {
ans.push_back(vector<int>{nums[first], nums[second], nums[third]});
// 去重逻辑应该放在找到一个三元组之后,因为此时应同时迭代,
// 因为此时first不变的情况,second和third应同时收缩,
// 如果其收缩后的值 nums[third - 1] 等于收缩前的值 nums[third],会造成组合重复。
while (second < third && nums[third] == nums[third - 1]) third--;
while (second < third && nums[second] == nums[second + 1]) second++;
// 找到答案时,双指针同时收缩
second++;
third--;
}
}
}
return ans;
}
};
##912. 排序数组
基于随机选取主元的快速排序
C++代码:
class Solution {
int partition(vector<int>& nums, int left, int right){
int pivot = nums[right];
int i = left - 1;
for(int j = left; j < right; j++){
if(nums[j] <= pivot){
i++;
swap(nums[i], nums[j]);
}
}
swap(nums[i + 1], nums[right]);
return i + 1;
}
int randomized_partition(vector<int>& nums, int left, int right){
int i = rand() % (right - left + 1) + left ; // 随机选一个作为我们的主元
swap(nums[i], nums[right]);
return partition(nums, left, right);
}
// 随机主元快排
void randomized_quicksort(vector<int>& nums, int left, int right){
if(left < right){
int pos = randomized_partition(nums, left, right);
randomized_quicksort(nums, left, pos - 1);
randomized_quicksort(nums, pos + 1, right);
}
}
public:
vector<int> sortArray(vector<int>& nums) {
randomized_quicksort(nums, 0, (nums.size() - 1));
return nums;
}
};
##53. 最大子数组和
动态规划
C++代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int prev = 0;
int maxAns = nums[0];
for(int i = 0; i < nums.size(); i++){
prev = max(prev + nums[i], nums[i]);
maxAns = max(maxAns, prev);
}
return maxAns;
}
};
解法2
C++代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT_MIN;
int count = 0;
for(int i = 0; i < nums.size(); i++){
count += nums[i];
if(count > result) result = count;
if(count < 0) count = 0;
}
return result;
}
};
##21. 合并两个有序链表
递归
C++代码:
// 如果要求去重,就单独考虑 list1->val = list2->val 的情况
/* 去重写法
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr) return list2;
else if(list2 == nullptr) return list1;
else if(list1->val < list2->val){
list1->next = mergeTwoLists(list1->next, list2);
return list1;
}
else if(list1->val > list2->val){
list2->next = mergeTwoLists(list1, list2->next);
return list2;
}
else{
list1->next = mergeTwoLists(list1->next, list2->next);
return list1;
}
}
};
*/
// 23题 是合并K个升序链表,是这道题变形
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//终止条件
if (l1 == nullptr) {
return l2;
}
else if (l2 == nullptr) {
return l1;
}
//自调用函数
else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
##1. 两数之和
哈希表
C++代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for(int i = 0; i < nums.size(); i++){
if(map.find(target - nums[i]) != map.end()){
return {map[target - nums[i]], i};
}
else{
map[nums[i]] = i;
}
}
return {};
}
};
##141. 环形链表
快慢指针
C++代码:
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next){
// 因为 slow 和 fast 起始点相同,所有先走再判断。
slow = slow->next;
fast = fast->next->next;
if(slow == fast) return true;
}
return false;
}
};
##102. 二叉树的层序遍历
层序遍历原理
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
这样就实现了层序从左到右遍历二叉树。
C++代码:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front(); // 因为要使用此节点,将节点从队列删除前,先保存节点。
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
补充:queue 操作
queue<TreeNode*> que; // 创建队列
queue.front(); // 返回 queue 中第一个元素的引用。
queue.back(); // 返回 queue 中最后一个元素的引用。
que.push(node->left); // 在 queue 的尾部添加 node->left 的副本。
que.pop() // 删除 queue 中的第一个元素。
que.size() // 返回 queue 中元素的个数。
que.empty() // 如果 queue 中没有元素的话,返回 true。
##121. 买卖股票的最佳时机
动态规划
动态规划一般分为一维、二维、多维(使用状态压缩),对应形式为 dp(i)
、dp(i)(j)
、二进制dp(i)(j)
。
- 动态规划做题步骤
明确 dp(i)
应该表示什么(二维情况:dp(i)(j)
);
根据 dp(i)
和 dp(i-1)
的关系得出状态转移方程;
确定初始条件,如 dp(0)
。
- 本题思路
其实方法一的思路不是凭空想象的,而是由动态规划的思想演变而来。这里介绍一维动态规划思想。
dp[i]
表示前 i
天的最大利润,因为我们始终要使利润最大化,则:
dp[i] = max(dp[i-1], prices[i]-minprice)
C++代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0; // 边界条件
int minprice = prices[0];
vector<int> dp (n, 0);
for (int i = 1; i < n; i++){
minprice = min(minprice, prices[i]);
dp[i] = max(dp[i - 1], prices[i] - minprice);
}
return dp[n - 1];
}
};
##160. 相交链表
双指针
headA + headB
与 headB + headA
对比。
C++代码:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* currA = headA;
ListNode* currB = headB;
while(currA != currB){
if(currA == nullptr) currA = headB;// A遍历结束,但B未遍历结束时,即A、B不等长。
else currA = currA->next;
if(currB == nullptr) currB = headA;// B遍历结束,但A未遍历结束时,即A、B不等长。
else currB = currB->next;
}
return currA;
}
};
哈希集合
C++代码:
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
unordered_set<ListNode*> visited;
ListNode* temp = headA;
while (temp != nullptr)
{
visited.insert(temp);
temp = temp->next;
}
temp = headB;
while (temp != nullptr)
{
if (visited.count(temp))
return temp;
temp = temp->next;
}
return nullptr;
}
};
##103. 二叉树的锯齿形层序遍历
层序遍历
只是在加入每层遍历 vec
到 ans
前,多了一步反转判断。
C++代码:
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> ans;
if(root != nullptr) que.push(root);
bool rev = 0;
while(!que.empty()){
int size = que.size();
vector<int> vec;
for(int i = 0; i < size; i++){
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
if(rev) reverse(vec.begin(), vec.end());
rev = !rev; // 每次反转开关判断结束,更替开关状况。
ans.push_back(vec);
}
return ans;
}
};
##88. 合并两个有序数组
逆向双指针
C++代码:
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
m--, n--; // m, n 分别指向 nums1 和 nums2 元素末端下标
int len = nums1.size() - 1;
while(n >= 0){ // nums2 遍历结束即结束循环
if(m == -1 || nums1[m] < nums2[n]) {
nums1[len] = nums2[n];
n--;
}
else {
nums1[len] = nums1[m];
m--;
}
len--;
}
}
};
##20. 有效的括号
栈方法
由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
C++代码:
class Solution {
public:
bool isValid(string s) {
unordered_map<char, char> pairs = {
{')', '('}, {']', '['}, {'}', '{'}
};
stack<char> cache;
for(const auto& ch: s){
if(pairs.count(ch)){
if(cache.empty() || cache.top() != pairs[ch]) return false;
else cache.pop();
}
else cache.push(ch);
}
return cache.empty();
}
};
补充:stack 操作
stack<char> cache; // 创建堆栈
stack.top(); // 返回堆栈中最上层元素的引用。
stack.push(ch); // 在堆栈的上层添加 ch 的副本。
stack.pop() // 删除堆栈中最上层元素。
stack.size() // 返回堆栈中元素的个数。
stack.empty() // 如果堆栈中没有元素的话,返回 true。
##236. 二叉树的最近公共祖先
递归遍历
当我们用递归去做这个题时不要被题目误导,应该要明确一点
这个函数的功能有三个:给定两个节点 p
和 q
1. 如果 `p` 和 `q`都存在,则返回它们的**公共祖先**;
2. 如果只存在一个,则返回存在的一个;
3. 如果 `p` 和 `q`都不存在,则返回`NULL`
本题说给定的两个节点都存在,那自然还是能用上面的函数来解决
具体思路:
(1) 如果当前结点 root
等于 NULL
,则直接返回 NULL
(2) 如果 root
等于 p
或者 q
,那这棵树一定返回 p
或者 q
(3) 然后递归左右子树,因为是递归,使用函数后可认为左右子树已经算出结果,用 left
和 right
表示
(4) 此时若 left
为空,那最终结果只要看 right
;若 right
为空,那最终结果只要看 left
(5) 如果 left
和 right
都非空,因为只给了 p
和 q
两个结点,都非空,说明一边一个,因此root
是他们的最近公共祖先
(6) 如果 left
和 right
都为空,则返回空(其实已经包含在前面的情况中了)
时间复杂度是 O(n)
:每个结点最多遍历一次或用主定理,空间复杂度是 O(n)
:需要系统栈空间
C++代码:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 终止条件:遍历结束,或者找到 p, q 节点。
if(root == nullptr) return nullptr;
if(root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left == nullptr) // 表明目前节点 root 的左子树不存在 p, q, 公共祖先 节点,
return right; // 所以目前节点 root 的传递值只与右子树有关。
else if(right == nullptr) // 同理
return left;
else // 如果左右子树的传递值都不为空,代表分别找到p, q。
return root; // 传递目前节点 root 为公共祖先,回溯该节点。
}
};
补充:前中后序递归遍历
前序遍历:中 ->
左 ->
右
void traversal(TreeNode* curr, vector<int>& vec){
if(curr == nullptr) return;
vec.push_back(curr->val); // 中
traversal(curr->left, vec); // 左
traversal(curr->right, vec); // 右
}
中序遍历:左 ->
中 ->
右
void traversal(TreeNode* curr, vector<int>& vec){
if(curr == nullptr) return;
traversal(curr->left, vec); // 左
vec.push_back(curr->val); // 中
traversal(curr->right, vec); // 右
}
后序遍历:左 ->
右 ->
中
void traversal(TreeNode* curr, vector<int>& vec){
if(curr == nullptr) return;
traversal(curr->left, vec); // 左
traversal(curr->right, vec); // 右
vec.push_back(curr->val); // 中
}
##5. 最长回文子串
动态规划
1.dp[i][j]
表示子串s[i..j]
是否为回文子串,值1为是,值0为否
2.(j - i + 1)
即子串长度<3
时: dp[i][j] = (s[i] == s[j])
(j - i + 1)
即子串长度>=3
时:dp[i][j] = dp[i+1]dp[j-1] ^ (s[i] == s[j])
3.所有dp[i][i]
的初始值为1,其余为0
C++代码:
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
// 边界考虑
if(n == 1) return s;
// dp[i][j] 表示 s[i..j] 是否是回文串
vector<vector<int>> dp(n, vector<int>(n, 0));
int maxLen = 1;
int startIndex = 0;
// 初始化:所有长度为 1 的子串都是回文串
for(int i = 0; i < n; i++){
dp[i][i] = 1;
}
// 递推开始
// 先枚举子串长度 len
for(int len = 2; len <= n; len++){
// 枚举左边界,左边界的上限设置可以宽松一些
for(int i = 0; i < n; i++){
// 由 len 和 i 可以确定右边界,即 j - i + 1 = len 得
int j = i + len - 1;
// 如果右边界越界,就可以退出当前循环
if(j > n - 1) break;
if(s[i] != s[j])
dp[i][j] = 0;
else {
if(j - i < 2)
dp[i][j] = 1;
else
dp[i][j] = dp[i + 1][j - 1];
}
// dp[i][j] = 1 表示子串 s[i..j] 是回文,更新最长回文的长度和起始位置
if((j - i + 1) > maxLen && dp[i][j]){
maxLen = j - i + 1;
startIndex = i;
}
}
}
return s.substr(startIndex, maxLen);
}
};
补充:一维二维vector初始化
vector <int> vec(10,90); // 将10个一维动态数组初始为90
vector<vector<int> > vec(row,vector<int>(col,0)); // 初始化row * col二维动态数组,初始化值为0
补充: vector排序、反转和复制子串
vector<int> vec;
sort(vec.begin(),vec.end());// 从小到大排序
reverse(vec.begin(),vec.end());// 反转到从大到小
int pos = 0, len = vec.size();// pos 为子串起始下标,len 为子串长度。
vector<int> subvec = vec.substr(pos, len);// 获得字符串vec中从第 pos 位开始的长度为 len 的字符串
##33. 搜索旋转排序数组
二分查找
C++代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
if(!n) return -1;
// 非必须条件
if(n == 1) {
return target == nums[0] ? 0 : -1;
}
int left = 0;
int right = nums.size() - 1;
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == target) return mid;
// 不存在数 x 使得 nums[n - 1] < x < nums[0] 成立
// 左子数组有序
if(nums[0] <= nums[mid]){
// target 在 [left, mid) 左区间内
if(nums[left] <= target && target < nums[mid])
right = mid - 1;
else
left = mid + 1;
}
// 右子数组有序
else{
// target 在 (mid, right] 右区间内
if(nums[mid] < target && target <= nums[right])
left = mid + 1;
else
right = mid - 1;
}
}
return -1;
}
};
补充:二分查找模板
我们定义 target
是在一个在左闭右闭的区间里,也就是[left, right]
(这个很重要非常重要)。
区间的定义这就决定了二分法的代码应该如何写,因为定义target
在[left, right]
区间,所以有如下两点:
while (left <= right)
要使用<=
,因为left == right
是有意义的,所以使用<=
if (nums[middle] > target)
时,right
要赋值为middle - 1
,因为当前这个nums[middle]
一定不是target
,那么接下来要查找的左区间结束下标位置就是middle - 1
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
C++代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
//也可写成 int middle = left + right >> 1;
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
##200. 岛屿数量
深度优先搜索(dfs)
将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0。
最终岛屿的数量就是我们进行深度优先搜索的次数。
C++代码:
class Solution {
public:
void dfs(vector<vector<char>>& grid, int r, int c){
int row = grid.size();
int col = grid[0].size();
grid[r][c] = '0';
if(r - 1 >= 0 && grid[r - 1][c] == '1') dfs(grid, r - 1, c);
if(c + 1 < col && grid[r][c + 1] == '1') dfs(grid, r, c + 1);
if(r + 1 < row && grid[r + 1][c] == '1') dfs(grid, r + 1, c);
if(c - 1 >= 0 && grid[r][c - 1] == '1') dfs(grid, r, c - 1);
}
int numIslands(vector<vector<char>>& grid) {
int row = grid.size();
if(!row) return 0;
int col = grid[0].size();
int count = 0;
for(int i = 0; i < row; i++){
for(int j = 0; j < col; j++){
if(grid[i][j] == '1'){
count++;
dfs(grid, i, j);
}
}
}
return count;
}
};
##46. 全排列
回溯算法
这个问题可以看作有 n 个排列成一行的空格,我们需要从左往右依此填入题目给定的 n 个数,每个数只能使用一次。那么很直接的可以想到一种穷举的算法,即从左往右每一个位置都依此尝试填入一个数,看能不能填完这 n 个空格,在程序中我们可以用「回溯法」来模拟这个过程。
例如:0 位的数字,可以从原[0, n - 1]位的数字从选择;
每次选定 0 位的数字后;
1 位的数字,可以从此次选定完 0 位后[1, n - 1]位的数字从选择;
…
每次回溯要撤销掉当前位置的数字选择;
C++代码:
class Solution {
public:
void backtracking(vector<vector<int>>& ans, vector<int>& nums, int pos, int len){
// 终止条件:所有位置都放置完数
if(pos == len){
ans.push_back(nums);
return;
}
// pos位数字的选择:[pos,len - 1]内选择
for(int i = pos; i < len; i++){
swap(nums[pos], nums[i]); // pos 位选择原 i 位的数字
backtracking(ans, nums, pos + 1, len); // 递归 pos + 1 位的选择
swap(nums[pos], nums[i]);// 撤销 pos 位的选择
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> ans;
int len = nums.size();
backtracking(ans, nums, 0, len);
return ans;
}
};
补充:回溯模板
回溯算法模板框架如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
##415. 字符串相加
模拟
本题只需要对两个大整数模拟「竖式加法」的过程。
C++代码:
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.size() - 1, j = num2.size() - 1;
string ans = "";
int count = 0;
while(i >= 0 || j >= 0 || count != 0){
int x = (i >= 0) ? num1[i] - '0' : 0;
int y = (j >= 0) ? num2[j] - '0' : 0;
int result = x + y + count;
ans.push_back((result % 10 + '0'));
count = result / 10;
i--;
j--;
}
// 计算完以后的答案需要翻转过来
reverse(ans.begin(),ans.end());
return ans;
}
};
补充:char型数字 与 int型数字 间转换
char a =‘9’;
int b = 9;
b = a - ‘0’; // 字符减去’0‘对应的asc2码,字符9变成数字9 char->int
a = b + ‘0’; // 数字加上’0‘对应的asc2码,数字9变成字符9 int ->char
补充:string操作
string str; // 生成一个空字符串
str.resize(10); // 表示设置当前string里的串大小,若设置大小大于当前串长度,则用字符\0来填充多余的
str.puch_back ('A'); // 在str1末尾添加一个'A'字符,参数必须是字符形式
str.empty(); // 判断为空, 为空返回true
##92. 反转链表 II
模拟方法(与25题相似)
C++代码:
class Solution {
public:
pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail){
ListNode* prev = nullptr;
ListNode* curr = head;
while(prev != tail){
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return {tail, head};
}
ListNode* reverseBetween(ListNode* head, int left, int right) {
while(head->next == nullptr || left == right) return head;
ListNode* hair = new ListNode();
hair->next = head;
ListNode* prev = hair;
ListNode* tail = head;
for(int i = 1; i < right; i++){
if(i < left){
prev = head;
head = head->next;
}
tail = tail->next;
}
ListNode* next = tail->next;
pair<ListNode*, ListNode*> result = myReverse(head, tail);
head = result.first;
tail = result.second;
prev->next = head;
tail->next = next;
return hair->next;
}
};
##23. 合并K个升序链表
递归(21题进阶)
C++代码:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2){
if(list1 == nullptr) return list2;
else if(list2 == nullptr) return list1;
else if(list1->val < list2->val){
list1->next = mergeTwoLists(list1->next, list2);
return list1;
}
else{
list2->next = mergeTwoLists(list1,list2->next);
return list2;
}
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode* ans = nullptr;
for(auto list:lists){
ans = mergeTwoLists(ans,list);
}
return ans;
}
};
##142. 环形链表 II
快慢指针(141题进阶)
我们使用两个指针,fast
与 slow
。它们起始都位于链表的头部。随后,slow
指针每次向后移动一个位置,而 fast
指针向后移动两个位置。如果链表中存在环,则 fast
指针最终将再次与 slow
指针在环中相遇,且在第二圈开始前。
如下图所示,设链表中环外部分的长度为 a
。 slow
指针进入环后,又走了 b
的距离与 fast
相遇。此时,slow
指针走过的总距离为 a+b
, fast
指针已经走完了环的 n
圈,因此它走过的总距离为 a+n(b+c)+b=a+(n+1)b+nc
。
根据题意,任意时刻, fast
指针走过的距离都为slow
指针的 2 倍。因此,有:
a+(n+1)b+nc = 2(a+b)
a = c+(n-1)(b+c)
即 a = c + m(b+c)
,我们会发现:从相遇点到入环点的距离加上 m
圈的环长,恰好等于从链表头部到入环点的距离。
因此,当发现 fast
与 slow
相遇时,我们再额外使用一个指针 ptr
。起始,它指向链表头部;随后,它和 slow
每次向后移动一个位置。最终,它们会在入环点相遇。
C++代码:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next){
// 因为 slow 和 fast 起始点相同,所有先走再判断。
slow = slow->next;
fast = fast->next->next;
if(slow == fast){
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
##54. 螺旋矩阵
按层模拟
循环次数由最短边决定
最短边为奇数,则最内层为单边,需单独考虑
C++代码:
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> ans;
int row = matrix.size();
int col = matrix[0].size();
// 四条边遍历的始末点
int starti = 0, startj = 0;
int endi = row - 1, endj = col - 1;
// 最短边 minlen 为奇数,最内圈为单边,需单独考虑
int minlen = min(row, col);
int loop = minlen / 2;
int i, j;
while(loop--){
i = starti, j = startj;
for(; j < endj; j++){
ans.push_back(matrix[i][j]);
}
for(; i < endi; i++){
ans.push_back(matrix[i][j]);
}
for(; j > startj; j--){
ans.push_back(matrix[i][j]);
}
for(; i > starti; i--){
ans.push_back(matrix[i][j]);
}
starti++, startj++;
endi--, endj--;
}
if(minlen % 2){
if(minlen == row){// row 为最短边
for(int j = startj; j <= endj; j++){ // 要遍历到 endj
ans.push_back(matrix[starti][j]);
}
}
else{// col 为最短边
for(int i = starti; i <= endi; i++){ // 要遍历到 endi
ans.push_back(matrix[i][startj]);
}
}
}
return ans;
}
};
##300. 最长递增子序列
动态规划
dp[i]
: 考虑前 i
个元素的最长上升子序列的长度,注意 nums[i]
必须被选取。
状态转移方程:dp[i] = max(dp[i], dp[j] + 1)
, if(0 <= j < i && nums[i] > nums[j])
初值:dp[i] = 1, (0<= i < nums.size())
, 因为自身算长度为 1 的递增序列。
C++代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(!n) return 0;
vector<int> dp(n, 1);
for(int i = 0; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = max(dp[i],dp[j] + 1);
}
}
}
return *max_element(dp.begin(),dp.end());
}
};
补充:max_element()
// max_element(r, r+6),返回数组r中[0, 6)之间的最大值的迭代器,
cout << *max_element(dp.begin(),dp.end()) << endl; // 输出数组 dp 中的最大值
##42. 接雨水
动态规划
对于下标 i
,下雨后水能到达的最大高度等于下标 i
两边的最大高度的最小值,下标 i
处能接的雨水量等于下标 i
处的水能到达的最大高度减去 height[i]
。
朴素的做法是对于数组 height
中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。假设数组 height
的长度为 n
,该做法需要对每个下标位置使用 O(n)
的时间向两边扫描并得到最大高度,因此总时间复杂度是 O(n^2)
。
C++代码:
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
// 正向遍历
vector<int> leftMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = max(leftMax[i - 1], height[i]);
}
// 反向遍历
vector<int> rightMax(n);
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = max(rightMax[i + 1], height[i]);
}
int ans = 0;
for (int i = 0; i < n; i++) {
ans += min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
};
##704. 二分查找
二分查找
C++代码:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while(left<=right){
int middle = left + (right-left)/2;
if(nums[middle]>target){
right = middle - 1;
}
else if(nums[middle]<target){
left = middle + 1;
}
else{
return middle;
}
}
return -1;
}
};
##94. 二叉树的中序遍历
递归:中序遍历
C++代码:
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
##232. 用栈实现队列
栈方法
队列是一种 先进先出(first in - first out, FIFO)的数据结构,队列中的元素都从后端(rear)入队(push),从前端(front)出队(pop)。
实现队列最直观的方法是用链表,但在这篇文章里我会介绍另一个方法 - 使用栈。
栈是一种 后进先出(last in - first out, LIFO)的数据结构,栈中元素从栈顶(top)压入(push),也从栈顶弹出(pop)。
为了满足队列的 FIFO 的特性,我们需要用到两个栈,用它们其中一个来反转元素的入队顺序,用另一个来存储元素的最终顺序。
C++代码:
class MyQueue {
private:
stack<int> inStack, outStack;
void outstackinput(){
if(outStack.empty()){
while(!inStack.empty()){
outStack.push(inStack.top());
inStack.pop();
}
}
}
public:
MyQueue() {
}
void push(int x) {
inStack.push(x);
}
int pop() {
outstackinput();
int result = outStack.top();
outStack.pop();
return result;
}
int peek() {
outstackinput();
return outStack.top();
}
bool empty() {
if(outStack.empty()&&inStack.empty()){
return true;
}
else{
return false;
}
}
};
##143. 重排链表
寻找链表中点 + 链表逆序 + 合并链表
注意到目标链表即为将原链表的左半端和反转后的右半端合并后的结果。
划分为三步:
1. 找到原链表的中点(参考「876. 链表的中间结点」)。
我们可以使用快慢指针来 O(N) 地找到链表的中间节点。
2. 将原链表的右半端反转(参考「206. 反转链表」)。
我们可以使用迭代法实现链表的反转。
3. 将原链表的两端合并。
因为两链表长度相差不超过 11,因此直接合并即可。
C++代码:
class Solution {
public:
ListNode* middleNode(ListNode* head){
ListNode* slow = head;
ListNode* fast = head->next;
while(fast && fast->next){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* reverseList(ListNode* head){
ListNode* prev = nullptr;
ListNode* curr = head;
while(curr){
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
void mergeList(ListNode* l1, ListNode* l2){
ListNode* l1_tmp = nullptr;
ListNode* l2_tmp = nullptr;
while(l1 && l2){
l1_tmp = l1->next;
l2_tmp = l2->next;
l1->next = l2;
l1 = l1_tmp;
l2->next = l1;
l2 = l2_tmp;
}
}
void reorderList(ListNode* head) {
if(head == nullptr || head->next == nullptr) return;
ListNode* middle = middleNode(head);
ListNode* list1 = head;
ListNode* list2 = reverseList(middle->next);
middle->next = nullptr;
mergeList(list1, list2);
}
};
##199. 二叉树的右视图
层序遍历
C++代码:
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> que;
if(root != NULL) que.push(root);
vector<int> res;
while(!que.empty()){
int size = que.size();
int tmp;
for(int i = 0; i < size; i++){
TreeNode* node = que.front();
que.pop();
tmp = node->val;
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
res.push_back(tmp);
}
return res;
}
};
##124. 二叉树中的最大路径和
递归遍历
C++代码:
class Solution {
private:
int maxSum = INT_MIN;
public:
int maxGain(TreeNode* root){
if(root == nullptr) return 0;
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = max(maxGain(root->left), 0);
int rightGain = max(maxGain(root->right), 0);
// 当前父节点下的最大值
int currGain = root->val + leftGain + rightGain;
// 更新答案
maxSum = max(maxSum, currGain);
// 当前父节点的最大贡献值
int maxNode = root->val + max(leftGain, rightGain);
return maxNode;
}
int maxPathSum(TreeNode* root) {
int result = maxGain(root);
return maxSum;
}
};
##70. 爬楼梯
动态规划
C++代码:
class Solution {
public:
int climbStairs(int n) {
if(n<=2) return n;
vector<int> dp(n+1);
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
##剑指 Offer 22. 链表中倒数第k个节点
快慢指针
C++代码:
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode* slow = head;
ListNode* fast = head;
for(int i = 0; i < k; i++){
fast = fast->next;
}
while(fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
##56. 合并区间
排序
C++代码:
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
// 先对数组排序
sort(intervals.begin(), intervals.end());
vector<vector<int>> result;
vector<int> prev = intervals[0];
int n = intervals.size();
for(int i = 1; i < n; i++){
// 当前数组左端点 > 前数组右端点
if(intervals[i][0] > prev[1]){
result.push_back(prev);
prev = intervals[i];
}
else{
prev[1] = max(prev[1], intervals[i][1]);
}
}
result.push_back(prev);
return result;
}
};
##69. x 的平方根
二分查找
C++代码:
class Solution {
public:
int mySqrt(int x) {
int left = 0;
int right = x;
while(left <= right){
int middle = left + (right - left) / 2;
long target = (long)middle * middle;
if(target > x) right = middle - 1;
else if(target < x) left = middle + 1;
else return middle;
}
return right;
}
};
##82. 删除排序链表中的重复元素 II
快慢指针遍历
C++代码:
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode* prev = new ListNode();
prev->next = head;
ListNode* slow = prev;
ListNode* fast = head;
while(fast && fast->next){
if(fast->val == fast->next->val){
while(fast->next && fast->val == fast->next->val){
ListNode* node = fast->next;
fast->next = node->next;
delete node;
}
ListNode* node = fast;
fast = fast->next;
slow->next = fast;
delete node;
}
else{
fast = fast->next;
slow = slow->next;
}
}
return prev->next;
}
};
##19. 删除链表的倒数第 N 个结点
快慢指针
C++代码:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* prev = new ListNode();
prev->next = head;
ListNode* slow = prev;
ListNode* fast = head;
while(n--){
fast = fast->next;
}
while(fast){
slow = slow->next;
fast = fast->next;
}
ListNode* node = slow->next;
slow->next = node->next;
delete node;
return prev->next;
}
};
##2. 两数相加
模拟
C++代码:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* prev = new ListNode();
ListNode* curr = prev;
int carry = 0;
while(l1 || l2 || carry != 0){
int x = l1 ? l1->val : 0;
int y = l2 ? l2->val : 0;
int sum = x + y + carry;
ListNode* node = new ListNode(sum % 10);
curr->next = node;
curr = curr->next;
carry = sum / 10;
if(l1) l1 = l1->next;
if(l2) l2 = l2->next;
}
return prev->next;
}
};
##148. 排序链表
归并排序
C++代码:
class Solution {
public:
ListNode* sortList(ListNode* head) {
return sortList(head, nullptr);
}
// 左闭右开:[head, tail)
ListNode* sortList(ListNode* head, ListNode* tail){
if(head == nullptr) return head;
if(head->next == tail){
head->next = nullptr; // 对子链进行截断
return head;
}
ListNode* mid = findMiddleNode(head, tail);
return mergeLists(sortList(head, mid), sortList(mid, tail));
}
// 在左闭右开:[head, tail) 的区间内寻找中间数
ListNode* findMiddleNode(ListNode* head, ListNode* tail){
ListNode* slow = head;
ListNode* fast = head;
while(fast != tail && fast->next != tail){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* mergeLists(ListNode* list1, ListNode* list2){
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
if(list1->val < list2->val){
list1->next = mergeLists(list1->next, list2);
return list1;
}
else{
list2->next = mergeLists(list1, list2->next);
return list2;
}
}
};
快速排序
C++代码:
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
// 链表开头元素定为主元
int pivot = head->val;
ListNode* left = new ListNode();
ListNode* right = new ListNode();
// 保留指向头部的节点
ListNode* hLeft = left;
ListNode* hRight = right;
ListNode* node = head->next;
while(node != nullptr){
int val = node->val;
// left 中存 < 主元的节点
if(val < pivot){
left->next = node;
left = left->next;
}
// right 中存 >= 主元的节点
else{
right->next = node;
right = right->next;
}
node = node->next;
}
// 此时的 head 为主元,即 left -> 主元
right->next = nullptr;
left->next = head;
head->next = nullptr;
// 分别对 left 和 right 链表进行排序
left = sortList(hLeft->next);
right = sortList(hRight->next);
// 主元比 left 中其他元素都大,所有一定在 left 末尾
// 主元 -> right
head->next = right;
// 最后 left -> 主元 -> right
return left;
}
};
ail);
return mergeLists(sortList(head, mid), sortList(mid, tail));
}
// 在左闭右开:[head, tail) 的区间内寻找中间数
ListNode* findMiddleNode(ListNode* head, ListNode* tail){
ListNode* slow = head;
ListNode* fast = head;
while(fast != tail && fast->next != tail){
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
ListNode* mergeLists(ListNode* list1, ListNode* list2){
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
if(list1->val < list2->val){
list1->next = mergeLists(list1->next, list2);
return list1;
}
else{
list2->next = mergeLists(list1, list2->next);
return list2;
}
}
};
## 快速排序
C++代码:
```cpp
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(head == nullptr || head->next == nullptr)
return head;
// 链表开头元素定为主元
int pivot = head->val;
ListNode* left = new ListNode();
ListNode* right = new ListNode();
// 保留指向头部的节点
ListNode* hLeft = left;
ListNode* hRight = right;
ListNode* node = head->next;
while(node != nullptr){
int val = node->val;
// left 中存 < 主元的节点
if(val < pivot){
left->next = node;
left = left->next;
}
// right 中存 >= 主元的节点
else{
right->next = node;
right = right->next;
}
node = node->next;
}
// 此时的 head 为主元,即 left -> 主元
right->next = nullptr;
left->next = head;
head->next = nullptr;
// 分别对 left 和 right 链表进行排序
left = sortList(hLeft->next);
right = sortList(hRight->next);
// 主元比 left 中其他元素都大,所有一定在 left 末尾
// 主元 -> right
head->next = right;
// 最后 left -> 主元 -> right
return left;
}
};