Leetcode 刷题笔记

Leetcode 初级算法刷题笔记

此为我在2022/4/24(文件最后修改日期)前写的一些leetcode刷题笔记,对应leetbook中《初级算法》一书。放在CSDN作为备份,方便后续复习回顾用。

350. 两个数组的交集 II
给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

提示:

1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
掌握思路一(排序+双指针)
  • 此题要求数组的交集,遇到数组问题,先不考虑复杂度,看排序后会不会更好做,很多题目排序后更直观,且c++排序用sort很方便
  • 排序用sort (nums1.begin(), nums1.end())
  • 双指针注意 while (双指针终止条件) { ... }的写法
class Solution {
public:
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        //排序
        sort (nums1.begin(), nums1.end());		
        sort (nums2.begin(), nums2.end());
        vector<int> res;
        int n1 = nums1.size();
        int n2 = nums2.size();
        //双指针
        int i = 0, j = 0;
        while (i < n1 && j < n2) {      		//不能只会用for循环,双指针可以用while里面放停止条件的写法
            if (nums1[i] < nums2[j]) i++;		//因为是有序数组,i和j谁的数小谁往前走,直到二者相等
            else if (nums1[i] > nums2[j]) j++;	//相等时将该值存到结果数组中,i和j再一起往前走一步
            else {
                res.push_back(nums1[i]);
                i++;
                j++;
            }
        }
        return res;
    }
};
掌握思路二(哈希表)
  • 哈希表是特别重要的解题方法,官方题解一用的就是哈希表,要多加熟练哈希表的使用
  • unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的
  • 在使用unordered_map时,需要引入头文件:#include < unordered_map >
  • unordered_map因为内部实现了哈希表,因此其查找速度非常的快
  • map对比,对于那些有顺序要求的问题,用map会更高效一些,对于查找问题,常会考虑一下用unordered_map
class Solution {
public:
    //由于同一个数字在两个数组中都可能出现多次,因此需要用哈希表存储每个数字出现的次数
    //对于一个数字,其在交集中出现的次数等于该数字在两个数组中出现次数的最小值
    vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
        if (nums1.size() > nums2.size()) {		//为了降低空间复杂度,首先遍历较短的数组并在哈希表中记录每个数字以及对应出现的次数
            return intersect(nums2, nums1);		//然后再遍历较长的数组得到交集
        }
        unordered_map <int, int> m;				
        for (int num : nums1) {					//首先遍历第一个数组,并在哈希表中记录第一个数组中的每个数字以及对应出现的次数
            ++m[num];							
        }
        vector<int> res;
        for (int num : nums2) {					//遍历第二个数组,对于第二个数组中的每个数字
            if (m.count(num)) {					//如果在哈希表中存在这个数字
                res.push_back(num);				//则将该数字添加到答案
                --m[num];						//并减少哈希表中该数字出现的次数
                if (m[num] == 0) {
                    m.erase(num);				//如果次数减至0,则在哈希表中删除该值
                }
            }
        }
        return res;
    }
};
283. 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:

输入: nums = [0]
输出: [0]

提示:

1 <= nums.length <= 10^4
-2^31 <= nums[i] <= 2^31 - 1
掌握思路一(删除0后再补0)
  • 自己的解法,遍历数组,删除0,用cnt来计数删除0的个数,末尾再补上
  • 用这种解法主要是为了特别学习一下vector.erase()的使用
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int n = nums.size();
        auto it = nums.begin();

        int cnt = 0;
        while (it != nums.end()) {
            if (*it == 0) {
                cnt++;
                it = nums.erase(it);		//要特别注意这种写法,删除后迭代器指向的是原来删除元素的后一个元素,所以不要多加it++
            } else {
                it++;
            }
        }
        while (cnt--) nums.push_back(0);
    }
};
掌握思路二(遍历数组并覆盖)
  • 用一个下标将合题意的值存起来,这种思路可移植性强,可以掌握一下,上面解法出于对erase的学习写的,实际上直接覆盖来的快
