剑指 Offer 刷题笔记 1

目录

剑指 Offer

第 1 天 栈与队列(简单)

剑指 Offer 09. 用两个栈实现队列

这道题我觉得思路主要是要去试,在试的时候会发现,只有把相应栈完全颠倒才能顺利出队入队

#include <stack>
using namespace std;

class CQueue {
private:
    stack<int> s1;
    stack<int> s2;

public:
    CQueue() {

    }

    // 入 1 2 3 到 s1
    // s1: [1 2 3] s2: []
    // 要出 1 那么最基本的 s1 要将所有元素倒到 s2
    // s2: [3 2 1] s1: []
    // 这时 s2 再出栈就是 1
    // 现在再要入 4
    // 如果要直接入到 s2 或者 s1 肯定是错的,不管怎么样都会得到 s2 [3 2 4]
    // 因此必须要把 s2 所有元素倒到 s1
    // s1: [2 3] s2: [] 
    // 再入 4 到 s1
    // s1: [2 3 4] s2: []
    void appendTail(int value) {
        int tmp;
        while(!s2.empty()){
            tmp = s2.top();
            s2.pop();
            s1.push(tmp);
        }
        s1.push(value);
    }
    
    int deleteHead() {
        int tmp;
        while(!s1.empty()){
            tmp = s1.top();
            s1.pop();
            s2.push(tmp);
        }
        if(!s2.empty())
        {   
            int head = s2.top();
            s2.pop();
            return head;
        }
        else
        {
            return -1;
        }
    }
};

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue* obj = new CQueue();
 * obj->appendTail(value);
 * int param_2 = obj->deleteHead();
 */

剑指 Offer 30. 包含min函数的栈

算法运行的每一步保存一次状态

这道题我觉得重要的思想在于,在算法运行的过程中可以保存一个状态,这个状态可以指最小值最大值等等

比如题目换成 包含 min 函数的队列或者等等,都是一样的

然后优化的思路就是只在状态改变的时候才记录这个状态

易错点在于,注意在栈为空时是要保证状态判断的正确性,因为"当前最小值"的更新分成两个部分,一开始是设了 INT_MAX,方便得到一个数就立即设为最小值;而第二部分就是"当前最小值"由“最小值堆栈”进行更新,那么在“最小值堆栈”为空的时候,就要注意"当前最小值"要变为 INT_MAX

using namespace std;
#include <stack>

class MinStack {
private: 
    stack<int> stk;
    stack<int> min_stk;
    int cur_min = INT_MAX;

public:
    /** initialize your data structure here. */
    MinStack() {

    }
    
    // 最开始我以为要保存一个列表来存所有元素
    // 然后我还以为每一次存元素都要对这个列表进行排序
    // 然后看题解才知道他这个辅助栈的思想
    // 我感觉这个根本思想是保存一个算法的状态
    // 例如“当前的最小值”就是一个属性

    // 然后之后可以优化的地方就是
    // 只在当前最小值改变的时候才将当前最小值入栈到 min_stk
    // 只在最小值出栈的时候才将 min_stk 出栈
    // 这样可以保证 min_stk 使用的空间变少

    void push(int x) {
        stk.push(x);
        if(cur_min > x)
            cur_min = x;
        min_stk.push(cur_min);
    }
    
    void pop() {
        if(!stk.empty())
        {
            stk.pop();
            min_stk.pop();
            // 出栈时需要更新最小值
            if(!min_stk.empty()){
                cur_min = min_stk.top();
            }
            // 易错点:min_stk 中没有元素了之后,cur_min 还是历史的最后一个最小值
            // 要记得把 cur_min 恢复为最大值
            else
            {
                cur_min = INT_MAX;
            }
        }
    }
    
    int top() {
        return stk.top();
    }
    
    int min() {
        return min_stk.top();
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj->push(x);
 * obj->pop();
 * int param_3 = obj->top();
 * int param_4 = obj->min();
 */

第 2 天 链表(简单)

剑指 Offer 06. 从尾到头打印链表

直接使用栈就行了

剑指 Offer 24. 反转链表

一开始我是把三个指针看成一个整体,但是我没想到是每一次只看一个 curr 指针这种

其实每次只看 curr 就好了,看三个指针反而有各种情况把……

还有要注意的点就是,反转之前的头结点是反转之后的尾结点,反转之后的尾结点的 next = NULL

有的时候就没有注意这个反转之后的尾结点的 next 还要改,不改的话就会在这里形成环

就比如原来是 1 -> 2 -> 3 然后反转之后的尾结点的 next 不改的话就会变成 1 <-> 2 <- 3

我的三个指针的版本

#include <iostream>
using namespace std;

struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* p1, * p2, * p3;
        p1 = head;

        // 如果输入的链表为空
        if (p1 == NULL)
            return NULL;

        // 如果输入的链表只有一个结点
        if (p1->next == NULL)
            return p1;

        p2 = p1->next;
        
        // 清空反转链表的尾结点的 next
        p1->next = NULL;

        // 如果输入的链表只有两个结点
        if (p2->next == NULL)
        {
            p2->next = p1;
            return p2;
        }

        p3 = p2->next;

        // p1 和 p2 对换
        // p2 原本的 next 是 p3 现在指向了 p1 但是原本的 next 保存在 p3
        p2->next = p1;

        // 如果 p3 移动不了
        while (p3->next != NULL) {

            // 向后移动
            p1 = p2;
            p2 = p3;
            p3 = p3->next;

            // p1 和 p2 对换
            // p2 原本的 next 是 p3 现在指向了 p1 但是原本的 next 保存在 p3
            p2->next = p1;
        }

        p3->next = p2;
        return p3;
    }
};

int main() {
    ListNode* head = new ListNode(1);
    ListNode* p1 = new ListNode(2);
    ListNode* p2 = new ListNode(3);
    ListNode* p3 = new ListNode(4);
    
    head->next = p1;
    p1->next = p2;
    p2->next = p3;

    Solution *sol = new Solution();

    ListNode* tmp = sol->reverseList(head);
    while (tmp != NULL) {
        cout << tmp->val << endl;
        tmp = tmp->next;
    }
}

剑指 Offer 35. 复杂链表的复制

这个核心在于 random 的复制,不是直接将原链表的 random 给到新链表对应结点的 random,因为新链表中的 random 要指向新链表中相应位置的结点,而原链表的 random 指向的是原链表中的结点

所以要想一个办法保存 random 所代表的位置关系

我这里就使用了一个 unordered_map 键值对是 <原链表第 i 个结点的指针, 新链表第 i 个结点的指针>

然后我使用 ptr_map[p1->random] 就可以得到新链表中第 i 个结点的 random 成员所指向的新链表中的第 random 个结点

class Solution {
public:
    Node* copyRandomList(Node* head) {
        Node* p1 = head, * p2 = head;

        Node* new_head = nullptr;
        Node* curr = nullptr;
        Node* prev = nullptr;

        bool isfirst = true;

        unordered_map<Node*, Node*> ptr_map;

        while (p1 != nullptr) {
            curr = new Node(p1->val);
            ptr_map.insert(pair<Node*, Node*>(p1, curr));

            if (isfirst) {
                new_head = curr;
                isfirst = false;
            }
            else
            {
                prev->next = curr;
            }
            
            p1 = p1->next;
            prev = curr;
        }

        p1 = head;
        p2 = new_head;
        while (p1 != nullptr) {
            if (p1->random == nullptr)
                p2->random = nullptr;
            else
                p2->random = ptr_map[p1->random];
            p1 = p1->next;
            p2 = p2->next;
        }

        return new_head;
    }
};

题解不需要哈希表的思路很强……

主要是他在原链表上做插入,这是我想不到的,因为我想着不动原链表的话,我就觉得什么都不要动

没想到他是先插空进去,就是 A -> B 变成 A -> A' -> B -> B',然后再提取出来 A' -> B',最后原链表不变,就很强

然后由于他是插空的,就相当于已经有了一个位置规律在这里了,那么其实 A -> A' -> B -> B' 本身就包含了一个 AA' 相对应的位置,这个用 map 来保存 AA' 的对应关系是一样的

总之就是要找到一个原结点和拷贝节点的关系,用 map 保存这个关系最直观,但是也可以直接在原链表上保存这个关系

第 3 天 字符串(简单)

剑指 Offer 05. 替换空格

确实是简单

class Solution {
public:
    string replaceSpace(string s) {
        string tmp = "";
        for(int i = 0;i < s.length();i++){
            if(s[i] != ' '){
                tmp.append(s,i,1);
            }
            else{
                tmp.append("%20");
            }
        }
        return tmp;
    }
};

主要在于熟悉一下 string 的基本操作

append 方法要不就用 string& append (size_t n, char c); 要不就用 string& append (const string& str, size_t subpos, size_t sublen);

剑指 Offer 58 - II. 左旋转字符串

string& append (const string& str, size_t subpos, size_t sublen); 很简单

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        string tmp = "";
        tmp.append(s, n, s.length()-n);
        tmp.append(s, 0, n);
        return tmp;
    }
};

但是我看到评论取才发现,原来建议是,想要让题目有意义,就不要使用辅助空间

所以原来大家都是用反转来做的

class Solution {
public:
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(), s.begin() + n);
        reverse(s.begin() + n, s.end());
        reverse(s.begin(), s.end());
        return s;
    }
};

他这个就让人想起线性代数的知识

原来是 [A, B],现在需要 [B, A],那就先对元素转置,得到 [A^T, B^T],然后对整体转置得到 [A^T, B^T]^T = [B, A]

