《代码随想录》学习笔记

一、数组

数组操作中用到的方法:二分,双指针(快慢指针、首尾指针),滑动窗口

1.【二分查找】

该题要注意的坑点就是开区间还是闭区间的问题。

思路:区间的定义有两种,左闭右闭或者左闭右开。先说左闭右闭,此时判断条件一定是while(left <= right),因为left==right是有意义的。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0, right = n - 1;
        while(left <= right){
            int mid = (left + right) >> 1;
            if(nums[mid] < target){
                left = mid + 1;
            }
            else if(nums[mid] > target){
                right = mid - 1;
            }else{
                return mid;
            }
        }
        return -1;
    }
};
//时间复杂度O(logn)
//空间复杂度O(1)

 再说左闭右开,首先边界条件变为while(left < right),因为left==right没有意义,其次要注意right的初值,因为是右开,所以考虑到数组只有一个元素的情况下,初值设为nums.size()即可。最后注意当nums[mid] > target 时,right = mid,因为开区间导致下一个查询区间不回去寻找nums[mid]

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0, right = n;
        while(left < right){
            int mid = (left + right) >> 1;
            if(nums[mid] < target){
                left = mid + 1;
            }
            else if(nums[mid] > target){
                right = mid;
            }else{
                return mid;
            }
        }
        return -1;
    }
};
//时间复杂度O(logn)
//空间复杂度O(1)

2.【移除元素】

第一反应是用vector.erase()方法来做,erase会删除指定位置的元素,并自动将后续元素移动填补被删除的位置,从而保持数组中元素的连续性。

顺便复习一下顺序容器,比如vector,deque,list的几个操作

push_back(val),pop_back(),insert(pos, val),erase(),该方法的缺点就是时间复杂度开销大

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] == val){
                nums.erase(nums.begin() + i);
                i--;
            }
        }
        return nums.size();
    }
};
//时间复杂度O(n^2)
//空间复杂度O(1)

 接下来使用双指针(快慢指针)的解法,原因:快指针去找不等于val的,慢指针更新新数组的下标

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        //使用快慢指针来做
        int slowIndex = 0;
        for(int fastIndex = 0; fastIndex < nums.size(); fastIndex++){
            if(nums[fastIndex] != val){
                nums[slowIndex] = nums[fastIndex];
                slowIndex++;
                
            }
        }
        return slowIndex;
    }
};
//时间复杂度O(n)
//空间复杂度O(1)

 3.【有序数组的平方】

 思路:第一反应是所有元素平方之后重新排序,但是没用到“非递减数列”这条信息 

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        for(int i = 0; i < nums.size(); i++){
            nums[i] = nums[i] * nums[i];
        }
        sort(nums.begin(), nums.end());
        return nums;
    }
};
//时间复杂度O(nlogn)
//空间复杂度O(n)

再说这题为什么会想到用双指针的方法呢, 原因是这个数组的特点是所有元素平方之后,最大值一定产生于两端,最小值一定是在中间,即平方之后,数组大小从两端递减到中间。

提交中遇到的一个溢出问题:

直接在原数组上进行更改,nums[k] =  nums[beginIndex] * nums[beginIndex]系统判定会发生int溢出的问题。修改策略:一种是根治,修改整数类型,来存放平方后的结果,一种是由于测试用例不会给溢出范围的值,所以重新定义一个result数组,用来存放结果,而不是直接在原数组上修改。

错误代码:

//错误代码
class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        //首尾指针,比较平方后的大小,再将新数组从尾开始赋值
        int beginIndex = 0, endIndex = nums.size() - 1;
        int k = nums.size() - 1;
        while(beginIndex <= endIndex){
            if(nums[beginIndex] * nums[beginIndex] >= nums[endIndex] * nums[endIndex]){
                nums[k] =  nums[beginIndex] * nums[beginIndex];
                beginIndex++;
            }else{
                nums[k] =  nums[endIndex] * nums[endIndex];
                endIndex--;
            }
            k--;

        }
        return nums;
    }
};

修改后: 

//正确代码
class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        //首尾指针,比较平方后的大小,再将新数组从尾开始赋值
        int beginIndex = 0, endIndex = nums.size() - 1;
        int k = nums.size() - 1;
        vector<int> result(nums.size());
        while(beginIndex <= endIndex){
            if(nums[beginIndex] * nums[beginIndex] >= nums[endIndex] * nums[endIndex]){
                result[k] =  nums[beginIndex] * nums[beginIndex];
                beginIndex++;
            }else{
                result[k] =  nums[endIndex] * nums[endIndex];
                endIndex--;
            }
            k--;

        }
        return result;
    }
};
//时间复杂度O(n)
//空间复杂度O(n)

 *4.【长度最小的子数组】 

 思路:没思路,本来想法是先排序,然后查找有没有target,有的话返回1,没有的话再双指针去找,但是想不出来怎么返回最小长度。。。看过解析之后,使用滑动窗口(不断的调整子数列的起始位置和终止位置从而得到想要的结果)。核心:while(sum >= target) 判断部分

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = INT32_MAX;
        int subLength = 0;//记录滑动窗口的长度
        int i = 0;//滑动窗口起始位置
        int sum = 0;//滑动窗口的和
        for(int j = 0; j < nums.size(); j++){
            sum += nums[j];
            while(sum >= target){
                subLength = j - i + 1;
                result = result < subLength ? result : subLength; 
                sum -= nums[i++]; 
            }
        }
        return result == INT32_MAX ? 0 : result;
    }
};
//时间复杂度O(n)
//空间复杂度O(1)

 *5.【螺旋矩阵】 

不是很有思路,直接看解析。参照K神解析,感觉这个版本比较容易理解,尤其是迭代条件while(num <= end) 简单易懂,此外学了一手二维矩阵初始化result(n, vector<int>(n, 0))

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        int left = 0, right = n - 1, top = 0, down = n - 1;
        vector<vector<int>> result(n, vector<int>(n, 0));
        int num = 1, end = n * n;
        while(num <= end){
            //从左到右
            for(int i = left; i <= right; i++){
                result[top][i] = num++; 
            }
            top++;
            //从上到下
            for(int i = top; i <= down; i++){
                result[i][right] = num++; 
            }
            right--;
            //从右到左
            for(int i = right; i >= left; i--){
                result[down][i] = num++; 
            }
            down--;
            //从下到上
            for(int i = down; i >= top; i--){
                result[i][left] = num++; 
            }
            left++;
        }
        return result;
    }
};
//时间复杂度O(n^2)
//空间复杂度O(n^2)

6.【无重复字符的最长子串】

思路:滑动窗口+哈希set

find()函数用于查找指定元素,如果找到返回指向该元素的迭代器,如果找不到返回set.end()

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0;
        unordered_set<int> set;
        int maxStreak = 0;
        for(int right = 0; right < s.size(); right++){
            //当有重复子串出现时,移动左指针,删除最左字符
            while(set.find(s[right]) != set.end()){
                set.erase(s[left]);
                left++;
            }
            //删除之后记得加入当前字符元素
            set.insert(s[right]);
            maxStreak = max(maxStreak, right - left + 1);
        }
        return maxStreak;
    }
};

7.【找到字符串中所有字母异位词】

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int> result;
        if(s.length() < p.length()) return result;

        unordered_map<char, int> sCount, pCount;
        int left = 0, right = 0;

        //统计p的字符
        for(char c : p){
            pCount[c]++;
        }
        //滑动窗口统计s字符串
        while(right < s.length()){
            //先统计s字符
            sCount[s[right]]++;
            //如果长度相等
            if(right - left + 1 == p.size()){
                if(sCount == pCount){
                    result.push_back(left);
                }
                sCount[s[left]]--;
                if(sCount[s[left]] == 0){
                    sCount.erase(s[left]);
                }
                left++;
            }
            right++;
        }
        return result;
    }
};

