代码随想录刷题小记

代码随想录刷题小记

6月计划

1.leetcode刷到200道

2.熟悉下求图最小路径怎么写(bfs,dfs,迪杰斯特拉)

3.netco项目理清流程,底层做进一步理解,照着它的代码手撸一遍;

cin输入数组

#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 来终止输入;测试平台会自动处理,可以提交通过。

cin.tie与sync_with_stdio加速IO

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

215.数组中的第K个最大元素(高频面试题)

  • 题目描述:给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
  • 思路:先快排再找第K个元素(nlog(n)),基于快排的选择方法(O(n), O(logn))、基于堆排的选择方法、计数排序
//采用基于快速排序的选择方法
//每次分治,会选择一个元素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);
	}
}

34.在排序数组中查找元素的第一个和最后一个位置

  • 题目描述:给你一个按照非递减顺序排列的整数数组 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;
    }
}

牛客101题

461.汉明距离

  • 题目描述:两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

    给你两个整数 xy,计算并返回它们之间的汉明距离。

//主要是计算一个数字二进制为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;
    }

20.数组中的逆序对

  • 题目描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

  • 思路:归并排序的思想;归并排序时间复杂度: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);
        }
    

448.找到所有数组中消失的数字

  • 题目描述:给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
  • 思路:哈希表求解:方法一:用额外空间 方法二:用原数组nums当哈希表,不用额外空间
//不用额外空间解法
//由于数组大小为n,所以联想到用数组来当作哈希表使用。nums的范围在[1-n]中,可以利用这个范围之外的数字来表达是否出现的含义
//具体来说,遍历nums,每遇到一个数x,就让nums[x-1]加n。由于nums中所有数均在[1-n]中,增加以后,这些数必然大于n。最后遍历nums,若nums[i]未大于n.就说明没有遇到数字i + 1,这样就找出了所有缺失的数字。

560.和为K的子数组

  • 题目描述:给你一个整数数组 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;
    

33.搜索旋转排序数组

  • 题目描述:整数数组 nums 按升序排列,数组中的值 互不相同

    在传递给函数之前,nums 在预先未知的某个下标 k0 <= 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;

287寻找重复数

  • 题目描述:
  • 思路:原数组作为哈希表
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;
    }
    

48.旋转图像

  • 题目描述:给定一个 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;
            }
        }
    }
};

128.最长连续序列

  • 题目描述:给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
  • 思路:用unordered_set来去重并处理
//首先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;

.31.下一个排列

  • 题目描述:leetcode
  • 思路:两边扫描(官方思路)
//自己写的一版
//思路:对于序列[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());
}

49.字母异位词分组

  • 题目描述:给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

    字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

  • 思路:哈希表比较、排序

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;
}

155.最小栈

//辅助栈(栈里放包括当前元素以前的最小元素)
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;
};

21.合并两个有序链表

  • 题目描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
  • 思路:迭代、递归。
//迭代法(自己写的版本,很罗嗦,直接在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;
}
};

148.排序链表

  • 题目描述:给你链表的头结点 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;
    }
    

106.构造二叉树

  1. 给出二叉树的前序/后序、中序的遍历数组,可以唯一确定一个二叉树结构;

  2. 只有前序和后序不能确认二叉树结构;

  3. 重构思想(以后序与中序为例):

    递归实现:首先后序的最后一个元素为根节点

    在中序中找到该节点,切割中序数组为左右两部分,左半部分为左子树 右半部分为右子树

    再来切割后序数组,以左子树的大小为依据切割

    递归重构 cur -> left = Traversal(vector inorderleft, vector postorderleft);

    cur -> right = Traversal(vector inorderright, vector postorderright);

  4. 优化方法;用左右数组范围替代传入vector

654.最大二叉树

1.以数组中最大元素为依据左右分割为左子树与右子树

2.思路与106相同

124.二叉树的最大路径和

题目描述:二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 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;
    }

543.二叉树的直径

  • 题目描述:给你一棵二叉树的根节点,返回该树的 直径 。二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。两节点之间路径的 长度 由它们之间边数表示。
  • 思路:后序遍历,同124的思路。
    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;
    }

94.二叉树中序遍历(Mirros)

题目描述:二叉树中序遍历(不用额外空间)

//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;
}

