LeetCode精选TOP面试题(中等篇)【出现率降序】

文章目录

143. 重排链表

在这里插入图片描述

(递归)

第一种方法我们可以使用递归的方式处理链表,即我们只需要处理头结点的逻辑,其他的节点只需要递归完成即可。

既然是要一个头结点一个尾节点,那么我们就可以手动地找到链表的尾结点,然后将head节点指向tail节点即可。至于(head, tail)中点的节点就可以递归按照这种逻辑处理即可。

而且可以发现如果一个链表的节点数量小于等于2的话,就可以不用处理直接返回了,所以if (!head || !head->next || !head->next->next) return head;

class Solution {
   
public:
    ListNode* reorder(ListNode* head) {
   
        if (!head || !head->next || !head->next->next) return head;
        ListNode* cur = head;
        while (cur->next->next) {
   
            cur = cur->next;
        }
        ListNode* next = head->next;
        ListNode* tail = cur->next;
        head->next = tail;
        cur->next = nullptr;
        tail->next = reorder(next);
        return head;
    }

    void reorderList(ListNode* head) {
   
        head = reorder(head);
    }
};

(找链表中点+反转链表+合并链表)

本题的难点就在于我们容易将这种链式结构一头一尾的将节点取出,所以使用递归的方式其实可以帮助我们跳过中间的节点而找到尾结点。

而如果我们就是想要使用迭代的方式完成链表的重排的话,我们就必须要解决从尾到头的方式获取节点。其实我们可以使用一个vector将节点都放入数组中,这样就可以从尾到头的获取节点了。但是这样就会浪费O(N)的空间。如果我们想要从尾到头的获取节点的话,其实可以通过反转链表的方式实现。然后再通过二路归并从两个链表中交替获取节点。

所以本题的思路就是反转一半的链表,再二路归并两个链表。而反转一半的链表就要先找到链表的中点,然后再通过迭代的方式反转链表。

class Solution {
   
public:
    void reorderList(ListNode* head) {
   
        // 只遍历一次找到链表中点
        ListNode* fast = head, *slow = head;
        while (fast && fast->next) {
   
            slow = slow->next;
            fast = fast->next->next;
        }
        ListNode* tail = slow->next;
        slow->next = nullptr;
        // 迭代版反转链表
        ListNode* t1 = nullptr;
        while (tail) {
   
            ListNode* next = tail->next;
            tail->next = t1;
            t1 = tail;
            tail = next;
        }
        // 找到规律合并链表
        ListNode* t2 = head;
        ListNode* dummy = new ListNode(0);
        ListNode* cur = dummy;
        while (t1 && t2) {
   
            cur = cur->next = t2;
            t2 = t2->next;
            cur = cur->next = t1;
            t1 = t1->next;
        }
        cur->next = t2;
    }
};

525. 连续数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5jAUz7KA-1640011537425)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1636897891418.png)]

(前缀和+哈希表)

我们需要找到一段区间中0和1的数量相同,最直接的想法就是我们需要将一段区间中的0和1的数量都求出来,然后判断是否相同即可。求出一段区间中的0和1的数量可以使用前缀和的思想将时间复杂度从O(N)降到O(1)。而且如果相比较出出一段区间中的0和1的数量可以不用两个前缀和数组来比较,可以使用一个前缀和数组表示0和1的个数的差值即可,如果结果为0就说明0和1的个数相同,否则0和1的个数不同。

但是即使解决了一段区间中的0和1的个数,但是我们还是需用两层循环去枚举数组中所有连续的区间,这样还是会超时。

我们再来看看计算一段区间中的1和0的数量的公式。[i, i], [i, i - 1], ..., [i, 1],我们需要枚举出i个区间,但是我们只是要求出是否在前i个数组成的区间中存在和前j个数组成的区间中0和1的个数的差值是相同的。所以就可以使用一个哈希表判断s[i]是否存在在哈希表中,并且将前i个数字组成的差值放入哈希表中等待查询即可。

如果数组的下标从1开始的话,那么没有数字组成的区间中0和1个数的差值为0,即hash[0] = 0

class Solution {
   
public:
    int findMaxLength(vector<int>& nums) {
   
        unordered_map<int, int> hash;
        int ans = 0;
        int count = 0;
        int n = nums.size();
        hash[0] = 0;
        for (int i = 1; i <= n; i ++) {
   
            count += nums[i - 1] == 1 ? 1 : -1;
            if (hash.count(count)) {
   
                ans = max(ans, i - hash[count]);
            } else {
   
                hash[count] = i;
            }
        }
        return ans;
    }
};