8.【和为k的子数组】

思路:前缀和,寻找前缀和 - k 是否存在于数组

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int result = 0;
        unordered_map<int, int> map;//存储前缀和及其出现次数
        int sum = 0;
        map[0] = 1;//初始化前缀和为0,出现一次
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
            //检查是否存在前缀和sum - k 
            if(map.count(sum - k)){
                result += map[sum - k];
            }
            //记录当前前缀和出现的次数
            map[sum]++;
        }
        return result;
    }
};

 9.【最小覆盖子串】

class Solution {
public:
    string minWindow(string s, string t) {
        if(s.empty() || t.empty()) return "";
        //统计t的字符个数
        unordered_map<char, int> t_map;
        for(char c : t){
            t_map[c]++;
        }

        //窗口内字符的数量
        unordered_map<char, int> windows_map;
        int left = 0, right = 0;
        int minLength = INT_MAX;
        int start = 0;
        int required = t_map.size();
        int formed = 0;//表示当前窗口内有多少种字符达到了t的需求

        //开始滑动窗口
        while(right < s.size()){
            char c = s[right];
            windows_map[c]++;

            //如果当前c出现的次数达到了t的要求
            //t_map.find(c) != t_map.end()这一步是为了避免不要的windows_map统计,避免性能上的冗余计算
            if(t_map.find(c) != t_map.end() && windows_map[c] == t_map[c]){
                formed++;
            }
            //t中所有字符都满足,开始收缩窗口
            while(left <= right && required == formed){
                //更新最小子串
                if(right - left + 1 < minLength){
                    minLength = right - left + 1;
                    start = left;
                }
                //收缩窗口
                char left_char = s[left];
                windows_map[left_char]--;
                if(t_map.find(left_char) != t_map.end() && windows_map[left_char] < t_map[left_char]){
                    formed--;
                }
                left++;
            }

            //t中字符还未满足
            right++;

        }
        //返回最小覆盖子串
        return minLength == INT_MAX ? "" : s.substr(start, minLength);

    }
};

10.【最大子数组和】

思路:比较当前值和当前值+currentSum

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int currentSum = nums[0];
        int sumMax = nums[0];
        for(int i = 1; i < nums.size(); i++){
            //如果nums[i]比currentSum + nums[i]大,说明新的子数组从当前开始更好,否则,currentSum + nums[i]
            currentSum = max(nums[i], currentSum + nums[i]);
            sumMax = max(sumMax, currentSum);
        }
        return sumMax;
    }
};

11.【合并区间】

思路:这里容易产生一个误区,即每个区间只有两个值,nums[0]和nums[1]

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if(intervals.empty()) return {};
        //sort只对每个数组的第一个数字进行排序
        sort(intervals.begin(), intervals.end());

        vector<vector<int>> result;
        result.push_back(intervals[0]);
        for(int i = 1; i < intervals.size(); i++){
            vector<int>& last = result.back();
            vector<int>& current = intervals[i];
            if(current[0] <= last[1]){
                last[1] = max(current[1], last[1]);
            }else{
                result.push_back(intervals[i]);
            }
        }
        return result;
    }
};

 12.【轮转数组】

//reverse用法,包含起始位置,不包含终止位置。

思路:轮转数组是一道技巧题,分三步,第一步reverse整个数组;第二步reverse(nums.begin(), nums.begin()+k);第三步reverse(nums.begin()+k, nums.end());

注意:如果不加 k = k % nums.size();,当 k 大于或等于 nums.size() 时,程序会出现 超出范围 的错误,或者导致逻辑错误。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        k = k % nums.size();
        reverse(nums.begin(), nums.end());
        reverse(nums.begin(), nums.begin() + k);
        reverse(nums.begin() + k, nums.end());
        
    }
};

排序

1.排序数组(快排)

思路:

1.首先设定一个分界值,通过该分界值将数组分为左右两部分;

2.小于等于数据集的放到左边,大于等于数据集的放到右边;

3.对于左右两部分,再次递归。

一开始是按照一个指针移动来写的,即只分为key值的左边和右边,如果遇到有多个相同key值的情况,并没有考虑设置一个中间区域,来优化时间复杂度。

partitionThreeWay() 需要 leftright 作为引用参数,以便 quickSort() 正确跳过等于 pivot 的区域,提高性能!

  • srand(time(0)) 初始化随机数种子,确保 rand() 每次运行都会产生不同的随机数
  • rand() % (r - l + 1) 生成 [l, r] 范围内的随机索引,用于随机选择 pivot,防止最坏情况 O(n²)
class Solution {
public:
    // **三向切分快速排序**
    void partitionThreeWay(vector<int>& nums, int pivot, int l, int r, int& left, int& right) {
        left = l;
        right = r;
        int i = left;

        while (i <= right) {
            if (nums[i] < pivot) {
                swap(nums[i], nums[left]); 
                left++;
                i++;
            } else if (nums[i] > pivot) {
                swap(nums[i], nums[right]); 
                right--;
            } else {
                i++; // 如果 nums[i] == pivot,直接跳过
            }
        }
    }

    // **快速排序**
    void quickSort(vector<int>& nums, int l, int r) {
        if (l >= r) return;

        int pivot = nums[l + rand() % (r - l + 1)]; // **随机选择 pivot**
        int leftPartition, rightPartition;
        partitionThreeWay(nums, pivot, l, r, leftPartition, rightPartition);

        quickSort(nums, l, leftPartition - 1);
        quickSort(nums, rightPartition + 1, r);
    }

    vector<int> sortArray(vector<int>& nums) {
        srand(time(0)); // **设定随机种子**
        quickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

矩阵

1.【矩阵置零】

思路:这题就是五个步骤,记住了就能做出来

第一二步,检查第一行、第一列是否有零,有的话就作标记,为后续整行整列清零准备。(避免错误地清零整个第一行或第一列,确保 matrix[0][0] 只是一个标记,而 firstRowZero / firstColZero 决定最终是否要清零第一行和第一列。)

第三步,遍历数组,遇到0,此时再用第一行和第一列作标记;

第四步,遍历数组,使用第一行和第一列的标记来置零;

第五步,单独处理第一行和第一列。

class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size(), n = matrix[0].size();
        bool firstRowZero = false, firstColZero = false;

        //检查第一行是否有零
        for(int i = 0; i < n; i++){
            if(matrix[0][i] == 0){
                firstRowZero = true;
                break;
            }
        }
        
        //检查第一列是否有零
        for(int i = 0; i < m; i++){
            if(matrix[i][0] == 0){
                firstColZero = true;
                break;
            }
        }

        //检查剩余是否有零,用第一行第一列来标记
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(matrix[i][j] == 0){
                    matrix[i][0] = 0;
                    matrix[0][j] = 0;
                } 
            }
        }

        //根据标记置零
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(matrix[i][0] == 0 || matrix[0][j] == 0){
                    matrix[i][j] = 0;
                } 
            }
        }

        //处理第一行
        if(firstRowZero){
            for(int j = 0; j < n; j++){
                matrix[0][j] = 0;
            }
        }

        //检查第一列
        if(firstColZero){
            for(int j = 0; j < m; j++){
                matrix[j][0] = 0;
            }
        }
    }
};

2.【螺旋矩阵】