114.二叉树展开为链表

  • 题目描述:给你二叉树的根结点 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(路径,选择列表); //递归
        回溯,撤销处理结果
	}
}

77.组合

  • 题目描述:给定两个整数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(); // 回溯,撤销处理的节点
}
  • 优化思路:当i-n之间的元素个数不足我们需要的组合时,比如n = 4, k = 4,那么当i = 2是没有意义的。所以比较剩余元素个数和需要元素的个数,进行剪枝优化。
  1. 已经选择的元素个数:path.size();
  2. 所需需要的元素个数为: k - path.size();
  3. 列表中剩余元素(n-i)+ 1 >= 所需需要的元素个数(k - path.size())
  4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

216.组合总和Ⅲ

  • 题目描述:找出所有相加之和为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();
    }
    
  • 剪枝

17.电话号码的字母组合

  • 给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。(对于重复数字,后台认为"ab"和"ba"不相同,这点使得这个题比较简单)
  • 思路:先用数组保存每个数字对应的字母字符串,然后用回溯算法求解。
  • 递归函数参数:
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();
        }
    }

39.组合总和

  • 题目描述:给定一个无重复元素的数组candidates和一个目标数target,找出candidates中所有使数字和为target的组合。(candidates中的数字可以无限制重复被选取)
  • 思路:回溯搜索,与216的不同点在于本题可以重复选取数字。
  • 递归函数参数:
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)来实现。(简单粗暴的理解方式)

40.组合总和Ⅱ

  • 题目描述:给定一个集合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();
            }
    

131.分割回文串

  • 题目描述:给定一个字符串s,请将s分割成一些子串,使每个子串都是回文串。返回所有的分割方案。
  • 思路:所有分割方案,这种题一般采用暴力搜索,所以用回溯算法来搜素。
  • 递归函数返回值
    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();
            }
        }

93.复原IP地址

**题目描述:**给定一个只包含数字的字符串,复原它并返回所有可能的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;
    }

78.子集

  • 题目描述:给定一组不含重复元素的整数数组nums,返回该数组所有可能的子集。(解集不能包含重复的子集)
  • 思路:如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
  • 递归函数参数:
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();
        }

90.子集Ⅱ

  • 题目描述:给定一个整数数组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();
            }
    

491.递增子序列(重要)

  • 题目描述:给定一个整形数组,你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。(数组中可能包含重复数组,相等的数字被视为递增的一种情况)
  • 思路:找到所有满足条件的组合,注意这个子序列是有顺序的,所以之前的排序去重套路失效!!转而使用used数组或者set。
  • 递归函数参数:
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();
}

46.全排列

  • 题目描述:给定一个不含重复数字的数组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();
    }
}

47.全排列Ⅱ

  • 题目描述:给定一个可包含重复数字的序列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();
                }
            }
    

51.N皇后

  • 题目描述:N皇后问题
  • 思路:棋盘的每一行代表一次递归层,通过回溯遍历所有条件,根据条件判定是否进入下一层回溯,有点套路化。
  • 递归参数:
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;
    }

37.数独

  • 题目描述:填充空格来解决数独问题。
  • 思路:这种问题一般需要穷举,所以考虑回溯遍历找解。与之前的递归不一样,比如N皇后问题,一行只能放一个皇后,而这道题一行要放九个数字,所以单层递归里面一个for循环是不够的,需要两层for循环来求解。
  • 递归函数参数:
bool backtracking(vector>& board);//这道题要直接修改board
  • 终止条件:本题的代码随想录写法不需要终止条件,因为通知bool的返回来判断是否找到解了。
  • 单层逻辑:
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;
   }

22.括号生成

  • 题目描述:数字 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;
}

贪心算法

455.分发饼干

  • 题目描述:你是一个家长,要给孩子们分发食物,g[i]表示每个孩子的胃口值,s[i]表示每个饼干的尺寸,求出尽可能喂饱孩子的数量。
  • 思路:用尽可能小的饼干去满足小胃口的孩子。先排序胃口和饼干,然后遍历饼干,去喂胃口小的孩子。

376.摆动序列

  • 题目描述:有一个数组,求出该数组中为摆动序列的最长子序列的长度。

  • 思路:贪心算法;找到数组的波峰和波谷,计算有摆动的位置。

  • 这道题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;
        }
    };
    

