leetcode刷题(一)

41. 缺失的第一个正数

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

题解

要求空间复杂度O(1)。前不久刚做了一道循环排列的题,所以看见“就地”两个字,秒懂: 将数字放到对应的位置上

有个特殊的样例,nums[i] - 1可能会超出int的范围

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; ++i) if (nums[i] >= 0) nums[i]--;

        for (int i = 0; i < n; ++i) {
            int pos = nums[i];
            while(pos >= 0 && pos < n && pos != nums[pos]) swap(pos, nums[pos]);
        }

        int ans = n + 1;
        for (int i = 0; i < n; ++i) if (nums[i] != i) {
            ans = i + 1;
            break;
        }
        return ans;
    }
};

287. 寻找重复数

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数

题解一

要求空间复杂度O(1)、时间复杂度小于O(\(n^2\))、不能更改原数组。二分枚举答案,遍历整个数组,如果小于或者等于枚举的答案的个数大于枚举的答案,说明左半段出现了重复,否则,说明右半段出现了重复。

妙啊!原味:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/er-fen-fa-si-lu-ji-dai-ma-python-by-liweiwei1419/

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size();
        int l = 1, r = n;
		
        // 时间换空间,有点儿意思
        while(l < r) {
            int mid = (l + r) >> 1;      // 枚举答案

            int cnt = 0;
            for (int num: nums) if (num <= mid) cnt++;

            if (cnt > mid) r = mid;
            else  l = mid + 1;
        }

        return l;
    }
};

题解二

oooooorz,还可以用二进制考虑。思想就一句话:如果一个数是重复的,那么对应的二进制位中1的个数肯定比原来多,因为重复了嘛。

详细讲解:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by--52/

// beautiful code
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size();
        int ans = 0;
        for (int i = 0; i < 32; ++i) {
            int a = 0, b = 0;
            int mask = 1 << i;
            for (int j = 0; j < n; ++j) {
                if (nums[j] & mask) a++;
                if (j & mask) b++;
            }
            if (a > b) ans += mask;  // ans = ans | mask;  is ok!
        }
        return ans;
    }
};
// sad code
class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int n = nums.size();

        int bit_cnt[40];
        memset(bit_cnt, 0, sizeof(bit_cnt));

        for (int num: nums) {
            int t = 0;
            while(num) bit_cnt[t++] += (num & 1), num >>= 1;
        }

        int bit_tot[40];
        memset(bit_tot, 0, sizeof(bit_tot));

        for (int i = 1; i < n; ++i) {
            int t = 0, num = i;
            while(num) bit_tot[t++] += (num & 1), num >>= 1;
        }

        int ans = 0;
        for (int i = 0; i < 32; ++i) if (bit_tot[i] < bit_cnt[i]) ans += (1 << i);
        return ans;
    }
};

题解三

\(n + 1\)\(1\)\(n\)之间的数(包括\(1\)\(n\))构成的数组,以下标(从0开始)和值建边构成的有向图必定含有环,因为有重复的数。其实任意一个排列(下标从1开始),按照这种方式构成的图都是由一个个环组成的。在这道题中环的入口就是重复的数,怎么求环的入口?快慢指针法,常用来判断单链表中是否有环,详细说明请看代码

神人之笔

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int slow = nums[0];                  // 慢指针
        int fast = nums[nums[0]];            // 快指针

        while(slow != fast) {                // 相等,代表在环中相遇
            slow = nums[slow];
            fast = nums[nums[fast]];         
        }

求环的入口?
令环的长度为\(m\),从0走到环的入口需要\(S\)
假设慢指针走了\(n\)步与快指针在环内相遇,那么快指针走了\(2*n\)步,且\(2 * n - n = n\)\(n \% m = 0\)。在第一次相遇的时候,慢指针在环内走了\(n - S\)步。令fast = 0,然后走\(S\)步走到环的入口,慢指针同时移动也走了\(S\)步,算上之前,慢指针在环内共走了\(n - S + S = n\)步,恰好走到环的入口(\(n \% m = 0\)),于是快慢指针在入口相遇。

        fast = 0;
        while(slow != fast) {             // 第二次相遇必定在入口处
            fast = nums[fast];
            slow = nums[slow];
        }
        return fast;
    }
};

765. 情侣牵手

N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。

人和座位用 0 到 2N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)。

这些情侣的初始座位 row[i] 是由最初始坐在第 i 个座位上的人决定的。

题解一

很容易想到先把情侣的一人固定在当前位置上,然后交换另一个人来凑成情侣。就是不知道这样为啥能得到最优解,想不通,orz。复杂度\(O(n^2)\)

class Solution {
public:
    int minSwapsCouples(vector<int>& row) {
        int n = row.size();
        int ans = 0;
        
        for (int i = 0; i < n; i += 2) {
            if (judeg(row[i],  row[i + 1])) continue;
            for (int j = i + 2; j < n; j += 2) {
                if (judeg(row[j], row[j + 1])) continue;
                if (judeg(row[i], row[j])) {
                    swap(row[i + 1], row[j]);
                    ans++;
                    break;
                }
                else if (judeg(row[i], row[j + 1])) {
                    swap(row[i + 1], row[j + 1]);
                    ans++;
                    break;
                }
            }
        }

        return ans;
    }
    bool judeg(int a, int b) {
        if (a > b) swap(a, b);
        if (b - a > 1) return false;
        if (!(a & 1) && (b & 1)) return true;
        return false;
    }
};

153. 寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。

题解