思路:绕着圈的遍历

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> result;
        if(matrix.empty() || matrix[0].empty()){
            return result;
        }

        int m = matrix.size(), n = matrix[0].size();
        int top = 0, bottom = m - 1, left = 0, right = n - 1;
        while(top <= bottom && left <= right){
            //从左到右遍历顶部行
            for(int i = left; i <= right; i++){
                result.push_back(matrix[top][i]);
            }
            top++;

            //从上到小遍历右侧列
            for(int i = top; i <= bottom; i++){
                result.push_back(matrix[i][right]);
            }
            right--;

            //从右到左遍历底部行
            if(top <= bottom){
                for(int i = right; i >= left; i--){
                result.push_back(matrix[bottom][i]);
            }
            bottom--;
            }

            //从下到上遍历左侧列
            if(left <= right){
                for(int i = bottom; i >= top; i--){
                result.push_back(matrix[i][left]);
            }
            left++;
            }

        }
        return result;
    }
};

二、链表

链表理论基础

链表这部分基础不咋地,记录一些基础学习。

链表节点定义:

struct ListNode{
    int val;//节点上存储的元素
    ListNode *next;//指向下一节点的指针
    ListNode(int x) : val(x), next(NULL) {}//节点的构造函数
};

如果不定义构造函数的话,使用默认构造函数,在初始化的时候不能直接赋值。

//通过自己定义构造函数初始化节点
ListNode* head = new ListNode(5);

//通过默认构造函数初始化节点
ListNode* head = new ListNode();
head -> val = 5;

比较一下链表和数组的性能优缺点,以及分别什么时候来使用。 

插入/删除 时间复杂度查询 时间复杂度          适用场景
  数组             O(n)           O(1)数据量固定,频繁查询,较少增删
  链表             O(1)           O(n)数据量不固定,频繁增删,较少查询

记得删除链表节点之后,及时清理内存。

1.【移除链表元素】

 三种做法:1.直接使用原来的链表进行删除操作。但此时要注意如果头节点需要删除怎么办,因为移除头节点和移除其他节点是不一样的,我们删除其他节点是通过其前一个节点来进行删除操作的,而头节点是没有前一个节点的。所以移除头节点就需要将其向后移动一位,那么就要单独写一段逻辑来处理头节点,记得释放内存。

 //直接使用原来的链表进行移除节点操作
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        //删除非头节点
        ListNode* cur = head;
        while(cur != nullptr && cur -> next != nullptr){
            if(cur -> next -> val == val){
                ListNode* tmp = cur -> next;
                cur -> next = cur -> next -> next;
                delete tmp;
            }else{
                cur = cur -> next;
            }
        }

        //删除头节点
        while(head != nullptr && head -> val == val){
            ListNode* tmp = head;
            head = head -> next;
            delete tmp;
        }
        return head;
    }
};
//时间复杂度O(n)
//空间复杂度O(1)

2.创建虚拟头节点 

关键是创建之后,虚拟节点要指向头节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummynode = new ListNode(0);
        dummynode -> next = head;
        ListNode* cur = dummynode;
        while(cur -> next != NULL){
            if(cur -> next -> val == val){
                ListNode* tmp = cur -> next;
                cur -> next = cur -> next -> next;
                delete tmp;
            }else{
                cur = cur -> next;
            }
        }
        head = dummynode -> next;
            delete dummynode;
            return head;
    }
};
//时间复杂度O(n)
//空间复杂度O(1)

3.迭代法,逻辑主要分为三个部分:头节点为空,头节点即为val(新创建一个节点,删除原头节点,令新节点为头节点),除头节点外,其余节点为val

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if(head == nullptr){
            return nullptr;
        }
        if(head -> val == val){
            ListNode* newHead = removeElements(head -> next, val);
            delete head;
            return newHead;
        }else{
            head -> next = removeElements(head -> next, val);
            return head;
        }
    }
};
//时间复杂度O(n)
//空间复杂度O(n)

*2.【设计链表】 

看到这道题,两眼一发黑,链表真是学不懂一点啊。。。看下解析,说是这道题的五个接口涉及了链表的常见五种操作,nice 学!

温故下链表的常见两种操作,直接在原链表上进行操作 or 创建虚拟头节点。一般建议采用创建虚拟头节点。

对这道题的一些理解写到代码注释里面了,确实搞懂这道题之后,对链表不是很畏惧了

class MyLinkedList {
public:
    //定义链表节点结构体
    struct listNode{
        int val;
        listNode* next;
        listNode(int val): val(val), next(nullptr){}
    };

    //初始化链表
    MyLinkedList() {
        _dummyHead = new listNode(0);
        _size = 0;//有效节点数量,即有效索引值为(0, _size - 1)
    }
    
    //获取第index个节点的数值
    int get(int index) {
        //index是非法数值时,返回-1
        if(index < 0 || index > _size - 1){
            return -1;
        }
        //链表的索引查询时间复杂度为O(n)
        listNode* cur = _dummyHead -> next;
        while(index--){
            cur = cur -> next;
        }
        return cur -> val;
    }
    
    //在链表的前面插入一个新的节点
    void addAtHead(int val) {
        listNode* newHead = new listNode(val);
        newHead -> next = _dummyHead -> next;
        _dummyHead -> next = newHead;
        _size++;
    }
    
    //在链表的末尾加入新的节点
    void addAtTail(int val) {
        listNode* newTail = new listNode(val);
        listNode* cur = _dummyHead;
        while(cur -> next != nullptr){
            cur = cur -> next;
        }
        cur -> next = newTail;
        _size++;
    }
    
    //在指定索引位置处加入新的节点
    void addAtIndex(int index, int val) {
        //此时要注意判断index是否为有效索引,并且如果index<0,要令index=0
        if(index > _size) return;
        if(index < 0) index = 0;
        listNode* newNode = new listNode(val);
        listNode* cur = _dummyHead;
        while(index--){
            cur = cur -> next;
        }
        newNode -> next = cur -> next;
        cur -> next = newNode;
        _size++;
    }
    //这里要注意释放内存之后,tmp指针会成为野指针,所以要指定指向空
    void deleteAtIndex(int index) {
        if(index >= _size || index < 0) return;
        listNode* cur = _dummyHead;
        while(index--){
            cur = cur -> next;
        }
        listNode* tmp = cur -> next;
        cur -> next = cur -> next -> next;
        delete tmp;
        tmp = nullptr;
        _size--;
    }
private:
    int _size;          // 链表中有效节点的数量
    listNode* _dummyHead;  // 虚拟头结点指针

};

*3.【反转链表】

逻辑:反转指针方向

法一:双指针

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        ListNode* tmp;
        while(cur){
            tmp = cur -> next;
            cur -> next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};
时间复杂度:O(n)
空间复杂度:O(1)

法二:递归(感觉啰嗦了 等我理解了再来写)

* 4.【两两交换链表中的的节点】

官方给的这个图就很清晰明了,我一开始不太理解为什么要有步骤三,步骤一、二就是将前两个节点交换位置,那么接下来我直接从第三个节点开始处理不就好了吗,为啥还要步骤三呢,后来想清楚,步骤二做完之后,总要指明接下来的链表方向。基本思路就是三个节点为一组。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyNode = new ListNode(0);
        dummyNode -> next = head;
        ListNode* cur = dummyNode;

        //判断条件即为当前节点的下一个节点和下两个节点不为空
        while(cur -> next != nullptr && cur -> next -> next != nullptr){
            ListNode* tmp = cur -> next;
            ListNode* tmp1 = cur -> next -> next -> next;

            cur -> next = cur -> next -> next;//步骤1
            cur -> next -> next = tmp;//步骤2
            cur -> next -> next -> next = tmp1;//步骤3

            cur = cur -> next -> next;//cur移动两位,准备下一轮交换
        }
        ListNode* result = dummyNode -> next;
        delete dummyNode;//释放内存,避免内存泄漏
        return result;
    }
};