class Solution {
    public void moveZeroes(int[] nums) {
        int index = 0;
        for (int num : nums) {
            if (num != 0) {
                nums[index++] = num;		//符合题意直接覆盖就行
            }
        }
        for (int i = index; i < nums.length; i++) {
            nums[i] = 0;
        }
    }
}
1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

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

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

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]
掌握思路一(初学者经典的两重for循环)
  • leetcode第一题暴力解法
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        
        vector<int> res;

        for (int i = 0; i < nums.size(); i++) {
            for (int j = i + 1; j < nums.size(); j++) {
                if (nums[j] == target - nums[i]) {
                    res.push_back(i);					//res = vector({i, j});
                    res.push_back(j);
                    return res;
                }
            }
        }
        return res;										
    }
};
掌握思路二(重要的哈希表)
  • 记录这题也就是为了再次强调使用C++中的哈希表unordered_map hash
  • 时间复杂度:由于只扫描一遍,且哈希表的插入和查询操作的复杂度是 O(1),所以总时间复杂度是 O(n)
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target)
    {
        //循环一遍 numsnums 数组,在每步循环中我们做两件事:
        //1.判断 target−nums[i]target−nums[i] 是否在哈希表中
        //2.将 nums[i]nums[i] 插入哈希表中
        vector<int> res;
        unordered_map<int,int> hash;
        for (int i = 0; i < nums.size(); i ++ )
        {
            int another = target - nums[i];
            if (hash.count(another))
            {
                res = vector<int>({hash[another], i});		//注意直接给vecto赋值的写法vector({数组各值,逗号隔开})
                break;										//用push_back()也行
            }
            hash[nums[i]] = i;								//键值对为值和下标,hash[值] = 下标
        }
        return res;
    }
};
48. 旋转图像
掌握思路一(找位置间的对应关系)
  • 此为官方题解1,使用一个辅助数组来存放旋转后的值再重新赋给原数组即可
  • 重点是要发现对应关系matrix_new[j][n - i - 1] = matrix[i][j]
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        // C++ 这里的 = 拷贝是值拷贝,会得到一个新的数组
        auto matrix_new = matrix;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                matrix_new[j][n - i - 1] = matrix[i][j];
            }
        }
        // 这里也是值拷贝
        matrix = matrix_new;
    }
};
掌握思路二(建议记住的旋转处理方法)
  • 两次反转可以实现旋转效果,翻转可以在原数组上处理,不需要另开辅助数组
  • 也可根据这种方法去考虑其他旋转情况
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        int n = matrix.size();
        for (int i = 0; i < n; i ++ )					//先以左上-右下对角条线为轴做翻转;
            for (int j = i + 1; j < n; j ++ )
                swap(matrix[i][j], matrix[j][i]);
        for (int i = 0; i < n; i ++ )					//再以中心的竖线为轴做翻转;
            for (int j = 0, k = n - 1;
                    j < k; j ++, k -- )
                swap(matrix[i][j], matrix[i][k]);
    }
};
7. 整数反转
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

如果反转后整数超过 32 位的有符号整数的范围 [2^31,  2^311] ,就返回 0。

假设环境不允许存储 64 位整数(有符号或无符号)。

示例 1:

输入:x = 123
输出:321
示例 2:

输入:x = -123
输出:-321
示例 3:

输入:x = 120
输出:21
示例 4:

输入:x = 0
输出:0
 

提示:

-2^31 <= x <= 2^31 - 1
掌握思路一(取余反转法)
  • 在C++中,负数的取模运算和数学意义上的取模运算不同,结果还是负数,比如 −12%10=−2−12%10=−2,所以我们不需要对负数进行额外处理
  • 记录这题的原因还有就是INT_MININT_MAX的运用,要注意防止溢出
  • 32位数int的最大值为2147483647,是21亿的数量级,比它小的数反转之后可能会溢出,要额外小心
class Solution {
public:
    int reverse(int x) {
        int rev = 0;
        while (x != 0) {
            if (rev < INT_MIN / 10 || rev > INT_MAX / 10) {
                return 0;
            }
            int digit = x % 10;
            x /= 10;
            rev = rev * 10 + digit;
        }
        return rev;
    }
};
387. 字符串中的第一个唯一字符
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。

示例 1:

输入: s = "leetcode"
输出: 0
示例 2:

输入: s = "loveleetcode"
输出: 2
示例 3:

输入: s = "aabb"
输出: -1

提示:

1 <= s.length <= 10^5
s 只包含小写字母
掌握思路一(哈希表存字母出现频率)
  • 首先,当然要复习一下哈希表的使用,这里哈希表存放的为<字母,出现的频率>,也就是hash[字母]=该字母出现频率
class Solution {
public:
    int firstUniqChar(string s) {
        unordered_map <char, int> hash;

        for (auto x : s) {
            hash[x]++;			
        }
        for (int i = 0; i < s.size(); i++) {
            if (hash[s[i]] == 1) {
                return i;
            }
        }

        return -1;
    }
};
//简化版
class Solution {
public:
    int firstUniqChar(string s) {
        unordered_map<char, int> hash;
        for (auto c: s) hash[c] ++ ;
        for (int i = 0; i < s.size(); i ++ )
            if (hash[s[i]] == 1)
                return i;
        return -1;
    }
};
掌握思路二(数组)
  • 用数组也可以,注意放在函数外面初始化为全0
class Solution {
public:
    int fre[26];
    int firstUniqChar(string s) {

        for (int i = 0; i < s.size(); i++) {
            fre[s[i] - 'a']++;
        }

        for (int i = 0; i < s.size(); i++) {
            if (fre[s[i] - 'a'] == 1) {
                return i;
            }
        }

        return -1;
    }
};
242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true
示例 2:

输入: s = "rat", t = "car"
输出: false
 
提示:

1 <= s.length, t.length <= 5 * 10^4
s 和 t 仅包含小写字母
掌握思路一(简单的双哈希比较)
  • 两个哈希表各自存放两个字符串各个字母的出现次数
  • 再比较这两个哈希表,首先如果哈希表长度不一样,说明出现的字母不一致,直接返回false
  • 其次,遍历一遍哈希表,判断字母出现次数是否一样(好像直接遍历就行)