也让人熟悉了一下迭代器的操作

然后也让人回忆起,begin() 是起点,end() 是最后一个元素的下一个元素

所以根据 reverse(s.begin(), s.end()); 可以反转整个 string 可以发现 reverse(first, last) 是只反转第 first 个元素到第 last-1 个元素,也就是左闭右开 [first, last)

这让人不禁会想,如果还有类似的使用两个迭代器表示范围的函数,会不会也是左闭右开

但是我暂时还没搜到类似的函数……?

一般的介绍都是讲了一下迭代器可能会失效,去看了一个介绍

https://www.geeksforgeeks.org/iterators-c-stl/

  1. begin() :- This function is used to return the beginning position of the container.

  2. end() :- This function is used to return the after end position of the container.

  3. advance() :- This function is used to increment the iterator position till the specified number mentioned in its arguments.

Example:

vector<int> ar = { 1, 2, 3, 4, 5 };
      
// Declaring iterator to a vector
vector<int>::iterator ptr = ar.begin();
  
// Using advance() to increment iterator position
// points to 4
advance(ptr, 3);
  1. next() :- This function returns the new iterator that the iterator would point after advancing the positions mentioned in its arguments.

  2. prev() :- This function returns the new iterator that the iterator would point after decrementing the positions mentioned in its arguments.

Example:

vector<int> ar = { 1, 2, 3, 4, 5 };
  
// Declaring iterators to a vector
vector<int>::iterator ptr = ar.begin();
auto it = next(ptr, 3);
  1. inserter() :- This function is used to insert the elements at any position in the container. It accepts 2 arguments, the container and iterator to position where the elements have to be inserted.

Example:

// C++ code to demonstrate the working of
// inserter()
#include<iostream>
#include<iterator> // for iterators
#include<vector> // for vectors
using namespace std;
int main()
{
	vector<int> ar = { 1, 2, 3, 4, 5 };
	vector<int> ar1 = {10, 20, 30};
	
	// Declaring iterator to a vector
	vector<int>::iterator ptr = ar.begin();
	
	// Using advance to set position
	advance(ptr, 3);
	
	// copying 1 vector elements in other using inserter()
	// inserts ar1 after 3rd position in ar
	copy(ar1.begin(), ar1.end(), inserter(ar,ptr));
	
	// Displaying new vector elements
	cout << "The new vector after inserting elements is : ";
	for (int &x : ar)
		cout << x << " ";
	
	return 0;	
}

Output:

The new vector after inserting elements is : 1 2 3 10 20 30 4 5

第 4 天 查找算法(简单)

剑指 Offer 03. 数组中重复的数字

从 C 的直觉来说就是用一个数组作为标记

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int* tmp = (int*)calloc(nums.size(), sizeof(int));
        for (int i = 0; i < nums.size(); i++) {
            if (tmp[nums[i]] != 0) {
                return nums[i];
            }
            else {
                tmp[nums[i]] = 1;
            }
        }
        return -1;
    }
};

从 C++ 的直觉是使用 set

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        unordered_set<int> mySet;
        for (int i = 0; i < nums.size(); i++) {
            if (mySet.find(nums[i]) == mySet.end())
            {
                mySet.insert(nums[i]);
            }
            else
            {
                return nums[i];
            }
        }
        return -1;
    }
};
原地交换 把 x 交换到 arr[x] 下一次取 arr[x] 一定等于 x

神奇的是原地交换方法

题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。 此说明含义:数组元素的 索引一对多 的关系。
因此,可遍历数组并通过交换操作,使元素的 索引 一一对应(即 nums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。

遍历中,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x]=x,此时即可得到一组重复数字。

作者:jyd
链接:https://leetcode.cn/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/solution/mian-shi-ti-03-shu-zu-zhong-zhong-fu-de-shu-zi-yua/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 53 - I. 在排序数组中查找数字 I

剑指 Offer 53 - II. 0~n-1中缺失的数字

二分法的要点在于对中点的情况进行判断

排序数组中的搜索问题,首先想到 二分法 解决

主要是要用到原数组的已经排好序的这个规律

而二分法要点在于对中点的情况做判断

比如查找 0~n-1中缺失的数字 这道题就是对中点判断

如果 middle == array[middle] 说明 [left, middle] 是正确的,那么缺失的数据会在 [middle, right] 反之亦然

又比如 在排序数组中查找数字 I 要查找出现次数的数字最终肯定落在一个递增区间内

class Solution {
public:
    int search(vector<int>& nums, int target) {
        return findCount(nums, 0, nums.size()-1, target);
    }

    int findCount(vector<int>& nums, int left, int right, int target){
        if(right < left)
            return 0;
        if(left < 0 || right >= nums.size())
            return 0;
            
        int middle = (left + right)/2;
        
        if(target < nums[middle]){
            return findCount(nums, left, middle-1, target);
        }
        else if(target > nums[middle]){
            return findCount(nums, middle+1, right, target);
        }
        else{
            return findCount(nums, left, middle-1, target) + findCount(nums, middle+1, right, target) + 1;
        }
    }
};

对于中点判断,如果 target < nums[middle] 说明要找的数字在左区间,反之在右区间

target == nums[middle] 时说明要找的数字在两边都有,所以要两边的数量加起来,再加上中间这一个已经知道的 + 1

然后还要考虑失效的边界条件,在失效的时候应该返回一个对结果没有影响的值,所以这里是返回 0

第 5 天 查找算法(中等)

剑指 Offer 04. 二维数组中的查找

本来我是想到对角线上的元素是左上角与该对角线元素所组成的子矩阵中的最大值

只考虑方阵的话,我想先找对角元素,如果 target 的值在两个对角元素 a1 a2 之间,那么 target 一定在 a2 所在的行或列上

如果 target 的值等于某个对角元素,那就简单了,直接返回 true

然后问题在于题目给的不是方阵,那么我就只能先取一个最大的子矩阵方阵,然后在这个方阵中如果没找到,再在剩下的部分取方阵递归下去……

感觉就很累,主要是找方阵这里会有一个偏移量的问题

规律性问题转化为固定方向走路问题

然后去看了题解,他是从左下角找到右上角。从左下角开始,可以走 row + col 步

因为是往右上角走,所以规定只能往右或者往上走,边界条件是不能走出这个矩阵

然后因为题目给的从左到右递增,从上到下递增,所以

  1. 如果当前的值小于 target,说明下一步应该要减小,所以应该往上走

  2. 如果当前的值大于 target,说明下一步应该要增大,所以应该往右走

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        int row = matrix.size();
        if(row == 0) return false;
        int col = matrix[0].size();

        int x = row - 1;
        int y = 0;

        for(int step = 0; step < row + col; step++)
        {   
            if(x < 0) return false;
            if(y >= col) return false;

            if(matrix[x][y] == target)
                return true;
            else
            {
                if(matrix[x][y] > target) x--;
                else y++;
            }
        }

        return false;
    }
};

他之所以会选择左下角到右上角,是因为他这样就可以有两种不同的路径,一个向右增大,一个向上减少

然后这两种路径都可以保证在身后的,自己不可能后退回到的地方一定是自己不需要再走的,也就是说一定不包含目标值的

证明:如果当前元素大于目标值,说明当前元素的下边的所有元素都一定大于目标值,因此往下查找不可能找到目标值,往左查找可能找到目标值。如果当前元素小于目标值,说明当前元素的左边的所有元素都一定小于目标值,因此往左查找不可能找到目标值,往下查找可能找到目标值。

作者:Nehzil
链接:https://leetcode.cn/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/solution/jian-zhi-offer-04-er-wei-shu-zu-zhong-de-qu56/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 11. 旋转数组的最小数字

我一开始还不知道二分法是有非递归的模板

看了一下感觉很舒服……主要是把 array 为空边界条件

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0, right = numbers.size();
        int middle = 0;
        while(left < right){
            middle = left + (right - left)/2;
            if(){

            }
            else{
                
            }
        }
    }
    
};

一开始我写的是

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0, right = numbers.size()-1;
        int middle = 0;
        // 一开始肯定是要放弃正常递增的区间
        // 但是找到后面之后,可能两个子区间都是正常递增的
        // 所以应该再加一个判断,直接用 left 和 right 来判断这一整个区间是正常的还是不正常的
        while(left < right){
            // 如果整个区间已经是正常递增了,那么就认为区间第一个就是最小的
            if(numbers[left] < numbers[right])
                return numbers[left];
                
            middle = left + (right - left)/2;
            // 这里是实际写了一下之后才确定更新的 left 是等于 middle 还是等于 middle + 1 更好一点,right 也是同理
            if(numbers[left] <= numbers[middle]){
                left = middle + 1;
            }
            else{
                right = middle;
            }
        }

        // 若 vector 为空,那么 .size() 也可以调用,会返回 0
        // 因此若 vector 为空,那么直接跳过了 while,那么 left = right = 0
        // 而这个时候不能取下标为 0 的元素,所以还要判断一下 left 是否小于数组的长度
        if(left < numbers.size())
            return numbers[left];
        else
            return 0;
    }
    
};

我一开始这样写是考虑到 [3, 4, 5, 5, 5, 1, 2, 2, 2] 和 [3, 4, 5, 5, 1, 2, 2, 2, 2]

但是之后我就错了

输入:

[10,1,10,10,10]

输出:

10

预期结果:

1