——————————————————————————————分割线 2025.2.4

寒假回家磨磨唧唧基本没开始学习,今天俺老孙真的要开始学习啦!务必完成既定任务,2.28完成一刷代码随想录。

*5.【删除链表的倒数第n个结点】

思路:快慢指针,快指针走n+1步后,再和慢指针同时走,此时当快指针指向nullptr时,慢指针指向要删除的节点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyNode = new ListNode(0);
        dummyNode -> next = head;
        ListNode* fast = dummyNode;
        ListNode* slow = dummyNode;

        while(n-- && fast != nullptr){
            fast = fast -> next;//fast走了n步
        }
        fast = fast -> next;//fast多走一步
        while(fast != nullptr){
            fast = fast -> next;
            slow = slow -> next;
        }
        slow -> next = slow -> next ->next;//通过链表指向来删除节点
        return dummyNode -> next;//不return head的原因是,有可能删除头节点
    }
};
时间复杂度:O(n)
空间复杂度:O(1)

 对比一下

6.【链表相交】

思路:遍历完自身之后,回到另一个链表的头节点。题目较为简单,但需注意细节

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* PA = headA;
        ListNode* PB = headB;
        
        if(!headA || !headB){
            return nullptr;
        }

        while(PA != PB){
            PA = (PA == nullptr) ? headB : PA -> next;
            PB = (PB == nullptr) ? headA : PB -> next;
        }

        return PA;
    }
};

*7.【环形链表Ⅱ】

思路:要进行两个判断,1.判断链表是否有环;2.如果有环,如何找到环的入口

对于1.可以用快慢指针来判断是否有环,快指针与慢指针相遇

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(true){
            if(fast == nullptr || fast -> next == nullptr) return nullptr;
            fast = fast -> next -> next;
            slow = slow -> next;
            if(fast == slow) break;
        }
        fast = head;
        while(slow != fast){
            slow = slow -> next;
            fast = fast -> next;
        }
        return fast;

    }
};

 8.【两数相加】

class Solution {
public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
        ListNode *head = nullptr, *tail = nullptr;//head:结果链表的头指针(最终返回),tail用于追踪当前链表的末尾,以便添加新节点
        int carry = 0;//存储进位值
        while(l1 || l2){ //只要l1和l2任意一个不为空
            int n1 = l1 ? l1 -> val : 0; //如果l1存在,取出值
            int n2 = l2 ? l2 -> val : 0;
            int sum = n1 + n2 + carry;
            //构建结果链表
            if(!head){ //head为空说明是第一个节点
                head = tail = new ListNode(sum % 10);//head和tail都指向同一个新创建的节点,后续可以正确添加新节点
            }else{//否则创建新节点并连接到链表
                tail -> next = new ListNode(sum % 10);
                tail = tail -> next;
            }
            carry = sum / 10;
            if(l1){
                l1 = l1 -> next;
            }
            if(l2){
                l2 = l2 -> next;
            }
        }
        if(carry > 0){
            tail -> next = new ListNode(carry);
        }
        return head;
        
        
    }
};

9.【合并两个有序链表】

思路:递归

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(!list1) return list2;
        if(!list2) return list1;

        if(list1 -> val < list2 -> val){
            list1 -> next = mergeTwoLists(list1 -> next, list2);
            return list1;//哪个链表的头节点小,就返回哪个
        }else{
            list2 -> next = mergeTwoLists(list1, list2 -> next);
            return list2;
        }
    }
};

三、哈希表

1.有效的字母异位词

思路:先判断两个字符串长度是否相等,若相等再通过对哈希表的键值增减,最终如果哈希表中的所有键所对应的值都为0时,返回true。

class Solution {
public:
    bool isAnagram(string s, string t) {
        if(s.length() != t.length()) return false;
        unordered_map<char, int> map;
        for(char c : s){
            map[c] += 1;
        }

        for(char c : t){
            map[c] -= 1;
        }

        for(auto kv : map){ // kv 是键值对的意思
            if(kv.second != 0) return false;
        }
        return true;
    }
};

2.两个数组的交集

思路:因为题目里有去重的要求,所以采用集合的方式;

在此辨析一下哈希表map、set和数组的异同

nums_set.find(num) != nums_set.end() 指find返回的迭代器不等于end(),说明num元素在nums_set中找到了。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set;
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for(int num : nums2){
            if(nums_set.find(num) != nums_set.end()){
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};

3.两数之和

思路:题目要求返回数组下标,所以不采用哈希表map、set

暴力解法,两个for循环:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        for(int i = 0; i < nums.size(); i++){
            for(int j = i + 1; j < nums.size(); j++){
                if(target - nums[i] == nums[j]){
                    return {i, j};
                }
            }
        }
        return {};
    }
};

采用哈希表map来解决,每遍历一个,查找一次其对应的目标值是否存在哈希表中,不存在,则加入该值,存在则返回这两个值的下标。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> nums_map;
        for(int i = 0; i < nums.size(); i++){
            if(nums_map.find(target - nums[i]) != nums_map.end()){
                return {i, nums_map.find(target - nums[i])->second};
            }else{
                nums_map.insert(pair<int, int>(nums[i], i));
            }
        }
        return {};
    }
};

*4.三数之和

思路(采用k神做法):1.先将数组排序;2.设三个指针,k为最左(最小)元素的指针,双指针i,j分别设在数组索引(k,len(nums))两端。双指针i,j交替向中间移动,记录满足题意的i,j,k组合。关键点在于,三个指针都要跳过重复值。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res; //存储结果
        if(nums.size() < 3) return res;
        //先排序
        sort(nums.begin(), nums.end());

        //遍历每一个元素,作为第一个指针nums[k]
        for(int k = 0; k < nums.size() - 2; k++){
            //跳过重复的元素,避免重复组合
            //k > 0的条件是为了避免跳过k=0这个元素
            if(k > 0 && nums[k] == nums[k - 1]) continue;

            int i = k + 1, j = nums.size() - 1;
            while(i < j){
                int sum = nums[k] + nums[i] + nums[j];
                if(sum < 0){
                    //如果和小于0,i向右移动
                    i++;
                    //跳过重复的元素
                    while(i < j && nums[i] == nums[i - 1]) i++;
                }else if(sum > 0){
                    //如果和大于0,j向左移动
                    j--;
                    //跳过重复的元素
                    while(i < j && nums[j] == nums[j + 1]) j--;
                }else{
                    //sum == 0, 找到一个解
                    res.push_back({nums[k], nums[i], nums[j]});
                    i++;
                    j--;
                    //跳过重复的元素
                    while(i < j && nums[i] == nums[i - 1]) i++;
                    while(i < j && nums[j] == nums[j + 1]) j--;
                }
            }
        }
        return res;
    }
};

 5.最长连续序列

unordered_set 会自动去重。这样,可以确保只有唯一的元素被存储。

思路:为了方便查找使用set哈希表存储数组,关键是找到连续子序列的起始位置,找到该位置之后向后寻找最长序列。

class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        if(nums.empty()) return 0;
        unordered_set<int> numSet(nums.begin(), nums.end());
        int longestStrak = 0;
        for(int num : numSet){
            //找到连续子序列的起点位置
            if(numSet.find(num - 1) == numSet.end()){
                int currentNum = num;
                int streakNum = 1;

                //向后寻找最长序列
                while(numSet.find(currentNum + 1) != numSet.end()){
                    streakNum++;
                    currentNum++;
                }
                longestStrak = max(streakNum, longestStrak);
            }
            
        }
         return longestStrak;
    }
};