class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> hash_s;
        unordered_map<char, int> hash_t;

        for (auto x : s) {
            hash_s[x]++;
        }
        for (auto x : t) {
            hash_t[x]++;
        }

        int n1 = hash_s.size();
        int n2 = hash_t.size();

        if (n1 != n2) return false;

        for (int i = 0; i < s.size(); i++) {
            if (hash_s[s[i]] != hash_t[s[i]]) return false;
        }
        return true;
    }
};
//简化版
class Solution {
public:
    bool isAnagram(string s, string t) {
        unordered_map<char, int> a, b;
        for (auto c: s) a[c] ++ ;
        for (auto c: t) b[c] ++ ;
        return a == b;						//这里提示我们哈希表是可以直接比较的
    }
};
掌握思路二(排序简化问题)
  • 回顾题350的笔记,“遇到数组问题,先不考虑复杂度,看排序后会不会更好做,很多题目排序后更直观,且c++排序用sort很方便”
  • 这里排序后显然直接再比较一下字符串即可
class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.length() != t.length()) {
            return false;
        }
        sort(s.begin(), s.end());
        sort(t.begin(), t.end());
        return s == t;
    }
};
掌握思路三(哈希表)
  • 此为官方题解中的哈希表做法,更为简洁
  • vector table(26, 0);学会这种初始化一维数组的办法
class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.length() != t.length()) {
            return false;
        }
        vector<int> table(26, 0);
        for (auto& ch: s) {
            table[ch - 'a']++;
        }
        for (auto& ch: t) {
            table[ch - 'a']--;
            if (table[ch - 'a'] < 0) {
                return false;
            }
        }
        return true;
    }
};
125. 验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
示例 2:

输入: "race a car"
输出: false
解释:"raceacar" 不是回文串
 
提示:

1 <= s.length <= 2 * 10^5
字符串 s 由 ASCII 字符组成
掌握思路一(筛选+判断)
  • 比较容易想到的就是预处理字符串,化成全小写无其他符号的新字符串
  • 将新字符串翻转比较即可判断是否为回文串
  • 这里学习一些函数用法
  • **tolower()**将字符转成小写, 非字母字符不做出处理
  • **isalnum()**判断是否为数字和字母,太妙了
  • 拓展学习
  • **isalpha()**用来判断一个字符是否为字母,如果是字符则返回非零,否则返回零
  • **isdigit()**函数判断字符是否是数字
  • islower()用来判断一个字符是否为小写字母,也就是是否属于a~z
  • isupper()islower()相反,用来判断一个字符是否为大写字母
class Solution {
public:
    bool isPalindrome(string s) {
        string sgood;
        for (char ch: s) {
            if (isalnum(ch)) {
                sgood += tolower(ch);
            }
        }
        string sgood_rev(sgood.rbegin(), sgood.rend());
        return sgood == sgood_rev;
    }
};
掌握思路二(双指针判断回文)
  • 同样先预处理再判断
class Solution {
public:
    bool isPalindrome(string s) {
        string sgood;
        for (char ch: s) {
            if (isalnum(ch)) {
                sgood += tolower(ch);
            }
        }
        int n = sgood.size();
        int left = 0, right = n - 1;
        while (left < right) {
           if (sgood[left] != sgood[right]) {
                return false;
            }
            ++left;
            --right;
        }
        return true;
    }
};
28. 实现 strStr()
实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回  -1 。

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。

示例 1:

输入:haystack = "hello", needle = "ll"
输出:2
示例 2:

输入:haystack = "aaaaa", needle = "bba"
输出:-1
示例 3:

输入:haystack = "", needle = ""
输出:0
 

提示:

1 <= haystack.length, needle.length <= 10^4
haystack 和 needle 仅由小写英文字符组成
掌握思路一(投机取巧)
  • 用库函数find()快速解决这题,笔试面试如果能这样AC就直接AC,不行就用KMP算法
class Solution {
public:
    int strStr(string haystack, string needle) {
        return haystack.find(needle); 
    }
};
掌握思路二(KMP算法)
  • 著名的KMP算法,建议把该模板背下来
  • 此处笔记有待看懂后补充
class Solution {
public:
    int strStr(string s, string p) {
        if (p.empty()) return 0;
        int n = s.size(), m = p.size();
        s = ' ' + s, p = ' ' + p;						//加一个空字符,让后面的遍历从1开始
		
        vector<int> next(m + 1);
        for (int i = 2, j = 0; i <= m; i ++ ) {			//求模式串的next数组
            while (j && p[i] != p[j + 1]) j = next[j];
            if (p[i] == p[j + 1]) j ++ ;
            next[i] = j;
        }

        for (int i = 1, j = 0; i <= n; i ++ ) {			//匹配
            while (j && s[i] != p[j + 1]) j = next[j];
            if (s[i] == p[j + 1]) j ++ ;
            if (j == m) return i - m;
        }

        return -1;
    }
};
38. 外观数列
给定一个正整数 n ,输出外观数列的第 n 项。

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。

你可以将其视作是由递归公式定义的数字字符串序列:

countAndSay(1) = "1"
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
前五项如下:

1.     1
2.     11
3.     21
4.     1211
5.     111221
第一项是数字 1 
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
掌握思路一(双指针)
  • 利用双指针求每个小段重复了几次
class Solution {
public:
    string countAndSay(int n) {
        string s = "1";
        for (int i = 1; i < n; i++) {
            string t;
            for (int j = 0; j < s.size(); ) {
                int k = j + 1;
                while (k < s.size() && s[k] == s[j]) k++;
                t += to_string(k - j) + s[j];
                j = k;
            }
            s = t;
        }
        return s;
    }
};
了解思路二(枚举法)

即将所有30种结果枚举出来,存在一个数组当中(不推荐)