50. Pow(x, n)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWLFd4Xz-1640011564728)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1636958563843.png)]

(快速幂)

如果我们想要求出一个数字的n次方的话,如果一个一个乘就太慢了。因为n有多大,num就需要乘以一个自身n次。所以我们可以观察是否可以简化这个过程。

我们通常使用位运算的方式简化计算,如果计算numn次方的话,那么我们只需要将n对应的二进制列出来即可。而n的二进制的位置就是ans需要乘的num的位数。所以我们可以预处理出num2,num4,num8,…,而numn=num1+2+4+…+。所以我们就将乘以n次方转化为了n的二进制位数次。

为什么可以想到这种方法?

因为如果想要对一个数字乘方计算进行简化的话,我们只能从n次方入手,而我们需要观察出n的特性,可以发现n可以拆解成二进制的形式计算,每一次通过乘方就是扩大平方倍,而每一个数字都是同二进制,即1,2,4,8…这些数构成的,所以我们通过这种方式对n进行简化计算。

class Solution {
   
public:
    double myPow(double x, int n) {
   
        typedef long long LL;
        bool flag = n < 0;
        double ans = 1;
        for (LL k = (LL)abs(n); k; k >>= 1) {
   
            if (k & 1) ans = ans * x;
            x *= x;
        }
        if (flag) return 1.0 / ans;
        else return ans;
    }
};
class Solution {
   
public:
    double myPow(double x, int n) {
   
        typedef long long LL;
        double ans = 1;
        bool flag = n < 0;
        LL k = abs(n);
        while (k) {
   
            if (k & 1) {
   
                ans = ans * x;
            }
            k >>= 1;
            x *= x;
        }
        if (flag) return 1.0 / ans;
        else return ans;
    }
};

18. 四数之和

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xF4sIDO-1640011594708)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1636961124706.png)]

(递归回溯)

最暴力的方法就是利用回溯算法,将nums数组中的数字每4个为一个组合,求出全部的组合方式。虽然中间使用了剪枝和判重的技巧,但是这样的时间复杂度还是指数级别的,所以还是会超时。

class Solution {
   
public:
    vector<vector<int>> ans;
    vector<int> path;
    typedef long long LL;
    void dfs(vector<int>& nums, LL target, int index) {
   
        if (path.size() == 4) {
   
            if (target == 0)
                ans.push_back(path);
            return ;
        }
        int n = nums.size();
        if (index + 4 - path.size() > n) return ; // 剪枝
        for (int i = index; i < n; i ++) {
   
            if (i != index && nums[i] == nums[i - 1]) continue;
            path.push_back(nums[i]);
            dfs(nums, target - nums[i], i + 1);
            path.pop_back();
        }
    }

    vector<vector<int>> fourSum(vector<int>& nums, int target) {
   
        sort(nums.begin(), nums.end());
        dfs(nums, target, 0);
        return ans;
    }
};

(双指针)

根据上面的经验,我们可以知道如果暴力四层循环一定也是不可以的,所以我们可以利用双指针的思想将数组先排序,然后两层循环枚举前面两个数字,nums[i], nums[j]。后面两个数字的和就是target - nums[i] - nums[j]了,这个时候数组就有了单调性,就可以使用双指针算法了。

class Solution {
   
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
   
        typedef long long LL;
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<vector<int>> ans;
        if (n < 4) return ans; // 剪枝
        for (int i = 0; i < n - 1; i ++) {
   
            if (i && nums[i] == nums[i - 1]) continue; // 避免重复元素
            for (int j = i + 1; j < n - 2; j ++) {
   
                if (j != i + 1 && nums[j - 1] == nums[j]) continue; // 避免重复元素
                // 双指针
                int l = j + 1, r = n - 1;
                LL sum = nums[i] + (LL)0 + nums[j] + nums[l] + nums[r];
                while (l < r) {
   
                    if (sum < target) l ++;
                    else if (sum > target) r --;
                    else {
   
                        ans.push_back({
   nums[i], nums[j], nums[l], nums[r]});
                        l ++, r --;
                        while (l < r && nums[l] == nums[l - 1]) l ++;
                        while (l < r && nums[r] == nums[r + 1]) r --; 
                    }
                    sum = nums[i] + (LL)0 + nums[j] + nums[l] + nums[r];
                }
            }
        }
        return ans;
    }
};
class Solution {
   
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
   
