Leetcode算法题笔记(1)

目录

哈希

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

1.1 解法1

依次取第i个数与第j个数相加,判断是否等于目标值,若相等则返回索引号i,j。两个for循环嵌套,时间复杂度O(n^2)。

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

1.1 解法2

索引数组每个元素,每次从哈希表中寻找target与该元素的差值。若不存在则将元素值作为key,索引号作为value放入哈希表中;若存在则返回当前元素的索引号i和找到的对应key的value。由于只需要遍历一次数组,时间复杂度为O(n)。

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

2. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
在这里插入图片描述

2.1 解法1

采用排序法、哈希表。字母异位词排序后的字符串一定相等,可以将其作为key,value为存放key对应的所有字母异位词(每发现一个符合条件的新字母异位词,则在value中添加该元素),之后遍历哈希表即可得到分组。

//采用unordered_map
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> mp;
        for (string& str: strs) {
            string key = str;
            sort(key.begin(), key.end());
            mp[key].emplace_back(str);
        }
        vector<vector<string>> ans;
        for (auto it = mp.begin(); it != mp.end(); ++it) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};

//or
//采用unordered_multimap
class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_multimap<string, vector<string>> mp;
        for (string& str: strs) {
            string key = str;
            
            sort(key.begin(), key.end());
            //mp[key].emplace_back(str);
            //vstr = {str};
            auto it = mp.find(key);
            if(it != mp.end())
            {
                it->second.push_back(str);
            }
            else
            {
                vector<string> v;
                v.push_back(str);
                mp.emplace(key, v);
            }
            
        }
        vector<vector<string>> ans;
        for (auto it = mp.begin(); it != mp.end(); ++it) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};


2.2 解法2

采用计数法、哈希表。记录每个词每个字母的出现频次在一个26大小的数组中,可以将数组作为key,value为存放key对应的所有字母异位词(每发现一个符合条件的新字母异位词,则在value中添加该元素),之后遍历哈希表即可得到分组。由于C++ 哈希表不知道如何计算26数组的哈希值,因此还需传入一个hash function(函数对象,lambda表达式)。

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        // 自定义对 array<int, 26> 类型的哈希函数
        auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
            return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
                return (acc << 1) ^ fn(num);
            });
        };

        unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
        for (string& str: strs) {
            array<int, 26> counts{};
            int length = str.length();
            for (int i = 0; i < length; ++i) {
                counts[str[i] - 'a'] ++;
            }
            mp[counts].emplace_back(str);
        }
        vector<vector<string>> ans;
        for (auto it = mp.begin(); it != mp.end(); ++it) {
            ans.emplace_back(it->second);
        }
        return ans;
    }
};

3. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
在这里插入图片描述

3.1 解法

  1. 采用基于哈希表实现的容器unordered_set(对元素不继续排序,key需不同)存储所有整数,可以去除重复元素。
  2. 之后遍历容器中整数,依次查看当前整数是否为一个序列的开头整数,即判断x-1是否存在于容器中。如果存在,则跳过,并遍历下一个整数;如果不存在,则说明该元素是一个序列的首元素,则开始依次判断x+1,x+2…是否存在,同时记录序列最长长度
  3. 容器unordered_map是基于哈希表实现,插入与判断key是否存在均为O(1),外层遍历元素次数为n,每个整数被外层循环访问了一次;内部中对于单个无连续的整数访问一次(因为x+1不存在),对于存在连续的整数序列也仅对序列中每个整数从小到大遍历一次,所以外层循环内部总共也只访问了n次。总共2n次,所以时间复杂度O(n) 。
class Solution {
public:
    int longestConsecutive(vector<int>& nums) {
        unordered_set<int> num_set;
        for (const int& num : nums) {
            num_set.insert(num);
        }

        int longestStreak = 0;

        for (const int& num : num_set) {
            if (!num_set.count(num - 1)) {
                int currentNum = num;
                int currentStreak = 1;

                while (num_set.count(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                longestStreak = max(longestStreak, currentStreak);
            }
        }

        return longestStreak;           
    }
};

小结

针对算法实现过程中需要插入、查找或判断元素是否存在的功能,可以考虑基于哈希表实现的容器unordered_map,unordered_set。unordered_map需要选择合适的数据分别作为key和vaule。unordered_set仅需要一个key即可,主要也是为了迅速判断key是否存在。

双指针

4. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
在这里插入图片描述

4.1 解法1

遍历数组中每个元素,并记录前方已经出现了几个0元素,以便明确后续非0元素需要往前移几位。首先判断该元素是否为0,如果为0则0元素计数加一;如果不为0,则按照之前出现的0个数zeronum将当前非0元素前移zeronum位,并将该位置0(当遍历到第一个元素时,非0元素需进行移动;前面0个数为0时,非0元素也无序移动)。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int zeronum = 0;
        int i = 0;
        for(auto& num:nums)
        {
            if(num == 0)
            {
                zeronum += 1;
            }
            else
            {
                if(i !=0 && zeronum != 0 ) 
                {
                    nums[i-zeronum] = num;
                    num = 0;
                }
            }
            i += 1;
        }

    }
};

4.2 解法2

使用双指针,左指针指向当前已经处理好的序列的尾部,右指针指向待处理序列的头部。只有当右指针所指元素非0,则需要将该元素放到已处理好的序列尾部(交换)。

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size(), left = 0, right = 0;
        while (right < n) {
            if (nums[right]) {
                swap(nums[left], nums[right]);
                left++;
            }
            right++;
        }
    }
};

5. 盛最多水的容器

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器
在这里插入图片描述

5.1 解法一

采用双for循环,依次计算出两垂线之间的容水体积,并找出最大值。虽然通过比较当前最大容量与当前左垂线的最大容量潜力减少不必要的计算,但双for循环最坏情况依然需要n*n。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int max_capacity = 0;
        int n = height.size();
        int tmp;
        for(int i = 0; i < n-1 ; i++)
        {
            if(max_capacity > height[i]*(n-i-1)) continue;
            for(int j = i+1; j < n ; j++)
            {
                
                tmp = min(height[i],height[j]) * ( j - i );
                if( tmp > max_capacity )
                {
                    max_capacity = tmp;
                }
            }
        }
        return max_capacity;

    }
};

5.2 解法二

采用双指针分别指向数组两端,每次让短板一端的指针往中间收拢一格,依次计算两指针所指垂线间的容水体积并保留最大值,总共只用遍历一次数组。该方法关键在于,向内移动短板,容水体积可能增大也可能减小;向内移动长板,容水体积必然减小(因为容水体积主要由短板决定,向内移动长板后,短板可能不变或者变得更短,同时两板距离必然缩短,所以容水体积必然缩短)。

class Solution {
public:
    int maxArea(vector<int>& height) {
        int i = 0, j = height.size() - 1, res = 0;
        while(i < j) {
            res = height[i] < height[j] ? 
                max(res, (j - i) * height[i++]): 
                max(res, (j - i) * height[j--]); 
        }
        return res;
    }
};

6. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
在这里插入图片描述

6.1 解法1

直接用三层for循环,每次取其中一个元素求和再判断。由于可能存在多个相同的三元组,因此还需要去重。时间复杂度O(n^3).

6.2 解法2

采用排序+双指针。为了便于实现取得不重复的三元组,对数组先进行排序,从而保证后续三元组(a,b,c)中a <= b <= c ,就不会出现(c,a,b)、(b,c,a)…重复三元组。其次排序后可以利用有序的特性,结合双指针减少计算次数。(当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从O(n^2)减少至 O(n)。使用双指针有一个必要的步骤就是左右指针碰面时即停止,使得n个元素只被遍历n次)。固定第一个参数a,左指针元素为b,右指针元素为c(一开始指向尾端),a+b+c>0,则右移,重复至a+b+c<=0,此时右指针不用回退,由于左指针下一个遍历元素一定大于等于前一个遍历的元素,所以a+b+c必然比之前大,此时右指针要么不动,要么往左移动(三者之和大于0),直至左指针右指针碰面,结束第二层for循环。此外,还需注意若前一个b和后一个b相等,则无需重复遍历记录三元组。总时间复杂度为O(n^2)。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // 如果指针重合,随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if (second == third) {
                    break;
                }
                if (nums[second] + nums[third] == target) {
                    ans.push_back({nums[first], nums[second], nums[third]});
                }
            }
        }
        return ans;
    }
};

7. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
在这里插入图片描述

7.1 解法1

