代码随想录一刷(Day01 ~ Day04)

代码随想录一刷

数组

1. 数组基础理论

对于C/C++,无论是一维还是二维,地址都是连续的

2. 二分查找

单点查找

闭区间写法

区间查找

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int nums_size = nums.size();
        int l = 0, r = nums_size - 1;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) return mid;
            else if (nums[mid] > target) r = mid - 1;
            else l = mid + 1;
        }
        return r + 1;	//	或return l;	此时闭区间对应 l = r + 1
    }
};

区间左闭右开写法

区间查找

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int nums_size = nums.size();
        int l = 0, r = nums_size;
        while (l < r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) return mid;
            else if (nums[mid] > target) r = mid;
            else l = mid + 1;
        }
        return r;	// 或 return l; 此时 l = r
    }
};

查找区间

大于某个数的最小位置和最大位置

class Solution {
    int get_left_border(vector<int> &nums, int target, int n) {
        int l = 0, r = n;
        int left_border = -2;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] >= target) {   /* 寻找比目标值小的第一个位置 */
                r = mid - 1;        // 寻找比目标值小的第一个下标位置
                left_border = r;    // 记录比目标值小的第一个下标位置
            }
            else {  /* 查找区间变为[mid + 1, r] */
                l = mid + 1;
            }
        }
        return left_border;
    }

    int get_right_border(vector<int> &nums, int target, int n) {
        int l = 0, r = n;
        int right_border = -2;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if (nums[mid] <= target) {  /* 寻找比目标值大的第一个位置 */
                l = mid + 1;        // 寻找比目标值大的第一个下标
                right_border = l;   // 记录比目标值大的第一个下标
            } else {    /* 查询区间变为[l, mid - 1] */
                r = mid - 1;
            }
        }
        return right_border;
    }
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int nums_size = nums.size();
        int left_border = -2, right_border = -2;
        left_border = get_left_border(nums, target, nums_size - 1);    // 找到小于目标的第一个元素下标
        right_border = get_right_border(nums, target, nums_size - 1);  // 找到大于目标的第一个元素下标
        
        // 情况(1): 目标小于数组最小元素,或大于数组最大元素
        if (left_border == -2 || right_border == -2) return {-1, -1};   
        // 情况(2): 如果存在目标值(或多个目标值),返回正确的闭区间
        if (right_border - left_border > 1) return {left_border + 1, right_border - 1}; 
        // 情况(3): target应该插入数组中,但是不在数组中
        return {-1, -1};    
    }
};

例题实战

1. 69. x 的平方根

    思路1 取对数
         $a^2 = x$ 由对数运算性质则 $a = e^{\frac{1}{2}lnx}$

class Solution {
    #define ll long long
public:
    int mySqrt(int x) {
        if (x == 0) return 0;
        double ans = exp(0.5 * log(x));
        ll ret = (ll)ans;
        // (ll)ans强转之后会出现四舍五入,而此题要求去尾
        return ((ret + 1) * (ret + 1) <= x) ? ret + 1 : ret;
    }
};

    思路2 二分查找
        在区间[0, x]中查找a,使得 $a^2 <= x$,返回第一个a
        找到a^2 == x的第一个大于a的ans,返回ans-1

class Solution {
    #define ll long long
public:
    int mySqrt(int x) {
        int l = 0, r = x;
        int ans = 0;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if ((ll)mid * mid <= x) {
                l = mid + 1;
                ans = l;    /* 记录a^2大于x的第一个下标位置 */
            } else {
                r = mid - 1;
            }
        }
        return ans - 1;
    }
};

    思路3 泰勒级数
        利用泰勒级数展开,逼近目标值
        $f(x) = x^2$,方法如下图所示:从$x_0 = C$开始,找到在曲线$f(x_0)$做切线,交$x$轴为$(x_1, 0)$点,反复迭代,直到$|x_i - \sqrt{x}| < 1e-6$,结束迭代,返回结果