主要是 numbers[left] == numbers[middle] 时,最小值既可以出现在左子区间也可以出现在右子区间,所以这里还真的可能同时找两个子区间的最小值,用递归方便一点

#include <algorithm>
#include <vector>
using namespace std;

class Solution {
public:
    int minArray(vector<int>& numbers) {
        if(numbers.size() == 0)
            return 0;
        else
            return findMin(numbers, 0, numbers.size()-1);
    }

    int findMin(vector<int>& numbers, int left, int right){
        // 边界条件
        if(left == right)
            return numbers[left];

        // 如果整个区间已经是正常递增了,那么就认为区间第一个就是最小的
        if(numbers[left] < numbers[right])
            return numbers[left];

        int middle = left + (right - left)/2;

        if(numbers[left] < numbers[middle]){
            return findMin(numbers, middle + 1, right);
        }
        else if(numbers[left] > numbers[middle]){
            return findMin(numbers, left, middle);
        }
        else{
            int tmp1 = findMin(numbers, left, middle);
            int tmp2 = findMin(numbers, middle + 1, right);
            return min(tmp1, tmp2);
        }
    }

};

剑指 Offer 50. 第一个只出现一次的字符

这里我用了一个自定义的标记,感觉还是可以的

其实这种自定义的标记就跟哈希表是一样的

#include<vector>

class Solution {
public:
    char firstUniqChar(string s) {
        vector<int> dict(26, 0);
        int seq = 0;
        // 100 开头表示已经出现过一次,100 表示第一个出现过一次,101 表示第二个出现过一次
        // 200 表示已经出现过第二次了
        for (int i = 0; i < s.size(); i++) {
            char tmp = s[i];
            int idx = tmp - 'a';
            if (dict[idx] == 0) {
                dict[idx] = 100 + seq;
                seq++;
            }
            else if (dict[idx] / 100 == 1) {
                dict[idx] = 200;
            }
        }

        int tmp = 27;
        int firstChar = 'a';
        int flag = false;
        for (int i = 0; i < 26; i++) {
            if (dict[i] / 100 == 1)
            {
                flag = true;
                if (tmp > dict[i] - 100)
                {
                    tmp = dict[i] - 100;
                    firstChar = 'a' + i;
                }
            }
        }

        if (!flag)
            return ' ';
        else
            return firstChar;
    }
};

第 6 天 搜索与回溯算法(简单)

剑指 Offer 32 - I. 从上到下打印二叉树

简单的层序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        vector<int> result;
        queue<TreeNode*> nodeQueue;
        
        nodeQueue.push(root);

        TreeNode *tmpNode;

        while(nodeQueue.size() != 0){
            tmpNode = nodeQueue.front();
            nodeQueue.pop();
            
            if(tmpNode == NULL)
                continue;
                
            result.push_back(tmpNode->val);

            if(tmpNode->left != NULL){
                nodeQueue.push(tmpNode->left);
            }

            if(tmpNode->right != NULL){
                nodeQueue.push(tmpNode->right);
            }
        }

        return result;
    }
};

剑指 Offer 32 - II. 从上到下打印二叉树 II

简单的层序遍历 + 保存深度的队列

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        queue<TreeNode*> nodeQueue;
        queue<int> levelQueue;

        nodeQueue.push(root);
        levelQueue.push(0);

        TreeNode *tmpNode;
        int tmpLevel;

        while(nodeQueue.size() != 0){
            tmpNode = nodeQueue.front();
            tmpLevel = levelQueue.front();
            nodeQueue.pop();
            levelQueue.pop();

            if(tmpNode == NULL)
                continue;
                
            if(result.size() <= tmpLevel){
                result.push_back(vector<int>());
            }

            result[tmpLevel].push_back(tmpNode->val);
            
            if(tmpNode->left != NULL){
                nodeQueue.push(tmpNode->left);
                levelQueue.push(tmpLevel+1);
            }

            if(tmpNode->right != NULL){
                nodeQueue.push(tmpNode->right);
                levelQueue.push(tmpLevel+1);
            }
        }

        return result;
    }
};

之后看到别人的版本,我看大家都对这个 for(int i=q.size();i>0;i--) 表示很惊讶……

确实,首先先拿 q.size() 就相当于拿到了当前这一层的节点的个数

然后在 for 循环中入队的话,会改变 q.size(),使得它的意义已经不再是当前这一层的节点的个数了

但是由于你是在初始的时候就取了,而不是 for(int i=0;i<q.size();i++) 这种在每一步都判断一次的,所以不会有错

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> q;
        vector<vector<int> > ans;
        if(root==NULL){
            return ans;
        }
        q.push(root);
        while(!q.empty()){
            vector<int> temp;
            for(int i=q.size();i>0;i--){
                TreeNode* node = q.front();
                q.pop();
                temp.push_back(node->val);
                if(node->left!=NULL) q.push(node->left);
                if(node->right!=NULL) q.push(node->right);
            }
            ans.push_back(temp);
        }

        return ans;
    }
};

我主要是从来没想到要确定每一层的节点个数

或者说他这个就相当于提供了一个计算每一层节点个数的办法:就是在 while(!q.empty()) 中每次从队列取出节点的时候使用 for(int i=q.size();i>0;i--),那么在 for 开始的时候,一定是进入下一层的时候,在 for 结束的时候,一定是将这一层的所有结点都入队的时候

一开始我想着奇数层和偶数层是不一样的,所以我就想着在奇数层用队列来存,在偶数层用堆栈来存

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        queue<TreeNode*> nodeQueue;
        stack<TreeNode*> nodeStack;
        int level = 0;

        nodeQueue.push(root);

        while(nodeQueue.size() != 0 || nodeStack.size() != 0){
            if(level % 2 == 1){
                // nodeStack.size() 表示第 level 层的节点的个数
                vector<int> tmpVector;
                TreeNode *tmpNode;

                for(int i = nodeStack.size(); i > 0; i--){
                    tmpNode = nodeStack.top();
                    nodeStack.pop();

                    if(tmpNode == NULL)
                        continue;

                    tmpVector.push_back(tmpNode->val);

                    if(tmpNode->left != NULL)
                        nodeQueue.push(tmpNode->left);

                    if(tmpNode->right != NULL)
                        nodeQueue.push(tmpNode->right);
                }
                result.push_back(tmpVector);
                level++;
            }
            else{
                // nodeQueue.size() 表示第 level 层的节点的个数
                vector<int> tmpVector;
                TreeNode *tmpNode;
                
                for(int i = nodeQueue.size(); i > 0; i--){
                    tmpNode = nodeQueue.front();
                    nodeQueue.pop();

                    if(tmpNode == NULL)
                        continue;
                        
                    tmpVector.push_back(tmpNode->val);

                    if(tmpNode->left != NULL)
                        nodeStack.push(tmpNode->left);

                    if(tmpNode->right != NULL)
                        nodeStack.push(tmpNode->right);
                }
                result.push_back(tmpVector);
                level++;
            }
        }

        return result;
    }
};

结果发现出错了,因为用堆栈出栈的顺序本身就是逆序,所以再进入队列的时候仍然是逆序,这是之前我想当然了

于是我就想只用堆栈的话,那应该就会顺序进去,逆序出来,逆序进去,顺序出来了

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        stack<TreeNode*> nodeStack;

        nodeStack.push(root);

        while(nodeStack.size() != 0){
            vector<int> tmpVector;
            TreeNode *tmpNode;

            // nodeStack.size() 表示第 level 层的节点的个数
            for(int i = nodeStack.size(); i > 0; i--){
                tmpNode = nodeStack.top();
                nodeStack.pop();

                if(tmpNode == NULL)
                    continue;

                tmpVector.push_back(tmpNode->val);

                if(tmpNode->left != NULL)
                    nodeStack.push(tmpNode->left);

                if(tmpNode->right != NULL)
                    nodeStack.push(tmpNode->right);
            }

            result.push_back(tmpVector);
        }

        return result;
    }
};

然后又是解答错误了

输入:

[1,2,3,4,null,null,5]

输出:

[[1],[3,2],[5,4]]

预期结果:

[[1],[3,2],[4,5]]

主要是因为层序遍历某一层时,如果一边遍历一边入栈的话,那么在从堆栈中取的时候可能会取到刚刚加入的节点,这样就乱了

于是我就想着用两个堆栈来实现

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        stack<TreeNode*> nodeStack1;
        stack<TreeNode*> nodeStack2;
        stack<TreeNode*> *stackPtr1, *stackPtr2;
        int level = 0;

        nodeStack1.push(root);

        while(nodeStack1.size() != 0 || nodeStack2.size() != 0){
            vector<int> tmpVector;
            TreeNode *tmpNode;

            if(level % 2 == 0)
            {
                stackPtr1 = &nodeStack1;
                stackPtr2 = &nodeStack2;
            }
            else
            {
                stackPtr1 = &nodeStack2;
                stackPtr2 = &nodeStack1;
            }

            // nodeStack.size() 表示第 level 层的节点的个数
            for(int i = stackPtr1->size(); i > 0; i--){
                tmpNode = stackPtr1->top();
                stackPtr1->pop();

                if(tmpNode == NULL)
                    continue;

                tmpVector.push_back(tmpNode->val);

                if(tmpNode->left != NULL)
                    stackPtr2->push(tmpNode->left);

                if(tmpNode->right != NULL)
                    stackPtr2->push(tmpNode->right);
            }

            result.push_back(tmpVector);
            level++;
        }

        return result;
    }
};

但是这样又会错,因为同一个节点的两个子节点之间的相互顺序乱了

