LeetCode 热题 HOT 100 打卡计划 | 第二十五天 | 每日进步一点点

173 阅读4分钟

图片.png

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情

287. 寻找重复数

思路

(二分,抽屉原理) O(nlogn)

抽屉原理: n+1 个苹果放在 n 个抽屉里,那么至少有一个抽屉中会放两个苹果。

在这个题目里,一共有 n+1 个数,每个数的取值范围是1n,所以至少会有一个数出现两次。

然后我们采用分治的思想,将每个数的取值的区间[1, n]划分成[1, n/2][n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。 注意这里的区间是指 数的取值范围,而不是 数组下标

划分之后,左右两个区间里一定至少存在一个区间,区间中数的个数大于区间长度。 这个可以用反证法来说明:如果两个区间中数的个数都小于等于区间长度,那么整个区间中数的个数就小于等于n,和有n+1个数矛盾。

因此我们可以把问题划归到左右两个子区间中的一个,而且由于区间中数的个数大于区间长度,根据抽屉原理,在这个子区间中一定存在某个数出现了两次。

依次类推,每次我们可以把区间长度缩小一半,直到区间长度为1时,我们就找到了答案。

时间复杂度分析: 每次会将区间长度缩小一半,一共会缩小 O(logn) 次。每次统计两个子区间中的数时需要遍历整个数组,时间复杂度是 O(n)。所以总时间复杂度是 O(nlogn)。

c++代码

 class Solution {
 public:
     int findDuplicate(vector<int>& nums) {
         int l = 1, r = nums.size() - 1;  //取值范围
         while(l < r){
             int mid = (l + r) / 2;
             int s = 0;
             for(int x : nums) s += x >= l && x <= mid; //左区间数的个数
             if(s > mid - l + 1) r = mid;
             else l = mid + 1;
         }
         return r;
     }
 };

297. 二叉树的序列化与反序列化

思路

(前序遍历序列化) O(n)

  1. 序列化:对整个二叉树进行先序遍历的序列存起来,同时需要把每个结点的空节点使用"#"进行标记,例如样例的顺序是1,2,#,#,3,4,#,#,5,#,#
  2. 反序列化:对整个字符串按照","进行分割,把所有的元素按序存到链表中(链表元素的顺序是先序序列),按先序遍历的方式拿链表的元素,每次拿第一个元素作为根结点,并删除链表中的第一个元素,然后递归到左儿子做同样的操作,递归到右儿子做同样的操作。注意:若第一个元素是"#",表示该节点是null,直接返回`null

时间复杂度分析: 每个节点仅遍历两次,故时间复杂度为 O(n)。

c++代码

 // 前序遍历 DFS
 class Codec {
 public:
     string path;
 ​
     // Encodes a tree to a single string.
     string serialize(TreeNode* root) {
         // if (root) dfs_s(root);  加if (root) 是错的
         dfs_s(root); // 不能加 if (root),因为空树 也要编码成"#,"
         return path;
     }
 ​
     // 序列化 dfs_s() 的返回类型 是 void
     void dfs_s(TreeNode* root) {
         if (!root) path += "#,"; // "#,"是字符串,要双引号
         else {
             path += to_string(root->val) + ','; // 单个字符,用单引号
             dfs_s(root->left);
             dfs_s(root->right);
         }
     }
 ​
 ​
     // Decodes your encoded data to tree.
     TreeNode* deserialize(string data) {
         int idx = 0;
         return dfs_d(data, idx); // 这里不能直接 dfs_s(data, 0),0是常量,应该是变量
     }
 ​
     // 反序列化 dfs_d() 的 返回类型是 TreeNode*, 参数 是 字符串data,和 当前 字符串下标
     TreeNode* dfs_d(string& data, int& idx){ // 第1个&防止拷贝,第2个&相当于把idx作为全局变量
         if (data[idx] == '#'){
         idx += 2;
         return NULL;
         } else {
             int k = idx;
             while (data[idx] != ',') idx ++; // 跳出while循环时,data[idx] == ','
             auto root = new TreeNode(stoi(data.substr(k, idx - k))); // stoi 和 data.substr(k, num) 用法
             idx ++ ; // 从',' 往下 跳一位
             root->left = dfs_d(data, idx);
             root->right = dfs_d(data, idx);
             return root; // return root; 不要写在下面
         }
         // return root;
     }
 ​
 };
 ​

300. 最长递增子序列

思路

(动态规划) O(n^2)

状态表示: f[i]表示以nums[i]为结尾的严格递增子序列的最大长度。

集合划分:nums[i]为结尾的严格递增子序列前一个数是nums[0]nums[1],,,nums[i-1]

状态计算: f[i] = max(f[i],f[j] + 1) (j<i && nums[j] < nums[i])

图示说明:

图片.png

时间复杂度分析: 状态数量为O(n),状态计算为O(n),故时间复杂度为O(n^2)。

c++代码

 class Solution {
 public:
     int lengthOfLIS(vector<int>& nums) {
         int n = nums.size();
         vector<int>f(n + 1);
         int res = 0;
         for(int i = 0; i < n; i++){
             f[i] = 1;
             for(int j = 0; j < i; j++)
                 if(nums[i] > nums[j])
                     f[i] = max(f[i], f[j] + 1);
             res = max(res, f[i]);        
         }
         return res;
     }
 };
- ```