在这里插入图片描述](https://img-blog.csdnimg.cn/4dbd31144d18490883511b4015a0eb5b.png)

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) return 0;

        double C = x, x0 = x;
        while (true) {
            double xi = 0.5 * (x0 + C / x0);    
            if (fabs(xi - x0) < 1e-6) break;
            x0 = xi;
        }
        return int(x0);
    }
};

2. 367. 有效的完全平方数

    思路
        区间[0, num]内二分查找,找到返回true,否则返回false。

class Solution {
    #define ll long long
public:
    bool isPerfectSquare(int num) {
        int l = 0, r = num;
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            if ((ll)mid * (ll)mid == num) return true;
            else if ((ll)mid * (ll)mid > num) r = mid - 1;
            else l = mid + 1;
        }
        return false;
    }
};

3. 移除元素

例题实战

1. 27. 移除元素

    思路
        利用双指针$O(n)$时间解决问题。
        利用快指针fast_index遍历数组,用慢指针slow_index指向当前新数组的最后一位。
         (1) 当删除一个元素时(nums[fast_index] == val),新数组不用扩容,slow_index不动,fast_index继续向后遍历;
         (2) 不用删除的元素,向前移动到slow_index位置,新数组扩容,slow_index++。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slow_index = 0;
        int nums_size = nums.size(), fast_index;
        for (fast_index = 0; fast_index < nums_size; ++fast_index) {
            if (nums[fast_index] != val) {
                nums[slow_index++] = nums[fast_index];
            }
        }
        return slow_index;
    }
};

2. 26.删除排序数组中的重复项

    思路
        利用一个慢指针指向新数组的最后一个元素后一位,用快指针遍历数组,时间复杂度$O(n)$
         (1) 出现重复数字,++fast_index,继续遍历,但是新数组不扩展
         (2) 出现不同数字,将新的不同数字加入新数组,并将slow_index++,更新到新数组最后一个元素后。

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int nums_size = nums.size();
        int slow_index = 1, fast_index = 1; // num[0]不用移动,直接保留即可
        for (fast_index = 1; fast_index < nums_size; ++fast_index) {
            if (nums[fast_index] != nums[fast_index - 1]) { /* 跳过重复元素,遇到不同元素向前移动 */
                nums[slow_index++] = nums[fast_index];
            }
        }
        return slow_index;
    }
};

3. 283.移动零

    思路
        利用双指针,一个指向最低位的零,一个指向零后的第一个数字,交换二者,继续向后更新指针。时间复杂度$O(n)$

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int nums_size = nums.size();
        int zero_pos = 0, nums_pos = 0;
        while (nums_pos < nums_size && zero_pos < nums_size) {
            if (nums[nums_pos] == 0) nums_pos++;
            if (nums[zero_pos] != 0) zero_pos++;
            if (nums_pos >= nums_size || zero_pos >= nums_size) break;  // 防止地址访问越界
            if (nums[nums_pos] != 0 && nums[zero_pos] == 0 && nums_pos > zero_pos) swap(nums[nums_pos++], nums[zero_pos++]); // 零和其之后的第一数字交换位置
            else if (nums[nums_pos] != 0 && nums[zero_pos] == 0 && nums_pos <= zero_pos) nums_pos = zero_pos + 1; // 数字在零前面,不满足条件,继续向后找零之后的数字,可以避免无用的访问(访问多个零之前的非零数字)
        }
    }
};