class Solution {
public:
    int trap(vector<int>& height) {
        int i=0,j=1,k=0;
        int h_size = height.size();
        int tmp = 0;
        int capacity = 0;
        while(j < h_size)
        {
            if(height[i] == 0)
            {
                i++;
                j++;
            }
            
            if( ((j-i) == 1) || (height[j] < height[i]) )
            {
                if( height[j] >= height[i] )
                {
                    i = j;
                    tmp = 0;
                }
                else
                {
                    tmp += height[j];
                }
                j++;
            }
            else
            {
                capacity = height[i]*(j-i-1)-tmp +capacity;
                tmp = 0;
                i = j;
                j++;
            }

        }
        if(tmp != 0)
        {
            tmp = 0;
            k = j-1; 
            j = j-2;
            // i = i-1;
            while(k != i)
            {
                if(height[k] == 0)
                {
                    k--;
                    j--;
                }

                if( ((k-j) == 1) || (height[j] < height[k]) )
                {
                    if( height[j] >= height[k] )
                    {
                        k = j;
                        tmp = 0;
                    }
                    else
                    {
                        tmp += height[j];
                    }
                    j--;
                }
                else
                {
                    capacity = height[k]*(k-j-1)-tmp +capacity;
                    tmp = 0;
                    k = j;
                    j--;
                }

            }
        }
        return capacity;
    }
};

小结

双指针主要用于需要遍历两次时,左右指针分别指向头尾,两者依次往中间靠拢,可以找到某一规律(例如计算规律)使得两个指针不用回溯,直至两者碰面结束。将遍历两次的复杂度O(n^2)降至O(n)。

滑动窗口

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

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:

输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:

输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

8.1 解法1

采用哈希表记录已出现且不重复的字符,key为每个字符,value为每个不同字符当前最大的索引号(方便移动指针到刚好除去旧的重复字符的位置)。采用双指针,一个指向当前搜索子串的前一个位置,一个指向当前搜索子串字符的最后一个字符位置(一直从头找到尾)。当有重复字符出现,下一个搜索的子串开头则是该重复字符上次出现的位置的后一个位置。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> dic;
        int i = -1, res = 0, len = s.size();
        for(int j = 0; j < len; j++) {
            if (dic.find(s[j]) != dic.end())
                i = max(i, dic.find(s[j])->second); // 更新左指针
            dic[s[j]] = j; // 哈希表记录,保证当有重复字符出现时,i能直接指到重复字符上次出现的位置
            res = max(res, j - i); // 更新结果
        }
        return res;
    }
};

9. 找到字符串中所有字母异位词

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:

输入: s = “cbaebabacd”, p = “abc”
输出: [0,6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的异位词。
示例 2:

输入: s = “abab”, p = “ab”
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。

9.1 解法一

时间超出限制。

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        unordered_map<char,int> mp;
        vector<int> result;
        int i = 0, psize = p.size();
        int ssize = s.size();
        if(psize > ssize) return result;
        //将p中的字符存储至哈希表中,key为单个字符,value为该字符出现的次数
        while(i < psize )
        {
            if(mp.find(p[i]) == mp.end())
                mp[p[i]] = 1;
            else
                mp[p[i]] += 1; 
            i++;
        }
        unordered_map<char,int> tmpmp;
        for(int j = 0; j < ssize - psize + 1; j++)
        {
            tmpmp = mp;
            for(i = 0; i < psize; i++)
            {
                if(tmpmp.find(s[j+i]) != tmpmp.end())
                {
                    if(tmpmp[s[j+i]] == 0)
                    {
                        break;
                    }
                    else
                    {
                        tmpmp[s[j+i]] -= 1;
                    }
                }
                else
                {
                    j = j + i;
                    break;
                }
            }
            if(i == psize)
            {
                result.push_back(j);
            }
        }
        return result;

    }
};

9.2 解法二

  • 用一个数组记录p字符串中26个字母出现频次(记录每个字符还需要几个,为0则表示刚好满足条件且不需要,负数则表示已经出现的太多了),采用左右指针分别指向搜索s字符串的当前子串的头和尾,整数count记录剩余所需继续匹配字符个数。
  • 左右指针均先指向s的首个字符,右指针依次往右移动,同时对应的字符在数组中的频次减1(仍待匹配的字符频次为正数,无需匹配的字符频次为非正数),每次检查右指针所指字符是否在p串中(即p中对应的字符频次大于0),若在则说明找到匹配的字符,则count–。
  • 当count为0时,则说明当前字串已经完全匹配p,返回子串首位置。
  • 当已经核验过的字符数量与p串的字符数量相等时,搜索窗口已经到达最大,因此需要收缩一下窗口,则左指针需要往右移,丢掉前面的一个字符,对应的也需增加被丢掉字符的频次。如果被丢掉字符所需要匹配的频次大于等于0(只有是p串中出现的字符,频次才会有可能大于等于0(可能为负数则表明该字符本应该只需要n次,子串中却已经有n+1个,则直接丢掉即可),未出现在p串中的字符一定是负数(因为前面会对索引过的字符频次进行减一,0-1=-1)),则count需要加1。由于对每个被右指针索引过的字符频次均会被减1,因此丢掉该字符时,频次均需要加1。
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int sLen = s.length(), pLen = p.length();

        vector<int> result;
        if (sLen < pLen) {
            return result; // 如果输入为空或者s的长度小于p的长度,则直接返回空结果
        }
        
        int pFreq[26] = {0}; // 数组记录字符出现频率,长度为26表示小写字母的26个字符
        
        // 记录p中每个字符的出现次数
        for (char c : p) {
            pFreq[c - 'a']++; // 将字符映射到数组的索引,增加出现次数
        }
        
        int left = 0, right = 0, count = pLen; // 初始化左右指针和计数器
        
        while (right < sLen) {
            // 步骤一
            if (pFreq[s[right] - 'a'] > 0) {
                count--; // 如果当前字符在p中存在,则减少count计数
            }
            pFreq[s[right] - 'a']--; // 更新频率数组,出现过的字符频率值-1
            right++; // 右指针向右移动
            
            // 步骤二 
            if (count == 0) {
                result.push_back(left); // 如果找到一个异位词,记录起始索引
            }
            
            // 步骤三
            if (right - left == pLen) { // 当窗口大小等于p的长度时
                if (pFreq[s[left] - 'a'] >= 0) {
                    count++; // 如果左边界对应字符在p中,则增加count计数
                }
                pFreq[s[left] - 'a']++; // 恢复频率值
                left++; // 左指针向右移动
            }
        }
        
        return result; // 返回结果
    }
};

子串

10 和为k的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。

子数组是数组中元素的连续非空序列。
在这里插入图片描述

解法1

暴力求解法,采用双for循环,遍历每一个子数组,看是否满足条件和为k。时间复杂度为O(n^2)。

解法2

序号[0…j…i…n]的数组,当[j…i]子数组之和等于k时,可以看作(前i个数之和)-(前j-1个数之和)。因此,通过这一点可以实现仅用i遍历一遍数组即可求得所有满足条件的子数组个数。

  • 每次将前i个元素的和pre[i]作为哈希表的key,value用来记录第i个元素之前有几个子数组【0…j-1】的和等于key。
  • 每当遍历到第i个元素时,则会寻找第i个元素前面有哪些【0…j-1】子数组(j < i)的和满足pre[i] - k == pre[j-1],找到的话即表示存在【j…i】的子数组之和等于k。
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp;
        mp[0] = 1;
        int count = 0, pre = 0;
        for (auto& x:nums) {
            pre += x;
            if (mp.find(pre - k) != mp.end()) {
                count += mp[pre - k];
            }
            mp[pre]++;//可能有不同的【0...j-1】子数组和相同
        }
        return count;
    }
};

11. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。
在这里插入图片描述

解法一

不妥,虽然速度快

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int i = 0,j = k-1,p,maxorder;
        int max = -2147483647;
        int flag = 0;
        int flag2 = 0; 
        int numsize = nums.size();
        vector<int> result;
        while(j < numsize)
        {
            p = i;
            max = -2147483647;
            if(flag == 1) flag2 = 1;//若上次得到窗口内是单调递减的序列,则表示可以开始利用前一次的全是单调递减特性进行优化
            if(i == 0 || maxorder < i)//若目前为第一个窗口或上次最大值已不在窗口内
            {
                if(flag == 1)//若上次得到窗口内是单调递减的序列
                {
                    if(nums[j] <= nums[j-1])
                    {
                        max = nums[i];
                        maxorder = i;
                    }
                    else
                    {
                        flag = 0;
                    }
                }

                if(flag2 == 1 && i < k)//若之前有过窗口内是单调递减的序列的情况
                {
                    if(nums[i] >= nums[j])//则直接拿新加入窗口的值,与当前窗口的第一个值比较
                    {
                        result.push_back(nums[i]);
                        i++;
                        j++;
                        continue;
                    }
                }

                if(flag == 0)
                {
                    while(p <= j)//遍历窗口里所有元素,获取最大值,同时记录单调性
                    {
                        if(nums[p] > max ) 
                        {
                            max = nums[p];
                            maxorder = p;
                        }
                        if(i == p ) 
                            flag = 1;
                        else if(nums[p-1] >= nums[p] && flag != 0 )
                            flag = 1; //单调递减
                        else
                            flag = 0;
                        p++;
                    }
                }
            }
            else//若前一个窗口的最大值仍在当前窗口内,则只需拿出前一个窗口的最大值与新加入窗口的值进行比较
            {
                max = result.back() > nums[j] ?  result.back():nums[j];
                if(result.back() > nums[j])
                {
                    max = result.back();
                }
                else
                {
                    max = nums[j];
                    maxorder = j;
                }

            }
            result.push_back(max);
            i++;
            j++;
        }
        return result;
    }
};