四、字符串

1.反转字符串

思路:遇到反转题目,想双指针,比如之前做过的链表反转。

class Solution {
public:
    void reverseString(vector<char>& s) {
        int i = 0, j = s.size() - 1;
        while(i < j){
            char tmp = s[i];
            s[i] = s[j];
            s[j] = tmp;
            i++;
            j--;
        }
    }
};

2.反转字符串Ⅱ

reverse(start_iterator,end_iterator)用法:start_iterator包含在内,而end_iterator不包含在内。

class Solution {
public:
    string reverseStr(string s, int k) {
        for(int i = 0; i < s.size(); i += 2*k){
            if(i + k <= s.size()){
                reverse(s.begin() + i, s.begin() + i + k);
            }else{
                reverse(s.begin() + i, s.end());
            }
        }
        return s;
    }
};

3.替换数字

思路:很多数组填充类问题,都是先扩充数组到填充后的大小,然后再从后向前填充;这样做避免了两个问题:1.重新申请一个新数组;2.避免了从前向后填充时,需要将添加元素之后的所有元素向后移动的问题。

#include <iostream>
using namespace std;
int main() {
    string s;
    while (cin >> s) {
        int sOldIndex = s.size() - 1;
        int count = 0; // 统计数字的个数
        for (int i = 0; i < s.size(); i++) {
            if (s[i] >= '0' && s[i] <= '9') {
                count++;
            }
        }
        // 扩充字符串s的大小,也就是将每个数字替换成"number"之后的大小
        s.resize(s.size() + count * 5);
        int sNewIndex = s.size() - 1;
        // 从后往前将数字替换为"number"
        while (sOldIndex >= 0) {
            if (s[sOldIndex] >= '0' && s[sOldIndex] <= '9') {
                s[sNewIndex--] = 'r';
                s[sNewIndex--] = 'e';
                s[sNewIndex--] = 'b';
                s[sNewIndex--] = 'm';
                s[sNewIndex--] = 'u';
                s[sNewIndex--] = 'n';
            } else {
                s[sNewIndex--] = s[sOldIndex];
            }
            sOldIndex--;
        }
        cout << s << endl;       
    }
}

*4.反转字符串中的单词

法一:双指针

思路(参考k神):1.倒序遍历字符串s,记录单词左右索引边界i,j;2.每确定一个单词的边界,则将其添加至单词列表res;3.最终将单词列表拼接成字符串,返回

class Solution {
public:
    string reverseWords(string s) {
        int n = s.size();
        vector<string> res;
        int i = n - 1;

        while(i >= 0){
            //跳过空格,寻找单词的结尾
            //外层循环(i >= 0),是从右向左扫描字符串,直到遍历完整个字符串;内层循环(i >= 0)是为了防止越界。
            while(i >= 0 && s[i] == ' '){
                i--;
            }
            if(i < 0) break;
            int j = i;//记录单词的结束位置

            //找到单词的开始位置
            while(i >= 0 && s[i] != ' '){
                i--;
            }

            //提取单词,将其添加到结果列表
            //substr的第二个参数表示字串的长度,而不是索引
            res.push_back(s.substr(i+1, j-i));
        }
        //拼接字符串
        string result;
        for(int k = 0; k < res.size(); k++){
            result += res[k];
            if(k != res.size() - 1){
                result += " ";
            }
        }
        return result;
    }
};

五、栈与队列

1.理论基础

栈和队列是STL(C++标准库)里面的两个数据结构。

【相关C++八股】

1.C++中stack 是容器么?

答:不是,而是一种容器适配器。

2.我们使用的stack是属于哪个版本的STL?

答:SGI STL里面的数据结构。

3.我们使用的STL中stack是如何实现的?

答:如果没有指定底层实现的话,默认是以deque为缺省的情况下栈的底层结构。deque是一个双端队列,封住一端,开通另一端就实现栈的逻辑了。同理,队列底层实现在缺省的情况下也是deque。

【栈】

栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。

栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)

所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。

栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。

我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。

【栈提供了以下基本操作】:

push( )-压栈;pop( )-弹栈;top( )-查看栈顶元素;empty( )-检查栈是否为空;size( )-获取栈的大小

【队列】

队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。 

【堆】

针对上述优先级队列(披着队列外衣的堆),引出堆的概念。

三者的区别与联系:

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

2.有效的括号

思路:1.如果s字符串个数为奇数,一定返回false;2.创建哈希表,把左右括号设为一组键值对,这里要注意哈希表赋初值,需要分号“;”结尾;3.括号匹配问题用栈来实现,所以创建栈;

class Solution {
public:
    bool isValid(string s) {
        int n = s.size();
        if(n % 2 == 1){
            return false;
        }

        unordered_map<char, char> pairs = {
            {')', '('},
            {']', '['},
            {'}', '{'}
        };

        stack<char> stk;
        for(char ch : s){
            //pairs.count(ch)类似于find,只返回0或1
            if(pairs.count(ch)){
                if(stk.empty() || stk.top() != pairs[ch]){
                    return false;
                }
                //栈顶元素是正确的配对括号,所以出栈
                stk.pop();
            }else{
                stk.push(ch);
            }
        }
        return stk.empty();
    }
};

3.删除字符串中的所有相邻重复项

思路:和“有效的括号”本质一样,都是匹配问题,前者匹配括号,后者匹配相邻元素。

栈的目的就是来存放遍历过的元素,当遍历当前元素时,去栈里看看是否遍历过,然后再去消除。

class Solution {
public:
    string removeDuplicates(string s) {
        stack<char> stack;
        string result = "";
        for(int i = 0; i < s.length(); i++){
            if(!stack.empty() && stack.top() == s[i]){
                stack.pop();
            }else{
                stack.push(s[i]);
            }
        }

        while(!stack.empty()){
            result += stack.top();
            stack.pop();
        }
        reverse(result.begin(), result.end());//记得翻转
        return result;
    }
};

不使用显示的栈,直接利用字符串本身模拟栈:

class Solution {
public:
    string removeDuplicates(string s) {
        string result;
        for(char c : s){
            if(!result.empty() && result.back() == c){
                result.pop_back();
            }else{
                result.push_back(c);
            }
        }
        return result;
    }
};

*4.滑动窗口最大值

滑动窗口问题常用双端队列

算法流程(参照k神):

1.初始化:双端队列deque,结果列表res,数组长度n

2.滑动窗口:左边界范围i∈[1−k,n−k],右边界范围j∈[0,n−1];

a.若i>0且 队首元素deque[0] =被删除元素nums[i−1]:则队首元素出队;
b.删除deque内所有<nums[j]的元素,以保持deque递减;
c.将nums[j]添加至deque尾部;
d.若已形成窗口(即i≥0):将窗口最大值(即队首元素deque[0])添加至列表res;

3.返回值:返回结果列表res

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.size() == 0 || k == 0) return{};
        deque<int> deque;
        vector<int> res(nums.size() - k + 1);//预先分配res数组大小
        //i,j的初始值这样设置的目的是为了保证deque.back()一个一个进,从而使得队列是递减的
        for(int i = 1 - k, j = 0; j < nums.size(); i++, j++){
            //删除deque中对应的nums[i - 1]
            if(i > 0 && deque.front() == nums[i - 1]){
                deque.pop_front();
            }
            //保持deque递减
            while(!deque.empty() && deque.back() < nums[j]){
                deque.pop_back();
            }
            deque.push_back(nums[j]);
            //记录窗口最大值
            if(i >= 0){
                res[i] = deque.front();
            }
        }
        return res;
    }
};

5.前k个高频元素

算法流程(三步):