4. 844.比较含退格的字符串

    思路
        双指针,慢指针指向读入回退之后新字符串的结尾处,快指针遍历原字符串。快指针遇到'#',慢指针-1,表示删除新字符串末尾字符。

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int s_len = s.length();
        int t_len = t.length();
        int l = 0, r = 0;
        while (r < s_len) { /* 获取读入回退后的s */
            if (s[r] != '#') s[l++] = s[r++];
            else {
                r++;
                l = l >= 1 ? l - 1 : 0;
            }
        }
        s_len = l;  // 更新处理后的s长度
        l = 0, r = 0;
        while (r < t_len) { /* 获取读入回退后的t */
            if (t[r] != '#') t[l++] = t[r++];
            else {
                r++;
                l = l >= 1 ? l - 1 : 0;
            }
        }
        t_len = l;  // 更新处理后的t长度

        // 判断两字符串是否相等
        if (s_len != t_len) return false;
        for (int i = 0; i < s_len; ++i) if (s[i] != t[i]) return false;
        return true;
    }
};

4. 有序数组的平方

例题实战

1. 977.有序数组的平方

    思路
        考虑到数组一开始有序,有正有负,那么平方后的值肯定是两边大,中间小,于是,利用双指针,一个指向最左边,一个指向最右边,比较两指针位置数值平方的大小,将较大值存为答案,并将指针向中间移动。只需遍历一边数组,时间复杂度为$O(n)$

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int nums_size = nums.size();
        vector<int> result(nums_size, 0);
        int l = 0, r = nums_size - 1, k = nums_size - 1;
        while (l <= r) {
            int left_s = nums[l] * nums[l];
            int right_s = nums[r] * nums[r];
            if (left_s <= right_s) {
                r--;
                result[k--] = right_s;
            } else {
                l++;
                result[k--] = left_s;
            }
        }
        return result;
    }
};

5. 长度最小的子数组

例题实战

1. 209. 长度最小的子数组

    思路
        维护一个区间和小于target的滑动窗口

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int nums_size = nums.size(), i;
        int l = 0, r = 0;
        int sum = 0, ans = 1e5 + 10;
        for (i = 0; i < nums_size; ++i) {
            sum += nums[i];
            r = i;
            while (sum >= target) { /* 如果区间和大于target不断去掉窗口起始位置的值,直到区间和小于target */
                ans = min(ans, r - l + 1);
                sum -= nums[l++];
            }
            cout << ans << endl;
        }
        return ans == 1e5+10 ? 0 : ans;
    }
};

2. 904. 水果成篮

    思路
        题意翻译的什么玩意儿?应该是数组中的每个数字代表该树生产的水果是哪一种,比如1代表苹果,2代表香蕉,3代表橘子,每个果篮只能放一种水果,两个果篮如果第一个放入了苹果,那该篮子就只能放苹果,除非拿出所有苹果才能放其他水果。
        利用滑动窗口解题,维护窗口内只有两种水果的最长窗口。即只有两种不同数字的最长连续子数组。

方法一: 窗口左侧删除某一种水果不好维护,所以使用unordered_map,当窗口中的水果数量大于两种,则收缩左侧窗口直到窗口内的水果种类等于两种,更新答案,继续向右扩展窗口,加入水果。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int n = fruits.size(), i;
        int type_max = 2;
        unordered_map<int, int> window;
        int l = 0, r = 0;
        int ans = 0;
        for (r = 0; r < n; ++r) {
            window[fruits[r]]++;    // 记录当前种类水果采摘的次数
            while (window.size() > type_max) {  // 种类大于2,窗口左端收缩,直到种类小于等于2
                window[fruits[l]]--;
                if (window[fruits[l]] == 0) window.erase(fruits[l]);    // 从窗口中删除一种水果
                l++;
            }
            ans = max(ans, r - l + 1);
        }
        return ans;
    }
};