        typedef long long LL;
        int n = nums.size();
        vector<vector<int>> ans;
        if (n < 4) return ans;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < n; i ++) {
   
            if (i && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < n; j ++) {
   
                if (j != i + 1 && nums[j] == nums[j - 1]) continue;
                for (int l = j + 1, r = n - 1; l < r; l ++) {
   
                    if (l != j + 1 && nums[l] == nums[l - 1]) continue;
                    while (r - 1 > l && 
                     nums[i] + nums[j] >= (LL)target - nums[l] - nums[r - 1]) r --;
                    if (nums[i] + nums[j] == (LL)target - nums[l] - nums[r]) {
   
                        ans.push_back({
   nums[i], nums[j], nums[l], nums[r]});
                    }
                }
            }
        }
        return ans;
    }
};

总结:两数之和,三数之和,四数之和。

三数之和和四数之和都是同一类型的题目,都是因为暴力循环不能解决问题,然后通过数组排序使得数组具有单调性。这样就可以使用双指针算法让查找的效率变成O(N)。

我们发现哈希表应用在两数之和中可以使得查找的效率变成O(1),但是因为两数之和要求出数字在原数组中的下标,所以不能使用排序+双指针来实现。

所以我们知道了通过排序+双指针的算法可以将O(N2)的查找降为O(N)的查找。而哈希表一般只适用于两数之和为target的情况下,否则枚举数字没有办法解决问题。

162. 寻找峰值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwLyPS85-1640011637939)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1636965969290.png)]

(暴力)

class Solution {
   
public:
    int findPeakElement(vector<int>& nums) {
   
        int n = nums.size();
        for (int i = 1; i < n - 1; i ++) {
   
            if (nums[i] > nums[i + 1] && nums[i] > nums[i - 1]) return i;
        }
        return nums.back() > nums[0] ? n - 1 : 0;
    }
};

(二分)

因为我们只需要求出一个峰值的下标即可。所以我们就可以把数组当做只有一个峰值。可以发现其实峰值的特点就是比左右两边的值都要大。但是如果只是抓住这一点的话,我们也就只能顺序查找一个值。我们可以将这个点看成是“下坡”的第一个点,这样峰值的前面都是单调递增,后面都是单调递减,这样划分的话,数组就具有了二段性,我们就可以使用二分来解决这个问题了。

总结:由本题可知二分的本质是「二段性」而不是「单调性」。本题就是可以有多个峰值,但是我们可以找到其中一个峰值。只需要有一个将一段数组划分成为两段的性质即可。

class Solution {
   
public:
    int findPeakElement(vector<int>& nums) {
   
        int n = nums.size();
        int l = 0, r = n - 1;
        while (l < r) {
   
            int mid = l + (r - l) / 2;
            if (mid + 1 < n && nums[mid] > nums[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};

(三分)

三分的基本原理是根据画图推出来的规律,int mid1 = l + (r - l) / 3;int mid2 = r - (r - l) / 3;。如果nums[mid1] > nums[mid2]的话,那么无论mid1在峰值的左边还是峰值的右边,[mid2, r]一定都不存在峰值,因为如果峰值在[mid2, r]中间的话,那么nums[mid2]一定大于nums[mid1],所以根据这种三分的方法我们就可以排除1/3的部分。同理if(nums[mid2] >= nums[mid1]),那么[l, mid1]中一定不存在峰值。

这种三分的方法是专门用来求出峰值的。

class Solution {
   
public:
    int findPeakElement(vector<int>& nums) {
   
        int n = nums.size();
        int l = 0, r = n - 1;
        while (l < r) {
   
            int mid1 = l + (r - l) / 3;
            int mid2 = r - (r - l) / 3;
            if (nums[mid2] > nums[mid1]) l = mid1 + 1;
            else r = mid2 - 1;
        }
        return l;
    }
};

881. 救生艇

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8c5NeJbB-1640011663250)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1637050308805.png)]

(贪心+双指针)

如果是站在做题的角度来说的话,数据范围是500000,所以一般暴力方法一定是过不了的,因此题解不是动规就是贪心。

本题需要使得船的数量最少。我们观察到如果想要求出第i个人或者前i个人所需的最少船的数量的话,没有一个子问题可以描述出来,也就是和前面一个人没有关系,所以本题不能使用贪心。

所以我们贪心地想,如果想要使得船数最少,那么每一次尽量每艘船上都尽可能的多带人。所以我们可以想到先将数组排序,因为people[i] < limit的,所以从前往后的每一个人一定都可以自己单独一条船。然后每一次如果people(最轻) + people(最重) <= limits的话,那么两个人共用一条船一定是最省的,反之就重的的人自己一条船即可。

这样每一次都将最重的人考虑在内,使得每条船在全局和局部都是最优解,这样就是贪心的解法。

class Solution {
   
public:
    int numRescueBoats(vector<int>& people, int limit) {
   
        int n = people.size();
        sort(people.begin(), people.end());
        int l = 0, r = n - 1;
        int ans = 0;
        while (l <= r) {
   
            if (people[r] + people[l] > limit) r --;
            else l ++, r --;
            ans ++;
        }
        return ans;
    }
};

59. 螺旋矩阵 II

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yfiV6lFK-1640011687088)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1637052147238.png)]