输入

[3,9,20,null,null,15,7]

输出

[[3],[20,9],[7,15]]

预期结果

[[3],[20,9],[15,7]]

如果在入栈的顺序颠倒一点,其实也没有用

if(tmpNode->right != NULL)
	stackPtr2->push(tmpNode->right);
if(tmpNode->left != NULL)
	stackPtr2->push(tmpNode->left);
输入

[3,9,20,null,null,15,7]

输出

[[3],[9,20],[15,7]]

预期结果

[[3],[20,9],[15,7]]

最后还是用双端队列做了,只能说这告诉我队列和栈之间组合不一定是能组合出双端队列的效果,毕竟怎么组合结合具体问题也是有很多细节的hhh

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        deque<TreeNode*> nodeDeque;
        int level = 0;

        if(root == NULL)
            return result;
            
        nodeDeque.push_back(root);

        while(nodeDeque.size() != 0){
            vector<int> tmpVector;
            TreeNode *tmpNode;

            // nodeDeque.size() 表示第 level 层的节点的个数
            for(int i = nodeDeque.size(); i > 0; i--){
                if(level % 2 == 0){
                    tmpNode = nodeDeque.front();
                    nodeDeque.pop_front();
                }
                else
                {
                    tmpNode = nodeDeque.back();
                    nodeDeque.pop_back();
                }

                if(tmpNode == NULL)
                    continue;

                tmpVector.push_back(tmpNode->val);

                if(level % 2 == 0){
                    if(tmpNode->left != NULL)
                        nodeDeque.push_back(tmpNode->left);
                    if(tmpNode->right != NULL)
                        nodeDeque.push_back(tmpNode->right);
                }
                else{
                    if(tmpNode->right != NULL)
                        nodeDeque.push_front(tmpNode->right);
                    if(tmpNode->left != NULL)
                        nodeDeque.push_front(tmpNode->left);
                }
            }

            result.push_back(tmpVector);
            level++;
        }

        return result;
    }
};

看了别人的题解,其实只用队列的话,只对最后插入的临时 vector 在层数为奇数的时候做转置就好了

确实更简单了

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        queue<TreeNode*> nodeQueue;
        
        int level = 0;

        if(root == NULL)
            return result;

        nodeQueue.push(root);

        TreeNode *tmpNode;

        while(nodeQueue.size() != 0){
            vector<int> tmpVector;
            TreeNode *tmpNode;

            // nodeDeque.size() 表示第 level 层的节点的个数
            for(int i = nodeQueue.size(); i > 0; i--){
                tmpNode = nodeQueue.front();
                nodeQueue.pop();

                if(tmpNode == NULL)
                    continue;

                tmpVector.push_back(tmpNode->val);

                if(tmpNode->left != NULL)
                    nodeQueue.push(tmpNode->left);
                if(tmpNode->right != NULL)
                    nodeQueue.push(tmpNode->right);
            }

            if(level % 2 == 1)
                reverse(tmpVector.begin(), tmpVector.end());

            result.push_back(tmpVector);
            level++;
        }

        return result;
    }
};

第 7 天 搜索与回溯算法(简单)

剑指 Offer 26. 树的子结构

一开始我的写法是要求 A B 每个节点的孩子指针要不都指向相同结点,要不就都指向空,否则就判断 B 不是 A 的子结构

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(B == NULL)
            return false;

        stack<TreeNode*> nodeStack;
        
        nodeStack.push(A);

        TreeNode* tmp = NULL;

        while(nodeStack.size() != 0){
            tmp = nodeStack.top();
            nodeStack.pop();

            if(tmp == NULL)
                continue;

            if(tmp->val == B->val){
                if(isSameStructure(tmp, B))
                    return true;
            }

            if(tmp->right != NULL)
                nodeStack.push(tmp->right);

            if(tmp->left != NULL)
                nodeStack.push(tmp->left);
        }

        return false;
    }

    bool isSameStructure(TreeNode* root1, TreeNode* root2){
        if(root2 == NULL)
            return false;

        stack<TreeNode*> nodeStack1, nodeStack2;

        nodeStack1.push(root1);
        nodeStack2.push(root2);

        TreeNode *tmp1 = NULL, *tmp2 = NULL;

        while(nodeStack1.size() != 0 && nodeStack2.size() != 0){
            tmp1 = nodeStack1.top();
            nodeStack1.pop();
            tmp2 = nodeStack2.top();
            nodeStack2.pop();

            if(tmp1 == NULL && tmp2 == NULL)
                continue;
            else if(tmp1 == NULL || tmp2 == NULL)
                return false;
            
            if(tmp1->val != tmp2->val){
                return false;
            }

            if(tmp1->right != NULL && tmp2->right != NULL){
                nodeStack1.push(tmp1->right);
                nodeStack2.push(tmp2->right);
            }
            else if(tmp1->right == NULL && tmp2->right == NULL){
                ;
            }
            else{
                return false;
            }

            if(tmp1->left != NULL && tmp2->left != NULL){
                nodeStack1.push(tmp1->left);
                nodeStack2.push(tmp2->left);
            }
            else if(tmp1->left == NULL && tmp2->left == NULL){
                ;
            }
            else{
                return false;
            }
        }

        if(nodeStack1.size() != 0 || nodeStack2.size() != 0)
            return false;
        else
            return true;
    }
};

但是这样是不对的,A 可能比 B 多一些分支

输入:

[10,12,6,8,3,11]
[10,12,6,8]

输出:

false

预期结果:

true

如果你单纯只考虑,对于 A B 中对应结点,如果左(右)孩子都非空,那么要求相等,其他情况不考虑的话,那么就会忽略一些情况,比如 B 中有的分支 A 中没有,也被判为 B 是 A 的子结构

            if(tmp1->right != NULL && tmp2->right != NULL){
                nodeStack1.push(tmp1->right);
                nodeStack2.push(tmp2->right);
            }

            if(tmp1->left != NULL && tmp2->left != NULL){
                nodeStack1.push(tmp1->left);
                nodeStack2.push(tmp2->left);
            }

所以解决方案是以 B 中的分支为准,B 中有的分支 A 中必须有,B 中没有的分支就不必要再看 A,所以最终代码为

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(B == NULL)
            return false;

        stack<TreeNode*> nodeStack;
        
        nodeStack.push(A);

        TreeNode* tmp = NULL;

        while(nodeStack.size() != 0){
            tmp = nodeStack.top();
            nodeStack.pop();

            if(tmp == NULL)
                continue;

            if(tmp->val == B->val){
                if(isSameStructure(tmp, B))
                    return true;
            }

            if(tmp->right != NULL)
                nodeStack.push(tmp->right);

            if(tmp->left != NULL)
                nodeStack.push(tmp->left);
        }

        return false;
    }

    bool isSameStructure(TreeNode* root1, TreeNode* root2){
        if(root2 == NULL)
            return false;

        stack<TreeNode*> nodeStack1, nodeStack2;

        nodeStack1.push(root1);
        nodeStack2.push(root2);

        TreeNode *tmp1 = NULL, *tmp2 = NULL;

        while(nodeStack1.size() != 0 && nodeStack2.size() != 0){
            tmp1 = nodeStack1.top();
            nodeStack1.pop();
            tmp2 = nodeStack2.top();
            nodeStack2.pop();

            if(tmp1 == NULL && tmp2 == NULL)
                continue;
            else if(tmp1 == NULL || tmp2 == NULL)
                return false;
            
            if(tmp1->val != tmp2->val){
                return false;
            }

            if(tmp2->right != NULL){
                nodeStack1.push(tmp1->right);
                nodeStack2.push(tmp2->right);
            }

            if(tmp2->left != NULL){
                nodeStack1.push(tmp1->left);
                nodeStack2.push(tmp2->left);
            }
        }

        if(nodeStack1.size() != 0 || nodeStack2.size() != 0)
            return false;
        else
            return true;
    }
};

但是看到别人的递归就很简单……

class Solution {
public:
        //输入两棵二叉树,判断B是不是A的子结构
        //思路:DFS,即先序遍历,然后判断A的每个节点都作为根节点是否可以跟B相匹配
        //匹配成功则返回true,匹配到空节点都还未成功则返回false
    
    //树A是否是树B的子结构
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(B == NULL || A == NULL){
            return false;
        }
        //遍历A中的每一个节点,若是包含B则返回true
        return isContain(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
    }

    //以A为根节点的树是否包含B
    bool isContain(TreeNode* A, TreeNode* B){
        if(B == NULL) return true;
        if(A == NULL || A->val != B->val) return false;
        return isContain(A->left, B->left) && isContain(A->right, B->right);
    }
};

//作者:you - tou - nao - he - hen - gao - xing - vv
//链接:https ://leetcode.cn/problems/shu-de-zi-jie-gou-lcof/solution/cshi-xian-xian-xu-bian-li-pan-duan-yi-aw-xd96/
//来源:力扣(LeetCode)
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

树的递归就是认为某个结点的子结点就是一颗子树

某个树是否具有某个性质 = 根节点是否具有某个性质 + 根节点的子树是否具有某个性质

剑指 Offer 27. 二叉树的镜像

经过上面一题就很简单了

对某个树进行一个操作 = 对根节点进行操作 + 对根节点的子树进行操作

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* mirrorTree(TreeNode* root) {
        if(root == NULL)
            return NULL;
        
        TreeNode* tmp;

        tmp = root->left;
        root->left = root->right;
        root->right = tmp;

        mirrorTree(root->left);
        mirrorTree(root->right);

        return root;
    }
};