方法二: 不使用unordered_map容器。利用hash表记录每种水果出现的次数,窗口的维护同方法一。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int n = fruits.size(), i;
        int type_max = 2;
        int type_tot = 0;
        vector<int> hash(n, 0);
        int l = 0, r = 0;
        int ans = 0;
        for (r = 0; r < n; ++r) {
            hash[fruits[r]]++;  // hash表记录当前窗口中fruits[r]这种水果出现的次数
            if (hash[fruits[r]] == 1) { // 当前水果在窗口中第一次出现
                type_tot++; // 窗口中的水果种类+1
            }
            while (type_tot > type_max) {   /* 从左侧收缩窗口直到窗口中的水果种类等于2 */
                hash[fruits[l]]--;
                if (hash[fruits[l++]] == 0) {   /* 从收缩左侧窗口,并将l+1 */
                    type_tot--;
                    break;
                }
            } 
            ans = max(ans, r - l + 1);  // 更新答案
        }
        return ans;
    }
};

3. 76. 最小覆盖子串

    思路
        和前两道例题的思路异曲同工,记录每种需求的每种字符的出现次数,维护一个滑动窗口,要求窗口内的s的子串中的字符包含t中的所有字符。实现细节见代码注释。

class Solution {
    #define minn -100010
public:
    string minWindow(string s, string t) {
        int s_len = s.length(), i;
        int t_len = t.length();
        int hash[60];   // 每个字符在t中是否出现,若出现,记录出现次数;若未出现,记为-1
        for (i = 0; i < 60; ++i) hash[i] = minn;    // 将t中未出现过的字符初始化为无穷小
        int l = 0, r = 0;
        int type_num = 0;   // 记录还需要出现的字符种类数
        string ans = "";    // 记录答案

        for (i = 0; i < t_len; ++i)  {  /* 求出t中每个字符出现的次数 */
            if (hash[t[i] - 'A'] == minn) {
                hash[t[i] - 'A'] = 1;
                type_num++; // 记录出现的字符的种类数
            }
            else hash[t[i] - 'A']++;    // 记录每种字符需要的数量
        }

        for (r = 0; r < s_len; ++r) {   /* 遍历s中的字符,寻找答案 */
            if (hash[s[r] - 'A'] == minn) continue; // t中未出现的字符,跳过即可
            hash[s[r] - 'A']--; // 对s[r]的需求量-1
            if (hash[s[r] - 'A'] == 0) type_num--;  // 如果s[r]的需求量为0,即在当前窗口中出现的数量与t中相同,则该种类的字符满足要求
            if (type_num == 0) {    // t中的所有字符都出现并且数量足够时,更新答案
                while (hash[s[l] - 'A'] == minn || hash[s[l] - 'A'] < 0) {  /* 窗口中的某些字符数量可能已超过了需求量,可以从左侧收缩窗口长度 */
                    hash[s[l++] - 'A']++;
                }
                if (ans == "" || ans.size() > r - l + 1) {  /* 更新答案 */
                    ans = s.substr(l, r - l + 1);   // 获取区间[l, r]的字串
                }
            }
            while (type_num == 0 && l <= r) {   /* 更新完一次答案后,首先要从左侧收缩,使一种字符的数量不够,再继续向右扩展窗口 */
                if (hash[s[l] - 'A'] != minn) {
                    hash[s[l] - 'A']++;
                    if (hash[s[l] - 'A'] == 1) type_num++;
                    l++;
                    while (l <= r && hash[s[l] - 'A'] == minn) l++; // 将窗口左侧的t中未出现的字符从窗口中丢出
                    if (type_num) break;
                }
            }
        }

        return ans;
    }
};

6. 螺旋矩阵 II

例题实战

1. 59. 螺旋矩阵 II

    思路
        此题没有什么算法,主要是在敲代码之前捋清逻辑,逻辑清晰了,就不容易出问题。
        依次进行上右下左的填充,每次填充都选择左闭右开的区间填充形式,防止产生边界处的冲突。示意图如下:
在这里插入图片描述
        红、绿、橙、棕四种颜色均是前闭后开的填充形式,将最后一个块留给下一次填充,利用for循环中的边界判断即可实现。

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> ans(n, vector<int>(n, 0));
        int start_x = 0, start_y = 0;
        int loop = n / 2;
        int mid = n / 2;
        int i, j;
        int cnt = 1;
        while (loop--) {
            for (j = start_y; j < n - start_y - 1; ++j) ans[start_x][j] = cnt++;    // 上
            for (i = start_x; i < n - start_x - 1; ++i) ans[i][j] = cnt++;  // 右
            for (; j > start_y; --j) ans[i][j] = cnt++; // 下
            for (; i > start_x; --i) ans[i][j] = cnt++; // 左

            start_x++;
            start_y++; 
        }
        if (n & 1) ans[mid][mid] = cnt; // n为奇数时中心要特殊处理
        return ans;
    }
};

2. 54.螺旋矩阵

    思路
        是第三题省掉n == 0的简化版,思路见第三题。

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int n = matrix.size();
        int m = matrix[0].size();
        vector<int> ans(n * m, 0);
        int start_x = 0, start_y = 0;
        int mid = min(n, m) >> 1;
        int loop = mid;
        int cnt = 0;
        int i, j;
        while (loop--) {
            for (j = start_y; j < m - start_y - 1; ++j) ans[cnt++] = matrix[start_x][j];
            for (i = start_x; i < n - start_x - 1; ++i) ans[cnt++] = matrix[i][j];
            for (; j > start_y; --j) ans[cnt++] = matrix[i][j];
            for (; i > start_x; --i) ans[cnt++] = matrix[i][j];

            start_x++;
            start_y++;
        }
        if ((n & 1) || (m & 1)) {
            if (n % 2 == 0 && n < m) return ans;
            if (m % 2 == 0 && m < n) return ans;
            if (n < m) for (i = start_x, j = start_y; j < m - start_y; ++j) ans[cnt++] = matrix[i][j];  // 行较小
            else for (i = start_x, j = start_y; i < n - start_x; ++i) ans[cnt++] = matrix[i][j];    // 列较小
        }
        return ans;
    }
};

3. 剑指Offer 29.顺时针打印矩阵

    思路
        思路和第一题相同, 都是顺时针遍历数组,不过此时行n和列数m可能不相等,要考虑奇偶和大小问题。
         (1) n,m均为奇数,最后较小一方的中间行(列);(特殊情况是n==m,此时剩中间一格)
         (2) n,m一奇、一偶,偶数小,此时没有剩余
         (3) n,m一奇、一偶,偶数大,此时剩余奇数对应的中间行(列)
         (4) n,m均为偶数,没有剩余

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int n = matrix.size();
        if (n == 0) return {};
        int m = matrix[0].size();
        vector<int> ans(n * m, 0);
        int start_x = 0, start_y = 0;
        int mid = (min(n, m) / 2);
        int loop = mid;
        int i, j;
        int cnt = 0;
        while (loop--) {
            for (j = start_y; j < m - start_y - 1; ++j) ans[cnt++] = matrix[start_x][j];
            for (i = start_x; i < n - start_x - 1; ++i) ans[cnt++] = matrix[i][j];
            for (; j > start_y; --j) ans[cnt++] = matrix[i][j];
            for (; i > start_x; --i) ans[cnt++] = matrix[i][j];

            start_x++;
            start_y++;
        }
        if (n & 1 || m & 1) {
            // (1) 如果较小的数是偶数,则没有剩余行(列)直接返回结果
            if (n % 2 == 0 && n < m) return ans;
            if (m % 2 == 0 && m < n) return ans;  
            // (2) 否则,存在剩余行(列),该行是较小奇数的中间行(列)
            if (n < m) for (i = start_x, j = start_y; j < m - start_y; ++j) ans[cnt++] = matrix[i][j];
            else for (i = start_x, j = start_y; i < n - start_x; ++i) ans[cnt++] = matrix[i][j];
        }
        return ans;
    }
};
posted @ 2022-06-17 08:56  番茄元  阅读(59)  评论(0)    收藏  举报