解法二

采用单调队列。

  • 假设第i个元素在第j个元素的前面(【0…i…j…n】),并且第i个元素不大于第j个元素(nums[j] >= nums[i])。当滑动窗口向右移动时,只要第i个元素还在窗口中,那么第j个元素一定也还在窗口中。因此,由于 nums[j]的存在,nums[i]一定不会是滑动窗口中的最大值了,我们可以将 nums[i]永久地移除
  • 可以用一个队列存储这些不被永久移出的元素下标,每次将一个新元素入队时严格循环检查该新元素是不是比队列的最后一个元素要大,如果是的,则可以永久地移出目前处于队尾的元素,在进行反复判断,至队列为空或有比新元素更大的元素在队尾;如果不是,直接将新元素插入队尾。这个队列将严格保证元素序列时单调递减的,因此队首元素是整个队列的最大值。
  • 窗口右移过程中,当队列的队首元素已经离开窗口后,则需要及时将该队首元素弹出。
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        deque<int> q;
        for (int i = 0; i < k; ++i) {
            while (!q.empty() && nums[i] >= nums[q.back()]) {
                q.pop_back();
            }
            q.push_back(i);
        }

        vector<int> ans = {nums[q.front()]};
        for (int i = k; i < n; ++i) {
            while (!q.empty() && nums[i] >= nums[q.back()]) {
                q.pop_back();
            }
            q.push_back(i);
            while (q.front() <= i - k) {
                q.pop_front();
            }
            ans.push_back(nums[q.front()]);
        }
        return ans;
    }
};

12. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
在这里插入图片描述

解法1

哈希表+双指针。

  1. 首先用哈希表存储t串中的字符以及出现的次数;
  2. 用双指针i,j,指向当前子串的首尾。j 依次向右移动,至 i 到 j 的子串中出现了 t 串的所有字符且频次更大于或等于,则 i 就往右移,开始收缩子串;
  3. 至子串中字符出现频次小于 t 串中的,则 j 继续右移,循环往复,记录满足条件的最短子串。
class Solution {
public:
    unordered_map<char,int> mp,tmpmp;
    bool check() 
    {
        for (const auto &p: mp) {
            if (tmpmp[p.first] < p.second) {
                return false;
            }
        }
        return true;
    }

    string minWindow(string s, string t) {
        
        int tsize = t.size();
        int ssize = s.size();
        int i = 0,j = 0;
        int si=-1,sj=-1;
        int minlenth = 10000000;
        for(i = 0;i < tsize; i++)
        {
            mp[t[i]] =  mp.find(t[i]) == mp.end()? 1: mp[t[i]] + 1 ;
        }
        i = j = 0;

        while(j < ssize)
        {
             if(tmpmp.find(s[j]) != tmpmp.end())
             {
                tmpmp[s[j]]++;
             }
             else
             {
                if(mp.find(s[j]) != mp.end()) 
                    tmpmp[s[j]] = 1;
             }
            while(check())
            {
                if(minlenth > j-i+1 )
                {
                    si = i;
                    sj = j;
                    minlenth = j-i+1;
                }

                if(tmpmp.find(s[i]) != tmpmp.end())
                {
                    tmpmp[s[i]]--;
                }
                i++;
            }
            j++;
        }
        if(si == -1) return string("");
        return s.substr(si, sj-si+1);
    }
};

数组

13 最大子数组和

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

子数组 是数组中的一个连续部分。
在这里插入图片描述

解法1

动态规划法:

  • 每次计算以第 i 个元素结尾的最大子数组和。
  • 如果第 i - 1 个元素结尾的最大子数组和为负数,则说明第 i 个元素加上前面最大子数组的总和肯定更小,因此直接放弃前面的所有子数组,以第 i 个元素当作以第 i 个元素结尾的最大子数组和(自立门户);
  • 如果第 i - 1 个元素结尾的最大子数组和为正数,则说明第 i 个元素加上前面最大子数组的总和肯定更大,可以组成以第 i 个元素结尾最大的子数组和。
    在这里插入图片描述
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int pre = 0, maxAns = nums[0];
        for (const auto &x: nums) {
            pre = max(pre + x, x);
            maxAns = max(maxAns, pre);
        }
        return maxAns;
    }
};

14. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
在这里插入图片描述

解法1

  • 首先按照每个区间的第一个元素进行排序;
  • 之后遍历每个区间,每次检查当前第 i 个 区间[a,b]的右侧是否大于等于下一个(第 i + 1个)区间[c,d]的左侧。如果是则表示可以融合为更大的区间[a,d];如果不是则表明两者不可融合,后面要重新开启一个新的区间。
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end());
        vector<vector<int>> ans;
        for (int i = 0; i < intervals.size();) {
            int t = intervals[i][1];
            int j = i + 1;
            while (j < intervals.size() && intervals[j][0] <= t) {
                t = max(t, intervals[j][1]);
                j++;
            }
            ans.push_back({ intervals[i][0], t });
            i = j;
        }
        return ans;
    }
};

15. 轮转数组

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
在这里插入图片描述

解法1

利用vector特性,先插入数组后k个元素到数组的首段,之后再删除数组后k个元素。

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        if(nums.size() == 1 || nums.size() == k) return;
        k = k % nums.size();
        nums.insert(nums.begin(),nums.end()-k,nums.end());
        nums.erase(nums.end()-k,nums.end());
    }
};

解法2

数组多次反转:
在这里插入图片描述

class Solution {
public:
    void reverse(vector<int>& nums, int start, int end) {
        while (start < end) {
            swap(nums[start], nums[end]);
            start += 1;
            end -= 1;
        }
    }

    void rotate(vector<int>& nums, int k) {
        k %= nums.size();
        reverse(nums, 0, nums.size() - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.size() - 1);
    }
};

16. 除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
在这里插入图片描述

解法1

所有的结果如下图所示。根据规律,可采用两次循环,分别遍历数组计算下三角的累乘和上三角的累乘。

  • 第一次for循环计算下三角的累乘并存储下来到B数组中。
  • 第二次for循环可以从n至1遍历,计算下三角每层的累乘,顺便直接与B数组中对应层相乘,直接计算出该层的最终结果值。

在这里插入图片描述

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int len = nums.size();
        if (len == 0) return {};
        vector<int> ans(len, 1);
        ans[0] = 1;
        int tmp = 1;
        for (int i = 1; i < len; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }
        for (int i = len - 2; i >= 0; i--) {
            tmp *= nums[i + 1];
            ans[i] *= tmp;
        }
        return ans;
    }
};

17. 缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
在这里插入图片描述

解法一

由于要求空间复杂度为常数项且时间复杂度在O(n),只能在原有的输入数组上进行操作了。

  • 遍历数组,将数组元素值nums[ i ]放置数组中对应的位置(nums[nums[ i ] - 1] = nums[ i ]),可采用swap方法交换 i 位置与nums[ i ] - 1位置上的数。交换一次后至少保证数组在nums[i] - 1位置上的元素值与数组下标是匹配的,但数组 i 位置的数不一定与元素值匹配,则需要循环进行,直至交换后两者均与下表匹配。此外,当交换两个元素值相同时,为了避免死循环应该不交换。
  • 之后依次检查每个元素值是否与对应数组下标号一致,不一致则表明未出现的最小正整数为nums[i] + 1,否则表明1至n的数全部出现,未出现的最小正整数为n+1。

对于循环交换部分,交换一次必然至少保证一个位置的元素被正确放置,交换n次必然就有n个元素被正确放置,后续遍历数组中满足位置与元素匹配的情况将直接跳过循环,因此每个元素位置至多被访问2遍(交换+遍历判断其是否还需要交换),总共为2n,所以时间复杂度依然为O(n)。

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; ++i) {
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                swap(nums[nums[i] - 1], nums[i]);
            }
        }
        for (int i = 0; i < n; ++i) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        return n + 1;
    }
};

二维数组

18. 矩阵置零

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
在这里插入图片描述
在这里插入图片描述

解法一

将矩阵的第一行和第一列分别用来记录剩余行和剩余列中需要置0的行和列。在此之前,用两个数记录第一行和第一列(是否有0)是否需要置0.