53.最大子数组和

  • 问题描述:给定一个整数数组,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
  • 贪心策略:从头开始计算累加和,如果累加和为负数,那么舍弃掉这个累加和,从下一个数重新计算;同时result要不断更新得到的最大子序列和。
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;
    }

55.跳跃游戏

  • 题目描述:给定一个非负整数数组,你最初位于数组的第一个位置,数组中的每个元素代表你在该位置可以跳跃的最大长度,判断你是否能够达到最后一个位置。
  • 思路:不拘泥于具体的跳跃过程,计算覆盖范围,从第一个数字的覆盖范围开始,计算后续范围内的最大覆盖范围,不断更新覆盖范围,如果最大覆盖范围能够包含最后一个数字,那么返回true;
    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;
    }

45.跳跃游戏Ⅱ

  • 题目描述:于55类似,本题求跳到最后一个位置的最少步数(测试用例保证可以跳到最后一个位置)。
  • 思路:同样不要拘泥于具体在哪个位置跳,只判断覆盖范围,当i到达上一跳的最大范围时,如果没达到终点,那么更新最大范围,步数+1,继续循环,否则结束输出结果。
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;
    }

1005.K次取反后最大化的数组和

  • 题目描述:给定一个数组A,选择某个下标i并将nums[i]替换为-nums[i],重复这个过程k次,可以多次选择同一个下标i。以这种方式修改数组后,返回数组最大的可能和。

  • 思路:如果数组存在负数,那么先反转负数(绝对值大的负数),如果负数都反转完了,将最小的正数反转剩下的次数。

  • 解题步骤:

    第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小

    第二步:从前向后遍历,遇到负数将其变为正数,同时K–

    第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完

    第四步:求和

134.加油站

  • 题目描述:在一条环路上有 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的剩余油量,可以保证汽车跑一圈。
    }

135.分发糖果

  • 题目描述:老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:

    ​ 每个孩子至少分配到 1 个糖果。

    ​ 相邻的孩子中,评分高的孩子必须获得更多的糖果。

    ​ 那么这样下来,老师至少需要准备多少颗糖果呢?

  • 思路:这是属于两边要兼顾的题目,需要考虑孩子左右两边得分情况。先从左边遍历,如果当前孩子比左边高,那么candys[i] = candys[i - 1] + 1;同理,再从右边遍历;最后取两个结果的最大值,表示同时满足两个条件。

860.柠檬水找零

  • 题目描述:

  • 在柠檬水摊上,每一杯柠檬水的售价为 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;

406.根据身高重建队列(不好想)

  • 题目描述:
  • 思路:有点类似于分发糖果的题目,先按照一个方向去贪心,然后根据另一个方向去调整结果;先按照身高从大到小排序,如果身高一样,按照K的值从小到大排序。然后从头开始遍历,按照K的值来插入。

在按照身高从大到小排序后:优先按照身高高的人的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;
    }

452.用最少数量的箭引爆气球

  • 题目描述:

  • 思路:类似于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;
    }

435.无重叠区间

  • 题目描述:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

    注意: 可以认为区间的终点总是大于它的起点。 区间 [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;
    }

763.划分字母区间

  • 字符串 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;
    }

56.合并区间

  • 题目描述:给出一个区间的集合,请合并所有重叠的区间。
  • 思路:与452、435类似;先按左边界从小到大排序;初始化左右边界,从头开始遍历,如果当前区间与下一个区间重叠,那么就合并这两个区间(取两个区间右边界的最大值、左边界的最小值);
    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;
    }

738.单调递增的数字

  • 题目描述:给定一个非负整数 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);
        }
    

    968.监控二叉树

    • 题目描述:给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。
    • 思路:让叶子节点的父节点安装摄像头,以此为依据,继续安装,直到所有节点都被监控;
    • 难点:二叉树的遍历、节点信息的传递
    • 遍历顺序:我们需要知道节点左右孩子的状态来判断当前节点的状态,所以需要后序遍历;
    • 返回值:关键的一点,把节点状态分类:无覆盖、有摄像头、已覆盖;
    • 细节:当遇到空节点返回?应该返回已覆盖
    • 最后判断头节点,会不符合设定的逻辑,比如出现无覆盖的情况,可以再判断一次或者添加虚拟头结点;
    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;
        }
    };
    

    581.最短无序连续子数组

    • 题目描述:给你一个整数数组 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 ;
      

