1.leetcode刷到200道
2.熟悉下求图最小路径怎么写(bfs,dfs,迪杰斯特拉)
3.netco项目理清流程,底层做进一步理解,照着它的代码手撸一遍;
#include
using namespace std;
int main () {
int num;
vector value;
while(cin >> num) {
value.push_back(num);
if (cin.get() == '\n') /这里判断条件不能写反,不然数组要少一个输入
break;
}
return 0;
}
如果要输入多行,且数量未知
while(cin >> num) {
value.push_back(num);
} 我们需要用回车 + CTRL + Z 来终止输入;测试平台会自动处理,可以提交通过。
std::ios::sync_with_stdio(false);//解除C++流与C流的同步
std::cin.tie(nullptr); //解除cin与cout的绑定,加速
std::cout.tie(nullptr);
思路:快速排序采用分治法。首先从数列中选取一个元素作为中间值。依次遍历数据,所有比中间值小的元素放在左边,所有比中间值大的元素放在右边。然后按照此方法对左右两个子序列分别进行递归操作,直到所有数据有序;
//快速排序
template
int Partition(T data[],int left,int right)
{
T pivot=data[left];
while(leftpivot)
right--;
data[left]=data[right];
while(left
void QuickSort(T data[],int left,int right)
{
if(left
nums
和整数 k
,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。//采用基于快速排序的选择方法
//每次分治,会选择一个元素i,把比它小的放在左边,把比它大的放在右边,那么这个元素就是第i大的元素
//如果i == nums.size() - k,那么找到结果,直接返回
//与快速排序不同,可以进行剪枝
//如果 i > nums.size() - k,那么直接在左半部分递归
//否则,在右半部分递归
//时间复杂度的期望是O(n),空间复杂度(log(n))
int result = 0;
//选择最左边的元素为基准元素,比它小的放在左边,比它大的放在右边
int Partition(vector& nums, int left, int right) {
int mid = nums[left];
while (left < right) {
while (left < right && nums[right] > mid)
right--;
nums[left] = nums[right];
while (left < right && nums[left] <= mid)
left++;
nums[right] = nums[left];
}
nums[left] = mid;
return left;
}
//递归快排
void QuickSort(vector& nums, int left, int right, int k) {
if (left <= right) {
int p = Partition(nums, left, right);
//如果刚好第K大元素,直接返回
if (p == nums.size() - k) {
result = nums[p];
return;
//剪枝过程
}else if (p > nums.size() - k) {
QuickSort(nums, left, p - 1, k);
}else {
QuickSort(nums, p + 1, right, k);
}
}
}
int findKthLargest(vector& nums, int k) {
QuickSort(nums, 0, nums.size() - 1, k);
return result;
}
//堆排序:构建一个完全二叉树,每个节点都比它的左右孩子大,那么根节点就是最大值,然后根节点跟最后一个节点对换,在进行一次重构,继续获取第二个最大值
//算法思路:因为是一个完全二叉树,那么nums[length/2]一定是第一个叶子节点,nums[length/2-1]是最后一个非叶子节点,从最后一个非叶子节点出发,依次调整二叉树,直到根节点,那么就完成了大顶堆的构建
//一次调整
void HeapAjust(vector& arr, int start, int end) {
int tmp = arr[start];
for (int i = 2 * start + 1; i <= end; i = i * 2 + 1)
{
if (i < end && arr[i] < arr[i + 1]) {//有右孩子并且左孩子小于右孩子
i++;
}//i一定是左右孩子的最大值
if (arr[i] > tmp) {
arr[start] = arr[i];
start = i;
}
else {
break;
}
}
arr[start] = tmp;
}
//堆排序
void HeapSort(vector& arr, int len) {
//第一次建立大根堆,从最后一个非叶子节点出发,依次调整二叉树
for (int i = len / 2 - 1; i >= 0; i--) {
HeapAjust(arr, i, len - 1);
}
//二叉树根节点与最后一个元素对换,调整二叉树
int tmp;
for (int i = 0; i < len - 1; i++) {
tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
HeapAjust(arr, 0, len - 1 - i - 1);
}
}
题目描述:给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
思路:看到有序和O(log(n))的要求,大概率是二分法
//二分法用于找左右边界的题目
//下面的写法都是找到target左右的第一个元素
//关键在于=号的位置
//如果等号直接返回,就是二分查找
int left = 0;
int right = nums.size() - 1;
while(left <= right) {//这里统一写法,left<=right,这样即使[left,right]也是合法区间,对于一个元素的情况,也可以考虑进去
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {//这里如果>=,那么就到左边去找,这样会找到左边界
right = mid - 1;
}else {//如果<,就到右边
left = mid + 1;
}
}
int left1 = 0;
int right1 = nums.size() - 1;
while(left1 <= right1) {
int mid = left1 + (right1 - left1) / 2;
if (nums[mid] > target) {
right1 = mid - 1;
}else {// <=,到右边去找,这样可以找到右边界
left1 = mid + 1;
}
}
题目描述:两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x
和 y
,计算并返回它们之间的汉明距离。
//主要是计算一个数字二进制为1的个数
int hammingDistance(int x, int y) {
int temp = x ^ y;
int result = 0;
//用右移后的结果与上一位一位的算
for (int i = 31; i >= 0; i--) {
if ((temp >> i) & 1) {
result++;
}
}
return result;
}
题目描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
思路:归并排序的思想;归并排序时间复杂度:O(nlogn)
int mergeSort(vector& nums, vector& tmp, int l, int r) {//递归归并排序并计算逆序对的数量
if (l >= r) {//只有一个元素 这里一定是>= 不然递归会范围出问题
return 0;
}
int mid = l + (r - l)/2;
int inv_count = mergeSort(nums, tmp, l, mid) + mergeSort(nums, tmp, mid + 1, r);
int i = l, j = mid + 1, pos = l; //归并排序过程,从小到大
while(i <= mid &&j <= r) {
if (nums[i] <= nums[j]) {//如果左边比右边小,不存在逆序对
tmp[pos] = nums[i];
i++;
} else {//左边比右边大,存在逆序对,计算逆序对数量
tmp[pos] = nums[j];
inv_count += mid - i + 1;
++j;
}
++pos;
}
for (int k = i; k <= mid; k++) {
tmp[pos++] = nums[k];
}
for (int k = j; k <= r; k++) {
tmp[pos++] = nums[k];
}
copy(tmp.begin() + l, tmp.begin() + r + 1, nums.begin() + l);//更新数组顺序
//vector迭代器一般为左闭右开
return inv_count; //返回当前区间逆序对数量
}
int reversePairs(vector& nums) {
int n = nums.size();
vector tmp(n); //中间数组
return mergeSort(nums, tmp, 0, n - 1);
}
n
个整数的数组 nums
,其中 nums[i]
在区间 [1, n]
内。请你找出所有在 [1, n]
范围内但没有出现在 nums
中的数字,并以数组的形式返回结果。//不用额外空间解法
//由于数组大小为n,所以联想到用数组来当作哈希表使用。nums的范围在[1-n]中,可以利用这个范围之外的数字来表达是否出现的含义
//具体来说,遍历nums,每遇到一个数x,就让nums[x-1]加n。由于nums中所有数均在[1-n]中,增加以后,这些数必然大于n。最后遍历nums,若nums[i]未大于n.就说明没有遇到数字i + 1,这样就找出了所有缺失的数字。
题目描述:给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的连续子数组的个数 。
思路:暴力(超时)、hash+前缀和(需要技巧,不然效率低也会超时)
//pre记录0-j的和,那么0-j+1的和可以用pre+nums[j + 1]表示
//unordered_map:key表示前缀和的值,value表示key出现的次数
//从前往后遍历,当遍历到nums[j]时,找map中是否存在nums[j] - k的值,如果存在(假设他是0-x的前缀和),那么存在连续子数组x + 1-j的前缀和为k
//技巧:1.pre可以用一个变量表示,而不是vector
//2.unorderde_map效率高,key和value分别存储什么需要思考
//3.从头开始遍历,以j为连续子数组的结尾可以解决重复问题,如果反方向需要去重
unordered_map mp;
int pre = 0;
int result = 0;
mp[0] = 1;//这里先插入一个虚拟0元素,不然后漏掉第一个元素,因为我们计算的时x+1-j前缀和
for (int i = 0; i < nums.size(); i++) {
pre += nums[i];
auto it = mp.find(pre - k);
if (it != mp.end()) result += it->second;
mp[pre]++;
}
return result;
题目描述:整数数组 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
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
思路:二次二分法,第一次二分找到旋转的位置,第二次二分找目标值;
//自己写的一版代码
int left = 0, right = nums.size() - 1;
//二分找旋转位置,可以找到第一个比它小的位置
while(left <= right) {
if (nums[(left + right)/2] > nums[0]) {
left = (left + right)/2 + 1;
}else if (nums[(left + right)/2] < nums[0]){
right = (left + right)/2 - 1;
}else if (nums[(left + right)/2] == nums[0]) break;
}
int k;//记录旋转位置变量
//条件很多,逐一判断
if (left == 0) {
if (nums.size() == 1) k = -1;
else if (nums.size() == 2){
if (nums[1] > nums[0]) k= -1;
else k = 0;
}else {
if (nums[1] > nums[0] && nums[2] > nums[1]) k= -1;
else if (nums[1] > nums[0] && nums[2] < nums[1]) k = 1;
else k = 0;
}
}else {
k = left - 1;
}
left = 0, right = nums.size() - 1;
if (k == -1) ;//没有旋转
else if (nums[0] > target) left = k + 1;//旋转的情况
else if (nums[0] <= target) right = k;
//二分查找
while(left <= right) {
if (nums[(left + right)/2] < target) {
left = (left + right)/2 + 1;
}else if (nums[(left + right)/2] > target){
right = (left + right)/2 - 1;
}else
return (left + right)/2;
}
return -1;
//网友版本
class Solution {
public:
int search(vector& nums, int target) {
int t = nums[0];
int l = 0, r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (t > nums[mid]) r = mid - 1;
else l = mid + 1;
}
if (target >= t) l = 0;
else r = nums.size() - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (target > nums[mid]) l = mid + 1;
else if (target < nums[mid]) r = mid - 1;
else return mid;
}
return -1;
}
};
//官方一次二分版本
//基于几个结论
//1:有序区间可以判断target在不在其中
//2:区间是否有序,在本题中可以比较区间左右端点的值
//思路:当我们把数组一分为二的时候,一定有一部分是有序的,那么可以判断有序的部分是不是包含了target,如果包含了,更新左右边界,如果不包含,更新左右边界,一直二分下去。
int left = 0, right = nums.size() - 1;
while(left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
return mid;
}else if (nums[mid] <= nums[right]) {//右半部分有序
if (target > nums[mid] && target <= nums[right])//如果target在此区间内
left = mid + 1;
else//不在
right = mid - 1;
}else {//左半部分有序
if (target >= nums[left] && target < nums[mid])
right = mid - 1;
else
left = mid + 1;
}
}
return -1;
int findDuplicate(vector& nums) {
int n = nums.size() - 1;
for (int i = 0; i < nums.size(); i++) {
nums[nums[i] % n] += n;//找到对应数组下标并且+n用来标记
}
int result;
//找到重复数字并且恢复原数组
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > n && nums[i] <= 2*n) {
nums[i] %= n;
}else if (nums[i] > 2*n){
nums[i] %= n;
if (i == 0)
result = n;
else
result = i;
}
}
return result;
}
思路:环形链表
因为数组大小为n+1,一定有一个数字([1,n])重复了一次,其他数字都出现并且只出现一次。那么可以将数组下标n和nums[n]建立一个映射关系f(n),其映射关系为n->f(n),假设从0出发,一定会产生一个有环的类似链表结构。假设数组为[1,3,4,2,2],那么链表可以抽象为:
那么问题就转换为求有环链表的入口问题,先用快慢指针确认有环,然后慢指针从头开始,直到和快指针相遇,相遇的节点就是环的入口。
int findDuplicate(vector& nums) {
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast) {
slow = nums[slow];
fast = nums[nums[fast]];
}
slow = 0;
while(slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
//这里slow不会返回下标,而是返回入口的值(重读数),因为一开始slow != fast
return slow;
}
题目描述:给定一个 n × n 的二维矩阵 matrix
表示一个图像。请你将图像顺时针旋转 90 度。你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
思路:使用矩阵当额外空间、原地旋转、先上下颠倒再沿对角线对换
//先上下颠倒再对角线对换
class Solution {
public:
void rotate(vector>& matrix) {
int slow = 0, fast = matrix.size() - 1;
while (slow < fast) {
//swap可以不用额外空间
swap(matrix[slow], matrix[fast]);
slow++;
fast--;
}
for (int i = 0; i < matrix.size(); i++) {
for (int j = i; j < matrix.size(); j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
}
};
//原地旋转
//对于矩阵的第一个元素,他旋转会覆盖右上角元素,右上角又会覆盖右下角元素,而右下角元素又会覆盖左下角元素,最后左下角元素覆盖第一个元素
//那么可以temp保存第一个元素,从左下角元素开始旋转,完成一个元素的旋转
//对矩阵的左上四分之子阵进行旋转即可完成整个矩阵90度旋转
class Solution {
public:
void rotate(vector>& matrix) {
int temp;
int n = matrix.size();
for (int i = 0; i < matrix.size()/2; i++) {
for (int j = 0; j < (matrix[0].size() + 1) / 2; j++) {
temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - 1 - j];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
};
nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n)
的算法解决此问题。//首先unordered_set用来去重
//一般的方法,对于num,循环去找num+1,如果存在,那么longestStreak+1。对于每个num,都去循环找,O(n*n)
//这里其实寻找重复了,如果对于数字num,存在num-1,那么去找一遍num-1即可,因为num-1开始肯定是最长的序列。
unordered_set num_set;
for (int num : nums) {//去重
num_set.insert(num);
}
int longestStreak = 0;
for (int num : num_set){
if (!num_set.count(num - 1)) {//如果不存在num-1了,开始循环遍历找
int currentNum = num;
int currentSreak = 1;
while(num_set.count(currentNum + 1)) {
currentNum += 1;
currentSreak += 1;
}
//更新最长序列
longestStreak = max(longestStreak, currentSreak);
}
}
return longestStreak;
//自己写的一版
//思路:对于序列[1,5,6,4,2,8,7,3],后三位是递减顺序,那么后三位不管怎么调换位置,也生成不了下一个排列
//2 < 8,那么意味着可以把8,7,3中第一个比2大的元素与2对换位置,再做排序
//即[1,5,6,4,3,8,7,2]
static bool cmp (int& a, int& b) {
return a >= b;
}
void nextPermutation(vector& nums) {
//先反转,方便处理
reverse(nums.begin(), nums.end());
int i = 0;
//找到第一个逆序数字i+1
for (i = 0; i < nums.size() - 1; i++) {
if (nums[i] <= nums[i + 1]) {
continue;
}else {
break;
}
//如果遍历完也没找到,那说明下一个排列就是1-n
if (i == nums.size() - 1) {
sort(nums.begin(), nums.end());
return;
}
//找到第一个比nums[i+1]大的数字
int j;
for (j = 0; j <= i; j++) {
if (nums[i + 1] < nums[j]) break;
}
//对换位置并且排序
swap(nums[i + 1], nums[j]);
sort(nums.begin(), nums.begin() + i + 1, cmp);
//最后反转返回
reverse(nums.begin(), nums.end());
}
题目描述:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
思路:哈希表比较、排序
unordered_map> mp;
//对于异位词,排序之后是相同的,把对应的str加入到value中
for (string& str : strs) {
string key = str;
//sort对string排序
sort(key.begin(), key.end());
mp[key].push_back(str);
}
vector> result;
for (auto x : mp) {
result.push_back(x.second);
}
return result;
}
//辅助栈(栈里放包括当前元素以前的最小元素)
class MinStack {
public:
MinStack() {
//初始化辅助栈里加入一个INT_MAX便于处理栈为空的情况
min_stack.push(INT_MAX);
}
void push(int val) {
//push:主栈里直接push
//辅助栈里更新当前元素的最小值
x_stack.push(val);
min_stack.push(min(min_stack.top(), val));
}
void pop() {
//直接pop
x_stack.pop();
min_stack.pop();
}
int top() {
//取top
return x_stack.top();
}
int getMin() {
//辅助栈顶元素即为最小值
return min_stack.top();
}
private:
stack x_stack;
stack min_stack;
};
//不使用辅助栈,用链表实现一个MIN_STACK
class MinStack {
public:
MinStack() {
head = nullptr;//头结点初始化
}
void push(int val) {
//若栈空,则申请新节点空间并赋予头结点
if (head == nullptr){
head = new Node(val, val);
}
// 若栈非空,则更新新节点的栈内元素最小值后,将新节点插入栈顶,最后更新头节点
else {
int tmp = val < head->min ? val : head->min;
Node* cur = new Node(val, tmp);
cur->next = head;
head = cur;
}
}
void pop() {
// 让头节点指向自身的下一个节点即可
// 不用管出栈之后的最小值变化,即使当前出栈元素就是最小值也无妨,
// 因为每个节点的 min 值记录的都是栈底到此节点的元素最小值
head = head->next;
}
int top() {
// 直接返回头节点 val 值即可,头节点永远指向栈顶
return head->val;
}
int getMin() {
// 直接返回头节点 min 值即可,头节点的 min 值永远是栈内所有元素的最小值
return head->min;
}
private:
//节点
struct Node{
int val;//节点的值
int min;//以该节点为栈顶的栈内最小元素的值
Node* next;//下一个节点
Node(int x, int y) :val(x), min(y), next(nullptr) {}
};
//定义头结点
Node *head;
};
//迭代法(自己写的版本,很罗嗦,直接在l1上操作)
//如果有空链表,直接返回不为空的链表头节点
//直接在list1上操作,需要一个虚拟头节点来合并,因为插入节点需要上一个节点来操作
if (list1 == nullptr || list2 == nullptr) return list1 == nullptr ? list2 : list1;//有空的情况
ListNode* headList = new ListNode(0, list1);//虚拟节点指向list1头部
ListNode* result = list1->val > list2->val ? list2 : list1;//记录要返回的头节点
while(headList->next != nullptr) {
if (list2 == nullptr) break;//如果list2遍历完了,那么已经合并完毕
if (headList->next->val <= list2->val) {//如果list1当前元素比list2小,那么不需要操作,list1移到下一个节点
if (headList->next->next == nullptr) {//如果list1下一个节点为空,那么直接指向list2的当前元素位置
headList->next->next = list2;
break;
} else//否则,移动一个位置
headList = headList->next;
}else if (headList->next->val > list2->val) {//如果list1当前元素比list2大,需要向list1插入list2的元素
ListNode* temp = headList->next;
ListNode* temp2 = list2->next;//这里list2的下一个节点必须要记录
headList->next = list2;//这里直接用原来的节点,不要new来操作,否则就是开辟新节点了
headList->next->next = temp;
list2 = temp2;//list2向下移动
headList = headList->next;//继续向下移动
}
}
return result;
//另外一个版本,比较清晰(新建一个虚拟头结点,尾部接入)
ListNode* merge(ListNode* l1, ListNode* l2) {
ListNode* dummyHead = new ListNode();//虚拟头结点,用于保存合并后的头结点
ListNode* cur = dummyHead;//实际操作的虚拟头结点
while(l1 && l2) {
if (l1->val <= l2->val) {//如果l1比较小,那么虚拟头结点尾部插入l1
cur->next = l1;
l1 = l1->next;
} else {//反之尾部插入l2
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;//节点后移
}
//如果其中某个链表为空,那么直接指向另一个链表遍历的位置,因为已经有序了
cur->next = l1 ? l1:l2;
return dummyHead->next;
}
//递归
//返回值,返回l1、l2合并的结果
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//终止条件,如果l1、或者l2为空,那么直接返回
if (l1 == NULL) {
return l2;
}
if (l2 == NULL) {
return l1;
}
//当前层逻辑:如果l1值比较小,那么去合并(l1->next,l2),并使l1->next去接住这个合并结果
if (l1->val <= l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
//如果l2值比较小,逻辑相同
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
};
题目描述:给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
思路:放到数组,排序,链表重构(O((n)log(n)),O(n))。归并排序(递归、迭代)
//归并排序,递归方式(自顶向下),需要用到栈空间O(log(n))
//递归排序主函数,先切割到最小单元,返回时进行排序回溯
ListNode* mergeSort(ListNode* head) {
if (!head || !head->next) return head;//空或者只有一个元素,直接返回
ListNode* mid = findMid(head);//找到链表中点
ListNode* l1 = head;//左半部分
ListNode* l2 = mid->next;//右半部分
mid->next = nullptr;//给左半部分设置停止标志
l1 = mergeSort(l1);//左右部分,别分排序
l2 = mergeSort(l2);
return merge(l1, l2);//将已经有序的左右部分合并
}
//找到链表中点方法,快慢指针法
ListNode* findMid(ListNode* head) {
ListNode* slow = head, *fast = head;
while(fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
//两个有序链表的合并
ListNode* merge(ListNode* l1, ListNode* l2) {
ListNode* dummyHead = new ListNode();
ListNode* cur = dummyHead;
while(l1 && l2) {
if (l1->val <= l2->val) {
cur->next = l1;
l1 = l1->next;
} else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
cur->next = l1 ? l1:l2;
return dummyHead->next;
}
ListNode* sortList(ListNode* head) {
return mergeSort(head);
}
//归并排序,自底向上,空间复杂度O(1)
//用subLength表示每次需要排序的子链表长度,初始时subLength = 1;
//每次将链表拆分为若干个长度为subLength的子链表,按照每两个子链表一组进行合并,合并后即可得到若干个长度为subLength*2的有序子链表
//将subLength的值加倍,重复第二步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于length,整个链表排序完毕
ListNode* sortList(ListNode* head) {
if (head == nullptr) return head;
//记录下链表长度
int length = 0;
ListNode* node = head;
while(node != nullptr) {
length++;
node = node->next;
}
//定义虚拟头结点
ListNode* dummyHead = new ListNode(0, head);
//subLength倍数增长,直到接近length
for (int subLength = 1; subLength < length; subLength <<= 1) {
ListNode* prev = dummyHead, *curr = dummyHead->next;
//将链表按照subLength分为两两一组分别去合并
while(curr != nullptr) {
//第一个subLength子链表
ListNode* head1 = curr;
for (int i = 1; i < subLength && curr->next != nullptr; i++) {
curr = curr->next;
}
//第二个subLength子链表
ListNode* head2 = curr->next;
curr->next = nullptr;
curr = head2;
for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
curr = curr->next;
}
ListNode* next = nullptr;
if (curr != nullptr) {
//记录两组子链表剩余元素下一个元素
next = curr->next;
curr->next = nullptr;
}
//合并两个有序链表
ListNode* merged = merge(head1, head2);
//有序的部分记录串联起来
prev->next = merged;
while(prev->next != nullptr){
prev = prev->next;
}
curr = next;
}
}
return dummyHead->next;
}
给出二叉树的前序/后序、中序的遍历数组,可以唯一确定一个二叉树结构;
只有前序和后序不能确认二叉树结构;
重构思想(以后序与中序为例):
递归实现:首先后序的最后一个元素为根节点
在中序中找到该节点,切割中序数组为左右两部分,左半部分为左子树 右半部分为右子树
再来切割后序数组,以左子树的大小为依据切割
递归重构 cur -> left = Traversal(vector
cur -> right = Traversal(vector
优化方法;用左右数组范围替代传入vector
1.以数组中最大元素为依据左右分割为左子树与右子树
2.思路与106相同
题目描述:二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
思路:递归遍历,后序;
简单的考虑一颗小树的情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3xMvjFlf-1689081778512)(E:\zhangfiles\markdownfile\代码随想录刷题小计\image\1685332306831.jpg)]
那么最大路径的情况可以概括为三种情况:
a+b+c:表示不联络父节点的情况或者本身是根节点
a+b a+c:递归时返回a+b 或 a+c,选择一个更优的方案返回
int maxPathValue(TreeNode* root, int& val) {
if (root == nullptr) return 0;
int left = maxPathValue(root -> left, val);
int right = maxPathValue(root -> right, val);//后序遍历
int lmr = root -> val + max(0, left) + max(0, right);//第一种情况
int ret = root -> val + max(0, max(left, right));//第2、3种情况
val = max(val, max(lmr, ret));//更新最大路径值
return ret;//注意这里只返回了第2、3情况的值,因为第一种情况没必要返回了,他不能链接父节点
}
int maxPathSum(TreeNode* root) {
int val = INT_MIN;
maxPathValue(root, val);
return val;
}
root
。两节点之间路径的 长度 由它们之间边数表示。 int MaxValue = 0;
int MaxPath(TreeNode* root) {
if (root == nullptr) return 0;//遇到空节点,最大节点数为0
int p1 = MaxPath(root->left);//左子树最大节点数
int p2 = MaxPath(root->right);//右子树最大节点数
MaxValue = max(p1 + p2 + 1, MaxValue);//更新最大值,中节点联结左右子树的最大值
return max(p1, p2) + 1;//返回只连接左子树或者右子树的最大值,因为这样向上回溯才有效(路径的定义),它不能连结父节点
}
int diameterOfBinaryTree(TreeNode* root) {
MaxPath(root);
return MaxValue - 1;
}
题目描述:二叉树中序遍历(不用额外空间)
//Morris中序遍历不需要额外空间,并且是一种迭代遍历法
//通过找到前序节点来指向当前节点,一是可以遍历完左子树回到中节点,二是可以告诉我们遍历完了左子树,省去了用栈去维护。
vector inorderTraversal(TreeNode* root) {
vector res;
TreeNode* predecessor = nullptr;
while(root != nullptr){
//如果左子树不为空,进入左子树
if (root->left != nullptr){
predecessor = root->left;
//找到前驱节点
while(predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
//如果前驱节点为空,说明左子树还没遍历
if (predecessor -> right == nullptr) {
predecessor ->right = root;
root = root->left;
}else {//说明已经遍历完毕
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
//左子树为空,直接进入右子树操作
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
root
,请你将它展开为一个单链表:
TreeNode
,其中 right
子指针指向链表中下一个结点,而左子指针始终为 null
//递归方法
//采用逆后续遍历方式
//在归的过程中改变指向,完成二叉树的展开
//记录之前的节点
TreeNode* pre = nullptr;
void flatten(TreeNode* root) {
solve(root);
}
void solve(TreeNode* root){
if (root == nullptr) return;
solve(root->right);
solve(root->left);
//左孩子设为空
root->left = nullptr;
//右孩子挂前序节点
root->right = pre;
//更新前序节点
pre = root;
}
动态规划五部曲:
1.确定dp数组以及下标的含义
2.确定递推公式
3.dp数组如何初始化
4.确定遍历顺序
5.举例推导dp数组
回溯三部曲
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理结点;
backtracking(路径,选择列表); //递归
回溯,撤销处理结果
}
}
题目描述:给定两个整数n和k,返回1-n中所有可能的k个数的组合。
思路:回溯算法递归遍历。
递归函数的返回值及参数:
vector> result;
vector tmp;
void backtracking(int n, int k, int index)//index用来标记本层递归的起始位置
回溯算法中一般返回值为void
终止条件:
if (tmp.size() == k) {
result.push_back(tmp);
return;
}
回溯法的搜索过程就是一个树型结构的遍历过程,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
题目描述:找出所有相加之和为n的k个数的组合。组合中只允许含有1-9个正整数,并且每种组合中不存在重复数字。
所有数组都是正数,解集不能包含重复的组合。
思路:在回溯算法求所有排列结果的基础上挑选出和为n的排列。
递归函数参数:
vector> result;
vector tmp; //可以作为参数传入 但浪费空间
void backtracking(int n, int k, int index, int sum);
终止条件:
if (tmp.size() == k) {
if (sum == n) result.push_back(tmp);
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
单层逻辑:
for (int i = index; i <= 9; i ++) {
tmp.push_back(i);
backtracking(n, k, i + 1, sum + i);
tmp.pop_back();
}
剪枝
void backtracking(string digits, int index);
if (tmp.size() == digits.size()) {
result.push_back(tmp);
return;
}
for (int i = 0; i < dic[digits[index] - '0'].size(); i++) {
tmp.push_back(dic[digits[index] - '0'][i]);
backtracking(digits, index + 1);
tmp.pop_back();
}
}
void backtracking(vector nums, int target, int index);//index是为了去重
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(tmp);
return;
}
单层逻辑:
for (int i = index; i < nums.size(); i++) {
tmp.push_back(nums[i]);
backtracking(nums, target - nums[i], i); //去重逻辑
tmp.pop_back();
}
去重逻辑:在216中,i = index; backtracking(i + 1),来实现组合的不重复,但是数字不能重复。如果想要数字可以重复,可以写为 i = index; backtracking(i)来实现。(简单粗暴的理解方式)
题目描述:给定一个集合candidates和一个目标数字target,找出candidate中所有使数字和为target的组合。candidates中的每个数字只能使用一次。
与39组合总和的异同点:
39:元素不重复,但可以重复使用
本题:元素重复,但是不可以重复使用
都要找到和为target的组合,但是组合之间不能重复。
思路:回溯算法搜索。本题跟216类似,只不过需要添加去重逻辑,这种排列组合题去重逻辑一定要在回溯的过程中去重,不然很容易超时。去重套路跟之前题目类似:先排序,然后在a的循环中,如果candidates[i] == candidates[i - 1],continue,因为a如果重复,那么后续得到的组合可能导致重复,所以这样可以实现对a的去重,然后递归实现对b、c、d位置的去重。
for (int i = index; i < candidates.size() && (target - candidates[i] >= 0); i++) {
if (i > index && candidates[i] == candidates[i - 1]) // 去重逻辑 当前组合位置去重
continue;
path.push_back(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1);
path.pop_back();
}
vector> result;//存放结果
vector tmp;//存放回文子串,存放中间结果
void backtracking(string s, int index)//index:用来避免重复切割,与组合的index作用一样
if (index == s.size()) {
result.push_back(tmp);
return;
}
string a = "";//存放当前层 子串的切割方法
for (int i = index; i < s.size(); i++) {
a = a + s[i];
string b = a; //直接判断 当前切割子串是否为回文,不是continue,是的话再放入中间结果
reverse(b.begin(), b.end());//可以自己写函数,用双指针法优化
if (a == b) {
tmp.push_back(a);
backtracking(s, i + 1);
tmp.pop_back();
}
}
**题目描述:**给定一个只包含数字的字符串,复原它并返回所有可能的IP地址格式。有效的IP地址正好由四个整数(每个整数位于0到255之间组成,且不能含有前导0),整数之间用’.'分隔。
思路:这种问题还是需要回溯算法遍历求解,与131类似。
递归函数返回值:
vector result;
vector path;
void backtracking(string s, int index); //index为了不重复选取
终止条件:
if (path.size() == 4 && index == s.size()) {
result.push_back(path[0] + '.' + path[1] + '.' + path[2] + '.' + path[3]);
return;
}
if (path.size() > 4)
return;
单层逻辑:
string a = "";
for (int i = index; i < s.size(); i++) {
a += s[i];
if (isIp(a)) {
path.push_back(a);
backtracking(s, i + 1);
path.pop_back();
}
}
}
bool isIp(string s) { //判断当前字符是不是合法IP
if (s.size() > 3)
return false;
if (s[0] == '0' && s.size() >= 2)
return false;
if (stoi(s) > 255)//字符串转int
return false;
return true;
}
vector> result;
vector path;
void backtracking(vector& nums, int startIndex); //index为了不重复选取
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己
if (startIndex >= nums.size()) { // 终止条件可以不加
return;
}
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
题目描述:给定一个整数数组nums,其中可能包含重复元素,请返回该数组所有可能的子集。解集不能包含重复的子集。
思路:与78相同,多了一个去重逻辑。
去重逻辑:与大多数的回溯算法去重套路相同,先排序,然后遇到连续相同的数,第一个进入循环,后续的跳过。
for (int i = index; i < nums.size(); i++) {
if (i > index && nums[i] == nums[i - 1]) //去重逻辑
continue;
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
vector> result;
vector path;
void backtracking(vector& nums, int startIndex) //index为了不重复选取
if (path.size() > 1) {
result.push_back(path);
// 注意这里不要加return,因为要取树上的所有节点
}
unordered_set uset; // 使用set来对本层元素进行去重
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
题目描述:给定一个不含重复数字的数组nums,返回其所有可能的排列。
递归函数参数:
vector> result;
vector tmp; //可以作为参数传入
void backtracking(vector nums, vector index)
递归终止条件:
if (tmp.size() == nums.size())
return;
单层逻辑:
for (int i = 0; i < nums.size(); i++) {
if (index[i]) {
tmp.push_back(nums[i]);
index[i] = false;
backtracking(nums, index);
index[i] = true;
tmp.push_back();
}
}
题目描述:给定一个可包含重复数字的序列nums,按任意顺序返回所有不重复的序列。
思路:与全排列思路类似,只不过需要加去重逻辑。
for (int i = 0; i < nums.size(); i++) {
//去重逻辑 注意先排序,如果后一个数等于前一个 跳过
if (i > 0 && nums[i] == nums[i - 1] && index[i] && index[i-1])
continue;
if (index[i]) {
index[i] = false;
tmp.push_back(nums[i]);
backtracking(nums, index);
index[i] = true;
tmp.pop_back();
}
}
vector> result;
vector path(n, string(n, '.'));//实际代码全局变量不能再此初始化
void backtracking(int n, int count);
if (count == n) {
result.push_back(path);
return;
}
单层逻辑:
for (int i = 0; i < n; i++) {
if (islegal(count, i, n)) {
path[count][i] = 'Q';
backtracking(n, count + 1);
path[count][i] = '.';
}
}
判断是否合法函数:这里比实际求对角线是否有皇后简单了一点,因为我们是逐层往下递归,只需要判断前面的对角线是否有皇后。
bool islegal(int count, int i, int n) {
for (int j = 0; j < n; j++)
if (path[j][i] == 'Q')
return false;
for (int j = count - 1, k = i - 1; j > -1 && k > -1; j--, k--) {
if (path[j][k] == 'Q')
return false;
}
for (int j = count - 1, k = i + 1; j > -1 && k < n; j--, k++) {
if (path[j][k] == 'Q')
return false;
}
return true;
}
bool backtracking(vector>& board);//这道题要直接修改board
bool backtracking(vector>& board) {
for (int i = 0; i < board.size(); i++) { // 遍历行
for (int j = 0; j < board[0].size(); j++) { // 遍历列
if (board[i][j] != '.') continue;
for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board)) {
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = '.'; // 回溯,撤销k
}
}
return false; // 9个数都试完了,都不行,那么就返回false
}
}
return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}
bool isvalid(int row, int col, char val, vector>& board) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == val)
return false;
}
for (int i = 0; i < 9; i++) {
if (board[i][col] == val)
return false;
}
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) {
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == val)
return false;
}
}
return true;
}
n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。//括号问题有个规律:如果')'数量大于'(',那么一定组不成合法括号
//如果从头开始组成合法括号,一直满足上述条件,那么最后的结果就是合法括号
//注意这里回溯的写法,与求排列不同,根据当前左右括号数进行添加,而不是一个一个去添加
void backtracking(string temp, vector& result, int i, int j) {
if (i < 0 || j < 0) return;
if (i == j && i == 0) {//左右括号用完,合法括号
result.push_back(temp);
return;
}else if (i == j) {//左右括号数量相等,下一个只能添加左括号
backtracking(temp + '(', result, i - 1, j);
}else if (i < j) {//左括号数量多,那么下一个左右括号都行
//temp+'('隐含了回溯的过程
backtracking(temp + ')', result, i, j - 1);
backtracking(temp + '(', result, i - 1, j);
} else
return;
}
vector generateParenthesis(int n) {
vector result;
backtracking("", result, n, n);
return result;
}
题目描述:有一个数组,求出该数组中为摆动序列的最长子序列的长度。
思路:贪心算法;找到数组的波峰和波谷,计算有摆动的位置。
这道题Leetcode官方题解好理解一点
class Solution {
public:
int wiggleMaxLength(vector& nums) {
int n = nums.size();
if (n < 2) {
return n;
}
int prevdiff = nums[1] - nums[0];
int ret = prevdiff != 0 ? 2 : 1; //当数组大小为2时,等式显然;
//当数组大小大于2,nums[0] != nums[1],当出现摆动result + 2;
//当nums[0] == nums[1], 属于平坡情况,当出现摆动result + 1;
for (int i = 2; i < n; i++) {
int diff = nums[i] - nums[i - 1];
if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
ret++;
prevdiff = diff;//这里有摆动才更新orediff的原因是因为出现单调坡的时候,一直更新会出现问题
}
}
return ret;
}
};
public:
int maxSubArray(vector& nums) {
int sum = 0;
int result = -100000;
for (int i = 0; i < nums.size(); i++) {
sum += nums[i]; //子序列和
result = max(sum, result);//更新结果数组
if (sum < 0) //如果和为负数,那么从下一个数开始计算
sum = 0;
}
return result;
}
bool canJump(vector& nums) {
int right = 0;//覆盖范围
for (int i = 0; i <= right; i++) {
right = max(i + nums[i], right);//更新覆盖范围
if (right >= nums.size() - 1) //如果能够包含最后一个元素,那么返回true;
return true;
}
return false;
}
int jump(vector& nums) {
if (nums.size() == 1) return 0;
int curDistance = 0; // 当前覆盖最远距离下标
int ans = 0; // 记录走的最大步数
int nextDistance = 0; // 下一步覆盖最远距离下标
for (int i = 0; i < nums.size(); i++) {
nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖最远距离下标
if (i == curDistance) { // 遇到当前覆盖最远距离下标
ans++; // 需要走下一步
curDistance = nextDistance; // 更新当前覆盖最远距离下标(相当于加油了)
if (nextDistance >= nums.size() - 1) break; // 当前覆盖最远距到达集合终点,不用做ans++操作了,直接结束
}
}
return ans;
}
题目描述:给定一个数组A,选择某个下标i并将nums[i]替换为-nums[i],重复这个过程k次,可以多次选择同一个下标i。以这种方式修改数组后,返回数组最大的可能和。
思路:如果数组存在负数,那么先反转负数(绝对值大的负数),如果负数都反转完了,将最小的正数反转剩下的次数。
解题步骤:
第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K–
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和
题目描述:在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
思路:暴力解法:遍历每个加油站,判断从当前加油站出发是否能转一圈(不用考虑逆向转圈,如果正向不能转一圈,那么逆向也不会有一个路径),for循环适合线性结构,首尾相连可以用while。
贪心:记录每个加油站剩余汽油,res[i] = gas[i] - cost[i],i从0开始累加res[i],如果res[i]小于0,那么从下一个位置开始重新计算。
int canCompleteCircuit(vector& gas, vector& cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
for (int i = 0; i < gas.size(); i++) {
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if (curSum < 0) { // 当前累加rest[i]和 curSum一旦小于0
start = i + 1; // 起始位置更新为i+1
curSum = 0; // curSum从0开始
}
}
if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
return start;
//这里之所以可以直接返回start,是因为total > 0,那么就证明start到最后一站的剩余油量一定大于0到start的剩余油量,可以保证汽车跑一圈。
}
题目描述:老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
思路:这是属于两边要兼顾的题目,需要考虑孩子左右两边得分情况。先从左边遍历,如果当前孩子比左边高,那么candys[i] = candys[i - 1] + 1;同理,再从右边遍历;最后取两个结果的最大值,表示同时满足两个条件。
题目描述:
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
思路:当顾客给5元,不需要找零;当顾客给10元,只能找5块的零钱;当顾客给20元,有两种选择;3个五元,1个五元和一个10元。 贪心思路,贪在第三种方式,优先找1个五元和10元的,因为5元更万能,既可以找零10元,又可以找零20元,类似于喂小孩子饼干的题目,用尽可能小的饼干去喂饱小孩子,这里是用尽可能面值大的钱去找零。
bool lemonadeChange(vector& bills) {
//自己写的一版用的unordered_map,但其实这种找零的情况只有两种,5元和10元,用数组或者变量就足够
int five = 0;
int ten = 0;
int twenty = 0;
for (int bill : bills) {
if (bill == 5) five++;
if (bill == 10) {
if (five <= 0) return false;
ten++;
five--;
}
if (bill == 20) {
if (five >= 1 && ten >=1) {
five--;
ten--;
} else {
if (five <= 2) return false;
five -= 3;
}
}
}
return true;
在按照身高从大到小排序后:优先按照身高高的人的k来插入,这样后续节点的插入不会影响之前的结果,最终按照k的规则实现了队列。
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
static bool cmd (vector a, vector b) {//排序规则
if (a[0] != b[0])
return a[0] > b[0];
return a[1] < b[1];
}
vector> reconstructQueue(vector>& people) {
vector> queue;//注意这里不要初始化大小,因为插入操作会增加size
sort(people.begin(), people.end(), cmd);
for (int i = 0; i < people.size(); i++) {
int position = people[i][1];//获取当前people的位置
queue.insert(queue.begin() + position, people[i]);
}
//用的vector数组,底层时数组实现,插入效率很低,可以改用list,底层实现时链表,插入效率很高
return queue;
}
题目描述:
思路:类似于406问题思路,先排序,后续跟进处理。将左边界从小到大排序。index = points[0][1]
,循环处理,如果index大于下一个气球的左边界,下一个气球可以一起射爆,index = min(index, points[i][1])
;反之,index = points[1], result++
;
static bool cmp(vector &a, vector &b) {//这里最好加上引用 会提高运行速度
return a[0] < b[0];
}
int findMinArrowShots(vector>& points) {
sort(points.begin(), points.end(), cmp);
int result = 1;//初始需要一只箭
int index = points[0][1];
for (int i = 0; i < points.size(); i++) {
if (index >= points[i][0]) {//如果可以一起射爆,那么更新右边界
index = min(index, points[i][1]);
}else {//不能一起射爆,箭数+1,更新有边界
index = points[i][1];
result++;
}
}
return result;
}
题目描述:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意: 可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
思路:贪心思路贪在哪儿?:先按照左边界从小到大排序,然后从头开始遍历,如果当前区间的左边界大于前一个区间的右边界,那么这两个区间一定交叠,必须要删除一个区间,那么我们贪心的策略在于:删除右边界大的那个区间。
static bool cmp(vector& a, vector& b) {
return a[0] < b[0];
}
int eraseOverlapIntervals(vector>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int result = 0;
int left = intervals[0][1];
for (int i = 0; i < intervals.size() - 1; i++) {
if (left > intervals[i + 1][0]) {//如果区间有交叠
left = min(left, intervals[i + 1][1]);//相当于删除右边界较大的区间
result++;
}else //否则更新新边界
left = intervals[i + 1][1];
}
return result;
}
字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
思路:没有很符合贪心算法的特性;
笨方法:创建hash数组,计算所有出现的字母频次。定义left、right边界,从头开始遍历,如果当前字母及之前所有字母他们的频次都用完了,那么输出一个length = right - left + 1;更新左右边界。
代码随想录(代码很巧妙):在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
统计每一个字符最后出现的位置
从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
vector partitionLabels(string s) {
int hash[27] = {0};//hash数组,注意最好不用vector
for (int i = 0; i < s.size(); i++) {
hash[s[i] - 'a'] = i;//很巧妙地去统计字母最后出现的位置
}
vector result;
int left = 0;
int right = 0;
for (int i = 0; i < s.size(); i++) {
right = max(right, hash[s[i] - 'a']);//当前字母以前的最大出现位置
if (i == right) {//当前位置到达最大位置
result.push_back(right - left + 1);//输出一次结果
left = i + 1;//更新左边界
}
}
return result;
}
static bool cmp(vector& a, vector& b) {
return a[0] < b[0];
}
vector> merge(vector>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
vector> result;
int left = intervals[0][0];//初始化左边界
int right = intervals[0][1];//初始化右边界
for (int i = 1; i < intervals.size(); i++) {
if (right >= intervals[i][0]) {//如果两个区间重叠
right = max(right, intervals[i][1]);//合并两个区间
} else {//如果不重叠
result.push_back({left, right});//输出不重叠区间
left = intervals[i][0];
right = intervals[i][1];
}
}
result.push_back({left, right});
return result;
}
题目描述:给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
思路:暴力解法,n–,每次去判断这个数字是否满足;
贪心:从后往前遍历,如果当前位小于前一位,那么当前位为9,前一位减一;这里存在一个问题,比如100,会输出90;所以需要设置一个标志位,记录当前置9的位,如果当前位被置9,那么后面所有位都要置9;
int monotoneIncreasingDigits(int n) {
string strNum = to_string(n);
int flag = strNum.size();//初始化标志位
for(int i = strNum.size() - 1; i > 0; i--) {
if (strNum[i - 1] > strNum[i]) {
strNum[i - 1]--;
flag = i;//更新标志位
}
}
for (int i = flag; i < strNum.size(); i++) {
strNum[i] = '9';//置9的位及后面所有位都置9
}
return stoi(strNum);
}
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
if (cur == NULL) return 2;//遇到空节点,返回已覆盖
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
if (left == 2 && right == 2) return 0;//这里状态分类是精简化的结果
else if (left == 0 || right == 0) {
result++;
return 1;
} else return 2;
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // 最后还要判断root的状态 root 无覆盖
result++;
}
return result;
}
};
题目描述:给你一个整数数组 nums
,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。请你找出符合题意的 最短 子数组,并输出它的长度。
思路:类似于身高排队问题,要确定两端的位置,可以一端一端去确定,可以避免顾此失彼的问题。
int leftMax = -1000000;
int rightMin = 1000000;
int left = nums.size() - 1;//左端极限情况
int right = 0;//右端极限情况
//先确定右端位置,循环从左到右遍历,如果当前元素比左边元素小,那么更新右端位置,因为这个元素肯定需要重新排序
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > leftMax) {
leftMax = nums[i];
}else if (nums[i] < leftMax){
right = i;
}
}
//相同思路确定左端位置
for (int i = nums.size() - 1; i >= 0; i--) {
if (nums[i] < rightMin) {
rightMin = nums[i];
}else if (nums[i] > rightMin){
left = i;
}
}
//right <= left表示数组有序,否则输出子数组大小
return right > left ? right - left + 1 : 0 ;
1.dp数组含义:dp[i][j]
表示 0 - i 的物品任意放在容量j的背包里
2.递推公式:不放物品idp[i-1][j]
放物品dp[i-1][j-weight]+value[i]
dp[i][j]
取上述两种情况最大值
3.dp数组初始化:背包容量为0的列初始化为0
物体为0的行初始化:如果背包容量可以装下则初始化为value[0]
否则初始化为0
4.遍历顺序:对于二维数组的01背包问题,先遍历背包或者物品都行
1.dp数组含义:dp[j]
容量为j的背包能装的最大价值
2.递推公式:dp = max(dp[j], dp[j - weight[i]] + value[i])
初始化:dp[0] = 0
题目描述:给出一个只包含正整数的非空数组,判断时候可以分割为两个子集,使得两个子集的元素和相等。
思路:动态规划;判断能付否将数组元素大小的物体装入背包,使背包装的价值最大
背包的容量为sum/2
,物品的重量和价值都为数组元素大小
当背包价值最大,如果最大价值等于背包容量,那么return true
;
题目描述:有一堆石头,用整数数组表示;每一回合,从中任意选出两个石头,将他们一起粉碎,有两种情况:
x==y 两个石头完全粉碎
x
思路:本质是一个石头分堆问题,当两堆石头质量越接近,那么相撞后剩余的小石头质量越小
转化为背包问题,设置背包容量为sum/2,找到价值最大的解即为其中一堆石头的重量,总重量减去即为另一堆石头的重量,最后计算相撞的结果。
题目描述:给你一个非负整数数组,一个目标target;对数组中的每个整数前添加+
或-
,然后串联起来,构成一个表达式,使得运算结果等于target,给出有多少种方法构造;
思路:这是动态规划的另一种应用方式;经典的动态规划为背包最大值问题;这道题转化为装满容量为j
的背包的种类问题;
dp[i][j]
:从0-i
物品中任意选取装满容量为j
的背包有多少种选法
递推公式:
dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j] if j >= nums[i - 1]
dp[i][j] = dp[i - 1][j] if j < nums[i - 1]
注意此处nums[i-1]
而不是nums[i]
,因为我们初始化dp
数组大小为nums.size() + 1
;
初始化:dp[0][0]=1
其他为0;因为在此题中空数组的情况,这种情况下可能满足最后的计算和为target;
遍历顺序:同基础01背包;但是需要注意,题解初始化第一行,所以j = 0
遍历顺序;
而经典背包问题初始化一行一列,所以j=1
遍历顺序,但两者等效;
剪枝/去掉异常数据:
if (sum + target < 0 || (sum + target) % 2 == 1) {
return 0;
}
题目描述:
给你一个二进制字符数组strs
和两个整数m和n;请你找出并返回strs的最大子集的长度,该子集中最多有m个0和n个1。
思路:该题是经典01背包问题的另一个应用;背包的容量从一维扩展成二维,问题转化为在 二维背包容量下,最多装多少个物品,或者最大价值是多少(每个物品的价值都为1)。
dp[i][j]
:在容量为i j
的情况下,背包最大价值
递推公式:
dp[i][j] = max(dp[i][j], dp[i - count0][j - count1] + 1) if i >= count0 && j >= count1
初始化:整体初始化为0
遍历顺利:倒序遍历,同01背包滚动数组解法
与01背包的区别:完全背包一个物品可以取多次放入背包;
dp[i][j]
:容量为j的背包,0-i
个物品任意放的最大价值(每个物品数量不限)
递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])
注意其中的区别:当放得下物品i时,要状态转移到同层,区别于01背包状态转移到上一层;
遍历顺序:与01背包相同
采用滚动数组与01背包的区别:背包容量需要顺序遍历、两层for循环可以颠倒(仅限纯完全背包问题)
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagWeight = 4;
vector dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
dp[j]
:装满背包j一共有多少种方法;dp[j] += dp[j - coins[i]]
;dp[0] = 1
;有争议,leetcode后台数据指定为0,如果为0,那么dp数组全部为0,没有意义;题目描述:给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的个数。(不同的排列当作不同的结果)
思路:多重背包问题:先遍历背包再遍历物品
题目描述:给定不同面额的硬币coins和一个总金额amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1,每种硬币的数量是无限的。
思路:与377类型相同;采用dp数组迭代求解,需要注意一些细节技巧;
dp[j]
:装满容量为j的背包最少物品数量
递推公式:
dp[j] = min(dp[j], dp[j - coins[i]] + 1)
初始化:需要一些技巧,用INT_,MAX来表示无法凑成面额,最后需要进行一步检测。
vector dp(amount + 1, INT_MAX);
dp[0] = 0;
...
if (dp[amount] == INT_MAX) return -1;
return dp[amout];
遍历顺序:先遍历物品、先遍历背包都可以;先遍历物品表示组合数,先遍历背包表示排列数。对于本题最小硬币数量来讲,结果是一样的。
dp[j]
:s字符串0-j的子串可不可以被拼接;dp[j] = if (dp[i] == true && st.find(substr(i, j -i) != st.end()) 0 < i < j
题目描述:你是一个小偷,不能偷相邻房间的钱。给定一个代表每个房屋存放金额的非负整数数组,计算不被抓的情况下,能偷到的最大金额;
思路:动态规划问题
dp[j]
:偷0-j个房间得到的最大金币数量
dp[j] = max(dp[j - 1], dp[j - 2] + value[i])
初始化:dp[0] = value[0] dp[1] = max(dp[0], dp[1])
遍历顺序:顺序遍历
题目描述:0198基础上,房子是一个环形的:第一个房子跟最后一个房子是挨着的。
思路:分为考虑偷第一间房子和不考虑第一间房子两种情况。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrCpxciJ-1689081778514)(E:\zhangfiles\markdownfile\代码随想录刷题小计\image\image-20230404113315966.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Glpg0XBB-1689081778515)(E:\zhangfiles\markdownfile\代码随想录刷题小计\image\image-20230404113342805.png)]
题目描述:偷的房间结构为一个二叉树,相邻结点不能被偷;求最大可偷金额;
思路:二叉树动态规划与常规数组动态规划有所不同
dp[0] dp[1]
:当前结点偷与不偷可以获得的最大金额;
dp[0] = max(left[0] , left[1]) + max(right[0], right[1]);
dp[1] = root -> val + left[0] + right[0];
遍历顺序:递归遍历(后序遍历),因为本层节点的逻辑需要比较结点左右儿子的函数返回值;
初始化:if (root == NULL) return {0, 0};
class Solution {
public:
int rob(TreeNode* root) {
vector result = robTree(root);
return max(result[0], result[1]);
}
// 长度为2的数组,0:不偷,1:偷
vector robTree(TreeNode* cur) {
if (cur == NULL) return vector{0, 0};
vector left = robTree(cur->left);
vector right = robTree(cur->right);
// 偷cur,那么就不能偷左右节点。
int val1 = cur->val + left[0] + right[0];
// 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
}
};
dp[i][1]
: 在第i天不持有股票的最大利润;dp[i][0]
:在第i天持有股票的最大利润dp[i][0] = max(dp[i - 1][0], -prices[i]); //这里表示要持有更便宜的
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
初始化:dp[0][0] = -prices[i]
dp[0][1] = 0
遍历顺序:顺序遍历
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]-prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
题目描述:给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
递推公式:
dp[i][0] = dp[i - 1][0];//第i天不操作 其实也可以省略这个状态 一直为0
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0]-prices[i]);//第i天 第一次持有
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);//第i天 第一次不持有
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);//第i天 第二次持有
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);//第i天 第二次不持有
初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0];//这里比较直观
dp[0][2] = 0;
dp[0][3] = -prices[0];//这里可以认为当天买卖一次,又买; 为了后续的遍历合理性
dp[0][4] = 0;
遍历顺序:
顺序遍历;
最后结果输出:
return max(dp[prices.size() - 1][2], dp[prices.size() - 1][4]);
//为什么最后一天一定有最大值,因为我们每次更新第i天的策略都是取当前操作的最大值
int maxProfit(int k, vector& prices) {
vector> dp(prices.size(), vector(2*k + 1, 0));
for (int j = 1; j < 2*k + 1; j++)
if (j % 2 == 1)//持有股票时,初始化为-第一天的股票价格
dp[0][j] = -prices[0];
for (int i = 1; i < prices.size(); i++) {
for (int j = 1; j < 2*k+1; j++) {
if (j % 2 == 1)//持有
dp[i][j] = max(dp[i-1][j], dp[i - 1][j - 1] - prices[i]);
else//不持有
dp[i][j] = max(dp[i- 1][j], dp[i - 1][j - 1] + prices[i]);
}
}
return dp[prices.size() - 1][2*k];
}
题目描述:给定一个整数数组prices
,其中第 prices[i]
表示第i天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票);卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
dp数组:
dp[i][0]//第i天持有股票
dp[i][1]//第i天不持有股票
dp[i][2]//第i天是否卖出股票
转移方程:
if (dp[i - 1][2] == 0)//如果前一天没卖股票,今天不是冷冻期
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
else if (dp[i - 1][2] == 1 && i - 2 >= 0)//今天是冷冻期
dp[i][0] = max(dp[i - 1][0], dp[i - 2][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
if (dp[i][1] == dp[i - 1][0] + prices[i])//如果今天卖出股票,那么标志位设为1
dp[i][2] = 1;
初始化:
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
题目描述:给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
与122区别,在卖出股票的时候,要减掉手续费,其他的相同。
nums
,找到其中最长严格递增子序列的长度。for (int j = i - 1; j > -1; j--) {//顺序遍历
if (nums[i] > nums[j]) {//循环跟之前的数比较,如果大于,那么取dp[i] 和 dp[i + 1]最大值
dp[i] = max(dp[i], dp[j] + 1);
}
}
vector dp(nums.size(), 1);//因为,如果没找到比之前元素大的,那么就说明当前数字自己就是最长递增子序列,长度为1
if (nums[i] > nums[i - 1])//如果大于前一个整数,那么最大长度+1
dp[i] = dp[i - 1] + 1;
else //否则,重置为1
dp[i] = dp[i];
题目描述:给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度(子数组是连续的)。
思路:与平时的动态规划数组不同,本题需要采用二维dp数组求解,因为是两个数组之间的比较。
dp[i][j]
:以**nums1[i-1]结尾的子数组和以nums2[i-1]**结尾的子数组最长公共子数组长度;
递推公式:
if (nums1[i - 1] == nums2[i - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
初始化:dp[0][j] dp[i][0]
初始化为0;
遍历顺序:先遍历一数组或者二数组都行,因为他们的状态都是对角线方向推导而来;
text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0。dp[i][j]
:0,i-1
和0,j-1
的最长公共子序列;if (nums[i - 1] == nums[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
else
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
dp[0][j] dp[i][0]
初始化为0;题目描述:给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
dp[i]
:以nums[i]为结尾的子数组的最大和;
递推公式:
dp[i] = max(dp[i - 1], dp[i - 1] + nums[i]);//延续前面和不延续两种情况
dp[i][j]
:以s[i - 1]结尾的子串中包含以t[j - 1]结尾的子串个数;if (s[i - 1] == s[j - 1])
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
//dp[i - 1][j - 1]表示使用s[i - 1]包含最大数量
//dp[i -1][j]表示不使用s[i - 1]包含最大数量
else
dp[i][j] = dp[i - 1][j];
初始化:
dp[i][0] = 1;//t为空,s的子序列中t出现个数
dp[0][j] = 0;
dp[0][0] = 1;
遍历顺序:从上到下,从左到右
题目描述:给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
dp[i][j]
:以i-1结尾的word1和以j-1结尾的word2相同所需的最小步数;
递推公式:
if (word1[i - 1] == word[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;//删掉word1[i - 1]或者word2[j - 1];
初始化:
dp[0][j] = j;
dp[i][0] = i;
dp[0][0] = 0;
题目描述:给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
与1143类似;
dp[i][j]
:以i - 1结尾的word1转换成以j - 1结尾的word2所需的最少操作数;
递推公式:
if (word1[i - 1] == word2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;
//分别对应删除、增加、修改
//dp[i][j - 1]表示删除word2,逆向来看就是增加word1
初始化:
dp[0][j] = j;
dp[i][0] = i;
dp[0][0] = 0;
题目描述:给定一个字符串,你的任务是计算字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
思路:动态规划(空间复杂度要高一点)或者双指针。
dp[i][j]
:i - j 子串是否是回文字符串。true or false
之所以这样设置是因为回文串是一个对称的关系。如果设置成传统的dp[i],那么dp[i - 1]与其关系就很弱。
vector> dp(s.size(), vector(s.size(), false));
int result = 0;
for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序!!!
for (int j = i; j < s.size(); j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二:只有一个字符和只有两个字符
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三:大于两个字符情况
result++;
dp[i][j] = true;
}
}
}
}//这道题的遍历顺序很重要,
初始化:初始化为false,方便代码的逻辑,只处理符合回文子串的情况。
**遍历顺序:**i的状态与i + 1 有关,所以i从大到小遍历;j与j - 1有关,所以从小到大遍历。
题目描述:给你一个字符串s,找出其中最大的回文子序列,返回改序列的长度(注意子序列和子串的区别)
思路:动态规划,或者跟自己的reverse求最长公共子序列;
dp[i][i]
:i-j 最长子序列长度
递推公式:
if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
初始化: dp[i][j] = 1 if i == j
遍历顺序:i从到到小,j从小到大。
代码对比,自己写的
vector> dp(s.size(), vector(s.size(), 0));
int result = 0;
for (int i = s.size() - 1; i > -1; i--) {
for (int j = i ; j < s.size(); j++) {
if (s[i] == s[j]) { //这里判断条件太多了,其实需要判断的大多是是单个字符的情况i == j
if (j == i -1) //两个字符情况
dp[i][j] = 2;
else if (j == i ) //单个字符情况
dp[i][j] = 1;
else
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
if (i == s.size() - 1)//单个字符
dp[i][j] = 1;
else if (j == 0) //单个字符
dp[i][j] = 1;
else
dp[i][j] = max(dp[i][j - 1], dp[i + 1][j]);
}
result = max(result, dp[i][j]);//这里效率也很低,因为是子序列,所以最后一个元素一定是最长回文子串
}
}
return result;
vector> dp(s.size(), vector(s.size(), 0));
for (int i = 0; i < s.size(); i++) dp[i][i] = 1;//初始化一个字符的回文数量
for (int i = s.size() - 1; i >= 0; i--) {
for (int j = i + 1; j < s.size(); j++) {//这里直接j=i+1,去判断两个及以上字符情况
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;//当一个字符的时候已经初始化了
//两个字符的时候:dp[i][j] = dp[j][i] + 2;
//这里dp[j][i]一定等于0,因为我们只会更新矩阵上半部分的值
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.size() - 1];
题目描述:给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
思路:dp求解
dp[i]
:以i结尾的最长无重复字符子串长度
递推公式:
if (s[i] == s[i - 1])
dp[i] = 1;
else {
int j;
for (int j = i - 1; j > i - 1 - dp[i - 1]; j--) {
if(s[i] == s[j])
break;
}
dp[i] = i - j;
}
int lengthOfLongestSubstring(string s) {
if (s.size() == 0)
return 0;
int result = 1;
int slow = 0;
int fast = 0;
while(fast < s.size()) {
int i;
for (i = fast - 1; i >= slow; i--) {//这里一定是>=slow 在滑动窗口内去寻找
if (s[i] == s[fast]) {
break;
}
}
if (i == slow && s[i] != s[slow]) {//如果没有重复的
result = max(result, fast - slow + 1);
fast++;
}else {//如果有重复的
slow = i + 1;
result = max(result, fast - slow + 1);
fast++;
}
}
return result;
}
'('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。dp[i]
:以i结尾的最长有效括号子串的长度。情况一:
s[i] == '(' 此时无法以s[i]结尾形成有效括号子串:dp[i] = 0;
情况二:
s[i] == ')'
子情况一:s[i - 1] == '('
此时s[i]与s[i - 1]形成有效括号,此时还要加上s[i - 2]的有效括号长度
dp[i] = dp[i - 2] + 2;
子情况二:s[i - 1] ==')'
此时需要去找与s[i - 1]匹配的有效括号的位置上一个位置,即s[i - dp[i - 1] - 1]
if s[i - dp[i - 1] - 1] == '('
那么此时dp[i] = dp[i - 1] + 2;
还要考虑"()(())"这种连接的情况,因为现在是一个完整的有效括号了
dp[i] = dp[i - 1] + 2 + dp[i - 2 - dp[i - 1]];
初始化:初始化为0;
遍历顺序:从小可以推出大,所以顺序遍历;
n
,对于 0 <= i <= n
中的每个 i
,计算其二进制表示中 1
的个数 ,返回一个长度为 n + 1
的数组 ans
作为答案。//如果x是偶数,那么它可以表示为x/2 * 2,即把x/2左移了一位,不会发生1数量变化
//如果x是奇数,那么它可以表示为x - 1 + 1,即比x小的偶数+1,1的数量随之+1
vector dp(n + 1, 0);
for (int i = 1; i <= n; i++) {
if (i % 2 == 0) {//偶数
dp[i] = dp[i / 2];
}else {//奇数
dp[i] = dp[i - 1] + 1;
}
}
return dp;
//int一共32位,常规求一个数字1的个数,要右移或者左移32次
//Brian Kernighan算法: 令x = x&(x-1),这个运算每次使x最后一个1变为0,重复这个操作,直到x变为0,可以得到1的个数,时间复杂度位log(n) < 32
题目描述:有 n
个气球,编号为0
到 n - 1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。戳破第 i
个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1]
枚硬币。 这里的 i - 1
和 i + 1
代表和 i
相邻的两个气球的序号。如果 i - 1
或 i + 1
超出了数组的边界,那么就当它是一个数字为 1
的气球。
求所能获得硬币的最大数量。
思路:区间dp
dp[i][j]
:区间(i,j)戳破所有气球能得到的最大金币数量
递推公式:
dp[i][j] = max k (dp[i][k] + dp[k][j]) + value[k]*value[i]*value][j] (k range (i,j))
//假定k是区间(i,j)最后一个被戳破气球的下标,那么可以将区间划分为(i, k), (k, j)
//那么dp[i][j]可以由dp[i][k] dp[k][j]状态转移过来
//因为k是最后一个被戳破的,所以两个区间互不影响,可以用和的形式来表示
初始化:可以全部初始化为0,因为空区间金币数本身为0,其他区间可以由空区间推导而来。
遍历顺序:
其中,红色表示已初始化,黑色表示当前需要求解的最大金币值,绿色表示当前区间推导需要的区间位置。
可以看出,每次都需要切割区间,那么大区间回切成一个个小区间去完成状态推导;比如长度为2的区间由区间长度1的区间推导而来,所以遍历顺序为:先去求解区间长度为1的区间,再去递增求解高长度区间。
int maxCoins(vector& nums) {
int n = nums.size();
//n+2,在区间两边添加1,方便处理边界情况
vector> dp(n + 2, vector(n+ 2, 0));
//存储nums,两边+1,方便处理边界
vector temp(n + 2);
temp[0] = 1;
temp[n + 1] = 1;
for (int i = 1; i <= n; i++) {
temp[i] = nums[i - 1];
}
//len表示区间长度,从区间长度递增方向遍历
for (int len = 3; len <= n + 2; len++) {
//l表示区间左边界
for (int l = 0; l <= n + 2 - len; l++) {
//遍历最后一个戳破的气球
for (int k = l + 1; k < l + len - 1; k++) {
int left = dp[l][k];
int right = dp[k][l + len - 1];
int sum = left + right + temp[k]*temp[l]*temp[l + len - 1];
dp[l][l + len - 1] = max(dp[l][l + len - 1], sum);
}
}
}
return dp[0][n + 1];
}
题目描述:给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
思路:滑动窗口求解(滑动hash超时);
滑动窗口思想:用i,j表示滑动窗口的左边界和右边界,通过改变i,j来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度j-i+1,这些长度中的最小值就是要求的结果。
步骤一:不断增加j使滑动窗口增大,直到窗口包含了T的所有元素
步骤二:不断增加i使窗口缩小,因为是要求最小子串,所以将不必要的元素排除在外,使长度减少,直到碰到一个必须包含的元素,这个时候不能再扔了,记录此时滑动窗口的长度,并更新最小值
步骤三:让i再增加一个位置,这个时候窗口包含子串不满足条件,那么继续从步骤一开始执行,寻找新的满足条件的滑动窗口,如此反复,直到j超出了字符串S的范围
//map记录字符串中各个字符需要数量,相当于hash表,need表示t需要的,win表示窗口
unordered_mapneed, win;
for (auto &i : t) need[i]++;
//left,right表示窗口左右端口
//count表示字符种类数,len表示最小长度,start表示最小长度起始位置
int left = 0, right = 0, count = 0, len = INT_MAX, start = 0;
while(right < s.size()) {
//窗口右边第一个元素是t需要的
if (need.find(s[right]) != need.end()) {
//对应字符数量+1
win[s[right]]++;
//这里注意==的用法,表示s[right]这个元素第一次满足需求的数量
if (win[s[right]] == need[s[right]]) {
//种类数+1
count++;
}
}
//如果窗口可以覆盖字符串t
while(count == need.size()) {
//更新最小长度
if (len > right - left + 1) {
start = left;
len = right - left + 1;
}
//窗口缩小,对应字符数量减少,如果不满足覆盖要求了,种类数-1
if (need.find(s[left]) != need.end()) {
if (need[s[left]] == win[s[left]]) {
--count;
}
win[s[left]]--;
}
//缩小窗口
left++;
}
//扩大窗口,这里主要right++位置,如果放在while前面会漏掉"AD" "A" 这种情况
right++;
}
//三元运算符
return len == INT_MAX ? "" : s.substr(start, len);
int maximalSquare(vector>& matrix) {
vector> dp(matrix.size(), vector(matrix[0].size(), 0));
int result = 0;
for (int i = 0; i < dp.size(); i++) {
if (matrix[i][0] == '1') {
dp[i][0] = 1;
result = 1;
}
}
for (int i = 0; i < dp[0].size(); i++) {
if (matrix[0][i] == '1') {
dp[0][i] = 1;
result = 1;
}
}
//递推公式: dp[i][j]以nums[i][j]为右下角的最大正方形
//dp[i][j] == 0, dp[i][j] = 0
//dp[i][j] == 1, dp[i][j] = min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j])
//有理论推导,但是可以画个图找规律(dp[i][j - 1] dp[i - 1][j] dp[i - 1][j - 1]分别最小,看看结果)
for (int i = 1; i < dp.size(); i++) {
for (int j = 1; j < dp[0].size(); j++) {
if (matrix[i][j] == '0') dp[i][j] = 0;
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}
result = max(result, dp[i][j]);
}
}
return result*result;
}
temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。题目描述:nums1
中数字 x
的 下一个更大元素 是指 x
在 nums2
中对应位置 右侧 的 第一个 比 x
大的元素。
给你两个 没有重复元素 的数组 nums1
和 nums2
,下标从 0 开始计数,其中nums1
是 nums2
的子集。
对于每个 0 <= i < nums1.length
,找出满足 nums1[i] == nums2[j]
的下标 j
,并且在 nums2
确定 nums2[j]
的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1
。
返回一个长度为 nums1.length
的数组 ans
作为答案,满足 ans[i]
是如上所述的 下一个更大元素 。
思路:单调栈,与739解题思路一样,但是需要map来存每一个元素和它的下一个更大元素,最后对nums1中的每个元素都搜索map中是否有索引;
题目描述:在496基础上,下一个最大元素可以继续从头去找,而不是到数组末尾就结束了(成环);
思路:与496思路相同,难点在于成环的处理,成环一般考虑成重复拼接的过程,搭配取模的操作,可以更简单的实现;
vector nextGreaterElements(vector& nums) {
stack st;
vector result(nums.size(), -1);
for (int i = 0; i < nums.size()*2; i++) {//关键在于取模的过程,其实相当于在后面又拼接了一遍nums数组
while(!st.empty() && nums[i%nums.size()] > nums[st.top()]) {
result[st.top()] = nums[i%nums.size()];
st.pop();
}
st.push(i%nums.size());
}
return result;
}
题目描述:给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路:双指针、单调栈;
双指针,关键是理解能接的雨水可以分解成如下一列列的形式:
那么能接的雨水数量,可以分解为对于每个height[i],寻找他左边的最大值和右边的最大值,那么i这一列可以接到的雨水数为:min(left,right)-height[i];
暴力解法为每次都遍历左右两边,找大最值,是超时的,可以通过先求每一列左边最大值和右边最大值数组left[i]、right[i],一次遍历求解能接的雨水数量。
具体思路:先将0压入栈,接着遍历height数组;如果height[i]比栈顶元素大,那么弹出栈顶元素,可以得到(height[i]是比弹出的元素大的右边第一个元素,现在的栈顶元素是比当前元素大的左边第一个元素),可以计算得一部分雨水量,继续比较栈顶元素与height[i],直到height[i]<=栈顶元素,st.push(i)。最后遍历结束,将所有雨水量收集起来为最终结果。
题目描述:给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路:单调栈(单调递减栈,用来找下一个小的元素),与接雨水思路相同;
难点在于,对于每个以heighs[i]为基准去寻找最大矩形,那么这个最大矩形是以左边第一个比height[i]小的列和右边第一个比heights[i]小的列组成(左闭右闭);这个思路与接雨水是很相似的,接雨水是找左边和右边第一个比当前列大的元素位置。
与接雨水类似,需要处理三种情况(或者两种情况)
情况一:当前遍历元素heights[i]比栈顶元素大
情况二:当前遍历元素heights[i]等于栈顶元素
情况三:当前遍历元素heights[i]小于栈顶元素
技巧:在heights[i]数组头部和尾部补0;如果高度数组为[1,2,3,4,5],那么此时全部入栈,我们是在情况三去计算result的,那么这时会输出0,需要加额外逻辑,此时尾部补0,可以顺应逻辑,如果高度数组为[5,4,3,2,1],这是会取不到left;
int largestRectangleArea(vector& heights) {
int result = 0;
stack st;
heights.insert(heights.begin(), 0); // 数组头部加入元素0
heights.push_back(0); // 数组尾部加入元素0
st.push(0);
// 第一个元素已经入栈,从下标1开始
for (int i = 1; i < heights.size(); i++) {
if (heights[i] > heights[st.top()]) { // 情况一
st.push(i);
} else if (heights[i] == heights[st.top()]) { // 情况二
st.pop(); // 这个可以加,可以不加,效果一样,思路不同
st.push(i);
} else { // 情况三
while (!st.empty() && heights[i] < heights[st.top()]) { // 注意是while
int mid = st.top();
st.pop();
if (!st.empty()) {
int left = st.top();
int right = i;
int w = right - left - 1;
int h = heights[mid];
result = max(result, w * h);
}
}
st.push(i);//这里最后一定要把当前元素入栈
}
}
return result;
}
0
和 1
、大小为 rows x cols
的二维二进制矩阵,找出只包含 1
的最大矩形,并返回其面积。第一行高度分别为:[1, 0, 1, 0, 0];
第二行高度分别为:[2, 0, 2, 1, 1];
第三行高度分别为:[3, 1, 3, 2, 2];
第四行高度分别为:[4, 0, 0, 3, 0];
注意计算第n行高度的时候,如果当前行元素为0,那么可以把本行高度置0;因为当前行元素为0,其它列的柱形想要联结本列形成最大矩形的话,一定不会包含本行为0的元素,也就是说上一行计算的最大矩阵已经包含了本行联结这一列的结果,所以可以高度置0;
思路:深度优先所搜算法,沿着每一个可能的路径向下进行搜索,直到不能再深入为止,并且每个节点只能访问一次;
求解迷宫问题代码(数组这种结构):
bool is_taoli = false;
int n;
vector dx = { 0,0,-1,1 };
vector dy = { -1,1,0,0 };
bool is_valid(int x, int y, vector> map, vector>& used) {
if (x > n || y > n || x < 1 || y < 1 || map[x][y] == 1)
return false;
return true;
}
void dfs(int x, int y, vector> map, vector>& used) {
if (used[x][y])
return;
if (x == n && y == n) {
is_taoli = true;
return;
}
used[x][y] = true;
for (int i = 0; i < 4; i++) {
if (is_valid(x + dx[i], y + dy[i], map, used)) {
dfs(x + dx[i], y + dy[i], map, used);
}
}
}
思路:bfs是一种盲目搜索法,它搜索检查每一个可以到达的点,直到找到结果位置;
求解迷宫问题代码:
void bfs(int x, int y, vector> map, vector>& used) {
queue> que;
que.push({ x, y });
while (!que.empty()) {
vector node = que.front();
que.pop();
if (used[node[0]][node[1]] == 1)
continue;
if (node[0] == n && node[1] == n) {
is_taoli = true;
break;
}
used[node[0]][node[1]] = true;
for (int i = 0; i < 4; i++) {
if (is_valid(node[0] + dx[i], node[1] + dy[i], map, used)) {
que.push({ node[0] + dx[i], node[1] + dy[i] });
}
}
}
}
题目描述:给你一个由若干括号和字母组成的字符串 s
,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。
思路:这道题包含小写字母并且需要返回所有满足条件的结果,基本可以确定需要暴力搜索来求解;
回溯:转换为子集问题,再从子集中找符合条件的结果返回;会超时,单单一个回溯就会超时,因为它相当于找了所有删除的结果,冗余度太高。(dfs思路好像可以)
bfs:每一层放上一层删除一个元素的结果,用set来去重,如果这一层有满足条件的字符串,那么就可以返回结果,从最小删除无效括号的数量出发。
class Solution {
public:
int legal(string s) {//计数器法判断字符串括号是否合法
int cnt = 0;
for(auto x : s){
if(x == '(') cnt++;
if(x == ')') cnt--;
if(cnt < 0) return 0;//')'多的情况
}
return cnt == 0; //'('是否多
}
vector removeInvalidParentheses(string s) {
vector res;//结果集
if(legal(s)) {
res.push_back(s);
return res;
}
int n = s.length();
queue q;
q.push(s);//第一层就是不删除的情况,s
while(!q.empty()){
vector all;//上一层元素
unordered_set now;//当前层元素,用set去重
unordered_set leg;//当前层是否有合法元素,set去重
int m = q.size();
for(int i = 0; i < m; i++) {
all.push_back(q.front());
q.pop();
}
for(auto s : all) {
int len = s.length();
for(int i = 0; i < len; i++) {
if(s[i] == '(' || s[i] == ')') {//任意删除一个元素
string tmp = s;
tmp.erase(i,1);
now.insert(tmp);
if(legal(tmp)) leg.insert(tmp);
}
}
}
if(!leg.empty()) {//如果当前层有合法元素,说明出现删除最小数量合法的字符串括号,返回结果
for(auto x : leg) {
res.push_back(x);
}
break;
} else {//否则,继续下一层删除
for(auto x : now) q.push(x);
}
}
return res;
}
};
#include
using namespace std;
#define maxSize 1000
struct ArcNode {
int adjvex;
ArcNode* nextarc;
int info;//权重
};
struct Vnode {
int data;
ArcNode* firstarc;
};
struct Graph {
Vnode adjlist[maxSize];
int n, e;
};
vector used(10, 0);
Graph* graph;
void insertNode(ArcNode* node, ArcNode* newNode) {
ArcNode* p = node;
while (p->nextarc != NULL)
p = p->nextarc;
p->nextarc = newNode;
}
void create() {
graph = new Graph;
cout << "输入顶点的数目" << endl;
cin >> graph->n;
cout << "输入边的个数" << endl;
cin >> graph->e;
int u = -1, v = -1, weight = -1;
for (int i = 0; i < graph->n; i++) {
graph->adjlist[i].firstarc = NULL;
}
ArcNode* node;
for (int i = 0; i < graph->e; i++) {
cin >> u >> v;
node = new ArcNode;
node->adjvex = v;
node->nextarc = NULL;
graph->adjlist[u].data = u;
if (graph->adjlist[u].firstarc == NULL) {
graph->adjlist[u].firstarc = node;
}
else
insertNode(graph->adjlist[u].firstarc, node);
}
}
void bfs() {
queue qe;
qe.push(graph->adjlist[0].data);
while (!qe.empty()) {
int temp = qe.front();
qe.pop();
if (used[temp] == 1)
continue;
used[temp] = 1;
cout << temp << endl;
ArcNode* node = graph->adjlist[temp].firstarc;
while (node != NULL) {
qe.push(node->adjvex);
node = node->nextarc;
}
}
}
'1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。题目描述:你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1]
表示:想要学习课程 0
,你需要先完成课程 1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
思路:拓朴排序相关题目。首先统计每个节点的入度,将入度为0的节点加入队列;通过bfs来遍历每个节点,将相应节点的入度-1,最后判断每个节点是否都没有入度了,如果都没有入度了,那么可以按照一定顺序修完所有课程;有入度即表示,想修该课程必须先修其他课程;.
bfs解法
bool canFinish(int numCourses, vector>& prerequisites) {
vector rudu(numCourses, 0);//入度数组
int count = 0;
for (const auto& info : prerequisites) {//统计入度
rudu[info[0]]++;
}
queue que;
for (int i = 0; i < numCourses; i++) {//入度为0加入队列,表示可以先修这个课程
if (rudu[i] == 0) {
que.push(i);
count++;
}
}
while(!que.empty()) {
for (const auto& info : prerequisites) {//先修可以修的课程,然后把它的所有相邻节点的入度-1,如果某个相邻节点的入度为0,那么将v放入队列中
if (info[1] == que.front()) {
rudu[info[0]]--;
if (rudu[info[0]] == 0) {
que.push(info[0]);
count++;
}
}
}
que.pop();
}
return count == numCourses;
}
题目描述:给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
思路:穷举搜索(类似dfs);每访问一个节点node,检测以node为起始节点向下延申的路径有多少种,同时判断是否满足条件。递归遍历每一个节点的所有可能路径,然后将这些路径数目加起来即为返回结果。
首先定义rootSum(p, val)表示以节点p为起点向下且满足路径和为val的路径数目。对二叉树上的每个节点p求出rootSum(p, targetSum),然后对这些路径数目求和返回结果。
//递归求解以root节点向下且符合路径和的路径数目
int rootSum(TreeNode* root, long targetSum) {
if (root == nullptr) return 0;
int ret = 0;
//判断到本节点是否满足路径和,这里不return,继续向下搜索,因为有负数情况
if (root -> val == targetSum) ret++;
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
//遍历每个节点,然后对每个节点求向下且满足路径和的路径数目,最后递归返回累加求和
int traversal(TreeNode* root, int targetSum) {
if (root == nullptr) return 0;
int ret = 0;
ret += rootSum(root, targetSum);
ret += traversal(root -> left, targetSum);
ret += traversal(root -> right, targetSum);
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
return traversal(root, targetSum);
}
djlist[u].data = u;
if (graph->adjlist[u].firstarc == NULL) {
graph->adjlist[u].firstarc = node;
}
else
insertNode(graph->adjlist[u].firstarc, node);
}
}
##### bfs(广度优先搜索)
```c++
void bfs() {
queue qe;
qe.push(graph->adjlist[0].data);
while (!qe.empty()) {
int temp = qe.front();
qe.pop();
if (used[temp] == 1)
continue;
used[temp] = 1;
cout << temp << endl;
ArcNode* node = graph->adjlist[temp].firstarc;
while (node != NULL) {
qe.push(node->adjvex);
node = node->nextarc;
}
}
}
'1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。题目描述:你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1]
表示:想要学习课程 0
,你需要先完成课程 1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
思路:拓朴排序相关题目。首先统计每个节点的入度,将入度为0的节点加入队列;通过bfs来遍历每个节点,将相应节点的入度-1,最后判断每个节点是否都没有入度了,如果都没有入度了,那么可以按照一定顺序修完所有课程;有入度即表示,想修该课程必须先修其他课程;.
bfs解法
bool canFinish(int numCourses, vector>& prerequisites) {
vector rudu(numCourses, 0);//入度数组
int count = 0;
for (const auto& info : prerequisites) {//统计入度
rudu[info[0]]++;
}
queue que;
for (int i = 0; i < numCourses; i++) {//入度为0加入队列,表示可以先修这个课程
if (rudu[i] == 0) {
que.push(i);
count++;
}
}
while(!que.empty()) {
for (const auto& info : prerequisites) {//先修可以修的课程,然后把它的所有相邻节点的入度-1,如果某个相邻节点的入度为0,那么将v放入队列中
if (info[1] == que.front()) {
rudu[info[0]]--;
if (rudu[info[0]] == 0) {
que.push(info[0]);
count++;
}
}
}
que.pop();
}
return count == numCourses;
}
题目描述:给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
思路:穷举搜索(类似dfs);每访问一个节点node,检测以node为起始节点向下延申的路径有多少种,同时判断是否满足条件。递归遍历每一个节点的所有可能路径,然后将这些路径数目加起来即为返回结果。
首先定义rootSum(p, val)表示以节点p为起点向下且满足路径和为val的路径数目。对二叉树上的每个节点p求出rootSum(p, targetSum),然后对这些路径数目求和返回结果。
//递归求解以root节点向下且符合路径和的路径数目
int rootSum(TreeNode* root, long targetSum) {
if (root == nullptr) return 0;
int ret = 0;
//判断到本节点是否满足路径和,这里不return,继续向下搜索,因为有负数情况
if (root -> val == targetSum) ret++;
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
//遍历每个节点,然后对每个节点求向下且满足路径和的路径数目,最后递归返回累加求和
int traversal(TreeNode* root, int targetSum) {
if (root == nullptr) return 0;
int ret = 0;
ret += rootSum(root, targetSum);
ret += traversal(root -> left, targetSum);
ret += traversal(root -> right, targetSum);
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
return traversal(root, targetSum);
}