//c++
class Solution {
public:
    void setZeroes(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        int flag_col0 = false, flag_row0 = false;
        for (int i = 0; i < m; i++) {
            if (!matrix[i][0]) {
                flag_col0 = true;
            }
        }
        for (int j = 0; j < n; j++) {
            if (!matrix[0][j]) {
                flag_row0 = true;
            }
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (!matrix[i][j]) {
                    matrix[i][0] = matrix[0][j] = 0;
                }
            }
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (!matrix[i][0] || !matrix[0][j]) {
                    matrix[i][j] = 0;
                }
            }
        }
        if (flag_col0) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
        if (flag_row0) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }
    }
};
//java
class Solution {
    public void setZeroes(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean flagCol0 = false, flagRow0 = false;
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0) {
                flagCol0 = true;
            }
        }
        for (int j = 0; j < n; j++) {
            if (matrix[0][j] == 0) {
                flagRow0 = true;
            }
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] == 0) {
                    matrix[i][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 (flagCol0) {
            for (int i = 0; i < m; i++) {
                matrix[i][0] = 0;
            }
        }
        if (flagRow0) {
            for (int j = 0; j < n; j++) {
                matrix[0][j] = 0;
            }
        }
    }
}

19. 螺旋矩阵

给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
在这里插入图片描述

解法1

用四个变量(up,down,left,right)分别控制索引的边界,马上要碰到边界的时候则调转方向,可用一个flag标记当前索引方向。flag = 0时,索引方向为从左到右;flag = 1时,从上到下;flag = 2时,从右到左;flag = 3时,从下到上。每次索引完一行或一列,对应的边界也应该收缩一个单位。

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> result;
        int up,down,left,right,i,j;
        int flag = 0;
        up = -1;
        down = matrix.size();
        left = -1;
        right = matrix[0].size();
        i = j = 0;
        while((up < i && i < down) && (left < j && j < right))
        {
            if(flag == 0)
            {
                result.push_back(matrix[i][j]);
                if(j + 1 == right)
                {
                    up++;
                    flag = 1;
                    i++;
                }
                else
                {
                    j++;
                }
            }
            else if(flag == 1)
            {
                result.push_back(matrix[i][j]);
                if(i + 1 == down)
                {
                    right--;
                    flag = 2;
                    j--;
                }
                else
                {
                    i++;
                }
            }
            else if(flag == 2)
            {
                result.push_back(matrix[i][j]);
                if(j - 1 == left)
                {
                    down--;
                    flag = 3;
                    i--;
                }
                else
                {
                    j--;
                }
            }
            else if(flag == 3)
            {
                
                result.push_back(matrix[i][j]);
                if(i - 1 == up)
                {
                    left++;
                    flag = 0;
                    j++;
                }
                else
                {
                    i--;
                }
            }
        }
        return result;
    }
};

解法二

原理与解法一类似,但代码更简洁

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector <int> ans;
        if(matrix.empty()) return ans; //若数组为空,直接返回答案
        int u = 0; //赋值上下左右边界
        int d = matrix.size() - 1;
        int l = 0;
        int r = matrix[0].size() - 1;
        while(true)
        {
            for(int i = l; i <= r; ++i) ans.push_back(matrix[u][i]); //向右移动直到最右
            if(++ u > d) break; //重新设定上边界,若上边界大于下边界,则遍历遍历完成,下同
            for(int i = u; i <= d; ++i) ans.push_back(matrix[i][r]); //向下
            if(-- r < l) break; //重新设定有边界
            for(int i = r; i >= l; --i) ans.push_back(matrix[d][i]); //向左
            if(-- d < u) break; //重新设定下边界
            for(int i = d; i >= u; --i) ans.push_back(matrix[i][l]); //向上
            if(++ l > r) break; //重新设定左边界
        }
        return ans;
    }
};

20. 旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

解法一

为了在原有的图像上进行旋转,但一个元素旋转时,会覆盖另外一个元素,此时将另外一个元素也旋转,又会复制另一个元素…,直至四次元素旋转后,则对应的位置元素就完成任务,可以从其中发现需要旋转元素的位置下标变换关系。旋转关系如下:
temp=matrix[row][col]
matrix[row][col]=matrix[n−col−1][row]
matrix[n−col−1][row]=matrix[n−row−1][n−col−1]
matrix[n−row−1][n−col−1]=matrix[col][n−row−1]
matrix[col][n−row−1]=temp
之后在旋转其他元素。相当于进旋转矩阵的四分之一块的元素,就会导致其他四分之三与其有旋转相对位置的元素被旋转。
在这里插入图片描述

//c++
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for (int i = 0; i < n / 2; ++i) {
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }
        }
    }
};
//java
class Solution {
    public void rotate(int[][] matrix) {
        int length = matrix.length;
        for(int i  = 0 ; i < length/2 ; ++i){
            for(int j = 0; j < (length + 1)/2 ; ++j){
                int temp = matrix[i][j];
                matrix[i][j] = matrix[length-1 - j][i];
                matrix[length-1 - j][i] = matrix[length-1 - i][length-1 - j];
                matrix[length-1 - i][length-1 - j] =  matrix[j][length-1 - i];
                matrix[j][length-1 - i] = temp;
            }
        }
    }
}

21. 搜索二维矩阵 ||

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
在这里插入图片描述

解法1

先从第一行从左往右找到大于target的列,之后回退一列,再从上到下找到大于target的行,之后再下左下左的寻找,直至索引行或列超出范围。最坏情况则是行方向一去一回2n,列方向一去m,即O(2n+m)

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target)
    {
        int m = matrix.size();
        int n = matrix[0].size();
        int i = 0,j = 0;
        int flag = 0;
        while(j < n)
        {
            if(matrix[i][j] == target) return true;
            if(target > matrix[i][j]) j++;
            else
            {
                if(j == 0) return false;
                j--;
                i++;
                flag = 1;
                break;
            }
        }
        if( flag == 0)
        {
            j--;
        }
        while(i < m && j < n)
        {
            if(matrix[i][j] == target) return true;
            if(target > matrix[i][j]) i++;
            else
            {
                if(j == 0) return false;
                j--;
            }
        }
        return false;
    }
};

解法2

由于行列有序,则可以在每一行中用二分查找法。

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        for (const auto& row: matrix) {
            auto it = lower_bound(row.begin(), row.end(), target);
            if (it != row.end() && *it == target) {
                return true;
            }
        }
        return false;
    }
};

解法3

z字形查找法,类似于第一种解法,区别在于一个从【0】【0】位置开始搜索,本算法则是从【n-1】【0】开始搜索。根据顺序排放的特性,每次排除一行或一列。时间复杂度O(m+n)
在这里插入图片描述

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        int x = 0, y = n - 1;
        while (x < m && y >= 0) {
            if (matrix[x][y] == target) {
                return true;
            }
            if (matrix[x][y] > target) {
                --y;
            }
            else {
                ++x;
            }
        }
        return false;
    }
};

链表

22. 相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
在这里插入图片描述

解法1

暴力求解,两个循环寻找指向的同一个节点的地址

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode *a = headA;
        ListNode *b = headB;
        while(a)
        {
            b = headB;
            while(b)
            {
                if(b == a) return a;
                b = b->next;
            }
            a = a->next;
        }
        return nullptr;
    }

}

解法2

遍历其中一个链表,并把节点地址存入哈希表中。再遍历另一个链表,每次在哈希表中寻找当前节点。

class Solution {
    public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        unordered_set<ListNode *> visited;
        ListNode *temp = headA;
        while (temp != nullptr) {
            visited.insert(temp);
            temp = temp->next;
        }
        temp = headB;
        while (temp != nullptr) {
            if (visited.count(temp)) {
                return temp;
            }
            temp = temp->next;
        }
        return nullptr;
    }
};

解法3

双指针法:链表headA 和headB 的长度分别是 m 和 n。假设链表 headA的不相交部分有 a 个节点,链表 headB 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则有 a+c=m,b+c=n。如果用两个指针分别指向两个链表的首节点,遍历到链表尾端后再指向另一个链表的首节点,继续遍历,两个指针必然会指向相同的节点或者null(两链表不相交),因为两个链表均走了a+c+b次(一个为a+c+b,另一个为b+c+a)就刚好指向相交部分的始端。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if (headA == nullptr || headB == nullptr) {
            return nullptr;
        }
        ListNode *pA = headA, *pB = headB;
        while (pA != pB) {
            pA = pA == nullptr ? headB : pA->next;
            pB = pB == nullptr ? headA : pB->next;
        }
        return pA;
    }
};

23. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述

解法一

遍历链表,重新开辟一个链表的头节点,将原来链表的元素用头插法转移到新链表中。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {

        ListNode *head2,*tmp;
        if(!head) return head;
        head2 = head->next;
        head->next = nullptr;
        while(head2)
        {
            tmp = head2;
            head2 = head2->next;
            tmp->next = head;
            head = tmp;
        }
        return head;
    }
};

24. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
(回文链表为正向遍历与反向遍历链表结果均一致)
在这里插入图片描述

解法一

将链表中的元素用数组存储,然后利用数组随机访问的特性对比头尾元素,看是否对称。

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
            if (vals[i] != vals[j]) {
                return false;
            }
        }
        return true;
    }
};

解法二

递归法:用一个指针p指向头节点,利用递归的调用顺序,用递归函数依次遍历到最后一个节点,之后递归从后往前返回,与此同时,每次返回一层递归前比较当前尾部元素与p指向节点元素关系。若相等则p往后移一步,递归返回一层,如此往复,直至递归全部返回。

class Solution {
    ListNode* frontPointer;
public:
    bool recursivelyCheck(ListNode* currentNode) {
        if (currentNode != nullptr) {
            if (!recursivelyCheck(currentNode->next)) {
                return false;
            }
            if (currentNode->val != frontPointer->val) {
                return false;
            }
            frontPointer = frontPointer->next;
        }
        return true;
    }

    bool isPalindrome(ListNode* head) {
        frontPointer = head;
        return recursivelyCheck(head);
    }
};

解法三

将后半段链表反转,然后与前半段链表同时遍历,看是否一致

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == nullptr) {
            return true;
        }

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode* firstHalfEnd = endOfFirstHalf(head);
        ListNode* secondHalfStart = reverseList(firstHalfEnd->next);

        // 判断是否回文
        ListNode* p1 = head;
        ListNode* p2 = secondHalfStart;
        bool result = true;
        while (result && p2 != nullptr) {
            if (p1->val != p2->val) {
                result = false;
            }
            p1 = p1->next;
            p2 = p2->next;
        }        

        // 还原链表并返回结果
        // firstHalfEnd->next = reverseList(secondHalfStart);
        return result;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr;
        ListNode* curr = head;
        while (curr != nullptr) {
            ListNode* nextTemp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    ListNode* endOfFirstHalf(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast->next != nullptr && fast->next->next != nullptr) {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};

25. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
在这里插入图片描述

解法一

哈希表:遍历链表,遍历每个节点之前,查看哈希表中是否有该节点的地址,如果没有就插入当前节点地址到哈希表中;如果在哈希表中找到了,则说明又访问了之前遍历过的节点,即存在环。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode *> set;
        ListNode *tmp = head;
        while(tmp != nullptr)
        {
            if(set.count(tmp)) return true;
            set.insert(tmp);
            tmp = tmp->next;
        }
        return false;
    }
};

解法二

快慢指针:一个指针(慢指针)从头一次移一步的遍历链表,另一个指针(快指针)从第二个节点一次移动两步的遍历链表。如果快指针反过来追上了慢指针(快指针先到环内循环遍历,慢指针后驶入,之后反被快指针追上),则说明有环;如果指针指向null,则说明无环。

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

26. 环形链表||

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

解法一

哈希表:如第25题的方法一样,当从哈希表中找到下一个节点的是地址时,则表明存在环且下一个节点就是入环的第一个节点。

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

解法二

快慢指针:与第25题方法不同的是,快指针(fast)与慢指针(slow)均从头节点出发。且需要证明以下结论:从头节点到入环第一个节点的距离(设为a)等于两指针在环中第一次相遇的节点继续走到入环第一个节点的距离(设为c) + “n-1倍”的环长。
设入环第一个节点到两指针在环中第一相遇的节点的距离为b
在这里插入图片描述

当fast与slow相遇时,fast在环中走了n圈,总共走了a+n(b+c)+b;slow走了a+b。由于设fast的速度为slow的两倍,则2*(a+b) = a+n(b+c)+b。化简可得a = (n-1)*(b+c)+c,说明a等于若干个环长+c。如果用另外一个指针ptr指向头节点,当slow与fast相遇后,slow继续一步一步往前走,ptr也一步一步往前走,那么当slow与ptr相遇时,则将满足上述等式,且slow与ptr会在入环点相遇。

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

    }
};

27. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
在这里插入图片描述

解法一

迭代法:每次从两个链表中选取较小的前端节点放在新链表的后面。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode head(-1);
        ListNode *pre = &head;
        while(list1 && list2 )
        {
            if(list1->val < list2->val)
            {
                pre->next = list1;
                list1 = list1->next;
            }
            else
            {
                pre->next = list2;
                list2 = list2->next;
            }
            pre = pre->next;
        }
        pre->next = list1 == nullptr ? list2:list1;
        return head.next;
    }
};

解法二

递归:将两个大链表融合递归分解为较小的节点+两个小链表融合,递归终止条件为小链表为空
在这里插入图片描述

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == nullptr) {
            return l2;
        } else if (l2 == nullptr) {
            return l1;
        } else if (l1->val < l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};

28. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
在这里插入图片描述

解法一

同时遍历两个链表,将两个数相加看是否大于等于10(需要考虑前面是否有进位),如果是则需要设置进位。两个链表中有一个为空后,则单独遍历剩余非空链表。注意两个链表均遍历完后,是否仍有进位,若是则需要再创一个节点存储进位1。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode head(-1);
        ListNode *p = &head;
        int flag = 0;
        int tmp = 0;
        while(l1 && l2)
        {
            ListNode * current = new ListNode(-1);
            p->next = current;
            tmp = l1->val + l2->val + flag;
            if(tmp < 10)
            {
                current->val = tmp;
                flag = 0;//无进位
            }
            else
            {
                current->val = tmp - 10;
                flag = 1;//有进位
            }
            l1 = l1->next;
            l2 = l2->next;
            p = current;
        }
        l1 = l1 == nullptr ? l2 : l1;
        while(l1)
        {
            ListNode * current = new ListNode(-1);
            p->next = current;
            tmp = l1->val + flag;
            if(tmp < 10)
            {
                current->val = tmp;
                flag = 0;
            }
            else
            {
                current->val = tmp - 10;
                flag = 1;
            }
            l1 = l1->next;
            p = current;
        }
        if(flag == 1)
        {
            ListNode * current = new ListNode(1);
            p->next = current;
        }
        return head.next;
    }
};

29. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
在这里插入图片描述

解法一

双指针:按照n的大小,固定两个指针之间的距离为n+1。然后一起移动,当一个指针到链表尾部(null)时,另一个指针刚好在要删除节点的前一个节点。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode h(-1);
        h.next = head;
        ListNode *p1,*p2;
        p1 = head;
        p2 = &h;
        for(int i = 0; i < n; i++)
        {
            p1 = p1->next;
        }
        while(p1)
        {
            p1 = p1->next;
            p2 = p2->next;
        }
        p1 = p2->next->next;
        delete p2->next;
        p2->next = p1;
        return h.next;
    }
};

解法二

首先遍历一次链表,确定链表长度,之后计算出删除节点的正序位置,再次遍历。

class Solution {
public:
    int getLength(ListNode* head) {
        int length = 0;
        while (head) {
            ++length;
            head = head->next;
        }
        return length;
    }

    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode* cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur->next;
        }
        cur->next = cur->next->next;
        ListNode* ans = dummy->next;
        delete dummy;
        return ans;
    }
};

30. 两两交换链表中相邻的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
在这里插入图片描述

解法一

链表题最好画图演示一下,避免写错代码。主要就是更换两个相邻节点的next与两个相邻节点前一个节点的next。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode h(-1);
        h.next = head;
        ListNode *p = &h;
        ListNode *tmp1,*tmp2;
        while(p->next && p->next->next)
        {
            tmp1 = p->next->next->next;
            tmp2 = p->next->next;
            p->next->next->next = p->next;
            p->next->next = tmp1;
            p->next = tmp2;
            p = p->next->next;
        }
        return h.next;
    }
};

31. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换
在这里插入图片描述

解法一

先遍历一次链表得到节点总数,之后每次翻转子链表之前,记录子链表的前一个节点,与子链表后一个节点,用于后续连接翻转好的子链表。子链表用头插法进行翻转。

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode h(-1);
        ListNode *p = head;
        h.next = head;
        int  n = 0;
        while(p)
        {
            n++;
            p = p->next;
        } 
        p = head;
        ListNode *startp = &h;//记录将要翻转的子链表首节点的前一个节点位置
        while(n >= k)
        {
            ListNode *pi = nullptr;
            ListNode *backp = p;//记录将要翻转的子链表首节点,之后将会成为翻转后的尾节点
            for(int i = 0; i < k;i++)//头插法翻转链表
            {
                ListNode *tmp = p;
                p = p->next;
                tmp->next = pi;
                pi = tmp;
            }
            startp->next = pi;//前半段不动的链表连接到翻转好的子链表头部
            backp->next = p;//翻转好的子链表尾部连接到后半段不动的链表头部
            startp = backp;
            n = n - k;
        }
        return h.next;
    }
};