剑指 Offer 28. 对称的二叉树

一开始用了笨方法,直接复制了一份镜像的二叉树来和原先的树做对比

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        TreeNode* new_root = getSymmetric(root);
        return isSame(root, new_root);
    }

    bool isSame(TreeNode* A, TreeNode* B)
    {
        if(A == NULL && B == NULL)
            return true;
        else if(A == NULL || B == NULL)
            return false;
        
        if(A->val != B->val)
            return false;
        
        return isSame(A->left, B->left) && isSame(A->right, B->right);
    }

    TreeNode* getSymmetric(TreeNode* root){
        if(root == NULL)
            return NULL;

        TreeNode* new_root = new TreeNode(root->val);

        new_root->left = getSymmetric(root->right);
        new_root->right = getSymmetric(root->left);

        return new_root;
    }
};

之后想着用递归,但是感觉算法有点复杂

其实我一开始想的是,递归函数输入一个树根,然后对于这个树根判断他的两个孩子,然后对它的两个孩子进行递归

然后就可以发现,对于这个原树与镜像树是否重合的问题,你光看两个孩子之间是没有用的,这两个孩子还能看啥?相等?

所以我就想着用两个树作为输入,然后判断这两个树根的四个孩子之间是否是镜像对称的

然后对

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if(root == NULL)
            return true;
        
        if(root->left == NULL && root->right == NULL)
            return true;
        else if(root->left == NULL || root->right == NULL)
            return false;
        else if(root->left->val != root->right->val)
            return false;

        return isTwoTreeSymmetric(root->left, root->right);
    }

    bool isTwoTreeSymmetric(TreeNode* A, TreeNode* B){
        bool flag1 = true, flag2 = true;

        if(A->left == NULL && B->right == NULL){
            ;
        }
        else if(A->left == NULL || B->right == NULL){
            return false;
        }
        else if(A->left->val != B->right->val){
            return false;
        }
        else{
            flag1 = isTwoTreeSymmetric(A->left, B->right);
        }
        
        if(A->right == NULL && B->left == NULL){
            ;
        }
        else if(A->right == NULL || B->left == NULL){
            return false;
        }
        else if(A->right->val != B->left->val){
            return false;
        }
        else{
            flag2 = isTwoTreeSymmetric(A->right, B->left);
        }

        if(A->left == NULL && B->right == NULL &&
            A->right == NULL && B->left == NULL){
            return true;
        }

        return flag1 && flag2;
    }
};

第 8 天 动态规划(简单)

剑指 Offer 10- I. 斐波那契数列

一开始我就是简单的写动规

class Solution {
public:
    int fib(int n) {
        if(n == 0)
            return 0;
        if(n == 1)
            return 1;
        vector<int> arr(n+1, 0);
        arr[1] = 1;
        for (int i = 2; i < n+1; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
            arr[i] = arr[i] % (int)(1e9 + 7);
        }
        return arr[n];
    }
};

主要是注意是对每一个元素取模,而不是只有最后一次对结果取模

因为如果只有最后一次的话,在累加的过程中可能就溢出到负数了

之后才看到别人的题解,别人看到了整个过程中只使用 arr[i], arr[i-1], arr[i-2] 的规律,所以发现根本不需要一个数组,只需要三个变量就好了

class Solution {
public:
    int fib(int n) {
        if(n == 0)
            return 0;
        if(n == 1)
            return 1;
        int sum = 0, a = 0, b = 1;
        for (int i = 2; i < n+1; i++) {
            sum = a + b;
            sum = sum % (int)(1e9 + 7);
            a = b;
            b = sum;
        }
        return sum;
    }
};

然后再看到别人的快速幂算法

在这里插入图片描述
就很强

快速幂算法原理

如需求数据 a 的幂次,此处 a 可以为数也可以为矩阵,常规做法需要对a进行不断的乘积即 a * a * a * … 此处的时间复杂度将为 O(n)

以求 3^10 的结果为例:

[优化步骤1:]

易知:

3^10=3*3*3*3*3*3*3*3*3*3

    =9^5 = 9^4*9

    =81^2*9

    =6561*9

基于以上原理,我们在计算一个数的多次幂时,可以先判断其幂次的奇偶性,然后:

如果幂次为偶直接 base(底数) 作平方,power(幂次) 除以2

如果幂次为奇则底数平方,幂次整除于2然后再多乘一次底数

[优化步骤2:]

对于以上涉及到 [判断奇偶性] 和 [除以2] 这样的操作。使用系统的位运算比普通运算的效率是高的,因此可以进一步优化:

  1. 把 power % 2 == 1 变为 (power & 1) == 1

  2. 把 power = power / 2 变为 power = power >> 1

class Solution {
    public int fib(int n) {
        //矩阵快速幂
        if (n < 2) {
            return n;
        }
        //定义乘积底数
        int[][] base = {{1, 1}, {1, 0}};
        //定义幂次
        int power = n - 1;
        int[][] ans = calc(base, power);
        //按照公式,返回的是两行一列矩阵的第一个数
        return ans[0][0];
    }

    //定义函数,求底数为 base 幂次为 power 的结果
    public int[][] calc(int[][] base, int power) {
        //定义变量,存储计算结果,此次定义为单位阵
        int[][] res = {{1, 0}, {0, 1}};

        //可以一直对幂次进行整除
        while (power > 0) {
            //1.若为奇数,需多乘一次 base
            //2.若power除到1,乘积后得到res
            //此处使用位运算在于效率高
            if ((power & 1) == 1) {
                res = mul(res, base);
            }
            //不管幂次是奇还是偶,整除的结果是一样的如 5/2 和 4/2
            //此处使用位运算在于效率高
            power = power >> 1;
            base = mul(base, base);
        }
        return res;
    }

    //定义函数,求二维矩阵:两矩阵 a, b 的乘积
    public int[][] mul(int[][] a, int[][] b) {
        int[][] c = new int[2][2];
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                //矩阵乘积对应关系,自己举例演算一遍便可找到规律
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
            }
        }
        return c;
    }
}

来源:https://leetcode.cn/problems/fei-bo-na-qi-shu-lie-lcof/solution/fei-bo-na-qi-shu-lie-by-leetcode-solutio-hbss/1422557

就很强……

这种题的规律就是,用模拟一定是最蠢的

但是为什么用模拟蠢,是因为他里面还有数学规律可以用

感觉应该是从题目描述中看到这很有规律,所以也要注意,你的解法是对这个题目规律的单纯模拟,但是真的有利用到这个规律进行化简

剑指 Offer 10- II. 青蛙跳台阶问题

因为每一个台阶的走法都是身后第一级台阶的走法(跳一级跳上来) + 身后第二级台阶的走法(跳两级跳上来)

就是一个修改版的斐波那契数列

fib(0) = 1, fib(1) = 1

剑指 Offer 63. 股票的最大利润

一开始我很正常的做错了

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<int> minP, maxP;
        int currMin = INT_MAX, currMax = INT_MIN;

        for(int i = 0; i < prices.size(); i++){
            if(currMin != min(currMin, prices[i])){
                currMin = min(currMin, prices[i]);
                minP.push_back(currMin);
            }
            if(currMax != max(currMax, prices[i])){
                currMax = max(currMax, prices[i]);
                maxP.push_back(currMax);
            }
        }

        int currProfit = INT_MIN;
        for(int i = 0; i < minP.size(); i++){
            for(int j = 0; j < maxP.size(); j++){
                currProfit = max(currProfit, maxP[j] - minP[i]);
            }
        }

        return currProfit;
    }
};

主要是没有动规的思路,只好死板的写 min 数组,max 数组,然后 o(n^2) 级别的遍历 min 和 max 数组求最大差

这样理所当然地没有考虑到最小值和最大值之间的时间关系

class Solution {
public:
    int sign(int x){
        if(x == 0)
            return 0;
        if(x < 0)
            return -1;
        return 1;
    }

    int maxProfit(vector<int>& prices) {
        if(prices.size() < 2)
            return 0;

        vector<int> minP, maxP;

        // last_trend 初始化为 -1,使得 prices 第一个点不能取为最大值,但可以取为最小值
        int last_trend = -1, curr_trend = 1;

        for(int i = 0; i < prices.size()-1; i++){
            curr_trend = sign(prices[i+1] - prices[i]);

            if(curr_trend == 0)
                continue;

            if(curr_trend != last_trend){
                if(curr_trend == 1)
                    minP.push_back(prices[i]);
                else
                    maxP.push_back(prices[i]);

                last_trend = curr_trend;
            }

            // 处理最后一个数,最后一个数不能取为最小值,但是可以取为最大值
            if(i == prices.size()-2){
                if(curr_trend == 1){
                    maxP.push_back(prices[i+1]);
                }
            }
        }

        int max_profit = 0;
        for(int i = 0; i < minP.size(); i++){
            for(int j = i; j < maxP.size(); j++){
                max_profit = max(max_profit, maxP[j] - minP[i]);
            }
        }

        return max_profit;
    }
};

失败的贪心

主要是想着先取整个数组的最大值和最小值,然后从最大值往前看,与每个 price 相减,取最大利润;从最小值往前看,去减每个 price,取最大利润

class Solution {
public:

    int maxProfit(vector<int>& prices) {
        if(prices.size() < 2)
            return 0;

        int max_price = INT_MIN, min_price = INT_MAX;
        int max_idx = 0, min_idx = 0;

        for(int i = 0; i < prices.size(); i++){
            if(max_price < max(max_price, prices[i])){
                max_idx = i;
                max_price = max(max_price, prices[i]);
            }
            if(min_price > min(min_price, prices[i])){
                min_idx = i;
                min_price = min(min_price, prices[i]);
            }
        }

        int profit = 0;

        for(int i = 0; i < max_idx; i++){
            profit = max(profit, max_price - prices[i]);
        }
        for(int i = min_idx; i < prices.size(); i++){
            profit = max(profit, prices[i] - min_price);
        }

        return profit;
    }
};

结果贪失败了hhh

主要是因为最大利润的最大值或者最小值不一定是整个数组的最大值或者最小值

动规

对于这种题,他是每一步更新一个状态,所以就要想什么时候我要保留这个状态,什么时候要更新这个状态

那么自然是我要在每一步都算一个最大利润,如果算出来的最大利润大于我的上一步 dp 值,那么我就更新我当前 dp 值;否则我的当前 dp 值就取上一步的 dp 值

class Solution {
public:

    int maxProfit(vector<int>& prices) {
        if(prices.size() < 2)
            return 0;

        vector<int> dp(prices.size(), 0);
        
        int his_min = prices[0];

        dp[0] = INT_MIN;

        for(int i = 1; i < prices.size(); i++){
            his_min = min(his_min, prices[i]);
            if(dp[i-1] < prices[i] - his_min){
                dp[i] = prices[i] - his_min;
            }
            else{
                dp[i] = dp[i-1];
            }
        }

        return dp[dp.size()-1];
    }
};

第 9 天 动态规划(中等)

剑指 Offer 42. 连续子数组的最大和

这道题其实我以前见过,但是具体怎么使用动规我就忘了

然后现在一看,真的思路清晰了

首先就是假设已经知道了 dp[i-1],那么判断 dp[i] 该怎么取

为什么是只看 dp[i-1] 而不看更多的呢?那这就涉及到对于题目的直觉了

在这里我觉得是因为遍历数组是一个一个元素看的,最大子数组的长度也是一个一个增加的,所以我会认为 dp 也是一个一个变化的

然后具体该怎么判断 dp[i] 该怎么取?假设你什么都不知道,但是你首先要分两个方向,一个是“不变”的情况,一个是“变”的情况

“不变”就是说新的这一步不如我上一步,所以我保持原状,不更新

“变”就是说新的一步优于我上一步,我要结合这一步的信息来更新

那么放到这里就是说,我觉得上一步如果大于等于 0,那么我就直接把这一步的值加进来,如果加进来之后为正数,那么我下一步就能用我这一步的累加和;如果加进来之后为负数,那么我下一步就知道,必须要新开一个子数组,不然我如果用这个负数作为子数组的一部分,我一直是亏的

那么自然地,如果上一步小于 0,那么这一步也就不用上一步的累加和,而是新开一个

综上,状态方程为

dp[i] = dp[i-1], when dp[i-1] >= 0; = dp[i-1] + nums[i], when dp[i-1] < 0

然后再用到之前说过的,不需要 dp 是一个数组,只需要把 dp[i]dp[i-1] 存成两个数就行了

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int curr_dp = 0, prev_dp = 0, max_sum = INT_MIN;
        for(int i = 0; i < nums.size(); i++){
            if(prev_dp >= 0){
                curr_dp = prev_dp + nums[i];
                max_sum = max(curr_dp, max_sum);
                prev_dp = curr_dp;
            }
            else{
                curr_dp = nums[i];
                max_sum = max(curr_dp, max_sum);
                prev_dp = curr_dp;
            }
        }
        return max_sum;
    }
};

剑指 Offer 47. 礼物的最大价值

一开始我就是简单的 mn 的空间和时间的动规

class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        if(grid.size() == 0)
            return 0;

        int row = grid.size(), col = grid[0].size();
        vector<vector<int>> dp(row, vector<int>(col, 0));

        // 在某一个格子能拿到的最大价值
        // 取决于他的左边一格和上方一格
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(i == 0 && j == 0)
                    dp[i][j] = grid[i][j];
                else if(j == 0)
                    dp[i][j] = dp[i-1][j] + grid[i][j];
                else if(i == 0)
                    dp[i][j] = dp[i][j-1] + grid[i][j];
                else
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }

        return dp[row-1][col-1];
    }
};

之后才看到能够减少一点内存的做法,dp 从一个二维数组降到一个一维数组

因为你可以是一行一行更新的

dp[0]dp[1]dp[2]
   
   
dp[1]dp[2]
dp[0]  
   
  dp[2]
dp[0]dp[1] 
   
   
dp[0]dp[1]dp[2]
   
class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {
        if(grid.size() == 0)
            return 0;

        int row = grid.size(), col = grid[0].size();
        vector<int> dp_row(col, 0);
        int dp_left = 0;
        
        // 在某一个格子能拿到的最大价值
        // 取决于他的左边一格和上方一格
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(i == 0 && j == 0)
                    dp_row[j] = grid[i][j];
                else if(i == 0)
                    dp_row[j] = dp_row[j-1] + grid[i][j];
                else if(j == 0)
                    dp_row[j] = dp_row[j] + grid[i][j];
                else
                    dp_row[j] = max(dp_row[j-1], dp_row[j]) + grid[i][j];
            }
        }

        return dp_row[col-1];
    }
};

如果能够修改 grid 数组的话,还可以直接在 grid 上做 dp,那就是原地算法了

第 10 天 动态规划(中等)

剑指 Offer 46. 把数字翻译成字符串

最终找到的规律是,如果新的一个数字与上一个数字能够组合成一个用两位数字表示的字符时 dp[i] = dp[i-1] + dp[i-2],否则 dp[i] = dp[i-1]

例如 1 8 5 8 0

1

默认 dp[0] = 1

1 8

1 和 8 能够组合为 18,所以 dp[1] = dp[0] + dp[-1] 默认 dp[-1] = 1,所以 dp[1] = 2

1 8 5

8 和 5 不能组合,所以 dp[2] = dp[1]

1 8 5 8

5 和 8 不能组合,所以 dp[3] = dp[2]

1 8 5 8 0

8 和 0 不能组合,所以 dp[4] = dp[3]

优化就是 dp 可以不用数组而只用两个数

class Solution {
public:
    int translateNum(int num) {
        int prev_num = 0, curr_num = 0;
        int prev_dp = 1, curr_dp = 1;
        
        stack<int> numStack;
        int tmp = num;

        while(tmp != 0){
            numStack.push(tmp % 10);
            tmp = tmp / 10;
        }

        bool first = true;

        while(numStack.size() != 0){
            prev_num = curr_num;
            curr_num = numStack.top();
            numStack.pop();

            if(first){
                prev_num = curr_num;
                first = false;
                continue;
            }

            // 如果能够和前一个数形成组合
            if((prev_num == 1) ||
                (prev_num == 2 && curr_num <= 5)){
                tmp = curr_dp;
                curr_dp = prev_dp + curr_dp;
                prev_dp = tmp;
            }
            else{
                prev_dp = curr_dp;
            }
        }

        return curr_dp;
    }
};

这还是第一个我第一眼看不出规律,但是写了一会之后发现了简单的规律的题,很感动

感觉找这种规律,主要还是要相信自己一步一步查看元素能够找到一个影响很长的序列的规律

剑指 Offer 48. 最长不含重复字符的子字符串

我一开始的想法就是,也是跟动规一样,遇到好的就下一个,遇到差的就更新

那么也就是,如果没有遇见过的字符,就增大子串,如果遇到了见过的字符,就从子串的左边界开始找起,找到新子串的左边界

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int head = 0;
        int max_size = 0;

        unordered_set<char> charSet;
        for (int i = 0; i < s.size(); i++) {
            // 如果没有重复
            if (charSet.find(s[i]) == charSet.end()) {
                charSet.insert(s[i]);
                // size() 返回 size_t 类型,转换成 int 类型
                max_size = max(max_size, (int)charSet.size());
            }
            // 如果有重复
            else {
                // 从 head 开始向后找,每找一个就从 set 中剔除,直到找到与当前 s[i] 相同的字符
                for (int j = head; j < i; j++) {
                    if (s[j] != s[i]) {
                        charSet.erase(s[j]);
                        head = j;
                    }
                    else {
                        head = j + 1;
                        break;
                    }
                }
            }
        }

        return max_size;
    }
};

第 11 天 双指针(简单)

剑指 Offer 18. 删除链表的节点

太简单了

剑指 Offer 22. 链表中倒数第k个节点

设两个指针,算法开始时,第一个指针开始移动,第二个指针先不动,等到两个指针相差 k 的时候,第二个指针再开始跟着第一个指针同步动,直到第一个指针走到结尾

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode *p1 = head, *p2 = head;
        for(int i = 0; i < k-1; i++){
            if(p1 != NULL){
                p1 = p1->next;
            }
            else{
                // 预料之外的情况:k 大于链表长度
                return NULL;
            }
        }

        while(p1->next != NULL){
            p1 = p1->next;
            p2 = p2->next;
        }

        return p2;
    }
};

第 12 天 双指针(简单)

剑指 Offer 25. 合并两个排序的链表