(偏移量)

我们只需要根据偏移量的设置使得数字在数组中顺时针旋转即可。

class Solution {
   
public:
    vector<vector<int>> generateMatrix(int n) {
   
       int xd[4] = {
   0, 1, 0, -1}, yd[4] = {
   1, 0, -1, 0};
       vector<vector<int>> matrix(n, vector<int>(n));
       int x = 0, y = 0, d = 0;
       for (int i = 1; i <= n * n; i ++) {
   
           matrix[x][y] = i;
           int a = x + xd[d], b = y + yd[d];
           if (a < 0 || b < 0 || a >= n || b >= n || matrix[a][b]) {
   
               d = (d + 1) % 4;
               a = x + xd[d], b = y + yd[d]; 
           }
           x = a, y = b;
       }
       return matrix;
    }
};

(模拟)

class Solution {
   
public:
    vector<vector<int>> generateMatrix(int n) {
   
        int t = 0, r = n - 1, b = n - 1, l = 0;
        vector<vector<int>> ans(n, vector<int>(n));
        int num = 1;
        while (num <= n * n) {
   
            for (int i = l; i <= r; i ++) ans[t][i] = num ++;
            t ++;
            for (int i = t; i <= b; i ++) ans[i][r] = num ++;
            r --;
            for (int i = r; i >= l; i --) ans[b][i] = num ++;
            b --;
            for (int i = b; i >= t; i --) ans[i][l] = num ++;
            l ++;
        }
        return ans;
    }
};

29. 两数相除

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PfA1yXTl-1640011715576)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1637144474261.png)]

(倍增)

和快速幂是一个思想,都是倍增的思想。倍增的思想就是将一个数字使用二进制表示的方式每一次都是成倍数的增加。

我们可以将divisor拆成二进制的形式。然后将小于dividend的所有divisor的倍数都保存起来,然后从大到小的从dividend扣除divisor的倍数,然后ans加上对应divisor的倍数即可。

class Solution {
   
public:
    typedef long long LL;
    int divide(int x, int y) {
   
        bool is_minus = false;
        if (x < 0 && y > 0 || x > 0 && y < 0) is_minus = true;
        vector<LL> nums;
        LL a = abs((LL)x);
        LL b = abs((LL)y);
        for (LL i = b; i <= a; i += i) nums.push_back(i);
        LL ans = 0;
        for (int i = nums.size() - 1; i >= 0; i --) {
   
            if (a >= nums[i]) {
   
                a -= nums[i];
                ans += 1ll << i;
            }
        }
        if (!is_minus && ans > INT_MAX || ans < INT_MIN) return INT_MAX;
        if (!is_minus) return ans;
        else return -ans;
    }
};

(倍增2-试减法)

也可以不用将divisor的倍数都保存起来,可以一边比较dividend中剩余的数值,一边从dividend中去除divisordividend的最高倍数。也就是将divisor的二进制从最大到最小的计算出来。

class Solution {
   
public:
    typedef long long LL;
    int divide(int dividend, int divisor) {
   
        if (dividend == INT_MIN && divisor == -1) return INT_MAX;
        LL x = dividend, y = divisor;
        bool is_minus = false;
        if ((x < 0 && y > 0) || (x > 0 && y < 0)) is_minus = true;
        if (x < 0) x *= -1;
        if (y < 0) y *= -1;

        LL ans = 0;
        while (x >= y) {
   
            LL cnt = 1;
            LL t = y;
            while (t + t <= x) {
   
                cnt += cnt;
                t += t;
            }
            ans += cnt;
            x -= t;
        }
        if (is_minus) ans *= -1;
        if (ans < INT_MIN || ans > INT_MAX) return INT_MAX;
        return ans;
    }
};

(倍增+二分)

我们也可以发现,如果说x/y = k的话,那么[1, k]使得y*k <= x[k+1, ...]使得y*k > x。所以这一段的数字具有了二段性。因此我们可以找到最后一个可以使得y*k <= xk即可。

因为中间不能使用乘除法,所以我们也可以利用上面同样的倍增的思想,利用位运算计算出a*b的大小即可。

class Solution {
   
public:
    typedef long long LL;
    LL mul(LL a, LL b) {
   
        LL ans = 0;
        while (b) {
   
            if (b & 1) ans += a;
            b >>= 1;
            a += a;
        }
        return ans;
    }