32. 随机链表的复制

对链表进行深拷贝:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。

在这里插入图片描述

解法一

递归+哈希表:每次遍历到一个节点时,就递归地创建它所指的next节点与random节点,直至递归创建的节点指向null就返回。为了避免递归创建next节点和random节点造成重复创建,用一个哈希表记录原链表中哪些节点已经被创建,并且对应的新节点是什么,key为原链表中的节点地址,value为对应复制新节点的地址。

class Solution {
public:
    unordered_map<Node*, Node*> cachedNode;

    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }
        if (!cachedNode.count(head)) {
            Node* headNew = new Node(head->val);
            cachedNode[head] = headNew;
            headNew->next = copyRandomList(head->next);
            headNew->random = copyRandomList(head->random);
        }
        return cachedNode[head];
    }
};

解法二

在原链表每个节点后面加一个新节点,新节点逐渐转移前一个结点的信息,完成三轮如下的操作即可:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }
        for (Node* node = head; node != nullptr; node = node->next->next) {
            Node* nodeNew = new Node(node->val);
            nodeNew->next = node->next;
            node->next = nodeNew;
        }
        for (Node* node = head; node != nullptr; node = node->next->next) {
            Node* nodeNew = node->next;
            nodeNew->random = (node->random != nullptr) ? node->random->next : nullptr;
        }
        Node* headNew = head->next;
        for (Node* node = head; node != nullptr; node = node->next) {
            Node* nodeNew = node->next;
            node->next = node->next->next;
            nodeNew->next = (nodeNew->next != nullptr) ? nodeNew->next->next : nullptr;
        }
        return headNew;
    }
};

33. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
在这里插入图片描述

解法一

自顶向下的归并排序:采用递归方法,将原链表分解为两个子链表,两个子链表又进行分解,直至分解到子链表只有一个元素或为null,则开始递归返回融合两个有序的子链表。

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        return sortList(head,nullptr);
    }

    ListNode* sortList(ListNode* head, ListNode* tail)
    {
        if(!head) return head;
        if(head->next == tail)
        {
            head->next = nullptr;
            return head;
        }
        ListNode *p1,*p2;
        p1 = p2 = head;
        while(p2 != tail )
        {
            p1 = p1->next;
            p2 = p2->next;
            if(p2 != tail) p2 = p2->next;
        }
        return mergeLists(sortList(head,p1),sortList(p1,tail));
    }

    ListNode* mergeLists(ListNode* list1, ListNode* list2) {
        ListNode head(-1);
        ListNode *pre = &head;
        while(list1 && list2 )
        {
            if(list1->val < list2->val)
            {
                pre->next = list1;
                list1 = list1->next;
            }
            else
            {
                pre->next = list2;
                list2 = list2->next;
            }
            pre = pre->next;
        }
        pre->next = list1 == nullptr ? list2:list1;
        return head.next;
    }
};

34. 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。
在这里插入图片描述

解法一

暴力融合法:依次将数组中的链表合并入一个链表中。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        ListNode *ans = nullptr;
        for (size_t i = 0; i < lists.size(); ++i) {
            ans = mergeTwoLists(ans, lists[i]);
        }
        return ans;
    }
};

解法二

分而治之:将k个链表两两合并为k/2个链表,再进行两两合并…至只有一个链表

class Solution {
public:
    ListNode* mergeTwoLists(ListNode *a, ListNode *b) {
        if ((!a) || (!b)) return a ? a : b;
        ListNode head, *tail = &head, *aPtr = a, *bPtr = b;
        while (aPtr && bPtr) {
            if (aPtr->val < bPtr->val) {
                tail->next = aPtr; aPtr = aPtr->next;
            } else {
                tail->next = bPtr; bPtr = bPtr->next;
            }
            tail = tail->next;
        }
        tail->next = (aPtr ? aPtr : bPtr);
        return head.next;
    }

    ListNode* merge(vector <ListNode*> &lists, int l, int r) {
        if (l == r) return lists[l];
        if (l > r) return nullptr;
        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        return merge(lists, 0, lists.size() - 1);
    }
};

解法三

优先队列:每次取k个链表中的第一个节点放入优先队列(自动排序,底层为大根堆)中,之后从队列中取出最小的节点,再补充该节点对应链表的新的第一个节点到优先队列中,循环往复。

class Solution {
public:
    struct Status {
        int val;
        ListNode *ptr;
        bool operator < (const Status &rhs) const {
            return val > rhs.val;
        }
    };

    priority_queue <Status> q;

    ListNode* mergeKLists(vector<ListNode*>& lists) {
        for (auto node: lists) {
            if (node) q.push({node->val, node});
        }
        ListNode head, *tail = &head;
        while (!q.empty()) {
            auto f = q.top(); q.pop();
            tail->next = f.ptr; 
            tail = tail->next;
            if (f.ptr->next) q.push({f.ptr->next->val, f.ptr->next});
        }
        return head.next;
    }
};

35. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
在这里插入图片描述

解法一

根据redisi淘汰策略与LRU缓存算法原理,用一个哈希表记录存储的数据key-value,用另一个哈希表记录存储key-最近访问key的时间戳。当容量不足时,遍历第二个哈希表时间戳最小的key,并在两个哈希表中将其删除。

class LRUCache {
public:
    int maxcapacity;
    long currentsize;
    unordered_map<int,int> dict;
    unordered_map<int,long long> expiredict;
    LRUCache(int capacity):maxcapacity(capacity),currentsize(0)
    {

    }
    
    int get(int key) {
        if(dict.count(key)) 
        {
            auto now = std::chrono::system_clock::now();
            auto duration = now.time_since_epoch();
            expiredict[key] = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();;
            return dict[key];
        }
        return -1;
    }
    
    void put(int key, int value) {
        if(dict.count(key))
        {
            auto now = std::chrono::system_clock::now();
            auto duration = now.time_since_epoch();
            dict[key] = value;
            expiredict[key] = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();
            return;
        }

        if(currentsize + 1  > maxcapacity)
        {
            deletekey();
        }
        auto now = std::chrono::system_clock::now();
        auto duration = now.time_since_epoch();
        dict[key] = value;
        expiredict[key] = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();
        ++currentsize;
    }

    void deletekey(){
        int minkey  = 0;
        long long min = LLONG_MAX;
        for(auto kv : expiredict)
        {
            if(kv.second < min )
            {
                min = kv.second;
                minkey = kv.first;
            }
        }
        dict.erase(minkey);
        expiredict.erase(minkey);
    }

};

解法二

用一个哈希表用于数据缓存查询,再用一个双向链表维护缓存数据的最近访问次序(越靠近头节点表明越最近被访问过)。为了避免key存两份,哈希表中value为链表节点,链表节点中有双指针和真正存储的数据值。当一个key-value被访问后,应该先从链表中删除该节点,之后再把该节点插入到链表的头。

 struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int size;
    int capacity;

public:
    LRUCache(int _capacity): capacity(_capacity), size(0) {
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();//应当定义析构函数释放堆内存
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (!cache.count(key)) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (!cache.count(key)) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode* node = new DLinkedNode(key, value);
            // 添加进哈希表
            cache[key] = node;
            // 添加至双向链表的头部
            addToHead(node);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode* removed = removeTail();
                // 删除哈希表中对应的项
                cache.erase(removed->key);
                // 防止内存泄漏
                delete removed;
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }

    void addToHead(DLinkedNode* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    
    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }
};

36. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
在这里插入图片描述

解法一

中序遍历的顺序为左,根,右节点,具有天然的递归特性。递归算法:

  1. 遍历当前节点的左节点;
  2. 遍历当前节点(根),实际上只用将当前节点输出;
  3. 遍历当前节点的右节点;
  4. 加入递归终止条件:当前节点为空。
/**
 1. Definition for a binary tree node.
 2. struct TreeNode {
 3.     int val;
 4.     TreeNode *left;
 5.     TreeNode *right;
 6.     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 7.     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 8.     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 9. };
 */
class Solution {
public:
    vector<int> traversal;
    vector<int> inorderTraversal(TreeNode* root) {
        inorder(root);
        return traversal;
    }

    void inorder(TreeNode* root)
    {
        if(!root) return;
        inorder(root->left);
        traversal.push_back(root->val);
        inorder(root->right);
    }
};
//java
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        traversal(root,list);
        return list;
    }

    void traversal(TreeNode root,List<Integer> list){
        if(root == null) return ;
        traversal(root.left,list);
        list.add(root.val);
        traversal(root.right,list);
    }
}

解法二