背包问题系列(01背包和完全背包)

01.背包理论基础

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背包问题,先遍历背包或者物品都行

01.背包理论基础(滚动数组)

1.dp数组含义:dp[j] 容量为j的背包能装的最大价值

2.递推公式:dp = max(dp[j], dp[j - weight[i]] + value[i])

初始化:dp[0] = 0

0416.分割等和子集

  • 题目描述:给出一个只包含正整数的非空数组,判断时候可以分割为两个子集,使得两个子集的元素和相等。

  • 思路:动态规划;判断能付否将数组元素大小的物体装入背包,使背包装的价值最大

  • 背包的容量为sum/2,物品的重量和价值都为数组元素大小

  • 当背包价值最大,如果最大价值等于背包容量,那么return true;

1049.最后一块石头的重量

  • 题目描述:有一堆石头,用整数数组表示;每一回合,从中任意选出两个石头,将他们一起粉碎,有两种情况:

    ​ x==y 两个石头完全粉碎

    ​ x

  • 思路:本质是一个石头分堆问题,当两堆石头质量越接近,那么相撞后剩余的小石头质量越小

  • 转化为背包问题,设置背包容量为sum/2,找到价值最大的解即为其中一堆石头的重量,总重量减去即为另一堆石头的重量,最后计算相撞的结果。

494.目标和

  • 题目描述:给你一个非负整数数组,一个目标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;
            }
    

474.一和零

  • 题目描述:

  • 给你一个二进制字符数组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背包滚动数组解法

02.完全背包理论基础

  • 与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]);
            }
        }
    

518.零钱兑换Ⅱ

  • 题目描述:给你一个整数数组coins表示不同面额的硬币,另有一个整数amount表示总金额,计算并返回可以凑成总金额的硬币组合数。
  • 思路:与494目标和类似,不过本问题是多重背包问题,因为不同面额的硬币有无数个;
  • dp[j]:装满背包j一共有多少种方法;
  • 递推公式:dp[j] += dp[j - coins[i]];
  • 初始化:dp[0] = 1;有争议,leetcode后台数据指定为0,如果为0,那么dp数组全部为0,没有意义;
  • 遍历顺序: 先遍历物品,再遍历背包(组合数); 如果先背包,再物品得到的是组合数;
  • 通过二维dp数组的递归规律判断滚动数组的遍历顺序;

377组合总和(与518很像,不过这是一个排列问题,后续再搞懂具体原理3/30)

  • 题目描述:给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的个数。(不同的排列当作不同的结果)

  • 思路:多重背包问题:先遍历背包再遍历物品

70.爬楼梯(完全背包求解)

  • 思路:与377题是一样的题目;

322.零钱兑换

  • 题目描述:给定不同面额的硬币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];
    
  • 遍历顺序:先遍历物品、先遍历背包都可以;先遍历物品表示组合数,先遍历背包表示排列数。对于本题最小硬币数量来讲,结果是一样的。

279完全平方数

  • 题目描述:给定正整数n,找到若干个完全平方数(如1,4,9,16)使得他们的和等于n。你需要让组成和的完全平方数的个数最少。
  • 思路:此题与322题完全相同,此题可能不会出现找不到组合的情况。

139.单词拆分

  • 题目描述:给定一个字符串s和一个字符串列表wordDict作为字典。请判断是否可以利用字典中出现的单词拼接出s。(字典中的单词可以重读使用,不要求字典中的单词全部使用)
  • dp[j]:s字符串0-j的子串可不可以被拼接;
  • 递推公式:
dp[j] = if (dp[i] == true && st.find(substr(i, j -i) != st.end())   0 < i < j
  • 初始化:dp[0] = true , 其他为false; 无明显含义,单纯为了递推公式推导;
  • 遍历顺序:先遍历容量,再遍历子串是否能组成(判断条件);
  • 这道题不要去套完全背包,单纯看作一个dp问题来看。

打家劫舍系列

0198.打家劫舍

  • 题目描述:你是一个小偷,不能偷相邻房间的钱。给定一个代表每个房屋存放金额的非负整数数组,计算不被抓的情况下,能偷到的最大金额;

  • 思路:动态规划问题

  • 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])

  • 遍历顺序:顺序遍历