14. 最长公共前缀
编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""。

 

示例 1:

输入:strs = ["flower","flow","flight"]
输出:"fl"
示例 2:

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
 

提示:

1 <= strs.length <= 200
0 <= strs[i].length <= 200
strs[i] 仅由小写英文字母组成
掌握思路一(基准遍历法)
  • 以string数组中第一个string为基准,遍历这个string,每次取一个字符,遍历数组中其他string同位置字符是否相等
  • 遍历成功将该字符存到结果中
  • 注意当i >= x.size() || x[i] != c时要终止
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        string res;
        if (strs.empty()) return res;

        for (int i = 0; i < strs[0].size(); i++) {
            
            char c = strs[0][i];

            for (auto x : strs) {
                if (i >= x.size() || x[i] != c) 
                    return res; 
            }
            res += c;
        }
        
        return res;
    }
};
掌握思路二(横向扫描)
  • 依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。
class Solution {
public:
    string longestCommonPrefix(vector<string>& strs) {
        if (!strs.size()) {
            return "";
        }
        string prefix = strs[0];
        int count = strs.size();
        for (int i = 1; i < count; ++i) {
            prefix = longestCommonPrefix(prefix, strs[i]);
            if (!prefix.size()) {
                break;
            }
        }
        return prefix;
    }

    string longestCommonPrefix(const string& str1, const string& str2) {
        int length = min(str1.size(), str2.size());
        int index = 0;
        while (index < length && str1[index] == str2[index]) {
            ++index;
        }
        return str1.substr(0, index);
    }
};

237. 删除链表中的节点
请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。

题目数据保证需要删除的节点 不是末尾节点 。

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
掌握思路一(替死删除法)
  • 题意不能访问head,也就是不能作常规删除(遍历到前一个结点再删除)
  • 那么就要把下一个结点的值拿过来,再把下一个结点删了
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        node->val = node->next->val;
        node->next = node->next->next;
    }
};
19. 删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]
掌握思路一(哑结点+链表长度简化问题)
  • 链表问题中,链表长度和虚拟头结点都有助于解决问题
  • 尤其是构造一个虚拟头结点(哑结点),有的时候显得非常重要
  • 我最开始也是因为没有构造虚拟头结点重新提交了好几次才意识到其重要性
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        //先添加一个哑节点,有助于统一情况,使得head也有前驱结点
        auto dummy = new ListNode(-1, head);	//auto dummy = new ListNode(-1);dummy->next = head;
        //计算链表长度
        int len = 0;
        while (head) {
            head = head->next;
            len++;
        }
        //定位到要删除结点的前驱
        auto p = dummy;
        for (int i = 0; i < len - n; i++) {
            p = p->next;
        }
        //删除操作
        p->next = p->next->next;
        // ListNode* ans = dummy->next;
        // delete dummy;
        // return ans;
        return dummy->next;					//只是解题的话,不用delete dummy也没关系
    }
};
了解思路二(栈)
  • 顺带了解一下栈的做法,我觉得思路一比较好,思路二了解即可
  • 其实也是为了找要删除结点的前驱结点
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(0, head);
        stack<ListNode*> stk;			//构造一个存放ListNode*结点的栈
        ListNode* cur = dummy;
        while (cur) {
            stk.push(cur);				//所有元素进栈
            cur = cur->next;
        }
        for (int i = 0; i < n; ++i) {
            stk.pop();					//倒数n个元素全出栈,停止
        }
        ListNode* prev = stk.top();		//那么下一个出栈的就是倒数第n个结点的前驱
        prev->next = prev->next->next;
        
        return dummy->next;
    }
};
206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

提示:

链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
掌握思路一(常规链表操作)
  • 常规的链表操作,注意事项见注释部分
  • 要额外注意ListNode* pre = nullptr;不能写成auto pre = nullptr;
  • 单是给pre下定义,右边没有相关变量类型信息,用auto会报错,如下所示
  • Line 21: Char 19: error: assigning to 'nullptr_t' from incompatible type 'ListNode *'
    pre = p; //pre更改为为当前结点,下一个结点就能指过来
    ^
    1 error generated.
  • 要注意auto的使用,虽然很好用还是得留意一下
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        //遍历一遍这个链表
        //为了不丢失下个结点的信息,在操作结点时要把其next先保存起来
        ListNode* pre = nullptr; 	//初始化空结点,表示反转后最后一个元素指为空
        auto p = head;		
        while (p) {
            auto q = p->next;        //要操作p,先把原本的next存起来让p能正常遍历下去
            p->next = pre;
            pre = p;            //pre更改为为当前结点,下一个结点就能指过来
            p = q;
        }
        return pre;             //pre最后会指向原链表最后的结点,而p会变成null终止循环
    }
};
了解思路二(递归)
  • 递归很妙但是不好想,实战如果能记起来可以快速写
  • 其实如果熟练的话用思路一也挺快的,所以没必要刻意去记
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !head->next) {
            return head;
        }
        ListNode* newHead = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return newHead;
    }
};

此处内容在寝室电脑


141. 环形链表
给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

链表中节点的数目范围是 [0, 10^4]
-10^5 <= Node.val <= 10^5
pos 为 -1 或者链表中的一个有效索引 。
掌握思路一(哈希表)
  • 熟悉了哈希表之后,这题最先想到的应该就是用哈希表来做
  • 注意些小细节,是用set而不是map,是用insert()而不是push_back()