迭代法:将递归算法转换为迭代算法本质上是要模拟出维护一个递归栈(节点入栈出栈)。

  1. 循环访问节点的左节点,将节点依次入栈,如果空则退出循环;
  2. 由于退出循环,则上一个节点为需要首先输出的节点,取出栈顶元素输出;
  3. 将取出的栈顶节点的右节点设为根,再次重复步骤1;
  4. 当访问的根节点为空并且栈为空,则说明所有节点遍历完成。
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (root != nullptr || !stk.empty()) {
            while (root != nullptr) {
                stk.push(root);
                root = root->left;
            }
            root = stk.top();
            stk.pop();
            res.push_back(root->val);
            root = root->right;
        }
        return res;
    }
};

37. 二叉树的最大深度

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
在这里插入图片描述

解法一

递归法:以当前节点为根节点的树的深度为其左子树和右子树中最大深度+1。

/**
 * 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 {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};

解法二

广度优先搜索算法:每次一层一层的探寻是否还有节点。用一个队列,依次维护每层的所有存在节点。当上一层出队一个节点,则需要将其左右节点入队。当上一层所有节点出队后,则开始下一层出队。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        queue<TreeNode*> Q;
        Q.push(root);
        int ans = 0;
        while (!Q.empty()) {
            int sz = Q.size();
            while (sz > 0) {
                TreeNode* node = Q.front();Q.pop();
                if (node->left) Q.push(node->left);
                if (node->right) Q.push(node->right);
                sz -= 1;
            }
            ans += 1;
        } 
        return ans;
    }
};

38. 翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
在这里插入图片描述

解法一

递归:依次交互当前节点的左子树与右子树;

/**
 * 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 {
public:
    TreeNode* invertTree(TreeNode* root) {
·       if (root == nullptr) {
            return nullptr;
        }
        TreeNode* left = invertTree(root->left);
        TreeNode* right = invertTree(root->right);
        root->left = right;
        root->right = left;
        return root;
    }
};

39. 对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。
在这里插入图片描述

解法一

递归:二叉树轴对称等价于依次对比对称位置上的节点值是否均相等。采用两个指针分别指向对称位置节点,如果值相同,则继续创建两个指针对比一个节点的左子树节点(及右子树节点)是否等于另一个对称位置节点的右子树节点(及左子树节点)值是否相等,若相等则继续对比,指到遍历到空节点或者有值不等的情况。

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        return check(root->left,root->right);
    }

bool check(TreeNode* p,TreeNode* q)
{
    if(p == nullptr && q == nullptr) return true;
    if(!p || !q) return false;
    if(p->val != q->val) return false;
    return check(p->left,q->right)&&check(p->right,q->left);
}
};

解法二

迭代法:将迭代过程中比较的节点存入队列中,每次取头两个节点(对称位置上的)对比,如果两个指针a、b不为空且值相等,则后面需要对比a左b右、a右b左,依次存入队列,再次取出头两个节点比较,循环往复至对比两节点不等(说明树不是对称的)或队列为空(说明树是对称的)。

class Solution {
public:
    bool check(TreeNode *u, TreeNode *v) {
        queue <TreeNode*> q;
        q.push(u); q.push(v);
        while (!q.empty()) {
            u = q.front(); q.pop();
            v = q.front(); q.pop();
            if (!u && !v) continue;
            if ((!u || !v) || (u->val != v->val)) return false;

            q.push(u->left); 
            q.push(v->right);

            q.push(u->right); 
            q.push(v->left);
        }
        return true;
    }

    bool isSymmetric(TreeNode* root) {
        return check(root, root);
    }
};

40. 二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径 。

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。

两节点之间路径的 长度 由它们之间边数表示。

在这里插入图片描述

解法一

递归:通过观察发现,最长路径不一定是经过根节点,但一定经过某一个子树的根节点。可以递归求每个节点左右子树的深度R、L,则经过该子树根节点的最长路径为路径上总节点数-1,即R+L+1-1 = R+L,最终保留路径最长值。

class Solution {
public:
    int maxlength;

    int diameterOfBinaryTree(TreeNode* root) {
        maxlength = 0;
        depth(root);
        return  maxlength;
    }

    int depth(TreeNode * p)
    {
        if(p == nullptr) return 0;
        int L = depth(p->left);
        int R = depth(p->right);
        maxlength = max(L + R ,maxlength);
        return max(L,R) + 1;
    }
};

41. 二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
在这里插入图片描述

解法一

层序遍历:用一个队列维持下一层将要遍历的节点,通过队列大小,依次取出该层元素并将其左右子节点(下一层)插入到队尾。当队列为空时则遍历完毕。

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

42. 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
在这里插入图片描述

解法一

中序遍历:平衡二叉搜索树的中序遍历就是顺序排列的,刚好对应本题的顺序数组。在给定中序遍历序列数组的情况下,每一个子树中的数字在数组中一定是连续的,因此可以通过数组下标范围确定子树包含的数字。每次构建子树时,取范围内的中点作为构建子树的根节点,保证下次构建子子树时两边顺序节点数量相等或之差不会超过1,即树是平衡的。递归取中点作为子根节点构建子树。

class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        int size = nums.size();
        return twodivision(nums,0,size-1);
    }
    TreeNode* twodivision(vector<int>& nums,int left, int right)
    {
        if(left > right) return nullptr;
        int mid = (right+left)/2;
        TreeNode* p = new TreeNode(nums[mid]);
        p->left = twodivision(nums,left,mid-1);
        p->right = twodivision(nums,mid+1,right);
        return p;
    }
};

43. 验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
在这里插入图片描述

解法一

中序遍历(左-根-右):利用二叉搜索树的中序遍历为升序序列,看遍历的元素是否总是大于前一个元素即可。

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        stack<TreeNode*> stack;
        long long inorder = (long long)INT_MIN - 1;

        while (!stack.empty() || root != nullptr) {
            while (root != nullptr) {
                stack.push(root);
                root = root -> left;
            }
            root = stack.top();
            stack.pop();
            // 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
            if (root -> val <= inorder) {
                return false;
            }
            inorder = root -> val;
            root = root -> right;
        }
        return true;
    }
};

解法二

递归范围法:用两个数确定一个子树的数值范围,与当前根节点比较,左子树的最大值应该小于当前根节点,右子树的最小值应当大于当前根节点。

class Solution {
public:
    bool helper(TreeNode* root, long long lower, long long upper) {
        if (root == nullptr) {
            return true;
        }
        if (root -> val <= lower || root -> val >= upper) {
            return false;
        }
        return helper(root -> left, lower, root -> val) && helper(root -> right, root -> val, upper);
    }
    bool isValidBST(TreeNode* root) {
        return helper(root, LONG_MIN, LONG_MAX);
    }
};

44. 二叉搜索树中第K小的元素

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
在这里插入图片描述

解法一

中序遍历:利用二叉搜索树的中序遍历为升序序列,当遍历到第k元素时即为结果。

class Solution {
public:
    int kthSmallest(TreeNode* root, int k) {
        stack<TreeNode*> stack;
        while(!stack.empty() || root)
        {
            while(root)
            {
                stack.push(root);
                root = root->left;
            }
            root = stack.top();
            stack.pop();
            k--;
            if(k == 0) break;
            root = root->right;
        }
        return root->val;
    }
};

45. 二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
在这里插入图片描述

解法一

深度优先搜索:按照深度优先搜索规则,从右子树开始搜索,并且记录到达某一深度时的第一个搜索到的节点即为右视图的节点。

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        unordered_map<int, int> rightmostValueAtDepth;
        int max_depth = -1;

        stack<TreeNode*> nodeStack;
        stack<int> depthStack;
        nodeStack.push(root);
        depthStack.push(0);

        while (!nodeStack.empty()) {
            TreeNode* node = nodeStack.top();nodeStack.pop();
            int depth = depthStack.top();depthStack.pop();

            if (node != NULL) {
            	// 维护二叉树的最大深度
                max_depth = max(max_depth, depth);

                // 如果不存在对应深度的节点我们才插入
                if (rightmostValueAtDepth.find(depth) == rightmostValueAtDepth.end()) {
                    rightmostValueAtDepth[depth] =  node -> val;
                }

                nodeStack.push(node -> left);
                nodeStack.push(node -> right);
                depthStack.push(depth + 1);
                depthStack.push(depth + 1);
            }
        }

        vector<int> rightView;
        for (int depth = 0; depth <= max_depth; ++depth) {
            rightView.push_back(rightmostValueAtDepth[depth]);
        }

        return rightView;
    }
};

解法二

广度优先搜索:结合队列(记录每一层的节点数并且按照从左到右的顺序依次入队),一层一层的搜索节点,每次取每层最后一个节点。

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        unordered_map<int, int> rightmostValueAtDepth;
        int max_depth = -1;

        queue<TreeNode*> nodeQueue;
        queue<int> depthQueue;
        nodeQueue.push(root);
        depthQueue.push(0);

        while (!nodeQueue.empty()) {
            TreeNode* node = nodeQueue.front();nodeQueue.pop();
            int depth = depthQueue.front();depthQueue.pop();

            if (node != NULL) {
            	// 维护二叉树的最大深度
                max_depth = max(max_depth, depth);

                // 由于每一层最后一个访问到的节点才是我们要的答案,因此不断更新对应深度的信息即可
                rightmostValueAtDepth[depth] =  node -> val;

                nodeQueue.push(node -> left);
                nodeQueue.push(node -> right);
                depthQueue.push(depth + 1);
                depthQueue.push(depth + 1);
            }
        }

        vector<int> rightView;
        for (int depth = 0; depth <= max_depth; ++depth) {
            rightView.push_back(rightmostValueAtDepth[depth]);
        }

        return rightView;
    }
};

解法三

借鉴先序遍历:按照根右左的顺序遍历,刚好可以优先看到每一层最右边的节点,同时记录每层第一个被访问到的节点。

class Solution {
public:
    vector<int> res;
public:
    vector<int> rightSideView(TreeNode* root) 
    {
        dfs(root,0); 
        return res;
    }
    void dfs(TreeNode* root, int depth) {
        if(root == nullptr) return;
        if(depth == res.size())
        {
            res.push_back(root->val);
        }
        depth++;
        dfs(root->right, depth);
        dfs(root->left, depth);
    }
};

46. 二叉树展开为链表

给你二叉树的根结点 root ,请你将它展开为一个单链表:

展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
在这里插入图片描述

解法一

首先进行先序遍历,之后创建新的节点组成链表。

class Solution {
public:
    void flatten(TreeNode* root) {
        vector<TreeNode*> l;
        preorderTraversal(root, l);
        int n = l.size();
        for (int i = 1; i < n; i++) {
            TreeNode *prev = l.at(i - 1), *curr = l.at(i);
            prev->left = nullptr;
            prev->right = curr;
        }
    }

    void preorderTraversal(TreeNode* root, vector<TreeNode*> &l) {
        if (root != NULL) {
            l.push_back(root);
            preorderTraversal(root->left, l);
            preorderTraversal(root->right, l);
        }
    }
};

解法二

不创建额外的节点:当遍历到一个节点时,其如果没有左子树,则可以直接遍历右子树,且满足链表要求;当存在左子树时,由于左子树先被遍历到,之后才是右子树,这就可以将右子树提前挂到左子树将要最后遍历的一个右节点上,这样整体先序遍历的顺序也不会被改变,再将整个左子树搬移到当前节点的右边。对每个节点都如此,即可变成一个只有右子树的链表树结构。

class Solution {
public:
    void flatten(TreeNode* root) {
        TreeNode *curr = root;
        while (curr != nullptr) {
            if (curr->left != nullptr) {
                auto next = curr->left;
                auto predecessor = next;
                while (predecessor->right != nullptr) {
                    predecessor = predecessor->right;
                }
                predecessor->right = curr->right;
                curr->left = nullptr;
                curr->right = next;
            }
            curr = curr->right;
        }
    }
};

47. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
在这里插入图片描述

解法一

根据先序遍历与中序遍历画出二叉树结构的过程,我们可以得知先序序列的第一个节点为根节点,再可根据根节点从中序序列中分出左右子树集合,再根据左右子树集合分别取对应先序序列子序列的第一个节点则为子树的根节点,再用中序序列子序列划分出新其他左右子树,依次类推可以得到整个书的结构。可以采用递归方法。

  • 从前序序列中取出根节点;
  • 在中序序列中划分左右子树集合;
  • 结合左子树集合的前序序列和中序序列递归构建左子树;
  • 结合右子树集合的前序序列和中序序列递归构建右子树;
  • 返回所构建子树的根节点,递归终止条件为前序序列为空。
class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

48. 路径总和 III

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
在这里插入图片描述

解法一

前缀和:依次深度优先搜索遍历每一个节点,遍历到每一个节点时,保存到根节点到本节点的路径和(前缀和),计算该条路径上有没有前缀和等于当前节点前缀和-targetSum,如果有则说明找到一个和为targetSum的路径。为了便于查找,采用哈希表来保存路径上所有节点的前缀和(key)。由于前缀和可能存在相等的情况,因此value采用计数的方式记录这样的可用路径有多少个。此外,由于深度优先搜索会有回溯,也就是说有些前缀和应该被及时剔除,下例代码采用将计数减一的方式进行剔除,对应路径和的value为0时则表明当前无该种路径和可用。

/**
 * 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 {
public:
    unordered_map<long long, int> prefix;//哈希表

    int dfs(TreeNode *root, long long curr, int targetSum) {
        if (!root) {
            return 0;
        }

        int ret = 0;
        curr += root->val;
        if (prefix.count(curr - targetSum)) {
            ret = prefix[curr - targetSum];//看看满足条件的有多少条路径,如果为0也就表示没有这样的路径可用,对后续结果也不影响
        }

        prefix[curr]++;//将当前节点前缀和加入或计数增加
        ret += dfs(root->left, curr, targetSum);//搜索左节点
        ret += dfs(root->right, curr, targetSum);//搜索右节点
        prefix[curr]--;//由于该节点左右子树已经搜索完,需要回溯到上一层节点,则当前节点前缀和计数应该减一,表示不可用了

        return ret;
    }

    int pathSum(TreeNode* root, int targetSum) {
        prefix[0] = 1;//当第一个节点为targetSum时,也是一条路径
        return dfs(root, 0, targetSum);
    }
};

解法二

双重递归:最简单的方法就是把所有路径都一一列举出来,判断有多少条路径满足条件。采用第一个递归rootSum去寻找以某一个当前节点为起点的所有路径。采用另一个递归pathSum依次输入以每个节点为起点的rootSum寻找。顺便统计满足条件的路径数量。

class Solution {
public:
    int rootSum(TreeNode* root, long targetSum) {
        if (!root) {
            return 0;
        }

        int ret = 0;
        if (root->val == targetSum) {
            ret++;
        } 

        ret += rootSum(root->left, targetSum - root->val);
        ret += rootSum(root->right, targetSum - root->val);
        return ret;
    }

    int pathSum(TreeNode* root, int targetSum) {
        if (!root) {
            return 0;
        }
        
        int ret = rootSum(root, targetSum);
        ret += pathSum(root->left, targetSum);
        ret += pathSum(root->right, targetSum);
        return ret;
    }
};

49 . 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

在这里插入图片描述

解法一

p,q有两种情况:如果p,q在同一条向下延申的路径上,则最近公共祖先肯定为深度min(p,q);如果p,q不在同一条向下延申的路径上,那么最近公共祖先肯定为左右子树各能找到q,p中的一员的节点。因此,可以通过深度优先搜索找等于p,q的节点。

  • 搜索当前节点的左子树;
  • 搜索当前节点的右子树;
  • 如果左右子树均搜到了q,p中的一员,则说明当前节点为最近公共祖先;如果当前节点值等于p或q,且左右子树中找到了另外一个,则说明当前节点为最近公共祖先;
  • 由于本质上为了找到q,p在哪里,然后向上传递信息。因此如果当前节点子树中有找到p或q,则需要返回true;如果当前节点值等于p或q,也需要返回true。
class Solution {
public:
    TreeNode* ans;
    bool dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr) return false;
        bool lson = dfs(root->left, p, q);
        bool rson = dfs(root->right, p, q);
        if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson))) {
            ans = root;
        } 
        return lson || rson || (root->val == p->val || root->val == q->val);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        dfs(root, p, q);
        return ans;
    }
};

50. 二叉树中的最大路径和

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

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

给你一个二叉树的根节点 root ,返回其 最大路径和 。

在这里插入图片描述

解法一

递归:树中的最大路径和可以看作某一节点与其左边最大路径和、右边最大路径和当前节点值组合相加找最大值,max(左+root,右+root,左+右+root)。但是由于当遍历到某一结点时不知道其是不是全局最大,则需要沿着节点继续向上伸展,则需要舍弃一侧,考虑max(root+左,root+右),取最大的分支向上伸展,作为上一层节点的左或右边提供的增益。由于左边或右边的增益可能为负值,这时就不应该在搜索最大路径中加入该增益,于是以0替代。

class Solution {
public:
    int maxlenth  = INT_MIN;

    int maxGain(TreeNode* root)
    {
        if(!root)
        {
            return 0;
        }
        int leftgain = max(maxGain(root->left),0);
        int rightgain = max(maxGain(root->right),0);
        maxlenth = max(root->val + leftgain + rightgain, maxlenth);
        return root->val + max(leftgain, rightgain);
    }
    int maxPathSum(TreeNode* root) {
        maxGain(root);
        return maxlenth;
    }
};
  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值