0213.打家劫舍Ⅱ

  • 题目描述:0198基础上,房子是一个环形的:第一个房子跟最后一个房子是挨着的。

  • 思路:分为考虑偷第一间房子和不考虑第一间房子两种情况。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mrCpxciJ-1689081778514)(E:\zhangfiles\markdownfile\代码随想录刷题小计\image\image-20230404113315966.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Glpg0XBB-1689081778515)(E:\zhangfiles\markdownfile\代码随想录刷题小计\image\image-20230404113342805.png)]

337.打家劫舍Ⅲ

  • 题目描述:偷的房间结构为一个二叉树,相邻结点不能被偷;求最大可偷金额;

  • 思路:二叉树动态规划与常规数组动态规划有所不同

  • 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};
    }
};

股票问题

121.买卖股票的最佳时机

  • 题目描述:
  • 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
  • 思路:有很多方法可以求解(贪心或者直接模拟),但是要考虑动态规划来求解一系列买卖股票问题。
  • 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

  • 遍历顺序:顺序遍历

122.买卖股票的最佳时机Ⅱ

  • 题目描述:可以多次购买股票,但是不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 思路:动态规划(或者直接求累加和)
  • 与121的不同点:本题可以多次购买;递推公式不同,当第i天持有时,如果购买当天股票,需要用前几天不持有股票的利润减去当天股票价格。
  • 递推公式:
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]);

123.买卖股票的最佳时机Ⅲ

  • 题目描述:给定一个数组,它的第 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天的策略都是取当前操作的最大值
    

188.买卖股票的最佳时机Ⅳ

  • 题目描述:与123的不同,你最多可以买卖K次。
  • 思路:与123完全相同,只不过不止5个状态,而是2k + 1个状态。
    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];
    }

309.最佳买卖股票时机含冷冻期

  • 题目描述:给定一个整数数组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;
  • 遍历顺序:顺序遍历

714.买卖股票的最佳时机含手续费

  • 题目描述:给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

    注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

  • 与122区别,在卖出股票的时候,要减掉手续费,其他的相同。

300.最长递增子序列

  • 题目描述:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
  • dp[i]:0-i中以nums[i]结尾的最长递增子序列的长度;
  • 递推公式:
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

674.最长连续递增序列

  • 题目描述:给定一个未经排序的整数数组,找到最长且 连续递增的序列,并返回该序列的长度。
  • dp[i]:0-i中以nums[i]结尾的最长连续递增序列
  • 递推公式:
if (nums[i] > nums[i - 1])//如果大于前一个整数,那么最大长度+1
	dp[i] = dp[i - 1] + 1;
else //否则,重置为1
    dp[i] = dp[i];
  • 遍历顺序:顺序遍历
  • 初始化:全初始化为1

718.最长重复子数组

  • 题目描述:给两个整数数组 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;

  • 遍历顺序:先遍历一数组或者二数组都行,因为他们的状态都是对角线方向推导而来;

1143.最长公共子序列

  • 题目描述:给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0。
  • dp[i][j]:0,i-10,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]);
  • 初始化:与718相同,dp[0][j] dp[i][0]初始化为0;
  • 遍历顺序:从左到右,从上到下;

1035.不相交的线

  • 题目描述:leetcode1035。
  • 思路:这道题可以转化为最长公共子序列问题,因为公共子序列包含了对应顺序,那么他们之间连线一定是不相交的,即最大连线数量。

53.最大子数组和

  • 题目描述:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

  • dp[i]:以nums[i]为结尾的子数组的最大和;

  • 递推公式:

dp[i] = max(dp[i - 1], dp[i - 1] + nums[i]);//延续前面和不延续两种情况
  • 初始化:dp[0] = nums[0];
  • 遍历顺序:顺序遍历;

392.判断子序列

  • 题目描述:给定字符串 st ,判断 s 是否为 t 的子序列。
  • 思路:双指针:分别指向s和t头部元素,如果相等,同时后移;反之,只移动t的指针;最后判断s的指针是否移动到末尾;
  • 动态规划:用最长公共子序列求解;

115.不同的子序列

  • 题目描述:给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
  • 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;
    
  • 遍历顺序:从上到下,从左到右

583.两个字符串的删除操作

  • 题目描述:给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

    每步 可以删除任意一个字符串中的一个字符。

  • 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;
  • 遍历顺序:从上到下,从左到右;