在新链表的头部虚增一个头节点,返回的时候返回这个虚增节点的 next

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == NULL)
            return l2;
        if (l2 == NULL)
            return l1;

        ListNode* head = new ListNode(0);

        ListNode* prev = head;

        while (l1 != NULL && l2 != NULL) {
            if (l1->val <= l2->val) {
                prev->next = l1;
                prev = prev->next;
                l1 = l1->next;
            }
            else {
                prev->next = l2;
                prev = prev->next;
                l2 = l2->next;
            }
        }

        if (l1 == NULL)
            prev->next = l2;
        else
            prev->next = l1;

        return head->next;
    }
};

剑指 Offer 52. 两个链表的第一个公共节点

双指针,首先两个指针 p1, p2 各自指向两个链表的头部,同步移动,直到有一个指针的 nextNULL

这个时候停下,再设置两个指针 p3, p4 各自指向两个链表的头部。假设之前是指向链表 1 的指针 p1 先达到 next == NULL,那么这个时候新的两个指针之中,指向链表 2 的 p4p2 同步移动,直到 p1 == p2,那么这个时候就已经在 p3p4 之间模拟出了 p1p2 之间的间隔,之后就是再让 p3p4 同步移动,一定会发现一个相交结点使得 p3 == p4;如果不存在相交结点使得 p1 == p2,那么最终也不会找到一个相交结点使得 p3 == p4,那么最终就返回 NULL

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if(headA == headB)
            return headA;
        if(headA == NULL || headB == NULL)
            return NULL;

        ListNode *p1 = headA, *p2 = headB;
        ListNode *p3 = headA, *p4 = headB;

        while(p1->next != NULL && p2->next != NULL){
            if(p1 == p2)
                return p1;
            p1 = p1->next;
            p2 = p2->next;
        }

        if(p1->next == NULL){
            while(p1 != p2 && p2 != NULL){
                p2 = p2->next;
                p4 = p4->next;
            }
        }
        else{
            while(p1 != p2 && p1 != NULL){
                p1 = p1->next;
                p3 = p3->next;
            }
        }

        while(p3 != NULL && p4 != NULL){
            if(p3 == p4)
                return p3;
            p3 = p3->next;
            p4 = p4->next;
        }

        return NULL;
    }
};

但是别人还有简练的写法……很强

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *node1 = headA;
        ListNode *node2 = headB;
        
        while (node1 != node2) {
            node1 = node1 != NULL ? node1->next : headB;
            node2 = node2 != NULL ? node2->next : headA;
        }
        return node1;
    }
};

作者:z1m
链接:https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/solution/shuang-zhi-zhen-fa-lang-man-xiang-yu-by-ml-zimingm/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

第 13 天 双指针(简单)

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

真的是一眼快排,没啥好说的

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        if(nums.size() <= 1)
            return nums;
            
        // 一眼快排
        auto it1 = nums.begin(), it2 = nums.end()-1;
        while(it1 != it2){
            while(it1 != it2 && *it1 % 2 == 1){
                it1++;
            }
            while(it1 != it2 && *it2 % 2 == 0){
                it2--;
            }
            if(it1 != it2)
                iter_swap(it1, it2);
        }

        return nums;
    }
};

剑指 Offer 57. 和为s的两个数字

也是简单的双指针,右指针从最后一个位置开始向左移动,移动到第一个小于 target 的位置,然后左指针初始化在数组头部

在循环中,右指针每向左移动一次,左指针都会从头开始向右走,查找左右指针指向的元素之和是否等于 target,直到左右指针相遇

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        auto it1 = nums.begin(), it2 = nums.end()-1;
        while(*it2 > target){
            it2--;
        }
        while(it1 != it2){
            while(it1 != it2 && (*it1 + *it2) < target){
                it1++;
            }
            if((*it1 + *it2) == target){
                return vector<int>{*it1, *it2};
            }

            it2--;
        }

        return vector<int>();
    }
};

评论区里面还说了,要注意避免两指针指向的数相加,而是用 target - *it1 == *it2 判断,这样可以避免溢出

剑指 Offer 58 - I. 翻转单词顺序

要注意的是,对于一个 string,计算他的 .size() 返回的是不包括 '\0' 的字符串的长度

但是如果你取 str[str.size()] 的话会返回 '\0'

就很神奇,估计是为了照顾 C 的习惯

但是我们自己在遍历 string 的时候就要小心了

一开始我就自己写的分割+倒序

class Solution {
public:
    string reverseWords(string s) {
        vector<string> words;
        string tmpStr;
        for (int i = 0; i < s.size(); i++) {
            while (i < s.size() && s[i] == ' ') {
                i++;
                continue;
            }

            tmpStr = "";

            while (i < s.size() && s[i] != ' ') {
                tmpStr.push_back(s[i]);
                i++;
            }

            if(tmpStr != "")
                words.push_back(tmpStr);
        }

        reverse(words.begin(), words.end());

        tmpStr = "";

        for (int i = 0; i < words.size(); i++) {
            tmpStr.append(words[i]);
            if (i != words.size() - 1)
                tmpStr.push_back(' ');
        }

        return tmpStr;
    }
};

第 14 天 搜索与回溯算法(中等)

剑指 Offer 12. 矩阵中的路径

	bool exist(vector<vector<char>>& board, string word) {
        int row = board.size(), col = board[0].size();

        if (row == 0)
            return false;
        if (word.size() == 0)
            return false;
        if (word.size() > row * col)
            return false;

        vector<vector<bool>> used(row, vector<bool>(col, false));


        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (board[i][j] == word[0])
                    if (exist_in_board(used, board, word.erase(0, 1), i, j))
                        return true;
            }
        }

        return false;
    }

之后我就是简单的递归搜索,用了一个辅助的二维数组来记录哪里走过

class Solution {
private:
    int dir[4][2] = { {-1, 0}, {1, 0}, {0, 1}, {0, -1} };

public:
    bool exist(vector<vector<char>>& board, string word) {
        int row = board.size(), col = board[0].size();

        if (row == 0)
            return false;
        if (word.size() == 0)
            return false;
        if (word.size() > row * col)
            return false;

        vector<vector<bool>> used(row, vector<bool>(col, false));

        string tmp = word;
        tmp.erase(0, 1);

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (board[i][j] == word[0])
                    if (exist_in_board(used, board, tmp, i, j))
                        return true;
            }
        }

        return false;
    }

    bool exist_in_board(vector<vector<bool>>& used, vector<vector<char>>& board, string word, int beginX, int beginY) {
        if (word.size() == 0)
            return true;

        used[beginX][beginY] = true;

        int row = board.size(), col = board[0].size();

        string tmp = word;
        tmp.erase(0, 1);

        for (int i = 0; i < 4; i++) {
            int nextX = beginX + dir[i][0];
            int nextY = beginY + dir[i][1];

            if(nextX >= 0 && nextX < row && nextY >= 0 && nextY < col)
                if (board[nextX][nextY] == word[0] && used[nextX][nextY] == false) {
                    if (exist_in_board(used, board, tmp, nextX, nextY)) {
                        used[beginX][beginY] = false;
                        return true;
                    }
                }
        }

        used[beginX][beginY] = false;
        return false;
    }
};

看到评论区里面说可以将走过的 board 上的元素暂时置为 '/' 这样就省下了辅助数组的空间,强啊

class Solution {
private:
    int dir[4][2] = { {-1, 0}, {1, 0}, {0, 1}, {0, -1} };

public:
    bool exist(vector<vector<char>>& board, string word) {
        int row = board.size(), col = board[0].size();

        if (row == 0)
            return false;
        if (word.size() == 0)
            return false;
        if (word.size() > row * col)
            return false;

        string tmp = word;
        tmp.erase(0, 1);

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (board[i][j] == word[0])
                    if (exist_in_board(board, tmp, i, j))
                        return true;
            }
        }

        return false;
    }

    bool exist_in_board(vector<vector<char>>& board, string word, int beginX, int beginY) {
        if (word.size() == 0)
            return true;

        char tmpChar = board[beginX][beginY];
        board[beginX][beginY] = '/';

        int row = board.size(), col = board[0].size();

        string tmp = word;
        tmp.erase(0, 1);

        for (int i = 0; i < 4; i++) {
            int nextX = beginX + dir[i][0];
            int nextY = beginY + dir[i][1];

            if(nextX >= 0 && nextX < row && nextY >= 0 && nextY < col)
                if (board[nextX][nextY] == word[0]) {
                    if (exist_in_board(board, tmp, nextX, nextY)) {
                        board[beginX][beginY] = tmpChar;
                        return true;
                    }
                }
        }

        board[beginX][beginY] = tmpChar;
        return false;
    }
};

我发现这种原地的方法都是要修改传入的参数的,感觉原地方法可以往这个方向思考

剑指 Offer 13. 机器人的运动范围

简单的广搜

class Solution {
private:
    int dir[4][2] = { {-1, 0}, {1, 0}, {0, 1}, {0, -1} };
public:
    int movingCount(int m, int n, int k) {
        queue<pair<int, int>> posQueue;
        vector<vector<bool>> flag(m, vector<bool>(n, false));

        posQueue.push(pair<int, int>(0, 0));

        pair<int, int> pos;

        // 初始的时候已经到达了 (0, 0)
        int count = 1;
        flag[0][0] = true;

        while(posQueue.size() != 0){
            pos = posQueue.front();
            posQueue.pop();

            for(int i = 0; i < 4; i++){
                int x = pos.first + dir[i][0];
                int y = pos.second + dir[i][1];

                if(x >= 0 && x < m && y >= 0 && y < n){
                    if(flag[x][y] == false){
                        // 是否满足题目条件
                        if(canMoveTo(x, y, k)){
                            count++;
                            flag[x][y] = true;
                            posQueue.push(pair<int, int>(x, y));
                        }
                    }
                }
            }
        }

        return count;
    }