第一步:使用map哈希表统计频率;

*第二步:将频率进行排序

采用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

在 C++ 中,priority_queue 默认是 大顶堆,即堆顶元素是最大值,最小的元素会被保存在队列的底部。而在题目中,我们需要一个 小顶堆min-heap),要求堆顶元素是 频率最小的元素,这样当堆大小超过 k 时,我们可以通过弹出堆顶元素来保证堆中始终存储的是前 k 个频率最高的元素。

为此,我们需要自定义一个 比较器,来修改 priority_queue 的默认排序方式,确保它按照频率的大小来组织堆中的元素。

class mycomparison {
public:
    bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
        return lhs.second > rhs.second;
    }
};

 //定义小顶堆

priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

通过使用比较器来实现 小顶堆 或其他排序规则。

priority_queue 需要三个参数来指定:

元素类型pair<int, int>)。

底层容器类型(通常是 vector<T>)。

比较器(用于指定堆中元素的排序规则)。

  1. pair<int, int>
    • 这是优先队列中元素的类型。因为我们存储的是 pair<int, int> 类型的元素,其中第一个 int 表示数字,第二个 int 表示该数字的出现次数(频率)。在这个例子中,队列的每个元素是一个 pair<int, int>,例如 (数字, 频率)
  2. vector<pair<int, int>>

    • 这是底层容器类型。priority_queue 在内部通常使用一个 动态数组(通常是 vector)来存储元素。这里,vector<pair<int, int>> 表示我们使用 vector 来存储 pair<int, int> 类型的元素。你也可以使用其他容器,如 deque,但通常 vector 是最常用的选择,因为它可以高效地动态扩展大小。
  3. mycomparison

    • 这是一个自定义的比较器。priority_queue 默认是一个大顶堆(大元素排在堆顶),但是在这里,我们需要一个小顶堆(小元素排在堆顶)。通过传递自定义的比较器 mycomparison,我们告诉 priority_queue 如何排序堆中的元素。mycomparison 是一个类,它重载了 operator() 来定义元素的排序规则。在这个例子中,mycomparison 会使得优先队列根据 pair 的第二个元素(即频率)来排序。

要遍历 unordered_map 中的所有元素,我们需要使用 迭代器iterator 是指向容器中某个元素的指针,可以通过它来访问容器中的每个元素。:: 是在声明迭代器类型时使用。

unordered_map<int, int>::iterator it = map.begin();

这个部分是用来定义一个迭代器(iterator),并将它初始化为 unordered_map 的开始位置。

for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
    pri_que.push(*it);  // 将每个元素(数字和频率)插入到优先队列中
    if (pri_que.size() > k) { // 如果堆的大小超过了k
        pri_que.pop();  // 弹出堆顶元素(频率最小的元素)
    }
}

 插入时,mycomparison 比较器 会被调用。这个比较器会根据 频率 排序,因此优先队列会保持一个 小顶堆(堆顶元素是频率最小的元素)。

第三步:找出前k个高频元素

构建小堆顶,将所有频率入堆,如果堆的大小大于k了,将元素从堆顶弹出

class Solution {
public:
    // 小顶堆
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 要统计元素出现频率
        unordered_map<int, int> map; // map<nums[i],对应出现的次数>
        for (int i = 0; i < nums.size(); i++) {
            map[nums[i]]++;
        }

        // 对频率排序
        // 定义一个小顶堆,大小为k
        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

        // 用固定大小为k的小顶堆,扫面所有频率的数值
        for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++) {
            pri_que.push(*it);
            if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k
                pri_que.pop();
            }
        }

        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> result(k);
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pri_que.top().first;
            pri_que.pop();
        }
        return result;

    }
};

6.用栈实现队列 

思路:需要两个栈,一个输入、一个输出。

六、二叉树

1.理论基础

二叉树题目分类

 【二叉树的种类】

  • 满二叉树:一颗二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层。深度为k,有2^k-1个节点的二叉树。
  • 完全二叉树:除了最底层节点可能没被填满,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
  • 二叉搜索树:一个有序数。左子树小于根节点值,右子数大于根节点值。
  • 平衡二叉搜索树:是一颗空树或者它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树。

【二叉树的存储方式】

链式存储:指针。通过指针把分布在各个地址的节点串连在一起。

顺序存储:数组。内存连续分布。

【二叉树的遍历方式】

1.深度优先遍历:先往深走,遇到叶子节点再往回走。

借助栈使用递归的方式来实现。

  • 前序遍历(递归法,迭代法)
  • 中序遍历(递归法,迭代法)
  • 后序遍历(递归法,迭代法)   这里的“前中后”指的是中间节点的位置

2.广度优先遍历:一层一层的去遍历。

借助队列先进先出的特点来实现。

  • 层次遍历(迭代法)

【二叉树的定义】

顺序存储即是用数组来存,这里主要讨论链式存储的二叉树节点的定义方式。

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

在此,对比一下链表定义,相对于链表,二叉树节点里多了一个指针

struct ListNode{
    int val;//节点上存储的元素
    ListNode *next;//指向下一节点的指针
    ListNode(int x) : val(x), next(NULL) {}//节点的构造函数
};

2.二叉树的递归遍历

递归三要素:

1>确定递归函数的参数和返回值;

2>确定终止条件;操作系统用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然会溢出。

3>确定单层递归的逻辑

例:前序遍历

class Solution {
public:
    void travelsal(TreeNode* cur, vector<int>& vec){
        if(cur == NULL) return;
        vec.push_back(cur -> val);
        travelsal(cur -> left, vec);
        travelsal(cur -> right, vec);
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        travelsal(root, res);
        return res;
    }
};

3.二叉树的层序遍历

广度优先搜索(BFS)通常借助队列的先入先出特性来实现。

算法流程:

1.特例处理:当根节点为空,返回空列表[ ];

2.初始化:打印结果列表res = [ ],包含根节点的队列 queue = [root]。

3.BFS循环:queue为空时跳出

  a.临时列表tmp,存储当前层打印结果

  b.当前层打印循环:循环次数为当前节点数(queue长度)

  c.将当前层结果tmp添加入res。

4.返回值:返回打印结果res

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> res;
        if(root != nullptr) que.push(root);//先把根节点入队
        while(!que.empty()){
            vector<int> tmp;//存储当前层的节点值
            for (int i = que.size(); i > 0; i--) {//遍历当前层的所有节点
                Treenode* node = que.front();// 获取队列的第一个节点
                que.pop();            // 弹出队列的第一个节点
                tmp.push_back(node->val);  // 将当前节点的值添加到当前层的结果中
                if (node->left != nullptr) que.push(node->left); // 如果有左子树,入队
                if (node->right != nullptr) que.push(node->right);// 如果有右子树,入队
            }
            res.push_back(tmp);
        }
        return res;
    }
};

【二叉树的最大深度】

vector 可以临时存储当前层节点,在每一层遍历结束后,可以直接更新 que。它不需要像 queue 那样每次进行 pop() 操作,这样代码更加简洁和直接。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if(root == nullptr) return 0;
        vector<TreeNode*> que;
        que.push_back(root);
        int res = 0;
        while(!que.empty()){
            vector<TreeNode*> tmp;
            for(TreeNode* node : que){
                if(node -> left != nullptr) tmp.push_back(node -> left);
                if(node -> right != nullptr) tmp.push_back(node -> right);
            }
            que = tmp;//由tmp更新que
            res++;
        }
        return res;
    }
};

【二叉树的右视图】