72.编辑距离

  • 题目描述:给你两个单词 word1word2请返回将 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;
  • 遍历顺序:从上到下,从左到右;

647.回文子串

  • 题目描述:给定一个字符串,你的任务是计算字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

  • 思路:动态规划(空间复杂度要高一点)或者双指针。

  • 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有关,所以从小到大遍历。

516.最长回文子序列

  • 题目描述:给你一个字符串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];

3.无重复字符的最长子串

  • 题目描述:给定一个字符串 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;
}
  • 初始化:全部初始化为1;
  • 遍历顺序:从头开始遍历.
  • 滑动窗口思路(其实跟动态规划思路是一样的,感觉这个题目的解法是滑动窗口)
    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;
    }

32.最长有效括号

  • 题目描述:给你一个只包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
  • 思路:动态规划
  • 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;

  • 遍历顺序:从小可以推出大,所以顺序遍历;

338.比特位计数

  • 题目描述:给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
  • 思路:一个一个求(复杂度较高),dp
//如果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

312.戳气球

  • 题目描述:有 n 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
    现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 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,其他区间可以由空区间推导而来。

  • 遍历顺序:

1688875807182

其中,红色表示已初始化,黑色表示当前需要求解的最大金币值,绿色表示当前区间推导需要的区间位置。
可以看出,每次都需要切割区间,那么大区间回切成一个个小区间去完成状态推导;比如长度为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];
}

滑动窗口

76.最小覆盖字串

  • 题目描述:给你一个字符串 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);
    

221.最大正方形

  • 题目描述:
  • 思路:暴力解法、动态规划
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;
}

单调栈系列

739.每日温度

  • 题目描述:给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
  • 思路:单调栈求解(栈里放数组下标);
  • 采用单调递增栈,先把第一个元素下标压入栈,接着循环遍历,如果当前元素比栈顶元素大,那么栈顶弹出,并更新result数组,直到当前元素小于等于栈顶元素,然后将当前元素压入栈;
  • 时间复杂度:O(n) 空间复杂度:O(n);

496.下一个更大元素Ⅰ

  • 题目描述:nums1 中数字 x下一个更大元素 是指 xnums2 中对应位置 右侧第一个x 大的元素。

    给你两个 没有重复元素 的数组 nums1nums2 ,下标从 0 开始计数,其中nums1nums2 的子集。

    对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j]下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1

    返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素

  • 思路:单调栈,与739解题思路一样,但是需要map来存每一个元素和它的下一个更大元素,最后对nums1中的每个元素都搜索map中是否有索引;

503.下一个更大元素Ⅱ

  • 题目描述:在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;
    }
    

42.接雨水

  • 题目描述:给定 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)。最后遍历结束,将所有雨水量收集起来为最终结果。

84.柱状图中最大的矩形

  • 题目描述:给定 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;
    }
    
    

85.最大矩形

  • 题目描述:给定一个仅包含 01 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
  • 思路:将矩阵分解为84题情景来分析,从上到下,一行一行来看,每一行都可以看作是一个柱状图的最大矩形子问题;

第一行高度分别为:[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;

图论

dfs(深度优先搜索)

思路:深度优先所搜算法,沿着每一个可能的路径向下进行搜索,直到不能再深入为止,并且每个节点只能访问一次;

求解迷宫问题代码(数组这种结构):


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(广度优先搜索)

思路: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] });
			}
		}
	}
}

301.删除无效括号

  • 题目描述:给你一个由若干括号和字母组成的字符串 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);
	}

}
bfs(广度优先搜索)
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;
        }
    }
}

200.岛屿数量

  • 题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
  • 思路:DFS

207.课程表

  • 题目描述:你这个学期必须选修 numCourses 门课程,记为 0numCourses - 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;
        }
    

437.路经总和Ⅲ

  • 题目描述:给定一个二叉树的根节点 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;
        }
    }
}

200.岛屿数量

  • 题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
  • 思路:DFS

207.课程表

  • 题目描述:你这个学期必须选修 numCourses 门课程,记为 0numCourses - 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;
        }
    

437.路经总和Ⅲ

  • 题目描述:给定一个二叉树的根节点 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);
        }
    

你可能感兴趣的:(leetcode)