    int divide(int a, int b) {
   
        // 特判a=INT_MIN,b=1的情况
        if (a == INT_MIN && b == 1) return a;
        LL x = a, y = b;
        bool is_minus = false;
        if ((x > 0 && y < 0) || (x < 0 && y > 0)) is_minus = true;
        if (x < 0) x *= -1;
        if (y < 0) y *= -1;
        LL l = 0, r = x;
        while (l < r) {
   
            LL mid = l + (r - l + 1) / 2;
            if (mul(mid, y) <= x) l = mid;
            else r = mid - 1;
        }
        if (l > INT_MAX || l < INT_MIN) return INT_MAX;
        if (is_minus) l *= -1;
        return l;
    }
    
};

(使用int的倍增法)

如果题目中要求只能使用int类型的变量的话,因为INT_MIN的绝对值要比INT_MAX多一,所以我们可以将所以的数字都变成负数,这样的话所以的数字至少都可以储存起来了,只是计算的逻辑刚好反过来了而已。

class Solution {
   
public:
    int divide(int x, int y) {
   
        // 防止x==INT_MIN的情况
        // 1.INT_MIN*-1比INT_MAX还要大
        if (x == INT_MIN && y == -1) return INT_MAX;
        // 2.INT_MIN*1之后的正数比INT_MAX还要大
        if (y == 1) return x; 
        
        bool is_minus = false;
        if ((x < 0 && y > 0) || (x > 0 && y < 0)) is_minus = true;
        if (x > 0) x *= -1;
        if (y > 0) y *= -1;
        int ans = 0;
        while (y >= x) {
   
            int cnt = 1;
            int t = y;
            while (t >= x - t) {
   
                t += t;
                cnt += cnt;
            } 
            ans += cnt;
            x -= t;
        }
        if (is_minus) ans *= -1;
        return ans; 
    }
};

总结:倍增的思想用于一个数的幂或者两个数字的快速相乘或者相除。本质都是将其中一个数字使用而二进制的方式表示出来,来达到快速计算的效果。

207. 课程表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A9jEa7sf-1640011778148)(D:\github\gitee\leet-code-solution\LeetCode精选TOP面试题.assets\1637413984347.png)]

(拓扑排序+bfs)

一个图不存在环等价于这个图的反向图不存在环。

本题是一个考察图论的题目,关键在于判断图中是否存在环(判断拓扑排序)。

拓扑排序四步走:

1.使用邻接表将这个图存储下来

2.找到度为0的定点,加入在队列中

3.bfs,减少点的入度,并将入读为0的定点加入到队列中

4.判断是否存在顶点存在入度,如果存在说明图中有环,因此没有拓扑序直接返回false

class Solution {
   
public:
    bool canFinish(int n, vector<vector<int>>& edges) {
   
        vector<vector<int>> g(n);
        vector<int> count(n);
        for (auto& edge : edges) {
   
            int a = edge[1], b = edge[0];
            g[a].push_back(b);
            count[b] ++;
        }
        
        queue<int> q;
        for (int i = 0; i < n; i ++)
            if (!count[i])
                q.push(i);
        
        int cnt = 0;
        while (!q.empty()) {
   
            int top = q.front();
            q.pop();
            cnt ++;
            int len = g[top].size();
            for (int i = 0; i < len; i ++) {
   
                count[g[top][i]] --;
                if (!count[g[top][i]])
                    q.push(g[top][i]);
            }
        }
        return n == cnt;
    }
};

(拓扑排序+dfs)

前面使用广度优先搜索时从正面的角度去理解一个图是否存在环,因为我们是在考虑一个课程前面是否还存在先修课程,对应到图中,也就是一个图中的某一个顶点是否存在入度,如果不存在入度的话,那么这个课程就可以开始学习了。

但是如果是深度优先遍历的话,我们就题目转化为一个图是否存在环?

我们从一个顶点出发的话,这个顶点可能前面有先修的课程,也有可能这个课程已经是入度为0的点了。这时我们只需要顺着这个顶点一直往前找到:**以这个顶点为出发点的终点是哪一个顶点。**在这之前我们就需要使用一个flags数组标记一下每一个位置的访问情况,flags[i] = 0表示没有访问过该节点,flags[i] = 1表示这个节点在本轮dfs中访问过,flags[i] = -1表示这个点已经被访问过了。如果一个图中存在环的话,那么一定会有重复访问flags[i] = 1的位置,这样就直接return false即可。

class Solution {
   
public:
    bool dfs(i
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值