思路:和层序遍历的逻辑类似,只需要判断当前指针是否是该层的最后一个元素,是的话放进result数组。

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        queue<TreeNode*> que;
        if(root != nullptr) que.push(root);
        vector<int> result;
        while(!que.empty()){
            int size = que.size();
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                if(i == (size - 1)) result.push_back(node -> val);
                if(node -> left) que.push(node -> left);
                if(node -> right) que.push(node -> right);
            }
        }
        return result;
    }
};

4.【翻转二叉树】

思路:交换每一个节点的左右孩子。

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(root == nullptr) return root;
        swap(root -> left, root -> right);
        invertTree(root -> left);
        invertTree(root -> right);
        return root;
    }
};

5.【对称二叉树】

递归法

递归三要素:1.递归函数的参数和返回值

因为要判断的是这个数是否是对称的,所以参数是左子树和右子树节点,返回值是bool类型

bool compare(Treenode* left, Treenode* right)

2.确定终止条件

if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else

3.确定单层递归的逻辑

单层递归的逻辑就是处理左右节点都不为空,且数值相同的情况。

  • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
  • 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
  • 如果左右都对称就返回true ,有一侧不对称就返回false 。
bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
bool isSame = outside && inside;                    // 左子树:中、 右子树:中(逻辑处理)
return isSame;

最终代码:

class Solution {
public:
    bool compare(TreeNode* left, TreeNode* right) {
        // 首先排除空节点的情况
        if (left == NULL && right != NULL) return false;
        else if (left != NULL && right == NULL) return false;
        else if (left == NULL && right == NULL) return true;
        // 排除了空节点,再排除数值不相同的情况
        else if (left->val != right->val) return false;

        // 此时就是:左右节点都不为空,且数值相同的情况
        // 此时才做递归,做下一层的判断
        bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
        bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
        bool isSame = outside && inside;                    // 左子树:中、 右子树:中 (逻辑处理)
        return isSame;

    }
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        return compare(root->left, root->right);
    }
};

6.【完全二叉树节点个数】

方法一:对层序遍历的模板稍作修改,统计节点个数。

class Solution {
public:
    int countNodes(TreeNode* root) {
        queue<TreeNode*> que;
        if(root != nullptr) que.push(root);
        int count = 0;
        while(!que.empty()){
            int size = que.size();
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                count++;
                if(node -> left) que.push(node -> left);
                if(node -> right) que.push(node -> right);
            }
        }
        return count;
    }
};

方法二:递归

确定返回参数和终止条件,确定递归逻辑,最终代码:

class Solution {
public:
    int getCount(TreeNode* cur){
        if(cur == nullptr) return 0;
        int leftNum = getCount(cur -> left);
        int rightNum = getCount(cur -> right);
        int count = leftNum + rightNum + 1;
        return count;
    }

    int countNodes(TreeNode* root) {
        if(root == nullptr) return 0;
        return getCount(root);
    }
};

7.平衡二叉树

class Solution {
public:
    //如果返回-1 则说明该节点不是平衡二叉树
    int balanced(TreeNode* cur){
        //如果当前根节点为空,则表示当前根节点的树高度为0
        if(cur == nullptr) return 0;
        int leftHeight = balanced(cur -> left);
        if(leftHeight == -1) return -1;
        int rightHeight = balanced(cur -> right);
        if(rightHeight == -1) return -1;
        //+1的原因是加上当前根节点
        return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);

    }

    bool isBalanced(TreeNode* root) {
        return balanced(root) == -1 ? false : true;
    }
};

8.二叉树的所有路径

思路:递归+回溯,回溯的原因:返回上一步根节点,继续遍历。

递归参数:节点值、遍历过的路径、存放结果集的result

返回类型:此处递归,不需要返回值

递归终止条件:

if(cur == nullptr){
            //处理递归逻辑
        }

通用递归结束条件可以这么写,但是本题递归条件应为,判断是否为叶子节点,为啥不用判断当前节点是否为空,因为后续逻辑能够使得空节点不入循环。

这里介绍一个函数to_string(),它是C++ 标准库中的一个函数,定义在 <string> 头文件中。它是从 C++11 标准开始引入的,用于将数值类型(如 intdoublelong 等)转换为string 类型。

开始动手写递归逻辑:

1.利用vector<int>的path容器,记录路径,则终止处理逻辑如下:

void versal(TreeNode* cur, vector<int>& path, vector<string>& result){
        path.push_back(cur -> val);
        if(cur -> left == nullptr && cur -> right == nullptr){
            string spath;
            //这里减1的原因是,最后一个path不需要箭头连接,所以单独处理
            for(int i = 0; i < path.size() - 1; i++){
                spath += to_string(path[i]);
                spath += "->";
            }
            spath += to_string(path[path.size() - 1]);//记录最后一个节点(叶子节点)
            result.push_back(spath);
            return;
        }
    }

 处理完叶子节点之后,回溯到上一个节点,这里明确一点,一个递归对应一个回溯。

if(cur -> left){
    versal(cur -> left, path, result);
    path.pop_back();
}
if(cur -> right){
    versal(cur -> right, path, result);
    path.pop_back();
}

整体代码:

class Solution {
public:
    void versal(TreeNode* cur, vector<int>& path, vector<string>& result){
        path.push_back(cur -> val);
        if(cur -> left == nullptr && cur -> right == nullptr){
            string spath;
            //这里减1的原因是,最后一个path不需要箭头连接,所以单独处理
            for(int i = 0; i < path.size() - 1; i++){
                spath += to_string(path[i]);
                spath += "->";
            }
            spath += to_string(path[path.size() - 1]);//记录最后一个节点(叶子节点)
            result.push_back(spath);
            return;
        }
        if(cur -> left){
            versal(cur -> left, path, result);
            path.pop_back();
        }
        if(cur -> right){
            versal(cur -> right, path, result);
            path.pop_back();
        }
    }

    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        vector<int> path;
        if(root == nullptr) return result;
        versal(root, path, result);
        return result;
    }
};

9.左叶子之和

class Solution {
public:
    int traversal(TreeNode* cur){
        if(cur == nullptr) return 0;
        //递归终止条件,递归逻辑
        if(cur -> left && !cur -> left -> left && !cur -> left -> right){
            return cur -> left -> val + traversal(cur -> right);
        }
        int leftVal = traversal(cur -> left);
        int rightVal = traversal(cur -> right);
        return leftVal + rightVal;
       
    }

    int sumOfLeftLeaves(TreeNode* root) {
        return traversal(root); 
    }
};

10.找树左下角的值

思路:层序遍历,输出最后一层第一个元素。此外,做了很多关于层序遍历的题目后,发现根据输出的值不同,往往做变化的都是que.pop()这一句之后。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        queue<TreeNode*> que;
        if(root) que.push(root);
        int res = 0;
        while(!que.empty()){
            int size = que.size();
            vector<int> tmp;
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                if(i == 0) res = node -> val;
                if(node -> left) que.push(node -> left);
                if(node -> right) que.push(node -> right);
            }
        }
        return res;
    }
};

七、回溯

递归和回溯的区别:

递归即是自己调用自己,回溯是枚举所有的可能,不满足条件的时候,撤销选择。

回溯是一个算法思想,递归是一种实现方式

回溯三部曲:

1.回溯返回值以及参数,返回值一般是void,参数很难一次性确定,所以一般先写逻辑,然后需要什么参数写什么。

2.回溯函数终止条件。

3.回溯搜索的遍历过程:回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度。

回溯算法模板框架

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

 1.组合问题

思路:组合问题可抽象为树形结构

可以看出,这棵树一开始集合是1,2,3,4,从左向右取数,取过的数,不再重复取。

n相当于树的宽度,k相当于树的深度。

因此在这棵树上遍历从而收集我们要的结果:每次搜索到了叶子节点,就找到了一个结果。