时间复杂度要求\(O(log_2(n))\),二分,然后找判断是在左区域还是右区域的条件

写惯了开区间的二分,上来就RE

class Solution {
public:
    int findMin(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        while(l < r) {
            int mid = (l + r) >> 1;
            if (nums[mid] <= nums[r]) r = mid;
            else l = mid + 1;
        }
        return nums[l];
        /*
        整理一下就是上面的代码
        for(int i = 0; i < 40; ++i) {
            int mid = (l + r) >> 1;
            if (nums[l] <= nums[mid]) {
                if(nums[r] > nums[mid]) r = mid;      // nums[r],r一定要小于n
                else l = mid;
            }
            else r = mid;   // 不要写成 l = mid,因为可能跳过了最小值
        }
        return min(nums[l], nums[r]);
        */
    }
};

154. 寻找旋转排序数组中的最小值 II【好题】

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,1,4,5,6,7] 可能变为 [4,5,6,7,0,1,1] )。
请找出其中最小的元素。
假设存在重复的数字

题解

时间复杂度要求在\([log_2(n), n]\)之间。显然整个数组被分成了两个上升段。二分,只是需要判断下面这三种情况,边界应该怎么移动:

  • nums[mid] < nums[r],说明midr处在同一个上升段内,且不会存在比nums[mid]更小的数,所以r = mid
  • nums[mid] == nums[r],此时无法判断答案在哪边,举例:[3,1,3,3,3][3,3,3,1,3],都满足等式,但最小值可能出现在mid之前也可能之后,所以只能令r = r - 1,逐步缩小查找范围。为什么不能l++,由样例1可知,这样更新边界可能忽略最小值。
  • nums[mid] > nums[r],说明midl处在同一个上升段内,而答案在另一个上升段内,所以l = mid + 1

看了一下讨论,发现可做以下几个扩展:①找出旋转点的位置(但存在无法找到旋转点位置的情况,如[1,1,1,1])。②如果是多次旋转,能不能设计较优的算法找到最小值

突然觉得leetcode上面的题很有趣啊,整道题的灵魂就是r--,以及考察特殊情形。妙,甚妙!总结:脑子不够灵活

while(l < r) {
    int mid = (l + r) >> 1;
    if (nums[mid] < nums[r]) r = mid;
    else if (nums[mid] == nums[r]) r--;
    else l = mid + 1;
}
return nums[l];

33. 搜索旋转排序数组

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是\(O(log_2(n))\)级别

题解

二分,关键在于判断lmidr的分布,分类讨论:

  • nums[mid] < nums[r]说明midr在同一个上升段,l是否与它们在同一个上升段需要讨论
    • target < nums[mid],说明targetmid的左边
    • target > nums[mid]target可能在mid的左边,既在第一个上升段内,也可能在mid的右边,需要讨论
  • nums[mid] > nums[r]说明midl在同在第一个上升段
    • target < nums[mid]target可能在mid的左边,既在第一个上升段内,也可能在mid的右边,即在第二个上升段内,需要讨论
    • target > nums[mid],说明targetmid的右边

这分类讨论,酸爽!其实正解是先用上一题的方法,先找到旋转点,既最小值的位置\(pos\),然后分别在两个上升段内\(([0, pos - 1], [pos + 1, r])\)用二分查找。

class Solution {
public:
    int search(vector<int>& nums, int target) 
    {
        int l = 0, r = nums.size() - 1;
        if (r < 0) return -1;                // 坑,可能输入空数组
        while(l < r){
            int mid = (l + r) >> 1;
            if (nums[mid] == target) return mid;

            if (nums[mid] < nums[r]) {
                if (target < nums[mid]) r = mid;
                else {
                    /* think! why this code is wrong ?

                    if (nums[l] > target) l = mid + 1;
                    else r = mid;

                    */
                    // target > nums[mid],讨论需仔细
                    if (nums[l] > nums[r]) {
                        if (nums[l] > target) l = mid + 1;
                        else r = mid;
                    }
                    else l = mid + 1;
                }
            }
            else {
                if (target < nums[mid]) {
                    if (nums[l] > target) l = mid + 1;
                    else r = mid;
                }
                else l = mid + 1;
            }
        }
        return nums[l] == target ? l : -1;
    }
};

81. 搜索旋转排序数组 II

假设按照升序排序的数组(可能有重复数字)在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。

题解

二分,依然是分类讨论,当有重复数字时,会导致边界无法确定更新,老办法,令r = r - 1

灵魂:r--,和上一道题相比,这题只能分类讨论,因为它的旋转位置可能无法确定,比如[1,1,1,1]。越想越妙啊,如果无法确定mid处在哪个上升段,就缩小r

while(l < r){
    int mid = (l + r) >> 1;
    if (nums[mid] == target) return true;

    if (nums[mid] < nums[r]) {
        if (target < nums[mid]) r = mid;
        else {
            if (nums[l] >= nums[r]) {      // 与上题相比,多了一个等号,因为相等也意味着l在第一个上升段
                if (nums[l] > target) l = mid + 1;
                else r = mid;
            }
            else l = mid + 1;
        }
    }
    else if (nums[mid] == nums[r]) {      // mid可能在第一个上升段,也可能在第二个上升段,所以直接令 r = r - 1
        r--;
    }
    else {
        if (target < nums[mid]) {
            if (nums[l] > target) l = mid + 1;
            else r = mid;
        }
        else l = mid + 1;
    }
}
posted @ 2020-04-22 21:08  天之道,利而不害  阅读(343)  评论(0编辑  收藏  举报