class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> hash;	//这里也要注意unordered_set hash而不是unordered_set hash
		//遍历一遍链表,如果读取的结点在哈希表里,说明访问过,存在环,不在哈希表则放入哈希表中
        while (head) {
            if (hash.count(head)) return true;
            hash.insert(head);
            head = head->next;
        }
        
        return false;
    }
};
了解思路二(快慢指针)
  • 官方题解之一,如果不存在环,则快指针与慢指针不可能相遇,存在则有可能,但还是推荐哈希做法
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) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};

104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3
掌握思路一(自己写的递归)
  • 个人认为递归在二叉树的应用非常重要,熟练之后可以比较快速的解题
  • 步骤一:先写递归出口,这里设计的递归出口是if (!root->left && !root->right) return 1;也就是找到叶子结点时为递归出口
  • 步骤二:考虑非递归出口情况,为if (root->left)if (root->right)两种情况,存在左右结点说明深度也要加1,用1 + maxDepth(root->left);1 + maxDepth(root->right);找到递归出口,这样就算出了左右子树的最大深度,再取最大值即可
/**
 * 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) return 0;
        if (!root->left && !root->right) return 1;
        int res1 = 0;
        int res2 = 0;
        if (root->left) res1 = 1 + maxDepth(root->left);
        if (root->right) res2 = 1 + maxDepth(root->right);
        return max(res1, res2);
    }
};
掌握思路二(官方题解的递归)
  • 如果我们知道了左子树和右子树的最大深度 lr,那么该二叉树的最大深度即为max(l,r)+1
  • 左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度
  • 具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1)O(1) 时间内计算出当前二叉树的最大深度
  • 递归在访问到空节点时退出
  • 对比上面的递归可以发现,这里的递归出口是空结点,上面递归出口为叶子结点
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};
了解思路三(队列实现广度优先搜索)
  • 我们也可以用「广度优先搜索」的方法来解决这道题目,但我们需要对其进行一些修改,此时我们广度优先搜索的队列里存放的是「当前层的所有节点」
  • 每次拓展下一层的时候,不同于广度优先搜索的每次只从队列里拿出一个节点,我们需要将队列里的所有节点都拿出来进行拓展
  • 这样能保证每次拓展完的时候队列里存放的是当前层的所有节点,即我们是一层一层地进行拓展
  • 最后我们用一个变量 ans 来维护拓展的次数,该二叉树的最大深度即为 ans
  • 能用递归尽量走递归吧
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;		//遍历完一层后ans加1
        } 
        return ans;
    }
};
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

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

节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。

示例 1:
输入:root = [2,1,3]
输出:true
示例 2:

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:
树中节点数目范围在[1, 104]-2^31 <= Node.val <= 2^31 - 1
掌握思路一(中序遍历递归)
  • 要清楚二叉搜索树就是中序排列递增的树
  • 还要特别注意这题的范围,在赋一个最小的初值时候要用LLONG_MIN
  • 补充了解,INT和long都是32位的,long long 为64位
  • INT_MIN:-2147483648
  • INT_MAX:2147483647
  • LONG_MIN:-2147483648
  • LONG_MAX:2147483647
  • LLONG_MIN:-9223372036854775808
  • LLONG_MAX:9223372036854775807
class Solution {
public:
    long long last = LLONG_MIN;						//下限值,初始为最小值,放在外面作用全局
    bool isValidBST(TreeNode* root) {
        if(!root) return true;

        if(!isValidBST(root->left)) return false;   //判断左子树
	
        if(root->val <= last) return false;			//判断当前
        last = root->val;                           //当前值存入last,为右子树的下限
        
        if(!isValidBST(root->right)) return false;
    
        return true;
    }
};
掌握思路二(栈)
  • 要掌握二叉树的三种遍历
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;
    }
};
101. 对称二叉树
掌握思路一(递归)
  • 两个子树互为镜像当且仅当:
    • 两个子树的根节点值相等;
    • 第一棵子树的左子树和第二棵子树的右子树互为镜像,且第一棵子树的右子树和第二棵子树的左子树互为镜像;
class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        return !root || dfs(root->left, root->right);
    }

    bool dfs(TreeNode*p, TreeNode*q)
    {
        if (!p || !q) return !p && !q;
        return p->val == q->val && dfs(p->left, q->right) && dfs(p->right, q->left);
    }
};
88. 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

提示:

nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-10^9 <= nums1[i], nums2[j] <= 10^9
掌握思路一(简单双指针)
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        vector<int> res;

        int i = 0, j = 0;
        while (i < m && j < n) {
            if (nums1[i] <= nums2[j]) {
                res.push_back(nums1[i]);
                i++;
            } else {
                res.push_back(nums2[j]);
                j++;
            }
        }
        while (i < m) {
            res.push_back(nums1[i]);
            i++;
        }
        while (j < n) {
            res.push_back(nums2[j]);
            j++;
        }

        nums1 = res;
    }
};
掌握思路二(合并后排序)
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        for (int i = 0; i != n; ++i) {
            nums1[m + i] = nums2[i];
        }
        sort(nums1.begin(), nums1.end());
    }
};
278. 第一个错误的版本
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
     
示例 1:

输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false 
调用 isBadVersion(5) -> true 
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。

提示:

1 <= bad <= n <= 2^31 - 1
掌握思路一(简单二分问题)
  • 为防止溢出,应将int mid = (left + right) / 2;写成int mid = left + (right - left) / 2;,这种写法值得记录
  • 由于区间以midmid+1为界,所以mid = (left + right) / 2
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int left = 1;
        int right = n;
        while (left < right) {
            //int mid = (left + right) / 2;
            int mid = left + (right - left) / 2; // 防止计算时溢出
            if (isBadVersion(mid)) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
};
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1+ 12. 2 阶

示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1+ 1+ 12. 1+ 23. 2+ 1 阶
 

提示:

1 <= n <= 45
掌握思路一(典型的斐波那契数列问题)
  • 我记得递归会超时
class Solution {
public:
    int climbStairs(int n) {
        
        int a = 1, b = 2;

        if (n == 1) return 1;
        if (n == 2) return 2;

        int sum = 0;

        for (int i = 3; i <= n; i++ ) {
            sum = a + b;
            a = b;
            b = sum;
        }

        return sum;
    }
};
掌握思路二(典型动态规划)
  • 学完动态规划再来补充
class Solution {
public:
    int climbStairs(int n) {
        int p = 0, q = 0, r = 1;
        for (int i = 1; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
};
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
     
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
 

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400
掌握思路一(动态规划)
  • 这题是经典的DP问题,这里对DP问题做一个初步的小结
    • DP 问题一般步骤
      1. 定义dp数组中的dp[i];
      2. 找dp数组间的关系式(核心
      3. 写出不能由关系式计算得到的初始边界值
    • 以此题为例
      1. 定义dp数组中的dp[i]dp[i] 表示表示前 i 间房屋能偷窃到的最高总金额
      2. 找dp数组间的关系式 :根据题意,要么偷前 i - 2 家和 i 家,要么偷前 i - 1 家不偷 i 家,取两种可能偷法的最大值
        • 则关系为dp[i] = max (dp[i - 2] + nums[i], dp[i - 1]);
      3. 写出不能由关系式计算得到的初始边界值 :看到 i - 2,很容易直到边界的 i 为 0 和 1
        • dp[0] = nums[0];
        • dp[1] = max(nums[0], nums[1]);
  • 动态规划是非常重要的思想,之后要多练习总结
  • 该题目中,我觉得可以养成一种习惯,看这两句:
  • if (n == 1) return nums[0];if (n == 2) return max(nums[0], nums[1]);
  • 这个细节是不用靠dp数组来判断的边界情况就直接返回,这样在计算初始的dp[0]dp[1]就能保证数组不越界
  • 比如说如果不写 if (n == 1) return nums[0];,那么输入的数组长度为 1 时,计算dp[1]会越界而报错
class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.empty()) return 0;     		//空数组,无家可偷,返回0
        
        int n = nums.size();            		//经典记录下数组长度

        if (n == 1) return nums[0];             //不用靠dp数组来判断的边界我们直接返回
        if (n == 2) return max(nums[0], nums[1]);   

        vector<int> dp = vector<int>(n, 0);     //初始化dp数组为0;

        dp[0] = nums[0];                        //对应直接返回的情况的值
        dp[1] = max(nums[0], nums[1]);          //dp方程算不到的初始值先赋值

        for (int i = 2; i < n; i++) {           //两个房子以上用dp数组算
            dp[i] = max (dp[i - 2] + nums[i], dp[i - 1]);
        }

        return dp[n - 1];

    }
};
204. 计数质数
给定整数 n ,返回 所有小于非负整数 n 的质数的数量 。

示例 1:
输入:n = 10
输出:4
解释:小于 10 的质数一共有 4, 它们是 2, 3, 5, 7 。

示例 2:
输入:n = 0
输出:0
   
示例 3:
输入:n = 1
输出:0

提示:

0 <= n <= 5 * 10^6
掌握思路一(枚举判断)
  • 最容易想到的办法,就是循环到 i * i < n 挨个来判断它是否是素数
  • 该办法在 leetcode 这题上会超时,连官方的题解一也会超时,但我觉得还是得会
class Solution {
public:
    int countPrimes(int n) {
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (isP(i)) cnt++;
        }
        return cnt;
    }

    bool isP (int x) {
        if (x == 0 || x == 1) return false;
        for (int i = 2; i * i <= x; i++) {
            if (x % i == 0) return false;
        }
        return true;
    }
};
掌握思路二(筛选法求素数)
  • 上面方法会超时,所以也很有必要掌握筛选法求素数
class Solution {
public:
    int countPrimes(int n) {
        if (n < 2) return 0;
        int cnt = 0;
        vector<bool> st = vector<bool>(n, false);	//初始化一个st数组,用来记录该数是否被筛
        for (int i = 2; i < n; i++) {
            if (st[i]) continue;					//该数未被筛,cnt++
            cnt++;									//这里计算素数个数用cnt,若要得到素数数组则另开一个数组存放这个数
            for (int j = 2 * i; j < n; j += i)	
                st[j] = true;						//开筛开筛
        }
        return cnt;
    }
};
3 的幂
给定一个整数,写一个函数来判断它是否是 3 的幂次方。如果是,返回 true ;否则,返回 false 。

整数 n 是 3 的幂次方需满足:存在整数 x 使得 n == 3^x

示例 1:
输入:n = 27
输出:true

示例 2:
输入:n = 0
输出:false

示例 3:
输入:n = 9
输出:true

示例 4:
输入:n = 45
输出:false
 
提示:

-2^31 <= n <= 2^31 - 1
掌握思路一(试除法)
  • 3的幂除以3再对3取余会一直为0,除到底变成1了说明是3的幂
class Solution {
public:
    bool isPowerOfThree(int n) {
        while (n && n % 3 == 0) {
            n /= 3;
        }
        if (n == 1) return true;
        else return false;
    }
};
了解思路二(约数来判断)
  • 32位有符号整数最大的3的幂是3的19次方,判断是否为其约数即可
class Solution {
public:
    bool isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;
    }
};
13. 罗马数字转整数
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000
例如, 罗马数字 2 写做 II ,即为两个并列的 112 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5)X (10) 的左边,来表示 49。
X 可以放在 L (50)C (100) 的左边,来表示 4090。 
C 可以放在 D (500)M (1000) 的左边,来表示 400900。
给定一个罗马数字,将其转换成整数。

示例 1:
输入: s = "III"
输出: 3
    
示例 2:
输入: s = "IV"
输出: 4
    
示例 3:
输入: s = "IX"
输出: 9
    
示例 4:
输入: s = "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.

示例 5:
输入: s = "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
 
提示:

1 <= s.length <= 15
s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内
题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。
关于罗马数字的详尽书写规则,可以参考 罗马数字 - Mathematics 。
掌握思路一(模拟)
  • 这题主要是要加深对HashMap的理解,将字符和数字对应起来
class Solution {
private:
    unordered_map<char, int> symbolValues = {					//简历unordered_map对应表
        {'I', 1},
        {'V', 5},
        {'X', 10},
        {'L', 50},
        {'C', 100},
        {'D', 500},
        {'M', 1000},
    };

public:
    int romanToInt(string s) {
        int ans = 0;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            int value = symbolValues[s[i]];
            if (i < n - 1 && value < symbolValues[s[i + 1]]) {	//按照规则进行加或减,不要想复杂了
                ans -= value;
            } else {
                ans += value;
            }
        }
        return ans;
    }
};
191. 位1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
    
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
    
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

输入必须是长度为 32 的 二进制串 。
掌握思路一(lowbit的用法)
  • 返回n的最后一位1:lowbit(n) = n & -n
  • 也就是说,每次减去最后的一位1,计数器就加1,直到0为止
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int res = 0;
        while (n) n -= n & -n, res ++ ;
        return res;
    }
};
掌握思路二(用n的第k位去算)
  • 求n的第k位数字: n >> k & 1
  • 那么,一直读第k位,读到1计数加1,读完为止
  • 题目中说输入必须是长度为 32 的 二进制串,所以读32次即可
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int res = 0;
        for (int i = 0; i < 32; i ++ )
            res += n >> i & 1;
        return res;
    }
};
掌握思路三(把最低位的1变为0)
  • 官方题解中,提到运算 n & (n−1),其运算结果恰为把 n的二进制位中的最低位的 1 变为 0 之后的结果
  • 在实际代码中,我们不断让当前的 n 与 n - 1 做与运算,直到 n 变为 0 即可
  • 可以记下来
class Solution {
public:
    int hammingWeight(uint32_t n) {
        int ret = 0;
        while (n) {
            n &= n - 1;
            ret++;
        }
        return ret;
    }
};
461. 汉明距离
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。

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

示例 1:
输入:x = 1, y = 4
输出:2
解释:
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑
上面的箭头指出了对应二进制位不同的位置。
    
示例 2:
输入:x = 3, y = 1
输出:1
 
提示:

0 <= x, y <= 2^31 - 1
掌握思路一(求1的个数)
  • 其实跟上题一样,就是就两数异或之后1的个数,这里复习一下
  • 多学一个内置函数 __builtin_popcount 用来表示二进制表示的1的个数
class Solution {
public:
    int hammingDistance(int x, int y) {
        //也就是求X和y异或之后的1的个数
        int n = x ^ y;
        int res = 0;
        //方法1 lowbits算,返回最后一个1,不断减去即可
        // while (n) {
        //     n -= n & -n;
        //     res++;
        // }

        //方法2 不断移位去求1的个数
        // while (n) {
        //     res += n & 1;
        //     n >>= 1;
        // }

        //方法3 内置函数法
        res = __builtin_popcount(n);

        //方法4 将最后一个1换0看能换多少次
        // while (n) {
        //     n &= n - 1;
        //     res++;
        // }

        return res;
    }
};
190. 颠倒二进制位
颠倒给定的 32 位无符号整数的二进制位。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。
 
示例 1:
输入:n = 00000010100101000001111010011100
输出:964176192 (00111001011110000010100101000000)
解释:输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
     因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
     
示例 2:
输入:n = 11111111111111111111111111111101
输出:3221225471 (10111111111111111111111111111111)
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。

提示:

输入是一个长度为 32 的二进制字符串
掌握思路一(逐位去加)
  • 不断左移加上原数的第i位即可
  • 还是这个:求n的第k位数字: n >> k & 1
class Solution {
public:
    uint32_t reverseBits(uint32_t n) {
        uint32_t res = 0;
        for (int i = 0; i < 32; i++) {
            res = (res << 1) + (n >> i & 1);
        }
        return res;
    }
};
118. 杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

示例 2:
输入: numRows = 1
输出: [[1]]
 
提示:

1 <= numRows <= 30
掌握思路一(递推)
  • 递推,从第三层(i = 2)开始由前一行的结果来计算得到
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> res;
        for (int i = 0; i < numRows; i++) {
            vector<int> temp = vector<int>(i+1, 1);					//要记住这种初始化vector的方法,这里初始化为1

            if (i >= 2) {											//前两行都是1,从第三行开始杨辉三角计算中间部分的值
                for (int j = 1; j < i; j++) {
                    temp[j] = res[i - 1][j - 1] + res[i - 1][j];
                }
            }
            res.push_back(temp);
        }

        return res;
    }
};
20. 有效的括号
掌握思路一(栈)
  • 这题不止做了一遍,这题是非常好的栈的题目,一定要重点掌握、
  • 这里给出两次自己做的答案
class Solution {
public:
    bool isValid(string s) {
        stack<char> stk;
        int n = s.size();

        for (int i = 0; i < n; i++) {								//可以用auto简写
            if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
                stk.push(s[i]);
            } 
            else if (s[i] == ')') {
                if (!stk.empty() && stk.top() == '(') stk.pop();	//先判断!stk.empty()
                else return false;
            } 
            else if (s[i] == ']') {
                if (!stk.empty() && stk.top() == '[') stk.pop();
                else return false;
            }
            else if (s[i] == '}') {
                if (!stk.empty() && stk.top() == '{') stk.pop();
                else return false;
            }
        }

        return stk.empty();

    }
};
class Solution {
public:
    
    bool isValid(string s) {
        //用栈来做
        stack<char> stk;

        for (auto c : s) {
            if (c == '(' || c == '[' || c == '{') stk.push(c);
            else if (c == ')') {
                //要特别注意先判断stk.empty(),若stk.empty()为1表示栈空,直接返回false,不判断后面的stk.top() != '('
                //如果写法为stk.top() != '(' || stk.empty() ,那么在输入为")"时执行stk.top()会报错,因为此时为空栈!
                if (stk.empty() || stk.top() != '(') return false;
                stk.pop();
            } else if (c == ']') {
                if (stk.empty() || stk.top() != '[') return false;
                stk.pop();
            } else  {
                if (stk.empty() || stk.top() != '{') return false;
                stk.pop();
            }            
        }

        return stk.empty();
    }
};
268. 丢失的数字
给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。
 
示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
    
示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
    
示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
    
示例 4:
输入:nums = [0]
输出:1
解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。
 
提示:

n == nums.length
1 <= n <= 104
0 <= nums[i] <= n
nums 中的所有数字都 独一无二
掌握思路一(排序后判断)
  • 很简单的一道题,可以短时间了解多种方法
  • 排序后判断对应位置的数是否是其索引值即可
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        sort (nums.begin(), nums.end());
        int n = nums.size();
        int i;
        for (i = 0; i < n; i++) {
            if (nums[i] != i) return i;
        }

        return i;
    }
};
掌握思路二(哈希表)
  • 练习一下哈希表的使用

  • 把存在的值放入哈希表,再把不存在的值找出来

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        unordered_set<int> set;
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            set.insert(nums[i]);
        }
        int missing = -1;
        for (int i = 0; i <= n; i++) {
            if (!set.count(i)) {
                missing = i;
                break;
            }
        }
        return missing;
    }
};
掌握思路三(异或运算的应用)
  • 所有的数和该有的数异或运算,得到的值为唯一缺少的那个值
  • 1^1^2^2^3 = 3
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int res = 0;
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            res ^= nums[i];
        }
        for (int i = 0; i <= n; i++) {
            res ^= i;
        }
        return res;
    }
};
102. 二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:
输入:root = [1]
输出:[[1]]

示例 3:
输入: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:
    vector> levelOrder(TreeNode* root) {
        vector> res;    
        if (!root) return res;      //养成习惯,先判空

        queue q;         //层序遍历用队列来做
        q.push(root);

        while (!q.empty()) {
            vector level;
            int n = q.size();

            while (n--) {                               //遍历队列中的结点,依次读它们的子结点
                auto t = q.front();                     //用t指向队首结点
                q.pop();                                //指完后出栈
                level.push_back(t->val);                //出栈后保存结点值
                if (t->left) q.push(t->left);			//如果有子结点,也要入栈
                if (t->right) q.push(t->right);
            }
            res.push_back(level);
        }

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

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
    
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案
    
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3][3,1] 都是高度平衡二叉搜索树。
 
提示:

1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums 按 严格递增 顺序排列
掌握思路一(递归)
  • 递归建立整棵二叉树。
  • 每次以中点为根,以左半部分为左子树,右半部分为右子树。先分别递归建立左子树和右子树,然后令根节点的指针分别指向两棵子树。
class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return build(nums, 0, nums.size() - 1);
    }

    TreeNode* build(vector<int> &nums, int l, int r) {
        if (l > r) return NULL;
        int mid = (l + r) >> 1;
        auto root = new TreeNode(nums[mid]);
        root->left = build (nums, l, mid -1);
        root->right = build (nums, mid + 1, r);

        return root;
    }
};

你可能感兴趣的:(基础算法,leetcode,刷题笔记,leetcode,算法,c++)