class Solution {
private:
    vector<vector<int>> result;//用来存放结果
    vector<int> tmp;//用来存放符合条件的结果
    void backTracking(int n, int k, int startIndex){
        //for循环每次从startIndex开始
        //回溯终止条件
        if(tmp.size() == k){
            result.push_back(tmp);
            return;
        }
        for(int i = startIndex; i <= n; i++){
            tmp.push_back(i);//处理结点
            backTracking(n, k, i + 1);//递归
            tmp.pop_back();//撤销处理的节点
        }
    }

public:
    vector<vector<int>> combine(int n, int k) {
        backTracking(n, k, 1);
        return result;
    }
};

 2.组合总和三

class Solution {
private:
    vector<vector<int>> result;
    vector<int> tmp;
    void backTracking(int k, int targetSum, int startIndex, int curSum){
        if(tmp.size() == k){
            if(curSum == targetSum) result.push_back(tmp);
            if(tmp.size() == k) 但是curSum != targetSum 直接返回
            return;
        }
        for(int i = startIndex; i <= 9; i++){
            curSum += i;
            tmp.push_back(i);
            backTracking(k, targetSum, i + 1, curSum);
            curSum -= i;
            tmp.pop_back();

        }
    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(k, n, 1, 0);
        return result;
    }
};

3.子集

思路:

class Solution {
//基于数组的索引,因而startIndex是从0开始的
private:
    vector<vector<int>> result;
    vector<int> tmp;
    void backTracking(vector<int>& nums, int startIndex){
        result.push_back(tmp);
        //终止条件为,startIndex的长度已经大于数组了,所以不用写,因为此时本层for循环,本来也结束了
        for(int i = startIndex; i < nums.size(); i++){
            tmp.push_back(nums[i]);
            backTracking(nums, i + 1);
            tmp.pop_back();
        }
    }

public:
    vector<vector<int>> subsets(vector<int>& nums) {
        backTracking(nums, 0);
        return result;
    }
};

 4.全排列

class Solution {
public:
    vector<vector<int>> result;
    vector<vector<int>> permute(vector<int>& nums) {
        dfs(nums, 0);
        return result;
    }

    void dfs(vector<int>& nums, int x){
        if(x == nums.size() - 1){//递归终止条件
            result.push_back(nums);//存储排列结果
            return;
        }
        for(int i = x; i < nums.size(); i++){
            swap(nums[i], nums[x]);//选择:交换元素
            dfs(nums, x + 1);//递归:固定当前元素,排列剩下的
            swap(nums[i], nums[x]);//回溯:撤销选择,恢复数组
        }
    }
};

八、贪心算法

1.贪心算法理论基础

贪心算法一般分为如下四步:

1.将问题分解为若干个子问题;

2.找出适合的贪心策略;

3.求解每一个问题的最优解;

4.将局部最优解堆叠成全局最优解。

实际做题,只要能够由局部最优推导出全局最优,就够用。

2.分发饼干

思路:该题局部最优就是大饼干喂给胃口大的,全局最优就是喂饱尽可能多的小孩。尝试使用贪心策略,先将饼干数组和小孩数组进行排序,然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int result = 0;
        int sIndex = s.size() - 1;//饼干数组下标
        //遍历小孩数组
        for(int i = g.size() - 1; i >= 0; i--){
            if(sIndex >= 0 && s[sIndex] >= g[i] ){
                result++;
                sIndex--;
            }
        }
        return result;
    }
};

3.摆动序列

思路:看完答案感觉还挺简单,逻辑就是,当数组长度小于等于1的时候直接返回数组长度,其余条件就是判断是否前向差值和后向差值相反。

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        int curDiff = 0; // 当前一对差值
        int preDiff = 0; // 前一对差值
        int result = 1;  // 记录峰值个数,序列默认序列最右边有一个峰值
        for (int i = 0; i < nums.size() - 1; i++) {
            curDiff = nums[i + 1] - nums[i];
            // 出现峰值
            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
                result++;
                preDiff = curDiff; // 注意这里,只在摆动变化的时候更新prediff
            }
        }
        return result;
    }
};

4.买卖股票的最佳时机 2

注意:想要获得利润,至少两天为一个交易单元。

思路:看完解析,这题真的又好简单,我又懂了。

就是统计所有正利润之和。即,局部最优:收集每天的正利润;全局最优:求得最大利润。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for(int i = 1; i < prices.size(); i++){
            if(prices[i] - prices[i - 1] > 0){
                result += prices[i] - prices[i - 1];
            }
        }
        return result;
    }
};

5.跳跃游戏

思路:局部最优解:每次取最大跳跃步数;整体最优解:是否能到达终点。

class Solution {
public:
    bool canJump(vector<int>& nums) {
        if(nums.size() <= 1) return true;
        int cover = 0;
        for(int i = 0; i <= cover; i++){
            cover = max(nums[i] + i, cover);
            if(cover >= nums.size() - 1) return true;
        }
        return false;
    }
};

6.跳跃游戏2

没看答案,自己的思路:从后向前遍历,先找到能跳跃到最后一个元素的下标,在该下标的基础上再去寻找能跳跃到此下标的下标。

发现自己的思路有问题,从后向前遍历的话,可能先遇到了一个能跳到最后一个元素的下标,但是该下标没有元素能跳过来。

class Solution {
public:
    int jump(vector<int>& nums) {
        int curDistance = 0;    // 当前覆盖的最远距离下标
        int ans = 0;            // 记录走的最大步数
        int nextDistance = 0;   // 下一步覆盖的最远距离下标
        for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1,这是关键所在
            nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖的最远距离下标
            if (i == curDistance) {                 // 遇到当前覆盖的最远距离下标
                curDistance = nextDistance;         // 更新当前覆盖的最远距离下标
                ans++;
            }
        }
        return ans;
    }
};

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

思路:局部最优:让绝对值大的负数变为正数;整体最优:整个数组和达到最大。

特殊情况:负数都转变为正数了,k依然大于0,此时的问题是一个有序正整数序列,如何转变k次正负,让数组和达到最大

算法步骤:1.将数组按照绝对值大小从大到小进行排序;

2.从前向后遍历,遇到负数将其变为正数,同时k--;

3.如果k依旧大于0, 反复转变数值最小的元素,将k用完;

4.求和

class Solution {
    static bool cmp(int a, int b){
        return abs(a) > abs(b);
    }
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        int sum = 0;
        //1.按照绝对值大小排序,从大到小
        sort(nums.begin(), nums.end(), cmp);
        //2.从前向后遍历,遇到负数则将其变为正数,同时k--
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] < 0 && k > 0){
                nums[i] = -nums[i];
                k--;
            }
        }
        //3.如果k依旧大于0
        if(k > 0){
            int n = nums.size() - 1;
            if(k % 2 == 1){
                nums[n] = -nums[n];
            }
        }
        //4.求和
        for(int i = 0; i < nums.size(); i++){
            sum += nums[i];
        }
        return sum;
    }
};

九、动态规划

1.动态规划理论基础

动态规划(Dynamic Programming,简称DP),如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来的。有别于贪心:局部直接选最优。

动态规划解题步骤(五步):

1.确定dp数组(dp table)以及下标的含义

2.确定递推公式

3.dp数组如何初始化

4.确定遍历顺序

5.举例推导dp数组

debug过程:

1.模拟状态转移在dp数组上的情况;

2.如果代码没通过,打印dp数组,看看和自己预先推导的哪里不一样;

3.如果预先模拟一样,就是递归公式、初始化、遍历顺序的问题。否则,代码实现细节问题。

2.斐波那契数

暴力解法

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值