    bool canMoveTo(int x, int y, int k){
        int sum = 0;
        while(x != 0){
            sum += x % 10;
            x = x / 10;
        }
        while(y != 0){
            sum += y % 10;
            y = y / 10;
        }
        if(sum <= k)
            return true;
        else
            return false;
    }
};

第 15 天 搜索与回溯算法(中等)

剑指 Offer 34. 二叉树中和为某一值的路径

最简单的递归

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
private:
    vector<vector<int>> result;
public:
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<int> path;
        Traverse(root, path, target, 0);

        return result;
    }

    void Traverse(TreeNode* root, vector<int> path, int target, int sum){
        if(root == NULL) return;

        sum += root->val;
        path.push_back(root->val);

        if(root->left == NULL && root->right == NULL){
            if(sum == target){
                vector<int> copy(path);
                result.push_back(copy);
            }
        }
        
        // val 可以小于等于 0
        // 因此不能根据 sum 与 target 之间的大小关系来判断是否终止
        Traverse(root->left, path, target, sum);
        Traverse(root->right, path, target, sum);

        path.pop_back();
    }
};

但是我的执行时间和内存消耗都很大,排名倒数

看到别人的执行时间是 0ms 的

class Solution {
public:
    vector<vector<int>> ans;
    vector<int>path;
    void traversal(TreeNode* root,int target,int sum){
        path.push_back(root->val);
        sum += root->val;
        if( root->left == nullptr && root->right == nullptr )
            if( sum == target ){
                ans.push_back(path);
                return ;
            }
        if( root->left ){
            traversal(root->left,target,sum);
            path.pop_back();
        }
        if( root->right ){
            traversal(root->right,target,sum);
            path.pop_back();
        }
    }
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        if( root == nullptr )   return ans;
        traversal(root,target,0); 
        return ans;
    }
};

// 作者:serene-i3oseese
// 链接:https://leetcode.cn/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/solution/by-serene-i3oseese-pujy/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

我第一个注意到的就是它把存储都放到外面了

于是我写成了

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
public:
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<int> path;
        Traverse(root, target, 0);

        return result;
    }

    void Traverse(TreeNode* root, int target, int sum){
        if(root == NULL) return;

        sum += root->val;
        path.push_back(root->val);

        if(root->left == NULL && root->right == NULL){
            if(sum == target){
                vector<int> copy(path);
                result.push_back(copy);
            }
        }
        
        // val 可以小于等于 0
        // 因此不能根据 sum 与 target 之间的大小关系来判断是否终止
        Traverse(root->left, target, sum);
        Traverse(root->right, target, sum);

        path.pop_back();
    }
};

果然两项都大大提升了

原来是我传值了

function1(std::vector<std::vector > vec),传值
function2(std::vector<std::vector >& vec),传引用
function3(std::vector<std::vector >* vec),传指针

这样的话我就知道了,那我加入二维数组的时候也不需要特意复制一遍

        if(root->left == NULL && root->right == NULL){
            if(sum == target){
                result.push_back(path);
            }
        }

剑指 Offer 36. 二叉搜索树与双向链表

这个是真的很惊喜,用先序遍历,一次过了

就是递归一般是把问题分解成一个可以嵌套的子问题嘛,然后最终在边界条件的地方是真正解决最小子问题的地方,其他就是要注意在嵌套返回的时候还要注意怎么返回的

或者说成分治也行

就是我希望是在进入某一个根 root 的时候,就获得这个根的左子树的双向链表的头尾 head_rear_left 右子树的双向链表的头尾 head_rear_right

然后根的左指针指向左子树的双向链表的尾部 root->left = head_rear_left[1]

根的右指针指向右子树的双向链表的头部 root->right = head_rear_right[0]

最终这个根要返回自己所代表的双向链表的头尾,所以应该返回 {head_rear_left[0], head_rear_right[1]}

最后返回了整个树的双向链表的头尾的时候,将头尾连接起来,返回头部

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;

    Node() {}

    Node(int _val) {
        val = _val;
        left = NULL;
        right = NULL;
    }

    Node(int _val, Node* _left, Node* _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
public:
    Node* treeToDoublyList(Node* root) {
        if(root == NULL) return NULL;
        vector<Node*> head_rear = treeToDoublyListAndGetHeadAndRear(root);
        head_rear[0]->left = head_rear[1];
        head_rear[1]->right = head_rear[0];
        return head_rear[0];
    }

    vector<Node*> treeToDoublyListAndGetHeadAndRear(Node* root){
        vector<Node*> head_rear{root, root};

        if(root->left == NULL && root->right == NULL)
            return head_rear;

        if(root->left != NULL){
            vector<Node*> head_rear_left = treeToDoublyListAndGetHeadAndRear(root->left);
            head_rear_left[1]->right = root;
            root->left = head_rear_left[1];

            head_rear[0] = head_rear_left[0];
        }
        if(root->right != NULL){
            vector<Node*> head_rear_right = treeToDoublyListAndGetHeadAndRear(root->right);
            head_rear_right[0]->left = root;
            root->right = head_rear_right[0];

            head_rear[1] = head_rear_right[1];
        }

        return head_rear;
    }
};
执行用时:4 ms, 在所有 C++ 提交中击败了92.54% 的用户
内存消耗:8.7 MB, 在所有 C++ 提交中击败了5.16% 的用户

一开始我是没有想到中序遍历

现在确实记起来了,二叉线索树的中序遍历就是排序数组

树的遍历的返回值的顺序不一定就是左孩子、根、右孩子这样的简单规律

但是其实我是应该想到的,只是我一开始在想常规遍历的时候,我在想,常规中序遍历返回值的顺序依然只是左孩子、根、右孩子

因此我写成这样就不太会写了,因为按照左孩子、根、右孩子来写肯定是错的

void MiddleTraverse(Node* root){
	if(root->left == NULL && root->right == NULL){
        if(!findHead){
            head = root;
            return;
        }
    }

    if(root->left != NULL){
        MiddleTraverse(root->left);
        root->left->right = root;
    }
    if(root->right != NULL){
        MiddleTraverse(root->right);
        root->right->left = root;
    }
    
}

问题的关键就在于中序的时候我还要返回一个最左和最右

这样就会让我想到,果然还是要返回一个数组

这样就把我引入了分治的思路了

实际上中序也是可以不死板地只知道左孩子、根、右孩子

只要在外面存储全局变量,然后在中序的过程中修改这个全局变量

然后之后还有一个点在于,要存储的全局变量是什么意义的

我一开始以为中序遍历要存储的变量也是二叉链表的头节点和尾结点这个意义

所以我在想写中序的时候我还在想半天

实际上是应该利用中序遍历的性质

利用中序遍历二叉线索树的访问顺序是递增数组的这个性质

那么全局变量应该是跟访问顺序有关,而不是跟二叉链表头尾相关

所以全局变量要保存的是 prev 和 curr

这不就是二叉线索树的那个建立中序线索的过程……

这就唤醒了我的久远的 408 的回忆……

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;

    Node() {}

    Node(int _val) {
        val = _val;
        left = NULL;
        right = NULL;
    }

    Node(int _val, Node* _left, Node* _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
public:
    Node *head;
    Node *prev, *curr;
    bool findHead = false;

    Node* treeToDoublyList(Node* root) {
        if(root == NULL) return NULL;

        MiddleTraverse(root);

        // 中序遍历结束时的 curr 就是二叉链表的尾部

        head->left = curr;
        curr->right = head;

        return head;
    }

    void MiddleTraverse(Node* root){
        // 左

        if(root->left != nullptr){
            MiddleTraverse(root->left);
        }

        // 中

        // 在访问根的时候,首先 curr 置为被访问的根
        // 将 prev 的 right 指向 curr
        // 将 curr 的 left 指向 prev
        // 最后将 prev 置为当前被访问的根

        // 问题在于第一次怎么确定 prev 和 curr
        // 第一次找到左孩子为空的时候就是访问的第一个节点,这个节点是双向链表的头部
        // 找到头部之后将 prev 和 curr 指向它
        // 然后就直接找下一个访问的结点

        curr = root;

        if(root->left == NULL && !findHead){
            findHead = true;
            prev = root;
            curr = root;
            head = root;
        }
        else{
            prev->right = curr;
            curr->left = prev;
        }

        prev = curr;

        // 右

        if(root->right != nullptr){
            MiddleTraverse(root->right);
        }
    }
};

但是时间还不如我的分治呢hhh

执行用时:8 ms, 在所有 C++ 提交中击败了38.59% 的用户
内存消耗:7.4 MB, 在所有 C++ 提交中击败了50.52% 的用户

剑指 Offer 54. 二叉搜索树的第k大节点

就是中序遍历调转了一个个而已,左中右变成右中左

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
private:
    int count = 0;
    int target = 0;
    bool hasFound = false;
    int result = 0;
public:
    int kthLargest(TreeNode* root, int k) {
        if(root == nullptr) return 0;
        if(k < 1) return 0;

        target = k;
        MiddleTraverse(root);

        return result;
    }

    void MiddleTraverse(TreeNode* root){
        // 右
        if(hasFound) return;

        if(root->right != nullptr){
            MiddleTraverse(root->right);
        }

        // 中
        count++;

        if(count == target){
            hasFound = true;
            result = root->val;
        }

        // 左

        if(root->left != nullptr){
            MiddleTraverse(root->left);
        }
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值